2023-04-25 17:06:14 +02:00
|
|
|
|
from django.db import models
|
2023-05-08 10:12:15 +02:00
|
|
|
|
from django.db.models import Q
|
2023-04-25 17:06:14 +02:00
|
|
|
|
|
2023-04-29 17:19:52 +02:00
|
|
|
|
from jarvis.tools.models import Markdownizable, max_even_if_none
|
2023-04-25 17:06:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Educative(Markdownizable):
|
|
|
|
|
"""
|
|
|
|
|
Classe `mère` educative. En trampoline tout est un éducatif : un saut, un enchainement, une
|
|
|
|
|
série de compétition, ….
|
|
|
|
|
|
|
|
|
|
Level (skill) :
|
|
|
|
|
Toutes les figures appartiennent à un niveau. Un niveau peut contenir plusieurs figures.
|
|
|
|
|
Par défaut, le niveau d'une figure est son coéfficient de difficulté (exprimé en 10ème
|
|
|
|
|
pour avoir des nombres entiers) auquel on ajoute 1 pour les positions tendue.
|
|
|
|
|
|
|
|
|
|
Exemples :
|
|
|
|
|
- saut groupé, saut carpé joint et saut écart ==> niveau 0
|
|
|
|
|
- salto avant groupé, salto arrière groupé ==> niveau 5
|
|
|
|
|
- salto avant carpé, barani groupé, salto arrière carpé ==> niveau 6
|
|
|
|
|
- salto avant tendu, salto arrière tendu, barani tendu ==> niveau 7
|
|
|
|
|
|
|
|
|
|
En plus de cela, il y a une limite minimum : le niveau d’une figure ne peut pas être plus
|
|
|
|
|
petit que le niveau maximum de ses prérequis.
|
|
|
|
|
Le niveau, avec le rang, ont pour but d’aider les coaches à planifier l’évolution et l’
|
|
|
|
|
apprentissage des figures les unes par rapport aux autres.
|
|
|
|
|
|
|
|
|
|
Level (routine) :
|
|
|
|
|
Toutes les séries ont également un niveau. Par défaut le niveau d'une série est le niveau
|
|
|
|
|
maximum des figures qui composent la série.
|
|
|
|
|
|
|
|
|
|
Rank (skill) :
|
|
|
|
|
Le rang permet, en plus du `level` (niveau), de classer les figures entre elles, de leur
|
|
|
|
|
donner un ordre (informatif). Le rang d’une figure est calculé par rapport aux prérequis et
|
|
|
|
|
au niveau : par défaut le rang d’une figure est le maximum entre le niveau maximum de ses
|
|
|
|
|
prérequis plus un et le niveau de la figure.
|
|
|
|
|
Le rang, avec le niveau, ont pour but d’aider les coaches à planifier l’évolution et
|
|
|
|
|
l’apprentissage des figures les unes par rapport aux autres.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
AGE_CHOICES = (
|
|
|
|
|
(6, "6-7"),
|
|
|
|
|
(7, "7-8"),
|
|
|
|
|
(8, "8-9"),
|
|
|
|
|
(9, "9-10"),
|
|
|
|
|
(10, "10-11"),
|
|
|
|
|
(11, "11-12"),
|
|
|
|
|
(12, "12-13"),
|
|
|
|
|
(13, "13-14"),
|
|
|
|
|
(14, "14-15"),
|
|
|
|
|
(15, "15-16"),
|
|
|
|
|
(16, "16-17"),
|
|
|
|
|
(17, "17+"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
verbose_name = "Educatif"
|
|
|
|
|
verbose_name_plural = "Educatifs"
|
|
|
|
|
ordering = ["long_label", "short_label"] # 'level',
|
|
|
|
|
|
|
|
|
|
long_label = models.CharField(max_length=255, verbose_name="Long Name")
|
|
|
|
|
short_label = models.CharField(max_length=255, verbose_name="Short Name")
|
|
|
|
|
difficulty = models.DecimalField(
|
|
|
|
|
max_digits=3, decimal_places=1, verbose_name="Difficulty", default=0.000
|
|
|
|
|
)
|
|
|
|
|
level = models.PositiveSmallIntegerField(verbose_name="Level", default=0)
|
|
|
|
|
rank = models.PositiveSmallIntegerField(verbose_name="Rank", default=0)
|
|
|
|
|
educatives = models.ManyToManyField(
|
|
|
|
|
"self", related_name="educatives_of", blank=True, symmetrical=False
|
|
|
|
|
)
|
|
|
|
|
prerequisites = models.ManyToManyField(
|
|
|
|
|
"self", related_name="prerequisite_of", blank=True, symmetrical=False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
age_boy_with_help = models.PositiveSmallIntegerField(
|
|
|
|
|
choices=AGE_CHOICES, verbose_name="Boy's age with help", default=6
|
|
|
|
|
)
|
|
|
|
|
age_boy_without_help = models.PositiveSmallIntegerField(
|
|
|
|
|
choices=AGE_CHOICES, verbose_name="Boy's age without help", default=6
|
|
|
|
|
)
|
|
|
|
|
age_boy_chained = models.PositiveSmallIntegerField(
|
|
|
|
|
choices=AGE_CHOICES, verbose_name="Boy's age chained", default=6
|
|
|
|
|
)
|
|
|
|
|
age_boy_masterised = models.PositiveSmallIntegerField(
|
|
|
|
|
choices=AGE_CHOICES, verbose_name="Boy's age masterised", default=6
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
age_girl_with_help = models.PositiveSmallIntegerField(
|
|
|
|
|
choices=AGE_CHOICES, verbose_name="Girl's age with help", default=6
|
|
|
|
|
)
|
|
|
|
|
age_girl_without_help = models.PositiveSmallIntegerField(
|
|
|
|
|
choices=AGE_CHOICES, verbose_name="Girl's age without help", default=6
|
|
|
|
|
)
|
|
|
|
|
age_girl_chained = models.PositiveSmallIntegerField(
|
|
|
|
|
choices=AGE_CHOICES, verbose_name="Girl's age chained", default=6
|
|
|
|
|
)
|
|
|
|
|
age_girl_masterised = models.PositiveSmallIntegerField(
|
|
|
|
|
choices=AGE_CHOICES, verbose_name="Girl's age masterised", default=6
|
|
|
|
|
)
|
|
|
|
|
# is_competitive = models.BooleanField(default=False)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2023-04-29 15:31:14 +02:00
|
|
|
|
return f"{self.long_label} ({self.short_label} - {self.difficulty})"
|
2023-04-25 17:06:14 +02:00
|
|
|
|
|
|
|
|
|
def breadcrumb(self, path=[]):
|
|
|
|
|
"""
|
|
|
|
|
Renvoie le breadcrumb pour l'édutatif courant.
|
|
|
|
|
Exemple :
|
|
|
|
|
>>> s = Skill.objects.get(pk=44)
|
|
|
|
|
>>> s.breadcrumb()
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
path = [self] + path
|
|
|
|
|
if self.prerequisites.all().count() == 0:
|
|
|
|
|
return [path]
|
|
|
|
|
|
|
|
|
|
path_list = []
|
|
|
|
|
for prerequisite in self.prerequisites.all():
|
|
|
|
|
if prerequisite.id == self.id:
|
|
|
|
|
return [self]
|
|
|
|
|
new_paths = prerequisite.breadcrumb(path)
|
|
|
|
|
for new_path in new_paths:
|
|
|
|
|
path_list.append(new_path)
|
|
|
|
|
|
|
|
|
|
return path_list
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PrerequisiteClosure(models.Model):
|
|
|
|
|
"""
|
|
|
|
|
Closure table de prérequis
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
unique_together = ("descendant", "ancestor", "level", "path")
|
|
|
|
|
|
|
|
|
|
descendant = models.ForeignKey(
|
|
|
|
|
Educative, on_delete=models.CASCADE, related_name="ancestor"
|
|
|
|
|
)
|
|
|
|
|
ancestor = models.ForeignKey(
|
|
|
|
|
Educative, on_delete=models.CASCADE, related_name="descendants"
|
|
|
|
|
)
|
|
|
|
|
level = models.PositiveIntegerField()
|
|
|
|
|
path = models.PositiveIntegerField()
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2023-04-29 15:31:14 +02:00
|
|
|
|
return f"{self.ancestor.long_label} -> {self.descendant.long_label} ({self.level}|{self.path})"
|
2023-04-25 17:06:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TouchPosition(models.Model):
|
|
|
|
|
"""
|
|
|
|
|
Classe représentant les différentes position d'arrivée/départ (landing position) en trampoline.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
verbose_name = "Landing"
|
|
|
|
|
verbose_name_plural = "Landings"
|
|
|
|
|
ordering = ["long_label", "short_label", "is_default", "allowed_in_competition"]
|
|
|
|
|
|
|
|
|
|
long_label = models.CharField(max_length=30, verbose_name="Long label")
|
|
|
|
|
short_label = models.CharField(max_length=15, verbose_name="Short label")
|
|
|
|
|
allowed_in_competition = models.BooleanField(
|
|
|
|
|
verbose_name="Allowed in competition", default=True
|
|
|
|
|
)
|
|
|
|
|
is_default = models.BooleanField(verbose_name="Défault ?", default=False)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2023-04-29 15:31:14 +02:00
|
|
|
|
return f"{self.long_label}"
|
2023-04-25 17:06:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_default_position():
|
|
|
|
|
"""
|
|
|
|
|
Renvoie la position d'arrivée/départ par définie par défaut si elle existe. Sinon, renvoie None.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return TouchPosition.objects.get(is_default=True).id
|
|
|
|
|
except TouchPosition.DoesNotExist:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Skill(Educative):
|
|
|
|
|
"""
|
|
|
|
|
Classe représentant une figure (un mouvement, un saut acrobatique). Elle hérite de la classe
|
|
|
|
|
`Educative`.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# SELECT * FROM `objective_skill`
|
|
|
|
|
# WHERE educative_ptr_id NOT IN (
|
|
|
|
|
# SELECT DISTINCT(from_educative_id) FROM `objective_educative_prerequisite`
|
|
|
|
|
# )
|
|
|
|
|
#
|
|
|
|
|
# SELECT * FROM `objective_skill`, `objective_educative`
|
|
|
|
|
# WHERE `objective_educative`.id = `objective_skill`.educative_ptr_id
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
verbose_name = "Skill"
|
|
|
|
|
verbose_name_plural = "Skills"
|
|
|
|
|
|
|
|
|
|
POSITION_CHOICES = (
|
|
|
|
|
("0", "none"),
|
|
|
|
|
("o", "tuck"),
|
|
|
|
|
("c", "puck"),
|
|
|
|
|
("<", "pike"),
|
|
|
|
|
("L", "half pike"),
|
|
|
|
|
("/", "straight"),
|
|
|
|
|
("//", "straddle"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ROTATION_CHOICES = (
|
|
|
|
|
(0, "none"),
|
|
|
|
|
(1, "frontward"),
|
|
|
|
|
(2, "backward"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
position = models.CharField(max_length=2, choices=POSITION_CHOICES)
|
|
|
|
|
departure = models.ForeignKey(
|
|
|
|
|
TouchPosition,
|
|
|
|
|
related_name="depart_of",
|
|
|
|
|
default=get_default_position,
|
|
|
|
|
verbose_name="Take-off position",
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
)
|
|
|
|
|
landing = models.ForeignKey(
|
|
|
|
|
TouchPosition,
|
|
|
|
|
related_name="landing_of",
|
|
|
|
|
default=get_default_position,
|
|
|
|
|
verbose_name="Landing position",
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
)
|
|
|
|
|
rotation_type = models.PositiveSmallIntegerField(
|
|
|
|
|
choices=ROTATION_CHOICES, verbose_name="Type de rotation"
|
|
|
|
|
)
|
|
|
|
|
rotation = models.PositiveSmallIntegerField(verbose_name="¼ de rotation")
|
|
|
|
|
twist = models.PositiveSmallIntegerField(verbose_name="½ Vrille")
|
|
|
|
|
notation = models.CharField(max_length=10)
|
|
|
|
|
simplified_notation = models.CharField(
|
|
|
|
|
max_length=10, verbose_name="Notation simplifiée"
|
|
|
|
|
)
|
|
|
|
|
is_competitive = models.BooleanField(default=False)
|
|
|
|
|
# importance = models.PositiveSmallIntegerField(default = 1)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2023-04-29 15:31:14 +02:00
|
|
|
|
return f"{self.long_label} ({self.notation})"
|
2023-04-25 17:06:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Routine(Educative):
|
|
|
|
|
"""
|
|
|
|
|
Classe représentant une série (enchainement de plusieurs figures). Elle hérite de la classe
|
|
|
|
|
`Educative`.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
verbose_name = "Routine"
|
|
|
|
|
verbose_name_plural = "Routines"
|
|
|
|
|
|
|
|
|
|
jumps = models.ManyToManyField(
|
|
|
|
|
Skill, through="RoutineSkill", verbose_name="routine"
|
|
|
|
|
)
|
|
|
|
|
is_active = models.BooleanField(default=True)
|
2023-05-01 17:07:50 +02:00
|
|
|
|
is_routine = models.BooleanField(default=False)
|
2023-04-25 17:06:14 +02:00
|
|
|
|
is_competitive = models.BooleanField(default=False)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2023-04-29 15:31:14 +02:00
|
|
|
|
return f"{self.long_label} ({self.short_label})"
|
2023-04-25 17:06:14 +02:00
|
|
|
|
|
|
|
|
|
def compute_informations(self):
|
2023-05-08 10:12:15 +02:00
|
|
|
|
"""Cette fonction a pour but d'assurer la cohérence des informations d'une combinaison.
|
|
|
|
|
La fonction vérifie :
|
|
|
|
|
- les âges pour les filles et garçons,
|
|
|
|
|
- le rang,
|
|
|
|
|
- le niveau,
|
|
|
|
|
- la difficulté
|
|
|
|
|
- si c'est une routine (série)
|
|
|
|
|
- si c'est une routine (série) de compétition
|
|
|
|
|
A chaque fois qu'on le peut, on garde les informations entrées par les utilisateurs néanmoins on vérifie qu'il
|
|
|
|
|
n'a pas encodé n'importe quoi.
|
|
|
|
|
"""
|
2023-04-25 17:06:14 +02:00
|
|
|
|
rank = 0
|
|
|
|
|
level = 0
|
|
|
|
|
age_boy_with_help = 0
|
|
|
|
|
age_girl_with_help = 0
|
|
|
|
|
age_boy_without_help = 0
|
|
|
|
|
age_girl_without_help = 0
|
|
|
|
|
age_boy_chained = 0
|
|
|
|
|
age_girl_chained = 0
|
|
|
|
|
age_boy_masterised = 0
|
|
|
|
|
age_girl_masterised = 0
|
|
|
|
|
difficulty = 0
|
2023-05-08 10:12:15 +02:00
|
|
|
|
is_routine = False
|
|
|
|
|
is_competitive = False
|
2023-04-25 17:06:14 +02:00
|
|
|
|
|
|
|
|
|
for skill_link in self.skill_links.all():
|
|
|
|
|
skill = skill_link.skill
|
|
|
|
|
|
|
|
|
|
difficulty += skill.difficulty
|
|
|
|
|
level = max(skill.level, level)
|
|
|
|
|
rank = max(skill.rank + 1, rank)
|
|
|
|
|
|
|
|
|
|
if not skill.is_competitive:
|
|
|
|
|
is_competitive = False
|
|
|
|
|
|
|
|
|
|
# Age boy computing
|
|
|
|
|
age_boy_with_help = max_even_if_none(
|
|
|
|
|
skill.age_boy_with_help, age_boy_with_help
|
|
|
|
|
)
|
|
|
|
|
age_boy_without_help = max_even_if_none(
|
|
|
|
|
skill.age_boy_without_help, age_boy_without_help
|
|
|
|
|
)
|
|
|
|
|
age_boy_chained = max_even_if_none(skill.age_boy_chained, age_boy_chained)
|
|
|
|
|
age_boy_masterised = max_even_if_none(
|
|
|
|
|
skill.age_boy_masterised, age_boy_masterised
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Age girl computing
|
|
|
|
|
age_girl_with_help = max_even_if_none(
|
|
|
|
|
skill.age_girl_with_help, age_girl_with_help
|
|
|
|
|
)
|
|
|
|
|
age_girl_without_help = max_even_if_none(
|
|
|
|
|
skill.age_girl_without_help, age_girl_without_help
|
|
|
|
|
)
|
|
|
|
|
age_girl_chained = max_even_if_none(
|
|
|
|
|
skill.age_girl_chained, age_girl_chained
|
|
|
|
|
)
|
|
|
|
|
age_girl_masterised = max_even_if_none(
|
|
|
|
|
skill.age_girl_masterised, age_girl_masterised
|
|
|
|
|
)
|
|
|
|
|
|
2023-05-08 10:12:15 +02:00
|
|
|
|
if self.skill_links.all().count() < 5:
|
|
|
|
|
is_routine = False
|
|
|
|
|
is_competitive = False
|
|
|
|
|
elif self.skill_links.all().count() != 10:
|
2023-04-25 17:06:14 +02:00
|
|
|
|
is_competitive = False
|
2023-05-08 10:12:15 +02:00
|
|
|
|
|
|
|
|
|
if not self.is_routine:
|
|
|
|
|
is_competitive = False
|
|
|
|
|
|
|
|
|
|
self.is_routine = is_routine
|
2023-04-25 17:06:14 +02:00
|
|
|
|
self.is_competitive = is_competitive
|
|
|
|
|
|
|
|
|
|
self.difficulty = difficulty
|
|
|
|
|
self.level = max(self.level, level)
|
|
|
|
|
self.rank = max(self.rank, rank)
|
|
|
|
|
|
|
|
|
|
self.age_boy_with_help = max(self.age_boy_with_help, age_boy_with_help)
|
|
|
|
|
self.age_boy_without_help = max(self.age_boy_without_help, age_boy_without_help)
|
|
|
|
|
self.age_boy_chained = max(self.age_boy_chained, age_boy_chained)
|
|
|
|
|
self.age_boy_masterised = max(self.age_boy_masterised, age_boy_masterised)
|
|
|
|
|
|
|
|
|
|
self.age_girl_with_help = max(self.age_girl_with_help, age_girl_with_help)
|
|
|
|
|
self.age_girl_without_help = max(
|
|
|
|
|
self.age_girl_without_help, age_girl_without_help
|
|
|
|
|
)
|
|
|
|
|
self.age_girl_chained = max(self.age_girl_chained, age_girl_chained)
|
|
|
|
|
self.age_girl_masterised = max(self.age_girl_masterised, age_girl_masterised)
|
|
|
|
|
|
|
|
|
|
self.save()
|
|
|
|
|
|
|
|
|
|
def contains_basic_jumps(self):
|
|
|
|
|
"""
|
|
|
|
|
Renvoie True si la série contient au moins un saut de base, False sinon.
|
|
|
|
|
"""
|
|
|
|
|
return self.skill_links.filter(skill__notation__in=["//", "<", "o"]).exists()
|
|
|
|
|
|
|
|
|
|
def contains_basic_fall(self):
|
|
|
|
|
"""
|
|
|
|
|
Renvoie True si la série contient au moins un tomber de base, False sinon.
|
|
|
|
|
"""
|
|
|
|
|
return self.skill_links.filter(
|
|
|
|
|
skill__landing__long_label__in=["Assis", "Dos", "Ventre"]
|
|
|
|
|
).exists()
|
|
|
|
|
|
|
|
|
|
def contains_basic_salto(self):
|
|
|
|
|
"""
|
|
|
|
|
Renvoie True si la série contient au moins un salto/barani de base,
|
|
|
|
|
False sinon.
|
|
|
|
|
"""
|
|
|
|
|
return self.skill_links.filter(
|
|
|
|
|
Q(skill__notation__icontains=".41") | Q(skill__notation__icontains="4.-")
|
|
|
|
|
).exists()
|
|
|
|
|
|
|
|
|
|
def contains_basic_three_quarters(self):
|
|
|
|
|
"""
|
|
|
|
|
Renvoie True si la série contient au moins un 3/4 de salto, False sinon.
|
|
|
|
|
"""
|
|
|
|
|
return self.skill_links.filter(
|
|
|
|
|
Q(skill__notation__icontains=".3") | Q(skill__notation__icontains="3.")
|
|
|
|
|
).exists()
|
|
|
|
|
|
|
|
|
|
def contains_basic_twist(self):
|
|
|
|
|
"""
|
|
|
|
|
Renvoie True si la série contient au moins une vrille de base, False sinon.
|
|
|
|
|
"""
|
|
|
|
|
return self.skill_links.filter(
|
|
|
|
|
Q(skill__notation__icontains=".43") | Q(skill__notation__icontains="4.2")
|
|
|
|
|
).exists()
|
|
|
|
|
|
|
|
|
|
def contains_double(self):
|
|
|
|
|
"""
|
|
|
|
|
Renvoie True si la série contient au moins un double, False sinon.
|
|
|
|
|
"""
|
|
|
|
|
return self.skill_links.filter(
|
|
|
|
|
Q(skill__notation__icontains=".8") | Q(skill__notation__icontains="8.")
|
|
|
|
|
).exists()
|
|
|
|
|
|
|
|
|
|
def contains_triple(self):
|
|
|
|
|
"""
|
|
|
|
|
Renvoie True si la série contient au moins un triple, False sinon.
|
|
|
|
|
"""
|
|
|
|
|
return self.skill_links.filter(
|
|
|
|
|
Q(skill__notation__icontains=".12") | Q(skill__notation__icontains="12.")
|
|
|
|
|
).exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoutineSkill(models.Model):
|
|
|
|
|
"""
|
|
|
|
|
Classe de liaison permettant de liée une figure à une série. (relation n-n)
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
ordering = ("rank",)
|
|
|
|
|
|
|
|
|
|
routine = models.ForeignKey(
|
|
|
|
|
Routine, on_delete=models.CASCADE, default=None, related_name="skill_links"
|
|
|
|
|
)
|
|
|
|
|
skill = models.ForeignKey(
|
|
|
|
|
Skill, on_delete=models.CASCADE, default=None, related_name="routine_links"
|
|
|
|
|
)
|
|
|
|
|
rank = models.PositiveSmallIntegerField()
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2023-04-29 15:31:14 +02:00
|
|
|
|
return f"{self.rank} - {self.routine.short_label} : {self.skill.short_label}"
|