8 Commits

Author SHA1 Message Date
  Alban Gruin aa29ed0143 Version 0.14.4-1-pa1ch 3 months ago
  Alban Gruin 3db1e1cf31 Merge branch 'ag/api' into prod/pa1ch/0.14.z 3 months ago
  Alban Gruin 9987d47ec8 Documentation: ajout d’une doc sur l’API REST 3 months ago
  Alban Gruin 1a582c97ec api: ajout d’un routeur pour l’API, branchement sur urls.py 3 months ago
  Alban Gruin 549e087ac3 api: complétion des vues de l’API 3 months ago
  Alban Gruin 1764d2d8f4 api: ajout des vues de l’API 3 months ago
  Alban Gruin 66853337a5 api: ajout d’un sérialiseur pour chaque modèle 3 months ago
  Alban Gruin 7084c5238c requirements: ajout d’une dépendance à Django REST Framework 3 months ago
10 changed files with 976 additions and 3 deletions
  1. 1
    1
      Documentation/conf.py
  2. 1
    0
      Documentation/index.rst
  3. 19
    0
      Documentation/usage/installation.rst
  4. 697
    0
      Documentation/usage/rest.rst
  5. 1
    1
      __init__.py
  6. 57
    0
      api/serializers.py
  7. 25
    0
      api/urls.py
  8. 170
    0
      api/views.py
  9. 1
    0
      requirements.txt
  10. 4
    1
      urls.py

+ 1
- 1
Documentation/conf.py View File

@@ -15,7 +15,7 @@ copyright = u'%d, Alban Gruin' % year
15 15
 author = u'Alban Gruin'
16 16
 
17 17
 version = u'0.14'
18
-release = u'0.14.4'
18
+release = u'0.14.4-1'
19 19
 
20 20
 language = 'fr'
21 21
 

+ 1
- 0
Documentation/index.rst View File

@@ -63,6 +63,7 @@ Utilisation de celcatsanitizer
63 63
    usage/commands/listtimetables
64 64
    usage/commands/reparse
65 65
    usage/commands/timetables
66
+   usage/rest
66 67
    usage/versions
67 68
 
68 69
 Développement

+ 19
- 0
Documentation/usage/installation.rst View File

@@ -8,6 +8,7 @@ celcatsanitizer est écrit en Python 3. Il dépend des bibliothèques
8 8
 suivantes :
9 9
 
10 10
  - `Django 2.0`_
11
+ - `Django REST Framework`_
11 12
  - requests_, pour récupérer les emplois du temps en HTTP(S)
12 13
  - BeautifulSoup4_ et LXML_, pour parser les emplois du temps en XML
13 14
  - icalendar_, pour générer des fichiers ICS_.
@@ -25,6 +26,7 @@ d’installer le module psycopg2_.
25 26
 Pour l’instant, l’installation doit passer par git_.
26 27
 
27 28
 .. _Django 2.0: https://www.djangoproject.com/
29
+.. _Django REST Framework: https://www.django-rest-framework.org/
28 30
 .. _requests: http://docs.python-requests.org/en/master/
29 31
 .. _BeautifulSoup4:
30 32
   https://www.crummy.com/software/BeautifulSoup/bs4/doc/
@@ -155,6 +157,23 @@ Si jamais vous utilisez Django en production, vous **devez
155 157
 impérativement** mettre la valeur de la variable ``DEBUG`` à
156 158
 ``False``.
157 159
 
160
+Configuration de Django REST Framework
161
+``````````````````````````````````````
162
+Ajoutez la chaîne de caractère ``rest_framework`` à la liste
163
+``INSTALLED_APPS``.
164
+
165
+Libre à vous de configurer DRF de la manière dont vous le souhaitez.
166
+`Les différents paramètres sont accessibles ici`__.  Les plus
167
+intéressants sont ``DEFAULT_PERMISSION_CLASSES``,
168
+``DEFAULT_RENDERER_CLASSES``, ``DEFAULT_PAGINATION_CLASS`` et
169
+``PAGE_SIZE``.
170
+
171
+__ https://www.django-rest-framework.org/api-guide/settings/
172
+
173
+Cette étape est **obligatoire**, mais deviendra optionnelle dans le
174
+futur.  Dans le cas ou vous ne souhaiterez pas la faire, l’API REST ne
175
+sera pas activée.
176
+
158 177
 Ajout de celcatsanitizer à la liste des applications Django
159 178
 ```````````````````````````````````````````````````````````
160 179
 Ajoutez la chaîne de caractère ``edt`` à la liste ``INSTALLED_APPS``.

+ 697
- 0
Documentation/usage/rest.rst View File

@@ -0,0 +1,697 @@
1
+========
2
+API REST
3
+========
4
+
5
+celcatsanitizer dispose d’une API REST pour permettre à des outils
6
+tiers d’accéder facilement à ses données, qui sont renvoyées en JSON.
7
+Pour l’instant, il ne permet pas la modification des données.
8
+
9
+Le point d’entrée se trouve à l’adresse ``api/`` de l’instance de
10
+celcatsanitizer.  Il retourne la liste des autres points d’accès en
11
+JSON.
12
+
13
+Années
14
+======
15
+
16
+``api/years/``
17
+--------------
18
+Liste les années par ordre alphabétique de nom.  :ref:`Le résultat
19
+peut être paginé <ref-pagination>`.
20
+
21
+Exemple :
22
+`````````
23
+.. code:: json
24
+
25
+  {
26
+      "count": 4,
27
+      "next": null,
28
+      "previous": null,
29
+      "results": [
30
+          {
31
+              "id": 3,
32
+              "name": "L1",
33
+              "slug": "l1"
34
+          },
35
+          {
36
+              "id": 4,
37
+              "name": "L2",
38
+              "slug": "l2"
39
+          },
40
+          {
41
+              "id": 1,
42
+              "name": "L3",
43
+              "slug": "l3"
44
+          },
45
+          {
46
+              "id": 2,
47
+              "name": "M1",
48
+              "slug": "m1"
49
+          }
50
+      ]
51
+  }
52
+
53
+``api/years/<id>/``
54
+-------------------
55
+Retourne seulement une année.
56
+
57
+Exemple :
58
+`````````
59
+.. code:: json
60
+
61
+  {
62
+      "id": 1,
63
+      "name": "L3",
64
+      "slug": "l3"
65
+  }
66
+
67
+``api/years/<id>/timetables``
68
+-----------------------------
69
+Liste les emplois du temps associés à une année par ordre alphabétique
70
+de nom.  :ref:`Le résultat peut être paginé <ref-pagination>`.
71
+
72
+Exemple :
73
+`````````
74
+.. code:: json
75
+
76
+  {
77
+      "count": 2,
78
+      "next": null,
79
+      "previous": null,
80
+      "results": [
81
+          {
82
+              "id": 2,
83
+              "name": "1ere année SRI",
84
+              "slug": "1ere-annee-sri",
85
+              "year": 1,
86
+              "source": 2
87
+          },
88
+          {
89
+              "id": 1,
90
+              "name": "Info",
91
+              "slug": "info",
92
+              "year": 1,
93
+              "source": 1
94
+          }
95
+      ]
96
+  }
97
+
98
+Emplois du temps
99
+================
100
+
101
+``api/timetables``
102
+------------------
103
+Liste les emplois du temps par ordre d’année (ID associé) puis de nom.
104
+:ref:`Le résultat peut être paginé <ref-pagination>`.
105
+
106
+Exemple :
107
+`````````
108
+.. code:: json
109
+
110
+  {
111
+      "count": 1,
112
+      "next": null,
113
+      "previous": null,
114
+      "results": [
115
+          {
116
+              "id": 1,
117
+              "name": "Info",
118
+              "slug": "info",
119
+              "year": 1,
120
+              "source": 1
121
+          }
122
+      ]
123
+  }
124
+
125
+``api/timetables/<id>/``
126
+------------------------
127
+Retourne seulement un emploi du temps.
128
+
129
+Exemple :
130
+`````````
131
+.. code:: json
132
+
133
+  {
134
+    "id": 1,
135
+    "name": "Info",
136
+    "slug": "info",
137
+    "year": 1,
138
+    "source": 1
139
+  }
140
+
141
+``api/timetables/<id>/groups/``
142
+-------------------------------
143
+Retourne la liste des groupes associés à un emploi du temps, triés par
144
+ordre alphabétique.  :ref:`Le résultat peut être paginé
145
+<ref-pagination>`.
146
+
147
+Exemple :
148
+`````````
149
+.. code:: json
150
+
151
+  {
152
+      "count": 2,
153
+      "next": null,
154
+      "previous": null,
155
+      "results": [
156
+          {
157
+              "id": 207,
158
+              "name": "L2 Info s1 CMA",
159
+              "celcat_name": "L2 Info s1 CMA",
160
+              "mention": "L2 Info",
161
+              "semester": 1,
162
+              "subgroup": "A",
163
+              "slug": "l2-info-s1-cma",
164
+              "hidden": false,
165
+              "source": 1
166
+          },
167
+          {
168
+              "id": 208,
169
+              "name": "L3 INFO (toutes sections et semestres confondus)",
170
+              "celcat_name": "L3 INFO (toutes sections et semestres confondus)",
171
+              "mention": "L3 INFO",
172
+              "semester": null,
173
+              "subgroup": "",
174
+              "slug": "l3-info-toutes-sections-et-semestres-confondus",
175
+              "hidden": false,
176
+              "source": 1
177
+          }
178
+      ]
179
+  }
180
+
181
+Sources
182
+=======
183
+
184
+``api/sources/``
185
+----------------
186
+Retourne la liste des sources par ordre d’ID.  :ref:`Le résultat peut
187
+être paginé <ref-pagination>`.
188
+
189
+Exemple :
190
+`````````
191
+.. code:: json
192
+
193
+  {
194
+      "count": 2,
195
+      "next": null,
196
+      "previous": null,
197
+      "results": [
198
+          {
199
+              "id": 1,
200
+              "url": "https://edt.univ-tlse3.fr/calendar/default.aspx?View=month&Type=group&ResourceN  ame=formation_ELINFE",
201
+              "last_update_date": null
202
+          },
203
+          {
204
+              "id": 2,
205
+              "url": "https://edt.univ-tlse3.fr/calendar/default.aspx?View=month&Type=group&ResourceN  ame=formation_ELUSR1_s1",
206
+              "last_update_date": null
207
+          }
208
+      ]
209
+  }
210
+
211
+``api/sources/<id>/``
212
+---------------------
213
+Renvoie seulement une source.
214
+
215
+Exemple :
216
+`````````
217
+.. code:: json
218
+
219
+  {
220
+      "id": 1,
221
+      "url": "https://edt.univ-tlse3.fr/calendar/default.aspx?View=month&Type=group&ResourceName=formation_ELINFE",
222
+      "last_update_date": null
223
+  }
224
+
225
+``api/sources/<id>/timetables/``
226
+--------------------------------
227
+Renvoie la liste des emplois du temps associé à une source triés par
228
+ordre alphabétique.  :ref:`Le résultat peut être paginé
229
+<ref-pagination>`.
230
+
231
+Exemple :
232
+`````````
233
+.. code:: json
234
+
235
+  {
236
+      "count": 1,
237
+      "next": null,
238
+      "previous": null,
239
+      "results": [
240
+          {
241
+              "id": 1,
242
+              "name": "Info",
243
+              "slug": "info",
244
+              "year": 1,
245
+              "source": 1
246
+          }
247
+      ]
248
+  }
249
+
250
+Groupes
251
+=======
252
+
253
+``api/groups/``
254
+---------------
255
+Liste les groupes par ordre alphabétique.  :ref:`Le résultat peut être
256
+paginé <ref-pagination>`.
257
+
258
+Exemple :
259
+`````````
260
+.. code:: json
261
+
262
+  {
263
+      "count": 2,
264
+      "next": null,
265
+      "previous": null,
266
+      "results": [
267
+          {
268
+              "id": 207,
269
+              "name": "L2 Info s1 CMA",
270
+              "celcat_name": "L2 Info s1 CMA",
271
+              "mention": "L2 Info",
272
+              "semester": 1,
273
+              "subgroup": "A",
274
+              "slug": "l2-info-s1-cma",
275
+              "hidden": false,
276
+              "source": 1
277
+          },
278
+          {
279
+              "id": 208,
280
+              "name": "L3 INFO (toutes sections et semestres confondus)",
281
+              "celcat_name": "L3 INFO (toutes sections et semestres confondus)",
282
+              "mention": "L3 INFO",
283
+              "semester": null,
284
+              "subgroup": "",
285
+              "slug": "l3-info-toutes-sections-et-semestres-confondus",
286
+              "hidden": false,
287
+              "source": 1
288
+          }
289
+      ]
290
+  }
291
+
292
+``api/groups/<id>/``
293
+--------------------
294
+Affiche seulement un groupe.
295
+
296
+Exemple :
297
+`````````
298
+.. code:: json
299
+
300
+  {
301
+      "id": 207,
302
+      "name": "L2 Info s1 CMA",
303
+      "celcat_name": "L2 Info s1 CMA",
304
+      "mention": "L2 Info",
305
+      "semester": 1,
306
+      "subgroup": "A",
307
+      "slug": "l2-info-s1-cma",
308
+      "hidden": false,
309
+      "source": 1
310
+  }
311
+
312
+.. _ref-groups-courses:
313
+
314
+``api/groups/<id>/courses/``
315
+----------------------------
316
+Retourne tous les cours d’un groupe et de ses parents triés par ordre
317
+de début.  :ref:`Le résultat peut être paginé <ref-pagination>`.
318
+
319
+Exemple :
320
+`````````
321
+.. code:: json
322
+
323
+  {
324
+      "count": 2,
325
+      "next": null,
326
+      "previous": null,
327
+      "results": [
328
+          {
329
+              "id": 34723,
330
+              "groups": [
331
+                  {
332
+                      "id": 98,
333
+                      "name": "L3 INFO s1 CMA",
334
+                      "celcat_name": "L3 INFO s1 CMA",
335
+                      "mention": "L3 INFO",
336
+                      "semester": 1,
337
+                      "subgroup": "A",
338
+                      "slug": "l3-info-s1-cma",
339
+                      "hidden": false,
340
+                      "source": 1
341
+                  },
342
+                  {
343
+                      "id": 207,
344
+                      "name": "L2 Info s1 CMA",
345
+                      "celcat_name": "L2 Info s1 CMA",
346
+                      "mention": "L2 Info",
347
+                      "semester": 1,
348
+                      "subgroup": "A",
349
+                      "slug": "l2-info-s1-cma",
350
+                      "hidden": false,
351
+                      "source": 1
352
+                  }
353
+              ],
354
+              "rooms": [],
355
+              "name": "REUNION / RENCONTRE",
356
+              "type": null,
357
+              "notes": "Nuit de l'info",
358
+              "begin": "2018-12-06T13:30:00+01:00",
359
+              "end": "2018-12-06T23:45:00+01:00",
360
+              "last_update": "2018-12-31T13:26:57.122490+01:00",
361
+              "source": 1
362
+          },
363
+          {
364
+              "id": 34727,
365
+              "groups": [
366
+                  {
367
+                      "id": 98,
368
+                      "name": "L3 INFO s1 CMA",
369
+                      "celcat_name": "L3 INFO s1 CMA",
370
+                      "mention": "L3 INFO",
371
+                      "semester": 1,
372
+                      "subgroup": "A",
373
+                      "slug": "l3-info-s1-cma",
374
+                      "hidden": false,
375
+                      "source": 1
376
+                  },
377
+                  {
378
+                      "id": 207,
379
+                      "name": "L2 Info s1 CMA",
380
+                      "celcat_name": "L2 Info s1 CMA",
381
+                      "mention": "L2 Info",
382
+                      "semester": 1,
383
+                      "subgroup": "A",
384
+                      "slug": "l2-info-s1-cma",
385
+                      "hidden": false,
386
+                      "source": 1
387
+                  }
388
+              ],
389
+              "rooms": [],
390
+              "name": "REUNION / RENCONTRE",
391
+              "type": null,
392
+              "notes": "Nuit de l'info",
393
+              "begin": "2018-12-07T07:45:00+01:00",
394
+              "end": "2018-12-07T23:45:00+01:00",
395
+              "last_update": "2018-12-31T13:26:57.136381+01:00",
396
+              "source": 1
397
+          }
398
+      ]
399
+  }
400
+
401
+``api/groups/<id>/courses/weeks/``
402
+----------------------------------
403
+Retourne la liste des semaines de cours d’un groupe.
404
+
405
+Exemple :
406
+`````````
407
+.. code:: json
408
+
409
+  [
410
+      "2018-12-03T00:00:00+01:00"
411
+  ]
412
+
413
+``api/groups/<id>/courses/weeks/current/``
414
+------------------------------------------
415
+Retourne la liste des cours du groupe et de ses parents pendant la
416
+semaine courante (ou prochaine lors du week-end) d’un groupe, par
417
+ordre de début.  :ref:`Le résultat peut être paginé <ref-pagination>`.
418
+Le format du résultat est identique à celui de
419
+:ref:`api/groups/\<id>/courses/ <ref-groups-courses>`.
420
+
421
+.. _ref-groups-courses-week-arg:
422
+
423
+``api/groups/<id>/courses/weeks/<year>/<week>/``
424
+------------------------------------------------
425
+Retourne la liste des cours du groupe et de ses parents pendant la
426
+semaine spécifiée, par ordre de début.  Si l’année et la semaine ne
427
+sont pas des nombres, un code 404 est renvoyé.  Si la semaine n’est
428
+pas comprise entre 1 et 53, une erreur 400 est renvoyée, et les
429
+erreurs rencontrées sont renvoyées.  :ref:`Le résultat peut être
430
+paginé <ref-pagination>`.  Le format du résultat est identique à celui
431
+de :ref:`api/groups/\<id>/courses/ <ref-groups-courses>`.
432
+
433
+Exemple d’erreur (``api/groups/<id>/courses/weeks/2018/111/``) :
434
+````````````````````````````````````````````````````````````````
435
+.. code:: json
436
+
437
+  {
438
+      "week": "Rentrez une semaine valide"
439
+  }
440
+
441
+Salles
442
+======
443
+
444
+``api/rooms/``
445
+--------------
446
+Liste les salles par ordre alphabétique.  :ref:`Le résultat peut être
447
+paginé <ref-pagination>`.
448
+
449
+Exemple :
450
+`````````
451
+.. code:: json
452
+
453
+  {
454
+      "count": 3,
455
+      "next": null,
456
+      "previous": null,
457
+      "results": [
458
+          {
459
+              "id": 26,
460
+              "name": "1R1-010",
461
+              "slug": "1r1-010"
462
+          },
463
+          {
464
+              "id": 11,
465
+              "name": "1TP1-B08",
466
+              "slug": "1tp1-b08"
467
+          },
468
+          {
469
+              "id": 5,
470
+              "name": "1TP1-B08bis",
471
+              "slug": "1tp1-b08bis"
472
+          }
473
+      ]
474
+  }
475
+
476
+``api/rooms/<id>``
477
+------------------
478
+Renvoie une seule salle.
479
+
480
+Exemple :
481
+`````````
482
+.. code:: json
483
+
484
+  {
485
+      "id": 26,
486
+      "name": "1R1-010",
487
+      "slug": "1r1-010"
488
+  }
489
+
490
+``api/rooms/<id>/courses/``
491
+---------------------------
492
+Renvoie la liste des cours se déroulant dans une salle par ordre de
493
+début.  :ref:`Le résultat peut être paginé <ref-pagination>`.  Le
494
+format du résultat est identique à celui de
495
+:ref:`api/groups/\<id>/courses/ <ref-groups-courses>`.
496
+
497
+``api/rooms/<id>/courses/weeks/current/``
498
+-----------------------------------------
499
+Renvoie la liste des cours se déroulant dans une salle pendant la
500
+semaine courante (ou la semaine prochaine le week-end) par ordre de
501
+début.  :ref:`Le résultat peut être paginé <ref-pagination>`.  Le
502
+format du résultat est identique à celui de
503
+:ref:`api/groups/\<id>/courses/ <ref-groups-courses>`.
504
+
505
+``api/rooms/<id>/courses/weeks/<year>/<week>/``
506
+-----------------------------------------------
507
+Renvoie la liste des cours se déroulant dans une salle pendant la
508
+semaine spécifiée.  Si l’année et la semaine ne sont pas des nombres,
509
+un code 404 est renvoyé.  Si la semaine n’est pas comprise entre 1 et
510
+53, une erreur 400 est renvoyée, et les erreurs rencontrées sont
511
+renvoyées.  :ref:`Le résultat peut être paginé <ref-pagination>`.  Le
512
+format du résultat est identique à celui de
513
+:ref:`api/groups/\<id>/courses/weeks/\<year>/\<week>
514
+<ref-groups-courses-week-arg>`.
515
+
516
+``api/rooms/qsjps/<day>/<begin>/<end>/``
517
+----------------------------------------
518
+Fournit un accès à QSJPS.  ``<day>`` est une date devant être formatée
519
+de cette manière : ``YYYY-MM-DD``.  ``<begin>`` et ``<end>`` sont des
520
+heures qui doivent être formatées de cette manière : ``HH:mm``.  La
521
+valeur de ``<begin>`` doit être inférieure à celle de ``<end>``.
522
+
523
+Renvoie la liste des salles vides le début du jour ``<day>`` de
524
+``<begin>`` à ``<end>``.
525
+
526
+En cas de mauvais formatage, une erreur 400 est renvoyée, et les
527
+erreurs sont détaillées dans le corps de la réponse.  Sinon, la liste
528
+des salles libres est renvoyée.
529
+
530
+Exemple :
531
+`````````
532
+.. code:: json
533
+
534
+  [
535
+    {
536
+        "id": 26,
537
+        "name": "1R1-010",
538
+        "slug": "1r1-010"
539
+    },
540
+    {
541
+        "id": 11,
542
+        "name": "1TP1-B08",
543
+        "slug": "1tp1-b08"
544
+    },
545
+    {
546
+        "id": 5,
547
+        "name": "1TP1-B08bis",
548
+        "slug": "1tp1-b08bis"
549
+    }
550
+  ]
551
+
552
+Exemple d’erreur (``api/rooms/qsjps/2019-01-35/12:00/10:00/``) :
553
+````````````````````````````````````````````````````````````````
554
+.. code:: json
555
+
556
+  {
557
+      "day": [
558
+          "Saisissez une date valide."
559
+      ],
560
+      "end": [
561
+          "L’heure de début doit être supérieure à celle de fin."
562
+      ]
563
+  }
564
+
565
+Exemple d’erreur (``api/rooms/qsjps/2019-01-35/12:70/10:70/``) :
566
+````````````````````````````````````````````````````````````````
567
+.. code:: json
568
+
569
+  {
570
+      "day": [
571
+          "Saisissez une date valide."
572
+      ],
573
+      "begin": [
574
+          "Saisissez une heure valide."
575
+      ],
576
+      "end": [
577
+          "Saisissez une heure valide."
578
+      ]
579
+  }
580
+
581
+Cours
582
+=====
583
+
584
+``api/courses/``
585
+----------------
586
+Renvoie la liste des cours.  :ref:`Le résultat peut être paginé
587
+<ref-pagination>`.
588
+
589
+Exemple :
590
+`````````
591
+.. code:: json
592
+
593
+  {
594
+      "count": 4766,
595
+      "next": "http://localhost:8000/api/courses/?page=2",
596
+      "previous": null,
597
+      "results": [
598
+          {
599
+              "id": 22133,
600
+              "groups": [
601
+                  {
602
+                      "id": 98,
603
+                      "name": "L3 INFO s1 CMA",
604
+                      "celcat_name": "L3 INFO s1 CMA",
605
+                      "mention": "L3 INFO",
606
+                      "semester": 1,
607
+                      "subgroup": "A",
608
+                      "slug": "l3-info-s1-cma",
609
+                      "hidden": false,
610
+                      "source": 1
611
+                  }
612
+              ],
613
+              "rooms": [
614
+                  {
615
+                      "id": 1,
616
+                      "name": "Amphi AMPERE (3A)",
617
+                      "slug": "amphi-ampere-3a"
618
+                  }
619
+              ],
620
+              "name": "REUNION / RENCONTRE",
621
+              "type": null,
622
+              "notes": null,
623
+              "begin": "2018-09-04T15:45:00+02:00",
624
+              "end": "2018-09-04T16:45:00+02:00",
625
+              "last_update": "2018-09-26T19:34:12.924533+02:00",
626
+              "source": 1
627
+          },
628
+          …
629
+      ]
630
+    }
631
+
632
+``api/courses/<id>``
633
+--------------------
634
+Renvoie un seul cours.
635
+
636
+Exemple :
637
+`````````
638
+.. code:: json
639
+
640
+    {
641
+      "id": 22133,
642
+      "groups": [
643
+          {
644
+              "id": 98,
645
+              "name": "L3 INFO s1 CMA",
646
+              "celcat_name": "L3 INFO s1 CMA",
647
+              "mention": "L3 INFO",
648
+              "semester": 1,
649
+              "subgroup": "A",
650
+              "slug": "l3-info-s1-cma",
651
+              "hidden": false,
652
+              "source": 1
653
+          }
654
+      ],
655
+      "rooms": [
656
+          {
657
+              "id": 1,
658
+              "name": "Amphi AMPERE (3A)",
659
+              "slug": "amphi-ampere-3a"
660
+          }
661
+      ],
662
+      "name": "REUNION / RENCONTRE",
663
+      "type": null,
664
+      "notes": null,
665
+      "begin": "2018-09-04T15:45:00+02:00",
666
+      "end": "2018-09-04T16:45:00+02:00",
667
+      "last_update": "2018-09-26T19:34:12.924533+02:00",
668
+      "source": 1
669
+  }
670
+
671
+.. _ref-pagination:
672
+
673
+Pagination
674
+==========
675
+Il est possible que les résultats soient paginés.  Cela dépend de la
676
+configuration de l’instance de celcatsanitizer.  Dans ce cas, tous les
677
+appels à des points d’accès renvoyant des résultats pouvant être
678
+paginés se trouvent dans ce genre de structure :
679
+
680
+.. code:: json
681
+
682
+  {
683
+      "count": 4766,
684
+      "next": "http://localhost:8000/api/courses/?page=2",
685
+      "previous": null,
686
+      "results": [
687
+          …
688
+      ]
689
+  }
690
+
691
+ - ``count`` représente le nombre d’éléments au total (et non pas sur
692
+   la page).
693
+ - ``next`` est le lien de la page de résultats suivants, si il y en a
694
+   une.
695
+ - ``previous`` est le lien de la page de résultats précédents, si il
696
+   y en a une.
697
+ - ``results`` est la liste des résultats, si il y en a.

+ 1
- 1
__init__.py View File

@@ -13,7 +13,7 @@
13 13
 #    You should have received a copy of the GNU Affero General Public License
14 14
 #    along with celcatsanitizer.  If not, see <http://www.gnu.org/licenses/>.
15 15
 
16
-VERSION = "0.14.4"
16
+VERSION = "0.14.4-1"
17 17
 __version__ = VERSION
18 18
 
19 19
 default_app_config = "edt.apps.EdtConfig"

+ 57
- 0
api/serializers.py View File

@@ -0,0 +1,57 @@
1
+#    Copyright (C) 2019  Alban Gruin
2
+#
3
+#    celcatsanitizer is free software: you can redistribute it and/or modify
4
+#    it under the terms of the GNU Affero General Public License as published
5
+#    by the Free Software Foundation, either version 3 of the License, or
6
+#    (at your option) any later version.
7
+#
8
+#    celcatsanitizer is distributed in the hope that it will be useful,
9
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+#    GNU Affero General Public License for more details.
12
+#
13
+#    You should have received a copy of the GNU Affero General Public License
14
+#    along with celcatsanitizer.  If not, see <http://www.gnu.org/licenses/>.
15
+
16
+from rest_framework import serializers
17
+
18
+from ..models import Course, Group, Room, Source, Timetable, Year
19
+
20
+
21
+class YearSerializer(serializers.ModelSerializer):
22
+    class Meta:
23
+        model = Year
24
+        fields = "__all__"
25
+
26
+
27
+class SourceSerializer(serializers.ModelSerializer):
28
+    class Meta:
29
+        model = Source
30
+        fields = "__all__"
31
+
32
+
33
+class TimetableSerializer(serializers.ModelSerializer):
34
+    class Meta:
35
+        model = Timetable
36
+        fields = "__all__"
37
+
38
+
39
+class GroupSerializer(serializers.ModelSerializer):
40
+    class Meta:
41
+        model = Group
42
+        fields = "__all__"
43
+
44
+
45
+class RoomSerializer(serializers.ModelSerializer):
46
+    class Meta:
47
+        model = Room
48
+        fields = "__all__"
49
+
50
+
51
+class CourseSerializer(serializers.ModelSerializer):
52
+    groups = GroupSerializer(many=True, read_only=True)
53
+    rooms = RoomSerializer(many=True, read_only=True)
54
+
55
+    class Meta:
56
+        model = Course
57
+        fields = "__all__"

+ 25
- 0
api/urls.py View File

@@ -0,0 +1,25 @@
1
+#    Copyright (C) 2019  Alban Gruin
2
+#
3
+#    celcatsanitizer is free software: you can redistribute it and/or modify
4
+#    it under the terms of the GNU Affero General Public License as published
5
+#    by the Free Software Foundation, either version 3 of the License, or
6
+#    (at your option) any later version.
7
+#
8
+#    celcatsanitizer is distributed in the hope that it will be useful,
9
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+#    GNU Affero General Public License for more details.
12
+#
13
+#    You should have received a copy of the GNU Affero General Public License
14
+#    along with celcatsanitizer.  If not, see <http://www.gnu.org/licenses/>.
15
+
16
+from rest_framework import routers
17
+from . import views
18
+
19
+router = routers.DefaultRouter()
20
+router.register(r"years", views.YearViewSet)
21
+router.register(r"sources", views.SourceViewSet)
22
+router.register(r"timetables", views.TimetableViewSet)
23
+router.register(r"rooms", views.RoomViewSet)
24
+router.register(r"groups", views.GroupViewSet)
25
+router.register(r"courses", views.CourseViewSet)

+ 170
- 0
api/views.py View File

@@ -0,0 +1,170 @@
1
+#    Copyright (C) 2019  Alban Gruin
2
+#
3
+#    celcatsanitizer is free software: you can redistribute it and/or modify
4
+#    it under the terms of the GNU Affero General Public License as published
5
+#    by the Free Software Foundation, either version 3 of the License, or
6
+#    (at your option) any later version.
7
+#
8
+#    celcatsanitizer is distributed in the hope that it will be useful,
9
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+#    GNU Affero General Public License for more details.
12
+#
13
+#    You should have received a copy of the GNU Affero General Public License
14
+#    along with celcatsanitizer.  If not, see <http://www.gnu.org/licenses/>.
15
+
16
+import datetime
17
+
18
+from django.db.models import Count
19
+from django.db.models.functions import ExtractWeek, ExtractYear
20
+from django.utils import timezone
21
+
22
+from rest_framework import viewsets
23
+from rest_framework.decorators import action, detail_route
24
+from rest_framework.response import Response
25
+
26
+from ..forms import QSJPSForm
27
+from ..models import Course, Group, Room, Source, Timetable, Year
28
+from ..utils import get_current_or_next_week, get_week
29
+
30
+from .serializers import CourseSerializer, GroupSerializer, RoomSerializer, \
31
+    SourceSerializer, TimetableSerializer, YearSerializer
32
+
33
+
34
+class YearViewSet(viewsets.ReadOnlyModelViewSet):
35
+    queryset = Year.objects.all().order_by("name")
36
+    serializer_class = YearSerializer
37
+
38
+    @detail_route(methods=["get"], url_path="timetables")
39
+    def timetable_list(self, request, pk):
40
+        year = self.get_object()
41
+        timetables = Timetable.objects.filter(year=year).distinct() \
42
+                                                        .order_by("name")
43
+        timetables_json = TimetableSerializer(
44
+            self.paginate_queryset(timetables), many=True)
45
+        return self.get_paginated_response(timetables_json.data)
46
+
47
+
48
+class SourceViewSet(viewsets.ReadOnlyModelViewSet):
49
+    queryset = Source.objects.all().order_by("pk")
50
+    serializer_class = SourceSerializer
51
+
52
+    @detail_route(methods=["get"], url_path="timetables")
53
+    def timetable_list(self, request, pk):
54
+        source = self.get_object()
55
+        timetables = Timetable.objects.filter(source=source).distinct() \
56
+                                                            .order_by("name")
57
+        timetables_json = TimetableSerializer(
58
+            self.paginate_queryset(timetables), many=True)
59
+        return self.get_paginated_response(timetables_json.data)
60
+
61
+
62
+class TimetableViewSet(viewsets.ReadOnlyModelViewSet):
63
+    queryset = Timetable.objects.all().select_related("source") \
64
+                                      .order_by("year", "name")
65
+    serializer_class = TimetableSerializer
66
+
67
+    @detail_route(methods=["get"], url_path="groups")
68
+    def group_list(self, request, pk):
69
+        timetable = self.get_object()
70
+        groups = Group.objects.filter(source=timetable.source).distinct() \
71
+                                                              .order_by("name")
72
+        groups_json = GroupSerializer(self.paginate_queryset(groups),
73
+                                      many=True)
74
+        return self.get_paginated_response(groups_json.data)
75
+
76
+
77
+class CourseListGroupSet(viewsets.ReadOnlyModelViewSet):
78
+    @detail_route(methods=["get"], url_path="courses")
79
+    def course_list(self, request, pk):
80
+        obj = self.get_object()
81
+        courses = Course.objects.get_courses(obj).prefetch_related("groups")
82
+        courses_json = CourseSerializer(self.paginate_queryset(courses),
83
+                                        many=True)
84
+        return self.get_paginated_response(courses_json.data)
85
+
86
+    @detail_route(methods=["get"], url_path="courses/weeks/current")
87
+    def current_week(self, request, pk):
88
+        obj = self.get_object()
89
+        start, end = get_week(*get_current_or_next_week())
90
+
91
+        courses = Course.objects.get_courses(obj,
92
+                                             begin__gte=start, end__lt=end) \
93
+                                .prefetch_related("groups")
94
+        courses_json = CourseSerializer(self.paginate_queryset(courses),
95
+                                        many=True)
96
+        return self.get_paginated_response(courses_json.data)
97
+
98
+    @detail_route(methods=["get"],
99
+                  url_path="courses/weeks/(?P<year>\d+)/(?P<week>\d+)")
100
+    def other_week(self, request, pk, year, week):
101
+        obj = self.get_object()
102
+
103
+        errors = {}
104
+        if not year.isdigit():
105
+            errors["year"] = "Rentrez une année valide"
106
+        if not week.isdigit() or not 0 < int(week) <= 53:
107
+            errors["week"] = "Rentrez une semaine valide"
108
+        if errors:
109
+            return Response(errors, status=400)
110
+
111
+        start, end = get_week(int(year), int(week))
112
+
113
+        courses = Course.objects.get_courses(obj,
114
+                                             begin__gte=start, end__lt=end) \
115
+                                .prefetch_related("groups")
116
+        courses_json = CourseSerializer(self.paginate_queryset(courses),
117
+                                        many=True)
118
+        return self.get_paginated_response(courses_json.data)
119
+
120
+
121
+class GroupViewSet(CourseListGroupSet):
122
+    queryset = Group.objects.all().order_by("name")
123
+    serializer_class = GroupSerializer
124
+
125
+    @detail_route(methods=["get"], url_path="courses/weeks")
126
+    def weeks(self, request, pk):
127
+        group = self.get_object()
128
+        groups = Group.objects.get_parents(group)
129
+
130
+        courses = Course.objects.filter(groups__in=groups) \
131
+                                .order_by("year", "week") \
132
+                                .annotate(year=ExtractYear("begin"),
133
+                                          week=ExtractWeek("begin")) \
134
+                                .values("year", "week") \
135
+                                .annotate(c=Count("*"))
136
+
137
+        weeks = [get_week(course["year"], course["week"])[0]
138
+                 for course in courses]
139
+
140
+        return Response(weeks)
141
+
142
+
143
+class RoomViewSet(CourseListGroupSet):
144
+    queryset = Room.objects.all().order_by("name")
145
+    serializer_class = RoomSerializer
146
+
147
+    @action(
148
+        methods=["get"],
149
+        detail=False,
150
+        url_path="qsjps/(?P<day>[0-9\-]+)/(?P<begin>[0-9:]+)/(?P<end>[0-9:]+)")
151
+    def qsjps(self, request, day, begin, end):
152
+        form = QSJPSForm({"day": day, "begin": begin, "end": end})
153
+        if not form.is_valid():
154
+            return Response(form.errors, status=400)
155
+
156
+        day = form.cleaned_data["day"]
157
+        begin_hour = form.cleaned_data["begin"]
158
+        end_hour = form.cleaned_data["end"]
159
+
160
+        begin = timezone.make_aware(datetime.datetime.combine(day, begin_hour))
161
+        end = timezone.make_aware(datetime.datetime.combine(day, end_hour))
162
+
163
+        rooms = Room.objects.qsjps(begin, end)
164
+        rooms_json = RoomSerializer(rooms, many=True)
165
+        return Response(rooms_json.data)
166
+
167
+
168
+class CourseViewSet(viewsets.ReadOnlyModelViewSet):
169
+    queryset = Course.objects.all().prefetch_related("groups", "rooms")
170
+    serializer_class = CourseSerializer

+ 1
- 0
requirements.txt View File

@@ -1,5 +1,6 @@
1 1
 beautifulsoup4==4.6.3
2 2
 Django==2.0.8
3
+djangorestframework==3.9.1
3 4
 gunicorn==19.9.0
4 5
 icalendar==4.0.2
5 6
 lxml==4.2.4

+ 4
- 1
urls.py View File

@@ -1,4 +1,4 @@
1
-#    Copyright (C) 2017-2018  Alban Gruin
1
+#    Copyright (C) 2017-2019  Alban Gruin
2 2
 #
3 3
 #    celcatsanitizer is free software: you can redistribute it and/or modify
4 4
 #    it under the terms of the GNU Affero General Public License as published
@@ -14,10 +14,13 @@
14 14
 #    along with celcatsanitizer.  If not, see <http://www.gnu.org/licenses/>.
15 15
 
16 16
 from django.urls import include, path
17
+
17 18
 from . import feeds, views
19
+from .api.urls import router
18 20
 
19 21
 urlpatterns = [
20 22
     path("", views.index, name="index"),
23
+    path("api/", include(router.urls)),
21 24
     path("pages/", include("django.contrib.flatpages.urls")),
22 25
     path("salles/", views.rooms, name="rooms"),
23 26
     path("salles/qsjps", views.qsjps, name="qsjps"),