aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/conf.py2
-rw-r--r--Documentation/index.rst1
-rw-r--r--Documentation/usage/installation.rst39
-rw-r--r--Documentation/usage/rest.rst753
-rw-r--r--Documentation/usage/versions.rst5
-rw-r--r--__init__.py2
-rw-r--r--api/serializers.py57
-rw-r--r--api/urls.py25
-rw-r--r--api/views.py190
-rw-r--r--requirements.txt1
-rw-r--r--templates/calendars.html35
-rw-r--r--templates/index.html2
-rw-r--r--templates/mention_list.html2
-rw-r--r--urls.py16
14 files changed, 1123 insertions, 7 deletions
diff --git a/Documentation/conf.py b/Documentation/conf.py
index c0ce370..bf32e61 100644
--- a/Documentation/conf.py
+++ b/Documentation/conf.py
@@ -15,7 +15,7 @@ copyright = u'%d, Alban Gruin' % year
author = u'Alban Gruin'
version = u'0.14'
-release = u'0.14.3'
+release = u'0.14.4'
language = 'fr'
diff --git a/Documentation/index.rst b/Documentation/index.rst
index 7051b93..7073b0d 100644
--- a/Documentation/index.rst
+++ b/Documentation/index.rst
@@ -64,6 +64,7 @@ Utilisation de celcatsanitizer
usage/commands/printvalues
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..92b3c5d 100644
--- a/Documentation/usage/installation.rst
+++ b/Documentation/usage/installation.rst
@@ -12,6 +12,10 @@ suivantes :
- BeautifulSoup4_ et LXML_, pour parser les emplois du temps en XML
- icalendar_, pour générer des fichiers ICS_.
+Une dépendance est optionnelle :
+
+ - `Django REST Framework`_, pour l’:doc:`API REST <rest>`.
+
celcatsanitizer requiert Python 3.4 au minimum, et marche avec les
versions 3.5 et 3.6. Les versions antérieures de Python 3 n’ont pas
étés testées, et les versions supérieures devraient fonctionner sans
@@ -31,6 +35,7 @@ Pour l’instant, l’installation doit passer par git_.
.. _LXML: https://lxml.de/
.. _icalendar: https://icalendar.readthedocs.io/en/latest/
.. _ICS: https://fr.wikipedia.org/wiki/ICalendar
+.. _Django REST Framework: https://www.django-rest-framework.org/
.. _PostgreSQL: https://www.postgresql.org/
.. _psycopg2: http://initd.org/psycopg/docs/install.html
.. _git: https://git-scm.com/
@@ -95,6 +100,13 @@ psycopg2 :
$ pip install psycopg2-binary
+Si vous souhaitez activer l’API REST, vous devez installer le module
+Django Rest Framework :
+
+.. code:: shell
+
+ $ pip install djangorestframework
+
Si vous êtes en production, il est recommandé d’utiliser gunicorn_ si
vous n’utilisez pas le serveur Apache. Installez-le de la même
manière :
@@ -232,6 +244,33 @@ Ce paramètre est **optionnel**.
__ https://docs.djangoproject.com/fr/2.0/topics/i18n/
+Activation de l’API REST et configuration de Django Rest Framework
+``````````````````````````````````````````````````````````````````
+L’API REST permet à des outils tiers d’accéder facilement aux données
+gérées par celcatsanitizer. Elle est optionnelle, et est basée sur
+Django Rest Framework. :doc:`Plus d’informations sur la page de l’API
+REST <rest>`.
+
+Si vous souhaitez l’activer, vous devez d’abord avoir installé Django
+REST Framework, puis mettre la variable ``CS_ENABLE_API`` à ``True``.
+
+.. code:: Python
+
+ CS_ENABLE_API = True
+
+Ajoutez ensuite 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 **optionnelle**.
+
``urls.py``
-----------
Dans le fichier ``celcatsanitizer/urls.py``, importez la fonction
diff --git a/Documentation/usage/rest.rst b/Documentation/usage/rest.rst
new file mode 100644
index 0000000..d18eaa5
--- /dev/null
+++ b/Documentation/usage/rest.rst
@@ -0,0 +1,753 @@
+========
+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.
+
+En fonction de la configuration de celcatsanitizer, l’API ne sera
+peut-être pas disponible.
+
+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/days/current/``
+-----------------------------------------
+Retourne la liste des cours du groupe et de ses parents d’un groupe
+pendant le jour courant, 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-day-arg:
+
+``api/groups/<id>/courses/days/<year>/<month>/<day>/``
+------------------------------------------------------
+Retourne la liste des cours du groupe et de ses parents pendant le
+jour spécifié, par ordre de début. Si l’année, le mois ou le jour ne
+sont pas des nombres, un code 404 est renvoyé. Si la date est
+invalide, 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/days/2018/111/22``) :
+`````````````````````````````````````````````````````````````````
+.. code:: json
+
+ {
+ "month": "Rentrez un mois valide"
+ }
+
+Exemple d’erreur (``api/groups/<id>/courses/days/2018/11/33``) :
+````````````````````````````````````````````````````````````````
+.. code:: json
+
+ {
+ "day": "Numéro de jour invalide pour le mois"
+ }
+
+``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 ou 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/days/current/``
+----------------------------------------
+Retourne la liste des cours se déroulant dans une salle pendant le
+jour courant, 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/days/<year>/<month>/<day>/``
+-----------------------------------------------------
+Retourne la liste des cours se déroulant dans une salle pendant le
+jour spécifié, par ordre de début. Si l’année, le mois ou le jour ne
+sont pas des nombres, un code 404 est renvoyé. Si la date est
+invalide, 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/days/\<year>/\<month>/\<day>/
+<ref-groups-courses-day-arg>`.
+
+``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 ou 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/Documentation/usage/versions.rst b/Documentation/usage/versions.rst
index 71f122d..ed76ce5 100644
--- a/Documentation/usage/versions.rst
+++ b/Documentation/usage/versions.rst
@@ -113,3 +113,8 @@ Version 0.14.3
d’une vérification lors de la récupération des pages ; si une page
est invalide, elle est re-demandée tant qu’elle est incomplète, et
ce trois fois au maximum.
+
+Version 0.14.4
+--------------
+ - Ajout d’une liste de logiciels lisant les calendriers au format ICS
+ et déconseillant l’usage de Google Calendar.
diff --git a/__init__.py b/__init__.py
index 5140a14..bd80cdb 100644
--- a/__init__.py
+++ b/__init__.py
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with celcatsanitizer. If not, see <http://www.gnu.org/licenses/>.
-VERSION = "0.14.3"
+VERSION = "0.14.4"
__version__ = VERSION
default_app_config = "edt.apps.EdtConfig"
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..b7b69e3
--- /dev/null
+++ b/api/views.py
@@ -0,0 +1,190 @@
+# 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)
+
+ def __get_courses(self, obj, start, end):
+ 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/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<year>\d+)/(?P<month>\d+)/(?P<day>\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<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)
+
+ return self.__get_courses(obj, *get_week(int(year), int(week)))
+
+
+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/templates/calendars.html b/templates/calendars.html
index e6fcd9f..95cda8f 100644
--- a/templates/calendars.html
+++ b/templates/calendars.html
@@ -8,7 +8,40 @@
Le format ICS (ou iCalendar) permet d’importer un calendrier
dans un agenda électronique.<br />
<a href="https://fr.wikipedia.org/wiki/ICalendar">En savoir plus</a>
- </p>
+ <p>
+ Il existe plusieurs logiciels ou services permettant
+ d’utiliser ces fichiers :
+ <ul>
+ <li>Sur Linux et Windows, l’extension Lightning du logiciel
+ Thunderbird ;</li>
+ <li>Sur Mac et iOS, iCloud ;</li>
+ <li>Sur Android, l’application libre ICSx<sup>5</sup> peut les
+ récupérer périodiquement et les afficher sur l’application
+ Agenda de base.
+ <a href="https://f-droid.org/fr/packages/at.bitfire.icsdroid/">Elle
+ est gratuite sur F-Droid</a> ;</li>
+ <li>Sur Web, NextCloud.</li>
+ </ul>
+ <p>
+ <b>N’utilisez pas Google Calendar pour synchroniser un
+ calendrier ICS</b>. Ce service empêche de définir la
+ fréquence de synchronisation ou de forcer une mise à jour et
+ conserve les événements en cache, même si on supprime le
+ calendrier. À cause de cela, il peut y avoir un délai de un
+ jour entre le changement d’une information sur celcatsanitizer
+ et sa prise en compte sans aucun recours possible.
+ <!-- Le lecteur attentif pourra se demander si il n’y a pas de
+conflit d’intérêt entre l’écosystème Android, dans lequel
+l’application de base (Agenda) ne peut se synchroniser qu’à Google
+Calendar à moins d’installer une application tierce (telles que
+DAVDroid ou ICSDroid, malheureusement payantes sur le Play Store mais
+gratuites sur F-Droid).
+
+Il pourra aussi se questionner sur la raison du mauvais support des
+ICS par ce service - serait-ce une technique pour inciter les
+utilisateurs à se servir de Google Calendar en priorité, au détriment
+des formats standards et des autres écosystèmes (par exemple, celui
+d’Apple), et ainsi attirer plus d’utilisateurs ? -->
<ul>
<li><a href="{% url "ics" timetable.year.slug timetable.slug group.slug %}">Un seul ICS pour tous les cours</a></li>
{% for group in groups %}
diff --git a/templates/index.html b/templates/index.html
index e86038e..53cefd5 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -25,7 +25,7 @@
{% endblock %}
</div>
<footer>
- <p>(c) 2018 &ndash; Alban Gruin &ndash; <a href="{% url "django.contrib.flatpages.views.flatpage" url="contact/" %}">contacter</a> &ndash; celcatsanitizer {{ celcatsanitizer_version }} &ndash; <a href="{% url "django.contrib.flatpages.views.flatpage" url="a-propos/" %}">à propos</a><br />
+ <p>(c) 2019 &ndash; Alban Gruin &ndash; <a href="{% url "django.contrib.flatpages.views.flatpage" url="contact/" %}">contacter</a> &ndash; celcatsanitizer {{ celcatsanitizer_version }} &ndash; <a href="{% url "django.contrib.flatpages.views.flatpage" url="a-propos/" %}">à propos</a><br />
Design inspiré par <a href="https://bestmotherfucking.website/">https://bestmotherfucking.website/</a></p>
</footer>
</body>
diff --git a/templates/mention_list.html b/templates/mention_list.html
index 3b9d8e8..08d3d4a 100644
--- a/templates/mention_list.html
+++ b/templates/mention_list.html
@@ -1,6 +1,6 @@
{% extends "index.html" %}
{% block title %}{{ year }} &ndash; {% endblock %}
-{% block pagetitle %}{{ year }} &ndash; Choississez votre mention{% endblock %}
+{% block pagetitle %}{{ year }} &ndash; Choisissez votre mention{% endblock %}
{% block url %}{% url "groups" year.slug element.slug %}{% endblock %}
{% block navigation %}<a href="{% url "index" %}">Retour à la liste des années</a>{% endblock %}
diff --git a/urls.py b/urls.py
index 977ac8d..a5f6b9b 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
@@ -13,11 +13,23 @@
# 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 django.conf import settings
from django.urls import include, path
+
from . import feeds, views
urlpatterns = [
- path("", views.index, name="index"),
+ path("", views.index, name="index")
+]
+
+if getattr(settings, "CS_ENABLE_API", False):
+ from .api.urls import router
+
+ urlpatterns += [
+ path("api/", include(router.urls)),
+ ]
+
+urlpatterns += [
path("pages/", include("django.contrib.flatpages.urls")),
path("salles/", views.rooms, name="rooms"),
path("salles/qsjps", views.qsjps, name="qsjps"),