From d98acb324319dbe180aba87f88ec34587ec08cad Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 22 Jun 2021 20:59:05 +0200 Subject: [PATCH] [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