# Copyright (C) 2017-2018 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 . import datetime from django.db import connection from django.db.models import Count, Max from django.db.models.functions import ExtractWeek, ExtractYear, Length from django.http import Http404 from django.shortcuts import get_object_or_404, render from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from .forms import QSJPSForm from .models import Course, Group, Room, Timetable, Year from .utils import get_current_week, get_current_or_next_week, get_week, \ group_courses import edt if connection.vendor == "postgresql": from django.contrib.postgres.aggregates import ArrayAgg from django.db.models.expressions import RawSQL def index(request): years = Year.objects.order_by("name") return render(request, "year_list.html", {"elements": years}) def mention_list(request, year_slug): year = get_object_or_404(Year, slug=year_slug) timetables = Timetable.objects.order_by("name").filter(year=year) return render(request, "mention_list.html", {"year": year, "elements": timetables}) def group_list(request, year_slug, timetable_slug): timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) start, _ = get_week(*get_current_week()) end = start + datetime.timedelta(weeks=4) groups = Group.objects.get_relevant_groups(start, source=timetable.source, hidden=False) if connection.vendor == "postgresql": weeks_sreq = RawSQL(""" SELECT ARRAY_AGG(DISTINCT DATE_TRUNC('week', begin)) FROM ( SELECT u2.begin from edt_group u0 LEFT OUTER JOIN edt_course_groups u1 ON (u0.id = u1.group_id) LEFT OUTER JOIN edt_group u2 ON (u1.course_id = u2.id) WHERE u0.source_id = edt_group.source_id AND u0.mention = edt_group.mention AND u0.semester = edt_group.semester AND u0.subgroup = SUBSTRING(edt_group.subgroup, 1, LENGTH(u0.subgroup)) AND u2.begin >= DATE_TRUNC('week', NOW()) AND u2.begin < DATE_TRUNC('week', NOW() + '1 MONTH'::INTERVAL) ORDER BY u2.begin ) weeks""", []) groups = groups.annotate(weeks=weeks_sreq) else: 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(group_week["groups__mention"], group_week["groups__semester"], group_week["groups__subgroup"]): if not hasattr(group, "weeks"): group.weeks = [] date, _ = get_week(group_week["year"], group_week["week"]) if date not in group.weeks: group.weeks.append(date) if hasattr(group, "weeks"): group.weeks.sort() return render(request, "group_list.html", {"timetable": timetable, "elements": groups}) def groups_all(request, year_slug, timetable_slug): # Récupération de l’emploi du temps et du groupe timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) groups = Group.objects.filter(source=timetable.source).order_by("name") # Rendu de la page return render(request, "groups_all_list.html", {"timetable": timetable, "elements": groups}) def group_weeks(request, year_slug, timetable_slug, group_slug): # Récupération de l’emploi du temps et des groupes timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) group = get_object_or_404(Group, slug=group_slug, source=timetable.source) # Groupes parents groups = Group.objects.get_parents(group) # Récupération de toutes les semaines avec des cours, sans doublons courses = Course.objects.filter(groups__in=groups) \ .order_by("year", "week") \ .annotate(year=ExtractYear("begin"), week=ExtractWeek("begin")) \ .values("year", "week") \ .annotate(c=Count("*")) # Conversion des semaines de cours en dates weeks = [get_week(course["year"], course["week"])[0] for course in courses] # Rendu return render(request, "group_weeks_list.html", {"timetable": timetable, "group": group, "elements": weeks}) def timetable_common(request, obj, year=None, week=None, timetable=None): current_year, current_week = get_current_or_next_week() 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 try: start, end = get_week(year, week) except ValueError: raise Http404 courses = Course.objects.get_courses(obj, begin__gte=start, begin__lt=end) if not courses.exists() and provided_week: raise Http404 # Récupération des semaines suivantes et précédentes pour les # afficher proprement dans l’emploi du temps last_course = Course.objects.get_courses(obj, begin__lt=start).last() last_week = getattr(last_course, "begin", None) next_course = Course.objects.get_courses(obj, begin__gte=end).first() next_week = getattr(next_course, "begin", None) last_update = courses.aggregate(Max("last_update"))["last_update__max"] grouped_courses = group_courses(courses) return render(request, "timetable.html", {"group": obj, "courses": grouped_courses, "last_update": last_update, "year": year, "week": int(week), "last_week": last_week, "next_week": next_week, "is_old_timetable": is_old_timetable, "group_mode": isinstance(obj, Group), "timetable": timetable}) def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=None): timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) group = get_object_or_404(Group, slug=group_slug, source=timetable.source) return timetable_common(request, group, year, week, timetable) def calendars(request, year_slug, timetable_slug, group_slug): timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug) group = get_object_or_404(Group, source=timetable.source, slug=group_slug) groups = Group.objects.get_parents(group) \ .annotate(length=Length("subgroup")) \ .order_by("length") return render(request, "calendars.html", {"timetable": timetable, "group": group, "groups": groups}) def rooms(request): # On récupère les dates allant de cette semaine à dans un mois start, _ = get_week(*get_current_week()) end = start + datetime.timedelta(weeks=4) if connection.vendor == "postgresql": # Si le SGBD est PostgreSQL, on utilise une requête à base de # ArrayAgg. Elle présente l’avantage d’être plus rapide que la # requête « généraliste » et de ne pas nécessiter de # traitement après. On récupère chaque salle ayant un cours # dans le mois à venir. Pour chacun de ses cours, on ne # récupère que le premier jour de la semaine, et si jamais ce # jour n’est pas déjà dans la liste des semaines de cours # (« weeks »), on l’y rajoute. rooms = Room.objects.filter(course__begin__gte=start, course__begin__lt=end) \ .order_by("name") \ .annotate(weeks=ArrayAgg( RawSQL("date_trunc('week', edt_course.begin)", []), distinct=True)) return render(request, "room_list.html", {"elements": rooms}) # Récupération des salles et de toutes les semaines où elles sont # concernées. # Cette requête associe chaque salle à toutes les semaines où un # cours s’y déroule. Le résultat est trié par le nom de la salle # et par semaine. # TODO optimiser cette requête, elle me semble un peu lente rooms = Room.objects.filter(course__begin__gte=start, course__begin__lt=end) \ .order_by("name") \ .annotate(year=ExtractYear("course__begin"), week=ExtractWeek("course__begin"), c=Count("*")) # Regroupement des semaines dans une liste de chaque objet salle rooms_weeks = [] for room in rooms: # Si on a pas traité de salle ou que la salle courante # dans le résultat de la requête est différente de la dernière # dans la liste des salles traitées if len(rooms_weeks) == 0 or rooms_weeks[-1].id != room.id: # On lui affecte un tableau et on l’ajoute dans # la liste des salles à traiter room.weeks = [] rooms_weeks.append(room) # On récupère le premier jour de la semaine date, _ = get_week(room.year, room.week) # Et on le rajoute dans la liste des semaines de la salle. rooms_weeks[-1].weeks.append(date) # Rendu de la page. return render(request, "room_list.html", {"elements": rooms_weeks}) def room_weeks(request, room_slug): room = get_object_or_404(Room, slug=room_slug) # Récupération des semaines de cours courses = Course.objects.filter(rooms=room) \ .order_by("year", "week") \ .annotate(year=ExtractYear("begin"), week=ExtractWeek("begin")) \ .values("year", "week") \ .annotate(c=Count("*")) weeks = [get_week(course["year"], course["week"])[0] for course in courses] return render(request, "room_weeks_list.html", {"room": room, "elements": weeks}) def room_timetable(request, room_slug, year=None, week=None): room = get_object_or_404(Room, slug=room_slug) return timetable_common(request, room, year, week) @csrf_exempt def qsjps(request): if request.method == "POST": # Si on traite un formulaire, on le valide form = QSJPSForm(request.POST) if form.is_valid(): # Formulaire validé day = form.cleaned_data["day"] begin_hour = form.cleaned_data["begin"] end_hour = form.cleaned_data["end"] begin = timezone.make_aware(datetime.datetime.combine(day, begin_hour)) end = timezone.make_aware(datetime.datetime.combine(day, end_hour)) rooms = Room.objects.qsjps(begin, end) return render(request, "qsjps.html", {"elements": rooms, "form": form}) # Si le formulaire est invalide, on ré-affiche le formulaire # avec les erreurs else: # Si le formulaire n’a pas été soumis, on en instancie un # nouveau form = QSJPSForm() return render(request, "qsjps_form.html", {"form": form}) def ctx_processor(request): return {"celcatsanitizer_version": edt.VERSION}