From 66853337a5f6bd2b67f1130e3f4db7febe27de1b Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 27 Jan 2019 19:22:35 +0100 Subject: api: ajout d’un sérialiseur pour chaque modèle Les sérialiseurs permettent de représenter les modèles en JSON. Tous les sérialiseurs exportent tous les champs de leurs modèles respectifs. En plus de cela, le sérialiseur du modèle des cours sérialise en plus les groupes et salles pour ne pas avoir à faire trop d’appels à l’API. Signed-off-by: Alban Gruin --- api/serializers.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 api/serializers.py (limited to 'api') diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..5d81f44 --- /dev/null +++ b/api/serializers.py @@ -0,0 +1,57 @@ +# Copyright (C) 2019 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 rest_framework import serializers + +from ..models import Course, Group, Room, Source, Timetable, Year + + +class YearSerializer(serializers.ModelSerializer): + class Meta: + model = Year + fields = "__all__" + + +class SourceSerializer(serializers.ModelSerializer): + class Meta: + model = Source + fields = "__all__" + + +class TimetableSerializer(serializers.ModelSerializer): + class Meta: + model = Timetable + fields = "__all__" + + +class GroupSerializer(serializers.ModelSerializer): + class Meta: + model = Group + fields = "__all__" + + +class RoomSerializer(serializers.ModelSerializer): + class Meta: + model = Room + fields = "__all__" + + +class CourseSerializer(serializers.ModelSerializer): + groups = GroupSerializer(many=True, read_only=True) + rooms = RoomSerializer(many=True, read_only=True) + + class Meta: + model = Course + fields = "__all__" -- cgit v1.2.1 From 1764d2d8f4ae02a81dd6bea1c9f8a374d9706b63 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 27 Jan 2019 19:32:38 +0100 Subject: api: ajout des vues de l’API L’API a besoin de vues pour renvoyer des données. Ces vues utilisent les sérialiseurs écrits précédement. Pour l’instant, les sérialiseurs sont assez rudimentaires (ils ne peuvent afficher qu’une liste d’objets ou un seul objet sans filtres ou fonctionnalités supplémentaires), et ne permettent pas d’effectuer de modifications. Signed-off-by: Alban Gruin --- api/views.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 api/views.py (limited to 'api') diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..c58c9c0 --- /dev/null +++ b/api/views.py @@ -0,0 +1,50 @@ +# Copyright (C) 2019 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 rest_framework import viewsets +from ..models import Course, Group, Room, Source, Timetable, Year +from .serializers import CourseSerializer, GroupSerializer, RoomSerializer, \ + SourceSerializer, TimetableSerializer, YearSerializer + + +class YearViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Year.objects.all().order_by("name") + serializer_class = YearSerializer + + +class SourceViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Source.objects.all().order_by("pk") + serializer_class = SourceSerializer + + +class TimetableViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Timetable.objects.all().select_related("source") \ + .order_by("year", "name") + serializer_class = TimetableSerializer + + +class GroupViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Group.objects.all().order_by("name") + serializer_class = GroupSerializer + + +class RoomViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Room.objects.all().order_by("name") + serializer_class = RoomSerializer + + +class CourseViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Course.objects.all().prefetch_related("groups", "rooms") + serializer_class = CourseSerializer -- cgit v1.2.1 From 549e087ac32484d661197745bccc801856bc2d26 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 27 Jan 2019 19:29:33 +0100 Subject: api: complétion des vues de l’API Ajout de vues permettant de consulter les emplois du temps associés à une année ou à une source, les groupes associés à un emploi du temps, les cours d’un groupe ou d’une salle (soit tous, soit ceux de la semaine courante, soit ceux d’une semaine précise), de lister les semaines de cours, et d’accéder à QSJPS. Signed-off-by: Alban Gruin --- api/views.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 2 deletions(-) (limited to 'api') diff --git a/api/views.py b/api/views.py index c58c9c0..f6aa620 100644 --- a/api/views.py +++ b/api/views.py @@ -13,8 +13,20 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . +import datetime + +from django.db.models import Count +from django.db.models.functions import ExtractWeek, ExtractYear +from django.utils import timezone + from rest_framework import viewsets +from rest_framework.decorators import action, detail_route +from rest_framework.response import Response + +from ..forms import QSJPSForm from ..models import Course, Group, Room, Source, Timetable, Year +from ..utils import get_current_or_next_week, get_week + from .serializers import CourseSerializer, GroupSerializer, RoomSerializer, \ SourceSerializer, TimetableSerializer, YearSerializer @@ -23,27 +35,135 @@ class YearViewSet(viewsets.ReadOnlyModelViewSet): queryset = Year.objects.all().order_by("name") serializer_class = YearSerializer + @detail_route(methods=["get"], url_path="timetables") + def timetable_list(self, request, pk): + year = self.get_object() + timetables = Timetable.objects.filter(year=year).distinct() \ + .order_by("name") + timetables_json = TimetableSerializer( + self.paginate_queryset(timetables), many=True) + return self.get_paginated_response(timetables_json.data) + class SourceViewSet(viewsets.ReadOnlyModelViewSet): queryset = Source.objects.all().order_by("pk") serializer_class = SourceSerializer + @detail_route(methods=["get"], url_path="timetables") + def timetable_list(self, request, pk): + source = self.get_object() + timetables = Timetable.objects.filter(source=source).distinct() \ + .order_by("name") + timetables_json = TimetableSerializer( + self.paginate_queryset(timetables), many=True) + return self.get_paginated_response(timetables_json.data) + class TimetableViewSet(viewsets.ReadOnlyModelViewSet): queryset = Timetable.objects.all().select_related("source") \ .order_by("year", "name") serializer_class = TimetableSerializer + @detail_route(methods=["get"], url_path="groups") + def group_list(self, request, pk): + timetable = self.get_object() + groups = Group.objects.filter(source=timetable.source).distinct() \ + .order_by("name") + groups_json = GroupSerializer(self.paginate_queryset(groups), + many=True) + return self.get_paginated_response(groups_json.data) + + +class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): + @detail_route(methods=["get"], url_path="courses") + def course_list(self, request, pk): + obj = self.get_object() + courses = Course.objects.get_courses(obj).prefetch_related("groups") + courses_json = CourseSerializer(self.paginate_queryset(courses), + many=True) + return self.get_paginated_response(courses_json.data) + + @detail_route(methods=["get"], url_path="courses/weeks/current") + def current_week(self, request, pk): + obj = self.get_object() + start, end = get_week(*get_current_or_next_week()) + + courses = Course.objects.get_courses(obj, + begin__gte=start, end__lt=end) \ + .prefetch_related("groups") + courses_json = CourseSerializer(self.paginate_queryset(courses), + many=True) + return self.get_paginated_response(courses_json.data) + + @detail_route(methods=["get"], + url_path="courses/weeks/(?P\d+)/(?P\d+)") + def other_week(self, request, pk, year, week): + obj = self.get_object() + + errors = {} + if not year.isdigit(): + errors["year"] = "Rentrez une année valide" + if not week.isdigit() or not 0 < int(week) <= 53: + errors["week"] = "Rentrez une semaine valide" + if errors: + return Response(errors, status=400) -class GroupViewSet(viewsets.ReadOnlyModelViewSet): + start, end = get_week(int(year), int(week)) + + courses = Course.objects.get_courses(obj, + begin__gte=start, end__lt=end) \ + .prefetch_related("groups") + courses_json = CourseSerializer(self.paginate_queryset(courses), + many=True) + return self.get_paginated_response(courses_json.data) + + +class GroupViewSet(CourseListGroupSet): queryset = Group.objects.all().order_by("name") serializer_class = GroupSerializer + @detail_route(methods=["get"], url_path="courses/weeks") + def weeks(self, request, pk): + group = self.get_object() + groups = Group.objects.get_parents(group) + + courses = Course.objects.filter(groups__in=groups) \ + .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 Response(weeks) -class RoomViewSet(viewsets.ReadOnlyModelViewSet): + +class RoomViewSet(CourseListGroupSet): queryset = Room.objects.all().order_by("name") serializer_class = RoomSerializer + @action( + methods=["get"], + detail=False, + url_path="qsjps/(?P[0-9\-]+)/(?P[0-9:]+)/(?P[0-9:]+)") + def qsjps(self, request, day, begin, end): + form = QSJPSForm({"day": day, "begin": begin, "end": end}) + if not form.is_valid(): + return Response(form.errors, status=400) + + 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) + rooms_json = RoomSerializer(rooms, many=True) + return Response(rooms_json.data) + class CourseViewSet(viewsets.ReadOnlyModelViewSet): queryset = Course.objects.all().prefetch_related("groups", "rooms") -- cgit v1.2.1 From 1a582c97ecf369ca7ca170aeed845aa05ce72432 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 27 Jan 2019 19:32:55 +0100 Subject: api: ajout d’un routeur pour l’API, branchement sur urls.py Signed-off-by: Alban Gruin --- api/urls.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 api/urls.py (limited to 'api') diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..0164a4f --- /dev/null +++ b/api/urls.py @@ -0,0 +1,25 @@ +# Copyright (C) 2019 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 rest_framework import routers +from . import views + +router = routers.DefaultRouter() +router.register(r"years", views.YearViewSet) +router.register(r"sources", views.SourceViewSet) +router.register(r"timetables", views.TimetableViewSet) +router.register(r"rooms", views.RoomViewSet) +router.register(r"groups", views.GroupViewSet) +router.register(r"courses", views.CourseViewSet) -- cgit v1.2.1 From 7b280f8e2a901297112bc15becb1704205e3e901 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 5 Feb 2019 19:14:25 +0100 Subject: api/views: ajout de routes pour lister les cours d’une seule journée Signed-off-by: Alban Gruin --- api/views.py | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) (limited to 'api') diff --git a/api/views.py b/api/views.py index f6aa620..b7b69e3 100644 --- a/api/views.py +++ b/api/views.py @@ -83,11 +83,7 @@ class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): many=True) return self.get_paginated_response(courses_json.data) - @detail_route(methods=["get"], url_path="courses/weeks/current") - def current_week(self, request, pk): - obj = self.get_object() - start, end = get_week(*get_current_or_next_week()) - + def __get_courses(self, obj, start, end): courses = Course.objects.get_courses(obj, begin__gte=start, end__lt=end) \ .prefetch_related("groups") @@ -95,6 +91,37 @@ class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): many=True) return self.get_paginated_response(courses_json.data) + @detail_route(methods=["get"], url_path="courses/days/current") + def current_day(self, request, pk): + obj = self.get_object() + start = datetime.date.today() + end = start + datetime.timedelta(days=1) + return self.__get_courses(obj, start, end) + + @detail_route(methods=["get"], url_path="courses/days/(?P\d+)/(?P\d+)/(?P\d+)") + def other_day(self, request, pk, year, month, day): + obj = self.get_object() + + try: + start = datetime.date(int(year), int(month), int(day)) + except ValueError as v: + errors = {} + message = v.args[0] + if message.split(" ")[0] == "month": + errors["month"] = "Rentrez un mois invalide" + else: + errors["day"] = "Numéro de jour invalide pour le mois" + return Response(errors, status=400) + + end = start + datetime.timedelta(days=1) + return self.__get_courses(obj, start, end) + + @detail_route(methods=["get"], url_path="courses/weeks/current") + def current_week(self, request, pk): + obj = self.get_object() + start, end = get_week(*get_current_or_next_week()) + return self.__get_courses(obj, start, end) + @detail_route(methods=["get"], url_path="courses/weeks/(?P\d+)/(?P\d+)") def other_week(self, request, pk, year, week): @@ -108,14 +135,7 @@ class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): if errors: return Response(errors, status=400) - start, end = get_week(int(year), int(week)) - - courses = Course.objects.get_courses(obj, - begin__gte=start, end__lt=end) \ - .prefetch_related("groups") - courses_json = CourseSerializer(self.paginate_queryset(courses), - many=True) - return self.get_paginated_response(courses_json.data) + return self.__get_courses(obj, *get_week(int(year), int(week))) class GroupViewSet(CourseListGroupSet): -- cgit v1.2.1