diff options
-rw-r--r-- | admin.py | 2 | ||||
-rw-r--r-- | models.py | 49 | ||||
-rw-r--r-- | tests.py | 62 | ||||
-rw-r--r-- | utils.py | 28 | ||||
-rw-r--r-- | views.py | 29 |
5 files changed, 80 insertions, 90 deletions
@@ -44,7 +44,7 @@ class TimetableAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin): fieldsets = ( (None, {"fields": ("name", "celcat_name", "timetable", "hidden",)}), - ("Groupes", {"fields": ("mention", "subgroup",)}),) + ("Groupes", {"fields": ("mention", "semester", "subgroup",)}),) list_display = ("name", "timetable", "hidden",) list_editable = ("hidden",) list_filter = ("timetable",) @@ -16,8 +16,7 @@ from functools import reduce from django.db import models -from django.db.models import Count, Manager, Q, Subquery, Value -from django.db.models.expressions import OuterRef +from django.db.models import Count, Manager, Q from django.db.models.functions import ExtractWeek, ExtractYear from django.utils import timezone from django.utils.text import slugify @@ -72,27 +71,18 @@ class Timetable(SlugModel): class GroupManager(Manager): def get_parents(self, group): - groups_criteria = Q(subgroup="") | Q(subgroup__startswith=group.subgroup) + groups_criteria = Q(subgroup="") if len(group.subgroup) != 0: groups_criteria |= 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, + return self.get_queryset().filter(groups_criteria, + Q(semester=None) | Q(semester=group.semester), + mention=group.mention, timetable=group.timetable) - def get_relevant_groups(self, timetable, *args, **criteria): - sub = self.get_queryset().filter(timetable=timetable, - mention__startswith=OuterRef("mention"), - subgroup__startswith=OuterRef("subgroup")) \ - .annotate(v=Value(0)).values("v") \ - .annotate(c=Count("v")).values("c") # fuck Count() - - return self.get_queryset().filter(*args, timetable=timetable, hidden=False, **criteria) \ - .annotate(nbsub=Subquery(sub, output_field=models.IntegerField())) \ - .filter(Q(nbsub=1) | Q(nbsub__isnull=True)).order_by("name") - class Group(models.Model): objects = GroupManager() @@ -104,26 +94,26 @@ class Group(models.Model): verbose_name="emploi du temps") mention = models.CharField(max_length=128) + semester = models.IntegerField(verbose_name="semestre", null=True) subgroup = models.CharField(max_length=16, verbose_name="sous-groupe", default="") slug = models.SlugField(max_length=64, default="") hidden = models.BooleanField(verbose_name="caché", default=False) - def corresponds_to(self, timetable_id, mention, subgroup): + def corresponds_to(self, mention, semester, 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) + subgroup_corresponds = self.subgroup.startswith(subgroup) - return self.timetable.id == timetable_id and \ - (self.mention.startswith(mention) or \ - mention.startswith(self.mention)) and \ - subgroup_corresponds + return (self.mention.startswith(mention) or \ + mention.startswith(self.mention)) and \ + (self.semester == semester or semester is None) and \ + subgroup_corresponds @property def group_info(self): - return self.timetable.id, self.mention, self.subgroup + return self.mention, self.semester, self.subgroup def __str__(self): return self.name @@ -133,7 +123,7 @@ class Group(models.Model): self.name = self.celcat_name self.slug = slugify(self.name) - self.mention, self.subgroup = parse_group(self.name) + self.mention, self.semester, self.subgroup = parse_group(self.name) if self.subgroup is None: self.subgroup = "" @@ -141,7 +131,7 @@ class Group(models.Model): class Meta: - index_together = ("mention", "subgroup",) + index_together = ("mention", "semester", "subgroup",) unique_together = (("name", "timetable",), ("celcat_name", "timetable",), ("slug", "timetable",),) @@ -166,15 +156,16 @@ class CourseManager(Manager): def get_courses_for_group(self, group, **criteria): return self.get_queryset() \ .filter(groups__in=Group.objects.get_parents(group), **criteria) \ - .order_by("begin") + .order_by("begin").prefetch_related("rooms") def get_weeks(self, **criteria): return self.get_queryset() \ .filter(**criteria) \ .order_by("groups__name", "year", "week") \ - .annotate(_=Count(("groups", "year", "week", "begin")), - year=ExtractYear("begin"), - week=ExtractWeek("begin")) + .annotate(year=ExtractYear("begin"), + week=ExtractWeek("begin")) \ + .values("groups__mention", "groups__semester", + "groups__subgroup", "year", "week") class Course(models.Model): @@ -82,36 +82,36 @@ class GroupTestCase(TestCase): general = Group.objects.get(celcat_name="L1 info (toutes sections et semestres confondus)", timetable=self.timetable) - self.assertTrue(cma.corresponds_to(*tda2.group_info)) # CMA corresponds to TDA2 - self.assertTrue(cma.corresponds_to(*tpa21.group_info)) # CMA corresponds to TPA21 - self.assertTrue(tda2.corresponds_to(*tpa21.group_info)) # TDA2 corresponds to TPA21 + self.assertFalse(cma.corresponds_to(*tda2.group_info)) + self.assertFalse(cma.corresponds_to(*tpa21.group_info)) + self.assertFalse(tda2.corresponds_to(*tpa21.group_info)) - self.assertTrue(cmb.corresponds_to(*tdb2.group_info)) # CMB corresponds to TDB2 - self.assertTrue(cmb.corresponds_to(*tpb21.group_info)) # CMB corresponds to TPB21 - self.assertTrue(tdb2.corresponds_to(*tpb21.group_info)) # TDB2 corresponds to TPB21 + self.assertFalse(cmb.corresponds_to(*tdb2.group_info)) + self.assertFalse(cmb.corresponds_to(*tpb21.group_info)) + self.assertFalse(tdb2.corresponds_to(*tpb21.group_info)) - self.assertFalse(cmb.corresponds_to(*tda2.group_info)) # CMB does not corresponds to TDA2 - self.assertFalse(cmb.corresponds_to(*tpa21.group_info)) # CMB does not corresponds to TPA21 - self.assertFalse(tdb2.corresponds_to(*tpa21.group_info)) # TDB2 does not corresponds to TPA21 + self.assertFalse(cmb.corresponds_to(*tda2.group_info)) + self.assertFalse(cmb.corresponds_to(*tpa21.group_info)) + self.assertFalse(tdb2.corresponds_to(*tpa21.group_info)) - self.assertTrue(tda2.corresponds_to(*cma.group_info)) # TDA2 corresponds to CMA - self.assertTrue(tpa21.corresponds_to(*cma.group_info)) # TPA21 corresponds to CMA - self.assertTrue(tpa21.corresponds_to(*tda2.group_info)) # TPA21 corresponds to TDA2 + self.assertTrue(tda2.corresponds_to(*cma.group_info)) + self.assertTrue(tpa21.corresponds_to(*cma.group_info)) + self.assertTrue(tpa21.corresponds_to(*tda2.group_info)) - self.assertTrue(tdb2.corresponds_to(*cmb.group_info)) # TDB2 corresponds to CMB - self.assertTrue(tpb21.corresponds_to(*cmb.group_info)) # TPB21 corresponds to CMB - self.assertTrue(tpb21.corresponds_to(*tdb2.group_info)) # TPB21 corresponds to TDB2 + self.assertTrue(tdb2.corresponds_to(*cmb.group_info)) + self.assertTrue(tpb21.corresponds_to(*cmb.group_info)) + self.assertTrue(tpb21.corresponds_to(*tdb2.group_info)) - self.assertFalse(tda2.corresponds_to(*cmb.group_info)) # TDA2 does not corresponds to CMB - self.assertFalse(tpa21.corresponds_to(*cmb.group_info)) # TPA21 does not corresponds to CMB - self.assertFalse(tpa21.corresponds_to(*tdb2.group_info)) # TPA21 does not corresponds to TDB2 + self.assertFalse(tda2.corresponds_to(*cmb.group_info)) + self.assertFalse(tpa21.corresponds_to(*cmb.group_info)) + self.assertFalse(tpa21.corresponds_to(*tdb2.group_info)) - self.assertTrue(general.corresponds_to(*cma.group_info)) - self.assertTrue(general.corresponds_to(*cmb.group_info)) - self.assertTrue(general.corresponds_to(*tda2.group_info)) - self.assertTrue(general.corresponds_to(*tdb2.group_info)) - self.assertTrue(general.corresponds_to(*tpa21.group_info)) - self.assertTrue(general.corresponds_to(*tpb21.group_info)) + self.assertFalse(general.corresponds_to(*cma.group_info)) + self.assertFalse(general.corresponds_to(*cmb.group_info)) + self.assertFalse(general.corresponds_to(*tda2.group_info)) + self.assertFalse(general.corresponds_to(*tdb2.group_info)) + self.assertFalse(general.corresponds_to(*tpa21.group_info)) + self.assertFalse(general.corresponds_to(*tpb21.group_info)) self.assertTrue(cma.corresponds_to(*general.group_info)) self.assertTrue(cmb.corresponds_to(*general.group_info)) @@ -152,12 +152,12 @@ class GroupTestCase(TestCase): general = Group.objects.get(celcat_name="L1 info (toutes sections et semestres confondus)", timetable=self.timetable) - self.assertEqual(cma.group_info, (self.timetable.id, "L1 info s2 ", "A")) - self.assertEqual(tda2.group_info, (self.timetable.id, "L1 info s2 ", "A2")) - self.assertEqual(tpa21.group_info, (self.timetable.id, "L1 info s2 ", "A21")) + self.assertEqual(cma.group_info, ("L1 info", 2, "A")) + self.assertEqual(tda2.group_info, ("L1 info", 2, "A2")) + self.assertEqual(tpa21.group_info, ("L1 info", 2, "A21")) - self.assertEqual(cmb.group_info, (self.timetable.id, "L1 info s2 ", "B")) - self.assertEqual(tdb2.group_info, (self.timetable.id, "L1 info s2 ", "B2")) - self.assertEqual(tpb21.group_info, (self.timetable.id, "L1 info s2 ", "B21")) + self.assertEqual(cmb.group_info, ("L1 info", 2, "B")) + self.assertEqual(tdb2.group_info, ("L1 info", 2, "B2")) + self.assertEqual(tpb21.group_info, ("L1 info", 2, "B21")) - self.assertEqual(general.group_info, (self.timetable.id, "L1 info ", "")) + self.assertEqual(general.group_info, ("L1 info", None, "")) @@ -30,7 +30,7 @@ def get_current_or_next_week(): def get_week(year, week): start = timezone.make_aware(datetime.datetime.strptime( - "{0}-W{1:02d}-1".format(year, week), "%Y-W%W-%w")) + "{0}-W{1}-1".format(year, week), "%Y-W%W-%w")) end = start + datetime.timedelta(weeks=1) return start, end @@ -48,24 +48,32 @@ def group_courses(courses): def parse_group(name): # Explication de la regex # - # ^((.+?)\s*(s\d\s+)?)((CM|TD|TP|G)(\w\d{0,3}))?(\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 + # (.+?) 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* éventuellement un ou plusieurs espaces - # \(.+\))? un ou plusieurs caractères entre parenthèses + # (\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*\(.+\))?$") + 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 + return name, None, None parts = search.groups() - return parts[0], parts[5] + + # On retourne la section (parts[0]), le semestre (parts[2]) et le groupe (parts[5]) + if parts[2] is not None: + return parts[0], int(parts[2]), parts[5] + else: + # Si jamais le semestre n’est pas présent dans la chaine parsée, + # parts[2] sera à None et sa conversion vers un int va provoquer + # une erreur. + return parts[0], None, parts[5] def tz_now(): """Retourne la date et l’heure avec le bon fuseau horaire""" @@ -31,21 +31,23 @@ def index(request): def mention_list(request, year_slug): year = get_object_or_404(Year, slug=year_slug) - timetables = Timetable.objects.order_by("name").filter(year=year) + timetables = Timetable.objects.order_by("name").filter(year=year).select_related("year") return render(request, "index.html", {"year": year, "elements": timetables}) -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, hidden=False).order_by("name") + start, _ = get_week(*get_current_week()) end = start + datetime.timedelta(weeks=4) - groups_weeks = Course.objects.get_weeks(begin__gte=start, begin__lt=end, timetable=timetable) \ - .values("groups__mention", "groups__subgroup", - "year", "week") + groups_weeks = Course.objects.get_weeks(begin__gte=start, begin__lt=end, groups__in=groups) for group in groups: for group_week in groups_weeks: - if group.corresponds_to(timetable.id, group_week["groups__mention"], + if group.corresponds_to(group_week["groups__mention"], + group_week["groups__semester"], group_week["groups__subgroup"]): if not hasattr(group, "weeks"): group.weeks = [] @@ -59,11 +61,6 @@ def group_list_common(request, timetable, groups): return render(request, "group_list.html", {"timetable": timetable, "groups": 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) - return group_list_common(request, timetable, groups) - 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, provided_week = False, True @@ -74,19 +71,13 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No elif (int(year), int(week)) < (current_year, current_week): is_old_timetable = True - start, end = get_week(int(year), int(week)) + start, end = get_week(year, week) 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.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: + if not courses.exists() and provided_week: raise Http404 last_update = courses.aggregate(Max("last_update"))["last_update__max"] |