Ce patch ajoute un lien bidirectionnel entre la page d'affichage d'un petit cours pour le Burô et l'administration générale. Plus précisément, - Un lien est ajouté sur la page du petit cours, ainsi que sur la page de traitement, vers l'administration générale - La fonctionalité "Voir sur le site" de Django est utilisée pour renvoyer sur la page de la demande. Si des modifications sont apportées, il faut choisir "Enregistrer et continuer les modifications", puis cliquer sur "Voir sur le site". Le workflow n'est pas forcément optimal, mais permet au COF d'accéder facilement à la demande si un traitement manuel ou complexe est nécessaire - et de facilement revenir à la vue de traitement.
246 lines
8.8 KiB
Python
246 lines
8.8 KiB
Python
from functools import reduce
|
|
|
|
from django.contrib.auth.models import User
|
|
from django.db import models
|
|
from django.db.models import Min
|
|
from django.utils.functional import cached_property
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
|
|
def choices_length(choices):
|
|
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
|
|
|
|
|
|
LEVELS_CHOICES = (
|
|
("college", _("Collège")),
|
|
("lycee", _("Lycée")),
|
|
("prepa1styear", _("Prépa 1ère année / L1")),
|
|
("prepa2ndyear", _("Prépa 2ème année / L2")),
|
|
("licence3", _("Licence 3")),
|
|
("master1", _("Master (1ère ou 2ème année)")),
|
|
("other", _("Autre (préciser dans les commentaires)")),
|
|
)
|
|
|
|
|
|
class PetitCoursSubject(models.Model):
|
|
name = models.CharField(_("Matière"), max_length=30)
|
|
users = models.ManyToManyField(
|
|
User, related_name="petits_cours_matieres", through="PetitCoursAbility"
|
|
)
|
|
|
|
class Meta:
|
|
app_label = "gestioncof"
|
|
verbose_name = "Matière de petits cours"
|
|
verbose_name_plural = "Matières des petits cours"
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class PetitCoursAbility(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
matiere = models.ForeignKey(
|
|
PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matière")
|
|
)
|
|
niveau = models.CharField(
|
|
_("Niveau"), choices=LEVELS_CHOICES, max_length=choices_length(LEVELS_CHOICES)
|
|
)
|
|
agrege = models.BooleanField(_("Agrégé"), default=False)
|
|
|
|
class Meta:
|
|
app_label = "gestioncof"
|
|
verbose_name = "Compétence petits cours"
|
|
verbose_name_plural = "Compétences des petits cours"
|
|
|
|
def __str__(self):
|
|
return "{:s} - {!s} - {:s}".format(
|
|
self.user.username, self.matiere, self.niveau
|
|
)
|
|
|
|
@cached_property
|
|
def counter(self) -> int:
|
|
"""Le compteur d'attribution associé au professeur pour cette matière."""
|
|
|
|
return PetitCoursAttributionCounter.get_uptodate(self.user, self.matiere).count
|
|
|
|
|
|
class PetitCoursDemande(models.Model):
|
|
name = models.CharField(_("Nom/prénom"), max_length=200)
|
|
email = models.CharField(_("Adresse email"), max_length=300)
|
|
phone = models.CharField(_("Téléphone (facultatif)"), max_length=20, blank=True)
|
|
quand = models.CharField(
|
|
_("Quand ?"),
|
|
help_text=_(
|
|
"Indiquez ici la période désirée pour les petits"
|
|
" cours (vacances scolaires, semaine, week-end)."
|
|
),
|
|
max_length=300,
|
|
blank=True,
|
|
)
|
|
freq = models.CharField(
|
|
_("Fréquence"),
|
|
help_text=_(
|
|
"Indiquez ici la fréquence envisagée "
|
|
"(hebdomadaire, 2 fois par semaine, ...)"
|
|
),
|
|
max_length=300,
|
|
blank=True,
|
|
)
|
|
lieu = models.CharField(
|
|
_("Lieu (si préférence)"),
|
|
help_text=_("Si vous avez avez une préférence sur le lieu."),
|
|
max_length=300,
|
|
blank=True,
|
|
)
|
|
|
|
matieres = models.ManyToManyField(
|
|
PetitCoursSubject, verbose_name=_("Matières"), related_name="demandes"
|
|
)
|
|
agrege_requis = models.BooleanField(_("Agrégé requis"), default=False)
|
|
niveau = models.CharField(
|
|
_("Niveau"),
|
|
default="",
|
|
choices=LEVELS_CHOICES,
|
|
max_length=choices_length(LEVELS_CHOICES),
|
|
)
|
|
|
|
remarques = models.TextField(_("Remarques et précisions"), blank=True)
|
|
|
|
traitee = models.BooleanField(_("Traitée"), default=False)
|
|
traitee_par = models.ForeignKey(
|
|
User, on_delete=models.CASCADE, blank=True, null=True
|
|
)
|
|
processed = models.DateTimeField(_("Date de traitement"), blank=True, null=True)
|
|
created = models.DateTimeField(_("Date de création"), auto_now_add=True)
|
|
|
|
def get_candidates(self, redo=False):
|
|
"""
|
|
Donne la liste des profs disponibles pour chaque matière de la demande.
|
|
- On ne donne que les agrégés si c'est demandé
|
|
- Si ``redo`` vaut ``True``, cela signifie qu'on retraite la demande et
|
|
il ne faut pas proposer à nouveau des noms qui ont déjà été proposés
|
|
"""
|
|
for matiere in self.matieres.all():
|
|
candidates = PetitCoursAbility.objects.filter(
|
|
matiere=matiere,
|
|
niveau=self.niveau,
|
|
user__profile__is_cof=True,
|
|
user__profile__petits_cours_accept=True,
|
|
)
|
|
if self.agrege_requis:
|
|
candidates = candidates.filter(agrege=True)
|
|
if redo:
|
|
attrs = self.petitcoursattribution_set.filter(matiere=matiere)
|
|
already_proposed = [attr.user for attr in attrs]
|
|
candidates = candidates.exclude(user__in=already_proposed)
|
|
candidates = candidates.order_by("?").select_related().all()
|
|
yield (matiere, candidates)
|
|
|
|
def get_proposals(self, *, max_candidates: int = None, redo: bool = False):
|
|
"""Calcule une proposition de profs pour la demande.
|
|
|
|
Args:
|
|
max_candidates (optionnel; défaut: `None`): Le nombre maximum de
|
|
candidats à proposer par demande. Si `None` ou non spécifié,
|
|
il n'y a pas de limite.
|
|
|
|
redo (optionel; défaut: `False`): Détermine si on re-calcule les
|
|
propositions pour la demande (les professeurs à qui on a déjà
|
|
proposé cette demande sont exclus).
|
|
|
|
Returns:
|
|
proposals: Le dictionnaire qui associe à chaque matière la liste
|
|
des professeurs proposés. Les matières pour lesquelles aucun
|
|
professeur n'est disponible ne sont pas présentes dans
|
|
`proposals`.
|
|
unsatisfied: La liste des matières pour lesquelles aucun
|
|
professeur n'est disponible.
|
|
"""
|
|
|
|
proposals = {}
|
|
unsatisfied = []
|
|
for matiere, candidates in self.get_candidates(redo=redo):
|
|
if not candidates:
|
|
unsatisfied.append(matiere)
|
|
else:
|
|
proposals[matiere] = matiere_proposals = []
|
|
|
|
candidates = sorted(candidates, key=lambda c: c.counter)
|
|
candidates = candidates[:max_candidates]
|
|
for candidate in candidates[:max_candidates]:
|
|
matiere_proposals.append(candidate.user)
|
|
|
|
return proposals, unsatisfied
|
|
|
|
def get_absolute_url(self):
|
|
from django.urls import reverse
|
|
|
|
return reverse("petits-cours-demande-details", kwargs={"pk": str(self.id)})
|
|
|
|
class Meta:
|
|
app_label = "gestioncof"
|
|
verbose_name = "Demande de petits cours"
|
|
verbose_name_plural = "Demandes de petits cours"
|
|
|
|
def __str__(self):
|
|
return "Demande {:d} du {:s}".format(self.id, self.created.strftime("%d %b %Y"))
|
|
|
|
|
|
class PetitCoursAttribution(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
demande = models.ForeignKey(
|
|
PetitCoursDemande, on_delete=models.CASCADE, verbose_name=_("Demande")
|
|
)
|
|
matiere = models.ForeignKey(
|
|
PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matière")
|
|
)
|
|
date = models.DateTimeField(_("Date d'attribution"), auto_now_add=True)
|
|
rank = models.IntegerField("Rang dans l'email")
|
|
selected = models.BooleanField(_("Sélectionné par le demandeur"), default=False)
|
|
|
|
class Meta:
|
|
app_label = "gestioncof"
|
|
verbose_name = "Attribution de petits cours"
|
|
verbose_name_plural = "Attributions de petits cours"
|
|
|
|
def __str__(self):
|
|
return "Attribution de la demande {:d} à {:s} pour {!s}".format(
|
|
self.demande.id, self.user.username, self.matiere
|
|
)
|
|
|
|
|
|
class PetitCoursAttributionCounter(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
matiere = models.ForeignKey(
|
|
PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matiere")
|
|
)
|
|
count = models.IntegerField("Nombre d'envois", default=0)
|
|
|
|
@classmethod
|
|
def get_uptodate(cls, user, matiere):
|
|
"""
|
|
Donne le compteur de l'utilisateur pour cette matière. Si le compteur
|
|
n'existe pas encore, il est initialisé avec le minimum des valeurs des
|
|
compteurs de tout le monde.
|
|
"""
|
|
counter, created = cls.objects.get_or_create(user=user, matiere=matiere)
|
|
if created:
|
|
mincount = (
|
|
cls.objects.filter(matiere=matiere)
|
|
.exclude(user=user)
|
|
.aggregate(Min("count"))["count__min"]
|
|
)
|
|
counter.count = mincount or 0
|
|
counter.save()
|
|
return counter
|
|
|
|
class Meta:
|
|
app_label = "gestioncof"
|
|
verbose_name = "Compteur d'attribution de petits cours"
|
|
verbose_name_plural = "Compteurs d'attributions de petits cours"
|
|
|
|
def __str__(self):
|
|
return "{:d} demandes envoyées à {:s} pour {!s}".format(
|
|
self.count, self.user.username, self.matiere
|
|
)
|