aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlban Gruin2017-11-26 18:27:55 +0100
committerAlban Gruin2017-11-26 18:27:55 +0100
commit772caa72ce7f80bfeb5fbb1d05b57838dafd48c3 (patch)
tree00482ecc09db0eefd0617c6f9225b7c3e6f96161
parentffffd9842dbaba0b0e89ff5f434f45792e2b73b6 (diff)
parent4998865c2d5e94c53547e44aed1dc825163c0c4a (diff)
Merge branch 'stable/0.12.z' into prod/pa1ch/0.y.zv0.12.0-pa1ch
-rw-r--r--README.md175
-rw-r--r--__init__.py2
-rw-r--r--admin.py2
-rw-r--r--management/commands/_private.py2
-rw-r--r--models.py63
-rw-r--r--templates/group_list.html2
-rw-r--r--tests.py74
-rw-r--r--utils.py38
-rw-r--r--views.py29
9 files changed, 238 insertions, 149 deletions
diff --git a/README.md b/README.md
index 20a2cbd..7dc9d5c 100644
--- a/README.md
+++ b/README.md
@@ -1,127 +1,192 @@
# celcatsanitizer
-celcatsanitizer est un système qui permet de récupérer des emplois du temps Celcat au format XML pour les afficher correctement.
+celcatsanitizer est un système qui permet de récupérer des emplois du
+temps Celcat au format XML pour les afficher correctement.
## Pourquoi ?
-Parce que les emplois du temps Celcat sont peu lisibles et peuvent facilement faire planter un navigateur, à cause du surplus d’informations affichées.
+Parce que les emplois du temps Celcat sont peu lisibles et peuvent
+facilement faire planter un navigateur, à cause du surplus
+d’informations affichées.
## Comment faire tourner celcatsanitizer chez moi ?
-celcatsanitizer est écrit en Python 3. Il dépend des bibliothèques suivantes :
- * Django 1.11
- * requests
- * BeautifulSoup4
- * icalendar
-
-Pour installer celcatsanitizer, il est possible d’utiliser git.
-
-Pour tester celcatsanitizer, il est recommandé d’utiliser SQLite ou PostgreSQL.
-
-Pour la production, il est recommandé d’utiliser PostgreSQL (avec le driver psycopg2) et de mettre le tout dans un environnement virtuel.
-
-Aucun autre SGBD n’a été testé, mais depuis la version 0.8.0, celcatsanitizer n’utilise plus de fonctions SQL brutes spécifiques. Tous les SGBD supportés par Django devraient fonctionner sans poser de problèmes.
+celcatsanitizer est écrit en Python 3. Il dépend des bibliothèques
+suivantes :
+ * [Django 1.11](https://www.djangoproject.com/)
+ * [requests](http://docs.python-requests.org/en/master/)
+ * [BeautifulSoup4](https://www.crummy.com/software/BeautifulSoup/bs4/doc/)
+ * [icalendar](https://icalendar.readthedocs.io/en/latest/)
+
+Pour installer celcatsanitizer, il est possible d’utiliser
+[git](https://git-scm.com/).
+
+Pour tester celcatsanitizer, il est recommandé d’utiliser
+[SQLite](https://www.sqlite.org/) ou
+[PostgreSQL](https://www.postgresql.org/?&).
+
+Pour la production, il est recommandé d’utiliser PostgreSQL (avec le
+driver
+[psycopg2](http://initd.org/psycopg/docs/install.html#binary-install-from-pypi))
+et de mettre le tout dans un environnement virtuel.
+
+Aucun autre SGBD n’a été testé, mais depuis la version 0.8.0,
+celcatsanitizer n’utilise plus de fonctions SQL brutes
+spécifiques. Tous les SGBD supportés par Django devraient fonctionner
+sans poser de problèmes.
### Installation
-Il est préférable d’utiliser un environnement virtuel, mais ce n’est pas obligatoire. Si vous ne souhaitez pas utiliser un environnement virtuel, passez directement à l’installation des dépendances.
+Il est préférable d’utiliser un environnement virtuel, mais ce n’est
+pas obligatoire. Si vous ne souhaitez pas utiliser un environnement
+virtuel, passez directement à l’installation des dépendances.
#### Création de l’environnement virtuel
-Déplacez-vous dans le répertoire souhaité, installez l’environnement virtuel, et activez-le :
+Déplacez-vous dans le répertoire souhaité, installez l’environnement
+virtuel, et activez-le :
-> $ virtualenv -p python3 celcatsanitizer
+```bash
+$ virtualenv -p python3 celcatsanitizer
+$ cd celcatsanitizer
+$ source bin/activate
+```
-> $ cd celcatsanitizer
+Il est possible que votre version de pip soit ancienne. Si vous le
+souhaitez, mettez ce programme à jour :
-> $ source bin/activate
-
-Il est possible que votre version de pip soit ancienne. Si vous le souhaitez, mettez ce programme à jour :
-
-> $ pip install --upgrade pip
+```bash
+$ pip install --upgrade pip
+```
#### Installation des dépendances
-> $ pip install requests django beautifulsoup4 icalendar
+```bash
+$ pip install requests django beautifulsoup4 icalendar
+```
-Si vous utilisez PostgreSQL, vous allez avoir besoin du driver psycopg2 :
+Si vous utilisez PostgreSQL, vous allez avoir besoin du driver
+psycopg2 :
-> $ pip install psycopg2
+```bash
+$ pip install psycopg2
+```
SQLite n’a pas besoin de driver.
#### Création du répertoire Django
-> $ django-admin startproject celcatsanitizer
-
-> $ cd celcatsanitizer
+```bash
+$ django-admin startproject celcatsanitizer
+$ cd celcatsanitizer
+```
#### Récupération des sources de celcatsanitizer
-> $ git clone https://git.pa1ch.fr/alban/celcatsanitizer.git edt
+```bash
+$ git clone https://git.pa1ch.fr/alban/celcatsanitizer.git edt
+```
-Pour la production, il est recommandé d’utiliser une version stable, accessibles à travers les tags git.
+Pour la production, il est recommandé d’utiliser une version stable,
+accessibles à travers les tags git.
#### Configuration de Django
-Dans le fichier celcatsanitizer/settings.py, vous devrez renseigner plusieurs informations.
+Dans le fichier celcatsanitizer/settings.py, vous devrez renseigner
+plusieurs informations.
##### Configuration des administrateurs
-[Vous pouvez retrouver la documentation de la variable ADMIN sur le site de Django.](https://docs.djangoproject.com/fr/1.11/ref/settings/#admins)
+[Vous pouvez retrouver la documentation de la variable ADMIN sur le
+site de
+Django.](https://docs.djangoproject.com/fr/1.11/ref/settings/#admins)
Cette variable est obligatoire.
##### Configuration de l’internationalisation
-Ce passage n’est pas obligatoire. [Vous pouvez retrouver la documentation de l’internationalisation sur le site de Django.](https://docs.djangoproject.com/fr/1.11/topics/i18n/)
+Ce passage n’est pas obligatoire. [Vous pouvez retrouver la
+documentation de l’internationalisation sur le site de
+Django.](https://docs.djangoproject.com/fr/1.11/topics/i18n/)
##### Configuration de la base de données
-[Vous pouvez retrouver la documentation de la base de données sur le site de Django.](https://docs.djangoproject.com/fr/1.11/ref/settings/#databases)
+[Vous pouvez retrouver la documentation de la base de données sur le
+site de
+Django.](https://docs.djangoproject.com/fr/1.11/ref/settings/#databases)
##### Configuration du mode de Django
-Si jamais vous utiliser Django en production, vous **devez** mettre la variable DEBUG à False.
+Si jamais vous utiliser Django en production, vous **devez** mettre la
+variable DEBUG à False.
##### Ajout de celcatsanitizer dans la liste des applications Django
-Ajoutez la chaine de caractère « edt » à la fin de la liste INSTALLED_APPS.
+Ajoutez la chaine de caractère « edt » à la fin de la liste
+INSTALLED_APPS.
##### Configuration des flatpages
-celcatsanitizer utilise les flatpages pour rendre les pages « contact » et « à propos ». Vous pouvez retrouver le guide d’installation sur [le site de Django](https://docs.djangoproject.com/fr/1.11/ref/contrib/flatpages/#installation). Effectuez uniquement les deux premières étapes, celcatsanitizer enregistre déjà une route pour les pages statiques, et la commande de l’étape 4 sera effectuée plus loin.
+celcatsanitizer utilise les flatpages pour rendre les pages
+« contact » et « à propos ». Vous pouvez retrouver le guide
+d’installation sur [le site de
+Django](https://docs.djangoproject.com/fr/1.11/ref/contrib/flatpages/#installation). Effectuez
+uniquement les deux premières étapes, celcatsanitizer enregistre déjà
+une route pour les pages statiques, et la commande de l’étape 4 sera
+effectuée plus loin.
##### Ajout du processeur de contexte de celcatsanitizer
Cette étape est fortement recommandée, mais pas obligatoire.
-Rajoutez la chaine de caractères 'edt.views.ctx_processor' à la liste 'context_processors' dans la variable « TEMPLATES ».
+Rajoutez la chaine de caractères 'edt.views.ctx_processor' à la liste
+'context_processors' dans la variable « TEMPLATES ».
##### Ajout des URLs de celcatsanitizer
-Dans le fichier celcatsanitizer/urls.py, importez la fonction django.conf.urls.include, et ajoutez url(r'^', include("edt.urls")) à la **fin** de la liste urlspatterns.
+Dans le fichier celcatsanitizer/urls.py, importez la fonction
+django.conf.urls.include, et ajoutez url(r'^', include("edt.urls")) à
+la **fin** de la liste urlspatterns.
##### Génération de la base de données
Générer les migrations de celcatsanitizer, puis appliquez-les :
-> $ ./manage.py makemigrations edt
-
-> $ ./manage.py migrate
+```bash
+$ ./manage.py makemigrations edt
+$ ./manage.py migrate
+```
##### Gestion des fichiers statiques
-Si vous êtes en production, vous devez renseigner l’emplacement de vos fichiers statiques dans la variable [STATIC_ROOT](https://docs.djangoproject.com/fr/1.11/ref/settings/#std:setting-STATIC_ROOT) de la configuration de Django, puis exécuter la commande suivante :
+Si vous êtes en production, vous devez renseigner l’emplacement de vos
+fichiers statiques dans la variable
+[STATIC_ROOT](https://docs.djangoproject.com/fr/1.11/ref/settings/#std:setting-STATIC_ROOT)
+de la configuration de Django, puis exécuter la commande suivante :
-> $ ./manage.py collectstatic
+```bash
+$ ./manage.py collectstatic
+```
Cette étape est inutile si vous êtes en mode de déboguage.
### Lancement de celcatsanitizer
-Si vous êtes en mode de déboguage, lancez le serveur de cette manière :
+Si vous êtes en mode de déboguage, lancez le serveur de cette
+manière :
-> $ ./manage.py runserver
+```bash
+$ ./manage.py runserver
+```
-Si vous êtes en production, il n’est pas recommandé d’utiliser ce serveur. Exécutez Django avec le module mod_wsgi d’Apache, ou avec un serveur [gunicorn](http://gunicorn.org/) derrière nginx.
+Si vous êtes en production, il n’est pas recommandé d’utiliser ce
+serveur. Exécutez Django avec le module mod_wsgi d’Apache, ou avec un
+serveur [gunicorn](http://gunicorn.org/) derrière nginx.
### Configuration de celcatsanitizer
#### Administrateur
-Pour avoir accès à l’interface d’administration, vous devez créer un utilisateur avec les droits administrateur. Pour cela, exécutez la commande suivante :
+Pour avoir accès à l’interface d’administration, vous devez créer un
+utilisateur avec les droits administrateur. Pour cela, exécutez la
+commande suivante :
-> $ ./manage.py createsuperuser
+```bash
+$ ./manage.py createsuperuser
+```
-Renseignez ensuite votre nom d’utilisateur, mot de passe et adresse email au fur et à mesure.
+Renseignez ensuite votre nom d’utilisateur, mot de passe et adresse
+email au fur et à mesure.
#### Pages statiques
-Comme indiqué plus haut, celcatsanitizer utilise l’application flatpages de Django.
+Comme indiqué plus haut, celcatsanitizer utilise l’application
+flatpages de Django.
-Si vous êtes en production, vous devez changer le site de base (« example.com ») par le site de celcatsanitizer.
+Si vous êtes en production, vous devez changer le site de base
+(« example.com ») par le site de celcatsanitizer.
Vous devez ensuite rajouter les pages /a-propos/ et /contact/.
-Vous pouvez effectuer tout ça à partir de l’interface d’administration de Django.
+Vous pouvez effectuer tout ça à partir de l’interface d’administration
+de Django.
diff --git a/__init__.py b/__init__.py
index 6a6789b..c438abb 100644
--- a/__init__.py
+++ b/__init__.py
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with celcatsanitizer. If not, see <http://www.gnu.org/licenses/>.
-VERSION = "0.11.0-pa1ch"
+VERSION = "0.12.0-pa1ch"
__version__ = VERSION
default_app_config = "edt.apps.EdtConfig"
diff --git a/admin.py b/admin.py
index c93b6b3..0235fb9 100644
--- a/admin.py
+++ b/admin.py
@@ -44,7 +44,7 @@ class TimetableAdmin(admin.ModelAdmin):
class GroupAdmin(admin.ModelAdmin):
fieldsets = (
(None, {"fields": ("name", "celcat_name", "timetable", "hidden",)}),
- ("Groupes", {"fields": ("mention", "subgroup",)}),)
+ ("Groupes", {"fields": ("mention", "semester", "subgroup",)}),)
list_display = ("name", "timetable", "hidden",)
list_editable = ("hidden",)
list_filter = ("timetable",)
diff --git a/management/commands/_private.py b/management/commands/_private.py
index b663454..4dd9262 100644
--- a/management/commands/_private.py
+++ b/management/commands/_private.py
@@ -54,7 +54,7 @@ def get_event(timetable, event, event_week, today):
end = add_time(date, event.endtime.text)
# On ne traite pas le cours si il commence après le moment du traitement
- if begin < today:
+ if today is not None and begin < today:
return
# Création de l’objet cours
diff --git a/models.py b/models.py
index 3af1c06..25ddd1b 100644
--- a/models.py
+++ b/models.py
@@ -16,8 +16,7 @@
from functools import reduce
from django.db import models
-from django.db.models import Count, Manager, Q, Subquery
-from django.db.models.expressions import OuterRef
+from django.db.models import Count, Manager, Q
from django.db.models.functions import ExtractWeek, ExtractYear
from django.utils import timezone
from django.utils.text import slugify
@@ -57,7 +56,8 @@ class Timetable(SlugModel):
url = models.URLField(max_length=255, verbose_name="URL")
slug = models.SlugField(max_length=64, default="")
- last_update_date = models.DateTimeField(verbose_name="dernière mise à jour Celcat", null=True)
+ last_update_date = models.DateTimeField(verbose_name="dernière mise à jour Celcat",
+ null=True, blank=True)
def __str__(self):
return self.year.name + " " + self.name
@@ -71,22 +71,17 @@ class Timetable(SlugModel):
class GroupManager(Manager):
def get_parents(self, group):
- groups_criteria = Q(subgroup__isnull=True) | Q(subgroup__startswith=group.subgroup) | \
- reduce(lambda x, y: x | y,
- [Q(subgroup=group.subgroup[:i])
- for i in range(1, len(group.subgroup) + 1)])
+ groups_criteria = Q(subgroup="")
- return self.get_queryset().filter(groups_criteria, mention=group.mention,
- timetable=group.timetable)
-
- def get_relevant_groups(self, timetable, *args, **criteria):
- sub = self.get_queryset().filter(timetable=timetable, mention=OuterRef("mention"),
- subgroup__startswith=OuterRef("subgroup")) \
- .order_by().values("mention").annotate(c=Count("*")).values("c")
+ if len(group.subgroup) != 0:
+ groups_criteria |= reduce(lambda x, y: x | y,
+ [Q(subgroup=group.subgroup[:i])
+ for i in range(1, len(group.subgroup) + 1)])
- return self.get_queryset().filter(*args, timetable=timetable, hidden=False, **criteria) \
- .annotate(nbsub=Subquery(sub, output_field=models.IntegerField())) \
- .filter(Q(nbsub=1) | Q(nbsub__isnull=True)).order_by("name")
+ return self.get_queryset().filter(groups_criteria,
+ Q(semester=None) | Q(semester=group.semester),
+ mention=group.mention,
+ timetable=group.timetable)
class Group(models.Model):
@@ -99,26 +94,26 @@ class Group(models.Model):
verbose_name="emploi du temps")
mention = models.CharField(max_length=128)
- subgroup = models.CharField(max_length=16, verbose_name="sous-groupe",
- null=True)
+ semester = models.IntegerField(verbose_name="semestre", null=True)
+ subgroup = models.CharField(max_length=16, verbose_name="sous-groupe", default="")
slug = models.SlugField(max_length=64, default="")
hidden = models.BooleanField(verbose_name="caché", default=False)
- def corresponds_to(self, timetable_id, mention, subgroup):
+ def corresponds_to(self, mention, semester, subgroup):
subgroup_corresponds = True
if self.subgroup is not None and subgroup is not None:
- subgroup_corresponds = subgroup.startswith(self.subgroup) or \
- self.subgroup.startswith(subgroup)
+ subgroup_corresponds = self.subgroup.startswith(subgroup)
- return self.timetable.id == timetable_id and \
- self.mention.startswith(mention) and \
- subgroup_corresponds
+ return (self.mention.startswith(mention) or \
+ mention.startswith(self.mention)) and \
+ (self.semester == semester or semester is None) and \
+ subgroup_corresponds
@property
def group_info(self):
- return self.timetable.id, self.mention, self.subgroup
+ return self.mention, self.semester, self.subgroup
def __str__(self):
return self.name
@@ -128,12 +123,15 @@ class Group(models.Model):
self.name = self.celcat_name
self.slug = slugify(self.name)
- self.mention, self.subgroup = parse_group(self.name)
+ self.mention, self.semester, self.subgroup = parse_group(self.name)
+ if self.subgroup is None:
+ self.subgroup = ""
+
super(Group, self).save()
class Meta:
- index_together = ("mention", "subgroup",)
+ index_together = ("mention", "semester", "subgroup",)
unique_together = (("name", "timetable",),
("celcat_name", "timetable",),
("slug", "timetable",),)
@@ -158,15 +156,16 @@ class CourseManager(Manager):
def get_courses_for_group(self, group, **criteria):
return self.get_queryset() \
.filter(groups__in=Group.objects.get_parents(group), **criteria) \
- .order_by("begin")
+ .order_by("begin").prefetch_related("rooms")
def get_weeks(self, **criteria):
return self.get_queryset() \
.filter(**criteria) \
.order_by("groups__name", "year", "week") \
- .annotate(_=Count(("groups", "year", "week", "begin")),
- year=ExtractYear("begin"),
- week=ExtractWeek("begin"))
+ .annotate(year=ExtractYear("begin"),
+ week=ExtractWeek("begin")) \
+ .values("groups__mention", "groups__semester",
+ "groups__subgroup", "year", "week")
class Course(models.Model):
diff --git a/templates/group_list.html b/templates/group_list.html
index aebb0db..f21b2b1 100644
--- a/templates/group_list.html
+++ b/templates/group_list.html
@@ -7,7 +7,7 @@
<h3><a href="{{ timetable.url }}">{{ timetable }}</a></h3>
<ul>
{% for group in groups %}
- <li><a class="text"{% if group.weeks is not None %} href="{% url "timetable" timetable.year.slug timetable.slug group.slug %}"{% endif %}>{{ group }}</a> &ndash; {% for week in group.weeks %}<a href="{% url "timetable" timetable.year.slug timetable.slug group.slug week.year week|dt_week %}">{{ week|dt_prettyprint }}</a> {% empty %}<em>aucun cours</em>{% endfor %}</li>
+ <li><a class="text"{% if group.weeks is not None %} href="{% url "timetable" timetable.year.slug timetable.slug group.slug %}"{% endif %}>{{ group }}</a> &mdash; {% for week in group.weeks %}<a href="{% url "timetable" timetable.year.slug timetable.slug group.slug week.year week|dt_week %}">{{ week|dt_prettyprint }}</a> {% if not forloop.last %}&ndash; {% endif %}{% empty %}<em>aucun cours dans le mois à venir</em>{% endfor %}</li>
{% endfor %}
</ul>
{% endblock %}
diff --git a/tests.py b/tests.py
index 051c733..1425a84 100644
--- a/tests.py
+++ b/tests.py
@@ -69,6 +69,8 @@ class GroupTestCase(TestCase):
Group.objects.create(celcat_name="L1 info s2 TDB2", timetable=self.timetable)
Group.objects.create(celcat_name="L1 info s2 TPB21", timetable=self.timetable)
+ Group.objects.create(celcat_name="L1 info (toutes sections et semestres confondus)", timetable=self.timetable)
+
def test_corresponds(self):
cma = Group.objects.get(celcat_name="L1 info s2 CMA", timetable=self.timetable)
tda2 = Group.objects.get(celcat_name="L1 info s2 TDA2", timetable=self.timetable)
@@ -78,29 +80,45 @@ class GroupTestCase(TestCase):
tdb2 = Group.objects.get(celcat_name="L1 info s2 TDB2", timetable=self.timetable)
tpb21 = Group.objects.get(celcat_name="L1 info s2 TPB21", timetable=self.timetable)
- self.assertTrue(cma.corresponds_to(*tda2.group_info)) # CMA corresponds to TDA2
- self.assertTrue(cma.corresponds_to(*tpa21.group_info)) # CMA corresponds to TPA21
- self.assertTrue(tda2.corresponds_to(*tpa21.group_info)) # TDA2 corresponds to TPA21
+ general = Group.objects.get(celcat_name="L1 info (toutes sections et semestres confondus)", timetable=self.timetable)
+
+ self.assertFalse(cma.corresponds_to(*tda2.group_info))
+ self.assertFalse(cma.corresponds_to(*tpa21.group_info))
+ self.assertFalse(tda2.corresponds_to(*tpa21.group_info))
+
+ self.assertFalse(cmb.corresponds_to(*tdb2.group_info))
+ self.assertFalse(cmb.corresponds_to(*tpb21.group_info))
+ self.assertFalse(tdb2.corresponds_to(*tpb21.group_info))
+
+ self.assertFalse(cmb.corresponds_to(*tda2.group_info))
+ self.assertFalse(cmb.corresponds_to(*tpa21.group_info))
+ self.assertFalse(tdb2.corresponds_to(*tpa21.group_info))
- self.assertTrue(cmb.corresponds_to(*tdb2.group_info)) # CMB corresponds to TDB2
- self.assertTrue(cmb.corresponds_to(*tpb21.group_info)) # CMB corresponds to TPB21
- self.assertTrue(tdb2.corresponds_to(*tpb21.group_info)) # TDB2 corresponds to TPB21
+ self.assertTrue(tda2.corresponds_to(*cma.group_info))
+ self.assertTrue(tpa21.corresponds_to(*cma.group_info))
+ self.assertTrue(tpa21.corresponds_to(*tda2.group_info))
- self.assertFalse(cmb.corresponds_to(*tda2.group_info)) # CMB does not corresponds to TDA2
- self.assertFalse(cmb.corresponds_to(*tpa21.group_info)) # CMB does not corresponds to TPA21
- self.assertFalse(tdb2.corresponds_to(*tpa21.group_info)) # TDB2 does not corresponds to TPA21
+ self.assertTrue(tdb2.corresponds_to(*cmb.group_info))
+ self.assertTrue(tpb21.corresponds_to(*cmb.group_info))
+ self.assertTrue(tpb21.corresponds_to(*tdb2.group_info))
- self.assertTrue(tda2.corresponds_to(*cma.group_info)) # TDA2 corresponds to CMA
- self.assertTrue(tpa21.corresponds_to(*cma.group_info)) # TPA21 corresponds to CMA
- self.assertTrue(tpa21.corresponds_to(*tda2.group_info)) # TPA21 corresponds to TDA2
+ self.assertFalse(tda2.corresponds_to(*cmb.group_info))
+ self.assertFalse(tpa21.corresponds_to(*cmb.group_info))
+ self.assertFalse(tpa21.corresponds_to(*tdb2.group_info))
- self.assertTrue(tdb2.corresponds_to(*cmb.group_info)) # TDB2 corresponds to CMB
- self.assertTrue(tpb21.corresponds_to(*cmb.group_info)) # TPB21 corresponds to CMB
- self.assertTrue(tpb21.corresponds_to(*tdb2.group_info)) # TPB21 corresponds to TDB2
+ self.assertFalse(general.corresponds_to(*cma.group_info))
+ self.assertFalse(general.corresponds_to(*cmb.group_info))
+ self.assertFalse(general.corresponds_to(*tda2.group_info))
+ self.assertFalse(general.corresponds_to(*tdb2.group_info))
+ self.assertFalse(general.corresponds_to(*tpa21.group_info))
+ self.assertFalse(general.corresponds_to(*tpb21.group_info))
- self.assertFalse(tda2.corresponds_to(*cmb.group_info)) # TDA2 does not corresponds to CMB
- self.assertFalse(tpa21.corresponds_to(*cmb.group_info)) # TPA21 does not corresponds to CMB
- self.assertFalse(tpa21.corresponds_to(*tdb2.group_info)) # TPA21 does not corresponds to TDB2
+ self.assertTrue(cma.corresponds_to(*general.group_info))
+ self.assertTrue(cmb.corresponds_to(*general.group_info))
+ self.assertTrue(tda2.corresponds_to(*general.group_info))
+ self.assertTrue(tdb2.corresponds_to(*general.group_info))
+ self.assertTrue(tpa21.corresponds_to(*general.group_info))
+ self.assertTrue(tpb21.corresponds_to(*general.group_info))
def test_get(self):
cma = Group.objects.get(name="L1 info s2 CMA", timetable=self.timetable)
@@ -111,6 +129,8 @@ class GroupTestCase(TestCase):
tdb2 = Group.objects.get(name="L1 info s2 TDB2", timetable=self.timetable)
tpb21 = Group.objects.get(name="L1 info s2 TPB21", timetable=self.timetable)
+ general = Group.objects.get(celcat_name="L1 info (toutes sections et semestres confondus)", timetable=self.timetable)
+
self.assertEqual(cma.celcat_name, "L1 info s2 CMA")
self.assertEqual(tda2.celcat_name, "L1 info s2 TDA2")
self.assertEqual(tpa21.celcat_name, "L1 info s2 TPA21")
@@ -119,6 +139,8 @@ class GroupTestCase(TestCase):
self.assertEqual(tdb2.celcat_name, "L1 info s2 TDB2")
self.assertEqual(tpb21.celcat_name, "L1 info s2 TPB21")
+ self.assertEqual(general.celcat_name, "L1 info (toutes sections et semestres confondus)")
+
def test_parse(self):
cma = Group.objects.get(celcat_name="L1 info s2 CMA", timetable=self.timetable)
tda2 = Group.objects.get(celcat_name="L1 info s2 TDA2", timetable=self.timetable)
@@ -128,10 +150,14 @@ class GroupTestCase(TestCase):
tdb2 = Group.objects.get(celcat_name="L1 info s2 TDB2", timetable=self.timetable)
tpb21 = Group.objects.get(celcat_name="L1 info s2 TPB21", timetable=self.timetable)
- self.assertEqual(cma.group_info, (self.timetable.id, "L1 info", "A"))
- self.assertEqual(tda2.group_info, (self.timetable.id, "L1 info", "A2"))
- self.assertEqual(tpa21.group_info, (self.timetable.id, "L1 info", "A21"))
+ general = Group.objects.get(celcat_name="L1 info (toutes sections et semestres confondus)", timetable=self.timetable)
+
+ self.assertEqual(cma.group_info, ("L1 info", 2, "A"))
+ self.assertEqual(tda2.group_info, ("L1 info", 2, "A2"))
+ self.assertEqual(tpa21.group_info, ("L1 info", 2, "A21"))
+
+ self.assertEqual(cmb.group_info, ("L1 info", 2, "B"))
+ self.assertEqual(tdb2.group_info, ("L1 info", 2, "B2"))
+ self.assertEqual(tpb21.group_info, ("L1 info", 2, "B21"))
- self.assertEqual(cmb.group_info, (self.timetable.id, "L1 info", "B"))
- self.assertEqual(tdb2.group_info, (self.timetable.id, "L1 info", "B2"))
- self.assertEqual(tpb21.group_info, (self.timetable.id, "L1 info", "B21"))
+ self.assertEqual(general.group_info, ("L1 info", None, ""))
diff --git a/utils.py b/utils.py
index 5d55eca..9f0a6b5 100644
--- a/utils.py
+++ b/utils.py
@@ -30,7 +30,7 @@ def get_current_or_next_week():
def get_week(year, week):
start = timezone.make_aware(datetime.datetime.strptime(
- "{0}-W{1:02d}-1".format(year, week), "%Y-W%W-%w"))
+ "{0}-W{1}-1".format(year, week), "%Y-W%W-%w"))
end = start + datetime.timedelta(weeks=1)
return start, end
@@ -48,24 +48,32 @@ def group_courses(courses):
def parse_group(name):
# Explication de la regex
#
- # ^(.+?)\s*(s\d\s+)?((CM|TD|TP|G)(\w\d{0,3}))?(\s+\(.+\))?$
- # ^ début de la ligne
- # (.+?) correspond à au moins un caractère
- # \s* éventuellement un ou plusieurs espaces
- # (s\d\s+)? éventuellement un s suivi d’un nombre et d’un ou plusieurs espaces
- # ((CM|TD|TP|G) « CM » ou « TD » ou « TP » ou « G »
- # (\w\d{0,3}) suivi d’un caractère puis entre 0 et 3 chiffres
- # )? groupe optionnel
- # (\s+ un ou plusieurs espaces
- # \(.+\))? un ou pliseurs caractères entre parenthèses
- # $ fin de la ligne
- group_regex = re.compile(r"^(.+?)\s*(s\d\s+)?((CM|TD|TP|G)(\w\d{0,3}))?(\s+\(.+\))?$")
+ # ^(.+?)\s*(s(\d)\s+)?((CM|TD|TP|G)(\w\d{0,3}))?(\s+\(.+\))?$
+ # ^ début de la ligne
+ # (.+?) correspond à au moins un caractère
+ # \s* éventuellement un ou plusieurs espaces
+ # (s(\d)\s+)? éventuellement un s suivi d’un nombre et d’un ou plusieurs espaces
+ # ((CM|TD|TP|G) « CM » ou « TD » ou « TP » ou « G »
+ # (\w\d{0,3}) suivi d’un caractère puis entre 0 et 3 chiffres
+ # )? groupe optionnel
+ # (\s+ un ou plusieurs espaces
+ # \(.+\))? un ou pliseurs caractères entre parenthèses
+ # $ fin de la ligne
+ group_regex = re.compile(r"^(.+?)\s*(s(\d)\s+)?((CM|TD|TP|G)(\w\d{0,3}))?(\s+\(.+\))?$")
search = group_regex.search(name)
if search is None:
- return name, None
+ return name, None, None
parts = search.groups()
- return parts[0], parts[4]
+
+ # On retourne la section (parts[0]), le semestre (parts[2]) et le groupe (parts[5])
+ if parts[2] is not None:
+ return parts[0], int(parts[2]), parts[5]
+ else:
+ # Si jamais le semestre n’est pas présent dans la chaine parsée,
+ # parts[2] sera à None et sa conversion vers un int va provoquer
+ # une erreur.
+ return parts[0], None, parts[5]
def tz_now():
"""Retourne la date et l’heure avec le bon fuseau horaire"""
diff --git a/views.py b/views.py
index c0c63cb..c647fac 100644
--- a/views.py
+++ b/views.py
@@ -31,21 +31,23 @@ def index(request):
def mention_list(request, year_slug):
year = get_object_or_404(Year, slug=year_slug)
- timetables = Timetable.objects.order_by("name").filter(year=year)
+ timetables = Timetable.objects.order_by("name").filter(year=year).select_related("year")
return render(request, "index.html", {"year": year, "elements": timetables})
-def group_list_common(request, timetable, groups):
+def group_list(request, year_slug, timetable_slug):
+ timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug)
+ groups = Group.objects.filter(timetable=timetable, hidden=False).order_by("name")
+
start, _ = get_week(*get_current_week())
end = start + datetime.timedelta(weeks=4)
- groups_weeks = Course.objects.get_weeks(begin__gte=start, begin__lt=end, timetable=timetable) \
- .values("groups__mention", "groups__subgroup",
- "year", "week")
+ groups_weeks = Course.objects.get_weeks(begin__gte=start, begin__lt=end, groups__in=groups)
for group in groups:
for group_week in groups_weeks:
- if group.corresponds_to(timetable.id, group_week["groups__mention"],
+ if group.corresponds_to(group_week["groups__mention"],
+ group_week["groups__semester"],
group_week["groups__subgroup"]):
if not hasattr(group, "weeks"):
group.weeks = []
@@ -59,11 +61,6 @@ def group_list_common(request, timetable, groups):
return render(request, "group_list.html", {"timetable": timetable, "groups": groups})
-def group_list(request, year_slug, timetable_slug):
- timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug)
- groups = Group.objects.get_relevant_groups(timetable)
- return group_list_common(request, timetable, groups)
-
def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=None):
current_year, current_week = get_current_or_next_week()
is_old_timetable, provided_week = False, True
@@ -74,19 +71,13 @@ def timetable(request, year_slug, timetable_slug, group_slug, year=None, week=No
elif (int(year), int(week)) < (current_year, current_week):
is_old_timetable = True
- start, end = get_week(int(year), int(week))
+ start, end = get_week(year, week)
timetable = get_object_or_404(Timetable, year__slug=year_slug, slug=timetable_slug)
group = get_object_or_404(Group, slug=group_slug, timetable=timetable)
- if Group.objects.filter(timetable=timetable, mention=group.mention,
- subgroup__startswith=group.subgroup).count() > 1:
- subgroups = Group.objects.get_relevant_groups(timetable, mention=group.mention,
- subgroup__startswith=group.subgroup)
- return group_list_common(request, timetable, subgroups)
-
courses = Course.objects.get_courses_for_group(group, begin__gte=start, begin__lt=end)
- if courses.count() == 0 and provided_week:
+ if not courses.exists() and provided_week:
raise Http404
last_update = courses.aggregate(Max("last_update"))["last_update__max"]