[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 "?")
|
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:
|
class Meta:
|
||||||
|
@ -73,3 +94,27 @@ class Club(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s (à %s)" % (self.name, self.place.city if self.place.city else "?")
|
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 people.models import Gymnast, Accident # people model
|
||||||
from .models import (
|
from .models import (
|
||||||
Club,
|
Club,
|
||||||
Place,
|
|
||||||
Country,
|
Country,
|
||||||
|
GymnastStatistics,
|
||||||
|
Place,
|
||||||
)
|
)
|
||||||
from .forms import PlaceForm
|
from .forms import PlaceForm
|
||||||
from objective.models import Skill, Routine # objective model
|
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
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def place_lookup(request):
|
def place_lookup(request):
|
||||||
|
@ -153,21 +144,6 @@ def chooseStatistics(request):
|
||||||
return render(request, "club_statistics.html", context)
|
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
|
@login_required
|
||||||
def club_statistics(request, clubid):
|
def club_statistics(request, clubid):
|
||||||
"""Construit les statistiques d'un club, pour une saison choisie
|
"""Construit les statistiques d'un club, pour une saison choisie
|
||||||
|
@ -179,13 +155,12 @@ def club_statistics(request, clubid):
|
||||||
Todo:
|
Todo:
|
||||||
* Tenir compte de la saison.
|
* Tenir compte de la saison.
|
||||||
"""
|
"""
|
||||||
|
club = Club.objects.get(pk=clubid)
|
||||||
courses = Course.objects.filter(club__in=clubid).order_by(
|
courses = Course.objects.filter(club__in=clubid).order_by(
|
||||||
"iso_day_number", "hour_begin"
|
"iso_day_number", "hour_begin"
|
||||||
)
|
)
|
||||||
|
|
||||||
totalHours = 0
|
total_hours = 0
|
||||||
totalCourses = 0
|
|
||||||
totalHoursByWeek = 0
|
totalHoursByWeek = 0
|
||||||
totalHoursPaid = 0
|
totalHoursPaid = 0
|
||||||
gymnastsDict = {}
|
gymnastsDict = {}
|
||||||
|
@ -193,33 +168,24 @@ def club_statistics(request, clubid):
|
||||||
courseList = []
|
courseList = []
|
||||||
|
|
||||||
for course in courses:
|
for course in courses:
|
||||||
number_of_trainers = course.trainers.count()
|
|
||||||
list_of_gymnasts = Gymnast.objects.filter(to_gym__in=course.to_subgroup.all())
|
list_of_gymnasts = Gymnast.objects.filter(to_gym__in=course.to_subgroup.all())
|
||||||
gymnasts.extend(list_of_gymnasts)
|
gymnasts.extend(list_of_gymnasts)
|
||||||
number_of_gymnasts = len(list_of_gymnasts)
|
number_of_gymnasts = len(list_of_gymnasts)
|
||||||
|
|
||||||
nbhour = __diffTime(course.hour_end, course.hour_begin)
|
number_of_course_hours = course.number_of_hours
|
||||||
totalHoursByWeek += nbhour.seconds
|
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
|
totalTimeForCourse = number_of_course_hours * counted # timedelta
|
||||||
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) + (
|
totalHourForCourse = (totalTimeForCourse.days * 24) + (
|
||||||
totalTimeForCourse.seconds / 3600
|
totalTimeForCourse.seconds / 3600
|
||||||
)
|
)
|
||||||
totalHours += totalHourForCourse
|
totalHours += totalHourForCourse
|
||||||
totalHoursPaidForCourse = totalHourForCourse * number_of_trainers
|
totalHoursPaidForCourse = totalHourForCourse * course.number_of_trainers
|
||||||
totalHoursPaid += totalHoursPaidForCourse
|
totalHoursPaid += totalHoursPaidForCourse
|
||||||
|
|
||||||
# tmp = int(nbhour.seconds/3600)
|
hour = number_of_course_hours.seconds / 3600
|
||||||
# hour = "%d:%02d" % (tmp, (nbhour.seconds - (tmp * 3600)) / 60)
|
|
||||||
hour = nbhour.seconds / 3600
|
|
||||||
|
|
||||||
courseList.append(
|
courseList.append(
|
||||||
(
|
(
|
||||||
|
@ -295,7 +261,7 @@ def club_statistics(request, clubid):
|
||||||
"courses": courseList,
|
"courses": courseList,
|
||||||
"gymnasts": gymnastsDict,
|
"gymnasts": gymnastsDict,
|
||||||
"totalHoursByWeek": totalHoursByWeek,
|
"totalHoursByWeek": totalHoursByWeek,
|
||||||
"totalCourses": totalCourses,
|
"totalCourses": club.get_number_of_real_occurrences,
|
||||||
"totalHours": totalHours,
|
"totalHours": totalHours,
|
||||||
"totalHoursPaid": totalHoursPaid,
|
"totalHoursPaid": totalHoursPaid,
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ from base.models import Markdownizable
|
||||||
from location.models import Club
|
from location.models import Club
|
||||||
from people.models import Gymnast
|
from people.models import Gymnast
|
||||||
|
|
||||||
|
from .utils import time_diff
|
||||||
|
|
||||||
|
|
||||||
def get_week(a_date):
|
def get_week(a_date):
|
||||||
"""
|
"""
|
||||||
|
@ -270,12 +272,14 @@ class Course(Markdownizable, Temporizable):
|
||||||
"""Classe représentant les cours.
|
"""Classe représentant les cours.
|
||||||
|
|
||||||
Un cours est défini par :
|
Un cours est défini par :
|
||||||
* une heure de début et une heure de fin,
|
* 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
|
* une date de début et une date de fin
|
||||||
ces deux dates) (hérite de la classe `Temporizable`)
|
|
||||||
* est associé à un ou plusieurs entraineurs,
|
Il est considéré comme donné hebdomadairement entre deux dates
|
||||||
* est associé à un club
|
|
||||||
* est associé à un jour de la semaine (numéro du jour dans la semaine : 0 = lundi, 6 = 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).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -300,6 +304,7 @@ class Course(Markdownizable, Temporizable):
|
||||||
)
|
)
|
||||||
hour_begin = models.TimeField(verbose_name="Heure de début")
|
hour_begin = models.TimeField(verbose_name="Heure de début")
|
||||||
hour_end = models.TimeField(verbose_name="Heure de fin")
|
hour_end = models.TimeField(verbose_name="Heure de fin")
|
||||||
|
|
||||||
season = models.ForeignKey(Season, on_delete=models.SET_NULL, null=True)
|
season = models.ForeignKey(Season, on_delete=models.SET_NULL, null=True)
|
||||||
trainers = models.ManyToManyField(
|
trainers = models.ManyToManyField(
|
||||||
User, verbose_name="Coach(es)", related_name="trainee"
|
User, verbose_name="Coach(es)", related_name="trainee"
|
||||||
|
@ -308,12 +313,45 @@ class Course(Markdownizable, Temporizable):
|
||||||
Gymnast, verbose_name="Gymnasts", related_name="courses"
|
Gymnast, verbose_name="Gymnasts", related_name="courses"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
@property
|
||||||
return "%s (%s à %s)" % (
|
def number_of_trainers(self):
|
||||||
self.get_iso_day_number_display(),
|
"""Retourne le nombre d'entraineurs liés à ce cours"""
|
||||||
self.hour_begin.strftime("%H:%M"),
|
return self.trainers.count()
|
||||||
self.hour_end.strftime("%H:%M"),
|
|
||||||
)
|
@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
|
@property
|
||||||
def duration(self):
|
def duration(self):
|
||||||
|
@ -332,6 +370,13 @@ class Course(Markdownizable, Temporizable):
|
||||||
)
|
)
|
||||||
return date_end.diff(date_begin).in_hours()
|
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):
|
class Group(models.Model):
|
||||||
"""Classe représentant les groupes (Loisir, D1, D2, A, B, …).
|
"""Classe représentant les groupes (Loisir, D1, D2, A, B, …).
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""Tests liés au modèle de l'application planning"""
|
"""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 django.test import TestCase
|
||||||
|
|
||||||
from location.models import Club, Place, Country
|
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
|
from ..models import get_number_of_weeks_between, Season, Course
|
||||||
|
|
||||||
|
|
||||||
|
USER_MODEL = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class TestCourse(TestCase):
|
class TestCourse(TestCase):
|
||||||
def setUp(self):
|
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):
|
def test_course_duration(self):
|
||||||
"""Vérifie le calcul de durée d'un cours"""
|
"""Vérifie le calcul de durée d'un cours"""
|
||||||
|
@ -20,17 +65,7 @@ class TestCourse(TestCase):
|
||||||
dateend=datetime(2021, 9, 30),
|
dateend=datetime(2021, 9, 30),
|
||||||
hour_begin=time(hour=19, minute=30),
|
hour_begin=time(hour=19, minute=30),
|
||||||
hour_end=time(hour=22, minute=45),
|
hour_end=time(hour=22, minute=45),
|
||||||
club=Club.objects.create(
|
club=self.club
|
||||||
name="RCW",
|
|
||||||
place=Place.objects.create(
|
|
||||||
name="Somewhere",
|
|
||||||
postal=1080,
|
|
||||||
country=Country.objects.create(
|
|
||||||
namefr="Belgique",
|
|
||||||
isonum=56
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(course.duration, 2)
|
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