[WIP] Review objectives #36

Draft
Fred wants to merge 2 commits from review/objectives into master
6 changed files with 121 additions and 91 deletions

View File

@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
# Register your models here. from django_extensions.admin import ForeignKeyAutocompleteAdmin
from .models import ( from .models import (
Educative, Educative,
TouchPosition, TouchPosition,
@ -9,7 +10,6 @@ from .models import (
Routine_Skill, Routine_Skill,
Chrono, Chrono,
) )
from django_extensions.admin import ForeignKeyAutocompleteAdmin
class TouchPositionAdmin(admin.ModelAdmin): class TouchPositionAdmin(admin.ModelAdmin):

View File

@ -1,7 +1,9 @@
# coding=UTF-8 """Formulaires d'édition des éducatifs, séries et compétences."""
from datetime import date
from django import forms from django import forms
from datetime import date
from .models import Routine, Routine_Skill, Chrono from .models import Routine, Routine_Skill, Chrono
@ -12,10 +14,10 @@ class RoutineForm(forms.ModelForm):
widgets = { widgets = {
# Champs obligatoires de la classe mère. # Champs obligatoires de la classe mère.
"longLabel": forms.TextInput( "longLabel": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Routine's long name"} attrs={"class": "form-control", "placeholder": "Routine long name"}
), ),
"shortLabel": forms.TextInput( "shortLabel": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Routine's short name"} attrs={"class": "form-control", "placeholder": "Routine short name"}
), ),
"difficulty": forms.HiddenInput(), "difficulty": forms.HiddenInput(),
"level": forms.HiddenInput(), "level": forms.HiddenInput(),

View File

@ -1,40 +1,52 @@
# coding=UTF-8 """Modélisation des éducatifs, objectifs et des compétences."""
from datetime import date from datetime import date
from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from base.models import Markdownizable from base.models import Markdownizable
from django.contrib.auth.models import User
class Educative(Markdownizable): class Educative(Markdownizable):
""" """Un éducatif représente une étape d'apprentissage.
Classe `mère`.
Elle sert de classe "mère" pour toutes les autres choses pouvant être apprises.
Elle est décrite par:
* Un libellé court et long
* Une difficulté
* Un niveau
* Des prérequis à réaliser
* Un âge minimum auquel il peut être réalisé, suivant que le gymnaste soit une fille ou un garçon
Un éducatif peut être lié à plusieurs autres éducatifs.
""" """
class Meta: class Meta:
verbose_name = "Educatif" verbose_name = "Educatif"
verbose_name_plural = "Educatifs" verbose_name_plural = "Educatifs"
ordering = ["longLabel", "shortLabel"] # 'level', ordering = ["longLabel", "shortLabel"]
longLabel = models.CharField(max_length = 255, verbose_name = "Long Name") longLabel = models.CharField(max_length=255, verbose_name="Long Name")
shortLabel = models.CharField(max_length = 255, verbose_name = "Short Name") shortLabel = models.CharField(max_length=255, verbose_name="Short Name")
difficulty = models.DecimalField( difficulty = models.DecimalField(
max_digits = 3, decimal_places = 1, verbose_name = "Difficulty" max_digits=3, decimal_places=1, verbose_name="Difficulty"
) )
level = models.PositiveSmallIntegerField(verbose_name = "Level", default = 0) level = models.PositiveSmallIntegerField(verbose_name="Level", default=0)
rank = models.PositiveSmallIntegerField(verbose_name = "Rank", default = 0) rank = models.PositiveSmallIntegerField(verbose_name="Rank", default=0)
educative = models.ManyToManyField( educative = models.ManyToManyField(
"self", related_name = "educativeOf", blank = True, symmetrical = False "self", related_name="educativeOf", blank=True, symmetrical=False
) )
prerequisite = models.ManyToManyField( prerequisite = models.ManyToManyField(
"self", related_name = "prerequisiteOf", blank = True, symmetrical = False "self", related_name="prerequisiteOf", blank=True, symmetrical=False
) )
ageBoy = models.PositiveSmallIntegerField( ageBoy = models.PositiveSmallIntegerField(
blank = True, null = True, verbose_name = "Boy's age" blank=True, null=True, verbose_name="Boy's age"
) )
ageGirl = models.PositiveSmallIntegerField( ageGirl = models.PositiveSmallIntegerField(
blank = True, null = True, verbose_name = "Girl's age" blank=True, null=True, verbose_name="Girl's age"
) )
def __str__(self): def __str__(self):
@ -47,8 +59,9 @@ class Educative(Markdownizable):
class TouchPosition(models.Model): class TouchPosition(models.Model):
""" """Représentation des différentes positions d'arrivée et de départ en trampoline.
Classe représentant les différentes position d'arrivée/départ (landing position) en trampoline.
Cette classe fait référence aux *landing positions*.
""" """
class Meta: class Meta:
@ -56,10 +69,10 @@ class TouchPosition(models.Model):
verbose_name_plural = "Landings" verbose_name_plural = "Landings"
ordering = ["longLabel", "shortLabel", "default", "competition"] ordering = ["longLabel", "shortLabel", "default", "competition"]
longLabel = models.CharField(max_length = 30, verbose_name = "Nom long") longLabel = models.CharField(max_length=30, verbose_name="Nom long")
shortLabel = models.CharField(max_length = 15, verbose_name = "Nom court") shortLabel = models.CharField(max_length=15, verbose_name="Nom court")
competition = models.BooleanField(verbose_name = "Compétition") competition = models.BooleanField(verbose_name="Compétition")
default = models.BooleanField(verbose_name = "Défaut") default = models.BooleanField(verbose_name="Défaut")
def __str__(self): def __str__(self):
return "%s" % (self.longLabel) return "%s" % (self.longLabel)
@ -67,7 +80,7 @@ class TouchPosition(models.Model):
def get_default_position(): def get_default_position():
""" """
Renvoie la position d'arrivée/départ par définie par défaut si elle existe. Sinon, renvoie None. Renvoie la position d'arrivée/départ par définie par défaut si elle existe. Sinon, renvoie None.
""" """
try: try:
return TouchPosition.objects.get(default=True).id return TouchPosition.objects.get(default=True).id
@ -76,13 +89,14 @@ def get_default_position():
class Skill(Educative): class Skill(Educative):
""" """Les `skills` représentent une figure ou un saut acrobatique.
Classe représentant une figure (aka un saut acrobatique).
"""
# SELECT * FROM `objective_skill` WHERE educative_ptr_id NOT IN (SELECT DISTINCT(from_educative_id) FROM `objective_educative_prerequisite`) Chaque saut est caractérisé par:
* Une position de départ
* Une position d'arrivée
* Un type de rotation
# SELECT * FROM `objective_skill`, `objective_educative` WHERE `objective_educative`.id = `objective_skill`.educative_ptr_id """
class Meta: class Meta:
verbose_name = "Skill" verbose_name = "Skill"
@ -104,29 +118,29 @@ class Skill(Educative):
(2, "Backward"), (2, "Backward"),
) )
position = models.CharField(max_length = 2, choices = POSITION_CHOICES) position = models.CharField(max_length=2, choices=POSITION_CHOICES)
departure = models.ForeignKey( departure = models.ForeignKey(
TouchPosition, TouchPosition,
related_name = "depart_of", related_name="depart_of",
default = get_default_position, default=get_default_position,
verbose_name = "Take-off position", verbose_name="Take-off position",
on_delete = models.CASCADE, on_delete=models.CASCADE,
) )
landing = models.ForeignKey( landing = models.ForeignKey(
TouchPosition, TouchPosition,
related_name = "landing_of", related_name="landing_of",
default = get_default_position, default=get_default_position,
verbose_name = "Landing position", verbose_name="Landing position",
on_delete = models.CASCADE, on_delete=models.CASCADE,
) )
rotationType = models.PositiveSmallIntegerField( rotationType = models.PositiveSmallIntegerField(
choices = ROTATION_CHOICES, verbose_name = "Type de rotation" choices=ROTATION_CHOICES, verbose_name="Type de rotation"
) )
rotation = models.PositiveSmallIntegerField(verbose_name = "1/4 de rotation") rotation = models.PositiveSmallIntegerField(verbose_name="1/4 de rotation")
twist = models.PositiveSmallIntegerField(verbose_name = "1/2 Vrille") twist = models.PositiveSmallIntegerField(verbose_name="1/2 Vrille")
notation = models.CharField(max_length = 25) notation = models.CharField(max_length=25)
simplyNotation = models.CharField(max_length = 25, verbose_name = "Notation simplifiée") simplyNotation = models.CharField(max_length=25, verbose_name="Notation simplifiée")
is_competitive = models.BooleanField(default = False) is_competitive = models.BooleanField(default=False)
# importance = models.PositiveSmallIntegerField(default = 1) # importance = models.PositiveSmallIntegerField(default = 1)
def __str__(self): def __str__(self):
@ -134,9 +148,7 @@ class Skill(Educative):
class Routine(Educative): class Routine(Educative):
""" """Une routine représente une série, càd un enchaînement de plusieurs figures."""
Classe représentant une série (enchainement de plusieurs figures).
"""
class Meta: class Meta:
verbose_name = "Routine" verbose_name = "Routine"
@ -144,9 +156,9 @@ class Routine(Educative):
active = models.BooleanField() active = models.BooleanField()
jumps = models.ManyToManyField( jumps = models.ManyToManyField(
Skill, through = "Routine_Skill", verbose_name = "routine" Skill, through="Routine_Skill", verbose_name="routine"
) # ceci n'est pas un vrai champ ) # ceci n'est pas un vrai champ (- Magritte ?)
is_competitive = models.BooleanField(default = False) is_competitive = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return "%s (%s)" % (self.shortLabel, self.shortLabel) return "%s (%s)" % (self.shortLabel, self.shortLabel)
@ -216,10 +228,10 @@ class Routine_Skill(models.Model):
ordering = ("rank",) ordering = ("rank",)
routine = models.ForeignKey( routine = models.ForeignKey(
Routine, on_delete = models.CASCADE, default = None, related_name = "skill_links" Routine, on_delete=models.CASCADE, default=None, related_name="skill_links"
) )
skill = models.ForeignKey( skill = models.ForeignKey(
Skill, on_delete = models.CASCADE, default = None, related_name = "routine_links" Skill, on_delete=models.CASCADE, default=None, related_name="routine_links"
) )
rank = models.PositiveSmallIntegerField() rank = models.PositiveSmallIntegerField()
@ -232,10 +244,9 @@ class Routine_Skill(models.Model):
class Chrono(models.Model): class Chrono(models.Model):
""" """Représentation du temps demandé à un gymnaste pour réaliser une série.
Classe représentant le temps réalisé par un élève pour faire une série.
.. todo:: lors de l'affichage, aller chercher les ToF contenu dans les points de compétition (avec traitement adéquat). TODO: lors de l'affichage, aller chercher les ToF contenu dans les points de compétition (avec traitement adéquat).
""" """
ROUTINETYPE_CHOICE = ( ROUTINETYPE_CHOICE = (
@ -247,13 +258,13 @@ class Chrono(models.Model):
(99, "Other"), (99, "Other"),
) )
routine = models.ForeignKey(Routine, on_delete = models.CASCADE, default = None) routine = models.ForeignKey(Routine, on_delete=models.CASCADE, default=None)
routine_type = models.PositiveSmallIntegerField(choices = ROUTINETYPE_CHOICE) routine_type = models.PositiveSmallIntegerField(choices=ROUTINETYPE_CHOICE)
gymnast = models.ForeignKey( gymnast = models.ForeignKey(
"people.gymnast", on_delete = models.CASCADE, default = None "people.gymnast", on_delete=models.CASCADE, default=None
) )
date = models.DateField(default = date.today, verbose_name = "Date") date = models.DateField(default=date.today, verbose_name="Date")
score = models.DecimalField(max_digits = 5, decimal_places = 2) score = models.DecimalField(max_digits=5, decimal_places=2)
def __str__(self): def __str__(self):
return "%s le %s : %s pour %s" % ( return "%s le %s : %s pour %s" % (
@ -274,17 +285,16 @@ class Chrono(models.Model):
class Evaluation(models.Model): class Evaluation(models.Model):
""" """Evaluation d'un éducatif par un utilisateur.
Classe permettant l'évaluation des éducatifs.
""" """
date = models.DateField(default = date.today, verbose_name = "Date") date = models.DateField(default=date.today, verbose_name="Date")
value = models.PositiveSmallIntegerField(default = 0) value = models.PositiveSmallIntegerField(default=0)
educative = models.ForeignKey( educative = models.ForeignKey(
Educative, Educative,
related_name = "depart_of", related_name="depart_of",
verbose_name = "Take-off position", verbose_name="Take-off position",
on_delete = models.CASCADE, on_delete=models.CASCADE,
) )
type_of_evaluator = models.BooleanField(default = 0) type_of_evaluator = models.BooleanField(default=0)
evaluator = models.ForeignKey(User, null = True, on_delete = models.SET_NULL) evaluator = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)

View File

@ -1,15 +1,26 @@
# coding=UTF-8 """Tests des routines, éducatifs, compétences, ..."""
from django.urls import reverse
from django.test import TestCase, Client from django.test import TestCase, Client
from django.urls import reverse
from .models import Routine_Skill, Routine, Skill, TouchPosition from .models import Routine_Skill, Routine, Skill, TouchPosition
from .views import link_skill_to_routine from .views import link_skill_to_routine
class RoutineTest(TestCase): class RoutineTest(TestCase):
"""Tests unitaires liés aux séries.
Attributes:
routine (Routine)
touch_position (TouchPosition)
skill (Skill)
client (django.test.Client)
"""
def setUp(self): def setUp(self):
self.routine, routine_created = Routine.objects.get_or_create( """Instancie une série (*routine*), des TouchPositions, ...
"""
self.routine, _ = Routine.objects.get_or_create(
longLabel="Routine test", longLabel="Routine test",
shortLabel="Routine test", shortLabel="Routine test",
difficulty=0, difficulty=0,
@ -17,14 +28,14 @@ class RoutineTest(TestCase):
) )
( (
self.touch_position, self.touch_position,
touch_position_created, _,
) = TouchPosition.objects.get_or_create( ) = TouchPosition.objects.get_or_create(
longLabel="Touch test", longLabel="Touch test",
shortLabel="Touch test", shortLabel="Touch test",
competition=True, competition=True,
default=True, default=True,
) )
self.skill, skill_created = Skill.objects.get_or_create( self.skill, _ = Skill.objects.get_or_create(
longLabel="Skill test", longLabel="Skill test",
shortLabel="Skill test", shortLabel="Skill test",
difficulty=0, difficulty=0,
@ -38,17 +49,19 @@ class RoutineTest(TestCase):
simplyNotation="t", simplyNotation="t",
) )
self.client = Client() self.client = Client()
self.url_details = {
def test_valid_link_skill_to_routine(self):
"""Vérifie que l'association d'un Skill à une série fonctionne correctement.
"""
url_details = {
"routineid": self.routine.id, "routineid": self.routine.id,
"skillid": self.skill.id, "skillid": self.skill.id,
"order": 1, "order": 1,
} }
def test_valid_link_skill_to_routine(self): url = reverse("link_skill_to_routine", kwargs=url_details)
c = Client()
url = reverse("link_skill_to_routine", kwargs=self.url_details) response = self.client.get(url)
response = c.get(url)
# print(response)
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertTrue(Routine_Skill.objects.exists()) self.assertTrue(Routine_Skill.objects.exists())

View File

@ -1,4 +1,11 @@
# coding=UTF-8 """Définition des URLs associées à l'application `objective`.
Il existe trois grandes rubriques pour cette application:
* chrono
* skill
* routine
"""
from django.urls import path, re_path from django.urls import path, re_path
@ -46,7 +53,7 @@ routine_urlpatterns = [
r"suggest/", r"suggest/",
views.suggest_routine, views.suggest_routine,
name="suggest_routine", name="suggest_routine",
) ),
] ]
# Chrono # Chrono

View File

@ -48,8 +48,7 @@ def __lookup(lookup_class, lookup_value):
@login_required @login_required
def skill_listing(request, field=None, expression=None, value=None, level=None): def skill_listing(request, field=None, expression=None, value=None, level=None):
""" """Récupère la liste des skills suivant un pattern si celui-ci est défini.
Récupère la liste des skills suivant un pattern si celui-ci est définit.
""" """
pattern = None pattern = None
@ -116,8 +115,7 @@ def skill_detail(request, skillid):
@login_required @login_required
@require_http_methods(["POST"]) @require_http_methods(["POST"])
def linkSkillGymnast(request): def linkSkillGymnast(request):
""" """Lie un gymnaste à une figure.
Lie un gymnast à une figure.
""" """
# utiliser un FORM pour cette fonction. # utiliser un FORM pour cette fonction.
gymnastid = request.POST.get("gymnastid", None) gymnastid = request.POST.get("gymnastid", None)
@ -370,7 +368,7 @@ def __construct_routine(
competition (bool): indique si la série doit respecter les règles de compétition. competition (bool): indique si la série doit respecter les règles de compétition.
logic (bool): indique si la série doit suivre les règles de logique (sportive). logic (bool): indique si la série doit suivre les règles de logique (sportive).
gymnast (gymnast): gymnaste. gymnast (gymnast): gymnaste.
Returns: Returns:
??? (list ?): liste des séries correspondantes aux criètres. ??? (list ?): liste des séries correspondantes aux criètres.
""" """
@ -457,7 +455,7 @@ def suggest_routine(
logic (bool): indique si la série doit suivre les règles de logique (sportive). logic (bool): indique si la série doit suivre les règles de logique (sportive).
gymnast (gymnast): gymnaste. gymnast (gymnast): gymnaste.
last_jump (skill): dernier saut sélectionné pour la série. last_jump (skill): dernier saut sélectionné pour la série.
Returns: Returns:
??? (list): liste des séries correspondantes aux criètres. ??? (list): liste des séries correspondantes aux criètres.