From 0aa314b4e1da753d24aa21d8649ace1128f155ea Mon Sep 17 00:00:00 2001 From: Gregory Trullemans Date: Mon, 20 May 2024 16:21:39 +0200 Subject: [PATCH] Add test and minor update. --- jarvis/followup/models.py | 148 ++++++++++---------------------- jarvis/followup/tests_models.py | 40 ++++++--- jarvis/followup/views_chrono.py | 37 ++------ 3 files changed, 79 insertions(+), 146 deletions(-) diff --git a/jarvis/followup/models.py b/jarvis/followup/models.py index a355c3c..3f6abcb 100644 --- a/jarvis/followup/models.py +++ b/jarvis/followup/models.py @@ -519,136 +519,74 @@ class Intensity(Markdownizable, Seasonisable): average_time_by_passe = models.DecimalField(max_digits=4, decimal_places=3) 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é. 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. 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 ( self.time_quality - + self.difficulty_quality - + self.quantity_of_skill_quality - + self.number_of_passes_quality - ) / 4 + + (self.quantity_of_skill_quality * 2) + + (self.number_of_passes_quality * 3) + + (self.difficulty_quality * 4) + ) / 10 def save(self, *args, **kwargs): - """Calcule les informations de qualité de l'intensité de entraînement et sauve les informations.""" - # self.average_time_by_skill = self.time / self.quantity_of_skill - self.time_quality = round((self.time / self.theorical_time) * 100, 3) - self.difficulty_quality = round( - (self.difficulty / self.difficulty_asked) * 100, 3 - ) - self.quantity_of_skill_quality = round( - (self.quantity_of_skill / self.quantity_of_skill_asked) * 100, 3 - ) - self.number_of_passes_quality = round( - (self.number_of_passes / self.number_of_passes_asked) * 100, 3 - ) - self.average_training_quality = round( - self.compute_average_training_quality(), 3 - ) - self.average_time_by_passe = round(self.time / self.number_of_passes, 3) + """Calculate quality metrics for training intensity and save the information.""" + + def calculate_quality(actual, expected): + """Helper function to calculate quality percentage.""" + if expected > 0: + return round((actual / expected) * 100, 3) + return 0 # Return 0 or some other appropriate value if expected is zero + + # Calculate quality metrics + self.time_quality = calculate_quality(self.time, self.theorical_time) + 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.number_of_passes_quality = calculate_quality(self.number_of_passes, self.number_of_passes_asked) + + # Calculate average training quality and time per pass + 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) 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 - # @property def passes_quality_for_gymnast(self): - """Calcule la qualité de passage pour un entraînement. On calcule le temps pour un gymnaste - en additionnant le passage théorique optimale d'un passage (90 secondes) et un temps de - 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. + """Calculate the quality of passes for a training session. This calculates the time for a gymnast + 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. - La qualité de passage représente donc le temps nécessaire pour que tous les gymnastes du - groupe ait fait chacun un passage. + The quality of passes represents the time needed for all gymnasts in the group to each make a pass. """ - optimal_time_by_gymnast = 90 - average_passe_time = ( - (optimal_time_by_gymnast + pow(100, 1 / self.number_of_gymnast)) - * self.number_of_gymnast - ) / 60 + if self.number_of_gymnast == 0: + return 0 # Handle case where there are no gymnasts to avoid division by zero + 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: return 1 - - if self.average_time_by_passe <= (average_passe_time * 1.05): + elif self.average_time_by_passe <= threshold_time: return 2 - - if self.average_time_by_passe >= (average_passe_time * 1.05): + else: 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 @property def average_time_by_skill(self): diff --git a/jarvis/followup/tests_models.py b/jarvis/followup/tests_models.py index 8ed1f51..de84188 100644 --- a/jarvis/followup/tests_models.py +++ b/jarvis/followup/tests_models.py @@ -17,6 +17,7 @@ from jarvis.followup.models import ( SeasonInformation, CompetitivePointsStats, ) +from datetime import date from jarvis.followup.models import ( CHRONO_TYPE_CHOICE, @@ -47,16 +48,16 @@ class TestModels(TestCase): expected_str = f"{gymnast} - 13.000 ({today} - 0)" self.assertEqual(str(chrono), expected_str, "The __str__ method does not return the expected string.") - # def test_chrono_timeline_representation(self): - # gymnast = Gymnast.objects.get(last_name="Pauchou") - # chrono = Chrono.objects.get(gymnast=gymnast) - # today = pendulum.now().date() - # self.assertEqual( - # chrono.timeline_representation, - # f"
  • {today.to_date_string()} - New personel best {CHRONO_TYPE_CHOICE[chrono.chrono_type][1]}: 15.000' (13.000')
  • ", # pylint: disable=line-too-long - # ) + def test_chrono_timeline_representation(self): + """Test the timeline_representation method to ensure it returns the correct HTML string.""" + gymnast = Gymnast.objects.get(last_name="Pauchou") + chrono = Chrono.objects.get(gymnast=gymnast) + today = pendulum.now().date() - def test_compute_tof(self): + expected_html = f"
  • {today:%d %b %Y} - New personel best 10 |: 15.000' (13.000')
  • " + 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) self.assertEqual(res, 13) @@ -66,7 +67,7 @@ class TestModels(TestCase): today = pendulum.now().date() 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.""" gymnast = Gymnast.objects.get(last_name="Pauchou") well_being = WellBeing.objects.get(gymnast=gymnast) @@ -74,7 +75,7 @@ class TestModels(TestCase): inversed_stress = well_being.get_inversed_stress 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.""" gymnast = Gymnast.objects.get(last_name="Pauchou") well_being = WellBeing.objects.get(gymnast=gymnast) @@ -82,10 +83,25 @@ class TestModels(TestCase): inversed_fatigue = well_being.get_inversed_fatigue 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.""" gymnast = Gymnast.objects.get(last_name="Pauchou") well_being = WellBeing.objects.get(gymnast=gymnast) 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") + + 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.") + + diff --git a/jarvis/followup/views_chrono.py b/jarvis/followup/views_chrono.py index 3961072..daaa204 100644 --- a/jarvis/followup/views_chrono.py +++ b/jarvis/followup/views_chrono.py @@ -143,7 +143,7 @@ def average_jump_chrono_details_for_gymnast( ) - +@login_required @require_http_methods(["POST"]) def remove_jump_chrono_value(request): """ @@ -152,25 +152,19 @@ def remove_jump_chrono_value(request): chrono_id = request.POST.get("chrono_id") order = request.POST.get("order") - print("delete jump chrono") - - # Validate required parameters if not chrono_id or not order: 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) - - # Attempt to delete the specified ChronoDetails record deleted, _ = ChronoDetails.objects.filter(chrono=chrono, order=order).delete() - # Check if any records were 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"]) def add_jump_chrono_value(request): """ @@ -180,25 +174,18 @@ def add_jump_chrono_value(request): order = request.POST.get("order") value = request.POST.get("value") - # Validate required parameters if not chrono_id or not order or value is None: 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) - - # Attempt to create a new ChronoDetails record row, created = ChronoDetails.objects.get_or_create( - # chrono=chrono, order=order, defaults={'value': value} - chrono=chrono, order=order, value=value + chrono=chrono, order=order, defaults={'value': value} ) - # Check if the record was created or just retrieved if created: - return HttpResponse(201, f"New chrono detail added: {row}") # 201 Created - else: - # If the record was not created, it means it already exists with the same order and chrono - return HttpResponse(409, f"Chrono detail already exists: {row}") # 409 Conflict + return HttpResponse(status=201) + + return HttpResponse(status=409) # 409 Conflict @login_required @@ -294,10 +281,8 @@ def get_chrono_detail_distinct_season(request, gymnast_id): Args: gymnast_id (int) Identifiant d'un gymnaste """ - # Ensure the gymnast exists get_object_or_404(Gymnast, pk=gymnast_id) - # Directly query the Chrono model for distinct seasons season_list = list( Chrono.objects.filter(gymnast_id=gymnast_id) .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 season (string) Season """ - # Ensure the gymnast exists get_object_or_404(Gymnast, pk=gymnast_id) - # Directly query the Chrono model for distinct week numbers in a specific season weeknumber_list = list( Chrono.objects.filter(gymnast_id=gymnast_id, season=season) .values_list("week_number", flat=True) @@ -340,8 +323,6 @@ def get_average_jump_chrono_details_for_season_and_week( season (string) Season week_number (int) Number of week """ - - # Optimize query by directly annotating and ordering in a single query stat_values = ChronoDetails.objects.filter( chrono__gymnast=gymnast_id, chrono__chrono_type=routine_type, @@ -349,7 +330,6 @@ def get_average_jump_chrono_details_for_season_and_week( chrono__week_number=week_number, ).values("order").annotate(avg_score=Avg("value")).order_by("order") - # Convert QuerySet to list for JSON serialization stat_values_list = list(stat_values) return JsonResponse(stat_values_list, safe=False) @@ -368,7 +348,6 @@ def average_jump_chrono_details_for_season_and_week( season (string) Season week_number (int) Numero de la semaine """ - gymnast = get_object_or_404(Gymnast, pk=gymnast_id) stat_values = (