From 523a52c5e5daa22d58125e0b51b1604906250473 Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Wed, 19 May 2021 20:58:25 +0200 Subject: [PATCH 01/17] Code quality * Create a makefile (for *nix only, right?) * Configure code coverage and tests * Configure linter * Configure linter for errors only --- Makefile | 13 +++++++++++++ requirements/dev.txt | 3 ++- src/.coveragerc | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 src/.coveragerc diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..516f730 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +#!/bin/bash + +.PHONY: coverage errors + +coverage: + cd src/; coverage run --source='.' manage.py test; coverage report -m; cd .. + @echo "Testing of coverage in the sources finished." + +lint: + pylint src/* --disable=django-not-configured + +errors: + pylint src/* --errors-only --disable=django-not-configured diff --git a/requirements/dev.txt b/requirements/dev.txt index 47b1c24..6cef1ea 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -6,4 +6,5 @@ flake8==3.9.1 django-spaghetti-and-meatballs==0.4.2 docutils==0.16 pytest==6.2.4 -pytest-django==4.2.0 \ No newline at end of file +pytest-django==4.2.0 + diff --git a/src/.coveragerc b/src/.coveragerc new file mode 100644 index 0000000..8d55d25 --- /dev/null +++ b/src/.coveragerc @@ -0,0 +1,15 @@ +[run] +branch = true +omit = + */tests/* + */test* + */migrations/* + */urls.py + */settings/* + */wsgi.py + */__init__.py + manage.py +source = . + +[report] +show_missing = true \ No newline at end of file -- 2.39.2 From 1a3c62d12b8144f9379d7c5f0df3afc0c50566bb Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Mon, 21 Jun 2021 21:37:37 +0200 Subject: [PATCH 02/17] Create a new `ClubStatistics` class to store data This will allow to make the location/views.py file more readable --- src/location/views.py | 56 +++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/location/views.py b/src/location/views.py index cd92c35..b01a832 100644 --- a/src/location/views.py +++ b/src/location/views.py @@ -153,6 +153,21 @@ def chooseStatistics(request): return render(request, "club_statistics.html", context) +class GymnastStatistics(): + def __init__(self, gymnast): + self.gymnast = gymnast + self.number_of_courses_by_week = 0 + self.number_of_hours_by_week = timedelta() + self.number_of_trainings = 0 + self.number_of_attendance = 0 + self.number_of_absences = 0 + self.number_of_training_hours = 0 + self.number_of_attendance_hours = timedelta() + self.percentage_of_attendance = 0 + self.number_of_hours_of_absence = 0 + self.percentage_of_absence = 0 + + @login_required def club_statistics(request, clubid): """ @@ -172,6 +187,7 @@ def club_statistics(request, clubid): gymnastsDict = {} gymnasts = [] courseList = [] + for course in courses: nbtrainer = course.trainers.count() list_of_gymnasts = Gymnast.objects.filter(to_gym__in=course.to_subgroup.all()) @@ -215,43 +231,25 @@ def club_statistics(request, clubid): ) for gymnast in list_of_gymnasts: - # print(gymnast) - if gymnast.id not in gymnastsDict: - gymnastsDict[gymnast.id] = { - "gymnast": gymnast, - "nbcoursebyweek": 0, - "nbhourbyweek": timedelta(), - "nbtraining": 0, - "nbattendance": 0, - "nbabsence": 0, - "nbhourtraining": 0, - "nbhourattendance": timedelta(), - "percentageattendance": 0, - "nbhourabsence": 0, - "percentageabsence": 0, - } - attendanceList = Training.objects.filter(course=course, gymnast=gymnast) - nbattendance = len(attendanceList) + gymnast_stats = gymnastsDict.setdefault(gymnast.id, GymnastStatistics(gymnast)) - # print(str(gymnast) + ' : ' + str(nbattendance) + ' for ' + str(course) ) + attendance_list = Training.objects.filter(course=course, gymnast=gymnast) + number_of_attendance = attendance_list.count() - gymnastsDict[gymnast.id]["nbcoursebyweek"] += 1 - gymnastsDict[gymnast.id]["nbhourbyweek"] += nbhour # timedelta - gymnastsDict[gymnast.id]["nbtraining"] += counted - gymnastsDict[gymnast.id]["nbattendance"] += nbattendance - gymnastsDict[gymnast.id]["nbhourtraining"] += totalHourForCourse - gymnastsDict[gymnast.id]["nbhourattendance"] += ( - nbhour * nbattendance - ) # timedelta - - # print(gymnastsDict[gymnast.id]) + gymnast_stats.number_of_courses_by_week += 1 + gymnast_stats.number_of_hours_by_week += nbhour + gymnast_stats.number_of_trainings += counted + gymnast_stats.number_of_attendance += number_of_attendance + gymnast_stats.number_of_training_hours += totalHourForCourse + gymnast_stats.number_of_attendance_hours += ( + nbhour * number_of_attendance + ) # tous les cours ont été traités totalHoursByWeek = totalHoursByWeek / 3600 gymnasts = set(gymnasts) - # print(gymnasts) for gymnast in gymnasts: tmp = int(gymnastsDict[gymnast.id]["nbhourbyweek"].seconds / 3600) -- 2.39.2 From de61d1851d3028dc475ac9683c2558034074decb Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Tue, 22 Jun 2021 19:15:51 +0200 Subject: [PATCH 03/17] Refactors clubs statistics --- src/khana/views.py | 24 - src/location/views.py | 23 +- src/planning/models.py | 744 +++++++++++++++-------------- src/planning/tests/tests_models.py | 36 +- 4 files changed, 421 insertions(+), 406 deletions(-) diff --git a/src/khana/views.py b/src/khana/views.py index 4c1e062..9e482df 100644 --- a/src/khana/views.py +++ b/src/khana/views.py @@ -81,23 +81,8 @@ def __getEventInfo(request): .. todo:: il refuse mon 'filter' que ce soit avant ou après le 'next(5)'. Une idée ? next_event = Event.objects.filter(club__in=(request.session["clubid"], )).next(5) """ - - # rest = 0 - # counted = 0 - # event_list = [] - # today = pendulum.now().date() next_event_list = Event.objects.next(5) - # for event in next_event: - # counted = event.get_number_of_occurence_to_event(today) - # # print('pouf !') - - # unavailabilities = Unavailability.objects.filter(datebegin__lte=event.datebegin.date(), dateend__gte=today) - # for unavailable in unavailabilities: - # counted -= unavailable.get_total_occurence() - - # event_list.append((event, counted, int((counted/16)*100))) - return next_event_list @@ -130,18 +115,9 @@ def __getCourseInfo(request): else: rest = int((tmp.days + 1) / 7) - # # select tous les unavailables liés au cours - # unavailabilities = Unavailability.objects.filter(course=course) - # for unavailable in unavailabilities: - # tmp = unavailable.get_total_occurence() - # counted -= tmp - # rest -= tmp # si un unavailability.date < today, on soustrait quand même de rest ??? Si oui => BUG !!!! - courses_left += rest courses_done += counted - rest - # courses.append((course, counted, (counted - rest))) - return courses, courses_done, courses_left diff --git a/src/location/views.py b/src/location/views.py index b01a832..5f4ff13 100644 --- a/src/location/views.py +++ b/src/location/views.py @@ -170,10 +170,14 @@ class GymnastStatistics(): @login_required def club_statistics(request, clubid): - """ - Renvoie les statistiques d'un club pour une saison choisie. + """Construit les statistiques d'un club, pour une saison choisie - .. todo:: tenir compte de la saison. + Questions: + Tu dis que cela construit les stats d'un club __pour une saison__... + Mais je ne vois pas la saison dans les paramètres... + + Todo: + * Tenir compte de la saison. """ courses = Course.objects.filter(club__in=clubid).order_by( @@ -189,13 +193,12 @@ def club_statistics(request, clubid): courseList = [] for course in courses: - nbtrainer = course.trainers.count() + number_of_trainers = course.trainers.count() list_of_gymnasts = Gymnast.objects.filter(to_gym__in=course.to_subgroup.all()) gymnasts.extend(list_of_gymnasts) - nbgymnast = len(list_of_gymnasts) - # gymnasts = set(gymnasts.extend(Gymnast.objects.filter(to_gym__in=course.to_subgroup.all()))) + number_of_gymnasts = len(list_of_gymnasts) - nbhour = __diffTime(course.hour_end, course.hour_begin) # timedelta + nbhour = __diffTime(course.hour_end, course.hour_begin) totalHoursByWeek += nbhour.seconds counted = course.get_total_occurence() @@ -211,7 +214,7 @@ def club_statistics(request, clubid): totalTimeForCourse.seconds / 3600 ) totalHours += totalHourForCourse - totalHoursPaidForCourse = totalHourForCourse * nbtrainer + totalHoursPaidForCourse = totalHourForCourse * number_of_trainers totalHoursPaid += totalHoursPaidForCourse # tmp = int(nbhour.seconds/3600) @@ -221,8 +224,8 @@ def club_statistics(request, clubid): courseList.append( ( course, - nbtrainer, - nbgymnast, + number_of_trainers, + number_of_gymnasts, hour, counted, totalHourForCourse, diff --git a/src/planning/models.py b/src/planning/models.py index 393a8f4..68ff806 100644 --- a/src/planning/models.py +++ b/src/planning/models.py @@ -13,496 +13,500 @@ from people.models import Gymnast def get_week(a_date): - """ + """ - Remarks: - Je ne comprends pas trop cette fonction... - Tu pars d'une date, et tu récupères le lundi et le samedi de la semaine correspondant ? - """ - the_date = pendulum.parse(a_date) - day = the_date.weekday() - monday = the_date - timedelta(days=day) - sunday = the_date + timedelta(days=(6 - day)) - return monday, sunday + Remarks: + Je ne comprends pas trop cette fonction... + Tu pars d'une date, et tu récupères le lundi et le samedi de la semaine correspondant ? + """ + the_date = pendulum.parse(a_date) + day = the_date.weekday() + monday = the_date - timedelta(days=day) + sunday = the_date + timedelta(days=(6 - day)) + return monday, sunday def get_number_of_weeks_between(start, stop): - """ - Renvoie le nombre de semaines entre deux dates. - Par extension, cela permet de connaitre le nombre d'occurence d'un - évènement (entraînement, par exemple) hebdromadaire entre deux dates - et ainsi pouvoir plannifier. + """ + Renvoie le nombre de semaines entre deux dates. + Par extension, cela permet de connaitre le nombre d'occurence d'un + évènement (entraînement, par exemple) hebdromadaire entre deux dates + et ainsi pouvoir plannifier. - :param start: date de début de la période - :type start: datetime.date - :param stop: date de fin de la période - :type stop: datetime.date - :return: Le nombre de semaines entre les deux dates. + :param start: date de début de la période + :type start: datetime.date + :param stop: date de fin de la période + :type stop: datetime.date + :return: Le nombre de semaines entre les deux dates. - Remarks: - Proposition d'utiliser isocalendar() sur une date. - L'indice 1 de la valeur de retour donne la semaine correspondant. + Remarks: + Proposition d'utiliser isocalendar() sur une date. + L'indice 1 de la valeur de retour donne la semaine correspondant. - Eg. - >>> from datetime import date - >>> d = date(2020, 9, 27) - >>> d.isocalendar() - (2020, 39, 7) + Eg. + >>> from datetime import date + >>> d = date(2020, 9, 27) + >>> d.isocalendar() + (2020, 39, 7) - -> Est-ce qu'il ne suffirait pas de faire la différence ? - """ + -> Est-ce qu'il ne suffirait pas de faire la différence ? + """ - tmp = stop - start - number_of_days = abs(tmp.days) - number_of_week = int((number_of_days + 1) / 7) + tmp = stop - start + number_of_days = abs(tmp.days) + number_of_week = int((number_of_days + 1) / 7) - if ((number_of_days + 1) % 7) > 0: - number_of_week += 1 + if ((number_of_days + 1) % 7) > 0: + number_of_week += 1 - if tmp.days < 0: - number_of_week *= -1 + if tmp.days < 0: + number_of_week *= -1 - return number_of_week + return number_of_week class TemporizableQuerySet(models.QuerySet): - """ - Classe permettant de spécifier le `QuerySet` de la classe `Temporizable`. - """ + """ + Classe permettant de spécifier le `QuerySet` de la classe `Temporizable`. + """ - def next(self, limit): - """ - Renvoie la liste des prochains "temporizable" (par rapport à la date du jour). + def next(self, limit): + """ + Renvoie la liste des prochains "temporizable" (par rapport à la date du jour). - :param limit: nombre d'éléments désirés. - :type limit: int - :return: une liste de `limit` éléments temporizables. - """ - return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:limit] + :param limit: nombre d'éléments désirés. + :type limit: int + :return: une liste de `limit` éléments temporizables. + """ + return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:limit] - def last(self, limit): - """ - Renvoie la liste des derniers "temporizable" (par rapport à la date du jour). + def last(self, limit): + """ + Renvoie la liste des derniers "temporizable" (par rapport à la date du jour). - :param limit: nombre d'éléments désirés. - :type limit: int - :return: une liste de `limit` éléments temporizables - """ - return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:limit] + :param limit: nombre d'éléments désirés. + :type limit: int + :return: une liste de `limit` éléments temporizables + """ + return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:limit] - # def get(self, date_string): - # """ - # """ - # try: - # selected_object = self.get(datebegin__lte=date_string, dateend__gte=date_string) - # except self.DoesNotExist: - # return None - # except self.MultipleObjectsReturned: - # return None + # def get(self, date_string): + # """ + # """ + # try: + # selected_object = self.get(datebegin__lte=date_string, dateend__gte=date_string) + # except self.DoesNotExist: + # return None + # except self.MultipleObjectsReturned: + # return None - # return selected_object + # return selected_object class Temporizable(models.Model): - """Classe abstraite définissant une période comprise entre deux dates. + """Classe abstraite définissant une période comprise entre deux dates. - """ + """ - class Meta: - abstract = True + class Meta: + abstract = True - datebegin = models.DateTimeField(verbose_name="Début") - dateend = models.DateTimeField(blank=True, verbose_name="Fin") + datebegin = models.DateTimeField(verbose_name="Début") + dateend = models.DateTimeField(blank=True, verbose_name="Fin") - objects = models.Manager.from_queryset(TemporizableQuerySet)() + objects = models.Manager.from_queryset(TemporizableQuerySet)() - def get_total_occurence(self): - """ - Renvoie le nombre de semaines entre les deux dates d'une instance de la - classe `Temporizable`. + def get_total_occurence(self): + """ + Renvoie le nombre de semaines entre les deux dates d'une instance de la + classe `Temporizable`. - :return: nombre de semaines. - """ - return get_number_of_weeks_between(self.datebegin.date(), self.dateend.date()) + :return: nombre de semaines. + """ + return get_number_of_weeks_between(self.datebegin.date(), self.dateend.date()) - def get_number_of_occurence_to_event(self, the_date): - """ - Renvoie le nombre semaines entre une date choisie et le début - (datebegin) d'une instance de la classe `Temporizable`. + def get_number_of_occurence_to_event(self, the_date): + """ + Renvoie le nombre semaines entre une date choisie et le début + (datebegin) d'une instance de la classe `Temporizable`. - :param the_date: date par rapport à laquelle le calcul sera fait. - :type the_date: datetime.date - :return: nombre de semaines. - """ - return get_number_of_weeks_between(the_date, self.datebegin.date()) + :param the_date: date par rapport à laquelle le calcul sera fait. + :type the_date: datetime.date + :return: nombre de semaines. + """ + return get_number_of_weeks_between(the_date, self.datebegin.date()) - def get_number_of_occurence_inbetween(self, the_date, rest=True): - """ - Renvoie le nombre semaines entre une date choisie et une instance de la - classe `Temporizable`. Le calcul peut se faire soit entre la date - choisie et le date de fin d'une occurence de la classe, soit entre la - date de début d'une occurence de la classe et la date choisie. + def get_number_of_occurence_inbetween(self, the_date, rest=True): + """ + Renvoie le nombre semaines entre une date choisie et une instance de la + classe `Temporizable`. Le calcul peut se faire soit entre la date + choisie et le date de fin d'une occurence de la classe, soit entre la + date de début d'une occurence de la classe et la date choisie. - :param the_date: date par rapport à laquelle le calcul sera fait. - :type the_date: datetime.date - :param rest: paramètre définissant s'il faut calculer le reste des - occurences à venir (depuis `the_date` jusqu'à la date de fin) ou - les occurences déjà passées (depuis la date de début jusqu'à - `the_date`) - :type rest: booléen - :return: nombre de semaines. - """ - if rest: - return get_number_of_weeks_between(the_date, self.dateend.date()) - else: - return get_number_of_weeks_between(self.datebegin.date(), the_date) + :param the_date: date par rapport à laquelle le calcul sera fait. + :type the_date: datetime.date + :param rest: paramètre définissant s'il faut calculer le reste des + occurences à venir (depuis `the_date` jusqu'à la date de fin) ou + les occurences déjà passées (depuis la date de début jusqu'à + `the_date`) + :type rest: booléen + :return: nombre de semaines. + """ + if rest: + return get_number_of_weeks_between(the_date, self.dateend.date()) + else: + return get_number_of_weeks_between(self.datebegin.date(), the_date) class Season(Temporizable): - """ - Classe représentant une saison. Une saison est déinie par : - - un id, - - un label, - - une date de début et - - une date de fin. + """ + Classe représentant une saison. Une saison est déinie par : + - un id, + - un label, + - une date de début et + - une date de fin. - La date de début est très souvent le : 01/09/xxxx - La date de fin est très souvent le : 31/08/xxxy - exemple : 1/9/2015 - 31/8/2016 - """ + La date de début est très souvent le : 01/09/xxxx + La date de fin est très souvent le : 31/08/xxxy + exemple : 1/9/2015 - 31/8/2016 + """ - class Meta: - verbose_name = "Season" - verbose_name_plural = "Seasons" + class Meta: + verbose_name = "Season" + verbose_name_plural = "Seasons" - label = models.CharField(max_length=11, verbose_name="Label") - # active ou default = models.BooleanField(verbose_name='Défaut') + label = models.CharField(max_length=11, verbose_name="Label") + # active ou default = models.BooleanField(verbose_name='Défaut') - def __str__(self): - return "%s" % (self.label) + def __str__(self): + return "%s" % (self.label) - def week_number_from_begin(self, target_date): - return get_number_of_weeks_between(self.datebegin.date(), target_date) + def week_number_from_begin(self, target_date): + return get_number_of_weeks_between(self.datebegin.date(), target_date) class EventType(models.Model): - """ - Classe représentant les types d'évènements. - C'est un dictionnaire fini : - - compétiton qualificative, - - compétition finale, - - démonstration, - - … - """ + """ + Classe représentant les types d'évènements. + C'est un dictionnaire fini : + - compétiton qualificative, + - compétition finale, + - démonstration, + - … + """ - class Meta: - verbose_name = "Event Type" - verbose_name_plural = "Event Types" + class Meta: + verbose_name = "Event Type" + verbose_name_plural = "Event Types" - name = models.CharField(max_length=255, verbose_name="Nom") - acronym = models.CharField(max_length=5, verbose_name="Acronyme") + name = models.CharField(max_length=255, verbose_name="Nom") + acronym = models.CharField(max_length=5, verbose_name="Acronyme") - def __str__(self): - return "%s (%s)" % (self.name, self.acronym) + def __str__(self): + return "%s (%s)" % (self.name, self.acronym) class Event(Markdownizable, Temporizable): - """Classe représentant les évènements. + """Classe représentant les évènements. - Un évènement est caractérisé par : - * un nom, - * un lieu (place), - * un type (compétition, démonstration, …), - * des gymnastes (participation prévue). - Je ne me rapelle plus à quoi sert le club. - """ + Un évènement est caractérisé par : + * un nom, + * un lieu (place), + * un type (compétition, démonstration, …), + * des gymnastes (participation prévue). + Je ne me rapelle plus à quoi sert le club. + """ - class Meta: - verbose_name = "Event" - verbose_name_plural = "Event" + class Meta: + verbose_name = "Event" + verbose_name_plural = "Event" - place = models.ForeignKey( - "location.Place", verbose_name="Lieu", on_delete=models.CASCADE, default=None - ) - eventtype = models.ForeignKey( - EventType, verbose_name="Type", on_delete=models.CASCADE, default=None - ) - name = models.CharField(max_length=255, verbose_name="Nom") - # club = models.ManyToManyField('location.Club', related_name="concernate_by", blank=True) - gymnasts = models.ManyToManyField( - "people.Gymnast", - through="Event_Participation", - related_name="participate_to", - verbose_name="Participants", - ) + place = models.ForeignKey( + "location.Place", verbose_name="Lieu", on_delete=models.CASCADE, default=None + ) + eventtype = models.ForeignKey( + EventType, verbose_name="Type", on_delete=models.CASCADE, default=None + ) + name = models.CharField(max_length=255, verbose_name="Nom") + # club = models.ManyToManyField('location.Club', related_name="concernate_by", blank=True) + gymnasts = models.ManyToManyField( + "people.Gymnast", + through="Event_Participation", + related_name="participate_to", + verbose_name="Participants", + ) - def __str__(self): - return "%s (à %s)" % (self.name, self.place.city) + def __str__(self): + return "%s (à %s)" % (self.name, self.place.city) - def save(self, *args, **kwargs): - self.checkdates() - super().save(*args, **kwargs) + def save(self, *args, **kwargs): + self.checkdates() + super().save(*args, **kwargs) - def checkdates(self): - """ - Fonction assignant la date de fin d'un évènement à la date de début, si la date - de fin n'est pas définie, l'heure de fin est par défaut 18h00. - """ - if self.dateend is None and self.datebegin is not None: - self.dateend = datetime.combine(self.datebegin.date(), time(18, 0)) + def checkdates(self): + """ + Fonction assignant la date de fin d'un évènement à la date de début, si la date + de fin n'est pas définie, l'heure de fin est par défaut 18h00. + """ + if self.dateend is None and self.datebegin is not None: + self.dateend = datetime.combine(self.datebegin.date(), time(18, 0)) - @property - def number_of_week_from_today(self): - today = pendulum.now().date() - return get_number_of_weeks_between(today, self.datebegin.date()) + @property + def number_of_week_from_today(self): + today = pendulum.now().date() + return get_number_of_weeks_between(today, self.datebegin.date()) class Event_Participation(models.Model): - """ - """ + """ + """ - event = models.ForeignKey(Event, on_delete=models.CASCADE) - gymnast = models.ForeignKey("people.Gymnast", on_delete=models.CASCADE) - rank = models.PositiveSmallIntegerField(default=0) + event = models.ForeignKey(Event, on_delete=models.CASCADE) + gymnast = models.ForeignKey("people.Gymnast", on_delete=models.CASCADE) + rank = models.PositiveSmallIntegerField(default=0) class Course(Markdownizable, Temporizable): - """Classe représentant les cours. + """Classe représentant les cours. - Un cours est défini par : - * une heure de début et une heure de fin, - * une date de début et une date de fin (un cours est considéré comme donné hebdromadairement entre - ces deux dates) (hérite de la classe `Temporizable`) - * est associé à un ou plusieurs entraineurs, - * est associé à un club - * est associé à un jour de la semaine (numéro du jour dans la semaine : 0 = lundi, 6 = dimanche). - """ + Un cours est défini par : + * une heure de début et une heure de fin, + * une date de début et une date de fin (un cours est considéré comme donné hebdromadairement entre + ces deux dates) (hérite de la classe `Temporizable`) + * est associé à un ou plusieurs entraineurs, + * est associé à un club + * est associé à un jour de la semaine (numéro du jour dans la semaine : 0 = lundi, 6 = dimanche). + """ - class Meta: - verbose_name = "Course" - verbose_name_plural = "Courses" + class Meta: + verbose_name = "Cours" + verbose_name_plural = "Cours" - DAY_CHOICE = ( - (1, "Lundi"), - (2, "Mardi"), - (3, "Mercredi"), - (4, "Jeudi"), - (5, "Vendredi"), - (6, "Samedi"), - (7, "Dimanche"), - ) + DAY_CHOICE = ( + (1, "Lundi"), + (2, "Mardi"), + (3, "Mercredi"), + (4, "Jeudi"), + (5, "Vendredi"), + (6, "Samedi"), + (7, "Dimanche"), + ) - club = models.ForeignKey( - "location.Club", verbose_name="Club", on_delete=models.CASCADE, default=None - ) - iso_day_number = models.PositiveSmallIntegerField( - choices=DAY_CHOICE, verbose_name="Jour" - ) - hour_begin = models.TimeField(verbose_name="Heure de début") - hour_end = models.TimeField(verbose_name="Heure de fin") - season = models.ForeignKey(Season, on_delete=models.SET_NULL, null=True) - trainers = models.ManyToManyField( - User, verbose_name="Coach(es)", related_name="trainee" - ) - gymnasts = models.ManyToManyField( - Gymnast, verbose_name="Gymnasts", related_name="courses" - ) + club = models.ForeignKey( + "location.Club", verbose_name="Club", on_delete=models.CASCADE, default=None + ) + iso_day_number = models.PositiveSmallIntegerField( + choices=DAY_CHOICE, verbose_name="Jour" + ) + hour_begin = models.TimeField(verbose_name="Heure de début") + hour_end = models.TimeField(verbose_name="Heure de fin") + season = models.ForeignKey(Season, on_delete=models.SET_NULL, null=True) + trainers = models.ManyToManyField( + User, verbose_name="Coach(es)", related_name="trainee" + ) + gymnasts = models.ManyToManyField( + Gymnast, verbose_name="Gymnasts", related_name="courses" + ) - def __str__(self): - return "%s (%s à %s)" % ( - self.get_iso_day_number_display(), - self.hour_begin.strftime("%H:%M"), - self.hour_end.strftime("%H:%M"), - ) + def __str__(self): + return "%s (%s à %s)" % ( + self.get_iso_day_number_display(), + self.hour_begin.strftime("%H:%M"), + self.hour_end.strftime("%H:%M"), + ) - @property - def duration(self): - """ - Renvoie la durée d'un cours en heures - """ - date_begin = pendulum.datetime( - 2000, 1, 1, self.hour_begin.hour, self.hour_begin.minute - ) - date_end = pendulum.datetime( - 2000, 1, 1, self.hour_end.hour, self.hour_end.minute - ) - return date_end.diff(date_begin).in_hours() + @property + def duration(self): + """Renvoie la durée d'un cours en heures + + Examples: + >>> course = Course(hour_begin=20, hour_end=22) + >>> course.duration + 2 + """ + date_begin = pendulum.datetime( + 2000, 1, 1, self.hour_begin.hour, self.hour_begin.minute + ) + date_end = pendulum.datetime( + 2000, 1, 1, self.hour_end.hour, self.hour_end.minute + ) + return date_end.diff(date_begin).in_hours() class Group(models.Model): - """Classe représentant les groupes (Loisir, D1, D2, A, B, …). + """Classe représentant les groupes (Loisir, D1, D2, A, B, …). - Un groupe appartient à un club. - """ + Un groupe appartient à un club. + """ - class Meta: - verbose_name = "Group" - verbose_name_plural = "Groups" + class Meta: + verbose_name = "Group" + verbose_name_plural = "Groups" - club = models.ForeignKey("location.Club", on_delete=models.CASCADE, default=None) - name = models.CharField(max_length=255) - acronym = models.CharField(max_length=50) - active = models.BooleanField(default=1) - season = models.CharField( - max_length=9, - default=str(timezone.now().year) + "-" + str(timezone.now().year + 1), - ) + club = models.ForeignKey("location.Club", on_delete=models.CASCADE, default=None) + name = models.CharField(max_length=255) + acronym = models.CharField(max_length=50) + active = models.BooleanField(default=1) + season = models.CharField( + max_length=9, + default=str(timezone.now().year) + "-" + str(timezone.now().year + 1), + ) - def __str__(self): - return "%s (%s)" % (self.name, self.acronym) + def __str__(self): + return "%s (%s)" % (self.name, self.acronym) class Subgroup(models.Model): - """Classe représentant les sous-groupes. + """Classe représentant les sous-groupes. - Un sous-groupe appartient à un groupe (lui-même lié à un club). + Un sous-groupe appartient à un groupe (lui-même lié à un club). - De cette manière, quand un gymnaste est mis dans un sous-groupe, en remontant via le groupe, - nous pouvons connaître le(s) club(s) du gymnaste pour chaque saison. - """ + De cette manière, quand un gymnaste est mis dans un sous-groupe, en remontant via le groupe, + nous pouvons connaître le(s) club(s) du gymnaste pour chaque saison. + """ - class Meta: - verbose_name = "Subgroup" - verbose_name_plural = "Subgroups" + class Meta: + verbose_name = "Subgroup" + verbose_name_plural = "Subgroups" - name = models.CharField(max_length=255) - acronym = models.CharField(max_length=50) - group = models.ForeignKey(Group, on_delete=models.CASCADE, default=None) - courses = models.ManyToManyField(Course, related_name="to_subgroup") - gymnasts = models.ManyToManyField( - "people.Gymnast", related_name="to_gym", blank=True - ) - active = models.BooleanField(default=1) + name = models.CharField(max_length=255) + acronym = models.CharField(max_length=50) + group = models.ForeignKey(Group, on_delete=models.CASCADE, default=None) + courses = models.ManyToManyField(Course, related_name="to_subgroup") + gymnasts = models.ManyToManyField( + "people.Gymnast", related_name="to_gym", blank=True + ) + active = models.BooleanField(default=1) - def __str__(self): - return "%s (%s)" % (self.name, self.group.name) + def __str__(self): + return "%s (%s)" % (self.name, self.group.name) class UnavailabilityManager(models.Manager): - """Classe représentant le manager de la classe `Unavailability`. - """ + """Classe représentant le manager de la classe `Unavailability`. + """ - def next(self, count): - return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:count] + def next(self, count): + return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:count] - def last(self, count): - return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:count] + def last(self, count): + return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:count] class Unavailability(Markdownizable, Temporizable): - """Classe représentant les indisponibilités. - """ + """Classe représentant les indisponibilités. + """ - class Meta: - verbose_name = "Indisponibilité" - verbose_name_plural = "Indisponibilités" + class Meta: + verbose_name = "Indisponibilité" + verbose_name_plural = "Indisponibilités" - course = models.ManyToManyField(Course, related_name="unavailability") + course = models.ManyToManyField(Course, related_name="unavailability") - objects = UnavailabilityManager() + objects = UnavailabilityManager() - def __str__(self): - return "du %s au %s" % (self.datebegin, self.dateend) + def __str__(self): + return "du %s au %s" % (self.datebegin, self.dateend) - def save(self, *args, **kwargs): - self.checkdates() + def save(self, *args, **kwargs): + self.checkdates() - super().save(*args, **kwargs) + super().save(*args, **kwargs) - def checkdates(self): - if self.dateend is None and self.datebegin is not None: - self.dateend = self.datebegin + def checkdates(self): + if self.dateend is None and self.datebegin is not None: + self.dateend = self.datebegin class Training(models.Model): - """Classe représentant les entraînements. + """Classe représentant les entraînements. - Un entraînement est une occurence d'un cours pendant lequel des gmnastes sont présents. + Un entraînement est une occurence d'un cours pendant lequel des gmnastes sont présents. - Un objet de cette classe lie donc un cours et un gymnaste à une date donnée. - """ + Un objet de cette classe lie donc un cours et un gymnaste à une date donnée. + """ - class Meta: - verbose_name = "Training" - verbose_name_plural = "Trainings" + class Meta: + verbose_name = "Training" + verbose_name_plural = "Trainings" - gymnast = models.ForeignKey( - "people.Gymnast", - verbose_name="Gymnast", - on_delete=models.CASCADE, - default=None, - related_name="trainings", - ) - course = models.ForeignKey( - Course, verbose_name="Course", on_delete=models.CASCADE, default=None - ) - trainingdate = models.DateField(verbose_name="Date") + gymnast = models.ForeignKey( + "people.Gymnast", + verbose_name="Gymnast", + on_delete=models.CASCADE, + default=None, + related_name="trainings", + ) + course = models.ForeignKey( + Course, verbose_name="Course", on_delete=models.CASCADE, default=None + ) + trainingdate = models.DateField(verbose_name="Date") - def __str__(self): - return "%s - %s, %s" % (self.trainingdate, self.course, self.gymnast) + def __str__(self): + return "%s - %s, %s" % (self.trainingdate, self.course, self.gymnast) - @staticmethod - def create(gymnast, course, trainingdate): - t = Training() - t.gymnast = gymnast - t.course = course - t.trainingdate = trainingdate - t.save() - return t + @staticmethod + def create(gymnast, course, trainingdate): + t = Training() + t.gymnast = gymnast + t.course = course + t.trainingdate = trainingdate + t.save() + return t class Round(Markdownizable): - """Classe représentant les passages des élèves lors d'un entrainement. + """Classe représentant les passages des élèves lors d'un entrainement. - Chaque record représente un passage. Il est donc lié à un record de la classe `Training`. - """ + Chaque record représente un passage. Il est donc lié à un record de la classe `Training`. + """ - class Meta: - verbose_name = "Round" - verbose_name_plural = "Rounds" + class Meta: + verbose_name = "Round" + verbose_name_plural = "Rounds" - EVALUATION_CHOICES = ( - (0, "- -"), - (1, "- +"), - (2, "+ -"), - (3, "+ +"), - ) + EVALUATION_CHOICES = ( + (0, "- -"), + (1, "- +"), + (2, "+ -"), + (3, "+ +"), + ) - training = models.ForeignKey( - Training, on_delete=models.CASCADE, default=None, related_name="rounds" - ) - educative = models.ForeignKey( - "objective.Educative", - on_delete=models.CASCADE, - default=None, - blank=True, - null=True, - ) - round_information = models.CharField(max_length=255, blank=True, null=True) - round_number = models.PositiveSmallIntegerField(blank=True, null=True) - gymnast_evaluation = models.PositiveSmallIntegerField( - choices=EVALUATION_CHOICES, blank=True, null=True - ) - coach_evaluation = models.PositiveSmallIntegerField(blank=True, null=True) - coachid = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL) - nb_of_realisation = models.PositiveSmallIntegerField(blank=True, null=True) - nb_of_success = models.PositiveSmallIntegerField(blank=True, null=True) - is_important = models.BooleanField(default=False) + training = models.ForeignKey( + Training, on_delete=models.CASCADE, default=None, related_name="rounds" + ) + educative = models.ForeignKey( + "objective.Educative", + on_delete=models.CASCADE, + default=None, + blank=True, + null=True, + ) + round_information = models.CharField(max_length=255, blank=True, null=True) + round_number = models.PositiveSmallIntegerField(blank=True, null=True) + gymnast_evaluation = models.PositiveSmallIntegerField( + choices=EVALUATION_CHOICES, blank=True, null=True + ) + coach_evaluation = models.PositiveSmallIntegerField(blank=True, null=True) + coachid = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL) + nb_of_realisation = models.PositiveSmallIntegerField(blank=True, null=True) + nb_of_success = models.PositiveSmallIntegerField(blank=True, null=True) + is_important = models.BooleanField(default=False) - def __str__(self): - return "%s" % (self.round_number) + def __str__(self): + return "%s" % (self.round_number) class PlanningLine(Markdownizable): - """Classe représentant les passages prévisionnels (incubating idea). - """ + """Classe représentant les passages prévisionnels (incubating idea). + """ - class Meta: - verbose_name = "Planning Line" - verbose_name_plural = "Planning lines" - # ordering = ['gymnast', 'date', 'order'] + class Meta: + verbose_name = "Planning Line" + verbose_name_plural = "Planning lines" + # ordering = ['gymnast', 'date', 'order'] - gymnast = models.ForeignKey(Gymnast, on_delete=models.CASCADE, default=None) - date = models.DateField(verbose_name="Date") - order = models.PositiveSmallIntegerField() - todo = models.CharField(max_length=255) + gymnast = models.ForeignKey(Gymnast, on_delete=models.CASCADE, default=None) + date = models.DateField(verbose_name="Date") + order = models.PositiveSmallIntegerField() + todo = models.CharField(max_length=255) diff --git a/src/planning/tests/tests_models.py b/src/planning/tests/tests_models.py index 3416ed2..4c99dee 100644 --- a/src/planning/tests/tests_models.py +++ b/src/planning/tests/tests_models.py @@ -1,7 +1,39 @@ +"""Tests liés au modèle de l'application planning""" -from datetime import datetime +from datetime import datetime, time from django.test import TestCase -from ..models import get_number_of_weeks_between, Season + +from location.models import Club, Place, Country + +from ..models import get_number_of_weeks_between, Season, Course + + +class TestCourse(TestCase): + def setUp(self): + pass + + def test_course_duration(self): + """Vérifie le calcul de durée d'un cours""" + course = Course.objects.create( + iso_day_number=2, + datebegin=datetime(2021, 1, 1), + dateend=datetime(2021, 9, 30), + hour_begin=time(hour=19, minute=30), + hour_end=time(hour=22, minute=45), + club=Club.objects.create( + name="RCW", + place=Place.objects.create( + name="Somewhere", + postal=1080, + country=Country.objects.create( + namefr="Belgique", + isonum=56 + ) + ) + ) + ) + + self.assertEqual(course.duration, 2) class TestUtils(TestCase): -- 2.39.2 From 1a392f4e4240ae47a536c5e6771636b39e92a79c Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 22 Jun 2021 19:20:19 +0200 Subject: [PATCH 04/17] Ignore admin.py files when covering code --- src/.coveragerc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/.coveragerc b/src/.coveragerc index 8d55d25..25657b0 100644 --- a/src/.coveragerc +++ b/src/.coveragerc @@ -5,6 +5,7 @@ omit = */test* */migrations/* */urls.py + */admin.py */settings/* */wsgi.py */__init__.py @@ -12,4 +13,4 @@ omit = source = . [report] -show_missing = true \ No newline at end of file +show_missing = true -- 2.39.2 From c8d543ab50221b89f948727921c6710d6b56049f Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 22 Jun 2021 19:20:35 +0200 Subject: [PATCH 05/17] Remove pytest old tests --- src/location/tests_models.py | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/location/tests_models.py diff --git a/src/location/tests_models.py b/src/location/tests_models.py deleted file mode 100644 index 1c93626..0000000 --- a/src/location/tests_models.py +++ /dev/null @@ -1,22 +0,0 @@ -# coding=UTF-8 - -from .models import ( - Club, - Place, - Country -) -import pytest - -# class GymnastTestCase(): -def test_country_tostring(): - c = Country(namefr="Belgique", iso2="56") - assert str(c) == "Belgique (56)" - -def test_place_tostring(): - p = Place(name="FATC", city="Lillois") - assert str(p) == "FATC (Lillois)" - -def test_club_tostring(): - p = Place(name="FATC", city="Lillois") - club = Club(place=p, name="FATC2") - assert str(club) == "FATC2 (à Lillois)" \ No newline at end of file -- 2.39.2 From 66735a49e33da348cefd1ac2c8862b5883d18a64 Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Wed, 19 May 2021 20:58:25 +0200 Subject: [PATCH 06/17] Code quality * Create a makefile (for *nix only, right?) * Configure code coverage and tests * Configure linter * Configure linter for errors only --- Makefile | 13 +++++++++++++ src/.coveragerc | 15 +++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 Makefile create mode 100644 src/.coveragerc diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..516f730 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +#!/bin/bash + +.PHONY: coverage errors + +coverage: + cd src/; coverage run --source='.' manage.py test; coverage report -m; cd .. + @echo "Testing of coverage in the sources finished." + +lint: + pylint src/* --disable=django-not-configured + +errors: + pylint src/* --errors-only --disable=django-not-configured diff --git a/src/.coveragerc b/src/.coveragerc new file mode 100644 index 0000000..8d55d25 --- /dev/null +++ b/src/.coveragerc @@ -0,0 +1,15 @@ +[run] +branch = true +omit = + */tests/* + */test* + */migrations/* + */urls.py + */settings/* + */wsgi.py + */__init__.py + manage.py +source = . + +[report] +show_missing = true \ No newline at end of file -- 2.39.2 From 00df7cbd0b31703384c0170c6c993f3975a52e57 Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Mon, 21 Jun 2021 21:37:37 +0200 Subject: [PATCH 07/17] Create a new `ClubStatistics` class to store data This will allow to make the location/views.py file more readable --- src/location/views.py | 56 +++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/location/views.py b/src/location/views.py index cd92c35..b01a832 100644 --- a/src/location/views.py +++ b/src/location/views.py @@ -153,6 +153,21 @@ def chooseStatistics(request): return render(request, "club_statistics.html", context) +class GymnastStatistics(): + def __init__(self, gymnast): + self.gymnast = gymnast + self.number_of_courses_by_week = 0 + self.number_of_hours_by_week = timedelta() + self.number_of_trainings = 0 + self.number_of_attendance = 0 + self.number_of_absences = 0 + self.number_of_training_hours = 0 + self.number_of_attendance_hours = timedelta() + self.percentage_of_attendance = 0 + self.number_of_hours_of_absence = 0 + self.percentage_of_absence = 0 + + @login_required def club_statistics(request, clubid): """ @@ -172,6 +187,7 @@ def club_statistics(request, clubid): gymnastsDict = {} gymnasts = [] courseList = [] + for course in courses: nbtrainer = course.trainers.count() list_of_gymnasts = Gymnast.objects.filter(to_gym__in=course.to_subgroup.all()) @@ -215,43 +231,25 @@ def club_statistics(request, clubid): ) for gymnast in list_of_gymnasts: - # print(gymnast) - if gymnast.id not in gymnastsDict: - gymnastsDict[gymnast.id] = { - "gymnast": gymnast, - "nbcoursebyweek": 0, - "nbhourbyweek": timedelta(), - "nbtraining": 0, - "nbattendance": 0, - "nbabsence": 0, - "nbhourtraining": 0, - "nbhourattendance": timedelta(), - "percentageattendance": 0, - "nbhourabsence": 0, - "percentageabsence": 0, - } - attendanceList = Training.objects.filter(course=course, gymnast=gymnast) - nbattendance = len(attendanceList) + gymnast_stats = gymnastsDict.setdefault(gymnast.id, GymnastStatistics(gymnast)) - # print(str(gymnast) + ' : ' + str(nbattendance) + ' for ' + str(course) ) + attendance_list = Training.objects.filter(course=course, gymnast=gymnast) + number_of_attendance = attendance_list.count() - gymnastsDict[gymnast.id]["nbcoursebyweek"] += 1 - gymnastsDict[gymnast.id]["nbhourbyweek"] += nbhour # timedelta - gymnastsDict[gymnast.id]["nbtraining"] += counted - gymnastsDict[gymnast.id]["nbattendance"] += nbattendance - gymnastsDict[gymnast.id]["nbhourtraining"] += totalHourForCourse - gymnastsDict[gymnast.id]["nbhourattendance"] += ( - nbhour * nbattendance - ) # timedelta - - # print(gymnastsDict[gymnast.id]) + gymnast_stats.number_of_courses_by_week += 1 + gymnast_stats.number_of_hours_by_week += nbhour + gymnast_stats.number_of_trainings += counted + gymnast_stats.number_of_attendance += number_of_attendance + gymnast_stats.number_of_training_hours += totalHourForCourse + gymnast_stats.number_of_attendance_hours += ( + nbhour * number_of_attendance + ) # tous les cours ont été traités totalHoursByWeek = totalHoursByWeek / 3600 gymnasts = set(gymnasts) - # print(gymnasts) for gymnast in gymnasts: tmp = int(gymnastsDict[gymnast.id]["nbhourbyweek"].seconds / 3600) -- 2.39.2 From 0bff7e0f6655867a06ac2a2303e7f7288dfcc6c6 Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Tue, 22 Jun 2021 19:15:51 +0200 Subject: [PATCH 08/17] Refactors clubs statistics --- src/khana/views.py | 24 - src/location/views.py | 23 +- src/planning/models.py | 744 +++++++++++++++-------------- src/planning/tests/tests_models.py | 36 +- 4 files changed, 421 insertions(+), 406 deletions(-) diff --git a/src/khana/views.py b/src/khana/views.py index 4c1e062..9e482df 100644 --- a/src/khana/views.py +++ b/src/khana/views.py @@ -81,23 +81,8 @@ def __getEventInfo(request): .. todo:: il refuse mon 'filter' que ce soit avant ou après le 'next(5)'. Une idée ? next_event = Event.objects.filter(club__in=(request.session["clubid"], )).next(5) """ - - # rest = 0 - # counted = 0 - # event_list = [] - # today = pendulum.now().date() next_event_list = Event.objects.next(5) - # for event in next_event: - # counted = event.get_number_of_occurence_to_event(today) - # # print('pouf !') - - # unavailabilities = Unavailability.objects.filter(datebegin__lte=event.datebegin.date(), dateend__gte=today) - # for unavailable in unavailabilities: - # counted -= unavailable.get_total_occurence() - - # event_list.append((event, counted, int((counted/16)*100))) - return next_event_list @@ -130,18 +115,9 @@ def __getCourseInfo(request): else: rest = int((tmp.days + 1) / 7) - # # select tous les unavailables liés au cours - # unavailabilities = Unavailability.objects.filter(course=course) - # for unavailable in unavailabilities: - # tmp = unavailable.get_total_occurence() - # counted -= tmp - # rest -= tmp # si un unavailability.date < today, on soustrait quand même de rest ??? Si oui => BUG !!!! - courses_left += rest courses_done += counted - rest - # courses.append((course, counted, (counted - rest))) - return courses, courses_done, courses_left diff --git a/src/location/views.py b/src/location/views.py index b01a832..5f4ff13 100644 --- a/src/location/views.py +++ b/src/location/views.py @@ -170,10 +170,14 @@ class GymnastStatistics(): @login_required def club_statistics(request, clubid): - """ - Renvoie les statistiques d'un club pour une saison choisie. + """Construit les statistiques d'un club, pour une saison choisie - .. todo:: tenir compte de la saison. + Questions: + Tu dis que cela construit les stats d'un club __pour une saison__... + Mais je ne vois pas la saison dans les paramètres... + + Todo: + * Tenir compte de la saison. """ courses = Course.objects.filter(club__in=clubid).order_by( @@ -189,13 +193,12 @@ def club_statistics(request, clubid): courseList = [] for course in courses: - nbtrainer = course.trainers.count() + number_of_trainers = course.trainers.count() list_of_gymnasts = Gymnast.objects.filter(to_gym__in=course.to_subgroup.all()) gymnasts.extend(list_of_gymnasts) - nbgymnast = len(list_of_gymnasts) - # gymnasts = set(gymnasts.extend(Gymnast.objects.filter(to_gym__in=course.to_subgroup.all()))) + number_of_gymnasts = len(list_of_gymnasts) - nbhour = __diffTime(course.hour_end, course.hour_begin) # timedelta + nbhour = __diffTime(course.hour_end, course.hour_begin) totalHoursByWeek += nbhour.seconds counted = course.get_total_occurence() @@ -211,7 +214,7 @@ def club_statistics(request, clubid): totalTimeForCourse.seconds / 3600 ) totalHours += totalHourForCourse - totalHoursPaidForCourse = totalHourForCourse * nbtrainer + totalHoursPaidForCourse = totalHourForCourse * number_of_trainers totalHoursPaid += totalHoursPaidForCourse # tmp = int(nbhour.seconds/3600) @@ -221,8 +224,8 @@ def club_statistics(request, clubid): courseList.append( ( course, - nbtrainer, - nbgymnast, + number_of_trainers, + number_of_gymnasts, hour, counted, totalHourForCourse, diff --git a/src/planning/models.py b/src/planning/models.py index 393a8f4..68ff806 100644 --- a/src/planning/models.py +++ b/src/planning/models.py @@ -13,496 +13,500 @@ from people.models import Gymnast def get_week(a_date): - """ + """ - Remarks: - Je ne comprends pas trop cette fonction... - Tu pars d'une date, et tu récupères le lundi et le samedi de la semaine correspondant ? - """ - the_date = pendulum.parse(a_date) - day = the_date.weekday() - monday = the_date - timedelta(days=day) - sunday = the_date + timedelta(days=(6 - day)) - return monday, sunday + Remarks: + Je ne comprends pas trop cette fonction... + Tu pars d'une date, et tu récupères le lundi et le samedi de la semaine correspondant ? + """ + the_date = pendulum.parse(a_date) + day = the_date.weekday() + monday = the_date - timedelta(days=day) + sunday = the_date + timedelta(days=(6 - day)) + return monday, sunday def get_number_of_weeks_between(start, stop): - """ - Renvoie le nombre de semaines entre deux dates. - Par extension, cela permet de connaitre le nombre d'occurence d'un - évènement (entraînement, par exemple) hebdromadaire entre deux dates - et ainsi pouvoir plannifier. + """ + Renvoie le nombre de semaines entre deux dates. + Par extension, cela permet de connaitre le nombre d'occurence d'un + évènement (entraînement, par exemple) hebdromadaire entre deux dates + et ainsi pouvoir plannifier. - :param start: date de début de la période - :type start: datetime.date - :param stop: date de fin de la période - :type stop: datetime.date - :return: Le nombre de semaines entre les deux dates. + :param start: date de début de la période + :type start: datetime.date + :param stop: date de fin de la période + :type stop: datetime.date + :return: Le nombre de semaines entre les deux dates. - Remarks: - Proposition d'utiliser isocalendar() sur une date. - L'indice 1 de la valeur de retour donne la semaine correspondant. + Remarks: + Proposition d'utiliser isocalendar() sur une date. + L'indice 1 de la valeur de retour donne la semaine correspondant. - Eg. - >>> from datetime import date - >>> d = date(2020, 9, 27) - >>> d.isocalendar() - (2020, 39, 7) + Eg. + >>> from datetime import date + >>> d = date(2020, 9, 27) + >>> d.isocalendar() + (2020, 39, 7) - -> Est-ce qu'il ne suffirait pas de faire la différence ? - """ + -> Est-ce qu'il ne suffirait pas de faire la différence ? + """ - tmp = stop - start - number_of_days = abs(tmp.days) - number_of_week = int((number_of_days + 1) / 7) + tmp = stop - start + number_of_days = abs(tmp.days) + number_of_week = int((number_of_days + 1) / 7) - if ((number_of_days + 1) % 7) > 0: - number_of_week += 1 + if ((number_of_days + 1) % 7) > 0: + number_of_week += 1 - if tmp.days < 0: - number_of_week *= -1 + if tmp.days < 0: + number_of_week *= -1 - return number_of_week + return number_of_week class TemporizableQuerySet(models.QuerySet): - """ - Classe permettant de spécifier le `QuerySet` de la classe `Temporizable`. - """ + """ + Classe permettant de spécifier le `QuerySet` de la classe `Temporizable`. + """ - def next(self, limit): - """ - Renvoie la liste des prochains "temporizable" (par rapport à la date du jour). + def next(self, limit): + """ + Renvoie la liste des prochains "temporizable" (par rapport à la date du jour). - :param limit: nombre d'éléments désirés. - :type limit: int - :return: une liste de `limit` éléments temporizables. - """ - return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:limit] + :param limit: nombre d'éléments désirés. + :type limit: int + :return: une liste de `limit` éléments temporizables. + """ + return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:limit] - def last(self, limit): - """ - Renvoie la liste des derniers "temporizable" (par rapport à la date du jour). + def last(self, limit): + """ + Renvoie la liste des derniers "temporizable" (par rapport à la date du jour). - :param limit: nombre d'éléments désirés. - :type limit: int - :return: une liste de `limit` éléments temporizables - """ - return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:limit] + :param limit: nombre d'éléments désirés. + :type limit: int + :return: une liste de `limit` éléments temporizables + """ + return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:limit] - # def get(self, date_string): - # """ - # """ - # try: - # selected_object = self.get(datebegin__lte=date_string, dateend__gte=date_string) - # except self.DoesNotExist: - # return None - # except self.MultipleObjectsReturned: - # return None + # def get(self, date_string): + # """ + # """ + # try: + # selected_object = self.get(datebegin__lte=date_string, dateend__gte=date_string) + # except self.DoesNotExist: + # return None + # except self.MultipleObjectsReturned: + # return None - # return selected_object + # return selected_object class Temporizable(models.Model): - """Classe abstraite définissant une période comprise entre deux dates. + """Classe abstraite définissant une période comprise entre deux dates. - """ + """ - class Meta: - abstract = True + class Meta: + abstract = True - datebegin = models.DateTimeField(verbose_name="Début") - dateend = models.DateTimeField(blank=True, verbose_name="Fin") + datebegin = models.DateTimeField(verbose_name="Début") + dateend = models.DateTimeField(blank=True, verbose_name="Fin") - objects = models.Manager.from_queryset(TemporizableQuerySet)() + objects = models.Manager.from_queryset(TemporizableQuerySet)() - def get_total_occurence(self): - """ - Renvoie le nombre de semaines entre les deux dates d'une instance de la - classe `Temporizable`. + def get_total_occurence(self): + """ + Renvoie le nombre de semaines entre les deux dates d'une instance de la + classe `Temporizable`. - :return: nombre de semaines. - """ - return get_number_of_weeks_between(self.datebegin.date(), self.dateend.date()) + :return: nombre de semaines. + """ + return get_number_of_weeks_between(self.datebegin.date(), self.dateend.date()) - def get_number_of_occurence_to_event(self, the_date): - """ - Renvoie le nombre semaines entre une date choisie et le début - (datebegin) d'une instance de la classe `Temporizable`. + def get_number_of_occurence_to_event(self, the_date): + """ + Renvoie le nombre semaines entre une date choisie et le début + (datebegin) d'une instance de la classe `Temporizable`. - :param the_date: date par rapport à laquelle le calcul sera fait. - :type the_date: datetime.date - :return: nombre de semaines. - """ - return get_number_of_weeks_between(the_date, self.datebegin.date()) + :param the_date: date par rapport à laquelle le calcul sera fait. + :type the_date: datetime.date + :return: nombre de semaines. + """ + return get_number_of_weeks_between(the_date, self.datebegin.date()) - def get_number_of_occurence_inbetween(self, the_date, rest=True): - """ - Renvoie le nombre semaines entre une date choisie et une instance de la - classe `Temporizable`. Le calcul peut se faire soit entre la date - choisie et le date de fin d'une occurence de la classe, soit entre la - date de début d'une occurence de la classe et la date choisie. + def get_number_of_occurence_inbetween(self, the_date, rest=True): + """ + Renvoie le nombre semaines entre une date choisie et une instance de la + classe `Temporizable`. Le calcul peut se faire soit entre la date + choisie et le date de fin d'une occurence de la classe, soit entre la + date de début d'une occurence de la classe et la date choisie. - :param the_date: date par rapport à laquelle le calcul sera fait. - :type the_date: datetime.date - :param rest: paramètre définissant s'il faut calculer le reste des - occurences à venir (depuis `the_date` jusqu'à la date de fin) ou - les occurences déjà passées (depuis la date de début jusqu'à - `the_date`) - :type rest: booléen - :return: nombre de semaines. - """ - if rest: - return get_number_of_weeks_between(the_date, self.dateend.date()) - else: - return get_number_of_weeks_between(self.datebegin.date(), the_date) + :param the_date: date par rapport à laquelle le calcul sera fait. + :type the_date: datetime.date + :param rest: paramètre définissant s'il faut calculer le reste des + occurences à venir (depuis `the_date` jusqu'à la date de fin) ou + les occurences déjà passées (depuis la date de début jusqu'à + `the_date`) + :type rest: booléen + :return: nombre de semaines. + """ + if rest: + return get_number_of_weeks_between(the_date, self.dateend.date()) + else: + return get_number_of_weeks_between(self.datebegin.date(), the_date) class Season(Temporizable): - """ - Classe représentant une saison. Une saison est déinie par : - - un id, - - un label, - - une date de début et - - une date de fin. + """ + Classe représentant une saison. Une saison est déinie par : + - un id, + - un label, + - une date de début et + - une date de fin. - La date de début est très souvent le : 01/09/xxxx - La date de fin est très souvent le : 31/08/xxxy - exemple : 1/9/2015 - 31/8/2016 - """ + La date de début est très souvent le : 01/09/xxxx + La date de fin est très souvent le : 31/08/xxxy + exemple : 1/9/2015 - 31/8/2016 + """ - class Meta: - verbose_name = "Season" - verbose_name_plural = "Seasons" + class Meta: + verbose_name = "Season" + verbose_name_plural = "Seasons" - label = models.CharField(max_length=11, verbose_name="Label") - # active ou default = models.BooleanField(verbose_name='Défaut') + label = models.CharField(max_length=11, verbose_name="Label") + # active ou default = models.BooleanField(verbose_name='Défaut') - def __str__(self): - return "%s" % (self.label) + def __str__(self): + return "%s" % (self.label) - def week_number_from_begin(self, target_date): - return get_number_of_weeks_between(self.datebegin.date(), target_date) + def week_number_from_begin(self, target_date): + return get_number_of_weeks_between(self.datebegin.date(), target_date) class EventType(models.Model): - """ - Classe représentant les types d'évènements. - C'est un dictionnaire fini : - - compétiton qualificative, - - compétition finale, - - démonstration, - - … - """ + """ + Classe représentant les types d'évènements. + C'est un dictionnaire fini : + - compétiton qualificative, + - compétition finale, + - démonstration, + - … + """ - class Meta: - verbose_name = "Event Type" - verbose_name_plural = "Event Types" + class Meta: + verbose_name = "Event Type" + verbose_name_plural = "Event Types" - name = models.CharField(max_length=255, verbose_name="Nom") - acronym = models.CharField(max_length=5, verbose_name="Acronyme") + name = models.CharField(max_length=255, verbose_name="Nom") + acronym = models.CharField(max_length=5, verbose_name="Acronyme") - def __str__(self): - return "%s (%s)" % (self.name, self.acronym) + def __str__(self): + return "%s (%s)" % (self.name, self.acronym) class Event(Markdownizable, Temporizable): - """Classe représentant les évènements. + """Classe représentant les évènements. - Un évènement est caractérisé par : - * un nom, - * un lieu (place), - * un type (compétition, démonstration, …), - * des gymnastes (participation prévue). - Je ne me rapelle plus à quoi sert le club. - """ + Un évènement est caractérisé par : + * un nom, + * un lieu (place), + * un type (compétition, démonstration, …), + * des gymnastes (participation prévue). + Je ne me rapelle plus à quoi sert le club. + """ - class Meta: - verbose_name = "Event" - verbose_name_plural = "Event" + class Meta: + verbose_name = "Event" + verbose_name_plural = "Event" - place = models.ForeignKey( - "location.Place", verbose_name="Lieu", on_delete=models.CASCADE, default=None - ) - eventtype = models.ForeignKey( - EventType, verbose_name="Type", on_delete=models.CASCADE, default=None - ) - name = models.CharField(max_length=255, verbose_name="Nom") - # club = models.ManyToManyField('location.Club', related_name="concernate_by", blank=True) - gymnasts = models.ManyToManyField( - "people.Gymnast", - through="Event_Participation", - related_name="participate_to", - verbose_name="Participants", - ) + place = models.ForeignKey( + "location.Place", verbose_name="Lieu", on_delete=models.CASCADE, default=None + ) + eventtype = models.ForeignKey( + EventType, verbose_name="Type", on_delete=models.CASCADE, default=None + ) + name = models.CharField(max_length=255, verbose_name="Nom") + # club = models.ManyToManyField('location.Club', related_name="concernate_by", blank=True) + gymnasts = models.ManyToManyField( + "people.Gymnast", + through="Event_Participation", + related_name="participate_to", + verbose_name="Participants", + ) - def __str__(self): - return "%s (à %s)" % (self.name, self.place.city) + def __str__(self): + return "%s (à %s)" % (self.name, self.place.city) - def save(self, *args, **kwargs): - self.checkdates() - super().save(*args, **kwargs) + def save(self, *args, **kwargs): + self.checkdates() + super().save(*args, **kwargs) - def checkdates(self): - """ - Fonction assignant la date de fin d'un évènement à la date de début, si la date - de fin n'est pas définie, l'heure de fin est par défaut 18h00. - """ - if self.dateend is None and self.datebegin is not None: - self.dateend = datetime.combine(self.datebegin.date(), time(18, 0)) + def checkdates(self): + """ + Fonction assignant la date de fin d'un évènement à la date de début, si la date + de fin n'est pas définie, l'heure de fin est par défaut 18h00. + """ + if self.dateend is None and self.datebegin is not None: + self.dateend = datetime.combine(self.datebegin.date(), time(18, 0)) - @property - def number_of_week_from_today(self): - today = pendulum.now().date() - return get_number_of_weeks_between(today, self.datebegin.date()) + @property + def number_of_week_from_today(self): + today = pendulum.now().date() + return get_number_of_weeks_between(today, self.datebegin.date()) class Event_Participation(models.Model): - """ - """ + """ + """ - event = models.ForeignKey(Event, on_delete=models.CASCADE) - gymnast = models.ForeignKey("people.Gymnast", on_delete=models.CASCADE) - rank = models.PositiveSmallIntegerField(default=0) + event = models.ForeignKey(Event, on_delete=models.CASCADE) + gymnast = models.ForeignKey("people.Gymnast", on_delete=models.CASCADE) + rank = models.PositiveSmallIntegerField(default=0) class Course(Markdownizable, Temporizable): - """Classe représentant les cours. + """Classe représentant les cours. - Un cours est défini par : - * une heure de début et une heure de fin, - * une date de début et une date de fin (un cours est considéré comme donné hebdromadairement entre - ces deux dates) (hérite de la classe `Temporizable`) - * est associé à un ou plusieurs entraineurs, - * est associé à un club - * est associé à un jour de la semaine (numéro du jour dans la semaine : 0 = lundi, 6 = dimanche). - """ + Un cours est défini par : + * une heure de début et une heure de fin, + * une date de début et une date de fin (un cours est considéré comme donné hebdromadairement entre + ces deux dates) (hérite de la classe `Temporizable`) + * est associé à un ou plusieurs entraineurs, + * est associé à un club + * est associé à un jour de la semaine (numéro du jour dans la semaine : 0 = lundi, 6 = dimanche). + """ - class Meta: - verbose_name = "Course" - verbose_name_plural = "Courses" + class Meta: + verbose_name = "Cours" + verbose_name_plural = "Cours" - DAY_CHOICE = ( - (1, "Lundi"), - (2, "Mardi"), - (3, "Mercredi"), - (4, "Jeudi"), - (5, "Vendredi"), - (6, "Samedi"), - (7, "Dimanche"), - ) + DAY_CHOICE = ( + (1, "Lundi"), + (2, "Mardi"), + (3, "Mercredi"), + (4, "Jeudi"), + (5, "Vendredi"), + (6, "Samedi"), + (7, "Dimanche"), + ) - club = models.ForeignKey( - "location.Club", verbose_name="Club", on_delete=models.CASCADE, default=None - ) - iso_day_number = models.PositiveSmallIntegerField( - choices=DAY_CHOICE, verbose_name="Jour" - ) - hour_begin = models.TimeField(verbose_name="Heure de début") - hour_end = models.TimeField(verbose_name="Heure de fin") - season = models.ForeignKey(Season, on_delete=models.SET_NULL, null=True) - trainers = models.ManyToManyField( - User, verbose_name="Coach(es)", related_name="trainee" - ) - gymnasts = models.ManyToManyField( - Gymnast, verbose_name="Gymnasts", related_name="courses" - ) + club = models.ForeignKey( + "location.Club", verbose_name="Club", on_delete=models.CASCADE, default=None + ) + iso_day_number = models.PositiveSmallIntegerField( + choices=DAY_CHOICE, verbose_name="Jour" + ) + hour_begin = models.TimeField(verbose_name="Heure de début") + hour_end = models.TimeField(verbose_name="Heure de fin") + season = models.ForeignKey(Season, on_delete=models.SET_NULL, null=True) + trainers = models.ManyToManyField( + User, verbose_name="Coach(es)", related_name="trainee" + ) + gymnasts = models.ManyToManyField( + Gymnast, verbose_name="Gymnasts", related_name="courses" + ) - def __str__(self): - return "%s (%s à %s)" % ( - self.get_iso_day_number_display(), - self.hour_begin.strftime("%H:%M"), - self.hour_end.strftime("%H:%M"), - ) + def __str__(self): + return "%s (%s à %s)" % ( + self.get_iso_day_number_display(), + self.hour_begin.strftime("%H:%M"), + self.hour_end.strftime("%H:%M"), + ) - @property - def duration(self): - """ - Renvoie la durée d'un cours en heures - """ - date_begin = pendulum.datetime( - 2000, 1, 1, self.hour_begin.hour, self.hour_begin.minute - ) - date_end = pendulum.datetime( - 2000, 1, 1, self.hour_end.hour, self.hour_end.minute - ) - return date_end.diff(date_begin).in_hours() + @property + def duration(self): + """Renvoie la durée d'un cours en heures + + Examples: + >>> course = Course(hour_begin=20, hour_end=22) + >>> course.duration + 2 + """ + date_begin = pendulum.datetime( + 2000, 1, 1, self.hour_begin.hour, self.hour_begin.minute + ) + date_end = pendulum.datetime( + 2000, 1, 1, self.hour_end.hour, self.hour_end.minute + ) + return date_end.diff(date_begin).in_hours() class Group(models.Model): - """Classe représentant les groupes (Loisir, D1, D2, A, B, …). + """Classe représentant les groupes (Loisir, D1, D2, A, B, …). - Un groupe appartient à un club. - """ + Un groupe appartient à un club. + """ - class Meta: - verbose_name = "Group" - verbose_name_plural = "Groups" + class Meta: + verbose_name = "Group" + verbose_name_plural = "Groups" - club = models.ForeignKey("location.Club", on_delete=models.CASCADE, default=None) - name = models.CharField(max_length=255) - acronym = models.CharField(max_length=50) - active = models.BooleanField(default=1) - season = models.CharField( - max_length=9, - default=str(timezone.now().year) + "-" + str(timezone.now().year + 1), - ) + club = models.ForeignKey("location.Club", on_delete=models.CASCADE, default=None) + name = models.CharField(max_length=255) + acronym = models.CharField(max_length=50) + active = models.BooleanField(default=1) + season = models.CharField( + max_length=9, + default=str(timezone.now().year) + "-" + str(timezone.now().year + 1), + ) - def __str__(self): - return "%s (%s)" % (self.name, self.acronym) + def __str__(self): + return "%s (%s)" % (self.name, self.acronym) class Subgroup(models.Model): - """Classe représentant les sous-groupes. + """Classe représentant les sous-groupes. - Un sous-groupe appartient à un groupe (lui-même lié à un club). + Un sous-groupe appartient à un groupe (lui-même lié à un club). - De cette manière, quand un gymnaste est mis dans un sous-groupe, en remontant via le groupe, - nous pouvons connaître le(s) club(s) du gymnaste pour chaque saison. - """ + De cette manière, quand un gymnaste est mis dans un sous-groupe, en remontant via le groupe, + nous pouvons connaître le(s) club(s) du gymnaste pour chaque saison. + """ - class Meta: - verbose_name = "Subgroup" - verbose_name_plural = "Subgroups" + class Meta: + verbose_name = "Subgroup" + verbose_name_plural = "Subgroups" - name = models.CharField(max_length=255) - acronym = models.CharField(max_length=50) - group = models.ForeignKey(Group, on_delete=models.CASCADE, default=None) - courses = models.ManyToManyField(Course, related_name="to_subgroup") - gymnasts = models.ManyToManyField( - "people.Gymnast", related_name="to_gym", blank=True - ) - active = models.BooleanField(default=1) + name = models.CharField(max_length=255) + acronym = models.CharField(max_length=50) + group = models.ForeignKey(Group, on_delete=models.CASCADE, default=None) + courses = models.ManyToManyField(Course, related_name="to_subgroup") + gymnasts = models.ManyToManyField( + "people.Gymnast", related_name="to_gym", blank=True + ) + active = models.BooleanField(default=1) - def __str__(self): - return "%s (%s)" % (self.name, self.group.name) + def __str__(self): + return "%s (%s)" % (self.name, self.group.name) class UnavailabilityManager(models.Manager): - """Classe représentant le manager de la classe `Unavailability`. - """ + """Classe représentant le manager de la classe `Unavailability`. + """ - def next(self, count): - return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:count] + def next(self, count): + return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:count] - def last(self, count): - return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:count] + def last(self, count): + return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:count] class Unavailability(Markdownizable, Temporizable): - """Classe représentant les indisponibilités. - """ + """Classe représentant les indisponibilités. + """ - class Meta: - verbose_name = "Indisponibilité" - verbose_name_plural = "Indisponibilités" + class Meta: + verbose_name = "Indisponibilité" + verbose_name_plural = "Indisponibilités" - course = models.ManyToManyField(Course, related_name="unavailability") + course = models.ManyToManyField(Course, related_name="unavailability") - objects = UnavailabilityManager() + objects = UnavailabilityManager() - def __str__(self): - return "du %s au %s" % (self.datebegin, self.dateend) + def __str__(self): + return "du %s au %s" % (self.datebegin, self.dateend) - def save(self, *args, **kwargs): - self.checkdates() + def save(self, *args, **kwargs): + self.checkdates() - super().save(*args, **kwargs) + super().save(*args, **kwargs) - def checkdates(self): - if self.dateend is None and self.datebegin is not None: - self.dateend = self.datebegin + def checkdates(self): + if self.dateend is None and self.datebegin is not None: + self.dateend = self.datebegin class Training(models.Model): - """Classe représentant les entraînements. + """Classe représentant les entraînements. - Un entraînement est une occurence d'un cours pendant lequel des gmnastes sont présents. + Un entraînement est une occurence d'un cours pendant lequel des gmnastes sont présents. - Un objet de cette classe lie donc un cours et un gymnaste à une date donnée. - """ + Un objet de cette classe lie donc un cours et un gymnaste à une date donnée. + """ - class Meta: - verbose_name = "Training" - verbose_name_plural = "Trainings" + class Meta: + verbose_name = "Training" + verbose_name_plural = "Trainings" - gymnast = models.ForeignKey( - "people.Gymnast", - verbose_name="Gymnast", - on_delete=models.CASCADE, - default=None, - related_name="trainings", - ) - course = models.ForeignKey( - Course, verbose_name="Course", on_delete=models.CASCADE, default=None - ) - trainingdate = models.DateField(verbose_name="Date") + gymnast = models.ForeignKey( + "people.Gymnast", + verbose_name="Gymnast", + on_delete=models.CASCADE, + default=None, + related_name="trainings", + ) + course = models.ForeignKey( + Course, verbose_name="Course", on_delete=models.CASCADE, default=None + ) + trainingdate = models.DateField(verbose_name="Date") - def __str__(self): - return "%s - %s, %s" % (self.trainingdate, self.course, self.gymnast) + def __str__(self): + return "%s - %s, %s" % (self.trainingdate, self.course, self.gymnast) - @staticmethod - def create(gymnast, course, trainingdate): - t = Training() - t.gymnast = gymnast - t.course = course - t.trainingdate = trainingdate - t.save() - return t + @staticmethod + def create(gymnast, course, trainingdate): + t = Training() + t.gymnast = gymnast + t.course = course + t.trainingdate = trainingdate + t.save() + return t class Round(Markdownizable): - """Classe représentant les passages des élèves lors d'un entrainement. + """Classe représentant les passages des élèves lors d'un entrainement. - Chaque record représente un passage. Il est donc lié à un record de la classe `Training`. - """ + Chaque record représente un passage. Il est donc lié à un record de la classe `Training`. + """ - class Meta: - verbose_name = "Round" - verbose_name_plural = "Rounds" + class Meta: + verbose_name = "Round" + verbose_name_plural = "Rounds" - EVALUATION_CHOICES = ( - (0, "- -"), - (1, "- +"), - (2, "+ -"), - (3, "+ +"), - ) + EVALUATION_CHOICES = ( + (0, "- -"), + (1, "- +"), + (2, "+ -"), + (3, "+ +"), + ) - training = models.ForeignKey( - Training, on_delete=models.CASCADE, default=None, related_name="rounds" - ) - educative = models.ForeignKey( - "objective.Educative", - on_delete=models.CASCADE, - default=None, - blank=True, - null=True, - ) - round_information = models.CharField(max_length=255, blank=True, null=True) - round_number = models.PositiveSmallIntegerField(blank=True, null=True) - gymnast_evaluation = models.PositiveSmallIntegerField( - choices=EVALUATION_CHOICES, blank=True, null=True - ) - coach_evaluation = models.PositiveSmallIntegerField(blank=True, null=True) - coachid = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL) - nb_of_realisation = models.PositiveSmallIntegerField(blank=True, null=True) - nb_of_success = models.PositiveSmallIntegerField(blank=True, null=True) - is_important = models.BooleanField(default=False) + training = models.ForeignKey( + Training, on_delete=models.CASCADE, default=None, related_name="rounds" + ) + educative = models.ForeignKey( + "objective.Educative", + on_delete=models.CASCADE, + default=None, + blank=True, + null=True, + ) + round_information = models.CharField(max_length=255, blank=True, null=True) + round_number = models.PositiveSmallIntegerField(blank=True, null=True) + gymnast_evaluation = models.PositiveSmallIntegerField( + choices=EVALUATION_CHOICES, blank=True, null=True + ) + coach_evaluation = models.PositiveSmallIntegerField(blank=True, null=True) + coachid = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL) + nb_of_realisation = models.PositiveSmallIntegerField(blank=True, null=True) + nb_of_success = models.PositiveSmallIntegerField(blank=True, null=True) + is_important = models.BooleanField(default=False) - def __str__(self): - return "%s" % (self.round_number) + def __str__(self): + return "%s" % (self.round_number) class PlanningLine(Markdownizable): - """Classe représentant les passages prévisionnels (incubating idea). - """ + """Classe représentant les passages prévisionnels (incubating idea). + """ - class Meta: - verbose_name = "Planning Line" - verbose_name_plural = "Planning lines" - # ordering = ['gymnast', 'date', 'order'] + class Meta: + verbose_name = "Planning Line" + verbose_name_plural = "Planning lines" + # ordering = ['gymnast', 'date', 'order'] - gymnast = models.ForeignKey(Gymnast, on_delete=models.CASCADE, default=None) - date = models.DateField(verbose_name="Date") - order = models.PositiveSmallIntegerField() - todo = models.CharField(max_length=255) + gymnast = models.ForeignKey(Gymnast, on_delete=models.CASCADE, default=None) + date = models.DateField(verbose_name="Date") + order = models.PositiveSmallIntegerField() + todo = models.CharField(max_length=255) diff --git a/src/planning/tests/tests_models.py b/src/planning/tests/tests_models.py index 3416ed2..4c99dee 100644 --- a/src/planning/tests/tests_models.py +++ b/src/planning/tests/tests_models.py @@ -1,7 +1,39 @@ +"""Tests liés au modèle de l'application planning""" -from datetime import datetime +from datetime import datetime, time from django.test import TestCase -from ..models import get_number_of_weeks_between, Season + +from location.models import Club, Place, Country + +from ..models import get_number_of_weeks_between, Season, Course + + +class TestCourse(TestCase): + def setUp(self): + pass + + def test_course_duration(self): + """Vérifie le calcul de durée d'un cours""" + course = Course.objects.create( + iso_day_number=2, + datebegin=datetime(2021, 1, 1), + dateend=datetime(2021, 9, 30), + hour_begin=time(hour=19, minute=30), + hour_end=time(hour=22, minute=45), + club=Club.objects.create( + name="RCW", + place=Place.objects.create( + name="Somewhere", + postal=1080, + country=Country.objects.create( + namefr="Belgique", + isonum=56 + ) + ) + ) + ) + + self.assertEqual(course.duration, 2) class TestUtils(TestCase): -- 2.39.2 From b26eab154ed1043b5ad2af0bceed7e47f30c01a3 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 22 Jun 2021 19:20:19 +0200 Subject: [PATCH 09/17] Ignore admin.py files when covering code --- src/.coveragerc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/.coveragerc b/src/.coveragerc index 8d55d25..25657b0 100644 --- a/src/.coveragerc +++ b/src/.coveragerc @@ -5,6 +5,7 @@ omit = */test* */migrations/* */urls.py + */admin.py */settings/* */wsgi.py */__init__.py @@ -12,4 +13,4 @@ omit = source = . [report] -show_missing = true \ No newline at end of file +show_missing = true -- 2.39.2 From 2a12177f745b80fcc789f4ff7a1f2698ccce1190 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 22 Jun 2021 19:20:35 +0200 Subject: [PATCH 10/17] Remove pytest old tests --- src/location/tests_models.py | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/location/tests_models.py diff --git a/src/location/tests_models.py b/src/location/tests_models.py deleted file mode 100644 index 1c93626..0000000 --- a/src/location/tests_models.py +++ /dev/null @@ -1,22 +0,0 @@ -# coding=UTF-8 - -from .models import ( - Club, - Place, - Country -) -import pytest - -# class GymnastTestCase(): -def test_country_tostring(): - c = Country(namefr="Belgique", iso2="56") - assert str(c) == "Belgique (56)" - -def test_place_tostring(): - p = Place(name="FATC", city="Lillois") - assert str(p) == "FATC (Lillois)" - -def test_club_tostring(): - p = Place(name="FATC", city="Lillois") - club = Club(place=p, name="FATC2") - assert str(club) == "FATC2 (à Lillois)" \ No newline at end of file -- 2.39.2 From d98acb324319dbe180aba87f88ec34587ec08cad Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 22 Jun 2021 20:59:05 +0200 Subject: [PATCH 11/17] [WIP] Refactoring Club & Course for statistics purpose --- src/location/models.py | 55 +++++++++++++++++++++--- src/location/views.py | 56 +++++------------------- src/planning/models.py | 69 ++++++++++++++++++++++++------ src/planning/tests/tests_models.py | 61 ++++++++++++++++++++------ src/planning/tests/tests_utils.py | 14 ++++++ src/planning/utils.py | 24 +++++++++++ 6 files changed, 204 insertions(+), 75 deletions(-) create mode 100644 src/planning/tests/tests_utils.py create mode 100644 src/planning/utils.py diff --git a/src/location/models.py b/src/location/models.py index a475259..8fdae83 100644 --- a/src/location/models.py +++ b/src/location/models.py @@ -51,12 +51,33 @@ class Place(models.Model): return "%s (%s)" % (self.name, self.city if self.city else "?") -class Club(models.Model): - """ - Représente un club. Un club est associé à un lieu. Pour faciliter les filtres, - un club peut être actif ou non. - .. todo:: Un club peut avoir plusieurs salle et une salle peut-être louée par plusieurs clubs... M2M ? +class GymnastStatistics(): + def __init__(self, gymnast): + self.gymnast = gymnast + self.number_of_courses_by_week = 0 + self.number_of_hours_by_week = timedelta() + self.number_of_trainings = 0 + self.number_of_attendance = 0 + self.number_of_absences = 0 + self.number_of_training_hours = 0 + self.number_of_attendance_hours = timedelta() + self.percentage_of_attendance = 0 + self.number_of_hours_of_absence = 0 + self.percentage_of_absence = 0 + + +class Club(models.Model): + """Représentation d'un club. + + Chaque club est associé à un lieu. + + Remarks: + Pour faciliter les filtres, un club peut être actif ou non. + + Todo: + * Un club peut avoir plusieurs salle + * Une salle peut-être louée par plusieurs clubs... M2M ? """ class Meta: @@ -73,3 +94,27 @@ class Club(models.Model): def __str__(self): return "%s (à %s)" % (self.name, self.place.city if self.place.city else "?") + + @property + def get_number_of_real_occurrences(self): + """Retourne le nombre de fois où un cours associé au club a été donné. + """ + courses = Course.objects.filter(club=self).order_by( + "iso_day_number", "hour_begin" + ) + + return sum([x.get_number_of_real_occurrences for x in courses]) + + @property + def get_gymnasts(self): + gymnasts = [] + + courses = Course.objects.filter(club=self).order_by( + "iso_day_number", "hour_begin" + ) + + for course in self.courses: + gymnasts.extend(Gymnast.objects.filter(to_gym__in=course.to_subgroup.all())) + + def build_statistics(self): + pass diff --git a/src/location/views.py b/src/location/views.py index 5f4ff13..4a31cbe 100644 --- a/src/location/views.py +++ b/src/location/views.py @@ -24,23 +24,14 @@ from planning.models import ( from people.models import Gymnast, Accident # people model from .models import ( Club, - Place, Country, + GymnastStatistics, + Place, ) from .forms import PlaceForm from objective.models import Skill, Routine # objective model -def __diffTime(end, start): - """ - Prend deux `datetime.time` en paramètre et calcul la différence entre les deux. - """ - startdate = datetime(2000, 1, 1, start.hour, start.minute) - enddate = datetime(2000, 1, 1, end.hour, end.minute) - - return enddate - startdate - - @login_required @require_http_methods(["GET"]) def place_lookup(request): @@ -153,21 +144,6 @@ def chooseStatistics(request): return render(request, "club_statistics.html", context) -class GymnastStatistics(): - def __init__(self, gymnast): - self.gymnast = gymnast - self.number_of_courses_by_week = 0 - self.number_of_hours_by_week = timedelta() - self.number_of_trainings = 0 - self.number_of_attendance = 0 - self.number_of_absences = 0 - self.number_of_training_hours = 0 - self.number_of_attendance_hours = timedelta() - self.percentage_of_attendance = 0 - self.number_of_hours_of_absence = 0 - self.percentage_of_absence = 0 - - @login_required def club_statistics(request, clubid): """Construit les statistiques d'un club, pour une saison choisie @@ -179,13 +155,12 @@ def club_statistics(request, clubid): Todo: * Tenir compte de la saison. """ - + club = Club.objects.get(pk=clubid) courses = Course.objects.filter(club__in=clubid).order_by( "iso_day_number", "hour_begin" ) - totalHours = 0 - totalCourses = 0 + total_hours = 0 totalHoursByWeek = 0 totalHoursPaid = 0 gymnastsDict = {} @@ -193,33 +168,24 @@ def club_statistics(request, clubid): courseList = [] for course in courses: - number_of_trainers = course.trainers.count() list_of_gymnasts = Gymnast.objects.filter(to_gym__in=course.to_subgroup.all()) gymnasts.extend(list_of_gymnasts) number_of_gymnasts = len(list_of_gymnasts) - nbhour = __diffTime(course.hour_end, course.hour_begin) - totalHoursByWeek += nbhour.seconds + number_of_course_hours = course.number_of_hours + totalHoursByWeek += number_of_course_hours.seconds - counted = course.get_total_occurence() + counted = course.get_number_of_real_occurrences - # select tous les unavailables liés au cours - unavailabilities = Unavailability.objects.filter(course=course) - for unavailable in unavailabilities: - counted -= unavailable.get_total_occurence() - - totalCourses += counted - totalTimeForCourse = nbhour * counted # timedelta + totalTimeForCourse = number_of_course_hours * counted # timedelta totalHourForCourse = (totalTimeForCourse.days * 24) + ( totalTimeForCourse.seconds / 3600 ) totalHours += totalHourForCourse - totalHoursPaidForCourse = totalHourForCourse * number_of_trainers + totalHoursPaidForCourse = totalHourForCourse * course.number_of_trainers totalHoursPaid += totalHoursPaidForCourse - # tmp = int(nbhour.seconds/3600) - # hour = "%d:%02d" % (tmp, (nbhour.seconds - (tmp * 3600)) / 60) - hour = nbhour.seconds / 3600 + hour = number_of_course_hours.seconds / 3600 courseList.append( ( @@ -295,7 +261,7 @@ def club_statistics(request, clubid): "courses": courseList, "gymnasts": gymnastsDict, "totalHoursByWeek": totalHoursByWeek, - "totalCourses": totalCourses, + "totalCourses": club.get_number_of_real_occurrences, "totalHours": totalHours, "totalHoursPaid": totalHoursPaid, } diff --git a/src/planning/models.py b/src/planning/models.py index 68ff806..9f33453 100644 --- a/src/planning/models.py +++ b/src/planning/models.py @@ -11,6 +11,8 @@ from base.models import Markdownizable from location.models import Club from people.models import Gymnast +from .utils import time_diff + def get_week(a_date): """ @@ -270,12 +272,14 @@ class Course(Markdownizable, Temporizable): """Classe représentant les cours. Un cours est défini par : - * une heure de début et une heure de fin, - * une date de début et une date de fin (un cours est considéré comme donné hebdromadairement entre - ces deux dates) (hérite de la classe `Temporizable`) - * est associé à un ou plusieurs entraineurs, - * est associé à un club - * est associé à un jour de la semaine (numéro du jour dans la semaine : 0 = lundi, 6 = dimanche). + * une heure de début et une heure de fin, + * une date de début et une date de fin + + Il est considéré comme donné hebdomadairement entre deux dates + + * est associé à un ou plusieurs entraineurs, + * est associé à un club + * est associé à un jour de la semaine (numéro du jour dans la semaine : 0 = lundi, 6 = dimanche). """ class Meta: @@ -300,6 +304,7 @@ class Course(Markdownizable, Temporizable): ) hour_begin = models.TimeField(verbose_name="Heure de début") hour_end = models.TimeField(verbose_name="Heure de fin") + season = models.ForeignKey(Season, on_delete=models.SET_NULL, null=True) trainers = models.ManyToManyField( User, verbose_name="Coach(es)", related_name="trainee" @@ -308,12 +313,45 @@ class Course(Markdownizable, Temporizable): Gymnast, verbose_name="Gymnasts", related_name="courses" ) - def __str__(self): - return "%s (%s à %s)" % ( - self.get_iso_day_number_display(), - self.hour_begin.strftime("%H:%M"), - self.hour_end.strftime("%H:%M"), - ) + @property + def number_of_trainers(self): + """Retourne le nombre d'entraineurs liés à ce cours""" + return self.trainers.count() + + @property + def number_of_hours(self) -> timedelta: + """Retourne le temps que dure le cours + + Returns: + (datetime.timedelta) La durée du cours + """ + return time_diff(self.hour_begin, self.hour_end) + + @property + def get_number_of_planned_occurrences(self): + return self.get_total_occurence() + + @property + def get_number_of_real_occurrences(self) -> int: + """Retourne le nombre de fois où le cours a réellement été donné. + + Ce résultat est calculé entre le nombre de fois où le cours était prévu, + auquel nous avons retiré les indisponibilités + + Examples: + >>> + """ + counted = course.get_total_occurence() + + unavailabilities = Unavailability.objects.filter(course=course) + for unavailable in unavailabilities: + counted -= unavailable.get_total_occurence() + + return counted + + @property + def get_gymnasts(self): + return Gymnast.objects.filter(to_gym__in=self.to_subgroup.all()) @property def duration(self): @@ -332,6 +370,13 @@ class Course(Markdownizable, Temporizable): ) return date_end.diff(date_begin).in_hours() + def __str__(self): + return "%s (%s à %s)" % ( + self.get_iso_day_number_display(), + self.hour_begin.strftime("%H:%M"), + self.hour_end.strftime("%H:%M"), + ) + class Group(models.Model): """Classe représentant les groupes (Loisir, D1, D2, A, B, …). diff --git a/src/planning/tests/tests_models.py b/src/planning/tests/tests_models.py index 4c99dee..eeffa9d 100644 --- a/src/planning/tests/tests_models.py +++ b/src/planning/tests/tests_models.py @@ -1,6 +1,7 @@ """Tests liés au modèle de l'application planning""" -from datetime import datetime, time +from datetime import datetime, time, timedelta +from django.contrib.auth import get_user_model from django.test import TestCase from location.models import Club, Place, Country @@ -8,9 +9,53 @@ from location.models import Club, Place, Country from ..models import get_number_of_weeks_between, Season, Course +USER_MODEL = get_user_model() + + class TestCourse(TestCase): def setUp(self): - pass + self.club = Club.objects.create( + name="RCW", + place=Place.objects.create( + name="Somewhere", + postal=1080, + country=Country.objects.create( + namefr="Belgique", + isonum=56 + ) + ) + ) + + self.user1 = USER_MODEL.objects.create(username="james_bond") + self.user2 = USER_MODEL.objects.create(username="doctor_no") + + def test_number_of_trainers_should_be_calculated(self): + course = Course.objects.create( + iso_day_number=2, + datebegin=datetime(2021, 1, 1), + dateend=datetime(2021, 9, 30), + hour_begin=time(hour=19, minute=30), + hour_end=time(hour=22, minute=45), + club=self.club + ) + + course.trainers.add(self.user1) + course.trainers.add(self.user2) + + self.assertEqual(course.number_of_trainers, 2) + + def test_number_of_course_hours(self): + course = Course.objects.create( + iso_day_number=2, + datebegin=datetime(2021, 1, 1), + dateend=datetime(2021, 9, 30), + hour_begin=time(hour=19, minute=30), + hour_end=time(hour=22, minute=45), + club=self.club + ) + + self.assertEqual(course.number_of_hours, timedelta(seconds=11700)) + def test_course_duration(self): """Vérifie le calcul de durée d'un cours""" @@ -20,17 +65,7 @@ class TestCourse(TestCase): dateend=datetime(2021, 9, 30), hour_begin=time(hour=19, minute=30), hour_end=time(hour=22, minute=45), - club=Club.objects.create( - name="RCW", - place=Place.objects.create( - name="Somewhere", - postal=1080, - country=Country.objects.create( - namefr="Belgique", - isonum=56 - ) - ) - ) + club=self.club ) self.assertEqual(course.duration, 2) diff --git a/src/planning/tests/tests_utils.py b/src/planning/tests/tests_utils.py new file mode 100644 index 0000000..b6bbc18 --- /dev/null +++ b/src/planning/tests/tests_utils.py @@ -0,0 +1,14 @@ +"""Tests liés aux fonctions d'utilité publique :-)""" + +from datetime import time, timedelta + +from django.test import SimpleTestCase + +from ..utils import time_diff + + +class TestTimeDiff(SimpleTestCase): + def test_time_should_be_timedelta(self): + td = time_diff(time(8, 30), time(10, 15)) + + self.assertEqual(td, timedelta(seconds=6300)) diff --git a/src/planning/utils.py b/src/planning/utils.py new file mode 100644 index 0000000..ca4f4c9 --- /dev/null +++ b/src/planning/utils.py @@ -0,0 +1,24 @@ +"""Fonctions utilitaires liées aux plannings""" + +from datetime import datetime + + +def time_diff(start, end): + """Calcule la différence de temps entre deux moments + + Args: + start (datetime.time) + end (datetime.time) + + Returns: + Le nombre de secondes de différence entre les paramètres `start` et `end`. + + Examples: + >>> from datetime import time + >>> time_difference(time(8, 30), time(10, 15)) + datetime.timedelta(seconds=6300) + """ + startdate = datetime(2000, 1, 1, start.hour, start.minute) + enddate = datetime(2000, 1, 1, end.hour, end.minute) + + return enddate - startdate -- 2.39.2 From 107b364028f9c3165c5ed96d2c7ff5de25c66b5e Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 23 Jun 2021 19:06:51 +0200 Subject: [PATCH 12/17] Retrieve original version of club_statistics view --- src/location/views.py | 149 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/src/location/views.py b/src/location/views.py index 0859b4e..61bf99d 100644 --- a/src/location/views.py +++ b/src/location/views.py @@ -145,7 +145,7 @@ def chooseStatistics(request): @login_required -def club_statistics(request, clubid): +def club_statistics_new(request, clubid): """Construit les statistiques d'un club, pour une saison choisie Questions: @@ -266,3 +266,150 @@ def club_statistics(request, clubid): "totalHoursPaid": totalHoursPaid, } return context + +@login_required +def club_statistics(request, clubid): + """ + Renvoie les statistiques d'un club pour une saison choisie. + .. todo:: tenir compte de la saison. + """ + + courses = Course.objects.filter(club__in=clubid).order_by( + "iso_day_number", "hour_begin" + ) + + totalHours = 0 + totalCourses = 0 + totalHoursByWeek = 0 + totalHoursPaid = 0 + gymnastsDict = {} + gymnasts = [] + courseList = [] + for course in courses: + nbtrainer = course.trainers.count() + list_of_gymnasts = Gymnast.objects.filter(to_gym__in=course.to_subgroup.all()) + gymnasts.extend(list_of_gymnasts) + nbgymnast = len(list_of_gymnasts) + # gymnasts = set(gymnasts.extend(Gymnast.objects.filter(to_gym__in=course.to_subgroup.all()))) + + nbhour = __diffTime(course.hour_end, course.hour_begin) # timedelta + totalHoursByWeek += nbhour.seconds + + counted = course.get_total_occurence() + + # select tous les unavailables liés au cours + unavailabilities = Unavailability.objects.filter(course=course) + for unavailable in unavailabilities: + counted -= unavailable.get_total_occurence() + + totalCourses += counted + totalTimeForCourse = nbhour * counted # timedelta + totalHourForCourse = (totalTimeForCourse.days * 24) + ( + totalTimeForCourse.seconds / 3600 + ) + totalHours += totalHourForCourse + totalHoursPaidForCourse = totalHourForCourse * nbtrainer + totalHoursPaid += totalHoursPaidForCourse + + # tmp = int(nbhour.seconds/3600) + # hour = "%d:%02d" % (tmp, (nbhour.seconds - (tmp * 3600)) / 60) + hour = nbhour.seconds / 3600 + + courseList.append( + ( + course, + nbtrainer, + nbgymnast, + hour, + counted, + totalHourForCourse, + totalHoursPaidForCourse, + ) + ) + + for gymnast in list_of_gymnasts: + # print(gymnast) + if gymnast.id not in gymnastsDict: + gymnastsDict[gymnast.id] = { + "gymnast": gymnast, + "nbcoursebyweek": 0, + "nbhourbyweek": timedelta(), + "nbtraining": 0, + "nbattendance": 0, + "nbabsence": 0, + "nbhourtraining": 0, + "nbhourattendance": timedelta(), + "percentageattendance": 0, + "nbhourabsence": 0, + "percentageabsence": 0, + } + + attendanceList = Training.objects.filter(course=course, gymnast=gymnast) + nbattendance = len(attendanceList) + + # print(str(gymnast) + ' : ' + str(nbattendance) + ' for ' + str(course) ) + + gymnastsDict[gymnast.id]["nbcoursebyweek"] += 1 + gymnastsDict[gymnast.id]["nbhourbyweek"] += nbhour # timedelta + gymnastsDict[gymnast.id]["nbtraining"] += counted + gymnastsDict[gymnast.id]["nbattendance"] += nbattendance + gymnastsDict[gymnast.id]["nbhourtraining"] += totalHourForCourse + gymnastsDict[gymnast.id]["nbhourattendance"] += ( + nbhour * nbattendance + ) # timedelta + + # print(gymnastsDict[gymnast.id]) + + # tous les cours ont été traités + totalHoursByWeek = totalHoursByWeek / 3600 + + gymnasts = set(gymnasts) + # print(gymnasts) + + for gymnast in gymnasts: + tmp = int(gymnastsDict[gymnast.id]["nbhourbyweek"].seconds / 3600) + gymnastsDict[gymnast.id]["nbhourbyweek"] = "%d:%02d" % ( + tmp, + (gymnastsDict[gymnast.id]["nbhourbyweek"].seconds - (tmp * 3600)) / 60, + ) + + gymnastsDict[gymnast.id]["nbabsence"] = ( + gymnastsDict[gymnast.id]["nbtraining"] + - gymnastsDict[gymnast.id]["nbattendance"] + ) + + # tmp = (gymnastsDict[gymnast.id]['nbhourattendance'].days * 24) + (gymnastsDict[gymnast.id]['nbhourattendance'].seconds/3600) + gymnastsDict[gymnast.id]["nbhourattendance"] = ( + gymnastsDict[gymnast.id]["nbhourattendance"].days * 24 + ) + (gymnastsDict[gymnast.id]["nbhourattendance"].seconds / 3600) + + gymnastsDict[gymnast.id]["nbhourabsence"] = ( + gymnastsDict[gymnast.id]["nbhourtraining"] + - gymnastsDict[gymnast.id]["nbhourattendance"] + ) + + gymnastsDict[gymnast.id]["percentageattendance"] = int( + ( + gymnastsDict[gymnast.id]["nbhourattendance"] + / gymnastsDict[gymnast.id]["nbhourtraining"] + ) + * 100 + ) + gymnastsDict[gymnast.id]["percentageabsence"] = int( + ( + gymnastsDict[gymnast.id]["nbhourabsence"] + / gymnastsDict[gymnast.id]["nbhourtraining"] + ) + * 100 + ) + + context = { + "courses": courseList, + "gymnasts": gymnastsDict, + "totalHoursByWeek": totalHoursByWeek, + "totalCourses": totalCourses, + "totalHours": totalHours, + "totalHoursPaid": totalHoursPaid, + } + return context + -- 2.39.2 From 63c46f6e6eb54e8f6f1f50df2db2c851929b4b1b Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 23 Jun 2021 20:13:59 +0200 Subject: [PATCH 13/17] Test location and planning models This will allows an easier refactoring of build_statistics functions --- src/location/models.py | 27 +++++++++++-- src/planning/models.py | 20 +++++++-- src/planning/tests/tests_models.py | 65 +++++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/location/models.py b/src/location/models.py index 8fdae83..18a7b44 100644 --- a/src/location/models.py +++ b/src/location/models.py @@ -51,7 +51,6 @@ class Place(models.Model): return "%s (%s)" % (self.name, self.city if self.city else "?") - class GymnastStatistics(): def __init__(self, gymnast): self.gymnast = gymnast @@ -66,6 +65,14 @@ class GymnastStatistics(): self.number_of_hours_of_absence = 0 self.percentage_of_absence = 0 + def add_course(self, course): + """Ajoute le fait que ce gymnaste a assisté à un cours""" + self.number_of_trainings += 1 + self.number_of_training_hours += course.number_of + + def percentage_of_absence(self): + return int(self.number_of_hours_of_absence / self.number_of_training_hours) * 100 + class Club(models.Model): """Représentation d'un club. @@ -116,5 +123,19 @@ class Club(models.Model): for course in self.courses: gymnasts.extend(Gymnast.objects.filter(to_gym__in=course.to_subgroup.all())) - def build_statistics(self): - pass + def build_statistics(self, season): + """Construit des statistiques liées à la saison passée en paramètre + + Args: + season_id (pk): L'identifiant de la saison. + + Returns: + Une structure (dict) reprenant les informations suivantes: + * La liste des cours associés à ce club et à la saison passée en paramètre + * Des informations statistiques concernant les gymnastes associés à chacun de ces cours + * Le nombre total de cours réellement donnés + * Le nombre + + """ + + courses = self.courses.filter(season=season) diff --git a/src/planning/models.py b/src/planning/models.py index 53c1a80..d60bb12 100644 --- a/src/planning/models.py +++ b/src/planning/models.py @@ -280,11 +280,15 @@ class Course(Markdownizable, Temporizable): * est associé à un ou plusieurs entraineurs, * est associé à un club * est associé à un jour de la semaine (numéro du jour dans la semaine : 0 = lundi, 6 = dimanche). + + Remarks: + Les cours par défaut triés sur les jours et sur les heures de début. """ class Meta: verbose_name = "Cours" verbose_name_plural = "Cours" + ordering = ("iso_day_number", "hour_begin") DAY_CHOICE = ( (1, "Lundi"), @@ -297,7 +301,7 @@ class Course(Markdownizable, Temporizable): ) club = models.ForeignKey( - "location.Club", verbose_name="Club", on_delete=models.CASCADE, default=None + "location.Club", verbose_name="Club", on_delete=models.CASCADE, default=None, related_name="courses" ) iso_day_number = models.PositiveSmallIntegerField( choices=DAY_CHOICE, verbose_name="Jour" @@ -340,14 +344,24 @@ class Course(Markdownizable, Temporizable): Examples: >>> """ - counted = course.get_total_occurence() + counted = self.get_number_of_planned_occurrences - unavailabilities = Unavailability.objects.filter(course=course) + unavailabilities = Unavailability.objects.filter(course=self) for unavailable in unavailabilities: counted -= unavailable.get_total_occurence() return counted + @property + def get_real_time_spent_during_season(self): + """Calcule le temps total dépensé pour ce cours + + + + """ + return self.get_number_of_real_occurrences * self.duration + + @property def get_gymnasts(self): return Gymnast.objects.filter(to_gym__in=self.to_subgroup.all()) diff --git a/src/planning/tests/tests_models.py b/src/planning/tests/tests_models.py index eeffa9d..d8cb8e5 100644 --- a/src/planning/tests/tests_models.py +++ b/src/planning/tests/tests_models.py @@ -6,7 +6,7 @@ from django.test import TestCase from location.models import Club, Place, Country -from ..models import get_number_of_weeks_between, Season, Course +from ..models import get_number_of_weeks_between, Season, Course, Unavailability USER_MODEL = get_user_model() @@ -29,6 +29,69 @@ class TestCourse(TestCase): self.user1 = USER_MODEL.objects.create(username="james_bond") self.user2 = USER_MODEL.objects.create(username="doctor_no") + def test_number_of_planned_occurrences(self): + """Vérifie le calcul du nombre de séances planifiées durant une saison""" + course = Course.objects.create( + iso_day_number=2, + datebegin=datetime(2021, 1, 1), + dateend=datetime(2021, 9, 30), + hour_begin=time(hour=19, minute=30), + hour_end=time(hour=22, minute=45), + club=self.club + ) + + self.assertEqual(course.get_number_of_real_occurrences, 39) + + course = Course.objects.create( + iso_day_number=2, + datebegin=datetime(2022, 1, 1), + dateend=datetime(2022, 10, 20), + hour_begin=time(hour=8, minute=30), + hour_end=time(hour=20, minute=0), + club=self.club + ) + + self.assertEqual(course.get_number_of_real_occurrences, 42) + + def test_number_of_real_occurrences_should_be_zero(self): + """Vérifie le calcul du nombre de séances réalisées durant une saison""" + course = Course.objects.create( + iso_day_number=2, + datebegin=datetime(2021, 1, 1), + dateend=datetime(2021, 9, 30), + hour_begin=time(hour=19, minute=30), + hour_end=time(hour=22, minute=45), + club=self.club + ) + + unavailabiliy = Unavailability.objects.create( + datebegin=datetime(2021, 1, 1), + dateend=datetime(2021, 9, 30), + ) + + unavailabiliy.course.add(course) + + self.assertEqual(course.get_number_of_real_occurrences, 0) + + def test_number_of_real_occurrences_should_be_21(self): + course = Course.objects.create( + iso_day_number=2, + datebegin=datetime(2021, 1, 1), + dateend=datetime(2021, 9, 30), + hour_begin=time(hour=19, minute=30), + hour_end=time(hour=22, minute=45), + club=self.club + ) + + unavailabiliy = Unavailability.objects.create( + datebegin=datetime(2021, 6, 1), + dateend=datetime(2021, 9, 30), + ) + + unavailabiliy.course.add(course) + + self.assertEqual(course.get_number_of_real_occurrences, 21) + def test_number_of_trainers_should_be_calculated(self): course = Course.objects.create( iso_day_number=2, -- 2.39.2 From a612503bf50cfd65141512a936dab1487a58316c Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 23 Jun 2021 20:28:55 +0200 Subject: [PATCH 14/17] Compute the total number of hours spent in a course --- src/planning/models.py | 6 ++++++ src/planning/tests/tests_models.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/planning/models.py b/src/planning/models.py index d60bb12..5798ab9 100644 --- a/src/planning/models.py +++ b/src/planning/models.py @@ -330,6 +330,12 @@ class Course(Markdownizable, Temporizable): """ return time_diff(self.hour_begin, self.hour_end) + @property + def total_number_of_hours(self) -> timedelta: + """Retourne le temps total a été consacré à ce cours + """ + return self.get_number_of_real_occurrences * self.number_of_hours + @property def get_number_of_planned_occurrences(self): return self.get_total_occurence() diff --git a/src/planning/tests/tests_models.py b/src/planning/tests/tests_models.py index d8cb8e5..cf7f946 100644 --- a/src/planning/tests/tests_models.py +++ b/src/planning/tests/tests_models.py @@ -73,6 +73,23 @@ class TestCourse(TestCase): self.assertEqual(course.get_number_of_real_occurrences, 0) + def test_total_number_of_hours_should_be_39_times_3(self): + """Vérifie que le nombre total d'heures consacrées est bien égal à 39 séances * 3h + + Remarks: + 39 séances * 3h = 117h = 4 jours + 21h = timedelta(days=3, seconds=75600) :-) + """ + course = Course.objects.create( + iso_day_number=2, + datebegin=datetime(2021, 1, 1), + dateend=datetime(2021, 9, 30), + hour_begin=time(hour=19, minute=0), + hour_end=time(hour=22, minute=0), + club=self.club + ) + + self.assertEqual(course.total_number_of_hours, timedelta(days=4, seconds=75600)) + def test_number_of_real_occurrences_should_be_21(self): course = Course.objects.create( iso_day_number=2, -- 2.39.2 From 40c59cbe229dc780d08380d541887b9e0948f850 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 23 Jun 2021 20:37:27 +0200 Subject: [PATCH 15/17] Starts clubs statistics --- src/location/models.py | 11 ++++++++++- src/planning/models.py | 6 ++++++ src/planning/tests/tests_models.py | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/location/models.py b/src/location/models.py index 18a7b44..415d218 100644 --- a/src/location/models.py +++ b/src/location/models.py @@ -131,11 +131,20 @@ class Club(models.Model): Returns: Une structure (dict) reprenant les informations suivantes: + + { + "total_hours_paid": + } + * La liste des cours associés à ce club et à la saison passée en paramètre * Des informations statistiques concernant les gymnastes associés à chacun de ces cours * Le nombre total de cours réellement donnés - * Le nombre + * Le nombre d'heures réellement passées pour l'ensemble des cours """ courses = self.courses.filter(season=season) + + return { + "total_hours_paid": sum[x.total_number_of_paid_hours for x in courses] + } diff --git a/src/planning/models.py b/src/planning/models.py index 5798ab9..49e4440 100644 --- a/src/planning/models.py +++ b/src/planning/models.py @@ -336,6 +336,12 @@ class Course(Markdownizable, Temporizable): """ return self.get_number_of_real_occurrences * self.number_of_hours + @property + def total_number_of_paid_hours(self) -> timedelta: + """Retourne le temps total consacré à ce cours par le(s) entraineur(s) + """ + return self.total_number_of_hours * self.number_of_trainers + @property def get_number_of_planned_occurrences(self): return self.get_total_occurence() diff --git a/src/planning/tests/tests_models.py b/src/planning/tests/tests_models.py index cf7f946..8d03165 100644 --- a/src/planning/tests/tests_models.py +++ b/src/planning/tests/tests_models.py @@ -124,6 +124,23 @@ class TestCourse(TestCase): self.assertEqual(course.number_of_trainers, 2) + def test_total_number_of_paid_hours(self): + course = Course.objects.create( + iso_day_number=2, + datebegin=datetime(2021, 1, 1), + dateend=datetime(2021, 9, 30), + hour_begin=time(hour=19, minute=0), + hour_end=time(hour=22, minute=0), + club=self.club + ) + + course.trainers.add(self.user1) + course.trainers.add(self.user2) + + self.assertEqual(course.total_number_of_paid_hours, timedelta(days=4, seconds=75600) * 2) + + + def test_number_of_course_hours(self): course = Course.objects.create( iso_day_number=2, -- 2.39.2 From 9c086475052a611b8bfeebe66c6047efb0575eba Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 24 Jun 2021 17:10:12 +0200 Subject: [PATCH 16/17] Start testing khana.people --- src/location/models.py | 5 ++- src/location/views.py | 33 +---------------- src/people/models.py | 9 ++++- src/people/tests/__init__.py | 0 src/people/tests/test_models.py | 65 +++++++++++++++++++++++++++++++++ src/people/tests_models.py | 16 -------- src/planning/models.py | 8 +--- src/planning/views.py | 2 +- 8 files changed, 81 insertions(+), 57 deletions(-) create mode 100644 src/people/tests/__init__.py create mode 100644 src/people/tests/test_models.py diff --git a/src/location/models.py b/src/location/models.py index 415d218..6d51c15 100644 --- a/src/location/models.py +++ b/src/location/models.py @@ -146,5 +146,8 @@ class Club(models.Model): courses = self.courses.filter(season=season) return { - "total_hours_paid": sum[x.total_number_of_paid_hours for x in courses] + "courses": courses, + "number_of_occured_courses": sum([x.get_number_of_real_occurrences for x in courses]), + "total_hours": sum([x.total_number_of_hours for x in courses]), + "total_hours_paid": sum([x.total_number_of_paid_hours for x in courses]), } diff --git a/src/location/views.py b/src/location/views.py index 61bf99d..24c6346 100644 --- a/src/location/views.py +++ b/src/location/views.py @@ -285,8 +285,9 @@ def club_statistics(request, clubid): gymnastsDict = {} gymnasts = [] courseList = [] + for course in courses: - nbtrainer = course.trainers.count() + list_of_gymnasts = Gymnast.objects.filter(to_gym__in=course.to_subgroup.all()) gymnasts.extend(list_of_gymnasts) nbgymnast = len(list_of_gymnasts) @@ -295,38 +296,8 @@ def club_statistics(request, clubid): nbhour = __diffTime(course.hour_end, course.hour_begin) # timedelta totalHoursByWeek += nbhour.seconds - counted = course.get_total_occurence() - - # select tous les unavailables liés au cours - unavailabilities = Unavailability.objects.filter(course=course) - for unavailable in unavailabilities: - counted -= unavailable.get_total_occurence() - - totalCourses += counted - totalTimeForCourse = nbhour * counted # timedelta - totalHourForCourse = (totalTimeForCourse.days * 24) + ( - totalTimeForCourse.seconds / 3600 - ) - totalHours += totalHourForCourse - totalHoursPaidForCourse = totalHourForCourse * nbtrainer - totalHoursPaid += totalHoursPaidForCourse - - # tmp = int(nbhour.seconds/3600) - # hour = "%d:%02d" % (tmp, (nbhour.seconds - (tmp * 3600)) / 60) hour = nbhour.seconds / 3600 - courseList.append( - ( - course, - nbtrainer, - nbgymnast, - hour, - counted, - totalHourForCourse, - totalHoursPaidForCourse, - ) - ) - for gymnast in list_of_gymnasts: # print(gymnast) if gymnast.id not in gymnastsDict: diff --git a/src/people/models.py b/src/people/models.py index 6adc6e7..02e7c6f 100644 --- a/src/people/models.py +++ b/src/people/models.py @@ -62,8 +62,7 @@ class Gymnast(Markdownizable): """ class Meta: - verbose_name = "Gymnast" - verbose_name_plural = "Gymnasts" + verbose_name = "gymnaste" ordering = ["user__last_name", "user__first_name"] GENDER_CHOICES = ((0, "Male"), (1, "Female")) @@ -189,6 +188,12 @@ class Gymnast(Markdownizable): period = pendulum.now() - pendulum.instance(self.created_at, "Europe/Brussels") return int(self.year_of_practice + period.in_years()) + def attendances(self, course): + """Retourne les séances d'un cours où le gymnaste était présent + + """ + return self.trainings.filter(course=course) + class Accident(Markdownizable): """La classe `Accident` permet d'indiquer qu'un gymnaste est tombé durant un saut. diff --git a/src/people/tests/__init__.py b/src/people/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/people/tests/test_models.py b/src/people/tests/test_models.py new file mode 100644 index 0000000..ea69c01 --- /dev/null +++ b/src/people/tests/test_models.py @@ -0,0 +1,65 @@ +"""Tests liés aux personnes, aux gymnastes, ...""" + +from django.contrib.auth import get_user_model +from django.test import TestCase + +from location.models import Club, Country, Place +from planning.models import Course, Training + +from ..models import Gymnast + + +USER_MODEL = get_user_model() + + +class TestGymnast(TestCase): + def setUp(self): + self.user1 = USER_MODEL.objects.create(username="james_bond", last_name="Bond", first_name="James") + self.club = Club.objects.create( + name="RCW", + place=Place.objects.create( + name="Somewhere", + postal=1080, + country=Country.objects.create( + namefr="Belgique", + isonum=56 + ) + ) + ) + self.course = Course.objects.create( + iso_day_number=2, + datebegin="2022-01-01", + dateend="2022-10-20", + hour_begin="08:30", + hour_end="20:00", + club=self.club + ) + + def test_gymnast_should_return_lastname_and_firstname(self): + gymnast = Gymnast(user=self.user1) + self.assertEqual(str(gymnast), "Bond, James") + + def test_gymnast_attendances_should_be_zero(self): + """Vérifie qu'un gymnaste n'assiste par défaut à aucun entrainement""" + gymnast = Gymnast.objects.create( + user=self.user1, + birthdate="1980-01-01", + gender=0, + ) + + self.assertEqual(gymnast.attendances(self.course).count(), 0) + + def test_attendances_should_be_equals_two(self): + """Vérifie que les entrainements auxquels le gymnaste a assisté sont bien pris en compte""" + gymnast = Gymnast.objects.create( + user=self.user1, + birthdate="1980-01-01", + gender=0, + ) + Training.objects.create(gymnast=gymnast, course=self.course, trainingdate="2021-02-01") + + self.assertEqual(gymnast.attendances(self.course).count(), 1) + + Training.objects.create(gymnast=gymnast, course=self.course, trainingdate="2021-02-02") + + self.assertEqual(gymnast.attendances(self.course).count(), 2) \ No newline at end of file diff --git a/src/people/tests_models.py b/src/people/tests_models.py index 3bb7587..94e39ab 100644 --- a/src/people/tests_models.py +++ b/src/people/tests_models.py @@ -5,22 +5,6 @@ from .models import Gymnast, Accident from datetime import datetime import pytest -# class GymnastTestCase(): -def test_gymnast_tostring(): - g = Gymnast(lastname="Pauchou", firstname="Fred") - assert str(g) == "Pauchou, Fred" - -def test_gymnaste_get_age(): - g = Gymnast(lastname="Pauchou", firstname="Fred", birthdate=datetime.strptime('03/07/1985', '%d/%m/%Y')); - assert g.age == 35 - -def test_gymnaste_get_next_age(): - g = Gymnast(lastname="Pauchou", firstname="Fred", birthdate=datetime.strptime('03/07/1985', '%d/%m/%Y')); - assert g.next_age == 36 - -def test_gymnaste_next_birthday(): - g = Gymnast(lastname="Pauchou", firstname="Fred", birthdate=datetime.strptime('03/07/1985', '%d/%m/%Y')); - assert g.next_birthday == datetime.strptime('03/07/2021', '%d/%m/%Y') def test_gymnast_known_skills(): # @Fred : Comment tester cela ? diff --git a/src/planning/models.py b/src/planning/models.py index 49e4440..46df81e 100644 --- a/src/planning/models.py +++ b/src/planning/models.py @@ -489,16 +489,12 @@ class Unavailability(Markdownizable, Temporizable): class Training(models.Model): - """Classe représentant les entraînements. + """Un entraînement est une occurence d'un cours pendant lequel des gymnastes sont présents. - Un entraînement est une occurence d'un cours pendant lequel des gmnastes sont présents. - - Un objet de cette classe lie donc un cours et un gymnaste à une date donnée. """ class Meta: - verbose_name = "Training" - verbose_name_plural = "Trainings" + verbose_name = "entraînement" gymnast = models.ForeignKey( "people.Gymnast", diff --git a/src/planning/views.py b/src/planning/views.py index 2fdd02c..c06fff8 100644 --- a/src/planning/views.py +++ b/src/planning/views.py @@ -1302,7 +1302,7 @@ def planningline_update(request, planninglineid=None): else: form = PlanningLineForm( initial={ - "gymnast": planningline.gymnast.id, + "gymnast": planningline.gymnast.id, "gymnast_related": planningline.gymnast, "date": planningline.date, "order": planningline.order, -- 2.39.2 From 8b3952b501390206955ca481f5080d5e53b5a2f8 Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 24 Jun 2021 21:37:36 +0200 Subject: [PATCH 17/17] Still trying to simplify club statistics ;) --- src/location/models.py | 34 +++++++++++++++++++++++----------- src/location/views.py | 6 +----- src/planning/models.py | 4 ++++ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/location/models.py b/src/location/models.py index 6d51c15..f23fda9 100644 --- a/src/location/models.py +++ b/src/location/models.py @@ -54,24 +54,30 @@ class Place(models.Model): class GymnastStatistics(): def __init__(self, gymnast): self.gymnast = gymnast - self.number_of_courses_by_week = 0 + self.number_of_courses = 0 self.number_of_hours_by_week = timedelta() self.number_of_trainings = 0 - self.number_of_attendance = 0 - self.number_of_absences = 0 - self.number_of_training_hours = 0 + self.number_of_attended_trainings = 0 self.number_of_attendance_hours = timedelta() - self.percentage_of_attendance = 0 - self.number_of_hours_of_absence = 0 - self.percentage_of_absence = 0 + + self.number_of_absences = 0 + self.number_of_attendance = 0 def add_course(self, course): """Ajoute le fait que ce gymnaste a assisté à un cours""" - self.number_of_trainings += 1 - self.number_of_training_hours += course.number_of + self.number_of_courses += 1 - def percentage_of_absence(self): - return int(self.number_of_hours_of_absence / self.number_of_training_hours) * 100 + self.number_of_attended_trainings += self.gymnast.attendances(course).count() + self.total_number_of_trainings += course.total_number_of_trainings + + self.number_of_attendance_hours += self.gymnast.attendances(course).count() * course.duration + self.number_of_total_hours += course.number_of_total_hours + + def percentage_of_attended_hours(self): + return int(self.number_of_attendance_hours / self.number_of_total_hours) * 100 + + def percentage_of_attended_trainings(self): + return int(self.number_of_attended_trainings / self.total_number_of_trainings) * 100 class Club(models.Model): @@ -145,8 +151,14 @@ class Club(models.Model): courses = self.courses.filter(season=season) + all_gymnasts = {} + + + return { + "gymnasts": all_gymnasts, "courses": courses, + "number_of_trainings": sum([x.number_of_trainings for x in courses]) "number_of_occured_courses": sum([x.get_number_of_real_occurrences for x in courses]), "total_hours": sum([x.total_number_of_hours for x in courses]), "total_hours_paid": sum([x.total_number_of_paid_hours for x in courses]), diff --git a/src/location/views.py b/src/location/views.py index 24c6346..4e2fa93 100644 --- a/src/location/views.py +++ b/src/location/views.py @@ -320,14 +320,10 @@ def club_statistics(request, clubid): # print(str(gymnast) + ' : ' + str(nbattendance) + ' for ' + str(course) ) - gymnastsDict[gymnast.id]["nbcoursebyweek"] += 1 gymnastsDict[gymnast.id]["nbhourbyweek"] += nbhour # timedelta - gymnastsDict[gymnast.id]["nbtraining"] += counted gymnastsDict[gymnast.id]["nbattendance"] += nbattendance gymnastsDict[gymnast.id]["nbhourtraining"] += totalHourForCourse - gymnastsDict[gymnast.id]["nbhourattendance"] += ( - nbhour * nbattendance - ) # timedelta + # print(gymnastsDict[gymnast.id]) diff --git a/src/planning/models.py b/src/planning/models.py index 46df81e..4745e20 100644 --- a/src/planning/models.py +++ b/src/planning/models.py @@ -346,6 +346,10 @@ class Course(Markdownizable, Temporizable): def get_number_of_planned_occurrences(self): return self.get_total_occurence() + @property + def total_number_of_trainings(self): + return Training.objects.filter(course=course).count() + @property def get_number_of_real_occurrences(self) -> int: """Retourne le nombre de fois où le cours a réellement été donné. -- 2.39.2