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"] | 
