Add test and minor update.

This commit is contained in:
Gregory Trullemans 2024-05-20 16:21:39 +02:00
parent 2f1a0365e3
commit 0aa314b4e1
3 changed files with 79 additions and 146 deletions

View File

@ -519,136 +519,74 @@ class Intensity(Markdownizable, Seasonisable):
average_time_by_passe = models.DecimalField(max_digits=4, decimal_places=3) average_time_by_passe = models.DecimalField(max_digits=4, decimal_places=3)
def compute_average_training_quality(self): def compute_average_training_quality(self):
""" """Calcul de la qualité d'un entrainement sur base des 4 données pratiques encodées : Temps,
Calcul de la qualité d'un entrainement sur base des 4 données pratiques encodées : Temps,
# de passage, # de saut et Difficulté. # de passage, # de saut et Difficulté.
Si les 4 données pratiques sont inférieures ou égales aux données théoriques, une moyenne Si les 4 données pratiques sont inférieures ou égales aux données théoriques, une moyenne
pondérée (D*4, p*3, S*2 et T) est calculée. pondérée (D*4, p*3, S*2 et T) est calculée.
Si une (ou plusieurs) données pratiques sont supérieures aux données théorique... ? Si une (ou plusieurs) données pratiques sont supérieures aux données théorique... ?
Pour les cas non traités, une moyenne arithmétique est calculée.
TODO:
- trouver un calcul d'efficacité qui tienne compte des statistiques (notamment le temps par
passage)
""" """
# Si les 4 données pratiques sont inférieures ou égales aux données théoriques, une moyenne
# pondérée (D*4, p*3, S*2 et T) est calculée.
if (
self.time <= self.theorical_time
and self.number_of_passes <= self.number_of_passes_asked
and self.difficulty <= self.difficulty_asked
and self.quantity_of_skill <= self.quantity_of_skill_asked
):
return (
self.time_quality
+ (self.quantity_of_skill_quality * 2)
+ (self.number_of_passes_quality * 3)
+ (self.difficulty_quality * 4)
) / 10
# if self.difficulty > self.difficulty_asked:
# if (
# self.time <= self.theorical_time
# and self.number_of_passes <= self.number_of_passes_asked
# and self.quantity_of_skill <= self.quantity_of_skill_asked
# ):
# return self.difficulty_quality
if (
self.time <= self.theorical_time
and self.number_of_passes <= self.number_of_passes_asked
):
if (
self.difficulty >= self.difficulty_asked
and self.quantity_of_skill >= self.quantity_of_skill_asked
):
return (
(self.difficulty_quality * 2) + self.quantity_of_skill_quality
) / 3
# Pour les cas non traités, une moyenne arithmétique est calculée.
return ( return (
self.time_quality self.time_quality
+ self.difficulty_quality + (self.quantity_of_skill_quality * 2)
+ self.quantity_of_skill_quality + (self.number_of_passes_quality * 3)
+ self.number_of_passes_quality + (self.difficulty_quality * 4)
) / 4 ) / 10
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Calcule les informations de qualité de l'intensité de entraînement et sauve les informations.""" """Calculate quality metrics for training intensity and save the information."""
# self.average_time_by_skill = self.time / self.quantity_of_skill
self.time_quality = round((self.time / self.theorical_time) * 100, 3) def calculate_quality(actual, expected):
self.difficulty_quality = round( """Helper function to calculate quality percentage."""
(self.difficulty / self.difficulty_asked) * 100, 3 if expected > 0:
) return round((actual / expected) * 100, 3)
self.quantity_of_skill_quality = round( return 0 # Return 0 or some other appropriate value if expected is zero
(self.quantity_of_skill / self.quantity_of_skill_asked) * 100, 3
) # Calculate quality metrics
self.number_of_passes_quality = round( self.time_quality = calculate_quality(self.time, self.theorical_time)
(self.number_of_passes / self.number_of_passes_asked) * 100, 3 self.difficulty_quality = calculate_quality(self.difficulty, self.difficulty_asked)
) self.quantity_of_skill_quality = calculate_quality(self.quantity_of_skill, self.quantity_of_skill_asked)
self.average_training_quality = round( self.number_of_passes_quality = calculate_quality(self.number_of_passes, self.number_of_passes_asked)
self.compute_average_training_quality(), 3
) # Calculate average training quality and time per pass
self.average_time_by_passe = round(self.time / self.number_of_passes, 3) self.average_training_quality = round(self.compute_average_training_quality(), 3)
if self.number_of_passes > 0:
self.average_time_by_passe = round(self.time / self.number_of_passes, 3)
else:
self.average_time_by_passe = 0 # Handle zero passes appropriately even if it should not append.
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): def __str__(self):
return f"{self.gymnast} - {self.date} : {self.time_quality} - {self.difficulty_quality} - {self.quantity_of_skill_quality} - {self.number_of_passes_quality} - {self.average_training_quality} - {self.average_time_by_passe}" # pylint: disable=line-too-long return f"{self.gymnast} - {self.date} : {self.time_quality} - {self.difficulty_quality} - {self.quantity_of_skill_quality} - {self.number_of_passes_quality} - {self.average_training_quality} - {self.average_time_by_passe}" # pylint: disable=line-too-long
#
@property @property
def passes_quality_for_gymnast(self): def passes_quality_for_gymnast(self):
"""Calcule la qualité de passage pour un entraînement. On calcule le temps pour un gymnaste """Calculate the quality of passes for a training session. This calculates the time for a gymnast
en additionnant le passage théorique optimale d'un passage (90 secondes) et un temps de by adding the optimal theoretical time for a pass (90 seconds) and a operational time (for getting on and off, communicating, etc.) (calculated as the cube root of the number of gymnasts), multiplied by the number of gymnasts in the group, and then converted from seconds to minutes.
fonctionnement (pour monter, descendre, communiquer, ) équivalent racine cubique du nombre
de gymnaste) ; le tout multiplié par le nombre de gymnaste du groupe. Le tout calculé en
seconde puis ramener en minute.
La qualité de passage représente donc le temps nécessaire pour que tous les gymnastes du The quality of passes represents the time needed for all gymnasts in the group to each make a pass.
groupe ait fait chacun un passage.
""" """
optimal_time_by_gymnast = 90 if self.number_of_gymnast == 0:
average_passe_time = ( return 0 # Handle case where there are no gymnasts to avoid division by zero
(optimal_time_by_gymnast + pow(100, 1 / self.number_of_gymnast))
* self.number_of_gymnast
) / 60
optimal_time_by_gymnast = 90
operational_time = pow(100, 1 / self.number_of_gymnast)
total_time_per_gymnast = optimal_time_by_gymnast + operational_time
total_group_time_seconds = total_time_per_gymnast * self.number_of_gymnast
average_passe_time = total_group_time_seconds / 60
# Calculate the threshold for a 5% increase
threshold_time = average_passe_time * 1.05
# Determine the quality based on the average time per pass
if self.average_time_by_passe <= average_passe_time: if self.average_time_by_passe <= average_passe_time:
return 1 return 1
elif self.average_time_by_passe <= threshold_time:
if self.average_time_by_passe <= (average_passe_time * 1.05):
return 2 return 2
else:
if self.average_time_by_passe >= (average_passe_time * 1.05):
return 3 return 3
# Theorical statistics
# @property
# def average_time_by_passe_theorical(self):
# return self.theorical_time / self.number_of_passes_asked
# @property
# def average_quantity_of_skill_by_time_theorical(self):
# return self.quantity_of_skill_asked / self.theorical_time
# @property
# def average_time_by_skill_theorical(self):
# return self.theorical_time / self.quantity_of_skill_asked
# @property
# def average_difficulty_by_passe_theorical(self):
# return self.difficulty_asked / self.number_of_passes_asked
# @property
# def average_quantity_of_skill_by_passe_theorical(self):
# return self.quantity_of_skill_asked / self.number_of_passes_asked
# @property
# def average_difficulty_by_skill_theorical(self):
# return self.difficulty_asked / self.quantity_of_skill_asked
# Real statistics # Real statistics
@property @property
def average_time_by_skill(self): def average_time_by_skill(self):

View File

@ -17,6 +17,7 @@ from jarvis.followup.models import (
SeasonInformation, SeasonInformation,
CompetitivePointsStats, CompetitivePointsStats,
) )
from datetime import date
from jarvis.followup.models import ( from jarvis.followup.models import (
CHRONO_TYPE_CHOICE, CHRONO_TYPE_CHOICE,
@ -47,16 +48,16 @@ class TestModels(TestCase):
expected_str = f"{gymnast} - 13.000 ({today} - 0)" expected_str = f"{gymnast} - 13.000 ({today} - 0)"
self.assertEqual(str(chrono), expected_str, "The __str__ method does not return the expected string.") self.assertEqual(str(chrono), expected_str, "The __str__ method does not return the expected string.")
# def test_chrono_timeline_representation(self): def test_chrono_timeline_representation(self):
# gymnast = Gymnast.objects.get(last_name="Pauchou") """Test the timeline_representation method to ensure it returns the correct HTML string."""
# chrono = Chrono.objects.get(gymnast=gymnast) gymnast = Gymnast.objects.get(last_name="Pauchou")
# today = pendulum.now().date() chrono = Chrono.objects.get(gymnast=gymnast)
# self.assertEqual( today = pendulum.now().date()
# chrono.timeline_representation,
# f"<li>{today.to_date_string()} - New personel best {CHRONO_TYPE_CHOICE[chrono.chrono_type][1]}: 15.000' (13.000')</li>", # pylint: disable=line-too-long
# )
def test_compute_tof(self): expected_html = f"<li>{today:%d %b %Y} - New personel best 10 |: 15.000' (13.000')</li>"
self.assertEqual(chrono.timeline_representation(), expected_html, "The timeline_representation method does not return the expected HTML string.")
def test_chrono_compute_tof(self):
res = Chrono.compute_tof(15) res = Chrono.compute_tof(15)
self.assertEqual(res, 13) self.assertEqual(res, 13)
@ -66,7 +67,7 @@ class TestModels(TestCase):
today = pendulum.now().date() today = pendulum.now().date()
self.assertEqual(str(injury), f"Fred Pauchou ({today})") self.assertEqual(str(injury), f"Fred Pauchou ({today})")
def test_get_inversed_stress(self): def test_wellbeing_get_inversed_stress(self):
"""Test the get_inversed_stress method to ensure it correctly calculates the inversed stress.""" """Test the get_inversed_stress method to ensure it correctly calculates the inversed stress."""
gymnast = Gymnast.objects.get(last_name="Pauchou") gymnast = Gymnast.objects.get(last_name="Pauchou")
well_being = WellBeing.objects.get(gymnast=gymnast) well_being = WellBeing.objects.get(gymnast=gymnast)
@ -74,7 +75,7 @@ class TestModels(TestCase):
inversed_stress = well_being.get_inversed_stress inversed_stress = well_being.get_inversed_stress
self.assertEqual(inversed_stress, 3, "The inversed stress should be 3 for a stress value of 7") self.assertEqual(inversed_stress, 3, "The inversed stress should be 3 for a stress value of 7")
def test_get_inversed_fatigue(self): def test_wellbeing_get_inversed_fatigue(self):
"""Test the get_inversed_fatigue property to ensure it correctly calculates the inversed fatigue.""" """Test the get_inversed_fatigue property to ensure it correctly calculates the inversed fatigue."""
gymnast = Gymnast.objects.get(last_name="Pauchou") gymnast = Gymnast.objects.get(last_name="Pauchou")
well_being = WellBeing.objects.get(gymnast=gymnast) well_being = WellBeing.objects.get(gymnast=gymnast)
@ -82,10 +83,25 @@ class TestModels(TestCase):
inversed_fatigue = well_being.get_inversed_fatigue inversed_fatigue = well_being.get_inversed_fatigue
self.assertEqual(inversed_fatigue, 4, "The inversed fatigue should be 4 for a fatigue value of 6") self.assertEqual(inversed_fatigue, 4, "The inversed fatigue should be 4 for a fatigue value of 6")
def test_get_inversed_muscle_soreness(self): def test_wellbeing_get_inversed_muscle_soreness(self):
"""Test the get_inversed_muscle_soreness property to ensure it correctly calculates the inversed muscle soreness.""" """Test the get_inversed_muscle_soreness property to ensure it correctly calculates the inversed muscle soreness."""
gymnast = Gymnast.objects.get(last_name="Pauchou") gymnast = Gymnast.objects.get(last_name="Pauchou")
well_being = WellBeing.objects.get(gymnast=gymnast) well_being = WellBeing.objects.get(gymnast=gymnast)
inversed_muscle_soreness = well_being.get_inversed_muscle_soreness inversed_muscle_soreness = well_being.get_inversed_muscle_soreness
self.assertEqual(inversed_muscle_soreness, 5, "The inversed muscle soreness should be 5 for a muscle soreness value of 5") self.assertEqual(inversed_muscle_soreness, 5, "The inversed muscle soreness should be 5 for a muscle soreness value of 5")
def test_heightweight_bmi_calculation(self):
"""Test the bmi method to ensure it correctly calculates the BMI."""
gymnast = Gymnast.objects.get(last_name="Pauchou")
heightweight = HeightWeight(
gymnast=gymnast,
height=180,
weight=75
)
heightweight.save()
expected_bmi = 75 / (1.8 ** 2)
self.assertAlmostEqual(heightweight.bmi, expected_bmi, places=2, msg="The bmi method does not return the expected BMI.")

View File

@ -143,7 +143,7 @@ def average_jump_chrono_details_for_gymnast(
) )
@login_required
@require_http_methods(["POST"]) @require_http_methods(["POST"])
def remove_jump_chrono_value(request): def remove_jump_chrono_value(request):
""" """
@ -152,25 +152,19 @@ def remove_jump_chrono_value(request):
chrono_id = request.POST.get("chrono_id") chrono_id = request.POST.get("chrono_id")
order = request.POST.get("order") order = request.POST.get("order")
print("delete jump chrono")
# Validate required parameters
if not chrono_id or not order: if not chrono_id or not order:
return HttpResponse(400, "Missing required parameters.") return HttpResponse(400, "Missing required parameters.")
# Retrieve the Chrono object or return 404 if not found
chrono = get_object_or_404(Chrono, pk=chrono_id) chrono = get_object_or_404(Chrono, pk=chrono_id)
# Attempt to delete the specified ChronoDetails record
deleted, _ = ChronoDetails.objects.filter(chrono=chrono, order=order).delete() deleted, _ = ChronoDetails.objects.filter(chrono=chrono, order=order).delete()
# Check if any records were deleted
if deleted: if deleted:
return HttpResponse(200) # OK status, deletion successful return HttpResponse(status=200)
return HttpResponse(404, "Chrono detail not found.") # Not found status if no records were deleted return HttpResponse(status=404)
@login_required
@require_http_methods(["POST"]) @require_http_methods(["POST"])
def add_jump_chrono_value(request): def add_jump_chrono_value(request):
""" """
@ -180,25 +174,18 @@ def add_jump_chrono_value(request):
order = request.POST.get("order") order = request.POST.get("order")
value = request.POST.get("value") value = request.POST.get("value")
# Validate required parameters
if not chrono_id or not order or value is None: if not chrono_id or not order or value is None:
return HttpResponse(400, "Missing required parameters.") return HttpResponse(400, "Missing required parameters.")
# Retrieve the Chrono object or return 404 if not found
chrono = get_object_or_404(Chrono, pk=chrono_id) chrono = get_object_or_404(Chrono, pk=chrono_id)
# Attempt to create a new ChronoDetails record
row, created = ChronoDetails.objects.get_or_create( row, created = ChronoDetails.objects.get_or_create(
# chrono=chrono, order=order, defaults={'value': value} chrono=chrono, order=order, defaults={'value': value}
chrono=chrono, order=order, value=value
) )
# Check if the record was created or just retrieved
if created: if created:
return HttpResponse(201, f"New chrono detail added: {row}") # 201 Created return HttpResponse(status=201)
else:
# If the record was not created, it means it already exists with the same order and chrono return HttpResponse(status=409) # 409 Conflict
return HttpResponse(409, f"Chrono detail already exists: {row}") # 409 Conflict
@login_required @login_required
@ -294,10 +281,8 @@ def get_chrono_detail_distinct_season(request, gymnast_id):
Args: Args:
gymnast_id (int) Identifiant d'un gymnaste gymnast_id (int) Identifiant d'un gymnaste
""" """
# Ensure the gymnast exists
get_object_or_404(Gymnast, pk=gymnast_id) get_object_or_404(Gymnast, pk=gymnast_id)
# Directly query the Chrono model for distinct seasons
season_list = list( season_list = list(
Chrono.objects.filter(gymnast_id=gymnast_id) Chrono.objects.filter(gymnast_id=gymnast_id)
.values_list("season", flat=True) .values_list("season", flat=True)
@ -315,10 +300,8 @@ def get_chrono_detail_distinct_weeknumber_for_season(request, gymnast_id, season
gymnast_id (int) Identifiant d'un gymnaste gymnast_id (int) Identifiant d'un gymnaste
season (string) Season season (string) Season
""" """
# Ensure the gymnast exists
get_object_or_404(Gymnast, pk=gymnast_id) get_object_or_404(Gymnast, pk=gymnast_id)
# Directly query the Chrono model for distinct week numbers in a specific season
weeknumber_list = list( weeknumber_list = list(
Chrono.objects.filter(gymnast_id=gymnast_id, season=season) Chrono.objects.filter(gymnast_id=gymnast_id, season=season)
.values_list("week_number", flat=True) .values_list("week_number", flat=True)
@ -340,8 +323,6 @@ def get_average_jump_chrono_details_for_season_and_week(
season (string) Season season (string) Season
week_number (int) Number of week week_number (int) Number of week
""" """
# Optimize query by directly annotating and ordering in a single query
stat_values = ChronoDetails.objects.filter( stat_values = ChronoDetails.objects.filter(
chrono__gymnast=gymnast_id, chrono__gymnast=gymnast_id,
chrono__chrono_type=routine_type, chrono__chrono_type=routine_type,
@ -349,7 +330,6 @@ def get_average_jump_chrono_details_for_season_and_week(
chrono__week_number=week_number, chrono__week_number=week_number,
).values("order").annotate(avg_score=Avg("value")).order_by("order") ).values("order").annotate(avg_score=Avg("value")).order_by("order")
# Convert QuerySet to list for JSON serialization
stat_values_list = list(stat_values) stat_values_list = list(stat_values)
return JsonResponse(stat_values_list, safe=False) return JsonResponse(stat_values_list, safe=False)
@ -368,7 +348,6 @@ def average_jump_chrono_details_for_season_and_week(
season (string) Season season (string) Season
week_number (int) Numero de la semaine week_number (int) Numero de la semaine
""" """
gymnast = get_object_or_404(Gymnast, pk=gymnast_id) gymnast = get_object_or_404(Gymnast, pk=gymnast_id)
stat_values = ( stat_values = (