[WIP] Refactoring Club & Course for statistics purpose

This commit is contained in:
Fred 2021-06-22 20:59:05 +02:00
parent 2a12177f74
commit d98acb3243
6 changed files with 204 additions and 75 deletions

View File

@ -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

View File

@ -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,
}

View File

@ -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 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, …).

View File

@ -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)

View File

@ -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))

24
src/planning/utils.py Normal file
View File

@ -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