Skip to content
Commits on Source (22)
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.mail import send_mail
from django.core.management import BaseCommand
from django.db.models import Sum, F
......@@ -11,29 +11,50 @@ from note.templatetags.pretty_money import pretty_money
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--sum-all', '-s', action='store_true', help='Check if the global sum is equal to zero')
parser.add_argument('--check-all', '-a', action='store_true', help='Check all notes')
parser.add_argument('--check', '-c', type=int, nargs='+', help='Select note ids')
parser.add_argument('--fix', '-f', action='store_true', help='Fix note balances')
parser.add_argument('--mail', '-m', action='store_true', help='Send mail to admins if there is an error')
def handle(self, *args, **options):
error = False
err_log = ""
if options["sum_all"]:
s = Note.objects.aggregate(Sum("balance"))["balance__sum"]
if s:
self.stderr.write(self.style.NOTICE("LA SOMME DES NOTES NE VAUT PAS ZÉRO : " + pretty_money(s)))
exit(-1)
err_log += self.style.NOTICE("LA SOMME DES NOTES NE VAUT PAS ZÉRO : " + pretty_money(s)) + "\n"
error = True
else:
self.stdout.write(self.style.SUCCESS("La somme des notes vaut bien zéro."))
exit(0)
error = False
for note in Note.objects.all():
notes = Note.objects.none()
if options["check_all"]:
notes = Note.objects.all()
elif options["check"]:
notes = Note.objects.filter(pk__in=options["check"])
for note in notes:
balance = note.balance
incoming = Transaction.objects.filter(valid=True, destination=note)\
.annotate(total=F("quantity") * F("amount")).aggregate(Sum("total"))["total__sum"] or 0
outcoming = Transaction.objects.filter(valid=True, source=note)\
.annotate(total=F("quantity") * F("amount")).aggregate(Sum("total"))["total__sum"] or 0
expected_balance = incoming - outcoming
if expected_balance != balance:
self.stderr.write(self.style.NOTICE("LA SOMME DES TRANSACTIONS DE LA NOTE {} NE CORRESPOND PAS "
"AVEC LE MONTANT RÉEL".format(str(note))))
self.stderr.write(self.style.NOTICE("Attendu : {}, calculé : {}"
.format(pretty_money(balance), pretty_money(expected_balance))))
calculated_balance = incoming - outcoming
if calculated_balance != balance:
err_log += self.style.NOTICE("LA SOMME DES TRANSACTIONS DE LA NOTE {} NE CORRESPOND PAS "
"AVEC LE MONTANT RÉEL".format(str(note))) + "\n"
err_log += self.style.NOTICE("Attendu : {}, calculé : {}"
.format(pretty_money(balance), pretty_money(calculated_balance))) + "\n"
if options["fix"]:
note.balance = calculated_balance
note.save()
error = True
if error:
self.stderr.write(err_log)
if options["mail"]:
send_mail("[Note Kfet] La base de données n'est pas consistante", err_log,
"NoteKfet2020 <notekfet2020@crans.org>", ["respo-info.bde@lists.crans.org"])
exit(1 if error else 0)
......@@ -6,6 +6,7 @@ import psycopg2.extras as pge
import datetime
import json
from django.template.loader import render_to_string
from django.utils.timezone import make_aware, now
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
......@@ -33,6 +34,11 @@ MAP_IDBDE = {
# some Aliases have been created in the fixtures
ALIAS_SET = {a[0] for a in Alias.objects.all().values_list("normalized_name")}
# Some people might loose some aliases due to normalization. We warn them on them.
LOST_ALIASES = {}
# In some rare cases, the username might be in conflict with some others. We change them and warn the users.
CHANGED_USERNAMES = []
note_user_type = ContentType.objects.get(app_label="note", model="noteuser")
note_club_type = ContentType.objects.get(app_label="note", model="noteclub")
......@@ -85,9 +91,10 @@ class Command(ImportCommand):
pseudo = row["pseudo"]
pseudo_norm = Alias.normalize(pseudo)
self.update_line(idx, n, pseudo)
# clean pseudo (normalized pseudo must be unique)
if pseudo_norm in ALIAS_SET:
# clean pseudo (normalized pseudo must be unique and not empty)
if not pseudo_norm or pseudo_norm in ALIAS_SET:
pseudo = pseudo + str(row["idbde"])
CHANGED_USERNAMES.append((pk_note, row[pseudo], pseudo))
else:
ALIAS_SET.add(pseudo_norm)
# clean date
......@@ -95,7 +102,7 @@ class Command(ImportCommand):
"pk": pk_note,
"balance": row['solde'],
"last_negative": None,
"is_active": True,
"is_active": not row["bloque"],
"display_image": "pic/default.png",
}
if row["last_negatif"] is not None:
......@@ -119,7 +126,7 @@ class Command(ImportCommand):
"first_name": row["prenom"],
"last_name": row["nom"],
"email": row["mail"],
"is_active": True, # temporary
"is_active": not row["bloque"],
"date_joined": make_aware(MAP_IDBDE_PROMOTION[row["idbde"]]["created_at"]),
}
profile_dict = {
......@@ -131,6 +138,8 @@ class Command(ImportCommand):
"registration_valid": True,
"email_confirmed": True,
"promotion": MAP_IDBDE_PROMOTION[row["idbde"]]["promo"],
"report_frequency": row["report_period"],
"last_report": make_aware(row["previous_report_date"]),
}
note_dict["created_at"] = make_aware(MAP_IDBDE_PROMOTION[row["idbde"]]["created_at"])
note_dict["polymorphic_ctype"] = note_user_type
......@@ -204,7 +213,9 @@ class Command(ImportCommand):
alias_norm = Alias.normalize(alias_name)
self.update_line(idx, n, alias_norm)
# clean pseudo (normalized pseudo must be unique)
if alias_norm in ALIAS_SET:
if not alias_norm or alias_norm in ALIAS_SET:
LOST_ALIASES.setdefault(MAP_IDBDE[row["idbde"]], [])
LOST_ALIASES[MAP_IDBDE[row["idbde"]]].append(alias_name)
continue
else:
ALIAS_SET.add(alias_norm)
......@@ -235,3 +246,20 @@ class Command(ImportCommand):
filename = kwargs["save"]
with open(filename, 'w') as fp:
json.dump(MAP_IDBDE, fp, sort_keys=True, indent=2)
for pk_user, old_username, new_username in CHANGED_USERNAMES:
user = User.objects.get(pk_user)
mail_text = render_to_string("scripts/unsupported_username.txt", dict(
user=user,
old_username=old_username,
new_username=new_username,
))
user.email_user("Transition à la Note Kfet 2020 : pseudo non supporté", mail_text)
for pk_user, aliases_list in CHANGED_USERNAMES:
user = User.objects.get(pk_user)
mail_text = render_to_string("scripts/deleted_aliases.txt", dict(
user=user,
aliases_list=aliases_list,
))
user.email_user("Transition à la Note Kfet 2020 : suppression d'alias", mail_text)
......@@ -40,7 +40,8 @@ class Command(ImportCommand):
cur.execute("SELECT * FROM activites ORDER by id")
n = cur.rowcount
bulk_mgr = BulkCreateManager(chunk_size=chunk)
activity_type_id = ActivityType.objects.get(name="Pot").pk # Need to be fixed manually
pot_id = ActivityType.objects.get(name="Pot").pk
club_id = ActivityType.objects.get(name="Soirée de club").pk
kfet = Club.objects.get(name="Kfet")
pk_activity = 1
for idx, row in enumerate(cur):
......@@ -63,7 +64,8 @@ class Command(ImportCommand):
"pk": pk_activity,
"name": row["titre"],
"description": row["description"],
"activity_type_id": activity_type_id, # By default Pot
"location": row["lieu"],
"activity_type_id": pot_id if row["liste"] else club_id,
"creater_id": NoteUser.objects.get(pk=note).user.id,
"organizer_id": organizer.pk,
"attendees_club_id": kfet.pk, # Maybe fix manually
......
......@@ -8,6 +8,7 @@ import pytz
import datetime
import copy
from django.contrib.auth.models import User
from django.utils.timezone import make_aware
from django.db import transaction
from django.contrib.contenttypes.models import ContentType
......@@ -23,7 +24,7 @@ from note.models import Note, NoteClub
from activity.models import Guest, GuestTransaction, Entry
from member.models import Membership
from treasury.models import Remittance, SpecialTransactionProxy
from treasury.models import Remittance, SpecialTransactionProxy, SogeCredit
from ._import_utils import ImportCommand, BulkCreateManager, timed
MAP_TRANSACTION = dict()
......@@ -53,8 +54,8 @@ CT = {
def get_date_end(date_start):
date_end = copy.deepcopy(date_start)
if date_start.month > 8:
date_end = date_start.replace(year=date_start.year+1)
if date_start.month >= 8:
date_end = date_start.replace(year=date_start.year + 1)
date_end = date_end.replace(month=9, day=30)
return date_end
......@@ -106,7 +107,6 @@ class Command(ImportCommand):
def _template_transaction(self, row, obj_dict, child_dict):
if self.buttons.get(row["description"]):
child_dict["category_id"] = self.buttons[row["description"]][1]
child_dict["template_id"] = self.buttons[row["description"]][0]
# elif self.categories.get(row["categorie"]):
# child_dict["category_id"] = self.categories[row["categorie"]]
......@@ -236,6 +236,7 @@ class Command(ImportCommand):
(obj_dict0,
child_dict0,
child_transaction) = self._membership_transaction(row, obj_dict, child_dict, pk_membership)
obj_dict0["destination_id"] = 6 # Kfet note id
bde_dict = {
"pk": pk_membership,
"user_id": user_id,
......@@ -366,6 +367,30 @@ class Command(ImportCommand):
except:
print("Failed to save row: " + str(row))
@timed
def import_soge_credits(self):
users = User.objects.filter(profile__registration_valid=True).order_by('pk')
n = users.count()
for idx, user in enumerate(users.all()):
self.update_line(idx, n, user.username)
soge_credit_transaction = SpecialTransaction.objects.filter(
reason__icontains="crédit sogé",
destination_id=user.note.id,
)
if soge_credit_transaction.exists():
soge_credit_transaction = soge_credit_transaction.get()
soge_credit = SogeCredit.objects.create(user=user, credit_transaction=soge_credit_transaction)
memberships = Membership.objects.filter(
user=user,
club_id__in=[BDE_PK, KFET_PK],
date_start__lte=soge_credit_transaction.created_at,
date_end__gte=soge_credit_transaction.created_at + datetime.timedelta(days=61),
).all()
for membership in memberships:
soge_credit.transactions.add(membership.transaction)
soge_credit.save()
@timed
def handle(self, *args, **kwargs):
# default args, provided by ImportCommand.
......@@ -382,3 +407,4 @@ class Command(ImportCommand):
self.set_roles()
self.import_remittances(cur, kwargs["chunk"])
self.import_checks(cur)
self.import_soge_credits()
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import os
from bs4 import BeautifulSoup
from django.core.management import BaseCommand
from django.urls import reverse
from django.utils import timezone
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from activity.models import Activity
class Command(BaseCommand):
acl_header = "#acl NoteKfet2015:read,write,admin NoteKfet2020:read,write,admin All:read Default\n"
warning_header = """## NE PAS ÉDITER CETTE PAGE MANUELLEMENT
## ELLE EST GÉNÉRÉE AUTOMATIQUEMENT PAR LA NOTE KFET 2020
## Adapté par [[WikiYnerant|ÿnérant]] du script de by 20-100, largement inspiré de la version de Barbichu.
"""
intro_generic = """ * Elle est générée automatiquement par la [[NoteKfet/NoteKfet2020|Note Kfet 2020]]
* Ne pas éditer cette page manuellement, toute modification sera annulée automatiquement.
* Pour annoncer un nouvel événement, rendez-vous sur {activities_url}
""".format(activities_url="https://" + os.getenv("NOTE_URL") + reverse("activity:activity_list"))
@staticmethod
def connection(url):
"""Se logue sur le wiki et renvoie le cookie de session"""
parameters = {
'action': 'login',
'login': 'Connexion',
'name': os.getenv("WIKI_USER", "NoteKfet2020"),
'password': os.getenv("WIKI_PASSWORD"),
}
# Il faut encoder ça proprement
data = urlencode(parameters).encode("utf-8")
request = Request(url, data)
# La requête est envoyée en HTTP POST
response = urlopen(request)
# a priori la page elle-même je m'en carre…
response.read(2)
# …ce qui m'intéresse, c'est le cookie qu'elle me file
cookie = response.headers['set-cookie']
return cookie
@staticmethod
def get_edition_ticket(url, cookie):
"""Récupère le ticket d'édition de la page"""
# On crée la requête d'édition…
suffix = "?action=edit&editor=text"
request = Request(url + suffix)
# …avec le cookie
request.add_header("Cookie", cookie)
# On l'envoie
pagecontent = urlopen(request)
html = pagecontent.read()
soup = BeautifulSoup(html, features="lxml")
# On va chercher le formulaire
form = soup.find(name="form", attrs={"id": "editor"})
# On récupère le ticket dedans
ticket = soup.find(name="input", attrs={"name": "ticket"})
return ticket["value"]
@staticmethod
def edit_wiki(page, content, comment=''):
"""Modifie une page du wiki"""
url = "https://wiki.crans.org/" + page
# On se connecte et on récupère le cookie de session
cookie = Command.connection(url)
# On demande l'édition et on récupère le ticket d'édition de la page
ticket = Command.get_edition_ticket(url, cookie)
# On construit la requête
data = {
'button_save': 'Enregistrer les modifications',
'category': '',
'comment': comment.encode("utf-8"),
'savetext': content.encode("utf-8"),
'action': 'edit',
'ticket': ticket
}
request = Request(url, urlencode(data).encode("utf-8"))
request.add_header("Cookie", cookie)
# On la poste
urlopen(request)
@staticmethod
def format_activity(act, raw=True):
"""Wiki-formate une activité, pour le calendrier raw si ``raw``, pour le human-readable sinon."""
if raw:
return """== {title} ==
start:: {start}
end:: {end}
description:: {description} -- {club}
location:: {location}
""".format(
title=act.name,
start=timezone.localtime(act.date_start).strftime("%Y-%m-%d %H:%M"),
end=timezone.localtime(act.date_end).strftime("%Y-%m-%d %H:%M"),
description=act.description,
club=act.organizer.name,
location=act.location,
)
else:
return "|| {start} || {title} || {description} || {club} || {location} ||".format(
title=act.name,
start=timezone.localtime(act.date_start).strftime("%d/%m/%Y"),
description=act.description,
club=act.organizer.name,
location=act.location,
)
@staticmethod
def get_raw_page():
page = "VieBde/PlanningSoirees/LeCalendrier"
header = Command.acl_header + Command.warning_header
header += """= Introduction =
* Cette page a pour but de recenser les activités BDE afin d'être signalées sur le calendrier de la
[[PageAccueil|page d'accueil]] du wiki.
"""
header += Command.intro_generic
body = "\n".join(Command.format_activity(activity) for activity in Activity.objects.filter(valid=True)
.order_by('-date_start').all())
footer = "\n----\nCatégorieCalendrierCampus"
return page, header + body + footer
@staticmethod
def get_human_readable_page():
page = "VieBde/PlanningSoirees"
header = Command.acl_header + Command.warning_header
header += """= Planning de soirées =
== Introduction ==
* Cette page est destinée à accueillir le planning des soirées BDE.
"""
header += Command.intro_generic + "\n"
body = """== Planning des activités à venir ==
||'''Date'''||'''Titre'''||'''Description'''||'''Par''' ||'''Lieu'''||
"""
body += "\n".join(Command.format_activity(activity, False) for activity in Activity.objects
.filter(valid=True, date_end__gte=timezone.now()).order_by('-date_start').all())
body += """\n\n== Planning des activités passées ==
||'''Date'''||'''Titre'''||'''Description'''||'''Par'''||'''Lieu'''||
"""
body += "\n".join(Command.format_activity(activity, False) for activity in Activity.objects
.filter(valid=True, date_end__lt=timezone.now()).order_by('-date_start').all())
return page, header + body
@staticmethod
def refresh_raw_wiki_page(comment="refresh", debug=True):
page, content = Command.get_raw_page()
if debug:
print(content)
else:
Command.edit_wiki(page, content, comment)
@staticmethod
def refresh_human_readable_wiki_page(comment="refresh", debug=True):
page, content = Command.get_human_readable_page()
if debug:
print(content)
else:
Command.edit_wiki(page, content, comment)
def add_arguments(self, parser):
parser.add_argument("--human", "-H", action="store_true", help="Save human readable page")
parser.add_argument("--raw", "-r", action="store_true", help="Save raw page, for the calendar")
parser.add_argument("--comment", "-c", action="store", type=str, default="", help="Comment of the modification")
parser.add_argument("--debug", "-d", action="store_true", help="Don't commit to the wiki, render in stdout")
def handle(self, *args, **options):
if options["raw"]:
Command.refresh_raw_wiki_page(options["comment"], options["debug"])
if options["human"]:
Command.refresh_human_readable_wiki_page(options["comment"], options["debug"])
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
from django.core.mail import send_mail
from django.core.management import BaseCommand
from django.db.models import Q
from django.template.loader import render_to_string
from django.utils.translation import activate
from note.models import NoteUser, Note
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument("--spam", "-s", action='store_true', help="Spam negative users")
parser.add_argument("--report", "-r", action='store_true', help="Report the list of negative users to admins")
parser.add_argument("--negative-amount", "-n", action='store', type=int, default=1000,
help="Maximum amount to be considered as very negative")
def handle(self, *args, **options):
activate('fr')
notes = Note.objects.filter(
Q(noteuser__user__memberships__date_end__gte=date.today()) | Q(noteclub__isnull=False),
balance__lte=-options["negative_amount"],
is_active=True,
).order_by('balance').distinct().all()
if options["spam"]:
for note in notes:
note.send_mail_negative_balance()
if options["report"]:
plain_text = render_to_string("note/mails/negative_notes_report.txt", context=dict(notes=notes))
html = render_to_string("note/mails/negative_notes_report.html", context=dict(notes=notes))
send_mail("[Note Kfet] Liste des négatifs", plain_text, "Note Kfet 2020 <notekfet2020@crans.org>",
recipient_list=["respo-info.bde@lists.crans.org", "tresorerie.bde@lists.crans.org"],
html_message=html)
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
from django.core.management import BaseCommand
from django.db.models import Q
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import activate
from note.models import NoteUser, Transaction
from note.tables import HistoryTable
class Command(BaseCommand):
def handle(self, *args, **options):
activate('fr')
notes = NoteUser.objects.filter(
user__memberships__date_end__gte=timezone.now(),
user__profile__report_frequency__gt=0,
).distinct().all()
for note in notes:
now = timezone.now()
last_report = note.user.profile.last_report
delta = now.date() - last_report.date()
if delta.days < note.user.profile.report_frequency:
continue
note.user.profile.last_report = now
note.user.profile.save()
last_transactions = Transaction.objects.filter(
Q(source=note) | Q(destination=note),
created_at__gte=last_report,
).order_by("created_at").all()
if not last_transactions.exists():
continue
table = HistoryTable(last_transactions)
incoming = sum(tr.total for tr in last_transactions if tr.destination.pk == note.pk if tr.valid)
outcoming = sum(tr.total for tr in last_transactions if tr.source.pk == note.pk if tr.valid)
context = dict(
user=note.user,
table=table,
incoming=incoming,
outcoming=outcoming,
diff=incoming - outcoming,
now=now,
last_report=last_report,
)
html = render_to_string("note/mails/weekly_report.html", context)
note.user.email_user("[Note Kfet] Rapport de la Note Kfet", html, html_message=html)
#!/bin/bash
# Create backups directory
[[ -d /var/www/note_kfet/backups ]] || (mkdir /var/www/note_kfet/backups && chown www-data:www-data /var/www/note_kfet/backups)
date=$(date +%Y-%m-%d)
# Backup database and save it as tar archive
su postgres -c "pg_dump -F t note_db" | tee "/var/www/note_kfet/backups/$date.tar" > /dev/null
# Compress backup as gzip
gzip "/var/www/note_kfet/backups/$date.tar"
chown www-data:www-data "/var/www/note_kfet/backups/$date.tar.gz"
# Delete backups that have more than 30 days
find /var/www/note_kfet/backups -type f -mtime +30 -exec rm {} \;
\ No newline at end of file
Bonjour {{ user.first_name }} {{ user.last_name }},
Ce message vous est envoyé automatiquement par la Note Kfet du BDE de
l'ENS Cachan, à laquelle vous êtes inscrit·e. Si vous n'êtes plus
adhérent·e, vous n'êtes pas nécessairement concerné·e par la suite
de ce message.
La Note Kfet 2020 vient d'être déployée, succédant à la Note Kfet 2015.
Les données ont été migrées.
Toutefois, la nouvelle note utilise un algorithme de normalisation des alias
permettant de rechercher plus facilement un nom de note, et empêchant la
création d'un alias trop proche d'un autre.
Nous vous informons que les alias suivants ont été supprimés de votre compte,
jugés trop proches d'autres alias déjà existants :
{{ aliases_list|join:", " }}
Nous nous excusons pour le désagrément, et espérons que vous pourrez
profiter de la nouvelle Note Kfet.
Cordialement,
--
Le BDE
Bonjour {{ user.first_name }} {{ user.last_name }},
Ce message vous est envoyé automatiquement par la Note Kfet du BDE de
l'ENS Cachan, à laquelle vous êtes inscrit·e. Si vous n'êtes plus
adhérent·e, vous n'êtes pas nécessairement concerné·e par la suite
de ce message.
La Note Kfet 2020 vient d'être déployée, succédant à la Note Kfet 2015.
Les données ont été migrées.
Toutefois, la nouvelle note utilise un algorithme de normalisation des alias
permettant de rechercher plus facilement un nom de note, et empêchant la
création d'un alias trop proche d'un autre.
Nous vous informons que votre pseudo {{ old_username }} fait pas partie des
alias problématiques. Il a été remplacé par le pseudo {{ new_username }},
que vous devrez utiliser pour pouvoir vous connecter. Il sera ensuite
possible de modifier votre pseudo.
Nous nous excusons pour le désagrément, et espérons que vous pourrez
profiter de la nouvelle Note Kfet.
Cordialement,
--
Le BDE