From 5935f588139e2860636bac9c573de99d1fd3c7e1 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Mon, 31 Dec 2018 12:02:41 +0100 Subject: templates: correction d’une faute d’orthographe On dit « choisissez », pas « choississez ». Signed-off-by: Alban Gruin --- templates/mention_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 %}Retour à la liste des années{% endblock %} -- cgit v1.2.1 From 0bafcacdad912a598cdf4e031cab5059efb30b18 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 23 Sep 2018 16:40:14 +0200 Subject: tests: ajout de tests pour le parseur UPS2018 Le parseur est une des parties les plus importantes de celcatsanitizer, mais ni le parseur 2017, ni le parseur 2018 n’ont eu de test unitaires à proprement parler. Jusqu’ici, pour tester ce composant, on ajoutait une source dans la base, on la récupérait, et on regardait si tout correspondait plus ou moins. Cette technique a plusieurs inconvénients : c’était une tâche rébarbative et pas systématiquement effectuée, ce qui a posé quelques problèmes par le passé, certains cas pouvaient ne pas se trouver dans la source au moment de la récupération, et ce n’était pas reproductible proprement. Rajouter des tests permettra donc de tester efficacement le parseur, avec tous les cas de figure, rapidement et en utilisant seulement des ressources locales. Pour éviter d’utiliser le réseau, le module requests est mocké lorsqu’on teste des fonctions qui l’utilisent. L’initialisation du parseur et ses fonctions __get_event(), get_events(), get_source() et get_update_date() (ainsi que la fonction find_events_list() de manière indirecte) sont testées. Signed-off-by: Alban Gruin --- tests.py | 227 +++++++++++++++++++++++++++++++++++++++++ tests/data/2018/october.html | 50 +++++++++ tests/data/2018/september.html | 51 +++++++++ 3 files changed, 328 insertions(+) create mode 100644 tests/data/2018/october.html create mode 100644 tests/data/2018/september.html diff --git a/tests.py b/tests.py index 2568688..a6c84d7 100644 --- a/tests.py +++ b/tests.py @@ -13,13 +13,42 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . +from unittest import mock + from django.test import TestCase from django.utils import timezone +from .management.parsers.ups2018 import Parser as UPS2018Parser from .models import Course, Group, Room, Source, Timetable, Year from .utils import tz_now import datetime +import os + + +def mock_requests_get(*args, **kwargs): + class MockedResponse: + def __init__(self, content=""): + self.encoding = "utf-8" + self.content = content + self.status = 200 + + def raise_for_status(self): + return + + def mocked_response_from_file(filename): + module_dir = os.path.dirname(__file__) + filepath = os.path.join(module_dir, filename) + with open(filepath, "r") as response: + return MockedResponse(response.read()) + + if args[0] == "https://example.org/2018": + if not kwargs["params"]: + return mocked_response_from_file("tests/data/2018/september.html") + elif kwargs["params"].get("Date") == "20181001": + return mocked_response_from_file("tests/data/2018/october.html") + else: + return MockedResponse("") class CourseTestCase(TestCase): @@ -345,3 +374,201 @@ class RoomTestCase(TestCase): self.assertNotIn(self.rooms[4], rooms) self.assertIn(self.rooms[5], rooms) self.assertIn(self.rooms[6], rooms) + + +class UPS2018ParserTestCase(TestCase): + @mock.patch("requests.get", side_effect=mock_requests_get) + def setUp(self, *args, **kwargs): + source = Source.objects.create(url="https://example.org/2018") + self.room = Room.objects.create(name="Salle quelconque") + self.room2 = Room.objects.create(name="Salle quelconque 2") + + self.group = Group(celcat_name="L3 Info s1 CMA") + self.group.source = source + self.group.save() + + self.parser = UPS2018Parser(source) + + def test_get_event(self): + get_event = self.parser._Parser__get_event + + event = get_event( + {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", + "text": "(10:00-12:00)
COURS/TD
Cours quelconque;AAA" + "
L3 Info s1 CMA;L3 Info s1 TDA2
" + "Salle quelconque;Salle quelconque 2
Commentaire"}, + timezone.make_aware(datetime.datetime(2018, 9, 21)), + timezone.make_aware(datetime.datetime(2018, 9, 1)), + timezone.make_aware(datetime.datetime(2018, 10, 1)), + 2018, 38) + + ngroup = Group.objects.filter(name="L3 Info s1 TDA2").first() + self.assertIsNotNone(ngroup) + + self.assertEqual(event.name, "Cours quelconque, AAA") + self.assertEqual(event.type, "COURS/TD") + self.assertIn(self.group, event.groups.all()) + self.assertIn(ngroup, event.groups.all()) + self.assertEqual(event.groups.count(), 2) + self.assertIn(self.room, event.rooms.all()) + self.assertIn(self.room2, event.rooms.all()) + self.assertEqual(event.rooms.count(), 2) + self.assertEqual(event.notes, "Commentaire") + self.assertEqual(event.begin, timezone.make_aware( + datetime.datetime(2018, 9, 21, 10, 0, 0))) + self.assertEqual(event.end, timezone.make_aware( + datetime.datetime(2018, 9, 21, 12, 0, 0))) + + events = [ + { + "text": "(10:00-12:00)
COURS/TD
Cours quelconque" + "
L3 Info s1 CMA
Salle quelconque", + "name": "Cours quelconque", "type": "COURS/TD", + "group": self.group, + "room": self.room, + }, + { + "text": "(10:00-12:00)
COURS/TD
Cours quelconque" + "
L3 Info s1 TDA2
Salle quelconque 3", + "name": "Cours quelconque", + "type": "COURS/TD", + "group": ngroup, + "notes": "Salle quelconque 3" + }, + { + "text": "(10:00-12:00)
COURS/TD
Cours quelconque" + "
L3 Info s1 CMA
Salle quelconque 3
Commentaire", + "name": "Cours quelconque", + "type": "COURS/TD", + "group": self.group, + "notes": "Salle quelconque 3\nCommentaire", + }, + { + "text": "(10:00-12:00)
COURS/TD" + "
L3 Info s1 CMA
Salle quelconque 3", + "name": "COURS/TD", + "group": self.group, + "notes": "Salle quelconque 3" + }, + { + "text": "COURS/TD
L3 Info s1 CMA
Salle quelconque 3", + "name": "COURS/TD", + "group": self.group, + "notes": "Salle quelconque 3" + }, + { + "text": "L3 Info s1 CMA
Salle quelconque", + "name": "Sans nom", + "group": self.group, + "room": self.room + }, + { + "text": "L3 Info s1 CMA
Salle quelconque 3", + "name": "Sans nom", + "group": self.group, + "notes": "Salle quelconque 3" + }, + { + "text": "(10:00-12:00)
L3 Info s1 CMA
Salle quelconque", + "name": "Sans nom", + "group": self.group, + "room": self.room + } + ] + + for e in events: + event = get_event( + {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", + "text": e["text"]}, + timezone.make_aware(datetime.datetime(2018, 9, 21)), + timezone.make_aware(datetime.datetime(2018, 9, 1)), + timezone.make_aware(datetime.datetime(2018, 10, 1)), + 2018, 38) + + self.assertEqual(event.name, e["name"]) + self.assertIn(e["group"], event.groups.all()) + self.assertEqual(event.groups.count(), 1) + + if "type" in e: + self.assertEqual(event.type, e["type"]) + else: + self.assertIsNone(event.type) + + if "room" in e: + self.assertIn(e["room"], event.rooms.all()) + self.assertEqual(event.rooms.count(), 1) + else: + self.assertEqual(event.rooms.count(), 0) + + if "notes" in e: + self.assertEqual(event.notes, e["notes"]) + else: + self.assertIsNone(event.notes) + + event = get_event( + {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", + "text": "Global Event"}, + timezone.make_aware(datetime.datetime(2018, 9, 21)), + timezone.make_aware(datetime.datetime(2018, 9, 1)), + timezone.make_aware(datetime.datetime(2018, 10, 1)), + 2018, 38) + self.assertIsNone(event) + + event = get_event( + {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", + "text": "L3 Info s1 CMA
Salle quelconque 2"}, + timezone.make_aware(datetime.datetime(2018, 9, 21)), + timezone.make_aware(datetime.datetime(2018, 9, 1)), + timezone.make_aware(datetime.datetime(2018, 10, 1)), + 2018, 39) + self.assertIsNone(event) + + @mock.patch("requests.get", side_effect=mock_requests_get) + def test_get_events(self, *args, **kwargs): + self.parser.get_source() + courses = [ + {"begin": + timezone.make_aware(datetime.datetime(2018, 9, 21, 10, 00, 00)), + "end": + timezone.make_aware(datetime.datetime(2018, 9, 21, 12, 00, 00))}, + {"begin": + timezone.make_aware(datetime.datetime(2018, 10, 22, 10, 00, 00)), + "end": + timezone.make_aware(datetime.datetime(2018, 10, 22, 12, 00, 00))} + ] + + for i, course in enumerate(self.parser.get_events( + timezone.make_aware(datetime.datetime(2018, 9, 21)))): + self.assertEqual(course.name, "Cours quelconque") + self.assertEqual(course.type, "COURS/TD") + self.assertIn(self.group, course.groups.all()) + self.assertEqual(course.groups.count(), 1) + self.assertIn(self.room, course.rooms.all()) + self.assertEqual(course.rooms.count(), 1) + self.assertIsNone(course.notes) + self.assertEqual(course.begin, courses[i]["begin"]) + self.assertEqual(course.end, courses[i]["end"]) + + self.assertEqual(i, len(courses) - 1) + + @mock.patch("requests.get", side_effect=mock_requests_get) + def test_get_source(self, *args, **kwargs): + events = self.parser.get_source() + self.assertEquals(events, [ + [{ + "start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", + "text": "(10:00-12:00)
COURS/TD
Cours quelconque" + "
L3 Info s1 CMA
Salle quelconque" + }], [{ + "start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", + "text": "(10:00-12:00)
COURS/TD
Cours quelconque" + "
L3 Info s1 CMA
Salle quelconque" + }, { + "start": "2018-10-22T10:00:00", "end": "2018-10-22T12:00:00", + "text": "(10:00-12:00)
COURS/TD
Cours quelconque" + "
L3 Info s1 CMA
Salle quelconque" + }], [], [], [], [], [], [], [], [], []]) + + def test_get_update_date(self): + # Pas de date de mise à jour dans ce format + self.assertIsNone(self.parser.get_update_date()) diff --git a/tests/data/2018/october.html b/tests/data/2018/october.html new file mode 100644 index 0000000..6f326f9 --- /dev/null +++ b/tests/data/2018/october.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/2018/september.html b/tests/data/2018/september.html new file mode 100644 index 0000000..3db6cbc --- /dev/null +++ b/tests/data/2018/september.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.1 From 070d613b29359465dc8ffdd1dc3ffb116717fc3b Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sat, 29 Sep 2018 20:32:26 +0200 Subject: tests: vérification du nombre de cours dans le test du parseur Dans certaines conditions, un nouveau cours doit être créé dans la base de données, mais parfois non. Pour vérifier qu’un seul cours a bien été rajouté (ou non, en fonction des cas), on teste aussi le nombre de cours présent dans la base de données. Signed-off-by: Alban Gruin --- tests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests.py b/tests.py index a6c84d7..8ec90ba 100644 --- a/tests.py +++ b/tests.py @@ -391,6 +391,7 @@ class UPS2018ParserTestCase(TestCase): def test_get_event(self): get_event = self.parser._Parser__get_event + count = Course.objects.count() event = get_event( {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", @@ -419,6 +420,9 @@ class UPS2018ParserTestCase(TestCase): self.assertEqual(event.end, timezone.make_aware( datetime.datetime(2018, 9, 21, 12, 0, 0))) + self.assertEqual(count, Course.objects.count() - 1) + count += 1 + events = [ { "text": "(10:00-12:00)
COURS/TD
Cours quelconque" @@ -505,6 +509,9 @@ class UPS2018ParserTestCase(TestCase): else: self.assertIsNone(event.notes) + self.assertEqual(count, Course.objects.count() - 1) + count += 1 + event = get_event( {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", "text": "Global Event"}, @@ -513,6 +520,7 @@ class UPS2018ParserTestCase(TestCase): timezone.make_aware(datetime.datetime(2018, 10, 1)), 2018, 38) self.assertIsNone(event) + self.assertEqual(count, Course.objects.count()) event = get_event( {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", @@ -522,6 +530,7 @@ class UPS2018ParserTestCase(TestCase): timezone.make_aware(datetime.datetime(2018, 10, 1)), 2018, 39) self.assertIsNone(event) + self.assertEqual(count, Course.objects.count()) @mock.patch("requests.get", side_effect=mock_requests_get) def test_get_events(self, *args, **kwargs): -- cgit v1.2.1 From 4980d9cb47c99056b2f228408bab536393e100bb Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 9 Oct 2018 20:23:21 +0200 Subject: tests: test du comportement du parseur lorsque la source est mauvaise Il est possible que la source renvoie des pages vides ou incorrectes, il est donc nécessaire de tester le comportement du parseur dans ces cas-là. Ajout d’un modèle de page vide dans les données de test. Signed-off-by: Alban Gruin --- tests.py | 32 +++++++++++++++++++++++++++-- tests/data/2018/empty.html | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/data/2018/empty.html diff --git a/tests.py b/tests.py index 8ec90ba..89aefdb 100644 --- a/tests.py +++ b/tests.py @@ -18,6 +18,7 @@ from unittest import mock from django.test import TestCase from django.utils import timezone +from .management.parsers.abstractparser import ParserError from .management.parsers.ups2018 import Parser as UPS2018Parser from .models import Course, Group, Room, Source, Timetable, Year from .utils import tz_now @@ -43,12 +44,14 @@ def mock_requests_get(*args, **kwargs): return MockedResponse(response.read()) if args[0] == "https://example.org/2018": - if not kwargs["params"]: + if "params" not in kwargs or not kwargs["params"]: return mocked_response_from_file("tests/data/2018/september.html") elif kwargs["params"].get("Date") == "20181001": return mocked_response_from_file("tests/data/2018/october.html") else: - return MockedResponse("") + return mocked_response_from_file("tests/data/2018/empty.html") + + return MockedResponse("") class CourseTestCase(TestCase): @@ -581,3 +584,28 @@ class UPS2018ParserTestCase(TestCase): def test_get_update_date(self): # Pas de date de mise à jour dans ce format self.assertIsNone(self.parser.get_update_date()) + + +class UPS2018BrokenSourceTestCase(TestCase): + @mock.patch("requests.get") + def test_broken_source(self, mock_get): + mock_get.return_value = mock_requests_get("") + + source = Source.objects.create(url="https://example.org/2018") + with self.assertRaises(ParserError): + UPS2018Parser(source) + + @mock.patch("requests.get") + def test_half_broken_source(self, mock_get): + source = Source.objects.create(url="https://example.org/2018") + mock_get.side_effect = [ + mock_requests_get(""), + mock_requests_get(source.url) + ] + + parser = UPS2018Parser(source) + self.assertEqual(parser.months, [ + "September, 2018", "October, 2018", "November, 2018", + "December, 2018", "January, 2019", "February, 2019", "March, 2019", + "April, 2019", "May, 2019", "June, 2019", "July, 2019" + ]) diff --git a/tests/data/2018/empty.html b/tests/data/2018/empty.html new file mode 100644 index 0000000..dde78b6 --- /dev/null +++ b/tests/data/2018/empty.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.1 From 44e17c23fc9285c24a0c1b3ea46e2ed27e55ae05 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 20 Sep 2018 23:57:21 +0200 Subject: tests: test du tag format_rooms Signed-off-by: Alban Gruin --- tests.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/tests.py b/tests.py index 89aefdb..627602c 100644 --- a/tests.py +++ b/tests.py @@ -21,6 +21,7 @@ from django.utils import timezone from .management.parsers.abstractparser import ParserError from .management.parsers.ups2018 import Parser as UPS2018Parser from .models import Course, Group, Room, Source, Timetable, Year +from .templatetags.rooms import format_rooms from .utils import tz_now import datetime @@ -332,13 +333,13 @@ class RoomTestCase(TestCase): group = Group.objects.create(celcat_name="L1 info s2 CMA", source=self.source) - self.rooms = [Room.objects.create(name="0"), - Room.objects.create(name="1"), - Room.objects.create(name="2"), - Room.objects.create(name="3"), - Room.objects.create(name="4"), - Room.objects.create(name="5"), - Room.objects.create(name="6")] + self.rooms = [ + Room.objects.create(name=str(i)) + for i in range(5) + ] + [ + Room.objects.create(name="Amphi {}".format(i)) + for i in range(5, 7) + ] hours = [({"begin": datetime.time(hour=14, minute=0)},), ({"begin": datetime.time(hour=16, minute=0)},), @@ -362,6 +363,23 @@ class RoomTestCase(TestCase): course.groups.add(group) course.rooms.add(room) + def test_format(self): + amphis = self.rooms[-2:] + + self.assertEqual(format_rooms([]), "") + self.assertEqual(format_rooms(self.rooms[:1]), "Salle 0") + self.assertEqual(format_rooms(self.rooms[:2]), "Salles 0, 1") + self.assertEqual(format_rooms([amphis[0]]), "Amphi 5") + self.assertEqual(format_rooms(amphis), "Amphi 5, Amphi 6") + self.assertEqual(format_rooms([amphis[0]] + self.rooms[:1]), + "Amphi 5, salle 0") + self.assertEqual(format_rooms([amphis[0]] + self.rooms[:2]), + "Amphi 5, salles 0, 1") + self.assertEqual(format_rooms(amphis + self.rooms[:1]), + "Amphi 5, Amphi 6, salle 0") + self.assertEqual(format_rooms(amphis + self.rooms[:2]), + "Amphi 5, Amphi 6, salles 0, 1") + def test_qsjps(self): begin = timezone.make_aware(datetime.datetime.combine( self.day, datetime.time(hour=15, minute=0))) -- cgit v1.2.1 From 7084c5238c84f7c232efff91ee60ef86cb23680c Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 27 Jan 2019 16:22:43 +0100 Subject: requirements: ajout d’une dépendance à Django REST Framework Django REST Framework sera utilisé pour réaliser l’API de celcatsanitizer. Signed-off-by: Alban Gruin --- requirements.txt | 1 + 1 file changed, 1 insertion(+) 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 -- cgit v1.2.1 From 66853337a5f6bd2b67f1130e3f4db7febe27de1b Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 27 Jan 2019 19:22:35 +0100 Subject: api: ajout d’un sérialiseur pour chaque modèle Les sérialiseurs permettent de représenter les modèles en JSON. Tous les sérialiseurs exportent tous les champs de leurs modèles respectifs. En plus de cela, le sérialiseur du modèle des cours sérialise en plus les groupes et salles pour ne pas avoir à faire trop d’appels à l’API. Signed-off-by: Alban Gruin --- api/serializers.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 api/serializers.py diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..5d81f44 --- /dev/null +++ b/api/serializers.py @@ -0,0 +1,57 @@ +# Copyright (C) 2019 Alban Gruin +# +# celcatsanitizer is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# celcatsanitizer is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with celcatsanitizer. If not, see . + +from rest_framework import serializers + +from ..models import Course, Group, Room, Source, Timetable, Year + + +class YearSerializer(serializers.ModelSerializer): + class Meta: + model = Year + fields = "__all__" + + +class SourceSerializer(serializers.ModelSerializer): + class Meta: + model = Source + fields = "__all__" + + +class TimetableSerializer(serializers.ModelSerializer): + class Meta: + model = Timetable + fields = "__all__" + + +class GroupSerializer(serializers.ModelSerializer): + class Meta: + model = Group + fields = "__all__" + + +class RoomSerializer(serializers.ModelSerializer): + class Meta: + model = Room + fields = "__all__" + + +class CourseSerializer(serializers.ModelSerializer): + groups = GroupSerializer(many=True, read_only=True) + rooms = RoomSerializer(many=True, read_only=True) + + class Meta: + model = Course + fields = "__all__" -- cgit v1.2.1 From 1764d2d8f4ae02a81dd6bea1c9f8a374d9706b63 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 27 Jan 2019 19:32:38 +0100 Subject: api: ajout des vues de l’API L’API a besoin de vues pour renvoyer des données. Ces vues utilisent les sérialiseurs écrits précédement. Pour l’instant, les sérialiseurs sont assez rudimentaires (ils ne peuvent afficher qu’une liste d’objets ou un seul objet sans filtres ou fonctionnalités supplémentaires), et ne permettent pas d’effectuer de modifications. Signed-off-by: Alban Gruin --- api/views.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 api/views.py diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..c58c9c0 --- /dev/null +++ b/api/views.py @@ -0,0 +1,50 @@ +# Copyright (C) 2019 Alban Gruin +# +# celcatsanitizer is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# celcatsanitizer is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with celcatsanitizer. If not, see . + +from rest_framework import viewsets +from ..models import Course, Group, Room, Source, Timetable, Year +from .serializers import CourseSerializer, GroupSerializer, RoomSerializer, \ + SourceSerializer, TimetableSerializer, YearSerializer + + +class YearViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Year.objects.all().order_by("name") + serializer_class = YearSerializer + + +class SourceViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Source.objects.all().order_by("pk") + serializer_class = SourceSerializer + + +class TimetableViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Timetable.objects.all().select_related("source") \ + .order_by("year", "name") + serializer_class = TimetableSerializer + + +class GroupViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Group.objects.all().order_by("name") + serializer_class = GroupSerializer + + +class RoomViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Room.objects.all().order_by("name") + serializer_class = RoomSerializer + + +class CourseViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Course.objects.all().prefetch_related("groups", "rooms") + serializer_class = CourseSerializer -- cgit v1.2.1 From 549e087ac32484d661197745bccc801856bc2d26 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 27 Jan 2019 19:29:33 +0100 Subject: api: complétion des vues de l’API Ajout de vues permettant de consulter les emplois du temps associés à une année ou à une source, les groupes associés à un emploi du temps, les cours d’un groupe ou d’une salle (soit tous, soit ceux de la semaine courante, soit ceux d’une semaine précise), de lister les semaines de cours, et d’accéder à QSJPS. Signed-off-by: Alban Gruin --- api/views.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 2 deletions(-) diff --git a/api/views.py b/api/views.py index c58c9c0..f6aa620 100644 --- a/api/views.py +++ b/api/views.py @@ -13,8 +13,20 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . +import datetime + +from django.db.models import Count +from django.db.models.functions import ExtractWeek, ExtractYear +from django.utils import timezone + from rest_framework import viewsets +from rest_framework.decorators import action, detail_route +from rest_framework.response import Response + +from ..forms import QSJPSForm from ..models import Course, Group, Room, Source, Timetable, Year +from ..utils import get_current_or_next_week, get_week + from .serializers import CourseSerializer, GroupSerializer, RoomSerializer, \ SourceSerializer, TimetableSerializer, YearSerializer @@ -23,27 +35,135 @@ class YearViewSet(viewsets.ReadOnlyModelViewSet): queryset = Year.objects.all().order_by("name") serializer_class = YearSerializer + @detail_route(methods=["get"], url_path="timetables") + def timetable_list(self, request, pk): + year = self.get_object() + timetables = Timetable.objects.filter(year=year).distinct() \ + .order_by("name") + timetables_json = TimetableSerializer( + self.paginate_queryset(timetables), many=True) + return self.get_paginated_response(timetables_json.data) + class SourceViewSet(viewsets.ReadOnlyModelViewSet): queryset = Source.objects.all().order_by("pk") serializer_class = SourceSerializer + @detail_route(methods=["get"], url_path="timetables") + def timetable_list(self, request, pk): + source = self.get_object() + timetables = Timetable.objects.filter(source=source).distinct() \ + .order_by("name") + timetables_json = TimetableSerializer( + self.paginate_queryset(timetables), many=True) + return self.get_paginated_response(timetables_json.data) + class TimetableViewSet(viewsets.ReadOnlyModelViewSet): queryset = Timetable.objects.all().select_related("source") \ .order_by("year", "name") serializer_class = TimetableSerializer + @detail_route(methods=["get"], url_path="groups") + def group_list(self, request, pk): + timetable = self.get_object() + groups = Group.objects.filter(source=timetable.source).distinct() \ + .order_by("name") + groups_json = GroupSerializer(self.paginate_queryset(groups), + many=True) + return self.get_paginated_response(groups_json.data) + + +class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): + @detail_route(methods=["get"], url_path="courses") + def course_list(self, request, pk): + obj = self.get_object() + courses = Course.objects.get_courses(obj).prefetch_related("groups") + courses_json = CourseSerializer(self.paginate_queryset(courses), + many=True) + return self.get_paginated_response(courses_json.data) + + @detail_route(methods=["get"], url_path="courses/weeks/current") + def current_week(self, request, pk): + obj = self.get_object() + start, end = get_week(*get_current_or_next_week()) + + courses = Course.objects.get_courses(obj, + begin__gte=start, end__lt=end) \ + .prefetch_related("groups") + courses_json = CourseSerializer(self.paginate_queryset(courses), + many=True) + return self.get_paginated_response(courses_json.data) + + @detail_route(methods=["get"], + url_path="courses/weeks/(?P\d+)/(?P\d+)") + def other_week(self, request, pk, year, week): + obj = self.get_object() + + errors = {} + if not year.isdigit(): + errors["year"] = "Rentrez une année valide" + if not week.isdigit() or not 0 < int(week) <= 53: + errors["week"] = "Rentrez une semaine valide" + if errors: + return Response(errors, status=400) -class GroupViewSet(viewsets.ReadOnlyModelViewSet): + start, end = get_week(int(year), int(week)) + + courses = Course.objects.get_courses(obj, + begin__gte=start, end__lt=end) \ + .prefetch_related("groups") + courses_json = CourseSerializer(self.paginate_queryset(courses), + many=True) + return self.get_paginated_response(courses_json.data) + + +class GroupViewSet(CourseListGroupSet): queryset = Group.objects.all().order_by("name") serializer_class = GroupSerializer + @detail_route(methods=["get"], url_path="courses/weeks") + def weeks(self, request, pk): + group = self.get_object() + groups = Group.objects.get_parents(group) + + courses = Course.objects.filter(groups__in=groups) \ + .order_by("year", "week") \ + .annotate(year=ExtractYear("begin"), + week=ExtractWeek("begin")) \ + .values("year", "week") \ + .annotate(c=Count("*")) + + weeks = [get_week(course["year"], course["week"])[0] + for course in courses] + + return Response(weeks) -class RoomViewSet(viewsets.ReadOnlyModelViewSet): + +class RoomViewSet(CourseListGroupSet): queryset = Room.objects.all().order_by("name") serializer_class = RoomSerializer + @action( + methods=["get"], + detail=False, + url_path="qsjps/(?P[0-9\-]+)/(?P[0-9:]+)/(?P[0-9:]+)") + def qsjps(self, request, day, begin, end): + form = QSJPSForm({"day": day, "begin": begin, "end": end}) + if not form.is_valid(): + return Response(form.errors, status=400) + + day = form.cleaned_data["day"] + begin_hour = form.cleaned_data["begin"] + end_hour = form.cleaned_data["end"] + + begin = timezone.make_aware(datetime.datetime.combine(day, begin_hour)) + end = timezone.make_aware(datetime.datetime.combine(day, end_hour)) + + rooms = Room.objects.qsjps(begin, end) + rooms_json = RoomSerializer(rooms, many=True) + return Response(rooms_json.data) + class CourseViewSet(viewsets.ReadOnlyModelViewSet): queryset = Course.objects.all().prefetch_related("groups", "rooms") -- cgit v1.2.1 From 1a582c97ecf369ca7ca170aeed845aa05ce72432 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 27 Jan 2019 19:32:55 +0100 Subject: api: ajout d’un routeur pour l’API, branchement sur urls.py Signed-off-by: Alban Gruin --- api/urls.py | 25 +++++++++++++++++++++++++ urls.py | 5 ++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 api/urls.py diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..0164a4f --- /dev/null +++ b/api/urls.py @@ -0,0 +1,25 @@ +# Copyright (C) 2019 Alban Gruin +# +# celcatsanitizer is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# celcatsanitizer is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with celcatsanitizer. If not, see . + +from rest_framework import routers +from . import views + +router = routers.DefaultRouter() +router.register(r"years", views.YearViewSet) +router.register(r"sources", views.SourceViewSet) +router.register(r"timetables", views.TimetableViewSet) +router.register(r"rooms", views.RoomViewSet) +router.register(r"groups", views.GroupViewSet) +router.register(r"courses", views.CourseViewSet) 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 . 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"), -- cgit v1.2.1 From 9987d47ec8c74d649de2a7d09ef62beb885949d9 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 27 Jan 2019 21:47:39 +0100 Subject: Documentation: ajout d’une doc sur l’API REST Signed-off-by: Alban Gruin --- Documentation/index.rst | 1 + Documentation/usage/installation.rst | 19 + Documentation/usage/rest.rst | 697 +++++++++++++++++++++++++++++++++++ 3 files changed, 717 insertions(+) create mode 100644 Documentation/usage/rest.rst 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é `. + +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//`` +------------------- +Retourne seulement une année. + +Exemple : +````````` +.. code:: json + + { + "id": 1, + "name": "L3", + "slug": "l3" + } + +``api/years//timetables`` +----------------------------- +Liste les emplois du temps associés à une année par ordre alphabétique +de nom. :ref:`Le résultat peut être paginé `. + +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é `. + +Exemple : +````````` +.. code:: json + + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "name": "Info", + "slug": "info", + "year": 1, + "source": 1 + } + ] + } + +``api/timetables//`` +------------------------ +Retourne seulement un emploi du temps. + +Exemple : +````````` +.. code:: json + + { + "id": 1, + "name": "Info", + "slug": "info", + "year": 1, + "source": 1 + } + +``api/timetables//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é +`. + +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é `. + +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//`` +--------------------- +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//timetables/`` +-------------------------------- +Renvoie la liste des emplois du temps associé à une source triés par +ordre alphabétique. :ref:`Le résultat peut être paginé +`. + +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é `. + +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//`` +-------------------- +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//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é `. + +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//courses/weeks/`` +---------------------------------- +Retourne la liste des semaines de cours d’un groupe. + +Exemple : +````````` +.. code:: json + + [ + "2018-12-03T00:00:00+01:00" + ] + +``api/groups//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é `. +Le format du résultat est identique à celui de +:ref:`api/groups/\/courses/ `. + +.. _ref-groups-courses-week-arg: + +``api/groups//courses/weeks///`` +------------------------------------------------ +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é `. Le format du résultat est identique à celui +de :ref:`api/groups/\/courses/ `. + +Exemple d’erreur (``api/groups//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é `. + +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/`` +------------------ +Renvoie une seule salle. + +Exemple : +````````` +.. code:: json + + { + "id": 26, + "name": "1R1-010", + "slug": "1r1-010" + } + +``api/rooms//courses/`` +--------------------------- +Renvoie la liste des cours se déroulant dans une salle par ordre de +début. :ref:`Le résultat peut être paginé `. Le +format du résultat est identique à celui de +:ref:`api/groups/\/courses/ `. + +``api/rooms//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é `. Le +format du résultat est identique à celui de +:ref:`api/groups/\/courses/ `. + +``api/rooms//courses/weeks///`` +----------------------------------------------- +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é `. Le +format du résultat est identique à celui de +:ref:`api/groups/\/courses/weeks/\/\ +`. + +``api/rooms/qsjps////`` +---------------------------------------- +Fournit un accès à QSJPS. ```` est une date devant être formatée +de cette manière : ``YYYY-MM-DD``. ```` et ```` sont des +heures qui doivent être formatées de cette manière : ``HH:mm``. La +valeur de ```` doit être inférieure à celle de ````. + +Renvoie la liste des salles vides le début du jour ```` de +```` à ````. + +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é +`. + +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/`` +-------------------- +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. -- cgit v1.2.1 From 61eb694cd445fd5f17c4dc6ded0b1d1f5eccdf57 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 5 Feb 2019 18:00:59 +0100 Subject: Documentation: correction de la doc de l’API Signed-off-by: Alban Gruin --- Documentation/usage/rest.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Documentation/usage/rest.rst b/Documentation/usage/rest.rst index bcaada9..60c8d44 100644 --- a/Documentation/usage/rest.rst +++ b/Documentation/usage/rest.rst @@ -98,8 +98,8 @@ Exemple : Emplois du temps ================ -``api/timetables`` ------------------- +``api/timetables/`` +------------------- Liste les emplois du temps par ordre d’année (ID associé) puis de nom. :ref:`Le résultat peut être paginé `. @@ -688,10 +688,10 @@ paginés se trouvent dans ce genre de structure : ] } - - ``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. +- ``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. -- cgit v1.2.1 From 7b280f8e2a901297112bc15becb1704205e3e901 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 5 Feb 2019 19:14:25 +0100 Subject: api/views: ajout de routes pour lister les cours d’une seule journée Signed-off-by: Alban Gruin --- api/views.py | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/api/views.py b/api/views.py index f6aa620..b7b69e3 100644 --- a/api/views.py +++ b/api/views.py @@ -83,11 +83,7 @@ class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): many=True) return self.get_paginated_response(courses_json.data) - @detail_route(methods=["get"], url_path="courses/weeks/current") - def current_week(self, request, pk): - obj = self.get_object() - start, end = get_week(*get_current_or_next_week()) - + def __get_courses(self, obj, start, end): courses = Course.objects.get_courses(obj, begin__gte=start, end__lt=end) \ .prefetch_related("groups") @@ -95,6 +91,37 @@ class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): many=True) return self.get_paginated_response(courses_json.data) + @detail_route(methods=["get"], url_path="courses/days/current") + def current_day(self, request, pk): + obj = self.get_object() + start = datetime.date.today() + end = start + datetime.timedelta(days=1) + return self.__get_courses(obj, start, end) + + @detail_route(methods=["get"], url_path="courses/days/(?P\d+)/(?P\d+)/(?P\d+)") + def other_day(self, request, pk, year, month, day): + obj = self.get_object() + + try: + start = datetime.date(int(year), int(month), int(day)) + except ValueError as v: + errors = {} + message = v.args[0] + if message.split(" ")[0] == "month": + errors["month"] = "Rentrez un mois invalide" + else: + errors["day"] = "Numéro de jour invalide pour le mois" + return Response(errors, status=400) + + end = start + datetime.timedelta(days=1) + return self.__get_courses(obj, start, end) + + @detail_route(methods=["get"], url_path="courses/weeks/current") + def current_week(self, request, pk): + obj = self.get_object() + start, end = get_week(*get_current_or_next_week()) + return self.__get_courses(obj, start, end) + @detail_route(methods=["get"], url_path="courses/weeks/(?P\d+)/(?P\d+)") def other_week(self, request, pk, year, week): @@ -108,14 +135,7 @@ class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): if errors: return Response(errors, status=400) - start, end = get_week(int(year), int(week)) - - courses = Course.objects.get_courses(obj, - begin__gte=start, end__lt=end) \ - .prefetch_related("groups") - courses_json = CourseSerializer(self.paginate_queryset(courses), - many=True) - return self.get_paginated_response(courses_json.data) + return self.__get_courses(obj, *get_week(int(year), int(week))) class GroupViewSet(CourseListGroupSet): -- cgit v1.2.1 From 09a5b8cb97769853ac8a3982bcb340c4c1422498 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Tue, 5 Feb 2019 19:14:48 +0100 Subject: Documentation: ajout des nouvelles routes dans la doc et corrections Signed-off-by: Alban Gruin --- Documentation/usage/rest.rst | 71 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/Documentation/usage/rest.rst b/Documentation/usage/rest.rst index 60c8d44..4908d8a 100644 --- a/Documentation/usage/rest.rst +++ b/Documentation/usage/rest.rst @@ -64,8 +64,8 @@ Exemple : "slug": "l3" } -``api/years//timetables`` ------------------------------ +``api/years//timetables/`` +------------------------------ Liste les emplois du temps associés à une année par ordre alphabétique de nom. :ref:`Le résultat peut être paginé `. @@ -398,6 +398,41 @@ Exemple : ] } +``api/groups//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é `. Le format du résultat est identique à +celui de :ref:`api/groups/\/courses/ `. + +.. _ref-groups-courses-day-arg: + +``api/groups//courses/days////`` +------------------------------------------------------ +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é `. Le +format du résultat est identique à celui de +:ref:`api/groups/\/courses/ `. + +Exemple d’erreur (``api/groups//courses/days/2018/111/22``) : +````````````````````````````````````````````````````````````````` +.. code:: json + + { + "month": "Rentrez un mois valide" + } + +Exemple d’erreur (``api/groups//courses/days/2018/11/33``) : +```````````````````````````````````````````````````````````````` +.. code:: json + + { + "day": "Numéro de jour invalide pour le mois" + } + ``api/groups//courses/weeks/`` ---------------------------------- Retourne la liste des semaines de cours d’un groupe. @@ -423,7 +458,7 @@ Le format du résultat est identique à celui de ``api/groups//courses/weeks///`` ------------------------------------------------ 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 +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 @@ -473,8 +508,8 @@ Exemple : ] } -``api/rooms/`` ------------------- +``api/rooms//`` +------------------- Renvoie une seule salle. Exemple : @@ -494,6 +529,24 @@ début. :ref:`Le résultat peut être paginé `. Le format du résultat est identique à celui de :ref:`api/groups/\/courses/ `. +``api/rooms//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é +`. Le format du résultat est identique à celui de +:ref:`api/groups/\/courses/ `. + +``api/rooms//courses/days////`` +----------------------------------------------------- +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é `. Le +format du résultat est identique à celui de +:ref:`api/groups/\/courses/days/\/\/\/ +`. + ``api/rooms//courses/weeks/current/`` ----------------------------------------- Renvoie la liste des cours se déroulant dans une salle pendant la @@ -505,12 +558,12 @@ format du résultat est identique à celui de ``api/rooms//courses/weeks///`` ----------------------------------------------- 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, +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é `. Le format du résultat est identique à celui de -:ref:`api/groups/\/courses/weeks/\/\ +:ref:`api/groups/\/courses/weeks/\/\/ `. ``api/rooms/qsjps////`` @@ -629,8 +682,8 @@ Exemple : ] } -``api/courses/`` --------------------- +``api/courses//`` +--------------------- Renvoie un seul cours. Exemple : -- cgit v1.2.1 From 2386b8d41d88c1408411b378ce5e2a8c09c382e7 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 13 Feb 2019 22:20:46 +0100 Subject: templates: bonne année 2019 Signed-off-by: Alban Gruin --- templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 %} -- cgit v1.2.1 From 3786f8ac9be60d6f05a8281564270225b03f5326 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 13 Feb 2019 19:59:07 +0100 Subject: commands: ajout d’un modèle abstrait de commande accédant au parseur Pour les besoins de l’ajout de la notion de module, un nouvel outil permettant de lister les attributs d’un cours sera rajouté. À l’instar de timetables, il fera aussi appel au parseur. Pour éviter de dupliquer du code, la partie accès au parseur est déplacé dans une autre classe. Signed-off-by: Alban Gruin --- management/commands/__parsercommand.py | 26 ++++++++++++++++++++++++++ management/commands/timetables.py | 16 ++++------------ 2 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 management/commands/__parsercommand.py diff --git a/management/commands/__parsercommand.py b/management/commands/__parsercommand.py new file mode 100644 index 0000000..99480cc --- /dev/null +++ b/management/commands/__parsercommand.py @@ -0,0 +1,26 @@ +# Copyright (C) 2019 Alban Gruin +# +# celcatsanitizer is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# celcatsanitizer is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with celcatsanitizer. If not, see . + +from importlib import import_module + +from django.conf import settings + +DEFAULT_PARSER = "edt.management.parsers.ups2017" + + +class ParserCommand: + def get_parser(self): + parser_module = getattr(settings, "CS_PARSER", DEFAULT_PARSER) + return getattr(import_module(parser_module), "Parser") diff --git a/management/commands/timetables.py b/management/commands/timetables.py index ee33f7e..f71accf 100644 --- a/management/commands/timetables.py +++ b/management/commands/timetables.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,20 +13,16 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . -from importlib import import_module - import datetime import traceback -from django.conf import settings from django.core.management.base import BaseCommand from django.db import transaction from django.db.models import Min from ...models import Course, Source from ...utils import get_week, tz_now - -DEFAULT_PARSER = "edt.management.parsers.ups2017" +from .__parsercommand import ParserCommand def delete_courses_in_week(source, year, week, today): @@ -122,7 +118,7 @@ def process_timetable(source, force, parser_cls, year=None, weeks=None): process_timetable_week(source, force, parser) -class Command(BaseCommand): +class Command(BaseCommand, ParserCommand): help = "Fetches registered celcat timetables" def add_arguments(self, parser): @@ -134,14 +130,10 @@ class Command(BaseCommand): nargs="+") parser.add_argument("--year", type=int, nargs=1) - def __get_parser(self): - parser_module = getattr(settings, "CS_PARSER", DEFAULT_PARSER) - return getattr(import_module(parser_module), "Parser") - def handle(self, *args, **options): year = None errcount = 0 - parser = self.__get_parser() + parser = self.get_parser() if options["all"]: weeks = None -- cgit v1.2.1 From d4060a8336554b6f7e154785a1f51f802ee90492 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 13 Feb 2019 20:04:03 +0100 Subject: commands: ajout d’une commande pour lister les propriétés des cours Pour pouvoir analyser plus facilement les attributs d’un cours, un nouvel outil est rajouté pour lister tous les attributs d’un ou plusieurs cours. Il fait appel au parseur et permet de sélectionner une source, et de limiter le nombre de cours affichés. Signed-off-by: Alban Gruin --- Documentation/index.rst | 1 + Documentation/usage/commands/printvalues.rst | 48 ++++++++++++++++++++++++++++ management/commands/printvalues.py | 45 ++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 Documentation/usage/commands/printvalues.rst create mode 100644 management/commands/printvalues.py diff --git a/Documentation/index.rst b/Documentation/index.rst index e793dd2..7051b93 100644 --- a/Documentation/index.rst +++ b/Documentation/index.rst @@ -61,6 +61,7 @@ Utilisation de celcatsanitizer usage/installation usage/commands/cleancourses usage/commands/listtimetables + usage/commands/printvalues usage/commands/reparse usage/commands/timetables usage/versions diff --git a/Documentation/usage/commands/printvalues.rst b/Documentation/usage/commands/printvalues.rst new file mode 100644 index 0000000..7d53d44 --- /dev/null +++ b/Documentation/usage/commands/printvalues.rst @@ -0,0 +1,48 @@ +=============== +``printvalues`` +=============== + +``printvalues`` affiche le contenu brut d’un ou plusieurs cours, sans +chercher à interpréter son contenu ou à l’enregistrer dans la base de +données. + +Utilisation +=========== +.. code:: shell + + $ ./manage.py printvalues --source id [--limit nb] + +``--source`` permet de spécifier la source depuis laquelle les cours +doivent être récupérés. ``id`` correspond à l’ID de la source, +trouvable à l’aide de la commande :doc:`listtimetables`. + +``--limit`` permet de limiter le nombre de cours affichés. ``nb`` +correspond au nombre maximum de cours affichés. + +Format de sortie +================ +:: + + { + "backColor": "#7D4F72", + "clickDisabled": true, + "doubleClickDisabled": true, + "end": "2019-01-28T09:45:00", + "html": "
(07:45-09:45)
COURS/TD
ELINF6Q1 - BIOLOGIE
L3 INFO s2 CMA
U3-307
", + "id": "76330023", + "moveDisabled": true, + "resizeDisabled": true, + "sort": [], + "start": "2019-01-28T07:45:00", + "tag": [ + "celcat", + "sat_notvalid", + "1", + "reg_notmark", + "ELINF6Q1", + "7491453" + ], + "text": "(07:45-09:45)
COURS/TD
ELINF6Q1 - BIOLOGIE
L3 INFO s2 CMA
U3-307", + "toolTip": "(07:45-09:45)
COURS/TD
ELINF6Q1 - BIOLOGIE
L3 INFO s2 CMA
U3-307" + } + Done. diff --git a/management/commands/printvalues.py b/management/commands/printvalues.py new file mode 100644 index 0000000..91dd18b --- /dev/null +++ b/management/commands/printvalues.py @@ -0,0 +1,45 @@ +# Copyright (C) 2019 Alban Gruin +# +# celcatsanitizer is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# celcatsanitizer is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with celcatsanitizer. If not, see . + +from django.core.management.base import BaseCommand + +from ...models import Source +from .__parsercommand import ParserCommand + +import json + + +class Command(BaseCommand, ParserCommand): + help = "List values from courses from a source" + + def add_arguments(self, parser): + parser.add_argument("--source", type=int, nargs=1, required=True) + parser.add_argument("--limit", type=int, nargs=1) + + def handle(self, *args, **options): + source = Source.objects.get(pk=options["source"][0]) + parser = self.get_parser()(source) + events = [event for month in parser.get_source() for event in month] + + i = 0 + limit = len(events) + if options["limit"] is not None: + limit = min(options["limit"][0], limit) + + while i < limit: + self.stdout.write(json.dumps(events[i], indent=4, sort_keys=True)) + i += 1 + + self.stdout.write(self.style.SUCCESS("Done.")) -- cgit v1.2.1 From b2eaa3be85b30732c4e6c083c6f0413c02dbaec4 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 3 Jan 2019 01:11:23 +0100 Subject: UPS2018: ajout du champ celcat_id Les cours dans celcat ont un champ id. Ce changement permet de le stocker dans la base de données (sous la forme d’un entier) et de l’afficher dans l’interface d’administration. Pour l’instant, on ne sait pas si cette valeur est unique ou non. Il n’y a donc pas de contraintes sur ce champ pour le moment. Signed-off-by: Alban Gruin --- admin.py | 10 ++++++---- management/parsers/ups2018.py | 5 +++-- models.py | 4 +++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/admin.py b/admin.py index 0dc7987..c40a148 100644 --- a/admin.py +++ b/admin.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 @@ -72,10 +72,12 @@ class RoomAdmin(admin.ModelAdmin): @admin.register(Course) class CourseAdmin(admin.ModelAdmin): fieldsets = ( - (None, {"fields": ("name", "type", "source", "groups", "rooms", - "last_update",)}), + (None, {"fields": ("name", "type", "source", "groups", "rooms",)}), ("Horaires", {"fields": ("begin", "end",)}), - ("Remarques", {"fields": ("notes",)}),) + ("Remarques", {"fields": ("notes",)}), + ("Avancé", {"fields": ("celcat_id", "last_update",), + "classes": ("collapse",)}),) list_display = ("name", "type", "source", "begin", "end",) list_filter = ("type", "source__timetables", "groups",) ordering = ("begin",) + readonly_fields = ("celcat_id", "last_update",) diff --git a/management/parsers/ups2018.py b/management/parsers/ups2018.py index f1da5bf..e3afbe5 100644 --- a/management/parsers/ups2018.py +++ b/management/parsers/ups2018.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018 Alban Gruin +# Copyright (C) 2018-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 @@ -114,7 +114,8 @@ class Parser(AbstractParser): return course = Course.objects.create( - source=self.source, begin=begin, end=end + source=self.source, begin=begin, end=end, + celcat_id=int(event["id"]) ) min_i = 0 diff --git a/models.py b/models.py index c8e7b3d..b1a6a1e 100644 --- a/models.py +++ b/models.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 @@ -246,6 +246,8 @@ class Course(models.Model): last_update = models.DateTimeField(verbose_name="dernière mise à jour", default=timezone.now) + celcat_id = models.IntegerField(verbose_name="ID Celcat", null=True) + def __str__(self): return self.name -- cgit v1.2.1 From 297632390e6ec051e315e6d9545d0110a41a8880 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 13 Feb 2019 22:02:09 +0100 Subject: UPS2018: récupération du module (UE) correspondant à un cours Il peut être intéressant de lister les cours par module (UE). Une table est donc rajoutée pour stocker cette information et permettre d’effectuer des recherches et des tris. Signed-off-by: Alban Gruin --- admin.py | 4 ++-- management/parsers/ups2018.py | 9 ++++++++- models.py | 13 +++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/admin.py b/admin.py index c40a148..5350aad 100644 --- a/admin.py +++ b/admin.py @@ -14,7 +14,7 @@ # along with celcatsanitizer. If not, see . from django.contrib import admin -from .models import Course, Group, Room, Source, Timetable, Year +from .models import Course, Group, Module, Room, Source, Timetable, Year def make_hidden(modeladmin, request, queryset): @@ -62,7 +62,7 @@ class GroupAdmin(admin.ModelAdmin): actions = (make_hidden, make_visible,) -@admin.register(Room) +@admin.register(Room, Module) class RoomAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} list_display = ("name",) diff --git a/management/parsers/ups2018.py b/management/parsers/ups2018.py index e3afbe5..afbfc4b 100644 --- a/management/parsers/ups2018.py +++ b/management/parsers/ups2018.py @@ -26,7 +26,7 @@ from django.utils import timezone import lxml.html import requests -from ...models import Course, Group, Room +from ...models import Course, Group, Module, Room from ...utils import get_current_week, get_week from .abstractparser import AbstractParser, ParserError @@ -137,6 +137,13 @@ class Parser(AbstractParser): # par un dictionnaire classique. names = OrderedDict.fromkeys(data[i - 1].split(';')) course.name = ", ".join(names.keys()) + + module_names = [t for t in event["tag"] + if len(t) > 0 and + any(n.startswith(t) for n in names.keys())] + if len(module_names) > 0: + module, _ = Module.objects.get_or_create(name=module_names[0]) + course.module = module else: course.name = "Sans nom" if i - 2 >= min_i: diff --git a/models.py b/models.py index b1a6a1e..b59f2d8 100644 --- a/models.py +++ b/models.py @@ -200,6 +200,18 @@ class Room(SlugModel): verbose_name_plural = "salles" +class Module(SlugModel): + name = models.CharField(max_length=255, unique=True, verbose_name="nom") + slug = models.SlugField(max_length=64, default="", unique=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "module" + verbose_name_plural = "modules" + + class CourseManager(Manager): def get_courses(self, obj, **criteria): qs = self.get_queryset() @@ -247,6 +259,7 @@ class Course(models.Model): default=timezone.now) celcat_id = models.IntegerField(verbose_name="ID Celcat", null=True) + module = models.ForeignKey(Module, on_delete=models.SET_NULL, null=True) def __str__(self): return self.name -- cgit v1.2.1 From 924ff4766e3d592f2bb397b0672c4b30d2549c33 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 13 Feb 2019 22:39:19 +0100 Subject: tests: test du parsage des ID de cours Celcat Signed-off-by: Alban Gruin --- tests.py | 54 +++++++++++++++++++++++++++++------------- tests/data/2018/october.html | 2 +- tests/data/2018/september.html | 2 +- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/tests.py b/tests.py index 627602c..e3b2310 100644 --- a/tests.py +++ b/tests.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 @@ -84,7 +84,7 @@ class CourseTestCase(TestCase): for group in (cma, tda2, self.tpa21, cmb, tdb2, self.tpb21,): course = Course.objects.create( name="{0} course".format(group.name), type="cours", - source=source, begin=dt, end=dt) + source=source, begin=dt, end=dt, celcat_id=0) course.groups.add(group) def test_get_courses_for_group(self): @@ -359,7 +359,8 @@ class RoomTestCase(TestCase): end = begin + datetime.timedelta(hours=rn.get("duration", 2)) course = Course.objects.create(source=self.source, - begin=begin, end=end) + begin=begin, end=end, + celcat_id=0) course.groups.add(group) course.rooms.add(room) @@ -418,7 +419,8 @@ class UPS2018ParserTestCase(TestCase): {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", "text": "(10:00-12:00)
COURS/TD
Cours quelconque;AAA" "
L3 Info s1 CMA;L3 Info s1 TDA2
" - "Salle quelconque;Salle quelconque 2
Commentaire"}, + "Salle quelconque;Salle quelconque 2
Commentaire", "id": "0", + "tag": []}, timezone.make_aware(datetime.datetime(2018, 9, 21)), timezone.make_aware(datetime.datetime(2018, 9, 1)), timezone.make_aware(datetime.datetime(2018, 10, 1)), @@ -436,6 +438,7 @@ class UPS2018ParserTestCase(TestCase): self.assertIn(self.room2, event.rooms.all()) self.assertEqual(event.rooms.count(), 2) self.assertEqual(event.notes, "Commentaire") + self.assertEqual(event.celcat_id, 0) self.assertEqual(event.begin, timezone.make_aware( datetime.datetime(2018, 9, 21, 10, 0, 0))) self.assertEqual(event.end, timezone.make_aware( @@ -451,6 +454,7 @@ class UPS2018ParserTestCase(TestCase): "name": "Cours quelconque", "type": "COURS/TD", "group": self.group, "room": self.room, + "id": "1" }, { "text": "(10:00-12:00)
COURS/TD
Cours quelconque" @@ -458,7 +462,8 @@ class UPS2018ParserTestCase(TestCase): "name": "Cours quelconque", "type": "COURS/TD", "group": ngroup, - "notes": "Salle quelconque 3" + "notes": "Salle quelconque 3", + "id": "2" }, { "text": "(10:00-12:00)
COURS/TD
Cours quelconque" @@ -467,44 +472,50 @@ class UPS2018ParserTestCase(TestCase): "type": "COURS/TD", "group": self.group, "notes": "Salle quelconque 3\nCommentaire", + "id": "3" }, { "text": "(10:00-12:00)
COURS/TD" "
L3 Info s1 CMA
Salle quelconque 3", "name": "COURS/TD", "group": self.group, - "notes": "Salle quelconque 3" + "notes": "Salle quelconque 3", + "id": "4" }, { "text": "COURS/TD
L3 Info s1 CMA
Salle quelconque 3", "name": "COURS/TD", "group": self.group, - "notes": "Salle quelconque 3" + "notes": "Salle quelconque 3", + "id": "5" }, { "text": "L3 Info s1 CMA
Salle quelconque", "name": "Sans nom", "group": self.group, - "room": self.room + "room": self.room, + "id": "6" }, { "text": "L3 Info s1 CMA
Salle quelconque 3", "name": "Sans nom", "group": self.group, - "notes": "Salle quelconque 3" + "notes": "Salle quelconque 3", + "id": "7" }, { "text": "(10:00-12:00)
L3 Info s1 CMA
Salle quelconque", "name": "Sans nom", "group": self.group, - "room": self.room + "room": self.room, + "id": "8" } ] for e in events: event = get_event( {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", - "text": e["text"]}, + "text": e["text"], "id": e["id"], "tag": []}, timezone.make_aware(datetime.datetime(2018, 9, 21)), timezone.make_aware(datetime.datetime(2018, 9, 1)), timezone.make_aware(datetime.datetime(2018, 10, 1)), @@ -513,6 +524,7 @@ class UPS2018ParserTestCase(TestCase): self.assertEqual(event.name, e["name"]) self.assertIn(e["group"], event.groups.all()) self.assertEqual(event.groups.count(), 1) + self.assertEqual(str(event.celcat_id), e["id"]) if "type" in e: self.assertEqual(event.type, e["type"]) @@ -560,11 +572,13 @@ class UPS2018ParserTestCase(TestCase): {"begin": timezone.make_aware(datetime.datetime(2018, 9, 21, 10, 00, 00)), "end": - timezone.make_aware(datetime.datetime(2018, 9, 21, 12, 00, 00))}, + timezone.make_aware(datetime.datetime(2018, 9, 21, 12, 00, 00)), + "id": 0}, {"begin": timezone.make_aware(datetime.datetime(2018, 10, 22, 10, 00, 00)), "end": - timezone.make_aware(datetime.datetime(2018, 10, 22, 12, 00, 00))} + timezone.make_aware(datetime.datetime(2018, 10, 22, 12, 00, 00)), + "id": 2} ] for i, course in enumerate(self.parser.get_events( @@ -578,6 +592,8 @@ class UPS2018ParserTestCase(TestCase): self.assertIsNone(course.notes) self.assertEqual(course.begin, courses[i]["begin"]) self.assertEqual(course.end, courses[i]["end"]) + self.assertEqual(course.celcat_id, courses[i]["id"]) + self.assertEqual(course.module, None) self.assertEqual(i, len(courses) - 1) @@ -588,15 +604,21 @@ class UPS2018ParserTestCase(TestCase): [{ "start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", "text": "(10:00-12:00)
COURS/TD
Cours quelconque" - "
L3 Info s1 CMA
Salle quelconque" + "
L3 Info s1 CMA
Salle quelconque", + "id": "0", + "tag": [], }], [{ "start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", "text": "(10:00-12:00)
COURS/TD
Cours quelconque" - "
L3 Info s1 CMA
Salle quelconque" + "
L3 Info s1 CMA
Salle quelconque", + "id": "1", + "tag": [], }, { "start": "2018-10-22T10:00:00", "end": "2018-10-22T12:00:00", "text": "(10:00-12:00)
COURS/TD
Cours quelconque" - "
L3 Info s1 CMA
Salle quelconque" + "
L3 Info s1 CMA
Salle quelconque", + "id": "2", + "tag": [], }], [], [], [], [], [], [], [], [], []]) def test_get_update_date(self): diff --git a/tests/data/2018/october.html b/tests/data/2018/october.html index 6f326f9..ab3da0b 100644 --- a/tests/data/2018/october.html +++ b/tests/data/2018/october.html @@ -40,7 +40,7 @@ do_something(); diff --git a/tests/data/2018/september.html b/tests/data/2018/september.html index 3db6cbc..c81fc3b 100644 --- a/tests/data/2018/september.html +++ b/tests/data/2018/september.html @@ -41,7 +41,7 @@ alert("something"); -- cgit v1.2.1 From b9f77648d0fb6897fad180627401c6b9c3e75943 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 9 Jun 2019 18:15:10 +0200 Subject: tests: test de la détection du module correspondant à un cours Signed-off-by: Alban Gruin --- tests.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tests.py b/tests.py index e3b2310..b411db3 100644 --- a/tests.py +++ b/tests.py @@ -20,7 +20,7 @@ from django.utils import timezone from .management.parsers.abstractparser import ParserError from .management.parsers.ups2018 import Parser as UPS2018Parser -from .models import Course, Group, Room, Source, Timetable, Year +from .models import Course, Group, Module, Room, Source, Timetable, Year from .templatetags.rooms import format_rooms from .utils import tz_now @@ -414,13 +414,14 @@ class UPS2018ParserTestCase(TestCase): def test_get_event(self): get_event = self.parser._Parser__get_event count = Course.objects.count() + module_count = Module.objects.count() event = get_event( {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", "text": "(10:00-12:00)
COURS/TD
Cours quelconque;AAA" "
L3 Info s1 CMA;L3 Info s1 TDA2
" "Salle quelconque;Salle quelconque 2
Commentaire", "id": "0", - "tag": []}, + "tag": ["abc", "def", "AAA"]}, timezone.make_aware(datetime.datetime(2018, 9, 21)), timezone.make_aware(datetime.datetime(2018, 9, 1)), timezone.make_aware(datetime.datetime(2018, 10, 1)), @@ -443,9 +444,13 @@ class UPS2018ParserTestCase(TestCase): datetime.datetime(2018, 9, 21, 10, 0, 0))) self.assertEqual(event.end, timezone.make_aware( datetime.datetime(2018, 9, 21, 12, 0, 0))) + self.assertEqual(event.module.name, "AAA") - self.assertEqual(count, Course.objects.count() - 1) count += 1 + module_count += 1 + + self.assertEqual(count, Course.objects.count()) + self.assertEqual(module_count, Module.objects.count()) events = [ { @@ -454,7 +459,9 @@ class UPS2018ParserTestCase(TestCase): "name": "Cours quelconque", "type": "COURS/TD", "group": self.group, "room": self.room, - "id": "1" + "id": "1", + "tag": ["aaa", "Cours"], + "meta": 1 }, { "text": "(10:00-12:00)
COURS/TD
Cours quelconque" @@ -515,7 +522,7 @@ class UPS2018ParserTestCase(TestCase): for e in events: event = get_event( {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", - "text": e["text"], "id": e["id"], "tag": []}, + "text": e["text"], "id": e["id"], "tag": e.get("tag", [])}, timezone.make_aware(datetime.datetime(2018, 9, 21)), timezone.make_aware(datetime.datetime(2018, 9, 1)), timezone.make_aware(datetime.datetime(2018, 10, 1)), @@ -542,9 +549,17 @@ class UPS2018ParserTestCase(TestCase): else: self.assertIsNone(event.notes) - self.assertEqual(count, Course.objects.count() - 1) + if "tag" in e: + self.assertEqual(event.module.name, e["tag"][e["meta"]]) + module_count += 1 + else: + self.assertIsNone(event.module) + count += 1 + self.assertEqual(count, Course.objects.count()) + self.assertEqual(module_count, Module.objects.count()) + event = get_event( {"start": "2018-09-21T10:00:00", "end": "2018-09-21T12:00:00", "text": "Global Event"}, -- cgit v1.2.1 From c2a2f5aac817d4255c30c447808ec08faecda036 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 9 Jun 2019 19:41:38 +0200 Subject: api: ajout d’un paramètre pour activer ou non l’API L’API devient optionnelle, et un paramètre, CS_ENABLE_API, est rajouté pour sélectionner son état (activé ou non). Par défaut, il est désactivé. La documentation est mise à jour pour refléter ces changements. Signed-off-by: Alban Gruin --- Documentation/usage/installation.rst | 58 ++++++++++++++++++++++++------------ Documentation/usage/rest.rst | 3 ++ urls.py | 15 ++++++++-- 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/Documentation/usage/installation.rst b/Documentation/usage/installation.rst index 75e0be1..92b3c5d 100644 --- a/Documentation/usage/installation.rst +++ b/Documentation/usage/installation.rst @@ -8,11 +8,14 @@ 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_. +Une dépendance est optionnelle : + + - `Django REST Framework`_, pour l’:doc:`API 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 @@ -26,13 +29,13 @@ 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/ .. _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/ @@ -97,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 : @@ -157,23 +167,6 @@ 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``. @@ -251,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 `. + +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 index 4908d8a..d18eaa5 100644 --- a/Documentation/usage/rest.rst +++ b/Documentation/usage/rest.rst @@ -10,6 +10,9 @@ 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 ====== diff --git a/urls.py b/urls.py index 5cd134e..a5f6b9b 100644 --- a/urls.py +++ b/urls.py @@ -13,14 +13,23 @@ # You should have received a copy of the GNU Affero General Public License # along with celcatsanitizer. If not, see . +from django.conf import settings 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("", 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"), -- cgit v1.2.1 From 274954dbc4c682492cf375045fd34f972769ae5f Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 28 Aug 2019 18:33:11 +0200 Subject: templates: nouveau nom de ICSdroid Ceci change le nom d’ICSdroid sur la page des ICS en ICSx⁵, son nouveau nom. Signed-off-by: Alban Gruin --- templates/calendars.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/calendars.html b/templates/calendars.html index 98c57e2..95cda8f 100644 --- a/templates/calendars.html +++ b/templates/calendars.html @@ -15,7 +15,7 @@
  • Sur Linux et Windows, l’extension Lightning du logiciel Thunderbird ;
  • Sur Mac et iOS, iCloud ;
  • -
  • Sur Android, l’application libre ICSDroid peut les +
  • Sur Android, l’application libre ICSx5 peut les récupérer périodiquement et les afficher sur l’application Agenda de base. Elle -- cgit v1.2.1 From 6fbcee6ddfdfb7d2674d3296d7a20f59905db7f8 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Wed, 28 Aug 2019 15:42:58 +0200 Subject: models & admin: ajout des champs nécessaires pour le parseur UPS2019 Le nouveau format utilisé requiert d’effectuer des requêtes POST et non plus GET, une URL n’est donc plus suffisante pour indiquer la source. Un champ `metadata' est rajouté, contenant une métadonnée au format texte. Il serait plus judicieux d’utiliser un champ JSONField, mais ce type est restreint à PostgreSQL (mon environnement de développement utilise toujours SQLite). Les ID des cours dans celcat ne sont plus de simples nombres mais des chaînes de caractères. Ce changement est donc reflété dans le modèle des cours. Dans le nouveau format, si un cours a plus de 3 groupes, seul les 3 premiers groupes sont listés, les autres sont marqués avec un texte du genre « 2 autres… ». (Ça fait 2 ans et demi que je travaille sur ce projet, et plus le temps passe, plus j’ai l’impression que la drogue est un outil de travail comme un autre chez celcat.) Le champ `buggy' est rajouté pour indiquer si c’est le cas de ce cours ou non, ce qui permettra d’ajouter une remarque sur les pages ouèbe ou les ICS. Les interfaces d’administration sont modifiées pour intégrer ces changements. Signed-off-by: Alban Gruin --- admin.py | 4 ++-- models.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/admin.py b/admin.py index 5350aad..527b1eb 100644 --- a/admin.py +++ b/admin.py @@ -38,7 +38,7 @@ class YearAdmin(admin.ModelAdmin): @admin.register(Source) class SourceAdmin(admin.ModelAdmin): - list_display = ("url", "last_update_date",) + list_display = ("url", "metadata", "last_update_date",) @admin.register(Timetable) @@ -75,7 +75,7 @@ class CourseAdmin(admin.ModelAdmin): (None, {"fields": ("name", "type", "source", "groups", "rooms",)}), ("Horaires", {"fields": ("begin", "end",)}), ("Remarques", {"fields": ("notes",)}), - ("Avancé", {"fields": ("celcat_id", "last_update",), + ("Avancé", {"fields": ("celcat_id", "last_update", "buggy",), "classes": ("collapse",)}),) list_display = ("name", "type", "source", "begin", "end",) list_filter = ("type", "source__timetables", "groups",) diff --git a/models.py b/models.py index b59f2d8..7fd5b3e 100644 --- a/models.py +++ b/models.py @@ -48,13 +48,15 @@ class Year(SlugModel): class Source(models.Model): - url = models.URLField(max_length=255, verbose_name="URL", unique=True) + url = models.URLField(max_length=255, verbose_name="URL") + metadata = models.CharField(max_length=256, verbose_name="Métadonnée", + blank=True, null=True) last_update_date = models.DateTimeField(null=True, blank=True, verbose_name="dernière mise à jour" " Celcat") def __str__(self): - return self.url + return "{}, {}".format(self.url, self.metadata) @property def formatted_timetables(self): @@ -62,6 +64,7 @@ class Source(models.Model): self.timetables.all()]) class Meta: + unique_together = (("url", "metadata",),) verbose_name = "source d’emploi du temps" verbose_name_plural = "sources d’emploi du temps" @@ -258,9 +261,12 @@ class Course(models.Model): last_update = models.DateTimeField(verbose_name="dernière mise à jour", default=timezone.now) - celcat_id = models.IntegerField(verbose_name="ID Celcat", null=True) + celcat_id = models.CharField(max_length=64, verbose_name="ID Celcat", + null=True) module = models.ForeignKey(Module, on_delete=models.SET_NULL, null=True) + buggy = models.BooleanField(verbose_name="Bogué", default=False) + def __str__(self): return self.name -- cgit v1.2.1 From 797a8a7b57b91823ee4b306ca91256dad4e3f504 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 29 Aug 2019 12:52:38 +0200 Subject: models: remplacement de RENCONTRE par rencontre dans le type d’un cours Signed-off-by: Alban Gruin --- models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/models.py b/models.py index 7fd5b3e..448b996 100644 --- a/models.py +++ b/models.py @@ -274,6 +274,7 @@ class Course(models.Model): if self.type is not None: self.type = self.type.replace("COURS", "cours") self.type = self.type.replace("REUNION", "réunion") + self.type = self.type.replace("RENCONTRE", "rencontre") if self.name is not None: self.name = self.name.split("(")[0].strip() -- cgit v1.2.1 From a235752368c6eff21400f6f8089ee3bf781cf36e Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 30 Aug 2019 12:27:00 +0200 Subject: ups2018: déplacement des préfixes de cours dans une constante Le parseur UPS2018 va se servir de cette liste aussi, elle est donc déplacée dans sa propre constante. Signed-off-by: Alban Gruin --- management/parsers/ups2018.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/management/parsers/ups2018.py b/management/parsers/ups2018.py index afbfc4b..0d6d798 100644 --- a/management/parsers/ups2018.py +++ b/management/parsers/ups2018.py @@ -32,6 +32,10 @@ from .abstractparser import AbstractParser, ParserError VARNAME = "v.events.list = " +GROUP_PREFIXES = ("L1 ", "L2 ", "L3 ", "L3P ", "M1 ", "M2 ", "DEUST ", "MAG1 ", + "1ERE ANNEE ", "2EME ANNEE ", "3EME ANNEE ", + "MAT-Agreg Interne ") + def find_events_list(soup): res = [] @@ -123,11 +127,7 @@ class Parser(AbstractParser): min_i = 1 i = min_i - while i < len(data) and not data[i].startswith( - ("L1 ", "L2 ", "L3 ", "L3P ", "M1 ", "M2 ", "DEUST ", "MAG1 ", - "1ERE ANNEE ", "2EME ANNEE ", "3EME ANNEE ", - "MAT-Agreg Interne ") - ): + while i < len(data) and not data[i].startswith(GROUP_PREFIXES): i += 1 groups = data[i] -- cgit v1.2.1 From 0717c8ccd6ac10989d86593ff73a86a0c4398408 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 29 Aug 2019 12:53:05 +0200 Subject: ups2019: nouveau parseur pour le format UPS2019 Signed-off-by: Alban Gruin --- management/parsers/ups2019.py | 128 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 management/parsers/ups2019.py diff --git a/management/parsers/ups2019.py b/management/parsers/ups2019.py new file mode 100644 index 0000000..c7ab7c9 --- /dev/null +++ b/management/parsers/ups2019.py @@ -0,0 +1,128 @@ +# Copyright (C) 2019 Alban Gruin +# +# celcatsanitizer is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# celcatsanitizer is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with celcatsanitizer. If not, see . + +from datetime import date, datetime, timedelta +from html import unescape + +from django.utils import timezone + +import requests + +from ...models import Course, Group, Module, Room +from ...utils import get_current_week, get_week +from .abstractparser import AbstractParser +from .ups2018 import GROUP_PREFIXES + + +class Parser(AbstractParser): + def __get_name(self, raw_name): + return raw_name.split('[')[1][:-1] + + def __get_event(self, event, year, week): + if event["allDay"]: + return + + begin = timezone.make_aware( + datetime.strptime(event["start"], "%Y-%m-%dT%H:%M:%S") + ) + end = timezone.make_aware( + datetime.strptime(event["end"], "%Y-%m-%dT%H:%M:%S") + ) + + if year is not None and week is not None: + event_year, event_week, _ = begin.isocalendar() + if event_year != year or event_week != week: + return + + data = [unescape(st.strip()) + for st in event["description"].split("
    ")] + groups = [] + rooms = [] + + course = Course.objects.create( + source=self.source, begin=begin, end=end, + celcat_id=event["id"] + ) + + i = 0 + if event.get("eventCategory") is not None and \ + len(event.get("eventCategory", "")) > 0: + course.type = event["eventCategory"] + i = 1 + + if event.get("module", "") is not None and \ + len(event.get("module", "")) > 0: + module, _ = Module.objects.get_or_create(name=event["module"]) + course.module = module + + if '[' in data[i]: + course.name = self.__get_name(data[i]) + i += 1 + + while '[' in data[i]: + course.name += ", " + self.__get_name(data[i]) + i += 1 + + while i < len(data) and not data[i].startswith(GROUP_PREFIXES): + rooms.append(data[i]) + i += 1 + course.rooms.add(*Room.objects.filter(name__in=rooms)) + + while i < len(data) and data[i].startswith(GROUP_PREFIXES): + groups.append(Group.objects.get_or_create( + source=self.source, celcat_name=data[i] + )[0]) + i += 1 + course.groups.add(*groups) + + if i < len(data): + course.notes = "\n".join(data[i:]).strip() + if "other" in course.notes: + print("Warning: 'other' in course.notes") + + return course + + def get_events(self, today, year=None, week=None): + for event in self.events: + course = self.__get_event(event, year, week) + if course is not None: + yield course + + def get_update_date(self): + return + + def get_weeks(self): + # FIXME: détection automatique à partir des événements présents + beginning, _ = get_week(*get_current_week()) + self.weeks = {"1": beginning} + + return self.weeks + + def get_source(self): + start = date.today() + end = start + timedelta(days=365) + + req = requests.post(self.source.url, + headers={"User-Agent": self.user_agent}, + data={"calView": "month", + "resType": 103, + "federationIds[]": self.source.metadata, + "start": start.strftime("%Y-%m-%d"), + "end": end.strftime("%Y-%m-%d")}) + req.encoding = "uft8" + req.raise_for_status() + + self.events = req.json() + return self.events -- cgit v1.2.1 From 5045af42503158db4155988f00ee0db0a6a67ff7 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Thu, 29 Aug 2019 17:19:12 +0200 Subject: templatetags/rooms: correction du nom brut des salles Les salles et amphis commencent maintenant par leur département, de cette manière : FSI / Amphi FERMAT (bat.1A) Au lieu de : Amphi FERMAT (bat.1A) Cela ne fait que rajouter du bruit sur la page. Ceci modifie le filtre de formatage des salles pour retirer le département si il est présent. Signed-off-by: Alban Gruin --- templatetags/rooms.py | 14 ++++++++++---- tests.py | 34 +++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/templatetags/rooms.py b/templatetags/rooms.py index f0e1b2e..d8b0e23 100644 --- a/templatetags/rooms.py +++ b/templatetags/rooms.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017 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 @@ -18,11 +18,17 @@ from django import template register = template.Library() +def __filter_room_name(name): + if '/' in name: + return name.split('/')[1].strip() + return name + + @register.filter def format_rooms(rooms): - amphi_list = [room.name for room in rooms if room.name.startswith("Amphi")] - room_list = [room.name for room in rooms - if not room.name.startswith("Amphi")] + names = [__filter_room_name(room.name) for room in rooms] + amphi_list = [name for name in names if name.startswith("Amphi")] + room_list = [name for name in names if not name.startswith("Amphi")] amphis = ", ".join(amphi_list) joined = ", ".join(room_list) diff --git a/tests.py b/tests.py index b411db3..1e80b48 100644 --- a/tests.py +++ b/tests.py @@ -341,6 +341,16 @@ class RoomTestCase(TestCase): for i in range(5, 7) ] + # On n’insère pas ces salles dans la base de données, elles ne + # servent que pour le test de formatage. + self.formatted_rooms = [ + Room(name="FSI / {}".format(str(i))) + for i in range(5) + ] + [ + Room(name="FSI / Amphi {}".format(str(i))) + for i in range(5, 7) + ] + hours = [({"begin": datetime.time(hour=14, minute=0)},), ({"begin": datetime.time(hour=16, minute=0)},), ({"begin": datetime.time(hour=13, minute=30)}, @@ -364,23 +374,29 @@ class RoomTestCase(TestCase): course.groups.add(group) course.rooms.add(room) - def test_format(self): - amphis = self.rooms[-2:] - + def __test_format(self, rooms, amphis): self.assertEqual(format_rooms([]), "") - self.assertEqual(format_rooms(self.rooms[:1]), "Salle 0") - self.assertEqual(format_rooms(self.rooms[:2]), "Salles 0, 1") + self.assertEqual(format_rooms(rooms[:1]), "Salle 0") + self.assertEqual(format_rooms(rooms[:2]), "Salles 0, 1") self.assertEqual(format_rooms([amphis[0]]), "Amphi 5") self.assertEqual(format_rooms(amphis), "Amphi 5, Amphi 6") - self.assertEqual(format_rooms([amphis[0]] + self.rooms[:1]), + self.assertEqual(format_rooms([amphis[0]] + rooms[:1]), "Amphi 5, salle 0") - self.assertEqual(format_rooms([amphis[0]] + self.rooms[:2]), + self.assertEqual(format_rooms([amphis[0]] + rooms[:2]), "Amphi 5, salles 0, 1") - self.assertEqual(format_rooms(amphis + self.rooms[:1]), + self.assertEqual(format_rooms(amphis + rooms[:1]), "Amphi 5, Amphi 6, salle 0") - self.assertEqual(format_rooms(amphis + self.rooms[:2]), + self.assertEqual(format_rooms(amphis + rooms[:2]), "Amphi 5, Amphi 6, salles 0, 1") + def test_format(self): + amphis = self.rooms[-2:] + self.__test_format(self.rooms, amphis) + + def test_reformat(self): + amphis = self.formatted_rooms[-2:] + self.__test_format(self.formatted_rooms, amphis) + def test_qsjps(self): begin = timezone.make_aware(datetime.datetime.combine( self.day, datetime.time(hour=15, minute=0))) -- cgit v1.2.1 From a85f2fb91d1a4a0e31c41c392e404d7ddbf21109 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 1 Sep 2019 12:41:19 +0200 Subject: ups2019: réadaptation du parseur L’emplacement des différents éléments a changé, mdr. Signed-off-by: Alban Gruin --- management/parsers/ups2019.py | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/management/parsers/ups2019.py b/management/parsers/ups2019.py index c7ab7c9..c6bd7e3 100644 --- a/management/parsers/ups2019.py +++ b/management/parsers/ups2019.py @@ -27,9 +27,6 @@ from .ups2018 import GROUP_PREFIXES class Parser(AbstractParser): - def __get_name(self, raw_name): - return raw_name.split('[')[1][:-1] - def __get_event(self, event, year, week): if event["allDay"]: return @@ -56,41 +53,42 @@ class Parser(AbstractParser): celcat_id=event["id"] ) - i = 0 + max_i = len(data) + if event.get("eventCategory") is not None and \ len(event.get("eventCategory", "")) > 0: course.type = event["eventCategory"] - i = 1 + max_i -= 1 if event.get("module", "") is not None and \ len(event.get("module", "")) > 0: module, _ = Module.objects.get_or_create(name=event["module"]) course.module = module - if '[' in data[i]: - course.name = self.__get_name(data[i]) - i += 1 - - while '[' in data[i]: - course.name += ", " + self.__get_name(data[i]) - i += 1 - - while i < len(data) and not data[i].startswith(GROUP_PREFIXES): + i = 0 + while i < max_i and not data[i].startswith(GROUP_PREFIXES): rooms.append(data[i]) i += 1 course.rooms.add(*Room.objects.filter(name__in=rooms)) - while i < len(data) and data[i].startswith(GROUP_PREFIXES): - groups.append(Group.objects.get_or_create( - source=self.source, celcat_name=data[i] - )[0]) + if len(rooms) != course.rooms.count(): + print(rooms, course.rooms) + + while i < max_i and data[i].startswith(GROUP_PREFIXES): + group, _ = Group.objects.get_or_create(source=self.source, + celcat_name=data[i]) + groups.append(group) i += 1 course.groups.add(*groups) - if i < len(data): - course.notes = "\n".join(data[i:]).strip() - if "other" in course.notes: - print("Warning: 'other' in course.notes") + if i < max_i and course.module is not None and \ + data[i].startswith(course.module.name): + course.name = data[i] + i += 1 + + course.notes = "\n".join(data[i:max_i]).strip() + if "other" in data[i]: + print("Warning: \"other\" in notes") return course -- cgit v1.2.1 From d1369ea3654b56e6a91335bd108035cd5eecbc76 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 30 Aug 2019 14:00:11 +0200 Subject: ups2018: correction d’une erreur de syntaxe avec Python 3.7 `async' est devenu un mot-clef avec Python 3.7. Or, un paramètre est appelé de cette manière dans le parseur UPS2018. Ceci le renome en `asynchronous' pour corriger ce problème Signed-off-by: Alban Gruin --- management/parsers/ups2018.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/management/parsers/ups2018.py b/management/parsers/ups2018.py index 0d6d798..ad8322c 100644 --- a/management/parsers/ups2018.py +++ b/management/parsers/ups2018.py @@ -221,10 +221,10 @@ class Parser(AbstractParser): responses = yield from asyncio.gather(*futures) return responses - def get_source_from_months(self, async=True): + def get_source_from_months(self, asynchronous=True): events = [] - if async: + if asynchronous: loop = asyncio.get_event_loop() events = loop.run_until_complete(self.get_months_async()) else: -- cgit v1.2.1 From 947632bbb0fb1a721bde549f0ca9bf8b8f281ca4 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 30 Aug 2019 14:01:21 +0200 Subject: views: utilisation de TruncWeek Signed-off-by: Alban Gruin --- views.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/views.py b/views.py index de38f35..7bb3916 100644 --- a/views.py +++ b/views.py @@ -17,7 +17,8 @@ import datetime from django.db import connection from django.db.models import Count, Max -from django.db.models.functions import ExtractWeek, ExtractYear, Length +from django.db.models.functions import ExtractWeek, ExtractYear, Length, \ + TruncWeek from django.http import Http404 from django.shortcuts import get_object_or_404, render from django.utils import timezone @@ -32,7 +33,6 @@ import edt if connection.vendor == "postgresql": from django.contrib.postgres.aggregates import ArrayAgg - from django.db.models.expressions import RawSQL def index(request): @@ -195,9 +195,8 @@ def rooms(request): rooms = Room.objects.filter(course__begin__gte=start, course__begin__lt=end) \ .order_by("name") \ - .annotate(weeks=ArrayAgg( - RawSQL("date_trunc('week', edt_course.begin)", - []), distinct=True)) + .annotate(weeks=ArrayAgg(TruncWeek("week"), + distinct=True)) return render(request, "room_list.html", {"elements": rooms}) @@ -210,8 +209,7 @@ def rooms(request): rooms = Room.objects.filter(course__begin__gte=start, course__begin__lt=end) \ .order_by("name") \ - .annotate(year=ExtractYear("course__begin"), - week=ExtractWeek("course__begin"), + .annotate(week=TruncWeek("course__begin"), c=Count("*")) # Regroupement des semaines dans une liste de chaque objet salle @@ -226,10 +224,8 @@ def rooms(request): room.weeks = [] rooms_weeks.append(room) - # On récupère le premier jour de la semaine - date, _ = get_week(room.year, room.week) # Et on le rajoute dans la liste des semaines de la salle. - rooms_weeks[-1].weeks.append(date) + rooms_weeks[-1].weeks.append(room.week) # Rendu de la page. return render(request, "room_list.html", {"elements": rooms_weeks}) -- cgit v1.2.1 From 66abc2a86eee041154978bbba78c999ffe92d099 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 1 Sep 2019 20:42:47 +0200 Subject: api: remplacement des appels à detail_route par action Signed-off-by: Alban Gruin --- api/views.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/api/views.py b/api/views.py index b7b69e3..2459455 100644 --- a/api/views.py +++ b/api/views.py @@ -20,7 +20,7 @@ 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.decorators import action from rest_framework.response import Response from ..forms import QSJPSForm @@ -35,7 +35,7 @@ class YearViewSet(viewsets.ReadOnlyModelViewSet): queryset = Year.objects.all().order_by("name") serializer_class = YearSerializer - @detail_route(methods=["get"], url_path="timetables") + @action(detail=True, methods=["get"], url_path="timetables") def timetable_list(self, request, pk): year = self.get_object() timetables = Timetable.objects.filter(year=year).distinct() \ @@ -49,7 +49,7 @@ class SourceViewSet(viewsets.ReadOnlyModelViewSet): queryset = Source.objects.all().order_by("pk") serializer_class = SourceSerializer - @detail_route(methods=["get"], url_path="timetables") + @action(detail=True, methods=["get"], url_path="timetables") def timetable_list(self, request, pk): source = self.get_object() timetables = Timetable.objects.filter(source=source).distinct() \ @@ -64,7 +64,7 @@ class TimetableViewSet(viewsets.ReadOnlyModelViewSet): .order_by("year", "name") serializer_class = TimetableSerializer - @detail_route(methods=["get"], url_path="groups") + @action(detail=True, methods=["get"], url_path="groups") def group_list(self, request, pk): timetable = self.get_object() groups = Group.objects.filter(source=timetable.source).distinct() \ @@ -75,7 +75,7 @@ class TimetableViewSet(viewsets.ReadOnlyModelViewSet): class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): - @detail_route(methods=["get"], url_path="courses") + @action(detail=True, methods=["get"], url_path="courses") def course_list(self, request, pk): obj = self.get_object() courses = Course.objects.get_courses(obj).prefetch_related("groups") @@ -91,14 +91,15 @@ class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): many=True) return self.get_paginated_response(courses_json.data) - @detail_route(methods=["get"], url_path="courses/days/current") + @action(detail=True, methods=["get"], url_path="courses/days/current") def current_day(self, request, pk): obj = self.get_object() start = datetime.date.today() end = start + datetime.timedelta(days=1) return self.__get_courses(obj, start, end) - @detail_route(methods=["get"], url_path="courses/days/(?P\d+)/(?P\d+)/(?P\d+)") + @action(detail=True, methods=["get"], + url_path="courses/days/(?P\d+)/(?P\d+)/(?P\d+)") def other_day(self, request, pk, year, month, day): obj = self.get_object() @@ -116,14 +117,14 @@ class CourseListGroupSet(viewsets.ReadOnlyModelViewSet): end = start + datetime.timedelta(days=1) return self.__get_courses(obj, start, end) - @detail_route(methods=["get"], url_path="courses/weeks/current") + @action(detail=True, methods=["get"], url_path="courses/weeks/current") def current_week(self, request, pk): obj = self.get_object() start, end = get_week(*get_current_or_next_week()) return self.__get_courses(obj, start, end) - @detail_route(methods=["get"], - url_path="courses/weeks/(?P\d+)/(?P\d+)") + @action(detail=True, methods=["get"], + url_path="courses/weeks/(?P\d+)/(?P\d+)") def other_week(self, request, pk, year, week): obj = self.get_object() @@ -142,7 +143,7 @@ class GroupViewSet(CourseListGroupSet): queryset = Group.objects.all().order_by("name") serializer_class = GroupSerializer - @detail_route(methods=["get"], url_path="courses/weeks") + @action(detail=True, methods=["get"], url_path="courses/weeks") def weeks(self, request, pk): group = self.get_object() groups = Group.objects.get_parents(group) -- cgit v1.2.1 From 2472c9d0a75360a2be235b4e6b49944cc667a11c Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Fri, 30 Aug 2019 14:01:57 +0200 Subject: Ajout d’un mailmap J’ai changé d’adresse entre le début de ce projet et maintenant Signed-off-by: Alban Gruin --- .mailmap | 1 + 1 file changed, 1 insertion(+) create mode 100644 .mailmap diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..392fcc0 --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ +Alban Gruin -- cgit v1.2.1 From 88fcd3f9d66713292fb8167ef58c7fa2455f3e8c Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 1 Sep 2019 18:50:40 +0200 Subject: requirements: mise à jour des dépendances Signed-off-by: Alban Gruin --- requirements.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6c91f54..1a7f902 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -beautifulsoup4==4.6.3 -Django==2.0.8 -djangorestframework==3.9.1 +beautifulsoup4==4.8.0 +Django==2.2.4 +djangorestframework==3.10.2 gunicorn==19.9.0 -icalendar==4.0.2 -lxml==4.2.4 -psycopg2-binary==2.7.5 -requests==2.19.1 +icalendar==4.0.3 +lxml==4.4.1 +psycopg2-binary==2.8.3 +requests==2.22.0 -- cgit v1.2.1 From a6eaecf48f63c37cf851386a45cd4b25ac3ed1e0 Mon Sep 17 00:00:00 2001 From: Alban Gruin Date: Sun, 1 Sep 2019 18:51:10 +0200 Subject: doc: mise à jour de la doc pour la future version 0.16 Signed-off-by: Alban Gruin --- Documentation/dev/roadmap.rst | 6 ++---- Documentation/usage/installation.rst | 25 ++++++++++++------------- Documentation/usage/versions.rst | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/Documentation/dev/roadmap.rst b/Documentation/dev/roadmap.rst index 14cad2a..1ae17af 100644 --- a/Documentation/dev/roadmap.rst +++ b/Documentation/dev/roadmap.rst @@ -2,14 +2,12 @@ Feuille de route ================ -.. _ref-ver-0.15: +.. _ref-ver-0.16: -Version 0.15 +Version 0.16 ============ - Optimisation des requêtes en utilisant des fonctionnalités spécifiques à PostgreSQL si nécessaire - - Utilisation de Django 2.1 et de l’aggrégat ``TruncWeek``. - - Amélioration du parseur UPS2018 et de sa documentation. - Remplacement du moteur de templates de Django par Jinja2_ ? - Améliorations de certaines pages ? diff --git a/Documentation/usage/installation.rst b/Documentation/usage/installation.rst index 92b3c5d..b86b257 100644 --- a/Documentation/usage/installation.rst +++ b/Documentation/usage/installation.rst @@ -7,7 +7,7 @@ Dépendances celcatsanitizer est écrit en Python 3. Il dépend des bibliothèques suivantes : - - `Django 2.0`_ + - `Django 2.2`_ - 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_. @@ -16,10 +16,9 @@ Une dépendance est optionnelle : - `Django REST Framework`_, pour l’:doc:`API 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 -problèmes. +celcatsanitizer requiert Python 3.6 au minimum. Les versions +supérieures devraient fonctionner sans problèmes, mais pas les +versions antérieures. *A priori*, il est possible d’utiliser n’importe quel SGBD supporté par Django avec celcatsanitizer. Cependant, l’utilisation de @@ -28,7 +27,7 @@ d’installer le module psycopg2_. Pour l’instant, l’installation doit passer par git_. -.. _Django 2.0: https://www.djangoproject.com/ +.. _Django 2.2: https://www.djangoproject.com/ .. _requests: http://docs.python-requests.org/en/master/ .. _BeautifulSoup4: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ @@ -46,12 +45,9 @@ celcatsanitizer utilise des versions assez récentes de Django, notamment en ce qui concerne son ORM. Le passage de Django 1.10 à Django 1.11 s’est fait pour utiliser l’annotation ``ExtractWeek``, le passage de Django 1.11 à Django 2.0 pour utiliser l’attribut -``distinct`` de l’aggrégat ``ArrayAgg``. - -celcatsanitizer passera à Django 2.1 lorsqu’il sortira pour utiliser -l’annotation ``TruncWeek``, pour l’instant implémenté avec une requête -SQL brute. Cette fonctionnalité ne sera nécessaire que pour les -utilisateurs de PostgreSQL. +``distinct`` de l’aggrégat ``ArrayAgg``. Il utilise l’aggrégat +``TruncWeek`` apparu dans Django 2.1, et se base officiellement sur +Django 2.2 pour bénéficier du support à long terme. Installation ============ @@ -191,7 +187,7 @@ __ Sélection du parseur ```````````````````` celcatsanitizer dispose d’un système de parseurs modulaires depuis la -:ref:`version 0.14 `, et embarque par défaut deux +:ref:`version 0.14 `, et embarque par défaut trois parseurs : - ``edt.management.parsers.ups2017``, pour le format utilisé par @@ -201,6 +197,9 @@ parseurs : - ``edt.management.parsers.ups2018``, pour le format utilisé par l’Université Paul Sabatier en 2018. Ce parseur utilise LXML_ et exploite l’IO asynchrone de Python. + - ``edt.management.parsers.ups2019``, pour le format utilisé par + l’Université Paul Sabatier en 2019. Ce parseur utilise le module + JSON standard. Pour spécifier le parseur à utiliser, il faut rajouter une variable ``CS_PARSER``, contenant le parseur à utiliser sous forme de chaîne de diff --git a/Documentation/usage/versions.rst b/Documentation/usage/versions.rst index ed76ce5..d4e6d1f 100644 --- a/Documentation/usage/versions.rst +++ b/Documentation/usage/versions.rst @@ -118,3 +118,35 @@ Version 0.14.4 -------------- - Ajout d’une liste de logiciels lisant les calendriers au format ICS et déconseillant l’usage de Google Calendar. + +.. _ref-ver-0.15: + +Version 0.15 +============ +Changements externes +-------------------- + - Utilisation du nouveau nom de ICSdroid (maintenant ICSx⁵) dans la + page des calendriers. + - Copyright 2019. + +Changements internes +-------------------- + - Ajout de tests pour le parseur UPS2018. + - Ajout du parseur UPS2019. + - Ajout d’une table « modules ». + - Mise à jour de Python. La version minimale supportée est la 3.6. + - Mise à jour de Django et utilisation de TruncWeek. + +Autres remarques +---------------- +Les objectifs originaux de celcatsanitizer consistaient en ceux de la +:ref:`version 0.16 `, à savoir : + + - Optimisation des requêtes en utilisant des fonctionnalités + spécifiques à PostgreSQL si nécessaire + - Remplacement du moteur de templates de Django par Jinja2_. + +.. _Jinja2: http://jinja.pocoo.org/ + +Ils n’ont pas pu être suivis à cause d’un manque de temps et de tests +et ont étés reportés à la version 0.16. -- cgit v1.2.1