[WIP] Refactoring Club & Course for statistics purpose
This commit is contained in:
parent
2a12177f74
commit
d98acb3243
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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, …).
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
|
@ -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
|
Loading…
Reference in New Issue