diff options
-rw-r--r-- | admin.py | 4 | ||||
-rw-r--r-- | management/commands/_private.py | 40 | ||||
-rw-r--r-- | management/commands/timetables.py | 1 | ||||
-rw-r--r-- | models.py | 60 | ||||
-rw-r--r-- | tests.py | 14 | ||||
-rw-r--r-- | utils.py | 36 | ||||
-rw-r--r-- | views.py | 14 |
7 files changed, 58 insertions, 111 deletions
@@ -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/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 » 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 @@ -13,8 +13,11 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see <http://www.gnu.org/licenses/>. +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 @@ -67,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): @@ -90,27 +89,26 @@ 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) - 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) - 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): - 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 +118,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",),) @@ -148,17 +146,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) + 1)]) | \ + 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") @@ -14,8 +14,6 @@ # along with celcatsanitizer. If not, see <http://www.gnu.org/licenses/>. from django.test import TestCase -from django.utils import timezone - from .models import Course, Group, Timetable, Year from .utils import tz_now @@ -130,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")) @@ -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""" @@ -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.get_relevant_groups(timetable) return group_list_common(request, timetable, groups) def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=None): @@ -78,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: |