Compare commits

...

2 Commits

Author SHA1 Message Date
Gregory Trullemans 291b9db321 Remove Passe, TrainingPasse, … models 2024-03-25 13:10:29 +01:00
Gregory Trullemans e57e414f27 update dashboard 2024-03-21 17:47:01 +01:00
10 changed files with 55 additions and 689 deletions

View File

@ -27,7 +27,7 @@
<h4 class=""><i class="text-primary fal fa-laugh-wink"></i> Hi {{ user.first_name }} !</h4> <h4 class=""><i class="text-primary fal fa-laugh-wink"></i> Hi {{ user.first_name }} !</h4>
</div> </div>
<div class="card-body text-justify pt-0"> <div class="card-body text-justify pt-0">
<p>Welcome to Jarvi v0.96.1 <span class="text-muted">(last update : 13-2-2024)</span></p> <p>Welcome to Jarvi v0.96.3 <span class="text-muted">(last update : 21-3-2024)</span></p>
<p>This application is here to help coaches to manage the gymnasts (evolution, evaluation, routines, scores, …). This tool is not perfect so feel free to make improvement proposals, bug reports, … by sending me an <a href="mailto:gregory@flyingacrobaticstrampoline.be">email</a>.</p> <p>This application is here to help coaches to manage the gymnasts (evolution, evaluation, routines, scores, …). This tool is not perfect so feel free to make improvement proposals, bug reports, … by sending me an <a href="mailto:gregory@flyingacrobaticstrampoline.be">email</a>.</p>
<p>You can find the user manual <a href="{% static "files/Manuel_Utilisateur.pdf" %}" download>here (in french)</a>.</p> <p>You can find the user manual <a href="{% static "files/Manuel_Utilisateur.pdf" %}" download>here (in french)</a>.</p>
</div> </div>

View File

@ -9,10 +9,8 @@ from django_admin_listfilter_dropdown.filters import (
from .models import ( from .models import (
TouchPosition, TouchPosition,
Skill, Skill,
Passe,
Routine, Routine,
RoutineSkill, RoutineSkill,
TrainingProgram,
PrerequisiteClosure, PrerequisiteClosure,
) )
@ -192,59 +190,3 @@ class PrerequisiteClosureAdmin(admin.ModelAdmin):
("level", DropdownFilter), ("level", DropdownFilter),
("path", DropdownFilter), ("path", DropdownFilter),
) )
@admin.register(Passe)
class PasseAdmin(admin.ModelAdmin):
model = Passe
fields = (
"label",
"educatives",
"regexp",
"number_of_skill",
"difficulty",
"informations",
)
list_display = ("label", "regexp", "number_of_skill", "difficulty")
list_filter = (
("number_of_skill", DropdownFilter),
("difficulty", DropdownFilter),
)
filter_horizontal = ("educatives",)
class Media:
js = (
"js/core/jquery-3.6.0.min.js",
"js/admin/passe.js",
)
@admin.register(TrainingProgram)
class TrainingProgramAdmin(admin.ModelAdmin):
model = TrainingProgram
fields = (
"gymnast",
"date",
"rank",
"passe",
"repetition",
"number_of_skill",
"difficulty",
# "score",
)
list_display = (
"date",
"gymnast",
"passe",
"rank",
"repetition",
"number_of_skill",
"difficulty",
)
list_filter = (
("gymnast", RelatedDropdownFilter),
("date", DropdownFilter),
)

View File

@ -1,15 +1,14 @@
import re
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
import re
from .models import ( from .models import (
Educative, Educative,
Skill, Skill,
Routine, Routine,
RoutineSkill, RoutineSkill,
Passe,
) )
@ -72,89 +71,3 @@ class CombinationSkillForm(forms.ModelForm):
"skill": forms.HiddenInput(), "skill": forms.HiddenInput(),
"rank": forms.NumberInput(), "rank": forms.NumberInput(),
} }
class PasseForm(forms.ModelForm):
educatives = forms.ModelMultipleChoiceField(
required=False,
queryset=Educative.objects.all(),
widget=FilteredSelectMultiple("Educatives", is_stacked=False),
# widget=customFilteredSelectMultiple(verbose_name='test2',is_stacked=False)
)
class Media:
css = {
"all": ["admin/css/widgets.css"],
}
# Adding this javascript is crucial
js = ["/admin/jsi18n/"]
class Meta:
model = Passe
fields = ("label", "regexp", "educatives", "informations")
widgets = {
"label": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Label (not mandatory)",
"maxlength": 30,
}
),
"regexp": forms.TextInput(
attrs={"class": "form-control", "placeholder": "[2-8]"}
),
"informations": forms.Textarea(
attrs={
"class": "form-control",
"placeholder": "Informations about the passe…", # pylint: disable=line-too-long
}
),
}
def clean_regexp(self):
"""Vérifie que la regexp entrée par l'utilisateur est valide."""
regexp = self.cleaned_data["regexp"]
if not Passe.is_valid_regexp(regexp):
raise ValidationError("Entered regexp not valid.")
return regexp
def clean(self):
"""Vérifie le contenu des champs `educatives` par rapport à la valeur de `regexp`. Si
`regexp` est définie par :
- valeurs de ROUTINE_TYPE_CHOICE il faut que Educatives soit VIDE
- avec [x-y]
- [x-y] " Educatives soit NON VIDE
- WC " il y ait 2+ Educatives
- x| " il y ait 1! Educatives
"""
cleaned_data = super().clean()
regexp = ["regexp"]
if regexp is not None:
arguments = regexp.split(" ")
educatives = cleaned_data["educatives"]
if Passe.is_valid_routine_type(arguments[0]) and educatives is not None:
raise ValidationError(
"Educatives must be empty with the entered Regexp."
)
if Passe.is_valid_subset(arguments[0]) and educatives is None:
raise ValidationError(
"Educatives can't be empty with the entered Regexp."
)
if re.match(r"[1-9]+\|", arguments[0]) and len(educatives) != 1:
raise ValidationError(
"One and only one Educatives allowed with the entered Regexp."
)
if arguments[0] == "WC" and (educatives is None or len(educatives) < 2):
raise ValidationError(
"At least two Educatives with the entered Regexp."
)
return cleaned_data

View File

@ -0,0 +1,31 @@
# Generated by Django 4.2 on 2024-03-25 11:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("objective", "0021_alter_passe_educatives"),
]
operations = [
migrations.AlterUniqueTogether(
name="trainingprogram",
unique_together=None,
),
migrations.RemoveField(
model_name="trainingprogram",
name="gymnast",
),
migrations.RemoveField(
model_name="trainingprogram",
name="passe",
),
migrations.DeleteModel(
name="Passe",
),
migrations.DeleteModel(
name="TrainingProgram",
),
]

View File

@ -464,301 +464,3 @@ class RoutineSkill(models.Model):
def __str__(self): def __str__(self):
return f"{self.rank} - {self.routine.short_label} : {self.skill.short_label}" return f"{self.rank} - {self.routine.short_label} : {self.skill.short_label}"
class Passe(Markdownizable):
"""Classe représentant les passages (à faire pendant un entraînement)."""
class Meta:
ordering = [
"label",
]
label = models.CharField(max_length=30)
educatives = models.ManyToManyField(
Educative, related_name="passes", blank=True, symmetrical=False
)
regexp = models.CharField(max_length=50, null=True, blank=True)
number_of_skill = models.PositiveSmallIntegerField(default=0)
difficulty = models.DecimalField(max_digits=4, decimal_places=1, default=0.0)
# TODO: number_of_skill doit être calculé correctement dans tous les cas.
def save(self, *args, **kwargs):
"""Sauve les informations de la personne et initialise les champs nettoyés.
On part du principe que self.regexp est correct.
"""
self.difficulty = 0
self.number_of_skill = 0
super().save(*args, **kwargs)
# print("Dans le save")
if self.educatives.count() == 0:
# print("educative is none")
present = False
operation_list = self.regexp.split(" ")
for item in ROUTINE_TYPE_CHOICE:
if item[1] == operation_list[0]:
present = True
break
if present and len(operation_list) == 2:
# print("present")
content = operation_list[1].replace("[", "").replace("]", "")
ranks = content.split("-")
if ranks[0] == "":
self.number_of_skill += int(ranks[1])
elif ranks[1] == "":
self.number_of_skill += (10 - int(ranks[0])) + 1
else:
self.number_of_skill += (int(ranks[1]) - int(ranks[0])) + 1
else:
self.number_of_skill += 10
else:
for educative in self.educatives.all():
is_skill = False
try:
educative = Routine.objects.get(pk=educative)
except Routine.DoesNotExist:
educative = Skill.objects.get(pk=educative)
is_skill = True
if is_skill:
self.difficulty += educative.difficulty
self.number_of_skill += 1
else:
if self.regexp is not None:
regexp = self.regexp.replace("[", "").replace("]", "")
position = regexp.find("-")
start = regexp[:position]
if start == "":
start = 0
else:
start = int(start)
end = regexp[position + 1 :]
if end == "":
end = educative.jumps.all().count()
else:
end = int(end)
self.number_of_skill += end - (start - 1)
list_of_skill = educative.skill_links.filter(
rank__gte=start, rank__lte=end
)
# .aggregate(total=Sum("value"))
tmp_difficulty = 0
for routine_skill in list_of_skill:
tmp_difficulty += routine_skill.skill.difficulty
self.difficulty += tmp_difficulty
else:
self.number_of_skill += educative.jumps.all().count()
self.difficulty += educative.difficulty
super().save(*args, **kwargs)
def __str__(self):
return f"{self.label} ({self.number_of_skill} | {self.difficulty})"
@staticmethod
def is_valid_regexp_one_arg(arg):
"""Vérifie une regexp avec un paramètre."""
if arg == "WC":
return True
if re.match(r"[1-9]+\|", arg):
return True
if not Passe.is_valid_dot(arg):
return False
value = arg.replace(".", "")
is_valid_routine = Passe.is_valid_routine_type(value)
if is_valid_routine:
return True
return Passe.is_valid_subset(arg)
@staticmethod
def is_valid_regexp_two_args(arg1, arg2):
"""Vérifie une regexp avec deux paramètres."""
if not Passe.is_valid_dot(arg1):
return False
value = arg1.replace(".", "")
is_valid_routine = Passe.is_valid_routine_type(value)
if is_valid_routine:
return Passe.is_valid_subset(arg2)
return False
@staticmethod
def is_valid_dot(pattern):
"""Reçoit une chaine de caratère et vérifie que si elle contient un point (.), il se trouve
soit à la première position soit à la dernière position.
"""
if len(re.findall("\.", pattern)) > 1:
return False
if re.search("\.", pattern):
last_place = len(pattern) - 1
if pattern[0] != "." and pattern[last_place] != ".":
return False
return True
@staticmethod
def is_valid_routine_type(routine_type):
"""Recoit une chaine de caractère et vérifie si elle est présente dans la liste
ROUTINE_TYPE_CHOICE (Educative vide !)
"""
is_valid = False
for item in ROUTINE_TYPE_CHOICE:
if item[1] == routine_type:
is_valid = True
break
return is_valid
@staticmethod
def is_valid_subset(subset):
"""Reçoit la description d'un subset sous forme de string et vérifie qu'elle est conforme.
Format attendu : [X-Y]
X ou Y peuvent être vide mais pas en même temps.
X est un entier >= 2
Y est un entier >= 2 OU Y > X si X est non vide
Exemples :
- [2-8] True
- [-5] True
- [3-] True
- [8-2] False
- [4--8] False
- [-] False
- [1-] False
- [-1] False
- [4] False
- [6-6] False
"""
if re.match(r"^\[(([2-9]+\-{1})|([2-9]+\-{1}[2-9]+)|(\-{1}[2-9]+))\]$", subset):
value = subset.replace("[", "").replace("]", "")
ranks = value.split("-")
if ranks[0] == "" or ranks[1] == "":
return True
if int(ranks[0]) < int(ranks[1]):
return True
return False
@staticmethod
def is_valid_regexp(regexp):
"""Vérifie le champ regexp
Exemples :
- Q1R1 True
- Q1R2 [2-8] True
- Q2R1 [-5] True
- SF [6-] True
- FS [3-7] True
- Q1R1. True
- .Q1R2 True
- Q1R1. [-4] True
- .Q1R2 [4-] True
- .FS [3-7] True
- [2-8] True
- [-5] True
- WC True
- 1| True
"""
argument_list = regexp.split(" ")
if len(argument_list) >= 3:
return False
if len(argument_list) == 2:
return Passe.is_valid_regexp_two_args(argument_list[0], argument_list[1])
else:
return Passe.is_valid_regexp_one_arg(argument_list[0])
return False
class TrainingProgram(Seasonisable, Markdownizable):
"""Classe représentant ?????
TODO:
- renommer (supprimer/remettre) TrainingProgram en TrainingPasse
- supprimer Seasonisable
- supprimer Markdownizable
- supprimer le champ Gymnast
- supprimer score
- supprimer rank ??? (--> dans la M2M)
- supprimer difficulty ??? (--> dans la M2M)
"""
class Meta:
verbose_name = "Training Program"
verbose_name_plural = "Trainings Programs"
ordering = [
"rank",
]
unique_together = ["date", "gymnast", "rank"]
gymnast = models.ForeignKey("people.Gymnast", on_delete=models.CASCADE) # TO DELETE
passe = models.ForeignKey(Passe, on_delete=models.CASCADE)
repetition = models.PositiveSmallIntegerField(default=1)
number_of_skill = models.PositiveSmallIntegerField(default=0)
difficulty = models.DecimalField(max_digits=4, decimal_places=1, default=0.0)
rank = models.PositiveSmallIntegerField(default=1) # TO DELETE
score = models.PositiveSmallIntegerField(blank=True, null=True) # TO DELETE
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return (
f"{self.gymnast} {self.date} - {self.rank} : {self.passe} {self.repetition}"
)
def save(self, *args, **kwargs):
"""Sauve les informations de la personne et initialise les champs nettoyés."""
super().save(*args, **kwargs)
self.difficulty = self.passe.difficulty * self.repetition
self.number_of_skill = self.passe.number_of_skill * self.repetition
super().save(*args, **kwargs)
# class TrainingPasseLink(models.Model):
# """Modèle M2M entre Training et TrainingPasse."""
# training = models.ForeignKey(
# Training,
# on_delete=models.CASCADE,
# default=None,
# related_name="program_passe_links",
# )
# program_passe = models.ForeignKey(
# TrainingPasse,
# on_delete=models.CASCADE,
# default=None,
# related_name="training_links",
# )
# rank = models.PositiveSmallIntegerField(default=1)
# number_of_skill = models.PositiveSmallIntegerField(default=0)
# difficulty = models.DecimalField(max_digits=4, decimal_places=1, default=0.0)
# score = models.PositiveSmallIntegerField(blank=True, null=True)
# class Training(Seasonisable, Markdownizable):
# """Classe représentant un entraînement."""
# gymnast = models.ForeignKey("people.Gymnast", on_delete=models.CASCADE)
# difficulty = models.DecimalField(max_digits=4, decimal_places=1, default=0.0)
# number_of_skill = models.PositiveSmallIntegerField(default=0)
# score = models.PositiveSmallIntegerField(default=1)

View File

@ -89,36 +89,4 @@ urlpatterns = [
name="competition_routine_listing", name="competition_routine_listing",
), ),
path(r"combination/", views.combination_listing, name="combination_listing"), path(r"combination/", views.combination_listing, name="combination_listing"),
#
# PASSES
#
path(
r"passe/<int:passe_id>/gymnast/<int:gymnast_id>/date/<str:date>/",
views.passe_details,
name="passe_details",
),
path(r"passe/", views.passe_listing, name="passe_listing"),
path(
r"passe/add/",
views.passe_create_or_update,
name="passe_create",
),
path(
r"passe/edit/<int:passe_id>/",
views.passe_create_or_update,
name="passe_update",
),
#
# TRAININGPROGRAM
#
path(
r"trainingprogram/detail/date/<str:date>/gymnast/<int:gymnast_id>/",
views.trainingprogram_details,
name="trainingprogram_details",
),
path(
r"trainingprogram/switch_trainingprogram_line/",
views.switch_trainingprogram_line,
name="switch_trainingprogram_line",
),
] ]

View File

@ -5,25 +5,18 @@ from django.shortcuts import render, get_object_or_404
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.urls import reverse from django.urls import reverse
import pendulum
from jarvis.core.global_vars import ROUTINE_TYPE_CHOICE
from jarvis.people.models import Gymnast from jarvis.people.models import Gymnast
from jarvis.followup.models import GymnastHasRoutine
from .forms import ( from .forms import (
SkillForm, SkillForm,
PasseForm,
CombinationForm, CombinationForm,
CombinationSkillForm, CombinationSkillForm,
) )
from .models import ( from .models import (
Skill, Skill,
Passe,
Routine, Routine,
Educative, Educative,
RoutineSkill, RoutineSkill,
TrainingProgram,
PrerequisiteClosure, PrerequisiteClosure,
) )
@ -510,191 +503,3 @@ def unlink_skill_from_combination(request):
return HttpResponse(409) return HttpResponse(409)
return HttpResponse(200) return HttpResponse(200)
@login_required
@require_http_methods(["GET"])
def passe_listing(request):
"""Liste des passages."""
passe_listing = Passe.objects.all()
context = {"passe_listing": passe_listing}
return render(request, "passes/list.html", context)
@login_required
@require_http_methods(["GET"])
def passe_details(request, passe_id, gymnast_id, date):
"""Détails d'un passage."""
is_skill = False
passe = get_object_or_404(Passe, pk=passe_id)
educative_list = passe.educatives.all()
# TODO: décryptage de la regexp
regexp = passe.regexp
routine = None
skill_link_list = None
if regexp is not None:
operation_list = regexp.split(" ")
routine_type = None
for item in ROUTINE_TYPE_CHOICE:
if item[1] == operation_list[0]:
routine_type = item[0]
break
if routine_type is not None:
# Récupération de la série
ghr = GymnastHasRoutine.objects.filter(
gymnast=gymnast_id,
date_begin__lte=date,
# date_end__gte=date,
routine_type=routine_type,
)
print(ghr.query)
if ghr.count() > 1:
print("Plus d'une série trouvée...")
print(ghr)
routine = ghr.first().routine
skill_link_list = routine.skill_links.all()
if len(operation_list) == 2:
content = operation_list[1].replace("[", "").replace("]", "")
ranks = content.split("-")
if ranks[0] != "":
skill_link_list = skill_link_list.filter(rank__gte=ranks[0])
if ranks[1] != "":
skill_link_list = skill_link_list.filter(rank__lte=ranks[1])
number_of_educative = skill_link_list.count()
print(number_of_educative)
else:
number_of_educative = educative_list.count()
context = {
"passe": passe,
"is_skill": is_skill,
"educative_list": educative_list,
"routine": routine,
"skill_link_list": skill_link_list,
"difficulty": passe.difficulty,
"number_of_skill": passe.number_of_skill,
"number_of_educative": number_of_educative,
}
return render(request, "passes/details.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def passe_create_or_update(request, passe_id=None):
"""Création d'un passage.
Args:
passe_id (int) identifiant d'un object de classe <Passe>.
"""
if passe_id:
passe = get_object_or_404(Passe, pk=passe_id)
else:
passe = None
if request.method == "POST":
form = PasseForm(request.POST, instance=passe)
if form.is_valid():
passe = form.save()
return HttpResponseRedirect(reverse("passe_details", args=(passe.pk,)))
return render(request, "passes/create.html", {"form": form})
form = PasseForm(instance=passe)
context = {"form": form, "passe_id": passe_id}
return render(request, "passes/create.html", context)
@login_required
@require_http_methods(["GET"])
def trainingprogram_details(request, date=None, gymnast_id=None):
"""Détails d'un entraînement."""
gymnast = None
trainingprogram_id = None
if trainingprogram_id is not None:
trainingprogram = get_object_or_404(TrainingProgram, pk=trainingprogram_id)
trainingprogram_list = None
else:
trainingprogram = None
trainingprogram_list = TrainingProgram.objects.all()
parsed_date = pendulum.parse(date).date()
if date is not None:
trainingprogram_list = trainingprogram_list.filter(date=parsed_date)
if gymnast_id is not None:
gymnast = get_object_or_404(Gymnast, pk=gymnast_id)
trainingprogram_list = trainingprogram_list.filter(gymnast=gymnast_id)
difficulty = 0
number_of_skill = 0
for trainingprogram in trainingprogram_list:
difficulty += trainingprogram.difficulty
number_of_skill += trainingprogram.number_of_skill
context = {
"gymnast": gymnast,
"date": parsed_date,
"difficulty": difficulty,
"number_of_skill": number_of_skill,
"trainingprogram": trainingprogram,
"trainingprogram_list": trainingprogram_list,
}
return render(request, "trainingprograms/details.html", context)
@require_http_methods(["POST"])
def switch_trainingprogram_line(request):
"""
Recoit dans request deux identifiants de trainingprogram qu'il faut échanger () :
- tp_id (int) identifiant d'une instance de TraiingProgram
- direction (str) la direction du changement (0: haut, 1: bas)
J'utilise `32767` comme valeur intermédiaire pour le `rank` car c'est la limite supérieure d'un
PositiveSmallIntegerField.
"""
try:
target_trainingprogram_id = request.POST.get("tpid", None)
direction = int(request.POST.get("direction", 0))
target_trainingprogram = get_object_or_404(
TrainingProgram, pk=target_trainingprogram_id
)
if direction == 0:
source_trainingprogram = (
TrainingProgram.objects.filter(rank__lt=target_trainingprogram.rank)
.order_by("-id")
.first()
)
else:
source_trainingprogram = (
TrainingProgram.objects.filter(rank__gt=target_trainingprogram.rank)
.order_by("id")
.first()
)
saved_source_rank = source_trainingprogram.rank
saved_target_rank = target_trainingprogram.rank
source_trainingprogram.rank = 32767
source_trainingprogram.save()
target_trainingprogram.rank = saved_source_rank
target_trainingprogram.save()
source_trainingprogram.rank = saved_target_rank
source_trainingprogram.save()
return HttpResponse(200)
except Exception:
return HttpResponse(409)

View File

@ -0,0 +1,21 @@
# Generated by Django 4.2 on 2024-03-25 11:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("people", "0011_gymnast_trainers"),
]
operations = [
migrations.AlterModelOptions(
name="gymnast",
options={
"ordering": ["first_name", "last_name"],
"verbose_name": "Gymnast",
"verbose_name_plural": "Gymnasts",
},
),
]

View File

@ -20,7 +20,6 @@ import pendulum
from jarvis.followup.models import Event from jarvis.followup.models import Event
from jarvis.followup.forms import GymnastHasRoutineForm from jarvis.followup.forms import GymnastHasRoutineForm
from jarvis.objective.models import TrainingProgram
from jarvis.followup.models import ( from jarvis.followup.models import (
Note, Note,
Plan, Plan,
@ -256,13 +255,6 @@ def gymnast_display_scores_chrono(request, gymnast_id):
gymnast=gymnast_id, date__gte=start_date gymnast=gymnast_id, date__gte=start_date
).order_by("date") ).order_by("date")
base_queryset = chrono_list.values("date").annotate(score_avg=Avg("tof")) base_queryset = chrono_list.values("date").annotate(score_avg=Avg("tof"))
today = pendulum.now().date()
date_list = (
TrainingProgram.objects.filter(gymnast=gymnast_id, date__gte=today)
.values_list("date", flat=True)
.order_by("date")
.distinct()
)
context = { context = {
"intensity_list": intensity_list, "intensity_list": intensity_list,

View File

@ -22,21 +22,13 @@ from weasyprint import HTML, CSS
import pendulum import pendulum
from jarvis.followup.models import Event from jarvis.followup.models import Event
from jarvis.followup.models import ( from jarvis.followup.models import (
Note,
Plan, Plan,
Skill, Skill,
Point, Point,
Chrono, Chrono,
Injury,
WellBeing,
Intensity,
LearnedSkill, LearnedSkill,
HeightWeight,
SeasonInformation,
NumberOfRoutineDone,
) )
from jarvis.followup.models import LEARNING_STEP_CHOICES from jarvis.followup.models import LEARNING_STEP_CHOICES