aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlban Gruin2019-01-27 21:59:34 +0100
committerAlban Gruin2019-01-27 21:59:34 +0100
commit3db1e1cf3147c2a12eb29973493d9fbae063ee7c (patch)
treef7427d1cef5e84ba09dc6f01884b306f986d6ea9
parent0001f374946a8778b0b5fad4c5baee091d328a9f (diff)
parent9987d47ec8c74d649de2a7d09ef62beb885949d9 (diff)
Merge branch 'ag/api' into prod/pa1ch/0.14.z
-rw-r--r--Documentation/index.rst1
-rw-r--r--Documentation/usage/installation.rst19
-rw-r--r--Documentation/usage/rest.rst697
-rw-r--r--api/serializers.py57
-rw-r--r--api/urls.py25
-rw-r--r--api/views.py170
-rw-r--r--requirements.txt1
-rw-r--r--urls.py5
8 files changed, 974 insertions, 1 deletions
diff --git a/Documentation/index.rst b/Documentation/index.rst
index e793dd2..e8dd4f2 100644
--- a/Documentation/index.rst
+++ b/Documentation/index.rst
@@ -63,6 +63,7 @@ Utilisation de celcatsanitizer
usage/commands/listtimetables
usage/commands/reparse
usage/commands/timetables
+ usage/rest
usage/versions
Développement
diff --git a/Documentation/usage/installation.rst b/Documentation/usage/installation.rst
index 4dde4f4..75e0be1 100644
--- a/Documentation/usage/installation.rst
+++ b/Documentation/usage/installation.rst
@@ -8,6 +8,7 @@ celcatsanitizer est écrit en Python 3. Il dépend des bibliothèques
suivantes :
- `Django 2.0`_
+ - `Django REST Framework`_
- requests_, pour récupérer les emplois du temps en HTTP(S)
- BeautifulSoup4_ et LXML_, pour parser les emplois du temps en XML
- icalendar_, pour générer des fichiers ICS_.
@@ -25,6 +26,7 @@ d’installer le module psycopg2_.
Pour l’instant, l’installation doit passer par git_.
.. _Django 2.0: https://www.djangoproject.com/
+.. _Django REST Framework: https://www.django-rest-framework.org/
.. _requests: http://docs.python-requests.org/en/master/
.. _BeautifulSoup4:
https://www.crummy.com/software/BeautifulSoup/bs4/doc/
@@ -155,6 +157,23 @@ Si jamais vous utilisez Django en production, vous **devez
impérativement** mettre la valeur de la variable ``DEBUG`` à
``False``.
+Configuration de Django REST Framework
+``````````````````````````````````````
+Ajoutez la chaîne de caractère ``rest_framework`` à la liste
+``INSTALLED_APPS``.
+
+Libre à vous de configurer DRF de la manière dont vous le souhaitez.
+`Les différents paramètres sont accessibles ici`__. Les plus
+intéressants sont ``DEFAULT_PERMISSION_CLASSES``,
+``DEFAULT_RENDERER_CLASSES``, ``DEFAULT_PAGINATION_CLASS`` et
+``PAGE_SIZE``.
+
+__ https://www.django-rest-framework.org/api-guide/settings/
+
+Cette étape est **obligatoire**, mais deviendra optionnelle dans le
+futur. Dans le cas ou vous ne souhaiterez pas la faire, l’API REST ne
+sera pas activée.
+
Ajout de celcatsanitizer à la liste des applications Django
```````````````````````````````````````````````````````````
Ajoutez la chaîne de caractère ``edt`` à la liste ``INSTALLED_APPS``.
diff --git a/Documentation/usage/rest.rst b/Documentation/usage/rest.rst
new file mode 100644
index 0000000..bcaada9
--- /dev/null
+++ b/Documentation/usage/rest.rst
@@ -0,0 +1,697 @@
+========
+API REST
+========
+
+celcatsanitizer dispose d’une API REST pour permettre à des outils
+tiers d’accéder facilement à ses données, qui sont renvoyées en JSON.
+Pour l’instant, il ne permet pas la modification des données.
+
+Le point d’entrée se trouve à l’adresse ``api/`` de l’instance de
+celcatsanitizer. Il retourne la liste des autres points d’accès en
+JSON.
+
+Années
+======
+
+``api/years/``
+--------------
+Liste les années par ordre alphabétique de nom. :ref:`Le résultat
+peut être paginé <ref-pagination>`.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "count": 4,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 3,
+ "name": "L1",
+ "slug": "l1"
+ },
+ {
+ "id": 4,
+ "name": "L2",
+ "slug": "l2"
+ },
+ {
+ "id": 1,
+ "name": "L3",
+ "slug": "l3"
+ },
+ {
+ "id": 2,
+ "name": "M1",
+ "slug": "m1"
+ }
+ ]
+ }
+
+``api/years/<id>/``
+-------------------
+Retourne seulement une année.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "id": 1,
+ "name": "L3",
+ "slug": "l3"
+ }
+
+``api/years/<id>/timetables``
+-----------------------------
+Liste les emplois du temps associés à une année par ordre alphabétique
+de nom. :ref:`Le résultat peut être paginé <ref-pagination>`.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 2,
+ "name": "1ere année SRI",
+ "slug": "1ere-annee-sri",
+ "year": 1,
+ "source": 2
+ },
+ {
+ "id": 1,
+ "name": "Info",
+ "slug": "info",
+ "year": 1,
+ "source": 1
+ }
+ ]
+ }
+
+Emplois du temps
+================
+
+``api/timetables``
+------------------
+Liste les emplois du temps par ordre d’année (ID associé) puis de nom.
+:ref:`Le résultat peut être paginé <ref-pagination>`.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "name": "Info",
+ "slug": "info",
+ "year": 1,
+ "source": 1
+ }
+ ]
+ }
+
+``api/timetables/<id>/``
+------------------------
+Retourne seulement un emploi du temps.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "id": 1,
+ "name": "Info",
+ "slug": "info",
+ "year": 1,
+ "source": 1
+ }
+
+``api/timetables/<id>/groups/``
+-------------------------------
+Retourne la liste des groupes associés à un emploi du temps, triés par
+ordre alphabétique. :ref:`Le résultat peut être paginé
+<ref-pagination>`.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 207,
+ "name": "L2 Info s1 CMA",
+ "celcat_name": "L2 Info s1 CMA",
+ "mention": "L2 Info",
+ "semester": 1,
+ "subgroup": "A",
+ "slug": "l2-info-s1-cma",
+ "hidden": false,
+ "source": 1
+ },
+ {
+ "id": 208,
+ "name": "L3 INFO (toutes sections et semestres confondus)",
+ "celcat_name": "L3 INFO (toutes sections et semestres confondus)",
+ "mention": "L3 INFO",
+ "semester": null,
+ "subgroup": "",
+ "slug": "l3-info-toutes-sections-et-semestres-confondus",
+ "hidden": false,
+ "source": 1
+ }
+ ]
+ }
+
+Sources
+=======
+
+``api/sources/``
+----------------
+Retourne la liste des sources par ordre d’ID. :ref:`Le résultat peut
+être paginé <ref-pagination>`.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "url": "https://edt.univ-tlse3.fr/calendar/default.aspx?View=month&Type=group&ResourceN ame=formation_ELINFE",
+ "last_update_date": null
+ },
+ {
+ "id": 2,
+ "url": "https://edt.univ-tlse3.fr/calendar/default.aspx?View=month&Type=group&ResourceN ame=formation_ELUSR1_s1",
+ "last_update_date": null
+ }
+ ]
+ }
+
+``api/sources/<id>/``
+---------------------
+Renvoie seulement une source.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "id": 1,
+ "url": "https://edt.univ-tlse3.fr/calendar/default.aspx?View=month&Type=group&ResourceName=formation_ELINFE",
+ "last_update_date": null
+ }
+
+``api/sources/<id>/timetables/``
+--------------------------------
+Renvoie la liste des emplois du temps associé à une source triés par
+ordre alphabétique. :ref:`Le résultat peut être paginé
+<ref-pagination>`.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "name": "Info",
+ "slug": "info",
+ "year": 1,
+ "source": 1
+ }
+ ]
+ }
+
+Groupes
+=======
+
+``api/groups/``
+---------------
+Liste les groupes par ordre alphabétique. :ref:`Le résultat peut être
+paginé <ref-pagination>`.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 207,
+ "name": "L2 Info s1 CMA",
+ "celcat_name": "L2 Info s1 CMA",
+ "mention": "L2 Info",
+ "semester": 1,
+ "subgroup": "A",
+ "slug": "l2-info-s1-cma",
+ "hidden": false,
+ "source": 1
+ },
+ {
+ "id": 208,
+ "name": "L3 INFO (toutes sections et semestres confondus)",
+ "celcat_name": "L3 INFO (toutes sections et semestres confondus)",
+ "mention": "L3 INFO",
+ "semester": null,
+ "subgroup": "",
+ "slug": "l3-info-toutes-sections-et-semestres-confondus",
+ "hidden": false,
+ "source": 1
+ }
+ ]
+ }
+
+``api/groups/<id>/``
+--------------------
+Affiche seulement un groupe.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "id": 207,
+ "name": "L2 Info s1 CMA",
+ "celcat_name": "L2 Info s1 CMA",
+ "mention": "L2 Info",
+ "semester": 1,
+ "subgroup": "A",
+ "slug": "l2-info-s1-cma",
+ "hidden": false,
+ "source": 1
+ }
+
+.. _ref-groups-courses:
+
+``api/groups/<id>/courses/``
+----------------------------
+Retourne tous les cours d’un groupe et de ses parents triés par ordre
+de début. :ref:`Le résultat peut être paginé <ref-pagination>`.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 34723,
+ "groups": [
+ {
+ "id": 98,
+ "name": "L3 INFO s1 CMA",
+ "celcat_name": "L3 INFO s1 CMA",
+ "mention": "L3 INFO",
+ "semester": 1,
+ "subgroup": "A",
+ "slug": "l3-info-s1-cma",
+ "hidden": false,
+ "source": 1
+ },
+ {
+ "id": 207,
+ "name": "L2 Info s1 CMA",
+ "celcat_name": "L2 Info s1 CMA",
+ "mention": "L2 Info",
+ "semester": 1,
+ "subgroup": "A",
+ "slug": "l2-info-s1-cma",
+ "hidden": false,
+ "source": 1
+ }
+ ],
+ "rooms": [],
+ "name": "REUNION / RENCONTRE",
+ "type": null,
+ "notes": "Nuit de l'info",
+ "begin": "2018-12-06T13:30:00+01:00",
+ "end": "2018-12-06T23:45:00+01:00",
+ "last_update": "2018-12-31T13:26:57.122490+01:00",
+ "source": 1
+ },
+ {
+ "id": 34727,
+ "groups": [
+ {
+ "id": 98,
+ "name": "L3 INFO s1 CMA",
+ "celcat_name": "L3 INFO s1 CMA",
+ "mention": "L3 INFO",
+ "semester": 1,
+ "subgroup": "A",
+ "slug": "l3-info-s1-cma",
+ "hidden": false,
+ "source": 1
+ },
+ {
+ "id": 207,
+ "name": "L2 Info s1 CMA",
+ "celcat_name": "L2 Info s1 CMA",
+ "mention": "L2 Info",
+ "semester": 1,
+ "subgroup": "A",
+ "slug": "l2-info-s1-cma",
+ "hidden": false,
+ "source": 1
+ }
+ ],
+ "rooms": [],
+ "name": "REUNION / RENCONTRE",
+ "type": null,
+ "notes": "Nuit de l'info",
+ "begin": "2018-12-07T07:45:00+01:00",
+ "end": "2018-12-07T23:45:00+01:00",
+ "last_update": "2018-12-31T13:26:57.136381+01:00",
+ "source": 1
+ }
+ ]
+ }
+
+``api/groups/<id>/courses/weeks/``
+----------------------------------
+Retourne la liste des semaines de cours d’un groupe.
+
+Exemple :
+`````````
+.. code:: json
+
+ [
+ "2018-12-03T00:00:00+01:00"
+ ]
+
+``api/groups/<id>/courses/weeks/current/``
+------------------------------------------
+Retourne la liste des cours du groupe et de ses parents pendant la
+semaine courante (ou prochaine lors du week-end) d’un groupe, par
+ordre de début. :ref:`Le résultat peut être paginé <ref-pagination>`.
+Le format du résultat est identique à celui de
+:ref:`api/groups/\<id>/courses/ <ref-groups-courses>`.
+
+.. _ref-groups-courses-week-arg:
+
+``api/groups/<id>/courses/weeks/<year>/<week>/``
+------------------------------------------------
+Retourne la liste des cours du groupe et de ses parents pendant la
+semaine spécifiée, par ordre de début. Si l’année et la semaine ne
+sont pas des nombres, un code 404 est renvoyé. Si la semaine n’est
+pas comprise entre 1 et 53, une erreur 400 est renvoyée, et les
+erreurs rencontrées sont renvoyées. :ref:`Le résultat peut être
+paginé <ref-pagination>`. Le format du résultat est identique à celui
+de :ref:`api/groups/\<id>/courses/ <ref-groups-courses>`.
+
+Exemple d’erreur (``api/groups/<id>/courses/weeks/2018/111/``) :
+````````````````````````````````````````````````````````````````
+.. code:: json
+
+ {
+ "week": "Rentrez une semaine valide"
+ }
+
+Salles
+======
+
+``api/rooms/``
+--------------
+Liste les salles par ordre alphabétique. :ref:`Le résultat peut être
+paginé <ref-pagination>`.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "count": 3,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 26,
+ "name": "1R1-010",
+ "slug": "1r1-010"
+ },
+ {
+ "id": 11,
+ "name": "1TP1-B08",
+ "slug": "1tp1-b08"
+ },
+ {
+ "id": 5,
+ "name": "1TP1-B08bis",
+ "slug": "1tp1-b08bis"
+ }
+ ]
+ }
+
+``api/rooms/<id>``
+------------------
+Renvoie une seule salle.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "id": 26,
+ "name": "1R1-010",
+ "slug": "1r1-010"
+ }
+
+``api/rooms/<id>/courses/``
+---------------------------
+Renvoie la liste des cours se déroulant dans une salle par ordre de
+début. :ref:`Le résultat peut être paginé <ref-pagination>`. Le
+format du résultat est identique à celui de
+:ref:`api/groups/\<id>/courses/ <ref-groups-courses>`.
+
+``api/rooms/<id>/courses/weeks/current/``
+-----------------------------------------
+Renvoie la liste des cours se déroulant dans une salle pendant la
+semaine courante (ou la semaine prochaine le week-end) par ordre de
+début. :ref:`Le résultat peut être paginé <ref-pagination>`. Le
+format du résultat est identique à celui de
+:ref:`api/groups/\<id>/courses/ <ref-groups-courses>`.
+
+``api/rooms/<id>/courses/weeks/<year>/<week>/``
+-----------------------------------------------
+Renvoie la liste des cours se déroulant dans une salle pendant la
+semaine spécifiée. Si l’année et la semaine ne sont pas des nombres,
+un code 404 est renvoyé. Si la semaine n’est pas comprise entre 1 et
+53, une erreur 400 est renvoyée, et les erreurs rencontrées sont
+renvoyées. :ref:`Le résultat peut être paginé <ref-pagination>`. Le
+format du résultat est identique à celui de
+:ref:`api/groups/\<id>/courses/weeks/\<year>/\<week>
+<ref-groups-courses-week-arg>`.
+
+``api/rooms/qsjps/<day>/<begin>/<end>/``
+----------------------------------------
+Fournit un accès à QSJPS. ``<day>`` est une date devant être formatée
+de cette manière : ``YYYY-MM-DD``. ``<begin>`` et ``<end>`` sont des
+heures qui doivent être formatées de cette manière : ``HH:mm``. La
+valeur de ``<begin>`` doit être inférieure à celle de ``<end>``.
+
+Renvoie la liste des salles vides le début du jour ``<day>`` de
+``<begin>`` à ``<end>``.
+
+En cas de mauvais formatage, une erreur 400 est renvoyée, et les
+erreurs sont détaillées dans le corps de la réponse. Sinon, la liste
+des salles libres est renvoyée.
+
+Exemple :
+`````````
+.. code:: json
+
+ [
+ {
+ "id": 26,
+ "name": "1R1-010",
+ "slug": "1r1-010"
+ },
+ {
+ "id": 11,
+ "name": "1TP1-B08",
+ "slug": "1tp1-b08"
+ },
+ {
+ "id": 5,
+ "name": "1TP1-B08bis",
+ "slug": "1tp1-b08bis"
+ }
+ ]
+
+Exemple d’erreur (``api/rooms/qsjps/2019-01-35/12:00/10:00/``) :
+````````````````````````````````````````````````````````````````
+.. code:: json
+
+ {
+ "day": [
+ "Saisissez une date valide."
+ ],
+ "end": [
+ "L’heure de début doit être supérieure à celle de fin."
+ ]
+ }
+
+Exemple d’erreur (``api/rooms/qsjps/2019-01-35/12:70/10:70/``) :
+````````````````````````````````````````````````````````````````
+.. code:: json
+
+ {
+ "day": [
+ "Saisissez une date valide."
+ ],
+ "begin": [
+ "Saisissez une heure valide."
+ ],
+ "end": [
+ "Saisissez une heure valide."
+ ]
+ }
+
+Cours
+=====
+
+``api/courses/``
+----------------
+Renvoie la liste des cours. :ref:`Le résultat peut être paginé
+<ref-pagination>`.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "count": 4766,
+ "next": "http://localhost:8000/api/courses/?page=2",
+ "previous": null,
+ "results": [
+ {
+ "id": 22133,
+ "groups": [
+ {
+ "id": 98,
+ "name": "L3 INFO s1 CMA",
+ "celcat_name": "L3 INFO s1 CMA",
+ "mention": "L3 INFO",
+ "semester": 1,
+ "subgroup": "A",
+ "slug": "l3-info-s1-cma",
+ "hidden": false,
+ "source": 1
+ }
+ ],
+ "rooms": [
+ {
+ "id": 1,
+ "name": "Amphi AMPERE (3A)",
+ "slug": "amphi-ampere-3a"
+ }
+ ],
+ "name": "REUNION / RENCONTRE",
+ "type": null,
+ "notes": null,
+ "begin": "2018-09-04T15:45:00+02:00",
+ "end": "2018-09-04T16:45:00+02:00",
+ "last_update": "2018-09-26T19:34:12.924533+02:00",
+ "source": 1
+ },
+ …
+ ]
+ }
+
+``api/courses/<id>``
+--------------------
+Renvoie un seul cours.
+
+Exemple :
+`````````
+.. code:: json
+
+ {
+ "id": 22133,
+ "groups": [
+ {
+ "id": 98,
+ "name": "L3 INFO s1 CMA",
+ "celcat_name": "L3 INFO s1 CMA",
+ "mention": "L3 INFO",
+ "semester": 1,
+ "subgroup": "A",
+ "slug": "l3-info-s1-cma",
+ "hidden": false,
+ "source": 1
+ }
+ ],
+ "rooms": [
+ {
+ "id": 1,
+ "name": "Amphi AMPERE (3A)",
+ "slug": "amphi-ampere-3a"
+ }
+ ],
+ "name": "REUNION / RENCONTRE",
+ "type": null,
+ "notes": null,
+ "begin": "2018-09-04T15:45:00+02:00",
+ "end": "2018-09-04T16:45:00+02:00",
+ "last_update": "2018-09-26T19:34:12.924533+02:00",
+ "source": 1
+ }
+
+.. _ref-pagination:
+
+Pagination
+==========
+Il est possible que les résultats soient paginés. Cela dépend de la
+configuration de l’instance de celcatsanitizer. Dans ce cas, tous les
+appels à des points d’accès renvoyant des résultats pouvant être
+paginés se trouvent dans ce genre de structure :
+
+.. code:: json
+
+ {
+ "count": 4766,
+ "next": "http://localhost:8000/api/courses/?page=2",
+ "previous": null,
+ "results": [
+ …
+ ]
+ }
+
+ - ``count`` représente le nombre d’éléments au total (et non pas sur
+ la page).
+ - ``next`` est le lien de la page de résultats suivants, si il y en a
+ une.
+ - ``previous`` est le lien de la page de résultats précédents, si il
+ y en a une.
+ - ``results`` est la liste des résultats, si il y en a.
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 <http://www.gnu.org/licenses/>.
+
+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__"
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 <http://www.gnu.org/licenses/>.
+
+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)
diff --git a/api/views.py b/api/views.py
new file mode 100644
index 0000000..f6aa620
--- /dev/null
+++ b/api/views.py
@@ -0,0 +1,170 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+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
+
+
+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<year>\d+)/(?P<week>\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)
+
+ 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(CourseListGroupSet):
+ queryset = Room.objects.all().order_by("name")
+ serializer_class = RoomSerializer
+
+ @action(
+ methods=["get"],
+ detail=False,
+ url_path="qsjps/(?P<day>[0-9\-]+)/(?P<begin>[0-9:]+)/(?P<end>[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")
+ serializer_class = CourseSerializer
diff --git a/requirements.txt b/requirements.txt
index f2c309f..6c91f54 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
beautifulsoup4==4.6.3
Django==2.0.8
+djangorestframework==3.9.1
gunicorn==19.9.0
icalendar==4.0.2
lxml==4.2.4
diff --git a/urls.py b/urls.py
index 977ac8d..5cd134e 100644
--- a/urls.py
+++ b/urls.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017-2018 Alban Gruin
+# Copyright (C) 2017-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
@@ -14,10 +14,13 @@
# along with celcatsanitizer. If not, see <http://www.gnu.org/licenses/>.
from django.urls import include, path
+
from . import feeds, views
+from .api.urls import router
urlpatterns = [
path("", views.index, name="index"),
+ path("api/", include(router.urls)),
path("pages/", include("django.contrib.flatpages.urls")),
path("salles/", views.rooms, name="rooms"),
path("salles/qsjps", views.qsjps, name="qsjps"),