Compare commits

...

19 Commits

Author SHA1 Message Date
Fred 8b3952b501 Still trying to simplify club statistics ;) 2021-06-24 21:37:36 +02:00
Fred 9c08647505 Start testing khana.people 2021-06-24 17:10:12 +02:00
Fred 40c59cbe22 Starts clubs statistics 2021-06-23 20:37:27 +02:00
Fred a612503bf5 Compute the total number of hours spent in a course 2021-06-23 20:28:55 +02:00
Fred 63c46f6e6e Test location and planning models
This will allows an easier refactoring of build_statistics functions
2021-06-23 20:13:59 +02:00
Fred 107b364028 Retrieve original version of club_statistics view 2021-06-23 19:06:51 +02:00
Fred 0a515fdf3a Merge branch 'refactoring/location-club-statistics' of grimbox.be:Sulley/khana into refactoring/location-club-statistics 2021-06-22 21:03:15 +02:00
Fred d98acb3243 [WIP] Refactoring Club & Course for statistics purpose 2021-06-22 20:59:05 +02:00
Fred 2a12177f74 Remove pytest old tests 2021-06-22 19:22:17 +02:00
Fred b26eab154e Ignore admin.py files when covering code 2021-06-22 19:22:17 +02:00
Fred Pauchet 0bff7e0f66 Refactors clubs statistics 2021-06-22 19:22:17 +02:00
Fred Pauchet 00df7cbd0b Create a new `ClubStatistics` class to store data
This will allow to make the location/views.py file more readable
2021-06-22 19:22:17 +02:00
Fred Pauchet 66735a49e3 Code quality
* Create a makefile (for *nix only, right?)
* Configure code coverage and tests
* Configure linter
* Configure linter for errors only
2021-06-22 19:22:17 +02:00
Fred c8d543ab50 Remove pytest old tests 2021-06-22 19:20:35 +02:00
Fred 1a392f4e42 Ignore admin.py files when covering code 2021-06-22 19:20:19 +02:00
Fred Pauchet de61d1851d Refactors clubs statistics 2021-06-22 19:15:51 +02:00
Fred Pauchet 1a3c62d12b Create a new `ClubStatistics` class to store data
This will allow to make the location/views.py file more readable
2021-06-21 21:37:37 +02:00
Fred 67b2b0ccd6 Merge branch 'master' into quality/code 2021-05-19 21:02:23 +02:00
Fred Pauchet 523a52c5e5 Code quality
* Create a makefile (for *nix only, right?)
* Configure code coverage and tests
* Configure linter
* Configure linter for errors only
2021-05-19 20:58:25 +02:00
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 ?
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)
# 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
@ -130,18 +115,9 @@ def __getCourseInfo(request):
else:
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_done += counted - rest
# courses.append((course, counted, (counted - rest)))
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 "?")
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.
class GymnastStatistics():
def __init__(self, gymnast):
self.gymnast = gymnast
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:
@ -73,3 +107,59 @@ 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, 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 .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,11 +144,133 @@ def chooseStatistics(request):
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
def club_statistics(request, clubid):
"""
Renvoie les statistiques d'un club pour une saison choisie.
.. todo:: tenir compte de la saison.
"""
@ -172,8 +285,9 @@ def club_statistics(request, clubid):
gymnastsDict = {}
gymnasts = []
courseList = []
for course in courses:
nbtrainer = course.trainers.count()
list_of_gymnasts = Gymnast.objects.filter(to_gym__in=course.to_subgroup.all())
gymnasts.extend(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
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
courseList.append(
(
course,
nbtrainer,
nbgymnast,
hour,
counted,
totalHourForCourse,
totalHoursPaidForCourse,
)
)
for gymnast in list_of_gymnasts:
# print(gymnast)
if gymnast.id not in gymnastsDict:
@ -236,14 +320,10 @@ def club_statistics(request, clubid):
# print(str(gymnast) + ' : ' + str(nbattendance) + ' for ' + str(course) )
gymnastsDict[gymnast.id]["nbcoursebyweek"] += 1
gymnastsDict[gymnast.id]["nbhourbyweek"] += nbhour # timedelta
gymnastsDict[gymnast.id]["nbtraining"] += counted
gymnastsDict[gymnast.id]["nbattendance"] += nbattendance
gymnastsDict[gymnast.id]["nbhourtraining"] += totalHourForCourse
gymnastsDict[gymnast.id]["nbhourattendance"] += (
nbhour * nbattendance
) # timedelta
# print(gymnastsDict[gymnast.id])
@ -299,3 +379,4 @@ def club_statistics(request, clubid):
"totalHoursPaid": totalHoursPaid,
}
return context

View File

@ -62,8 +62,7 @@ class Gymnast(Markdownizable):
"""
class Meta:
verbose_name = "Gymnast"
verbose_name_plural = "Gymnasts"
verbose_name = "gymnaste"
ordering = ["user__last_name", "user__first_name"]
GENDER_CHOICES = ((0, "Male"), (1, "Female"))
@ -189,6 +188,12 @@ class Gymnast(Markdownizable):
period = pendulum.now() - pendulum.instance(self.created_at, "Europe/Brussels")
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):
"""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
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():
# @Fred : Comment tester cela ?

View File

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

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:
form = PlanningLineForm(
initial={
"gymnast": planningline.gymnast.id,
"gymnast": planningline.gymnast.id,
"gymnast_related": planningline.gymnast,
"date": planningline.date,
"order": planningline.order,