diff options
-rw-r--r-- | Documentation/conf.py | 2 | ||||
-rw-r--r-- | Documentation/index.rst | 1 | ||||
-rw-r--r-- | Documentation/usage/installation.rst | 39 | ||||
-rw-r--r-- | Documentation/usage/rest.rst | 753 | ||||
-rw-r--r-- | Documentation/usage/versions.rst | 5 | ||||
-rw-r--r-- | __init__.py | 2 | ||||
-rw-r--r-- | api/serializers.py | 57 | ||||
-rw-r--r-- | api/urls.py | 25 | ||||
-rw-r--r-- | api/views.py | 190 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | templates/calendars.html | 35 | ||||
-rw-r--r-- | templates/index.html | 2 | ||||
-rw-r--r-- | templates/mention_list.html | 2 | ||||
-rw-r--r-- | urls.py | 16 |
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 – Alban Gruin – <a href="{% url "django.contrib.flatpages.views.flatpage" url="contact/" %}">contacter</a> – celcatsanitizer {{ celcatsanitizer_version }} – <a href="{% url "django.contrib.flatpages.views.flatpage" url="a-propos/" %}">à propos</a><br /> + <p>(c) 2019 – Alban Gruin – <a href="{% url "django.contrib.flatpages.views.flatpage" url="contact/" %}">contacter</a> – celcatsanitizer {{ celcatsanitizer_version }} – <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 }} – {% endblock %} -{% block pagetitle %}{{ year }} – Choississez votre mention{% endblock %} +{% block pagetitle %}{{ year }} – 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 %} @@ -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"), |