From b0154d43011825731b0e4ff7c4f44b7f5770b3c2 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 26 Mar 2017 14:23:44 +0200 Subject: Modification de la regex de validation de groupe pour gérer globalement les licences entières Modification de la méthode de correspondance des groupes --- models.py | 4 ++-- utils.py | 37 ++++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/models.py b/models.py index 478b48f..32a5903 100644 --- a/models.py +++ b/models.py @@ -66,14 +66,14 @@ class Group(models.Model): timetable = models.ForeignKey(Timetable, on_delete=models.CASCADE, verbose_name="emploi du temps") mention = models.CharField(max_length=32) - subgroup = models.CharField(max_length=1, verbose_name="sous-groupe") + subgroup = models.CharField(max_length=1, verbose_name="sous-groupe", null=True) td = models.IntegerField(verbose_name="groupe de TD", null=True) tp = models.IntegerField(verbose_name="groupe de TP", null=True) slug = models.SlugField(max_length=64, default="") def corresponds_to(self, timetable_id, mention, subgroup, td, tp): - return self.timetable.id == timetable_id and self.mention == mention and self.subgroup == subgroup and (self.td == td or self.td is None or td is None) and (self.tp == tp or self.tp is None or tp is None) + return self.timetable.id == timetable_id and self.mention.startswith(mention) and (self.subgroup == subgroup or self.subgroup is None) and (self.td == td or self.td is None or td is None) and (self.tp == tp or self.tp is None or tp is None) @property def group_info(self): diff --git a/utils.py b/utils.py index cff4c9c..8e87c0f 100644 --- a/utils.py +++ b/utils.py @@ -49,25 +49,28 @@ def group_courses(courses): def parse_group(name): # Explication de la regex # - # ^(.+?)\s*\-\s*(((CM)(\w))|((TD)(\w)(\d))|((TP)(\w)(\d)(\d)))$ - # ^ début de la ligne - # (.+?) correspond à au moins un caractère - # \s* zéro, un ou plusieurs espaces - # \- un tiret - # \s* zéro, un ou plusieurs espaces - # (((CM)(\w))| correspond à CM suivi d'une lettre ou... - # ((TD)(\w)(\d))| ... à TD suivi d'une lettre et d'un chiffre ou... - # ((TP)(\w)(\d)(\d))) ... à TP suivi d'une lettre et de deux chiffres - # $ fin de la ligne - group_regex = re.compile("^(.+?)\s*\-\s*(((CM)(\w))|((TD)(\w)(\d))|((TP)(\w)(\d)(\d)))$") + # ^([\w ]+?)(\s*\-\s*(((CM)(\w))|((TD)(\w)(\d))|((TP)(\w)(\d)(\d))))?$ + # ^ début de la ligne + # ([\w ]+?) correspond à au moins un caractère + # (\s* zéro, un ou plusieurs espaces + # \- un tiret + # \s* zéro, un ou plusieurs espaces + # (((CM)(\w))| correspond à CM suivi d'une lettre ou... + # ((TD)(\w)(\d))| ... à TD suivi d'une lettre et d'un chiffre ou... + # ((TP)(\w)(\d)(\d))) ... à TP suivi d'une lettre et de deux chiffres + # )? groupe optionel + # $ fin de la ligne + group_regex = re.compile("^([\w ]+?)(\s*\-\s*(((CM)(\w))|((TD)(\w)(\d))|((TP)(\w)(\d)(\d))))?$") search = group_regex.search(name) if search is None: return None, None, None, None parts = search.groups(0) - if parts[3] == "CM": - return parts[0], parts[4], None, None - elif parts[6] == "TD": - return parts[0], parts[7], parts[8], None - elif parts[10] == "TP": - return parts[0], parts[11], parts[12], parts[13] + if parts[1] == 0: + return parts[0], None, None, None + elif parts[4] == "CM": + return parts[0], parts[5], None, None + elif parts[7] == "TD": + return parts[0], parts[8], parts[9], None + elif parts[11] == "TP": + return parts[0], parts[12], parts[13], parts[14] -- cgit v1.2.1 From 9f86eeed1eadb0e6c6fdc97fba1b7b0c55d3a284 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 26 Mar 2017 15:54:55 +0200 Subject: Oui --- management/commands/listtimetables.py | 1 - 1 file changed, 1 deletion(-) diff --git a/management/commands/listtimetables.py b/management/commands/listtimetables.py index c5ef41f..e4b782f 100644 --- a/management/commands/listtimetables.py +++ b/management/commands/listtimetables.py @@ -27,7 +27,6 @@ class Command(BaseCommand): def handle(self, *args, **options): timetables = Timetable.objects.all() if options["order_by_id"]: - print("oui") timetables = timetables.order_by("id") else: timetables = timetables.order_by("name") -- cgit v1.2.1 From df8f43e5af448a7d635e762e15ff906ab76565f1 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 26 Mar 2017 17:50:14 +0200 Subject: Affichage d'un compteur d'erreurs de traitement --- management/commands/timetables.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index d39075d..d596233 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -64,6 +64,8 @@ class Command(BaseCommand): def handle(self, *args, **options): year = None + errcount = 0 + if options["week"] is None: _, week, day = timezone.now().isocalendar() if day >= 6: @@ -84,5 +86,9 @@ class Command(BaseCommand): process_timetable(timetable, year, weeks) except Exception as e: self.stderr.write(self.style.ERROR("Failed to process {0}: {1}".format(timetable, e))) + errcount += 1 - self.stdout.write(self.style.SUCCESS("Done.")) + if errcount == 0: + self.stdout.write(self.style.SUCCESS("Done.")) + else: + self.stdout.write(self.style.ERROR("Done with {0} errors.".format(errcount))) -- cgit v1.2.1 From b3a9ed0743f0db3ba65973769ea981bb50c64482 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 26 Mar 2017 18:17:33 +0200 Subject: Le sous-groupe peut être nul --- models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models.py b/models.py index 32a5903..b047b44 100644 --- a/models.py +++ b/models.py @@ -133,7 +133,7 @@ class Room(models.Model): class CourseManager(Manager): def get_courses_for_group(self, group, **filters): - return self.get_queryset().filter(Q(groups__td__isnull=True) | Q(groups__td=group.td), Q(groups__tp__isnull=True) | Q(groups__tp=group.tp), groups__mention=group.mention, groups__subgroup=group.subgroup, timetable=group.timetable, **filters).order_by("begin") + return self.get_queryset().filter(Q(groups__td__isnull=True) | Q(groups__td=group.td), Q(groups__tp__isnull=True) | Q(groups__tp=group.tp), Q(groups__subgroup__isnull=True) | Q(groups__subgroup=group.subgroup), groups__mention=group.mention, timetable=group.timetable, **filters).order_by("begin") def get_weeks(self, **criteria): qs = self.get_queryset().filter(**criteria).order_by("groups__name", "year", "week").annotate(_=Count(("groups", "year", "week", "begin")), year=ExtractYear("begin")) -- cgit v1.2.1 From f413b2382baca5c1cb3a219afa02867bd0348b03 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 26 Mar 2017 18:24:01 +0200 Subject: En enregistrant les modèles c'est mieux --- models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models.py b/models.py index b047b44..975631c 100644 --- a/models.py +++ b/models.py @@ -87,7 +87,7 @@ class Group(models.Model): if self.name == "": self.name = self.celcat_name self.slug = slugify(self.name) - super(Group, self).save() + super(Group, self).save() class Meta: -- cgit v1.2.1 From 7a470357a430a61fd61e5601fd2dde37f13bc646 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 26 Mar 2017 18:26:34 +0200 Subject: Les parties du groupe sont déduites du nom "réel" et non plus du nom dans celcat --- models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models.py b/models.py index 975631c..3e48622 100644 --- a/models.py +++ b/models.py @@ -83,10 +83,11 @@ class Group(models.Model): return self.name def save(self, *args, **kwargs): - self.mention, self.subgroup, self.td, self.tp = parse_group(self.celcat_name) if self.name == "": self.name = self.celcat_name self.slug = slugify(self.name) + + self.mention, self.subgroup, self.td, self.tp = parse_group(self.name) super(Group, self).save() -- cgit v1.2.1 From 21b0f53454e60c672f501389f225bb76d062e33c Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 5 Apr 2017 14:54:17 +0200 Subject: Django 1.11 est la version minimale requise. Suppression de la classe ExtractWeek maison en faveur de celle fournie par Django 1.11. Modification de la requête de récupération des semaines pour effacer le code spécifique à PostgreSQL et SQLite. Mise à jour du README pour refléter ces changements --- README.md | 16 ++++++++-------- models.py | 13 ++----------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ece8601..af24156 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Parce que j'en avait ma claque de consulter un emploi du temps mal formaté. Par ## Comment faire tourner celcatsanitizer chez moi ? celcatsanitizer est écrit en Python 3. Il dépend des bibliothèques suivantes : - * Django 1.10 + * Django 1.11 * requests * BeautifulSoup4 @@ -16,7 +16,7 @@ Pour tester celcatsanitizer, il est recommandé d'utiliser SQLite ou PostgreSQL. Pour la production, il est recommandé d'utiliser PostgreSQL (avec le driver psycopg2) et de mettre le tout dans un environnement virtuel. -Aucun autre SGBD n'a été testé. Toute modification visant à faire fonctionner celcatsanitizer avec un autre SGBD sera bien entendu acceptée. +Aucun autre SGBD n'a été testé, mais depuis la version 0.7.3 (non incluse), celcatsanitizer n'utilise plus de fonctions SQL brutes spécifiques. Tous les SGBD supportés par Django devraient fonctionner sans poser de problèmes. ### Installation Il est préférable d'utiliser un environnement virtuel, mais ce n'est pas obligatoire. Si vous ne souhaitez pas utiliser un environnement virtuel, passez directement à l'installation des dépendances. @@ -40,7 +40,7 @@ Notez que cette étape n'est pas obligatoire > $ pip install requests -> $ pip install django=="1.10.*" +> $ pip install django > $ pip install beautiful4 @@ -66,18 +66,18 @@ Pour la production, il est recommandé d'utiliser une version stable, accessible Dans le fichier celcatsanitizer/settings.py, vous devrez renseigner plusieurs informations. ##### Configuration du serveur mail -[Vous pouvez trouver la documentation concernant l'envoi des mails sur le site de Django.](https://docs.djangoproject.com/fr/1.10/topics/email/) +[Vous pouvez trouver la documentation concernant l'envoi des mails sur le site de Django.](https://docs.djangoproject.com/fr/1.11/topics/email/) ##### Configuration des administrateurs -[Vous pouvez retrouver la documentation de la variable ADMIN sur le site de Django.](https://docs.djangoproject.com/fr/1.10/ref/settings/#admins) +[Vous pouvez retrouver la documentation de la variable ADMIN sur le site de Django.](https://docs.djangoproject.com/fr/1.11/ref/settings/#admins) Cette variable est obligatoire. ##### Configuration de l'internationalisation -Ce passage n'est pas obligatoire. [Vous pouvez retrouver la documentation de l'internationalisation sur le site de Django.](https://docs.djangoproject.com/fr/1.10/topics/i18n/) +Ce passage n'est pas obligatoire. [Vous pouvez retrouver la documentation de l'internationalisation sur le site de Django.](https://docs.djangoproject.com/fr/1.11/topics/i18n/) ##### Configuration de la base de données -[Vous pouvez retrouver la documentation de la base de données sur le site de Django.](https://docs.djangoproject.com/fr/1.10/ref/settings/#databases) +[Vous pouvez retrouver la documentation de la base de données sur le site de Django.](https://docs.djangoproject.com/fr/1.11/ref/settings/#databases) ##### Configuration du mode de Django Si jamais vous utiliser Django en production, vous **devez** mettre la variable DEBUG à False. @@ -99,7 +99,7 @@ Vous avez besoin de générer les migrations de celcatsanitizer, puis appliquez- > $ ./manage.py migrate ##### Gestion des fichiers statiques -Si vous êtes en production, vous devez renseigner l'emplacement de vos fichiers statiques dans la variable [STATIC_ROOT](https://docs.djangoproject.com/fr/1.10/ref/settings/#std:setting-STATIC_ROOT) de la configuration de Django, puis exécuter la commande suivante : +Si vous êtes en production, vous devez renseigner l'emplacement de vos fichiers statiques dans la variable [STATIC_ROOT](https://docs.djangoproject.com/fr/1.11/ref/settings/#std:setting-STATIC_ROOT) de la configuration de Django, puis exécuter la commande suivante : > $ ./manage.py collectstatic diff --git a/models.py b/models.py index 3e48622..876160b 100644 --- a/models.py +++ b/models.py @@ -17,7 +17,7 @@ from django.db import connection, models from django.db.models import Count, Manager, Q from django.db.models.expressions import RawSQL -from django.db.models.functions import Extract, ExtractYear +from django.db.models.functions import ExtractWeek, ExtractYear from django.utils.text import slugify from .utils import parse_group @@ -26,10 +26,6 @@ import hashlib import os -class ExtractWeek(Extract): - lookup_name = "week" - - class Timetable(models.Model): name = models.CharField(max_length=64, unique=True, verbose_name="nom") url = models.URLField(max_length=255, unique=True, verbose_name="URL") @@ -137,12 +133,7 @@ class CourseManager(Manager): return self.get_queryset().filter(Q(groups__td__isnull=True) | Q(groups__td=group.td), Q(groups__tp__isnull=True) | Q(groups__tp=group.tp), Q(groups__subgroup__isnull=True) | Q(groups__subgroup=group.subgroup), groups__mention=group.mention, timetable=group.timetable, **filters).order_by("begin") def get_weeks(self, **criteria): - qs = self.get_queryset().filter(**criteria).order_by("groups__name", "year", "week").annotate(_=Count(("groups", "year", "week", "begin")), year=ExtractYear("begin")) - - if connection.vendor == "postgresql": - return qs.annotate(week=ExtractWeek("begin")) - else: - return qs.annotate(week=RawSQL("""cast(strftime("%%W", "begin") as integer)""", [])) + return self.get_queryset().filter(**criteria).order_by("groups__name", "year", "week").annotate(_=Count(("groups", "year", "week", "begin")), year=ExtractYear("begin"), week=ExtractWeek("begin")) class Course(models.Model): -- cgit v1.2.1 From d412a58b8fc7478846ae4ed9d7a15257f0aacd79 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 12 May 2017 11:07:17 +0200 Subject: Si la regex n'arrive pas à parser le groupe, alors la mention correspond au nom du groupe --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 8e87c0f..fe6ec4c 100644 --- a/utils.py +++ b/utils.py @@ -63,7 +63,7 @@ def parse_group(name): group_regex = re.compile("^([\w ]+?)(\s*\-\s*(((CM)(\w))|((TD)(\w)(\d))|((TP)(\w)(\d)(\d))))?$") search = group_regex.search(name) if search is None: - return None, None, None, None + return name, None, None, None parts = search.groups(0) if parts[1] == 0: -- cgit v1.2.1 From cd67ce3c1eeca28d7cfeaa71cc67165ee71a5fd6 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 4 Sep 2017 16:09:24 +0200 Subject: Changement de la regex des groupes --- utils.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/utils.py b/utils.py index fe6ec4c..8dc386f 100644 --- a/utils.py +++ b/utils.py @@ -49,18 +49,16 @@ def group_courses(courses): def parse_group(name): # Explication de la regex # - # ^([\w ]+?)(\s*\-\s*(((CM)(\w))|((TD)(\w)(\d))|((TP)(\w)(\d)(\d))))?$ - # ^ début de la ligne - # ([\w ]+?) correspond à au moins un caractère - # (\s* zéro, un ou plusieurs espaces - # \- un tiret - # \s* zéro, un ou plusieurs espaces - # (((CM)(\w))| correspond à CM suivi d'une lettre ou... - # ((TD)(\w)(\d))| ... à TD suivi d'une lettre et d'un chiffre ou... - # ((TP)(\w)(\d)(\d))) ... à TP suivi d'une lettre et de deux chiffres - # )? groupe optionel - # $ fin de la ligne - group_regex = re.compile("^([\w ]+?)(\s*\-\s*(((CM)(\w))|((TD)(\w)(\d))|((TP)(\w)(\d)(\d))))?$") + # ^([\w ]+?)(\s*(((CM)(\w))|((TD)(\w)(\d))|((TP)(\w)(\d)(\d))))?$ + # ^ début de la ligne + # ([\w ]+?) correspond à au moins un caractère + # (\s* zéro, un ou plusieurs espaces + # (((CM)(\w))| correspond à CM suivi d'une lettre ou... + # ((TD)(\w)(\d))| ... à TD suivi d'une lettre et d'un chiffre ou... + # ((TP)(\w)(\d)(\d))) ... à TP suivi d'une lettre et de deux chiffres + # )? groupe optionel + # $ fin de la ligne + group_regex = re.compile("^([\w ]+?)(\s*(((CM)(\w))|((TD)(\w)(\d))|((TP)(\w)(\d)(\d))))?$") search = group_regex.search(name) if search is None: return name, None, None, None -- cgit v1.2.1 From c618a8612370b94b89180527cb28b703a8201668 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 4 Sep 2017 16:47:49 +0200 Subject: Utilisation de l’apostrophe typographique et de l’espace insécable. --- templates/mail/mail_confirm.txt | 6 +++--- templates/mail/mail_footer.txt | 4 ++-- templates/mail/mail_timetable.txt | 2 +- templates/mail/mail_unsubscribed.txt | 2 +- templates/subscribe.html | 6 +++--- templates/timetable.html | 4 ++-- views.py | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/templates/mail/mail_confirm.txt b/templates/mail/mail_confirm.txt index b6cc08a..34aca3d 100644 --- a/templates/mail/mail_confirm.txt +++ b/templates/mail/mail_confirm.txt @@ -1,8 +1,8 @@ -Vous avez été abonné à l'emploi du temps {{ group.timetable.name }} - {{ group.name }} +Vous avez été abonné à l’emploi du temps {{ group.timetable.name }} - {{ group.name }} -Pour valider l'abonnement, suivez ce lien : {{ domain }}{% url "confirm" token %} +Pour valider l’abonnement, suivez ce lien : {{ domain }}{% url "confirm" token %} -Si vous pensez que vous avez été abonné par erreur, suivez ce lien : {{ domain }}{% url "cancel" token %} +Si vous pensez que vous avez été abonné par erreur, suivez ce lien : {{ domain }}{% url "cancel" token %} Vous ne recevrez aucun mail tant que vous n'avez pas validé votre abonnement. diff --git a/templates/mail/mail_footer.txt b/templates/mail/mail_footer.txt index 7b71122..b39f738 100644 --- a/templates/mail/mail_footer.txt +++ b/templates/mail/mail_footer.txt @@ -1,2 +1,2 @@ -Pour vous désinscrire de cet emploi du temps, suivez ce lien : {{ domain }}{% url "cancel" token %} -Pour contacter l'administrateur du service, envoyez un mail à cette adresse : {{ admins|first|last }} +Pour vous désinscrire de cet emploi du temps, suivez ce lien : {{ domain }}{% url "cancel" token %} +Pour contacter l’administrateur du service, envoyez un mail à cette adresse : {{ admins|first|last }} diff --git a/templates/mail/mail_timetable.txt b/templates/mail/mail_timetable.txt index f6b8364..7cc6b26 100644 --- a/templates/mail/mail_timetable.txt +++ b/templates/mail/mail_timetable.txt @@ -1,7 +1,7 @@ {% load rooms %}{% autoescape off %}{% for day in courses %}{% filter title %}{{ day.0.begin|date:"l j F o" }}{% endfilter %} - de {{ day.0.begin|date:"H:i" }} à {% with day|last as last %}{{ last.end|date:"H:i" }}{% endwith %} {% for course in day %} * {{ course.name }} ({{ course.type }}), de {{ course.begin|date:"H:i" }} à {{ course.end|date:"H:i" }}{% if course.rooms.all|length > 0 %} {{ course.rooms.all|format_rooms }}{% endif %}{% if course.notes is not None %} - Remarques : {{ course.notes }}{% endif %} + Remarques : {{ course.notes }}{% endif %} {% endfor %}{% empty %}Aucun cours pour le groupe {{ group }} pendant la semaine {{ week }}. {% endfor %}{% endautoescape %} diff --git a/templates/mail/mail_unsubscribed.txt b/templates/mail/mail_unsubscribed.txt index d5c5df2..8d75ccf 100644 --- a/templates/mail/mail_unsubscribed.txt +++ b/templates/mail/mail_unsubscribed.txt @@ -1,2 +1,2 @@ -Vous avez été désabonné de l'emploi du temps {{ group.timetable.name }} - {{ group.name }} +Vous avez été désabonné de l’emploi du temps {{ group.timetable.name }} - {{ group.name }} Notez que si vous vous êtes abonné à un autre emploi du temps, vous recevrez toujours les mails de ceux-ci. diff --git a/templates/subscribe.html b/templates/subscribe.html index 7b542f9..1c1bc3a 100644 --- a/templates/subscribe.html +++ b/templates/subscribe.html @@ -1,6 +1,6 @@ {% extends "index.html" %} -{% block title %}S'abonner à {{ group.timetable.name }} – {{ group.name }}{% endblock %} +{% block title %}S’abonner à {{ group.timetable.name }} – {{ group.name }}{% endblock %} {% block body %}

S'abonner à {{ group.timetable.name }} – {{ group.name }}

@@ -9,7 +9,7 @@ {{ form }} -

Après l'abonnement, vous allez recevoir un mail avec un lien de confirmation. Aucun autre mail ne vous sera envoyé si vous n'avez pas validé votre abonnement.
- Vous pouvez vous désabonner à tout moment à l'aide d'un lien contenu dans tout les mails que nous vous enverrons.
+

Après l’abonnement, vous allez recevoir un mail avec un lien de confirmation. Aucun autre mail ne vous sera envoyé si vous n'avez pas validé votre abonnement.
+ Vous pouvez vous désabonner à tout moment à l’aide d'un lien contenu dans tout les mails que nous vous enverrons.
Nous ne partageons votre adresse à qui que se soit. Lorsque vous vous désabonnez, votre adresse est effacée de nos serveurs.

{% endblock %} diff --git a/templates/timetable.html b/templates/timetable.html index 78dcfed..264a25c 100644 --- a/templates/timetable.html +++ b/templates/timetable.html @@ -9,7 +9,7 @@

{% filter title %}{{ day.0.begin|date:"l j F o" }}{% endfilter %} – de {{ day.0.begin|date:"H:i" }} à {% with day|last as last %}{{ last.end|date:"H:i" }}{% endwith %}

{% endfor %} -

S'abonner à cet emploi du temps

{% endblock %} +

S’abonner à cet emploi du temps

{% endblock %} diff --git a/views.py b/views.py index 22c08ac..9365695 100644 --- a/views.py +++ b/views.py @@ -74,7 +74,7 @@ def subscribe(request, timetable_slug, group_slug, year, week): template = loader.get_template("mail/mail_confirm.txt") context = Context({"group": group, "admins": settings.ADMINS, "token": subscription.token, "domain": settings.DEFAULT_DOMAIN}) - send_mail("Confirmation de l'abonnemenent", template.render(context), settings.DEFAULT_FROM_EMAIL, [request.POST["email"]]) + send_mail("Confirmation de l’abonnemenent", template.render(context), settings.DEFAULT_FROM_EMAIL, [request.POST["email"]]) return redirect("timetable", timetable_slug=timetable_slug, group_slug=group_slug, year=year, week=int(week)) else: -- cgit v1.2.1 From a6c5bfa4796081747e04ae4047007d5c8ad23164 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 4 Sep 2017 18:35:26 +0200 Subject: Ajout d’un champ « groupe parent » au modèle Group pour n’afficher que les groupes qui n’ont pas d’enfants. Par exemple, le groupe TPA21 aura comme parent le groupe TDA2, qui aura le groupe CMA comme parent. Pour l’instant, le parseur d’emploi du temps ne créée pas de telles relations. --- admin.py | 2 +- models.py | 8 ++++++++ utils.py | 4 ++-- views.py | 4 ++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/admin.py b/admin.py index f818865..1c8606a 100644 --- a/admin.py +++ b/admin.py @@ -34,7 +34,7 @@ class LastUpdateAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin): fieldsets = ( (None, {"fields": ("name", "celcat_name", "timetable",)}), - ("Groupes", {"fields": ("mention", "subgroup", "td", "tp",)}),) + ("Groupes", {"fields": ("mention", "subgroup", "td", "tp", "parent_group",)}),) list_display = ("name", "timetable",) list_filter = ("timetable__name",) readonly_fields = ("celcat_name", "mention", "subgroup", "td", "tp",) diff --git a/models.py b/models.py index 876160b..53bcef7 100644 --- a/models.py +++ b/models.py @@ -56,7 +56,14 @@ class LastUpdate(models.Model): verbose_name_plural = "dernières mises à jour" +class GroupManager(Manager): + def get_relevant_groups(self): + return self.get_queryset().annotate(children_count=Count("children")).filter(children_count=0) + + class Group(models.Model): + objects = GroupManager() + name = models.CharField(max_length=255, verbose_name="nom") celcat_name = models.CharField(max_length=255, verbose_name="nom dans Celcat") timetable = models.ForeignKey(Timetable, on_delete=models.CASCADE, verbose_name="emploi du temps") @@ -65,6 +72,7 @@ class Group(models.Model): subgroup = models.CharField(max_length=1, verbose_name="sous-groupe", null=True) td = models.IntegerField(verbose_name="groupe de TD", null=True) tp = models.IntegerField(verbose_name="groupe de TP", null=True) + parent_group = models.ForeignKey("self", verbose_name="groupe parent", null=True, default=None, related_name="children") slug = models.SlugField(max_length=64, default="") diff --git a/utils.py b/utils.py index 8dc386f..8630036 100644 --- a/utils.py +++ b/utils.py @@ -54,8 +54,8 @@ def parse_group(name): # ([\w ]+?) correspond à au moins un caractère # (\s* zéro, un ou plusieurs espaces # (((CM)(\w))| correspond à CM suivi d'une lettre ou... - # ((TD)(\w)(\d))| ... à TD suivi d'une lettre et d'un chiffre ou... - # ((TP)(\w)(\d)(\d))) ... à TP suivi d'une lettre et de deux chiffres + # ((TD)(\w)(\d))| ... à TD suivi d’une lettre et d'un chiffre ou... + # ((TP)(\w)(\d)(\d))) ... à TP suivi d’une lettre et de deux chiffres # )? groupe optionel # $ fin de la ligne group_regex = re.compile("^([\w ]+?)(\s*(((CM)(\w))|((TD)(\w)(\d))|((TP)(\w)(\d)(\d))))?$") diff --git a/views.py b/views.py index 9365695..176f174 100644 --- a/views.py +++ b/views.py @@ -24,8 +24,8 @@ from .models import Timetable, LastUpdate, Group, Subscription, Course from .utils import get_current_week, get_week, group_courses def index(request): - timetables = Timetable.objects.all().order_by("name") - groups = Group.objects.filter(tp__isnull=False).order_by("name") + timetables = Timetable.objects.order_by("name") + groups = Group.objects.get_relevant_groups().order_by("name") year, week = get_current_week() start, _ = get_week(year, week) -- cgit v1.2.1 From 6302ee0f04702c21101f07377b5f2484a165cf16 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 4 Sep 2017 19:36:16 +0200 Subject: On retrouve le parent d’un groupe lorsqu’on l’enregistre --- models.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/models.py b/models.py index 53bcef7..1843a9f 100644 --- a/models.py +++ b/models.py @@ -92,6 +92,18 @@ class Group(models.Model): self.slug = slugify(self.name) self.mention, self.subgroup, self.td, self.tp = parse_group(self.name) + + group_content_list = [self.mention, self.subgroup, self.td, self.tp] + group_content_keys = ("mention", "subgroup", "td", "tp") + for i in range(len(group_content_list))[::-1]: + if group_content_list[i] is not None: + group_content_list[i] = None + break + + if group_content_list[1] is not None: + group_content = dict(zip(group_content_keys, group_content_list)) + self.parent_group_id = Group.objects.filter(**group_content).first().id + super(Group, self).save() -- cgit v1.2.1 From 276158c7813cb75df3ea433d3b8543bdf5c7b7d3 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 4 Sep 2017 21:42:25 +0200 Subject: On trouve le parent de chaque groupe à sa création par le parseur d’emploi du temps, et pas autre part. Si jamais le parent est déjà connu, on ignore cette étape. --- management/commands/_private.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/management/commands/_private.py b/management/commands/_private.py index 3cd23ca..dec13cd 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -41,6 +41,27 @@ def add_time(date, time): delta = datetime.timedelta(hours=time.hour, minutes=time.minute) return date + delta +def consolidate_group(group): + group_content_key = ("mention", "subgroup", "td", "tp") + group_content_list = group.group_info[1:] + group_content = dict(zip(group_content_key, group_content_list)) + + for i in range(len(group_content_list))[::-1]: + del group_content[group_content_key[i]] + group_content[group_content_key[i] + "__isnull"] = True + + if group_content_list[i] is not None: + break + + if "subgroup" in group_content: + group.parent_group = Group.objects.filter(**group_content).first() + group.save() + +def consolidate_groups(groups): + for group in groups: + if group.parent_group == None: + consolidate_group(group) + def delete_courses_in_week(timetable, year, week): start, end = get_week(year, week) Course.objects.filter(begin__gte=start, begin__lt=end, @@ -79,6 +100,7 @@ def get_events(timetable, year, week, soup, weeks_in_soup): groups = [get_from_db_or_create(Group, timetable=timetable, celcat_name=item.text) for item in event.resources.group.find_all("item")] + consolidate_groups(groups) if event.notes is not None: notes = event.notes.text -- cgit v1.2.1 From 8526c4588d88e98f59611771dbc0ca34c1b66d66 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 4 Sep 2017 21:43:40 +0200 Subject: Rennomage de parent_group en parent. --- admin.py | 2 +- management/commands/_private.py | 4 ++-- models.py | 14 +------------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/admin.py b/admin.py index 1c8606a..2408623 100644 --- a/admin.py +++ b/admin.py @@ -34,7 +34,7 @@ class LastUpdateAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin): fieldsets = ( (None, {"fields": ("name", "celcat_name", "timetable",)}), - ("Groupes", {"fields": ("mention", "subgroup", "td", "tp", "parent_group",)}),) + ("Groupes", {"fields": ("mention", "subgroup", "td", "tp", "parent",)}),) list_display = ("name", "timetable",) list_filter = ("timetable__name",) readonly_fields = ("celcat_name", "mention", "subgroup", "td", "tp",) diff --git a/management/commands/_private.py b/management/commands/_private.py index dec13cd..c31eb34 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -54,12 +54,12 @@ def consolidate_group(group): break if "subgroup" in group_content: - group.parent_group = Group.objects.filter(**group_content).first() + group.parent = Group.objects.filter(**group_content).first() group.save() def consolidate_groups(groups): for group in groups: - if group.parent_group == None: + if group.parent == None: consolidate_group(group) def delete_courses_in_week(timetable, year, week): diff --git a/models.py b/models.py index 1843a9f..3d4fdda 100644 --- a/models.py +++ b/models.py @@ -72,7 +72,7 @@ class Group(models.Model): subgroup = models.CharField(max_length=1, verbose_name="sous-groupe", null=True) td = models.IntegerField(verbose_name="groupe de TD", null=True) tp = models.IntegerField(verbose_name="groupe de TP", null=True) - parent_group = models.ForeignKey("self", verbose_name="groupe parent", null=True, default=None, related_name="children") + parent = models.ForeignKey("self", verbose_name="groupe parent", null=True, default=None, related_name="children") slug = models.SlugField(max_length=64, default="") @@ -92,18 +92,6 @@ class Group(models.Model): self.slug = slugify(self.name) self.mention, self.subgroup, self.td, self.tp = parse_group(self.name) - - group_content_list = [self.mention, self.subgroup, self.td, self.tp] - group_content_keys = ("mention", "subgroup", "td", "tp") - for i in range(len(group_content_list))[::-1]: - if group_content_list[i] is not None: - group_content_list[i] = None - break - - if group_content_list[1] is not None: - group_content = dict(zip(group_content_keys, group_content_list)) - self.parent_group_id = Group.objects.filter(**group_content).first().id - super(Group, self).save() -- cgit v1.2.1 From 828f19bde69dd525f08ad0c0f46e3929d89463b9 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 4 Sep 2017 21:45:14 +0200 Subject: Suppression de l’import de RawSQL dans models.py --- models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/models.py b/models.py index 3d4fdda..21b08f5 100644 --- a/models.py +++ b/models.py @@ -16,7 +16,6 @@ from django.db import connection, models from django.db.models import Count, Manager, Q -from django.db.models.expressions import RawSQL from django.db.models.functions import ExtractWeek, ExtractYear from django.utils.text import slugify -- cgit v1.2.1 From 87524ee9598673aba562ba25b078e7996870ee6d Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 4 Sep 2017 21:49:32 +0200 Subject: Correction des tests en accord avec le nouveau style de groupe --- tests.py | 76 ++++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/tests.py b/tests.py index b6b9a70..a3ae55e 100644 --- a/tests.py +++ b/tests.py @@ -27,13 +27,13 @@ class CourseTestCase(TestCase): self.timetable = Timetable(name="Test timetable 2", url="http://example.org/", slug="test-timetable2") self.timetable.save() - cma = Group.objects.create(celcat_name="L1 info s2 - CMA", timetable=self.timetable) - tda2 = Group.objects.create(celcat_name="L1 info s2 - TDA2", timetable=self.timetable) - self.tpa21 = Group.objects.create(celcat_name="L1 info s2 - TPA21", timetable=self.timetable) + cma = Group.objects.create(celcat_name="L1 info s2 CMA", timetable=self.timetable) + tda2 = Group.objects.create(celcat_name="L1 info s2 TDA2", timetable=self.timetable) + self.tpa21 = Group.objects.create(celcat_name="L1 info s2 TPA21", timetable=self.timetable) - cmb = Group.objects.create(celcat_name="L1 info s2 - CMB", timetable=self.timetable) - tdb2 = Group.objects.create(celcat_name="L1 info s2 - TDB2", timetable=self.timetable) - self.tpb21 = Group.objects.create(celcat_name="L1 info s2 - TPB21", timetable=self.timetable) + cmb = Group.objects.create(celcat_name="L1 info s2 CMB", timetable=self.timetable) + tdb2 = Group.objects.create(celcat_name="L1 info s2 TDB2", timetable=self.timetable) + self.tpb21 = Group.objects.create(celcat_name="L1 info s2 TPB21", timetable=self.timetable) for group in (cma, tda2, self.tpa21, cmb, tdb2, self.tpb21,): course = Course.objects.create(name="{0} course".format(group.name), type="cours", timetable=self.timetable, begin=dt, end=dt) @@ -43,8 +43,8 @@ class CourseTestCase(TestCase): tpa21_courses = Course.objects.get_courses_for_group(self.tpa21) tpb21_courses = Course.objects.get_courses_for_group(self.tpb21) - tpa21_course_names = ["L1 info s2 - CMA course", "L1 info s2 - TDA2 course", "L1 info s2 - TPA21 course"] - tpb21_course_names = ["L1 info s2 - CMB course", "L1 info s2 - TDB2 course", "L1 info s2 - TPB21 course"] + tpa21_course_names = ["L1 info s2 CMA course", "L1 info s2 TDA2 course", "L1 info s2 TPA21 course"] + tpb21_course_names = ["L1 info s2 CMB course", "L1 info s2 TDB2 course", "L1 info s2 TPB21 course"] for courses, names in ((tpa21_courses, tpa21_course_names,), (tpb21_courses, tpb21_course_names,),): for course in courses: @@ -57,22 +57,22 @@ class GroupTestCase(TestCase): self.timetable = Timetable(name="Test timetable", url="http://example.com/", slug="test-timetable") self.timetable.save() - Group.objects.create(celcat_name="L1 info s2 - CMA", timetable=self.timetable) - Group.objects.create(celcat_name="L1 info s2 - TDA2", timetable=self.timetable) - Group.objects.create(celcat_name="L1 info s2 - TPA21", timetable=self.timetable) + Group.objects.create(celcat_name="L1 info s2 CMA", timetable=self.timetable) + Group.objects.create(celcat_name="L1 info s2 TDA2", timetable=self.timetable) + Group.objects.create(celcat_name="L1 info s2 TPA21", timetable=self.timetable) - Group.objects.create(celcat_name="L1 info s2 - CMB", timetable=self.timetable) - Group.objects.create(celcat_name="L1 info s2 - TDB2", timetable=self.timetable) - Group.objects.create(celcat_name="L1 info s2 - TPB21", timetable=self.timetable) + Group.objects.create(celcat_name="L1 info s2 CMB", timetable=self.timetable) + Group.objects.create(celcat_name="L1 info s2 TDB2", timetable=self.timetable) + Group.objects.create(celcat_name="L1 info s2 TPB21", timetable=self.timetable) def test_corresponds(self): - cma = Group.objects.get(celcat_name="L1 info s2 - CMA", timetable=self.timetable) - tda2 = Group.objects.get(celcat_name="L1 info s2 - TDA2", timetable=self.timetable) - tpa21 = Group.objects.get(celcat_name="L1 info s2 - TPA21", timetable=self.timetable) + cma = Group.objects.get(celcat_name="L1 info s2 CMA", timetable=self.timetable) + tda2 = Group.objects.get(celcat_name="L1 info s2 TDA2", timetable=self.timetable) + tpa21 = Group.objects.get(celcat_name="L1 info s2 TPA21", timetable=self.timetable) - cmb = Group.objects.get(celcat_name="L1 info s2 - CMB", timetable=self.timetable) - tdb2 = Group.objects.get(celcat_name="L1 info s2 - TDB2", timetable=self.timetable) - tpb21 = Group.objects.get(celcat_name="L1 info s2 - TPB21", timetable=self.timetable) + cmb = Group.objects.get(celcat_name="L1 info s2 CMB", timetable=self.timetable) + tdb2 = Group.objects.get(celcat_name="L1 info s2 TDB2", timetable=self.timetable) + tpb21 = Group.objects.get(celcat_name="L1 info s2 TPB21", timetable=self.timetable) self.assertTrue(cma.corresponds_to(*tda2.group_info)) # CMA corresponds to TDA2 self.assertTrue(cma.corresponds_to(*tpa21.group_info)) # CMA corresponds to TPA21 @@ -99,30 +99,30 @@ class GroupTestCase(TestCase): self.assertFalse(tpa21.corresponds_to(*tdb2.group_info)) # TPA21 does not corresponds to TDB2 def test_get(self): - cma = Group.objects.get(name="L1 info s2 - CMA", timetable=self.timetable) - tda2 = Group.objects.get(name="L1 info s2 - TDA2", timetable=self.timetable) - tpa21 = Group.objects.get(name="L1 info s2 - TPA21", timetable=self.timetable) + cma = Group.objects.get(name="L1 info s2 CMA", timetable=self.timetable) + tda2 = Group.objects.get(name="L1 info s2 TDA2", timetable=self.timetable) + tpa21 = Group.objects.get(name="L1 info s2 TPA21", timetable=self.timetable) - cmb = Group.objects.get(name="L1 info s2 - CMB", timetable=self.timetable) - tdb2 = Group.objects.get(name="L1 info s2 - TDB2", timetable=self.timetable) - tpb21 = Group.objects.get(name="L1 info s2 - TPB21", timetable=self.timetable) + cmb = Group.objects.get(name="L1 info s2 CMB", timetable=self.timetable) + tdb2 = Group.objects.get(name="L1 info s2 TDB2", timetable=self.timetable) + tpb21 = Group.objects.get(name="L1 info s2 TPB21", timetable=self.timetable) - self.assertEqual(cma.celcat_name, "L1 info s2 - CMA") - self.assertEqual(tda2.celcat_name, "L1 info s2 - TDA2") - self.assertEqual(tpa21.celcat_name, "L1 info s2 - TPA21") + self.assertEqual(cma.celcat_name, "L1 info s2 CMA") + self.assertEqual(tda2.celcat_name, "L1 info s2 TDA2") + self.assertEqual(tpa21.celcat_name, "L1 info s2 TPA21") - self.assertEqual(cmb.celcat_name, "L1 info s2 - CMB") - self.assertEqual(tdb2.celcat_name, "L1 info s2 - TDB2") - self.assertEqual(tpb21.celcat_name, "L1 info s2 - TPB21") + self.assertEqual(cmb.celcat_name, "L1 info s2 CMB") + self.assertEqual(tdb2.celcat_name, "L1 info s2 TDB2") + self.assertEqual(tpb21.celcat_name, "L1 info s2 TPB21") def test_parse(self): - cma = Group.objects.get(celcat_name="L1 info s2 - CMA", timetable=self.timetable) - tda2 = Group.objects.get(celcat_name="L1 info s2 - TDA2", timetable=self.timetable) - tpa21 = Group.objects.get(celcat_name="L1 info s2 - TPA21", timetable=self.timetable) + cma = Group.objects.get(celcat_name="L1 info s2 CMA", timetable=self.timetable) + tda2 = Group.objects.get(celcat_name="L1 info s2 TDA2", timetable=self.timetable) + tpa21 = Group.objects.get(celcat_name="L1 info s2 TPA21", timetable=self.timetable) - cmb = Group.objects.get(celcat_name="L1 info s2 - CMB", timetable=self.timetable) - tdb2 = Group.objects.get(celcat_name="L1 info s2 - TDB2", timetable=self.timetable) - tpb21 = Group.objects.get(celcat_name="L1 info s2 - TPB21", timetable=self.timetable) + cmb = Group.objects.get(celcat_name="L1 info s2 CMB", timetable=self.timetable) + tdb2 = Group.objects.get(celcat_name="L1 info s2 TDB2", timetable=self.timetable) + tpb21 = Group.objects.get(celcat_name="L1 info s2 TPB21", timetable=self.timetable) self.assertEqual(cma.group_info, (self.timetable.id, "L1 info s2", "A", None, None)) self.assertEqual(tda2.group_info, (self.timetable.id, "L1 info s2", "A", 2, None)) -- cgit v1.2.1 From 4b3fc9e4c41e2247bf0fa0fe2629b2b57fc174b0 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 4 Sep 2017 21:56:04 +0200 Subject: Version 0.8.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af24156..d3cbb6c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Pour tester celcatsanitizer, il est recommandé d'utiliser SQLite ou PostgreSQL. Pour la production, il est recommandé d'utiliser PostgreSQL (avec le driver psycopg2) et de mettre le tout dans un environnement virtuel. -Aucun autre SGBD n'a été testé, mais depuis la version 0.7.3 (non incluse), celcatsanitizer n'utilise plus de fonctions SQL brutes spécifiques. Tous les SGBD supportés par Django devraient fonctionner sans poser de problèmes. +Aucun autre SGBD n'a été testé, mais depuis la version 0.8.0, celcatsanitizer n'utilise plus de fonctions SQL brutes spécifiques. Tous les SGBD supportés par Django devraient fonctionner sans poser de problèmes. ### Installation Il est préférable d'utiliser un environnement virtuel, mais ce n'est pas obligatoire. Si vous ne souhaitez pas utiliser un environnement virtuel, passez directement à l'installation des dépendances. -- cgit v1.2.1