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 %}
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.