From a61f5a3f507395c2aa0cab09f14f4308159935f6 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 4 Oct 2017 17:38:01 +0200 Subject: Ajout d’une description en balise meta et de liens alternate --- templates/index.html | 1 + templates/timetable.html | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/templates/index.html b/templates/index.html index 8d7634e..005fd5b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,6 +3,7 @@ +{% block head %}{% endblock %} {% block title %}{% if year %}{{ year }} – {% endif %}{% endblock %}celcatsanitizer diff --git a/templates/timetable.html b/templates/timetable.html index 9143c2f..4ab6a1a 100644 --- a/templates/timetable.html +++ b/templates/timetable.html @@ -1,5 +1,12 @@ {% extends "index.html" %} +{% block head %} + + + + +{% endblock %} + {% block title %}{{ group.timetable }} – {{ group }} – Semaine {{ week }} – {% endblock %} {% block body %} -- cgit v1.2.1 From cede815e9314290227f18156b93f8020041381d6 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 5 Oct 2017 09:08:40 +0200 Subject: Support des catégories iCalendar --- feeds.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/feeds.py b/feeds.py index 564b285..c2c5b14 100644 --- a/feeds.py +++ b/feeds.py @@ -29,7 +29,7 @@ from .templatetags.rooms import format_rooms from .utils import get_current_or_next_week, get_week, group_courses ICAL_NAMES = ["uid", "summary", "description", "location", - "start", "dtstart", "dtend", "dtstamp"] + "start", "dtstart", "dtend", "dtstamp", "categories"] class IcalFeedGenerator(SyndicationFeed): @@ -66,6 +66,9 @@ class IcalFeed(Feed): else: return group + def item_categories(self, item): + return (item.type,) + def item_description(self, item): return item.notes -- cgit v1.2.1 From 295f8cbd261cf588f1f5f8eb2b0d82b1ae9c1fe0 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 5 Oct 2017 09:35:14 +0200 Subject: On ne supprime pas les cours qui commencent avant le début du traitement --- management/commands/_private.py | 13 +++++++++---- management/commands/timetables.py | 9 +++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/management/commands/_private.py b/management/commands/_private.py index 8f195a1..d576daf 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -65,9 +65,9 @@ def consolidate_groups(groups): if group.parent is None: consolidate_group(group) -def delete_courses_in_week(timetable, year, week): +def delete_courses_in_week(timetable, year, week, today): start, end = get_week(year, week) - Course.objects.filter(begin__gte=start, begin__lt=end, + Course.objects.filter(begin__gte=max(start, today), begin__lt=end, timetable=timetable).delete() def get_from_db_or_create(cls, **kwargs): @@ -128,7 +128,7 @@ def get_event(timetable, event, event_week): return course -def get_events(timetable, soup, weeks_in_soup, year=None, week=None): +def get_events(timetable, soup, weeks_in_soup, today, year=None, week=None): """Récupère tous les cours disponibles dans l’emploi du temps Celcat. Le traîtement se limitera à la semaine indiquée si il y en a une.""" for event in soup.find_all("event"): @@ -142,7 +142,12 @@ def get_events(timetable, soup, weeks_in_soup, year=None, week=None): year is None or week is None) and \ event.resources.group is not None and \ event.starttime is not None and event.endtime is not None: - yield get_event(timetable, event, event_week) + course = get_event(timetable, event, event_week) + + # On ne sauvegarde le cours que si il ne + # commence après le moment du traitement + if course.begin >= today: + yield course def get_update_date(soup): # Explication de la regex diff --git a/management/commands/timetables.py b/management/commands/timetables.py index 76f0a7c..8b37a5d 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -27,6 +27,7 @@ from ._private import delete_courses_in_week, get_events, get_update_date, get_w @transaction.atomic def process_timetable_week(timetable, soup, weeks_in_soup, force, year=None, week=None): criteria = {} + today = timezone.make_aware(datetime.now()) if year is not None and week is not None: begin, end = get_week(year, week) criteria["begin__gte"] = begin @@ -42,12 +43,12 @@ def process_timetable_week(timetable, soup, weeks_in_soup, force, year=None, wee return if year is not None and week is not None: - delete_courses_in_week(timetable, year, week) + delete_courses_in_week(timetable, year, week, today) else: - Course.objects.filter(timetable=timetable, - begin__gte=min(weeks_in_soup.values())).delete() + delete_from = max(min(weeks_in_soup.values()), today) + Course.objects.filter(timetable=timetable, begin__gte=delete_from).delete() - for course in get_events(timetable, soup, weeks_in_soup, year, week): + for course in get_events(timetable, soup, weeks_in_soup, today, year, week): course.save() timetable.last_update_date = new_update_date -- cgit v1.2.1 From ccfedd0bbb8f9e5229b4007157fb0c2aa8484668 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 5 Oct 2017 12:45:01 +0200 Subject: Mauvais module --- management/commands/timetables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index 8b37a5d..8ba8768 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -27,7 +27,7 @@ from ._private import delete_courses_in_week, get_events, get_update_date, get_w @transaction.atomic def process_timetable_week(timetable, soup, weeks_in_soup, force, year=None, week=None): criteria = {} - today = timezone.make_aware(datetime.now()) + today = timezone.make_aware(datetime.datetime.now()) if year is not None and week is not None: begin, end = get_week(year, week) criteria["begin__gte"] = begin -- cgit v1.2.1 From 76e344f68c1616693e93791f55348272cc61267e Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 5 Oct 2017 22:02:05 +0200 Subject: Simplification du filtrage de process_timetable_week() (pourquoi réinventer la roue carrée ?…) Ajout de commentaires --- management/commands/timetables.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index 8ba8768..8945950 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -26,31 +26,49 @@ from ._private import delete_courses_in_week, get_events, get_update_date, get_w @transaction.atomic def process_timetable_week(timetable, soup, weeks_in_soup, force, year=None, week=None): - criteria = {} today = timezone.make_aware(datetime.datetime.now()) + + # On récupère la mise à jour la plus ancienne dans les cours de l’emploi du temps + # commençant à partir de maintenant + last_update_date = Course.objects.filter(timetable=timetable, begin__gte=today) if year is not None and week is not None: - begin, end = get_week(year, week) - criteria["begin__gte"] = begin - criteria["begin__lt"] = end + # Si jamais on traite une semaine spécifique, on limite les cours sélectionnés + # à ceux qui commencent entre le début du traitement et la fin de la semaine + _, end = get_week(year, week) + last_update_date = last_update_date.filter(begin__lt=end) + + last_update_date = last_update_date.aggregate(Min("last_update")) \ + ["last_update__min"] - last_update_date = Course.objects.filter(timetable=timetable, **criteria) \ - .aggregate(Min("last_update")) \ - ["last_update__min"] + # Date de mise à jour de Celcat, utilisée à des fins de statistiques new_update_date = get_update_date(soup) + # On ne fait pas la mise à jour si jamais la dernière date de MàJ est plus récente + # que celle indiquée par Celcat. + # Attention, le champ last_update de la classe Course représente l’heure à laquelle + # le cours a été inséré dans la base de données, et non pas la date indiquée par + # Celcat. if not force and last_update_date is not None and new_update_date is not None and \ last_update_date >= new_update_date: return if year is not None and week is not None: + # On efface la semaine à partir de maintenant si jamais + # on demande le traitement d’une seule semaine delete_courses_in_week(timetable, year, week, today) else: - delete_from = max(min(weeks_in_soup.values()), today) + # Sinon, on efface tous les cours à partir de maintenant. + # Précisément, on prend la plus grande valeur entre la première semaine + # présente dans Celcat et maintenant. + delete_from = max(min(weeks_in_soup.values()), today) # Vraiment utile ? Course.objects.filter(timetable=timetable, begin__gte=delete_from).delete() + # Tous les cours commençant sur la période traitée + # sont parsés, puis enregistrés dans la base de données. for course in get_events(timetable, soup, weeks_in_soup, today, year, week): course.save() + # On renseigne la date de mise à jour de Celcat, à des fins de statistiques timetable.last_update_date = new_update_date timetable.save() -- cgit v1.2.1 From 7f0dac074465ef838788202b80988005c35c0833 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 6 Oct 2017 18:52:52 +0200 Subject: Si on force une mise à jour, on efface les cours autant qu’on peut au lieu de le faire à partir du lancement du traitement --- management/commands/timetables.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index 8945950..a152f9e 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -26,7 +26,13 @@ from ._private import delete_courses_in_week, get_events, get_update_date, get_w @transaction.atomic def process_timetable_week(timetable, soup, weeks_in_soup, force, year=None, week=None): - today = timezone.make_aware(datetime.datetime.now()) + begin, end = get_week(year, week) + # Si on force la mise à jour, on définit de moment + # de la mise à jour au début de la semaine + if force: + today = begin + else: + today = timezone.make_aware(datetime.datetime.now()) # On récupère la mise à jour la plus ancienne dans les cours de l’emploi du temps # commençant à partir de maintenant @@ -34,7 +40,6 @@ def process_timetable_week(timetable, soup, weeks_in_soup, force, year=None, wee if year is not None and week is not None: # Si jamais on traite une semaine spécifique, on limite les cours sélectionnés # à ceux qui commencent entre le début du traitement et la fin de la semaine - _, end = get_week(year, week) last_update_date = last_update_date.filter(begin__lt=end) last_update_date = last_update_date.aggregate(Min("last_update")) \ @@ -60,7 +65,10 @@ def process_timetable_week(timetable, soup, weeks_in_soup, force, year=None, wee # Sinon, on efface tous les cours à partir de maintenant. # Précisément, on prend la plus grande valeur entre la première semaine # présente dans Celcat et maintenant. - delete_from = max(min(weeks_in_soup.values()), today) # Vraiment utile ? + delete_from = min(weeks_in_soup.values()) + if not force: + # Si jamais on force la MàJ, on efface tout à partir de la première semaine + delete_from = max(delete_from, today) Course.objects.filter(timetable=timetable, begin__gte=delete_from).delete() # Tous les cours commençant sur la période traitée -- cgit v1.2.1 From ab47155c3d7eb9b5bf421c5f9c9c903602ebb175 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sat, 7 Oct 2017 11:13:09 +0200 Subject: La fonction create() des modèles créée un objet en base, donc ne pas sauvegarder après coup, comme ce qui était fait jusque là créait des cours sans nom ni type, faisant crasher le générateur d’ICS. La comparaison du début d’un cours se fait maintenant directement dans get_event(), avant que l’objet Course ne soit créé. --- management/commands/_private.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/management/commands/_private.py b/management/commands/_private.py index d576daf..bad6e6f 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -80,7 +80,7 @@ def get_from_db_or_create(cls, **kwargs): return obj -def get_event(timetable, event, event_week): +def get_event(timetable, event, event_week, today): """Renvoie une classe Course à partir d’un événement récupéré par BS4""" # On récupère la date de l’évènement à partir de la semaine # et de la semaine référencée, puis l’heure de début et de fin @@ -88,6 +88,10 @@ def get_event(timetable, event, event_week): begin = add_time(date, event.starttime.text) end = add_time(date, event.endtime.text) + # On ne traite pas le cours si il commence après le moment du traitement + if begin < today: + return + # Création de l’objet cours course = Course.objects.create(timetable=timetable, begin=begin, end=end) @@ -142,11 +146,10 @@ def get_events(timetable, soup, weeks_in_soup, today, year=None, week=None): year is None or week is None) and \ event.resources.group is not None and \ event.starttime is not None and event.endtime is not None: - course = get_event(timetable, event, event_week) + course = get_event(timetable, event, event_week, today) - # On ne sauvegarde le cours que si il ne - # commence après le moment du traitement - if course.begin >= today: + # On renvoie le cours si il n’est pas nul + if course is not None: yield course def get_update_date(soup): -- cgit v1.2.1 From 72dcae5c9d58005af71b293d1ec09a4c50335c64 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 9 Oct 2017 23:11:16 +0200 Subject: Lorsque le nom d’un cours et son champ remarque est vide, le nom de l’objet cours final était égal à None et faisait crasher les flux RSS et Atom, ainsi que l’ICS et n’affichait aucun cours à la semaine du cours problématique. C’est maintenant corrigé. --- management/commands/_private.py | 17 ++++++++--------- models.py | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/management/commands/_private.py b/management/commands/_private.py index bad6e6f..3b6a164 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -103,20 +103,19 @@ def get_event(timetable, event, event_week, today): consolidate_groups(groups) course.groups.add(*groups) - # On récupère le champ « remarque » - if event.notes is not None: - course.notes = event.notes.text - - # On récupère le nom du cours + # On récupère le nom du cours et le champ « remarque » if event.resources.module is not None: course.name = event.resources.module.item.text - else: + if course.notes is not None: + course.notes = event.notes.text + + elif event.resources.module is None and event.notes is not None: # Il est possible qu’un cours n’ait pas de nom. Oui oui. # Qui sont les concepteurs de ce système ? Quels sont leurs # réseaux ? - # Bref, dans ce cas, on déplace le champ « remarque » de - # l’objet dans le champ « nom ». - course.name, course.notes = course.notes, None + # Bref, dans ce cas, si le cours possède une remarque, elle + # devient le nom du cours. + course.name = event.notes.text # Récupération du type de cours if event.category is not None: diff --git a/models.py b/models.py index ec141a2..e48efa5 100644 --- a/models.py +++ b/models.py @@ -176,7 +176,7 @@ class Course(models.Model): objects = CourseManager() name = models.CharField(max_length=255, verbose_name="nom", default="Sans nom", - null=True) + null=True) # TODO 0.11: null=False type_ = models.CharField(name="type", max_length=255, verbose_name="type de cours", null=True) timetable = models.ForeignKey(Timetable, on_delete=models.CASCADE, -- cgit v1.2.1 From e6ccfea46083c4cd228be57565be006e8c3c59f8 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 10 Oct 2017 18:07:54 +0200 Subject: Correction de l’affichage de la dernière mise à jour --- views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/views.py b/views.py index bd93712..5988536 100644 --- a/views.py +++ b/views.py @@ -80,15 +80,15 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No if group.children.count(): return group_list_common(request, timetable, Group.objects.get_relevant_children(group)) - courses = Course.objects.get_courses_for_group(group, begin__gte=start, begin__lt=end) \ - .annotate(Max("last_update")) + courses = Course.objects.get_courses_for_group(group, begin__gte=start, begin__lt=end) if courses.count() == 0: raise Http404 + last_update = courses.aggregate(Max("last_update")) grouped_courses = group_courses(courses) return render(request, "timetable.html", {"group": group, "courses": grouped_courses, - "last_update": courses.first().last_update__max, + "last_update": last_update["last_update__max"], "year": year, "week": int(week), "is_old_timetable": is_old_timetable}) -- cgit v1.2.1 From 5939a5dc3747e8b872dd4cb0c622621c25b72c5d Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 10 Oct 2017 18:26:37 +0200 Subject: Si la semaine courante n’a pas de cours existant, la page générale existe quand même --- templates/timetable.html | 2 +- templates/timetable_common.html | 3 ++- views.py | 9 +++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/templates/timetable.html b/templates/timetable.html index 4ab6a1a..e4dffe7 100644 --- a/templates/timetable.html +++ b/templates/timetable.html @@ -15,7 +15,7 @@ {% if is_old_timetable %} Accéder à l’emploi du temps de cette semaine.
{% endif %} - Dernière mise à jour le {{ last_update|date:"l j F o" }} à {{ last_update|date:"H:i" }} + {% if last_update %}Dernière mise à jour le {{ last_update|date:"l j F o" }} à {{ last_update|date:"H:i" }}{% endif %}

{% include "timetable_common.html" %}

ICSRSSAtom

{% endblock %} diff --git a/templates/timetable_common.html b/templates/timetable_common.html index 4319e60..9a2c27e 100644 --- a/templates/timetable_common.html +++ b/templates/timetable_common.html @@ -9,4 +9,5 @@ Remarques : {{ course.notes }}{% endif %} {% endfor %} - {% endfor %} + {% empty %} +

Aucun cours cette semaine.

{% endfor %} diff --git a/views.py b/views.py index 5988536..2100972 100644 --- a/views.py +++ b/views.py @@ -65,10 +65,11 @@ def group_list(request, year_slug, timetable_slug): def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=None): current_year, current_week = get_current_or_next_week() - is_old_timetable = False + is_old_timetable, provided_week = False, True if year is None or week is None: year, week = current_year, current_week + provided_week = False elif (int(year), int(week)) < (current_year, current_week): is_old_timetable = True @@ -81,14 +82,14 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No return group_list_common(request, timetable, Group.objects.get_relevant_children(group)) courses = Course.objects.get_courses_for_group(group, begin__gte=start, begin__lt=end) - if courses.count() == 0: + if courses.count() == 0 and provided_week: raise Http404 - last_update = courses.aggregate(Max("last_update")) + last_update = courses.aggregate(Max("last_update"))["last_update__max"] grouped_courses = group_courses(courses) return render(request, "timetable.html", {"group": group, "courses": grouped_courses, - "last_update": last_update["last_update__max"], + "last_update": last_update, "year": year, "week": int(week), "is_old_timetable": is_old_timetable}) -- cgit v1.2.1 From 494e8150032178ee530c69ef253fd5903544a167 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 10 Oct 2017 21:26:27 +0200 Subject: Ajout de la variable VERSION --- __init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/__init__.py b/__init__.py index dbcf830..13ff905 100644 --- a/__init__.py +++ b/__init__.py @@ -13,4 +13,5 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . +VERSION = "dev" default_app_config = "edt.apps.EdtConfig" -- cgit v1.2.1 From e80ab8a7232a73d8a0f09f8e41a88893d4c65d63 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 10 Oct 2017 21:26:39 +0200 Subject: Envoi d’un user-agent personnalisé --- management/commands/_private.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/management/commands/_private.py b/management/commands/_private.py index 3b6a164..943a3fb 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -23,6 +23,7 @@ from edt.models import Group, Room, Course from edt.utils import get_week import requests +import edt def add_time(date, time): ptime = datetime.datetime.strptime(time, "%H:%M") @@ -194,7 +195,8 @@ def get_weeks(soup): return weeks def get_xml(url): - req = requests.get(url) + user_agent = "celcatsanitizer/" + edt.VERSION + req = requests.get(url, headers={"User-Agent": user_agent}) req.encoding = "utf8" soup = BeautifulSoup(req.content, "html.parser") -- cgit v1.2.1 From 0e0e59e3530dcfbb8e92b5a3926edad4f58d72a1 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 10 Oct 2017 21:34:26 +0200 Subject: Il est plus pertinent de remplacer le nom du cours par son type lorsqu’il n’y en a pas que par sa remarque. --- management/commands/_private.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/management/commands/_private.py b/management/commands/_private.py index 943a3fb..e7f0a3c 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -104,19 +104,21 @@ def get_event(timetable, event, event_week, today): consolidate_groups(groups) course.groups.add(*groups) - # On récupère le nom du cours et le champ « remarque » + # On récupère le champ « remarque » + if course.notes is not None: + course.notes = event.notes.text + + # On récupère le champ « nom » if event.resources.module is not None: course.name = event.resources.module.item.text - if course.notes is not None: - course.notes = event.notes.text - - elif event.resources.module is None and event.notes is not None: + elif event.category is not None: # Il est possible qu’un cours n’ait pas de nom. Oui oui. # Qui sont les concepteurs de ce système ? Quels sont leurs # réseaux ? - # Bref, dans ce cas, si le cours possède une remarque, elle - # devient le nom du cours. - course.name = event.notes.text + # Bref, dans ce cas, si le cours a un type, il devient son nom. + course.type = event.category.text + # Si il n’a pas de type (mais je ne pense pas que ça soit possible…), + # il obtiendra une valeur par défaut définie à l’avance. # Récupération du type de cours if event.category is not None: -- cgit v1.2.1 From e2fb71d3377dee14cf1934ac3d6ad448bb7063bc Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 10 Oct 2017 21:35:55 +0200 Subject: Un cours doit obligatoirement avoir un nom --- models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/models.py b/models.py index e48efa5..fbbf877 100644 --- a/models.py +++ b/models.py @@ -175,8 +175,7 @@ class CourseManager(Manager): class Course(models.Model): objects = CourseManager() - name = models.CharField(max_length=255, verbose_name="nom", default="Sans nom", - null=True) # TODO 0.11: null=False + name = models.CharField(max_length=255, verbose_name="nom", default="Sans nom") type_ = models.CharField(name="type", max_length=255, verbose_name="type de cours", null=True) timetable = models.ForeignKey(Timetable, on_delete=models.CASCADE, -- cgit v1.2.1 From 640496deb262349101567de12d2476cbfe5065b3 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sat, 14 Oct 2017 11:25:46 +0200 Subject: timezone.now() retourne l’heure UTC, ce qui empêche la mise à jour de la semaine prochaine le samedi à minuit. Remplacement de ces appels par une fonction qui retourne la bonne heure dans le bon fuseau horaire. --- management/commands/timetables.py | 10 +++++----- tests.py | 3 ++- utils.py | 10 +++++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index a152f9e..35fb26e 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -21,7 +21,7 @@ from django.db.models import Min from django.utils import timezone from edt.models import Course, Timetable -from edt.utils import get_week +from edt.utils import get_week, tz_now from ._private import delete_courses_in_week, get_events, get_update_date, get_weeks, get_xml @transaction.atomic @@ -32,7 +32,7 @@ def process_timetable_week(timetable, soup, weeks_in_soup, force, year=None, wee if force: today = begin else: - today = timezone.make_aware(datetime.datetime.now()) + today = tz_now() # On récupère la mise à jour la plus ancienne dans les cours de l’emploi du temps # commençant à partir de maintenant @@ -107,16 +107,16 @@ class Command(BaseCommand): if options["all"]: weeks = None elif options["week"] is None: - _, week, day = timezone.now().isocalendar() + _, week, day = tz_now().isocalendar() if day >= 6: - year, week, _ = (timezone.now() + datetime.timedelta(weeks=1)).isocalendar() + year, week, _ = (tz_now() + datetime.timedelta(weeks=1)).isocalendar() weeks = [week] else: weeks = options["week"] if not options["all"]: if options["year"] is None and year is None: - year = timezone.now().year + year = tz_now().year elif year is None: year = options["year"][0] diff --git a/tests.py b/tests.py index 1d23620..be7b884 100644 --- a/tests.py +++ b/tests.py @@ -17,11 +17,12 @@ from django.test import TestCase from django.utils import timezone from .models import Course, Group, Timetable, Year +from .utils import tz_now class CourseTestCase(TestCase): def setUp(self): - dt = timezone.now() + dt = tz_now() self.year = Year(name="L2", slug="l2") self.year.save() diff --git a/utils.py b/utils.py index bd337e4..28a1ba2 100644 --- a/utils.py +++ b/utils.py @@ -19,12 +19,12 @@ import re from django.utils import timezone def get_current_week(): - return timezone.now().isocalendar()[:2] + return tz_now().isocalendar()[:2] def get_current_or_next_week(): - year, week, day = timezone.now().isocalendar() + year, week, day = tz_now().isocalendar() if day >= 6: - year, week, _ = (timezone.now() + datetime.timedelta(weeks=1)).isocalendar() + year, week, _ = (tz_now() + datetime.timedelta(weeks=1)).isocalendar() return year, week @@ -74,3 +74,7 @@ def parse_group(name): return parts[0], parts[5], parts[6], None elif parts[7] is not None: # Groupe de TP return parts[0], parts[8], parts[9], parts[10] + +def tz_now(): + """Retourne la date et l’heure avec le bon fuseau horaire""" + return timezone.make_aware(datetime.datetime.now()) -- cgit v1.2.1 From fbf65db250dbc86b04cfcf6175e4b167e1be23f0 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 18 Oct 2017 13:37:36 +0200 Subject: Suppression des imports inutiles --- management/commands/timetables.py | 1 - tests.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index 35fb26e..2d8a17e 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -18,7 +18,6 @@ import datetime from django.core.management.base import BaseCommand from django.db import transaction from django.db.models import Min -from django.utils import timezone from edt.models import Course, Timetable from edt.utils import get_week, tz_now diff --git a/tests.py b/tests.py index be7b884..a3475ff 100644 --- a/tests.py +++ b/tests.py @@ -14,8 +14,6 @@ # along with celcatsanitizer. If not, see . from django.test import TestCase -from django.utils import timezone - from .models import Course, Group, Timetable, Year from .utils import tz_now -- cgit v1.2.1 From e13b5bb8b08c336f61403480df6bbfbe29f2316b Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 14:04:41 +0200 Subject: Changement radical du fonctionnement de la regex en vue d’un gros changement de la structure de la base de données --- utils.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/utils.py b/utils.py index 28a1ba2..5d55eca 100644 --- a/utils.py +++ b/utils.py @@ -48,32 +48,24 @@ def group_courses(courses): def parse_group(name): # Explication de la regex # - # ^(.+?)\s+((CM(\w))|(TD(\w)(\d))|(TP(\w)(\d)(\d)))?(\s+\(.+\))?$ - # ^ début de la ligne - # (.+?) correspond à au moins un caractère - # \s+ 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 optionnel - # (\s+ un ou plusieurs espaces - # \(.+\)) un ou plusieurs caractères quelconques entre parenthèses - # ? groupe optionnel - # $ fin de la ligne - group_regex = re.compile(r"^(.+?)\s+((CM(\w))|(TD(\w)(\d))|(TP(\w)(\d)(\d)))?(\s+\(.+\))?$") + # ^(.+?)\s*(s\d\s+)?((CM|TD|TP|G)(\w\d{0,3}))?(\s+\(.+\))?$ + # ^ début de la ligne + # (.+?) correspond à au moins un caractère + # \s* éventuellement un ou plusieurs espaces + # (s\d\s+)? éventuellement un s suivi d’un nombre et d’un ou plusieurs espaces + # ((CM|TD|TP|G) « CM » ou « TD » ou « TP » ou « G » + # (\w\d{0,3}) suivi d’un caractère puis entre 0 et 3 chiffres + # )? groupe optionnel + # (\s+ un ou plusieurs espaces + # \(.+\))? un ou pliseurs caractères entre parenthèses + # $ fin de la ligne + group_regex = re.compile(r"^(.+?)\s*(s\d\s+)?((CM|TD|TP|G)(\w\d{0,3}))?(\s+\(.+\))?$") search = group_regex.search(name) if search is None: - return name, None, None, None + return name, None parts = search.groups() - if parts[1] is None: # Pas de groupe précis indiqué - return parts[0], None, None, None - elif parts[2] is not None: # Groupe de CM - return parts[0], parts[3], None, None - elif parts[4] is not None: # Groupe de TD - return parts[0], parts[5], parts[6], None - elif parts[7] is not None: # Groupe de TP - return parts[0], parts[8], parts[9], parts[10] + return parts[0], parts[4] def tz_now(): """Retourne la date et l’heure avec le bon fuseau horaire""" -- cgit v1.2.1 From 781ba0419a5e7bba423c922180c2c2db2327543a Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 19:04:23 +0200 Subject: Suppressions des champs td, tp et parents --- models.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/models.py b/models.py index fbbf877..bfc026b 100644 --- a/models.py +++ b/models.py @@ -92,11 +92,6 @@ class Group(models.Model): mention = models.CharField(max_length=128) 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 = models.ForeignKey("self", verbose_name="groupe parent", null=True, - default=None, related_name="children") - slug = models.SlugField(max_length=64, default="") hidden = models.BooleanField(verbose_name="caché", default=False) @@ -110,7 +105,7 @@ class Group(models.Model): @property def group_info(self): - return self.timetable.id, self.mention, self.subgroup, self.td, self.tp + return self.timetable.id, self.mention, self.subgroup def __str__(self): return self.name @@ -120,12 +115,12 @@ class Group(models.Model): self.name = self.celcat_name self.slug = slugify(self.name) - self.mention, self.subgroup, self.td, self.tp = parse_group(self.name) + self.mention, self.subgroup = parse_group(self.name) super(Group, self).save() class Meta: - index_together = ("mention", "subgroup", "td", "tp",) + index_together = ("mention", "subgroup",) unique_together = (("name", "timetable",), ("celcat_name", "timetable",), ("slug", "timetable",),) -- cgit v1.2.1 From e983619333a715bbc35deecf019a95addb5b6009 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 21:38:40 +0200 Subject: Suppression des champs retirés de l’interface d’administration. La longueur du champ sous-groupe est maintenant fixée à 16. --- admin.py | 4 ++-- models.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/admin.py b/admin.py index e17948a..c93b6b3 100644 --- a/admin.py +++ b/admin.py @@ -44,12 +44,12 @@ class TimetableAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin): fieldsets = ( (None, {"fields": ("name", "celcat_name", "timetable", "hidden",)}), - ("Groupes", {"fields": ("mention", "subgroup", "td", "tp", "parent",)}),) + ("Groupes", {"fields": ("mention", "subgroup",)}),) list_display = ("name", "timetable", "hidden",) list_editable = ("hidden",) list_filter = ("timetable",) ordering = ("timetable",) - readonly_fields = ("celcat_name", "mention", "subgroup", "td", "tp",) + readonly_fields = ("celcat_name", "mention",) actions = (make_hidden, make_visible,) diff --git a/models.py b/models.py index bfc026b..868cbd1 100644 --- a/models.py +++ b/models.py @@ -90,8 +90,9 @@ class Group(models.Model): verbose_name="emploi du temps") mention = models.CharField(max_length=128) - subgroup = models.CharField(max_length=1, verbose_name="sous-groupe", + subgroup = models.CharField(max_length=16, verbose_name="sous-groupe", null=True) + slug = models.SlugField(max_length=64, default="") hidden = models.BooleanField(verbose_name="caché", default=False) -- cgit v1.2.1 From 9efb1796aed00f4c7a8c9cd7e6b8a169318b1be0 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 21:48:39 +0200 Subject: Adaptation de corresponds_to() et des tests de parsage --- models.py | 11 +++++++---- tests.py | 12 ++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/models.py b/models.py index 868cbd1..c69bec3 100644 --- a/models.py +++ b/models.py @@ -97,12 +97,15 @@ class Group(models.Model): hidden = models.BooleanField(verbose_name="caché", default=False) - def corresponds_to(self, timetable_id, mention, subgroup, td, tp): + def corresponds_to(self, timetable_id, mention, subgroup): + subgroup_corresponds = True + if self.subgroup is not None and subgroup is not None: + subgroup_corresponds = subgroup.startswith(self.subgroup) or \ + self.subgroup.startswith(subgroup) + 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) + subgroup_corresponds @property def group_info(self): diff --git a/tests.py b/tests.py index a3475ff..051c733 100644 --- a/tests.py +++ b/tests.py @@ -128,10 +128,10 @@ class GroupTestCase(TestCase): 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)) - self.assertEqual(tpa21.group_info, (self.timetable.id, "L1 info s2", "A", 2, 1)) + self.assertEqual(cma.group_info, (self.timetable.id, "L1 info", "A")) + self.assertEqual(tda2.group_info, (self.timetable.id, "L1 info", "A2")) + self.assertEqual(tpa21.group_info, (self.timetable.id, "L1 info", "A21")) - self.assertEqual(cmb.group_info, (self.timetable.id, "L1 info s2", "B", None, None)) - self.assertEqual(tdb2.group_info, (self.timetable.id, "L1 info s2", "B", 2, None)) - self.assertEqual(tpb21.group_info, (self.timetable.id, "L1 info s2", "B", 2, 1)) + self.assertEqual(cmb.group_info, (self.timetable.id, "L1 info", "B")) + self.assertEqual(tdb2.group_info, (self.timetable.id, "L1 info", "B2")) + self.assertEqual(tpb21.group_info, (self.timetable.id, "L1 info", "B21")) -- cgit v1.2.1 From acc4475efca6326f59673d88865eecbef0511755 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 22:14:28 +0200 Subject: Changement de la logique de récupération des cours pour un groupe adaptée à la nouvelle structure du modèle Group. --- models.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/models.py b/models.py index c69bec3..fe285ac 100644 --- a/models.py +++ b/models.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . +from functools import reduce + from django.db import models from django.db.models import Count, Manager, Q from django.db.models.functions import ExtractWeek, ExtractYear @@ -147,17 +149,13 @@ class Room(models.Model): class CourseManager(Manager): def get_courses_for_group(self, group, **criteria): - groups_criteria = [] - if group.subgroup is not None: - groups_criteria.append(Q(groups__subgroup__isnull=True) | \ - Q(groups__subgroup=group.subgroup)) - if group.td is not None: - groups_criteria.append(Q(groups__td__isnull=True) | Q(groups__td=group.td)) - if group.tp is not None: - groups_criteria.append(Q(groups__tp__isnull=True) | Q(groups__tp=group.tp)) + groups_criteria = reduce(lambda x, y: x | y, + [Q(groups__subgroup=group.subgroup[:i]) + for i in range(1, len(group.subgroup))]) | \ + Q(groups__subgroup__isnull=True) return self.get_queryset() \ - .filter(*groups_criteria, + .filter(groups_criteria, groups__mention=group.mention, timetable=group.timetable, **criteria) \ .order_by("begin") -- cgit v1.2.1 From e7df75f46aec90652c2e501cfacdd31a7be5be4a Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 22:41:47 +0200 Subject: Suppression de l’étape de consolidation --- management/commands/_private.py | 40 +--------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/management/commands/_private.py b/management/commands/_private.py index e7f0a3c..f28512f 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -30,42 +30,6 @@ def add_time(date, time): delta = datetime.timedelta(hours=ptime.hour, minutes=ptime.minute) return date + delta -def consolidate_group(group): - group_content_key = ("mention", "subgroup", "td", "tp") - group_content_list = group.group_info[1:] - - if group.subgroup is not None: - 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 - - group.parent = Group.objects.filter(timetable=group.timetable, - **group_content).first() - group.save() - - if group.tp is None: - group_content = dict(zip(group_content_key, group_content_list)) - last_is_none = False - - for i, key in enumerate(group_content_key): - if group_content_list[i] is None or last_is_none: - del group_content[key] - group_content[key + "__isnull"] = last_is_none - last_is_none = True - - Group.objects.filter(timetable=group.timetable, parent__isnull=True, - **group_content).update(parent=group) - -def consolidate_groups(groups): - for group in groups: - if group.parent is None: - consolidate_group(group) - def delete_courses_in_week(timetable, year, week, today): start, end = get_week(year, week) Course.objects.filter(begin__gte=max(start, today), begin__lt=end, @@ -96,12 +60,10 @@ def get_event(timetable, event, event_week, today): # Création de l’objet cours course = Course.objects.create(timetable=timetable, begin=begin, end=end) - # On récupère les groupes concernés par les cours, on les - # « consolide », puis on les insère dans l’objet cours. + # On récupère les groupes concernés par les cours 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) course.groups.add(*groups) # On récupère le champ « remarque » -- cgit v1.2.1 From 276dc9017b414404539b20f32bcc65959fd32adb Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 22:48:16 +0200 Subject: Adaptation des vues, correction du critère de sélection des groupes --- models.py | 2 +- views.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/models.py b/models.py index fe285ac..db9e262 100644 --- a/models.py +++ b/models.py @@ -151,7 +151,7 @@ class CourseManager(Manager): def get_courses_for_group(self, group, **criteria): groups_criteria = reduce(lambda x, y: x | y, [Q(groups__subgroup=group.subgroup[:i]) - for i in range(1, len(group.subgroup))]) | \ + for i in range(1, len(group.subgroup) + 1)]) | \ Q(groups__subgroup__isnull=True) return self.get_queryset() \ diff --git a/views.py b/views.py index 2100972..e5c89a2 100644 --- a/views.py +++ b/views.py @@ -39,13 +39,12 @@ def group_list_common(request, timetable, groups): groups_weeks = Course.objects.get_weeks(begin__gte=start, begin__lt=end, timetable=timetable) \ .values("groups__mention", "groups__subgroup", - "groups__td", "groups__tp", "year", "week") + "year", "week") for group in groups: for group_week in groups_weeks: if group.corresponds_to(timetable.id, group_week["groups__mention"], - group_week["groups__subgroup"], group_week["groups__td"], - group_week["groups__tp"]): + group_week["groups__subgroup"]): if not hasattr(group, "weeks"): group.weeks = [] @@ -60,7 +59,7 @@ def group_list_common(request, timetable, groups): def group_list(request, year_slug, timetable_slug): timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) - groups = Group.objects.get_relevant_groups(timetable=timetable).order_by("name") + groups = Group.objects.filter(timetable=timetable).order_by("name") return group_list_common(request, timetable, groups) def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=None): @@ -78,8 +77,8 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) group = get_object_or_404(Group, slug=group_slug, timetable=timetable) - if group.children.count(): - return group_list_common(request, timetable, Group.objects.get_relevant_children(group)) + # if group.children.count(): + # return group_list_common(request, timetable, Group.objects.get_relevant_children(group)) courses = Course.objects.get_courses_for_group(group, begin__gte=start, begin__lt=end) if courses.count() == 0 and provided_week: -- cgit v1.2.1 From 07e9a1d1e6c77d64e5c3daf0324ae751117f168e Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 20 Oct 2017 13:29:07 +0200 Subject: Modification de la requête qui sélectionne les groupes pertinents. --- models.py | 21 +++++++++------------ views.py | 9 ++++++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/models.py b/models.py index db9e262..74fab9e 100644 --- a/models.py +++ b/models.py @@ -16,7 +16,8 @@ from functools import reduce from django.db import models -from django.db.models import Count, Manager, Q +from django.db.models import Count, Manager, Q, Subquery +from django.db.models.expressions import OuterRef from django.db.models.functions import ExtractWeek, ExtractYear from django.utils import timezone from django.utils.text import slugify @@ -69,17 +70,13 @@ class Timetable(SlugModel): class GroupManager(Manager): - def get_relevant_children(self, group): - parent_in = self.get_queryset().filter(parent=group) - return self.get_queryset().filter(Q(parent=group) | Q(parent__in=parent_in)) \ - .annotate(children_count=Count("children")) \ - .filter(children_count=0, hidden=False) \ - .order_by("name") - - def get_relevant_groups(self, *args, **criteria): - return self.get_queryset().filter(*args, **criteria) \ - .annotate(children_count=Count("children")) \ - .filter(children_count=0, hidden=False) + def get_relevant_groups(self, timetable, *args, **criteria): + sub = Group.objects.filter(timetable=timetable,mention=OuterRef("mention"), + subgroup__startswith=OuterRef("subgroup")) \ + .order_by().values("mention").annotate(c=Count("*")).values("c") + return Group.objects.filter(*args, timetable=timetable, hidden=False, **criteria) \ + .annotate(nbsub=Subquery(sub, output_field=models.IntegerField())) \ + .filter(nbsub=1).order_by("name") class Group(models.Model): diff --git a/views.py b/views.py index e5c89a2..9c38524 100644 --- a/views.py +++ b/views.py @@ -59,7 +59,7 @@ def group_list_common(request, timetable, groups): def group_list(request, year_slug, timetable_slug): timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) - groups = Group.objects.filter(timetable=timetable).order_by("name") + groups = Group.objects.get_relevant_groups(timetable) return group_list_common(request, timetable, groups) def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=None): @@ -77,8 +77,11 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) group = get_object_or_404(Group, slug=group_slug, timetable=timetable) - # if group.children.count(): - # return group_list_common(request, timetable, Group.objects.get_relevant_children(group)) + if Group.objects.filter(timetable=timetable, mention=group.mention, + subgroup__startswith=group.subgroup).count() > 1: + subgroups = Group.objects.get_relevant_groups(timetable, mention=group.mention, + subgroup__startswith=group.subgroup) + return group_list_common(request, timetable, subgroups) courses = Course.objects.get_courses_for_group(group, begin__gte=start, begin__lt=end) if courses.count() == 0 and provided_week: -- cgit v1.2.1 From 3830f9c786f1a790d67958b0a6d38bea75679159 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 22 Oct 2017 12:43:23 +0200 Subject: Le parseur de cours récupère à nouveau le champ « remarque ». Pour récupérer ce champ, le parseur est censé vérifier que la valeur existe dans un cours, sinon il ne faisait rien. Sauf que depuis un moment (commit 72dcae5c), la valeur qu’il vérifiait était celle de l’objet en cours de création, forcément nul, et non pas celle du XML. --- management/commands/_private.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/commands/_private.py b/management/commands/_private.py index e7f0a3c..a9d283d 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -105,7 +105,7 @@ def get_event(timetable, event, event_week, today): course.groups.add(*groups) # On récupère le champ « remarque » - if course.notes is not None: + if event.notes is not None: course.notes = event.notes.text # On récupère le champ « nom » -- cgit v1.2.1 From 00bd85d3d19ad829566835d20cb04fecf324a6c1 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 18 Oct 2017 13:37:36 +0200 Subject: Suppression des imports inutiles --- management/commands/timetables.py | 1 - tests.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index 35fb26e..2d8a17e 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -18,7 +18,6 @@ import datetime from django.core.management.base import BaseCommand from django.db import transaction from django.db.models import Min -from django.utils import timezone from edt.models import Course, Timetable from edt.utils import get_week, tz_now diff --git a/tests.py b/tests.py index be7b884..a3475ff 100644 --- a/tests.py +++ b/tests.py @@ -14,8 +14,6 @@ # along with celcatsanitizer. If not, see . from django.test import TestCase -from django.utils import timezone - from .models import Course, Group, Timetable, Year from .utils import tz_now -- cgit v1.2.1 From ff0e5d6388e921ddab2c05cafb27dd662595dee0 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 14:04:41 +0200 Subject: Changement radical du fonctionnement de la regex en vue d’un gros changement de la structure de la base de données --- utils.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/utils.py b/utils.py index 28a1ba2..5d55eca 100644 --- a/utils.py +++ b/utils.py @@ -48,32 +48,24 @@ def group_courses(courses): def parse_group(name): # Explication de la regex # - # ^(.+?)\s+((CM(\w))|(TD(\w)(\d))|(TP(\w)(\d)(\d)))?(\s+\(.+\))?$ - # ^ début de la ligne - # (.+?) correspond à au moins un caractère - # \s+ 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 optionnel - # (\s+ un ou plusieurs espaces - # \(.+\)) un ou plusieurs caractères quelconques entre parenthèses - # ? groupe optionnel - # $ fin de la ligne - group_regex = re.compile(r"^(.+?)\s+((CM(\w))|(TD(\w)(\d))|(TP(\w)(\d)(\d)))?(\s+\(.+\))?$") + # ^(.+?)\s*(s\d\s+)?((CM|TD|TP|G)(\w\d{0,3}))?(\s+\(.+\))?$ + # ^ début de la ligne + # (.+?) correspond à au moins un caractère + # \s* éventuellement un ou plusieurs espaces + # (s\d\s+)? éventuellement un s suivi d’un nombre et d’un ou plusieurs espaces + # ((CM|TD|TP|G) « CM » ou « TD » ou « TP » ou « G » + # (\w\d{0,3}) suivi d’un caractère puis entre 0 et 3 chiffres + # )? groupe optionnel + # (\s+ un ou plusieurs espaces + # \(.+\))? un ou pliseurs caractères entre parenthèses + # $ fin de la ligne + group_regex = re.compile(r"^(.+?)\s*(s\d\s+)?((CM|TD|TP|G)(\w\d{0,3}))?(\s+\(.+\))?$") search = group_regex.search(name) if search is None: - return name, None, None, None + return name, None parts = search.groups() - if parts[1] is None: # Pas de groupe précis indiqué - return parts[0], None, None, None - elif parts[2] is not None: # Groupe de CM - return parts[0], parts[3], None, None - elif parts[4] is not None: # Groupe de TD - return parts[0], parts[5], parts[6], None - elif parts[7] is not None: # Groupe de TP - return parts[0], parts[8], parts[9], parts[10] + return parts[0], parts[4] def tz_now(): """Retourne la date et l’heure avec le bon fuseau horaire""" -- cgit v1.2.1 From 044e7412856aeefa3c82b39130aa8df6582a0826 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 19:04:23 +0200 Subject: Suppressions des champs td, tp et parents --- models.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/models.py b/models.py index fbbf877..bfc026b 100644 --- a/models.py +++ b/models.py @@ -92,11 +92,6 @@ class Group(models.Model): mention = models.CharField(max_length=128) 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 = models.ForeignKey("self", verbose_name="groupe parent", null=True, - default=None, related_name="children") - slug = models.SlugField(max_length=64, default="") hidden = models.BooleanField(verbose_name="caché", default=False) @@ -110,7 +105,7 @@ class Group(models.Model): @property def group_info(self): - return self.timetable.id, self.mention, self.subgroup, self.td, self.tp + return self.timetable.id, self.mention, self.subgroup def __str__(self): return self.name @@ -120,12 +115,12 @@ class Group(models.Model): self.name = self.celcat_name self.slug = slugify(self.name) - self.mention, self.subgroup, self.td, self.tp = parse_group(self.name) + self.mention, self.subgroup = parse_group(self.name) super(Group, self).save() class Meta: - index_together = ("mention", "subgroup", "td", "tp",) + index_together = ("mention", "subgroup",) unique_together = (("name", "timetable",), ("celcat_name", "timetable",), ("slug", "timetable",),) -- cgit v1.2.1 From 05b8c81e21dbd33680683238ee044d20a9882a04 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 21:38:40 +0200 Subject: Suppression des champs retirés de l’interface d’administration. La longueur du champ sous-groupe est maintenant fixée à 16. --- admin.py | 4 ++-- models.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/admin.py b/admin.py index e17948a..c93b6b3 100644 --- a/admin.py +++ b/admin.py @@ -44,12 +44,12 @@ class TimetableAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin): fieldsets = ( (None, {"fields": ("name", "celcat_name", "timetable", "hidden",)}), - ("Groupes", {"fields": ("mention", "subgroup", "td", "tp", "parent",)}),) + ("Groupes", {"fields": ("mention", "subgroup",)}),) list_display = ("name", "timetable", "hidden",) list_editable = ("hidden",) list_filter = ("timetable",) ordering = ("timetable",) - readonly_fields = ("celcat_name", "mention", "subgroup", "td", "tp",) + readonly_fields = ("celcat_name", "mention",) actions = (make_hidden, make_visible,) diff --git a/models.py b/models.py index bfc026b..868cbd1 100644 --- a/models.py +++ b/models.py @@ -90,8 +90,9 @@ class Group(models.Model): verbose_name="emploi du temps") mention = models.CharField(max_length=128) - subgroup = models.CharField(max_length=1, verbose_name="sous-groupe", + subgroup = models.CharField(max_length=16, verbose_name="sous-groupe", null=True) + slug = models.SlugField(max_length=64, default="") hidden = models.BooleanField(verbose_name="caché", default=False) -- cgit v1.2.1 From f033f310ac8331de24a5fc2028ba22978c1d941e Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 21:48:39 +0200 Subject: Adaptation de corresponds_to() et des tests de parsage --- models.py | 11 +++++++---- tests.py | 12 ++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/models.py b/models.py index 868cbd1..c69bec3 100644 --- a/models.py +++ b/models.py @@ -97,12 +97,15 @@ class Group(models.Model): hidden = models.BooleanField(verbose_name="caché", default=False) - def corresponds_to(self, timetable_id, mention, subgroup, td, tp): + def corresponds_to(self, timetable_id, mention, subgroup): + subgroup_corresponds = True + if self.subgroup is not None and subgroup is not None: + subgroup_corresponds = subgroup.startswith(self.subgroup) or \ + self.subgroup.startswith(subgroup) + 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) + subgroup_corresponds @property def group_info(self): diff --git a/tests.py b/tests.py index a3475ff..051c733 100644 --- a/tests.py +++ b/tests.py @@ -128,10 +128,10 @@ class GroupTestCase(TestCase): 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)) - self.assertEqual(tpa21.group_info, (self.timetable.id, "L1 info s2", "A", 2, 1)) + self.assertEqual(cma.group_info, (self.timetable.id, "L1 info", "A")) + self.assertEqual(tda2.group_info, (self.timetable.id, "L1 info", "A2")) + self.assertEqual(tpa21.group_info, (self.timetable.id, "L1 info", "A21")) - self.assertEqual(cmb.group_info, (self.timetable.id, "L1 info s2", "B", None, None)) - self.assertEqual(tdb2.group_info, (self.timetable.id, "L1 info s2", "B", 2, None)) - self.assertEqual(tpb21.group_info, (self.timetable.id, "L1 info s2", "B", 2, 1)) + self.assertEqual(cmb.group_info, (self.timetable.id, "L1 info", "B")) + self.assertEqual(tdb2.group_info, (self.timetable.id, "L1 info", "B2")) + self.assertEqual(tpb21.group_info, (self.timetable.id, "L1 info", "B21")) -- cgit v1.2.1 From 5489101669d57c834c65f5a889a6d40ce3bcfb1c Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 22:14:28 +0200 Subject: Changement de la logique de récupération des cours pour un groupe adaptée à la nouvelle structure du modèle Group. --- models.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/models.py b/models.py index c69bec3..fe285ac 100644 --- a/models.py +++ b/models.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . +from functools import reduce + from django.db import models from django.db.models import Count, Manager, Q from django.db.models.functions import ExtractWeek, ExtractYear @@ -147,17 +149,13 @@ class Room(models.Model): class CourseManager(Manager): def get_courses_for_group(self, group, **criteria): - groups_criteria = [] - if group.subgroup is not None: - groups_criteria.append(Q(groups__subgroup__isnull=True) | \ - Q(groups__subgroup=group.subgroup)) - if group.td is not None: - groups_criteria.append(Q(groups__td__isnull=True) | Q(groups__td=group.td)) - if group.tp is not None: - groups_criteria.append(Q(groups__tp__isnull=True) | Q(groups__tp=group.tp)) + groups_criteria = reduce(lambda x, y: x | y, + [Q(groups__subgroup=group.subgroup[:i]) + for i in range(1, len(group.subgroup))]) | \ + Q(groups__subgroup__isnull=True) return self.get_queryset() \ - .filter(*groups_criteria, + .filter(groups_criteria, groups__mention=group.mention, timetable=group.timetable, **criteria) \ .order_by("begin") -- cgit v1.2.1 From a7f533b2ddedf69a79d7ec68200ece5bedd59b2d Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 22:41:47 +0200 Subject: Suppression de l’étape de consolidation --- management/commands/_private.py | 40 +--------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/management/commands/_private.py b/management/commands/_private.py index a9d283d..b663454 100644 --- a/management/commands/_private.py +++ b/management/commands/_private.py @@ -30,42 +30,6 @@ def add_time(date, time): delta = datetime.timedelta(hours=ptime.hour, minutes=ptime.minute) return date + delta -def consolidate_group(group): - group_content_key = ("mention", "subgroup", "td", "tp") - group_content_list = group.group_info[1:] - - if group.subgroup is not None: - 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 - - group.parent = Group.objects.filter(timetable=group.timetable, - **group_content).first() - group.save() - - if group.tp is None: - group_content = dict(zip(group_content_key, group_content_list)) - last_is_none = False - - for i, key in enumerate(group_content_key): - if group_content_list[i] is None or last_is_none: - del group_content[key] - group_content[key + "__isnull"] = last_is_none - last_is_none = True - - Group.objects.filter(timetable=group.timetable, parent__isnull=True, - **group_content).update(parent=group) - -def consolidate_groups(groups): - for group in groups: - if group.parent is None: - consolidate_group(group) - def delete_courses_in_week(timetable, year, week, today): start, end = get_week(year, week) Course.objects.filter(begin__gte=max(start, today), begin__lt=end, @@ -96,12 +60,10 @@ def get_event(timetable, event, event_week, today): # Création de l’objet cours course = Course.objects.create(timetable=timetable, begin=begin, end=end) - # On récupère les groupes concernés par les cours, on les - # « consolide », puis on les insère dans l’objet cours. + # On récupère les groupes concernés par les cours 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) course.groups.add(*groups) # On récupère le champ « remarque » -- cgit v1.2.1 From 86efcdcd2df04a59eb5f27212d8d41f60f861f6c Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 19 Oct 2017 22:48:16 +0200 Subject: Adaptation des vues, correction du critère de sélection des groupes --- models.py | 2 +- views.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/models.py b/models.py index fe285ac..db9e262 100644 --- a/models.py +++ b/models.py @@ -151,7 +151,7 @@ class CourseManager(Manager): def get_courses_for_group(self, group, **criteria): groups_criteria = reduce(lambda x, y: x | y, [Q(groups__subgroup=group.subgroup[:i]) - for i in range(1, len(group.subgroup))]) | \ + for i in range(1, len(group.subgroup) + 1)]) | \ Q(groups__subgroup__isnull=True) return self.get_queryset() \ diff --git a/views.py b/views.py index 2100972..e5c89a2 100644 --- a/views.py +++ b/views.py @@ -39,13 +39,12 @@ def group_list_common(request, timetable, groups): groups_weeks = Course.objects.get_weeks(begin__gte=start, begin__lt=end, timetable=timetable) \ .values("groups__mention", "groups__subgroup", - "groups__td", "groups__tp", "year", "week") + "year", "week") for group in groups: for group_week in groups_weeks: if group.corresponds_to(timetable.id, group_week["groups__mention"], - group_week["groups__subgroup"], group_week["groups__td"], - group_week["groups__tp"]): + group_week["groups__subgroup"]): if not hasattr(group, "weeks"): group.weeks = [] @@ -60,7 +59,7 @@ def group_list_common(request, timetable, groups): def group_list(request, year_slug, timetable_slug): timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) - groups = Group.objects.get_relevant_groups(timetable=timetable).order_by("name") + groups = Group.objects.filter(timetable=timetable).order_by("name") return group_list_common(request, timetable, groups) def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=None): @@ -78,8 +77,8 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) group = get_object_or_404(Group, slug=group_slug, timetable=timetable) - if group.children.count(): - return group_list_common(request, timetable, Group.objects.get_relevant_children(group)) + # if group.children.count(): + # return group_list_common(request, timetable, Group.objects.get_relevant_children(group)) courses = Course.objects.get_courses_for_group(group, begin__gte=start, begin__lt=end) if courses.count() == 0 and provided_week: -- cgit v1.2.1 From b7773b892ee68209feb33870496985f78243681a Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 20 Oct 2017 13:29:07 +0200 Subject: Modification de la requête qui sélectionne les groupes pertinents. --- models.py | 21 +++++++++------------ views.py | 9 ++++++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/models.py b/models.py index db9e262..74fab9e 100644 --- a/models.py +++ b/models.py @@ -16,7 +16,8 @@ from functools import reduce from django.db import models -from django.db.models import Count, Manager, Q +from django.db.models import Count, Manager, Q, Subquery +from django.db.models.expressions import OuterRef from django.db.models.functions import ExtractWeek, ExtractYear from django.utils import timezone from django.utils.text import slugify @@ -69,17 +70,13 @@ class Timetable(SlugModel): class GroupManager(Manager): - def get_relevant_children(self, group): - parent_in = self.get_queryset().filter(parent=group) - return self.get_queryset().filter(Q(parent=group) | Q(parent__in=parent_in)) \ - .annotate(children_count=Count("children")) \ - .filter(children_count=0, hidden=False) \ - .order_by("name") - - def get_relevant_groups(self, *args, **criteria): - return self.get_queryset().filter(*args, **criteria) \ - .annotate(children_count=Count("children")) \ - .filter(children_count=0, hidden=False) + def get_relevant_groups(self, timetable, *args, **criteria): + sub = Group.objects.filter(timetable=timetable,mention=OuterRef("mention"), + subgroup__startswith=OuterRef("subgroup")) \ + .order_by().values("mention").annotate(c=Count("*")).values("c") + return Group.objects.filter(*args, timetable=timetable, hidden=False, **criteria) \ + .annotate(nbsub=Subquery(sub, output_field=models.IntegerField())) \ + .filter(nbsub=1).order_by("name") class Group(models.Model): diff --git a/views.py b/views.py index e5c89a2..9c38524 100644 --- a/views.py +++ b/views.py @@ -59,7 +59,7 @@ def group_list_common(request, timetable, groups): def group_list(request, year_slug, timetable_slug): timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) - groups = Group.objects.filter(timetable=timetable).order_by("name") + groups = Group.objects.get_relevant_groups(timetable) return group_list_common(request, timetable, groups) def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=None): @@ -77,8 +77,11 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) group = get_object_or_404(Group, slug=group_slug, timetable=timetable) - # if group.children.count(): - # return group_list_common(request, timetable, Group.objects.get_relevant_children(group)) + if Group.objects.filter(timetable=timetable, mention=group.mention, + subgroup__startswith=group.subgroup).count() > 1: + subgroups = Group.objects.get_relevant_groups(timetable, mention=group.mention, + subgroup__startswith=group.subgroup) + return group_list_common(request, timetable, subgroups) courses = Course.objects.get_courses_for_group(group, begin__gte=start, begin__lt=end) if courses.count() == 0 and provided_week: -- cgit v1.2.1 From 12184b16e97d9f5c6c99d6bfe79b1e5c53464fb3 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 20 Oct 2017 17:46:37 +0200 Subject: Ajout d’un calendrier ICS spécifique pour un groupe qui ne prend pas en compte les parents --- feeds.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/feeds.py b/feeds.py index c2c5b14..66be12e 100644 --- a/feeds.py +++ b/feeds.py @@ -87,6 +87,11 @@ class IcalFeed(Feed): "location": format_rooms(item.rooms.all())} +class IcalOnlyOneFeed(IcalFeed): + def items(self, obj): + return Course.objects.filter(groups=obj).order_by("begin") + + class RSSFeed(Feed): def get_object(self, request, year_slug, timetable_slug, group_slug): year, week = get_current_or_next_week() -- cgit v1.2.1 From 827641acd0b2c64feb704bae989b139458a3d0ca Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 27 Oct 2017 09:37:25 +0200 Subject: Fonction pour récupérer les parents d’un groupe --- models.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/models.py b/models.py index 74fab9e..c546737 100644 --- a/models.py +++ b/models.py @@ -70,11 +70,21 @@ class Timetable(SlugModel): class GroupManager(Manager): + def get_parents(self, group): + groups_criteria = Q(subgroup__isnull=True) | \ + reduce(lambda x, y: x | y, + [Q(subgroup=group.subgroup[:i]) + for i in range(1, len(group.subgroup) + 1)]) + + return self.get_queryset().filter(groups_criteria, mention=group.mention, + timetable=group.timetable) + def get_relevant_groups(self, timetable, *args, **criteria): sub = Group.objects.filter(timetable=timetable,mention=OuterRef("mention"), subgroup__startswith=OuterRef("subgroup")) \ .order_by().values("mention").annotate(c=Count("*")).values("c") - return Group.objects.filter(*args, timetable=timetable, hidden=False, **criteria) \ + + return self.get_queryset().filter(*args, timetable=timetable, hidden=False, **criteria) \ .annotate(nbsub=Subquery(sub, output_field=models.IntegerField())) \ .filter(nbsub=1).order_by("name") @@ -146,15 +156,8 @@ class Room(models.Model): class CourseManager(Manager): def get_courses_for_group(self, group, **criteria): - groups_criteria = reduce(lambda x, y: x | y, - [Q(groups__subgroup=group.subgroup[:i]) - for i in range(1, len(group.subgroup) + 1)]) | \ - Q(groups__subgroup__isnull=True) - return self.get_queryset() \ - .filter(groups_criteria, - groups__mention=group.mention, - timetable=group.timetable, **criteria) \ + .filter(groups__in=Group.objects.get_parents(group), **criteria) \ .order_by("begin") def get_weeks(self, **criteria): -- cgit v1.2.1 From df3cb623a79d925e6af048bc88e607094c6b692e Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 27 Oct 2017 09:38:50 +0200 Subject: Ajout de l’url pour avoir un calendrier correspondant seulement à un groupe. --- urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/urls.py b/urls.py index 58cf019..d269400 100644 --- a/urls.py +++ b/urls.py @@ -23,6 +23,7 @@ urlpatterns = [ url(r"^(?P[-\w]+)/(?P[-\w]+)/$", views.group_list, name="groups"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/$", views.timetable, name="timetable"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendar.ics$", feeds.IcalFeed(), name="ics"), + url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendar-group.ics$", feeds.IcalOnlyOneFeed(), name="ics-group"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/feed.atom$", feeds.AtomFeed(), name="atom"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/feed.rss$", feeds.RSSFeed(), name="rss"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/(?P[0-9]{4})/(?P[0-4]?[0-9]|5[0-3])/$", views.timetable, name="timetable"), -- cgit v1.2.1 From 7ef57a0226fa25b8ea5e9a6ff5526889dc300649 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 27 Oct 2017 21:51:12 +0200 Subject: Ajout d’une page pour lister les calendriers disponibles --- templates/index.html | 6 +++--- urls.py | 1 + views.py | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/templates/index.html b/templates/index.html index 005fd5b..c47a0be 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,7 @@ {% block head %}{% endblock %} - {% block title %}{% if year %}{{ year }} – {% endif %}{% endblock %}celcatsanitizer + {% block title %}{% if year %}{{ year }}{% elif group %}{{ group }}{% endif %}{% if year or group %} – {% endif %}{% endblock %}celcatsanitizer @@ -13,10 +13,10 @@
{% block body %} -

{% if year %}{{ year }} – Choisissez votre mention{% else %}Choisissez votre année{% endif %}

+

{% if year %}{{ year }} – Choisissez votre mention{% elif group %}Calendriers disponibles pour le groupe {{ group }}{% else %}Choisissez votre année{% endif %}

    {% for element in elements %} -
  • {{ element }}
  • +
  • {{ element }}
  • {% empty %}

    Aucun emploi du temps à afficher

    {% endfor %} diff --git a/urls.py b/urls.py index d269400..b30e8d9 100644 --- a/urls.py +++ b/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ url(r"^(?P[-\w]+)/$", views.mention_list, name="mentions"), url(r"^(?P[-\w]+)/(?P[-\w]+)/$", views.group_list, name="groups"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/$", views.timetable, name="timetable"), + url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendars$", views.calendar_list, name="calendar-list"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendar.ics$", feeds.IcalFeed(), name="ics"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendar-group.ics$", feeds.IcalOnlyOneFeed(), name="ics-group"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/feed.atom$", feeds.AtomFeed(), name="atom"), diff --git a/views.py b/views.py index 9c38524..76b7127 100644 --- a/views.py +++ b/views.py @@ -17,6 +17,7 @@ import datetime from django.conf import settings from django.db.models import Max +from django.db.models.functions import Length from django.http import Http404 from django.shortcuts import get_object_or_404, render @@ -95,5 +96,19 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No "year": year, "week": int(week), "is_old_timetable": is_old_timetable}) +def calendar_list(request, year_slug, timetable_slug, group_slug): + """Affiche une liste des ICS disponibles pour un groupe.""" + # On commence par récupérer l’emploi du temps associé, puis le groupe + # Si la récupération de l’un d’entre eux échoue, on affiche une erreur 404. + timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) + group = get_object_or_404(Group, slug=group_slug, timetable=timetable) + + # On récupère les groupes « parents » au groupe spécifié. + groups = Group.objects.get_parents(group).annotate(length=Length("subgroup")) \ + .order_by("length") + + # On réutilise encore la template principale + return render(request, "index.html", {"group": group, "elements": groups}) + def contact(request): return render(request, "contact.html", {"email": settings.ADMINS[0][1]}) -- cgit v1.2.1 From 7b66e90a4ff559323bd0664b88e0cdefa0f38322 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 27 Oct 2017 21:56:04 +0200 Subject: Ajout d’un lien pour aller sur la liste des ICS séparés --- templates/timetable.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/timetable.html b/templates/timetable.html index e4dffe7..b009174 100644 --- a/templates/timetable.html +++ b/templates/timetable.html @@ -18,4 +18,4 @@ {% if last_update %}Dernière mise à jour le {{ last_update|date:"l j F o" }} à {{ last_update|date:"H:i" }}{% endif %}

    {% include "timetable_common.html" %} - {% endblock %} + {% endblock %} -- cgit v1.2.1 From f0790a2420684a39e2b48922beec1245f4446912 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 29 Oct 2017 15:17:03 +0100 Subject: Revert "Ajout d’une page pour lister les calendriers disponibles" This reverts commit 7ef57a0226fa25b8ea5e9a6ff5526889dc300649. --- templates/index.html | 6 +++--- urls.py | 1 - views.py | 15 --------------- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/templates/index.html b/templates/index.html index c47a0be..005fd5b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,7 @@ {% block head %}{% endblock %} - {% block title %}{% if year %}{{ year }}{% elif group %}{{ group }}{% endif %}{% if year or group %} – {% endif %}{% endblock %}celcatsanitizer + {% block title %}{% if year %}{{ year }} – {% endif %}{% endblock %}celcatsanitizer @@ -13,10 +13,10 @@
    {% block body %} -

    {% if year %}{{ year }} – Choisissez votre mention{% elif group %}Calendriers disponibles pour le groupe {{ group }}{% else %}Choisissez votre année{% endif %}

    +

    {% if year %}{{ year }} – Choisissez votre mention{% else %}Choisissez votre année{% endif %}

      {% for element in elements %} -
    • {{ element }}
    • +
    • {{ element }}
    • {% empty %}

      Aucun emploi du temps à afficher

      {% endfor %} diff --git a/urls.py b/urls.py index b30e8d9..d269400 100644 --- a/urls.py +++ b/urls.py @@ -22,7 +22,6 @@ urlpatterns = [ url(r"^(?P[-\w]+)/$", views.mention_list, name="mentions"), url(r"^(?P[-\w]+)/(?P[-\w]+)/$", views.group_list, name="groups"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/$", views.timetable, name="timetable"), - url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendars$", views.calendar_list, name="calendar-list"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendar.ics$", feeds.IcalFeed(), name="ics"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendar-group.ics$", feeds.IcalOnlyOneFeed(), name="ics-group"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/feed.atom$", feeds.AtomFeed(), name="atom"), diff --git a/views.py b/views.py index 76b7127..9c38524 100644 --- a/views.py +++ b/views.py @@ -17,7 +17,6 @@ import datetime from django.conf import settings from django.db.models import Max -from django.db.models.functions import Length from django.http import Http404 from django.shortcuts import get_object_or_404, render @@ -96,19 +95,5 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No "year": year, "week": int(week), "is_old_timetable": is_old_timetable}) -def calendar_list(request, year_slug, timetable_slug, group_slug): - """Affiche une liste des ICS disponibles pour un groupe.""" - # On commence par récupérer l’emploi du temps associé, puis le groupe - # Si la récupération de l’un d’entre eux échoue, on affiche une erreur 404. - timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) - group = get_object_or_404(Group, slug=group_slug, timetable=timetable) - - # On récupère les groupes « parents » au groupe spécifié. - groups = Group.objects.get_parents(group).annotate(length=Length("subgroup")) \ - .order_by("length") - - # On réutilise encore la template principale - return render(request, "index.html", {"group": group, "elements": groups}) - def contact(request): return render(request, "contact.html", {"email": settings.ADMINS[0][1]}) -- cgit v1.2.1 From c80a41ffafe3065f35f07b71063b5596d33d0ff2 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 29 Oct 2017 15:37:43 +0100 Subject: Déplacement de tous les liens vers les ICS dans une page spéciale --- templates/calendars.html | 13 +++++++++++++ templates/timetable.html | 2 +- urls.py | 1 + views.py | 9 +++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 templates/calendars.html diff --git a/templates/calendars.html b/templates/calendars.html new file mode 100644 index 0000000..70f3dac --- /dev/null +++ b/templates/calendars.html @@ -0,0 +1,13 @@ +{% extends "index.html" %} + +{% block title %}Calendriers disponibles pour le groupe {{ group }} – {% endblock %} + +{% block body %} +

      Calendriers disponibles pour le groupe {{ group }}

      + +{% endblock %} diff --git a/templates/timetable.html b/templates/timetable.html index b009174..fc2065f 100644 --- a/templates/timetable.html +++ b/templates/timetable.html @@ -18,4 +18,4 @@ {% if last_update %}Dernière mise à jour le {{ last_update|date:"l j F o" }} à {{ last_update|date:"H:i" }}{% endif %}

      {% include "timetable_common.html" %} - {% endblock %} + {% endblock %} diff --git a/urls.py b/urls.py index d269400..c84ac75 100644 --- a/urls.py +++ b/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ url(r"^(?P[-\w]+)/$", views.mention_list, name="mentions"), url(r"^(?P[-\w]+)/(?P[-\w]+)/$", views.group_list, name="groups"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/$", views.timetable, name="timetable"), + url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendars", views.calendars, name="calendars"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendar.ics$", feeds.IcalFeed(), name="ics"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/calendar-group.ics$", feeds.IcalOnlyOneFeed(), name="ics-group"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/feed.atom$", feeds.AtomFeed(), name="atom"), diff --git a/views.py b/views.py index 9c38524..7c19840 100644 --- a/views.py +++ b/views.py @@ -17,6 +17,7 @@ import datetime from django.conf import settings from django.db.models import Max +from django.db.models.functions import Length from django.http import Http404 from django.shortcuts import get_object_or_404, render @@ -95,5 +96,13 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No "year": year, "week": int(week), "is_old_timetable": is_old_timetable}) +def calendars(request, year_slug, timetable_slug, group_slug): + group = get_object_or_404(Group, timetable__year__slug=year_slug, + timetable__slug=timetable_slug, slug=group_slug) + groups = Group.objects.get_parents(group).annotate(length=Length("subgroup")) \ + .order_by("length") + + return render(request, "calendars.html", {"group": group, "groups": groups}) + def contact(request): return render(request, "contact.html", {"email": settings.ADMINS[0][1]}) -- cgit v1.2.1 From 5082e2aa0083a1c78cd39e86cb141786c2ab6efe Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 29 Oct 2017 15:41:40 +0100 Subject: Changement des texte de la page des calendriers --- templates/calendars.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/calendars.html b/templates/calendars.html index 70f3dac..d97ea78 100644 --- a/templates/calendars.html +++ b/templates/calendars.html @@ -1,13 +1,13 @@ {% extends "index.html" %} -{% block title %}Calendriers disponibles pour le groupe {{ group }} – {% endblock %} +{% block title %}ICS disponibles pour le groupe {{ group }} – {% endblock %} {% block body %} -

      Calendriers disponibles pour le groupe {{ group }}

      +

      ICS disponibles pour le groupe {{ group }}

      {% endblock %} -- cgit v1.2.1 From 03bf0f33ac47b2f5597c39b10d9f26d7fb893d79 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 29 Oct 2017 22:07:12 +0100 Subject: Création d’une template pour les flatpages Django --- templates/flatpages/default.html | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 templates/flatpages/default.html diff --git a/templates/flatpages/default.html b/templates/flatpages/default.html new file mode 100644 index 0000000..42f67db --- /dev/null +++ b/templates/flatpages/default.html @@ -0,0 +1,8 @@ +{% extends "index.html" %} + +{% block title %}{{ flatpage.title }} – {% endblock %} + +{% block body %} +

      {{ flatpage.title }}

      +

      {{ flatpage.content }}

      +{% endblock %} -- cgit v1.2.1 From eed022ccf449a4cfceb39c28bb41741bc490d343 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 30 Oct 2017 18:23:43 +0100 Subject: Ajout d’une URL pour les flatpages --- urls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/urls.py b/urls.py index c84ac75..1f686e4 100644 --- a/urls.py +++ b/urls.py @@ -13,11 +13,12 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . -from django.conf.urls import url +from django.conf.urls import include, url from . import feeds, views urlpatterns = [ url(r"^$", views.index, name="index"), + url(r"^pages/", include("django.contrib.flatpages.urls")), url(r"^contact$", views.contact, name="contact"), url(r"^(?P[-\w]+)/$", views.mention_list, name="mentions"), url(r"^(?P[-\w]+)/(?P[-\w]+)/$", views.group_list, name="groups"), -- cgit v1.2.1 From b68c59c72be3a8e3d3201dca1941879508afcb3b Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 30 Oct 2017 18:29:21 +0100 Subject: Liens vers les flatpages --- templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/index.html b/templates/index.html index 005fd5b..abaf5f1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -24,7 +24,7 @@ {% endblock %}
    -- cgit v1.2.1 From 55ad46fe32ce8122d884dda4eb498e20e39622ad Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 31 Oct 2017 12:38:34 +0100 Subject: Suppression de l’ancienne page de contact --- templates/contact.html | 9 --------- templates/flatpages/default.html | 2 +- templatetags/email.py | 24 ------------------------ urls.py | 1 - views.py | 4 ---- 5 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 templates/contact.html delete mode 100644 templatetags/email.py diff --git a/templates/contact.html b/templates/contact.html deleted file mode 100644 index 1359a16..0000000 --- a/templates/contact.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "index.html" %} -{% load email %} - -{% block title %}Contacter – {% endblock %} - -{% block body %} -

    Contacter

    -

    Pour contacter l’administrateur du service, envoyez un mail à l’adresse suivante :
    {{ email|format_email }}.

    -{% endblock %} diff --git a/templates/flatpages/default.html b/templates/flatpages/default.html index 42f67db..913cda9 100644 --- a/templates/flatpages/default.html +++ b/templates/flatpages/default.html @@ -4,5 +4,5 @@ {% block body %}

    {{ flatpage.title }}

    -

    {{ flatpage.content }}

    + {{ flatpage.content }} {% endblock %} diff --git a/templatetags/email.py b/templatetags/email.py deleted file mode 100644 index 68dbd84..0000000 --- a/templatetags/email.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (C) 2017 Alban Gruin -# -# celcatsanitizer is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# celcatsanitizer is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with celcatsanitizer. If not, see . - -from django import template - -register = template.Library() - -@register.filter -def format_email(address): - return address.replace("+", " [plus] ") \ - .replace("@", " [arobase] ") \ - .replace(".", " [point] ") diff --git a/urls.py b/urls.py index 1f686e4..487994e 100644 --- a/urls.py +++ b/urls.py @@ -19,7 +19,6 @@ from . import feeds, views urlpatterns = [ url(r"^$", views.index, name="index"), url(r"^pages/", include("django.contrib.flatpages.urls")), - url(r"^contact$", views.contact, name="contact"), url(r"^(?P[-\w]+)/$", views.mention_list, name="mentions"), url(r"^(?P[-\w]+)/(?P[-\w]+)/$", views.group_list, name="groups"), url(r"^(?P[-\w]+)/(?P[-\w]+)/(?P[-\w]+)/$", views.timetable, name="timetable"), diff --git a/views.py b/views.py index 7c19840..384fdd0 100644 --- a/views.py +++ b/views.py @@ -15,7 +15,6 @@ import datetime -from django.conf import settings from django.db.models import Max from django.db.models.functions import Length from django.http import Http404 @@ -103,6 +102,3 @@ def calendars(request, year_slug, timetable_slug, group_slug): .order_by("length") return render(request, "calendars.html", {"group": group, "groups": groups}) - -def contact(request): - return render(request, "contact.html", {"email": settings.ADMINS[0][1]}) -- cgit v1.2.1 From 3f610831227e7def4b74651747d5625c9a009311 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 31 Oct 2017 13:10:18 +0100 Subject: Création d’une flatpage spécifique à la page « à propos » Ajout d’un processeur de contexte pour afficher la version de CS en bas de page. --- templates/flatpages/about.html | 6 ++++++ templates/index.html | 3 ++- views.py | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 templates/flatpages/about.html diff --git a/templates/flatpages/about.html b/templates/flatpages/about.html new file mode 100644 index 0000000..3df7c53 --- /dev/null +++ b/templates/flatpages/about.html @@ -0,0 +1,6 @@ +{% extends "flatpages/default.html" %} + +{% block body %} +

    {{ flatpage.title }} – celcatsanitizer {{ celcatsanitizer_version }}

    + {{ flatpage.content }} +{% endblock %} diff --git a/templates/index.html b/templates/index.html index abaf5f1..1ea6e53 100644 --- a/templates/index.html +++ b/templates/index.html @@ -24,7 +24,8 @@ {% endblock %}
diff --git a/views.py b/views.py index 384fdd0..c0c63cb 100644 --- a/views.py +++ b/views.py @@ -23,6 +23,8 @@ from django.shortcuts import get_object_or_404, render from .models import Timetable, Group, Course, Year from .utils import get_current_week, get_current_or_next_week, get_week, group_courses +import edt + def index(request): years = Year.objects.order_by("name") return render(request, "index.html", {"elements": years}) @@ -102,3 +104,6 @@ def calendars(request, year_slug, timetable_slug, group_slug): .order_by("length") return render(request, "calendars.html", {"group": group, "groups": groups}) + +def ctx_processor(request): + return {"celcatsanitizer_version": edt.VERSION} -- cgit v1.2.1 From 70ad14a54a23744d10054a71d61a5be6df72780d Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 31 Oct 2017 13:26:58 +0100 Subject: Suppression d’une ligne inutile --- templates/index.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/index.html b/templates/index.html index 1ea6e53..71665bc 100644 --- a/templates/index.html +++ b/templates/index.html @@ -24,8 +24,7 @@ {% endblock %} -- cgit v1.2.1 From bafa5f07a4e1b5c2cc2bb234ad2f12929a983025 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 31 Oct 2017 14:31:36 +0100 Subject: Ajout des étapes de configuration nécessaire pour les flatpages. Suppression des étapes devenues inutiles. Corrections typographiques. --- README.md | 74 +++++++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 82e6053..20a2cbd 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,29 @@ # celcatsanitizer celcatsanitizer est un système qui permet de récupérer des emplois du temps Celcat au format XML pour les afficher correctement. -## Pourquoi ? +## Pourquoi ? Parce que les emplois du temps Celcat sont peu lisibles et peuvent facilement faire planter un navigateur, à cause du surplus d’informations affichées. -## Comment faire tourner celcatsanitizer chez moi ? -celcatsanitizer est écrit en Python 3. Il dépend des bibliothèques suivantes : +## Comment faire tourner celcatsanitizer chez moi ? +celcatsanitizer est écrit en Python 3. Il dépend des bibliothèques suivantes : * Django 1.11 * requests * BeautifulSoup4 * icalendar -Pour installer celcatsanitizer, il est possible d'utiliser git. +Pour installer celcatsanitizer, il est possible d’utiliser git. -Pour tester celcatsanitizer, il est recommandé d'utiliser SQLite ou PostgreSQL. +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. +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.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. +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. +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. -#### Création de l'environnement virtuel -Déplacez-vous dans le répertoire souhaité, installez l'environnement virtuel, et activez-le : +#### Création de l’environnement virtuel +Déplacez-vous dans le répertoire souhaité, installez l’environnement virtuel, et activez-le : > $ virtualenv -p python3 celcatsanitizer @@ -31,21 +31,19 @@ Déplacez-vous dans le répertoire souhaité, installez l'environnement virtuel, > $ source bin/activate -Il est possible que votre version de pip soit ancienne. Si vous le souhaitez, mettez ce programme à jour : +Il est possible que votre version de pip soit ancienne. Si vous le souhaitez, mettez ce programme à jour : > $ pip install --upgrade pip -Notez que cette étape n'est pas obligatoire - #### Installation des dépendances > $ pip install requests django beautifulsoup4 icalendar -Si vous utilisez PostgreSQL, vous allez avoir besoin du driver psycopg2 : +Si vous utilisez PostgreSQL, vous allez avoir besoin du driver psycopg2 : > $ pip install psycopg2 -SQLite n'a pas besoin de driver. +SQLite n’a pas besoin de driver. #### Création du répertoire Django @@ -57,7 +55,7 @@ SQLite n'a pas besoin de driver. > $ git clone https://git.pa1ch.fr/alban/celcatsanitizer.git edt -Pour la production, il est recommandé d'utiliser une version stable, accessibles à travers les tags git. +Pour la production, il est recommandé d’utiliser une version stable, accessibles à travers les tags git. #### Configuration de Django Dans le fichier celcatsanitizer/settings.py, vous devrez renseigner plusieurs informations. @@ -67,8 +65,8 @@ Dans le fichier celcatsanitizer/settings.py, vous devrez renseigner plusieurs in 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.11/topics/i18n/) +##### 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.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.11/ref/settings/#databases) @@ -76,30 +74,54 @@ Ce passage n'est pas obligatoire. [Vous pouvez retrouver la documentation de l'i ##### Configuration du mode de Django Si jamais vous utiliser Django en production, vous **devez** mettre la variable DEBUG à False. -##### Configuration personnalisée nécessaire à celcatsanitizer -celcatsanitizer a besoin d'une variable DEFAULT_DOMAIN qui contient l'URL de base de l'instance. - ##### Ajout de celcatsanitizer dans la liste des applications Django -Ajoutez la chaine de caractère "edt" à la fin de la liste INSTALLED_APPS +Ajoutez la chaine de caractère « edt » à la fin de la liste INSTALLED_APPS. + +##### Configuration des flatpages +celcatsanitizer utilise les flatpages pour rendre les pages « contact » et « à propos ». Vous pouvez retrouver le guide d’installation sur [le site de Django](https://docs.djangoproject.com/fr/1.11/ref/contrib/flatpages/#installation). Effectuez uniquement les deux premières étapes, celcatsanitizer enregistre déjà une route pour les pages statiques, et la commande de l’étape 4 sera effectuée plus loin. + +##### Ajout du processeur de contexte de celcatsanitizer +Cette étape est fortement recommandée, mais pas obligatoire. + +Rajoutez la chaine de caractères 'edt.views.ctx_processor' à la liste 'context_processors' dans la variable « TEMPLATES ». ##### Ajout des URLs de celcatsanitizer Dans le fichier celcatsanitizer/urls.py, importez la fonction django.conf.urls.include, et ajoutez url(r'^', include("edt.urls")) à la **fin** de la liste urlspatterns. ##### Génération de la base de données -Vous avez besoin de générer les migrations de celcatsanitizer, puis appliquez-les : +Générer les migrations de celcatsanitizer, puis appliquez-les : > $ ./manage.py makemigrations edt > $ ./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.11/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 +Cette étape est inutile si vous êtes en mode de déboguage. + ### Lancement de celcatsanitizer -Si vous êtes en mode de débuggage, lancez le serveur de cette manière : +Si vous êtes en mode de déboguage, lancez le serveur de cette manière : > $ ./manage.py runserver -Si vous êtes en production, il n'est pas recommandé d'utiliser ce serveur. Exécutez Django avec le module mod_wsgi d'Apache, ou avec un serveur [gunicorn](http://gunicorn.org/) derrière nginx. +Si vous êtes en production, il n’est pas recommandé d’utiliser ce serveur. Exécutez Django avec le module mod_wsgi d’Apache, ou avec un serveur [gunicorn](http://gunicorn.org/) derrière nginx. + +### Configuration de celcatsanitizer +#### Administrateur +Pour avoir accès à l’interface d’administration, vous devez créer un utilisateur avec les droits administrateur. Pour cela, exécutez la commande suivante : + +> $ ./manage.py createsuperuser + +Renseignez ensuite votre nom d’utilisateur, mot de passe et adresse email au fur et à mesure. + +#### Pages statiques +Comme indiqué plus haut, celcatsanitizer utilise l’application flatpages de Django. + +Si vous êtes en production, vous devez changer le site de base (« example.com ») par le site de celcatsanitizer. + +Vous devez ensuite rajouter les pages /a-propos/ et /contact/. + +Vous pouvez effectuer tout ça à partir de l’interface d’administration de Django. -- cgit v1.2.1 From 912f3cca24da9d870aa191348fef901adcd55b7d Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 1 Nov 2017 15:17:35 +0100 Subject: get_parents() sélectionne aussi les groupes enfants dont le sous-groupe commence par la même chaîne que celui du groupe passé en paramètre. Correction cosmétique du code. --- models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models.py b/models.py index c546737..c3118a3 100644 --- a/models.py +++ b/models.py @@ -71,7 +71,7 @@ class Timetable(SlugModel): class GroupManager(Manager): def get_parents(self, group): - groups_criteria = Q(subgroup__isnull=True) | \ + groups_criteria = Q(subgroup__isnull=True) | Q(subgroup__startswith=group.subgroup) | \ reduce(lambda x, y: x | y, [Q(subgroup=group.subgroup[:i]) for i in range(1, len(group.subgroup) + 1)]) @@ -80,7 +80,7 @@ class GroupManager(Manager): timetable=group.timetable) def get_relevant_groups(self, timetable, *args, **criteria): - sub = Group.objects.filter(timetable=timetable,mention=OuterRef("mention"), + sub = Group.objects.filter(timetable=timetable, mention=OuterRef("mention"), subgroup__startswith=OuterRef("subgroup")) \ .order_by().values("mention").annotate(c=Count("*")).values("c") -- cgit v1.2.1 From 2d68777e50e261cb793186321947b677ed9de692 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 1 Nov 2017 15:20:55 +0100 Subject: Correction des crashes provoqués lors de la génération des ICS lorsque le type d’un cours est nul. --- feeds.py | 7 ++++++- templates/timetable_common.html | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/feeds.py b/feeds.py index 66be12e..aced5e6 100644 --- a/feeds.py +++ b/feeds.py @@ -75,6 +75,11 @@ class IcalFeed(Feed): def item_link(self, item): return "" + def item_summary(self, item): + if item.type is not None: + return item.name + " (" + item.type + ")" + return item.name + def items(self, obj): return Course.objects.get_courses_for_group(obj) @@ -83,7 +88,7 @@ class IcalFeed(Feed): "dtstart": item.begin, "dtend": item.end, "dtstamp": item.last_update, - "summary": item.name + " (" + item.type + ")", + "summary": self.item_summary(item), "location": format_rooms(item.rooms.all())} diff --git a/templates/timetable_common.html b/templates/timetable_common.html index 9a2c27e..62b1d71 100644 --- a/templates/timetable_common.html +++ b/templates/timetable_common.html @@ -4,7 +4,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 %}

    {% for course in day %}
  • - {{ course }} ({{ course.type }}), de {{ course.begin|date:"H:i" }} à {{ course.end|date:"H:i" }}{% if course.rooms.all|length > 0 %}
    + {{ course }}{% if course.type %} ({{ course.type }}){% endif %}, 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 %}
    Remarques : {{ course.notes }}{% endif %}
  • {% endfor %} -- cgit v1.2.1 From 9d364f627dc90f3a361cc9acbc2e91f64ee9edcf Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 1 Nov 2017 16:08:38 +0100 Subject: Utilisation de self.get_queryset() plutôt que Group.objects --- models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models.py b/models.py index c3118a3..cb975ab 100644 --- a/models.py +++ b/models.py @@ -80,7 +80,7 @@ class GroupManager(Manager): timetable=group.timetable) def get_relevant_groups(self, timetable, *args, **criteria): - sub = Group.objects.filter(timetable=timetable, mention=OuterRef("mention"), + sub = self.get_queryset().filter(timetable=timetable, mention=OuterRef("mention"), subgroup__startswith=OuterRef("subgroup")) \ .order_by().values("mention").annotate(c=Count("*")).values("c") -- cgit v1.2.1 From bb973abfbd566a0f019822453d06f8b0ef3f6dac Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 3 Nov 2017 17:40:11 +0100 Subject: Prise en compte des groupes qui n’ont pas de sous-groupe dans la liste des groupes pertinents --- models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models.py b/models.py index cb975ab..3af1c06 100644 --- a/models.py +++ b/models.py @@ -86,7 +86,7 @@ class GroupManager(Manager): return self.get_queryset().filter(*args, timetable=timetable, hidden=False, **criteria) \ .annotate(nbsub=Subquery(sub, output_field=models.IntegerField())) \ - .filter(nbsub=1).order_by("name") + .filter(Q(nbsub=1) | Q(nbsub__isnull=True)).order_by("name") class Group(models.Model): -- cgit v1.2.1 From c476a83d3c92d705d0bdea8b37a9aa6f10cb50ac Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sat, 4 Nov 2017 12:24:25 +0100 Subject: Correction d’un bug qui empêche la mise à jour complète de l’emploi du temps Lorsqu’on demande une mise à jour, la semaine et le mois à mettre à jour sont passés en paramètre de la fonction de mise à jour. Mais quand on demande la mise à jour de tout l’emploi du temps, ces deux paramètres sont à None, faisant planter la fonction qui calcul le début et la fin de la semaine. --- management/commands/timetables.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index 2d8a17e..d0bf0bd 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -25,17 +25,28 @@ from ._private import delete_courses_in_week, get_events, get_update_date, get_w @transaction.atomic def process_timetable_week(timetable, soup, weeks_in_soup, force, year=None, week=None): - begin, end = get_week(year, week) + if year is not None and week is not None: + begin, end = get_week(year, week) + # Si on force la mise à jour, on définit de moment # de la mise à jour au début de la semaine - if force: + if force and year is not None and week is not None: today = begin + elif force: + # Si la mise à jour est faite sur tout l’emploi du temps, + # alors la date de début est indéfinie. + today = None else: today = tz_now() # On récupère la mise à jour la plus ancienne dans les cours de l’emploi du temps - # commençant à partir de maintenant - last_update_date = Course.objects.filter(timetable=timetable, begin__gte=today) + last_update_date = Course.objects.filter(timetable=timetable) + + if today is not None: + # Cette date concerne les éléments commençant à partir d’aujourd’hui si la valeur + # n’est pas nulle. + last_update_date = last_update_date.filter(begin__gte=today) + if year is not None and week is not None: # Si jamais on traite une semaine spécifique, on limite les cours sélectionnés # à ceux qui commencent entre le début du traitement et la fin de la semaine -- cgit v1.2.1 From 37a95855076035470e7484804ed48b779ed22277 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sat, 4 Nov 2017 12:32:26 +0100 Subject: Affichage de la pile d’exécution lors d’une erreur de màj d’emploi du temps --- management/commands/timetables.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index d0bf0bd..b254788 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -14,6 +14,7 @@ # along with celcatsanitizer. If not, see . import datetime +import traceback from django.core.management.base import BaseCommand from django.db import transaction @@ -135,9 +136,11 @@ class Command(BaseCommand): try: process_timetable(timetable, options["force"], year, weeks) - except Exception as exc: + except Exception: self.stderr.write( - self.style.ERROR("Failed to process {0}: {1}".format(timetable, exc))) + self.style.ERROR("Failed to process {0}:".format(timetable)) + ) + self.stderr.write(self.style.ERROR(traceback.format_exc())) errcount += 1 if errcount == 0: -- cgit v1.2.1 From 9447e0865c9d8a374ff6feb1bcf501c5eb73faf2 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 7 Nov 2017 17:40:44 +0100 Subject: La commande `timetables` n’affiche pas de trace d’erreur lorsqu’on l’arrête avec un Ctrl-C. --- management/commands/timetables.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/management/commands/timetables.py b/management/commands/timetables.py index b254788..ff00c8f 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.py @@ -136,6 +136,8 @@ class Command(BaseCommand): try: process_timetable(timetable, options["force"], year, weeks) + except KeyboardInterrupt: + break except Exception: self.stderr.write( self.style.ERROR("Failed to process {0}:".format(timetable)) -- cgit v1.2.1 From 04ca9906dccbfd290a08aef037873302ff7eac3d Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 7 Nov 2017 17:58:26 +0100 Subject: Version 0.11.0 --- __init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 13ff905..1740b96 100644 --- a/__init__.py +++ b/__init__.py @@ -13,5 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . -VERSION = "dev" +VERSION = "0.11" +__version__ = VERSION + default_app_config = "edt.apps.EdtConfig" -- cgit v1.2.1