[WIP] refactoring/location-club-statistics #71

Draft
Fred wants to merge 19 commits from refactoring/location-club-statistics into master
15 changed files with 975 additions and 491 deletions

13
Makefile Normal file
View File

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

16
src/.coveragerc Normal file
View File

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

View File

@ -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 ? .. 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) 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) 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 return next_event_list
@ -130,18 +115,9 @@ def __getCourseInfo(request):
else: else:
rest = int((tmp.days + 1) / 7) 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_left += rest
courses_done += counted - rest courses_done += counted - rest
# courses.append((course, counted, (counted - rest)))
return courses, courses_done, courses_left return courses, courses_done, courses_left

View File

@ -51,12 +51,46 @@ 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): class GymnastStatistics():
""" def __init__(self, gymnast):
Représente un club. Un club est associé à un lieu. Pour faciliter les filtres, self.gymnast = gymnast
un club peut être actif ou non. 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: class Meta:
@ -73,3 +107,59 @@ 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, 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": <Le nombre d'heures devant être payées pour ce cours>
}
* 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]),
}

View File

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

View File

@ -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,11 +144,133 @@ def chooseStatistics(request):
return render(request, "club_statistics.html", context) 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 @login_required
def club_statistics(request, clubid): def club_statistics(request, clubid):
""" """
Renvoie les statistiques d'un club pour une saison choisie. Renvoie les statistiques d'un club pour une saison choisie.
.. todo:: tenir compte de la saison. .. todo:: tenir compte de la saison.
""" """
@ -172,8 +285,9 @@ def club_statistics(request, clubid):
gymnastsDict = {} gymnastsDict = {}
gymnasts = [] gymnasts = []
courseList = [] courseList = []
for course in courses: for course in courses:
nbtrainer = 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)
nbgymnast = len(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 nbhour = __diffTime(course.hour_end, course.hour_begin) # timedelta
totalHoursByWeek += nbhour.seconds 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 hour = nbhour.seconds / 3600
courseList.append(
(
course,
nbtrainer,
nbgymnast,
hour,
counted,
totalHourForCourse,
totalHoursPaidForCourse,
)
)
for gymnast in list_of_gymnasts: for gymnast in list_of_gymnasts:
# print(gymnast) # print(gymnast)
if gymnast.id not in gymnastsDict: if gymnast.id not in gymnastsDict:
@ -236,14 +320,10 @@ def club_statistics(request, clubid):
# print(str(gymnast) + ' : ' + str(nbattendance) + ' for ' + str(course) ) # print(str(gymnast) + ' : ' + str(nbattendance) + ' for ' + str(course) )
gymnastsDict[gymnast.id]["nbcoursebyweek"] += 1
gymnastsDict[gymnast.id]["nbhourbyweek"] += nbhour # timedelta gymnastsDict[gymnast.id]["nbhourbyweek"] += nbhour # timedelta
gymnastsDict[gymnast.id]["nbtraining"] += counted
gymnastsDict[gymnast.id]["nbattendance"] += nbattendance gymnastsDict[gymnast.id]["nbattendance"] += nbattendance
gymnastsDict[gymnast.id]["nbhourtraining"] += totalHourForCourse gymnastsDict[gymnast.id]["nbhourtraining"] += totalHourForCourse
gymnastsDict[gymnast.id]["nbhourattendance"] += (
nbhour * nbattendance
) # timedelta
# print(gymnastsDict[gymnast.id]) # print(gymnastsDict[gymnast.id])
@ -299,3 +379,4 @@ def club_statistics(request, clubid):
"totalHoursPaid": totalHoursPaid, "totalHoursPaid": totalHoursPaid,
} }
return context return context

View File

@ -62,8 +62,7 @@ class Gymnast(Markdownizable):
""" """
class Meta: class Meta:
verbose_name = "Gymnast" verbose_name = "gymnaste"
verbose_name_plural = "Gymnasts"
ordering = ["user__last_name", "user__first_name"] ordering = ["user__last_name", "user__first_name"]
GENDER_CHOICES = ((0, "Male"), (1, "Female")) GENDER_CHOICES = ((0, "Male"), (1, "Female"))
@ -189,6 +188,12 @@ class Gymnast(Markdownizable):
period = pendulum.now() - pendulum.instance(self.created_at, "Europe/Brussels") period = pendulum.now() - pendulum.instance(self.created_at, "Europe/Brussels")
return int(self.year_of_practice + period.in_years()) 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): class Accident(Markdownizable):
"""La classe `Accident` permet d'indiquer qu'un gymnaste est tombé durant un saut. """La classe `Accident` permet d'indiquer qu'un gymnaste est tombé durant un saut.

View File

View File

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

View File

@ -5,22 +5,6 @@ from .models import Gymnast, Accident
from datetime import datetime from datetime import datetime
import pytest 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(): def test_gymnast_known_skills():
# @Fred : Comment tester cela ? # @Fred : Comment tester cela ?

View File

@ -11,498 +11,572 @@ 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):
""" """
Remarks: Remarks:
Je ne comprends pas trop cette fonction... 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 ? 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) the_date = pendulum.parse(a_date)
day = the_date.weekday() day = the_date.weekday()
monday = the_date - timedelta(days=day) monday = the_date - timedelta(days=day)
sunday = the_date + timedelta(days=(6 - day)) sunday = the_date + timedelta(days=(6 - day))
return monday, sunday return monday, sunday
def get_number_of_weeks_between(start, stop): def get_number_of_weeks_between(start, stop):
""" """
Renvoie le nombre de semaines entre deux dates. Renvoie le nombre de semaines entre deux dates.
Par extension, cela permet de connaitre le nombre d'occurence d'un Par extension, cela permet de connaitre le nombre d'occurence d'un
évènement (entraînement, par exemple) hebdromadaire entre deux dates évènement (entraînement, par exemple) hebdromadaire entre deux dates
et ainsi pouvoir plannifier. et ainsi pouvoir plannifier.
:param start: date de début de la période :param start: date de début de la période
:type start: datetime.date :type start: datetime.date
:param stop: date de fin de la période :param stop: date de fin de la période
:type stop: datetime.date :type stop: datetime.date
:return: Le nombre de semaines entre les deux dates. :return: Le nombre de semaines entre les deux dates.
Remarks: Remarks:
Proposition d'utiliser isocalendar() sur une date. Proposition d'utiliser isocalendar() sur une date.
L'indice 1 de la valeur de retour donne la semaine correspondant. L'indice 1 de la valeur de retour donne la semaine correspondant.
Eg. Eg.
>>> from datetime import date >>> from datetime import date
>>> d = date(2020, 9, 27) >>> d = date(2020, 9, 27)
>>> d.isocalendar() >>> d.isocalendar()
(2020, 39, 7) (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 tmp = stop - start
number_of_days = abs(tmp.days) number_of_days = abs(tmp.days)
number_of_week = int((number_of_days + 1) / 7) number_of_week = int((number_of_days + 1) / 7)
if ((number_of_days + 1) % 7) > 0: if ((number_of_days + 1) % 7) > 0:
number_of_week += 1 number_of_week += 1
if tmp.days < 0: if tmp.days < 0:
number_of_week *= -1 number_of_week *= -1
return number_of_week return number_of_week
class TemporizableQuerySet(models.QuerySet): 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): def next(self, limit):
""" """
Renvoie la liste des prochains "temporizable" (par rapport à la date du jour). Renvoie la liste des prochains "temporizable" (par rapport à la date du jour).
:param limit: nombre d'éléments désirés. :param limit: nombre d'éléments désirés.
:type limit: int :type limit: int
:return: une liste de `limit` éléments temporizables. :return: une liste de `limit` éléments temporizables.
""" """
return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:limit] return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:limit]
def last(self, limit): def last(self, limit):
""" """
Renvoie la liste des derniers "temporizable" (par rapport à la date du jour). Renvoie la liste des derniers "temporizable" (par rapport à la date du jour).
:param limit: nombre d'éléments désirés. :param limit: nombre d'éléments désirés.
:type limit: int :type limit: int
:return: une liste de `limit` éléments temporizables :return: une liste de `limit` éléments temporizables
""" """
return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:limit] return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:limit]
# def get(self, date_string): # def get(self, date_string):
# """ # """
# """ # """
# try: # try:
# selected_object = self.get(datebegin__lte=date_string, dateend__gte=date_string) # selected_object = self.get(datebegin__lte=date_string, dateend__gte=date_string)
# except self.DoesNotExist: # except self.DoesNotExist:
# return None # return None
# except self.MultipleObjectsReturned: # except self.MultipleObjectsReturned:
# return None # return None
# return selected_object # return selected_object
class Temporizable(models.Model): 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: class Meta:
abstract = True abstract = True
datebegin = models.DateTimeField(verbose_name="Début") datebegin = models.DateTimeField(verbose_name="Début")
dateend = models.DateTimeField(blank=True, verbose_name="Fin") 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): def get_total_occurence(self):
""" """
Renvoie le nombre de semaines entre les deux dates d'une instance de la Renvoie le nombre de semaines entre les deux dates d'une instance de la
classe `Temporizable`. classe `Temporizable`.
:return: nombre de semaines. :return: nombre de semaines.
""" """
return get_number_of_weeks_between(self.datebegin.date(), self.dateend.date()) return get_number_of_weeks_between(self.datebegin.date(), self.dateend.date())
def get_number_of_occurence_to_event(self, the_date): def get_number_of_occurence_to_event(self, the_date):
""" """
Renvoie le nombre semaines entre une date choisie et le début Renvoie le nombre semaines entre une date choisie et le début
(datebegin) d'une instance de la classe `Temporizable`. (datebegin) d'une instance de la classe `Temporizable`.
:param the_date: date par rapport à laquelle le calcul sera fait. :param the_date: date par rapport à laquelle le calcul sera fait.
:type the_date: datetime.date :type the_date: datetime.date
:return: nombre de semaines. :return: nombre de semaines.
""" """
return get_number_of_weeks_between(the_date, self.datebegin.date()) return get_number_of_weeks_between(the_date, self.datebegin.date())
def get_number_of_occurence_inbetween(self, the_date, rest=True): def get_number_of_occurence_inbetween(self, the_date, rest=True):
""" """
Renvoie le nombre semaines entre une date choisie et une instance de la Renvoie le nombre semaines entre une date choisie et une instance de la
classe `Temporizable`. Le calcul peut se faire soit entre la date 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 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. 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. :param the_date: date par rapport à laquelle le calcul sera fait.
:type the_date: datetime.date :type the_date: datetime.date
:param rest: paramètre définissant s'il faut calculer le reste des :param rest: paramètre définissant s'il faut calculer le reste des
occurences à venir (depuis `the_date` jusqu'à la date de fin) ou occurences à venir (depuis `the_date` jusqu'à la date de fin) ou
les occurences déjà passées (depuis la date de début jusqu'à les occurences déjà passées (depuis la date de début jusqu'à
`the_date`) `the_date`)
:type rest: booléen :type rest: booléen
:return: nombre de semaines. :return: nombre de semaines.
""" """
if rest: if rest:
return get_number_of_weeks_between(the_date, self.dateend.date()) return get_number_of_weeks_between(the_date, self.dateend.date())
else: else:
return get_number_of_weeks_between(self.datebegin.date(), the_date) return get_number_of_weeks_between(self.datebegin.date(), the_date)
class Season(Temporizable): class Season(Temporizable):
""" """
Classe représentant une saison. Une saison est déinie par : Classe représentant une saison. Une saison est déinie par :
- un id, - un id,
- un label, - un label,
- une date de début et - une date de début et
- une date de fin. - une date de fin.
La date de début est très souvent le : 01/09/xxxx 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 La date de fin est très souvent le : 31/08/xxxy
exemple : 1/9/2015 - 31/8/2016 exemple : 1/9/2015 - 31/8/2016
""" """
class Meta: class Meta:
verbose_name = "Season" verbose_name = "Season"
verbose_name_plural = "Seasons" verbose_name_plural = "Seasons"
label = models.CharField(max_length=11, verbose_name="Label") label = models.CharField(max_length=11, verbose_name="Label")
# active ou default = models.BooleanField(verbose_name='Défaut') # active ou default = models.BooleanField(verbose_name='Défaut')
def __str__(self): def __str__(self):
return "%s" % (self.label) return "%s" % (self.label)
def week_number_from_begin(self, target_date): def week_number_from_begin(self, target_date):
return get_number_of_weeks_between(self.datebegin.date(), target_date) return get_number_of_weeks_between(self.datebegin.date(), target_date)
class EventType(models.Model): class EventType(models.Model):
""" """
Classe représentant les types d'évènements. Classe représentant les types d'évènements.
C'est un dictionnaire fini : C'est un dictionnaire fini :
- compétiton qualificative, - compétiton qualificative,
- compétition finale, - compétition finale,
- démonstration, - démonstration,
- -
""" """
class Meta: class Meta:
verbose_name = "Event Type" verbose_name = "Event Type"
verbose_name_plural = "Event Types" verbose_name_plural = "Event Types"
name = models.CharField(max_length=255, verbose_name="Nom") name = models.CharField(max_length=255, verbose_name="Nom")
acronym = models.CharField(max_length=5, verbose_name="Acronyme") acronym = models.CharField(max_length=5, verbose_name="Acronyme")
def __str__(self): def __str__(self):
return "%s (%s)" % (self.name, self.acronym) return "%s (%s)" % (self.name, self.acronym)
class Event(Markdownizable, Temporizable): class Event(Markdownizable, Temporizable):
"""Classe représentant les évènements. """Classe représentant les évènements.
Un évènement est caractérisé par : Un évènement est caractérisé par :
* un nom, * un nom,
* un lieu (place), * un lieu (place),
* un type (compétition, démonstration, ), * un type (compétition, démonstration, ),
* des gymnastes (participation prévue). * des gymnastes (participation prévue).
Je ne me rapelle plus à quoi sert le club. Je ne me rapelle plus à quoi sert le club.
""" """
class Meta: class Meta:
verbose_name = "Event" verbose_name = "Event"
verbose_name_plural = "Event" verbose_name_plural = "Event"
place = models.ForeignKey( place = models.ForeignKey(
"location.Place", verbose_name="Lieu", on_delete=models.CASCADE, default=None "location.Place", verbose_name="Lieu", on_delete=models.CASCADE, default=None
) )
eventtype = models.ForeignKey( eventtype = models.ForeignKey(
EventType, verbose_name="Type", on_delete=models.CASCADE, default=None EventType, verbose_name="Type", on_delete=models.CASCADE, default=None
) )
name = models.CharField(max_length=255, verbose_name="Nom") name = models.CharField(max_length=255, verbose_name="Nom")
# club = models.ManyToManyField('location.Club', related_name="concernate_by", blank=True) # club = models.ManyToManyField('location.Club', related_name="concernate_by", blank=True)
gymnasts = models.ManyToManyField( gymnasts = models.ManyToManyField(
"people.Gymnast", "people.Gymnast",
through="Event_Participation", through="Event_Participation",
related_name="participate_to", related_name="participate_to",
verbose_name="Participants", verbose_name="Participants",
) )
def __str__(self): def __str__(self):
return "%s%s)" % (self.name, self.place.city) return "%s%s)" % (self.name, self.place.city)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.checkdates() self.checkdates()
super().save(*args, **kwargs) super().save(*args, **kwargs)
def checkdates(self): def checkdates(self):
""" """
Fonction assignant la date de fin d'un évènement à la date de début, si la date 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. 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: if self.dateend is None and self.datebegin is not None:
self.dateend = datetime.combine(self.datebegin.date(), time(18, 0)) self.dateend = datetime.combine(self.datebegin.date(), time(18, 0))
@property @property
def number_of_week_from_today(self): def number_of_week_from_today(self):
today = pendulum.now().date() today = pendulum.now().date()
return get_number_of_weeks_between(today, self.datebegin.date()) return get_number_of_weeks_between(today, self.datebegin.date())
class Event_Participation(models.Model): class Event_Participation(models.Model):
""" """
""" """
event = models.ForeignKey(Event, on_delete=models.CASCADE) event = models.ForeignKey(Event, on_delete=models.CASCADE)
gymnast = models.ForeignKey("people.Gymnast", on_delete=models.CASCADE) gymnast = models.ForeignKey("people.Gymnast", on_delete=models.CASCADE)
rank = models.PositiveSmallIntegerField(default=0) rank = models.PositiveSmallIntegerField(default=0)
class Course(Markdownizable, Temporizable): 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,
* est associé à un club
* est associé à un jour de la semaine (numéro du jour dans la semaine : 0 = lundi, 6 = dimanche).
"""
class Meta: Il est considéré comme donné hebdomadairement entre deux dates
verbose_name = "Course"
verbose_name_plural = "Courses"
DAY_CHOICE = ( * est associé à un ou plusieurs entraineurs,
(1, "Lundi"), * est associé à un club
(2, "Mardi"), * est associé à un jour de la semaine (numéro du jour dans la semaine : 0 = lundi, 6 = dimanche).
(3, "Mercredi"),
(4, "Jeudi"),
(5, "Vendredi"),
(6, "Samedi"),
(7, "Dimanche"),
)
club = models.ForeignKey( Remarks:
"location.Club", verbose_name="Club", on_delete=models.CASCADE, default=None Les cours par défaut triés sur les jours et sur les heures de début.
) """
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"
)
def __str__(self): class Meta:
return "%s (%s à %s)" % ( verbose_name = "Cours"
self.get_iso_day_number_display(), verbose_name_plural = "Cours"
self.hour_begin.strftime("%H:%M"), ordering = ("iso_day_number", "hour_begin")
self.hour_end.strftime("%H:%M"),
)
@property DAY_CHOICE = (
def duration(self): (1, "Lundi"),
""" (2, "Mardi"),
Renvoie la durée d'un cours en heures (3, "Mercredi"),
""" (4, "Jeudi"),
date_begin = pendulum.datetime( (5, "Vendredi"),
2000, 1, 1, self.hour_begin.hour, self.hour_begin.minute (6, "Samedi"),
) (7, "Dimanche"),
date_end = pendulum.datetime( )
2000, 1, 1, self.hour_end.hour, self.hour_end.minute
) club = models.ForeignKey(
return date_end.diff(date_begin).in_hours() "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 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): 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: class Meta:
verbose_name = "Group" verbose_name = "Group"
verbose_name_plural = "Groups" verbose_name_plural = "Groups"
club = models.ForeignKey("location.Club", on_delete=models.CASCADE, default=None) club = models.ForeignKey("location.Club", on_delete=models.CASCADE, default=None)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
acronym = models.CharField(max_length=50) acronym = models.CharField(max_length=50)
active = models.BooleanField(default=1) active = models.BooleanField(default=1)
season = models.CharField( season = models.CharField(
max_length=9, max_length=9,
default=str(timezone.now().year) + "-" + str(timezone.now().year + 1), default=str(timezone.now().year) + "-" + str(timezone.now().year + 1),
) )
def __str__(self): def __str__(self):
return "%s (%s)" % (self.name, self.acronym) return "%s (%s)" % (self.name, self.acronym)
class Subgroup(models.Model): 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, 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. nous pouvons connaître le(s) club(s) du gymnaste pour chaque saison.
""" """
class Meta: class Meta:
verbose_name = "Subgroup" verbose_name = "Subgroup"
verbose_name_plural = "Subgroups" verbose_name_plural = "Subgroups"
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
acronym = models.CharField(max_length=50) acronym = models.CharField(max_length=50)
group = models.ForeignKey(Group, on_delete=models.CASCADE, default=None) group = models.ForeignKey(Group, on_delete=models.CASCADE, default=None)
courses = models.ManyToManyField(Course, related_name="to_subgroup") courses = models.ManyToManyField(Course, related_name="to_subgroup")
gymnasts = models.ManyToManyField( gymnasts = models.ManyToManyField(
"people.Gymnast", related_name="to_gym", blank=True "people.Gymnast", related_name="to_gym", blank=True
) )
active = models.BooleanField(default=1) active = models.BooleanField(default=1)
def __str__(self): def __str__(self):
return "%s (%s)" % (self.name, self.group.name) return "%s (%s)" % (self.name, self.group.name)
class UnavailabilityManager(models.Manager): 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): def next(self, count):
return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:count] return self.filter(datebegin__gte=timezone.now()).order_by("datebegin")[0:count]
def last(self, count): def last(self, count):
return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:count] return self.filter(dateend__lte=timezone.now()).order_by("-dateend")[0:count]
class Unavailability(Markdownizable, Temporizable): class Unavailability(Markdownizable, Temporizable):
"""Classe représentant les indisponibilités. """Classe représentant les indisponibilités.
""" """
class Meta: class Meta:
verbose_name = "Indisponibilité" verbose_name = "Indisponibilité"
verbose_name_plural = "Indisponibilités" 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): def __str__(self):
return "du %s au %s" % (self.datebegin, self.dateend) return "du %s au %s" % (self.datebegin, self.dateend)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.checkdates() self.checkdates()
super().save(*args, **kwargs) super().save(*args, **kwargs)
def checkdates(self): def checkdates(self):
if self.dateend is None and self.datebegin is not None: if self.dateend is None and self.datebegin is not None:
self.dateend = self.datebegin self.dateend = self.datebegin
class Training(models.Model): 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: gymnast = models.ForeignKey(
verbose_name = "Training" "people.Gymnast",
verbose_name_plural = "Trainings" 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( def __str__(self):
"people.Gymnast", return "%s - %s, %s" % (self.trainingdate, self.course, self.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): @staticmethod
return "%s - %s, %s" % (self.trainingdate, self.course, self.gymnast) def create(gymnast, course, trainingdate):
t = Training()
@staticmethod t.gymnast = gymnast
def create(gymnast, course, trainingdate): t.course = course
t = Training() t.trainingdate = trainingdate
t.gymnast = gymnast t.save()
t.course = course return t
t.trainingdate = trainingdate
t.save()
return t
class Round(Markdownizable): 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: class Meta:
verbose_name = "Round" verbose_name = "Round"
verbose_name_plural = "Rounds" verbose_name_plural = "Rounds"
EVALUATION_CHOICES = ( EVALUATION_CHOICES = (
(0, "- -"), (0, "- -"),
(1, "- +"), (1, "- +"),
(2, "+ -"), (2, "+ -"),
(3, "+ +"), (3, "+ +"),
) )
training = models.ForeignKey( training = models.ForeignKey(
Training, on_delete=models.CASCADE, default=None, related_name="rounds" Training, on_delete=models.CASCADE, default=None, related_name="rounds"
) )
educative = models.ForeignKey( educative = models.ForeignKey(
"objective.Educative", "objective.Educative",
on_delete=models.CASCADE, on_delete=models.CASCADE,
default=None, default=None,
blank=True, blank=True,
null=True, null=True,
) )
round_information = models.CharField(max_length=255, blank=True, null=True) round_information = models.CharField(max_length=255, blank=True, null=True)
round_number = models.PositiveSmallIntegerField(blank=True, null=True) round_number = models.PositiveSmallIntegerField(blank=True, null=True)
gymnast_evaluation = models.PositiveSmallIntegerField( gymnast_evaluation = models.PositiveSmallIntegerField(
choices=EVALUATION_CHOICES, blank=True, null=True choices=EVALUATION_CHOICES, blank=True, null=True
) )
coach_evaluation = models.PositiveSmallIntegerField(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) coachid = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL)
nb_of_realisation = models.PositiveSmallIntegerField(blank=True, null=True) nb_of_realisation = models.PositiveSmallIntegerField(blank=True, null=True)
nb_of_success = models.PositiveSmallIntegerField(blank=True, null=True) nb_of_success = models.PositiveSmallIntegerField(blank=True, null=True)
is_important = models.BooleanField(default=False) is_important = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return "%s" % (self.round_number) return "%s" % (self.round_number)
class PlanningLine(Markdownizable): class PlanningLine(Markdownizable):
"""Classe représentant les passages prévisionnels (incubating idea). """Classe représentant les passages prévisionnels (incubating idea).
""" """
class Meta: class Meta:
verbose_name = "Planning Line" verbose_name = "Planning Line"
verbose_name_plural = "Planning lines" verbose_name_plural = "Planning lines"
# ordering = ['gymnast', 'date', 'order'] # ordering = ['gymnast', 'date', 'order']
gymnast = models.ForeignKey(Gymnast, on_delete=models.CASCADE, default=None) gymnast = models.ForeignKey(Gymnast, on_delete=models.CASCADE, default=None)
date = models.DateField(verbose_name="Date") date = models.DateField(verbose_name="Date")
order = models.PositiveSmallIntegerField() order = models.PositiveSmallIntegerField()
todo = models.CharField(max_length=255) todo = models.CharField(max_length=255)

View File

@ -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 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): class TestUtils(TestCase):

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

View File

@ -1302,7 +1302,7 @@ def planningline_update(request, planninglineid=None):
else: else:
form = PlanningLineForm( form = PlanningLineForm(
initial={ initial={
"gymnast": planningline.gymnast.id, "gymnast": planningline.gymnast.id,
"gymnast_related": planningline.gymnast, "gymnast_related": planningline.gymnast,
"date": planningline.date, "date": planningline.date,
"order": planningline.order, "order": planningline.order,