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..25657b0 --- /dev/null +++ b/src/.coveragerc @@ -0,0 +1,16 @@ +[run] +branch = true +omit = + */tests/* + */test* + */migrations/* + */urls.py + */admin.py + */settings/* + */wsgi.py + */__init__.py + manage.py +source = . + +[report] +show_missing = true 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/models.py b/src/location/models.py index a475259..f23fda9 100644 --- a/src/location/models.py +++ b/src/location/models.py @@ -51,12 +51,46 @@ 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. +class GymnastStatistics(): + def __init__(self, gymnast): + self.gymnast = gymnast + self.number_of_courses = 0 + self.number_of_hours_by_week = timedelta() + self.number_of_trainings = 0 + self.number_of_attended_trainings = 0 + self.number_of_attendance_hours = timedelta() - .. todo:: Un club peut avoir plusieurs salle et une salle peut-être louée par plusieurs clubs... M2M ? + 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_courses += 1 + + 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): + """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 +107,59 @@ 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, 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: + + { + "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 d'heures réellement passées pour l'ensemble des cours + + """ + + 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/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 diff --git a/src/location/views.py b/src/location/views.py index cd92c35..4e2fa93 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,11 +144,133 @@ def chooseStatistics(request): return render(request, "club_statistics.html", context) +@login_required +def club_statistics_new(request, clubid): + """Construit les statistiques d'un club, pour une saison choisie + + 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. + """ + club = Club.objects.get(pk=clubid) + courses = Course.objects.filter(club__in=clubid).order_by( + "iso_day_number", "hour_begin" + ) + + total_hours = 0 + totalHoursByWeek = 0 + totalHoursPaid = 0 + gymnastsDict = {} + gymnasts = [] + courseList = [] + + for course in courses: + 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) + + number_of_course_hours = course.number_of_hours + totalHoursByWeek += number_of_course_hours.seconds + + counted = course.get_number_of_real_occurrences + + totalTimeForCourse = number_of_course_hours * counted # timedelta + totalHourForCourse = (totalTimeForCourse.days * 24) + ( + totalTimeForCourse.seconds / 3600 + ) + totalHours += totalHourForCourse + totalHoursPaidForCourse = totalHourForCourse * course.number_of_trainers + totalHoursPaid += totalHoursPaidForCourse + + hour = number_of_course_hours.seconds / 3600 + + courseList.append( + ( + course, + course.number_of_trainers, + number_of_gymnasts, + hour, + course.get_number_of_real_occurrences, + totalHourForCourse, + totalHoursPaidForCourse, + ) + ) + + for gymnast in list_of_gymnasts: + + gymnast_stats = gymnastsDict.setdefault(gymnast.id, GymnastStatistics(gymnast)) + + attendance_list = Training.objects.filter(course=course, gymnast=gymnast) + number_of_attendance = attendance_list.count() + + 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) + + 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": club.get_number_of_real_occurrences, + "totalHours": totalHours, + "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. """ @@ -172,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) @@ -182,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: @@ -236,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]) @@ -299,3 +379,4 @@ def club_statistics(request, clubid): "totalHoursPaid": totalHoursPaid, } return context + 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 393a8f4..4745e20 100644 --- a/src/planning/models.py +++ b/src/planning/models.py @@ -11,498 +11,572 @@ 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): - """ + """ - 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 - class Meta: - verbose_name = "Course" - verbose_name_plural = "Courses" + Il est considéré comme donné hebdomadairement entre deux dates - DAY_CHOICE = ( - (1, "Lundi"), - (2, "Mardi"), - (3, "Mercredi"), - (4, "Jeudi"), - (5, "Vendredi"), - (6, "Samedi"), - (7, "Dimanche"), - ) + * 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). - 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" - ) + Remarks: + Les cours par défaut triés sur les jours et sur les heures de début. + """ - 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 Meta: + verbose_name = "Cours" + verbose_name_plural = "Cours" + ordering = ("iso_day_number", "hour_begin") - @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() + 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, related_name="courses" + ) + 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" + ) + + @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 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 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() + + @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é. + + 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 = self.get_number_of_planned_occurrences + + 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()) + + @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() + + 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, …). + """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. + """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 = "entraînement" - 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..8d03165 100644 --- a/src/planning/tests/tests_models.py +++ b/src/planning/tests/tests_models.py @@ -1,7 +1,171 @@ +"""Tests liés au modèle de l'application planning""" -from datetime import datetime +from datetime import datetime, time, timedelta +from django.contrib.auth import get_user_model 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, Unavailability + + +USER_MODEL = get_user_model() + + +class TestCourse(TestCase): + def setUp(self): + 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_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_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, + 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, + 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_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, + 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""" + 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.duration, 2) class TestUtils(TestCase): 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 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,