[petitscours] Extrait la proposition de profs dans une méthode

Ce patch simplifie le code (dupliqué) de calcul des proposition de profs
pour une demande dans une méthode du modèle`Demande`, et l'utilise.  Il
s'agit d'un préparatif pour #208; ce code devra être réutilisé dans le
nouveau système.

J'en ai également profité pour nettoyer deux vues de `petitscours`,
`retraitement` et `demande_raw`, qui dupliquaient les vues `traitement`
et `demande`, en utilisant des arguments nommés.

petitscours/
 * models.py:
    Définition de `get_proposals` pour calculer les propositions de
    profs pour une demande.
 * views.py:
    Utilise `get_proposals` à la place du code copié-collé.  La fonction
    `_finalize_traitement` est maintenant responsable du calcul des
    `proposed_for` et `attribdata` à fournir aux templates.
 * urls.py:
    Passe directement les arguments aux vues plutôt que de faire deux
    fonctions séparées.
This commit is contained in:
Basile Clement 2018-11-25 17:05:55 +01:00
parent c960d97b67
commit 2b8f81c94b
3 changed files with 78 additions and 129 deletions

View file

@ -3,6 +3,7 @@ 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 _
@ -55,6 +56,12 @@ class PetitCoursAbility(models.Model):
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)
@ -128,6 +135,42 @@ class PetitCoursDemande(models.Model):
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
class Meta:
app_label = "gestioncof"
verbose_name = "Demande de petits cours"

View file

@ -7,7 +7,12 @@ from gestioncof.decorators import buro_required
urlpatterns = [
url(r"^inscription$", views.inscription, name="petits-cours-inscription"),
url(r"^demande$", views.demande, name="petits-cours-demande"),
url(r"^demande-raw$", views.demande_raw, name="petits-cours-demande-raw"),
url(
r"^demande-raw$",
views.demande,
kwargs={"raw": True},
name="petits-cours-demande-raw",
),
url(
r"^demandes$",
buro_required(DemandeListView.as_view()),
@ -25,7 +30,8 @@ urlpatterns = [
),
url(
r"^demandes/(?P<demande_id>\d+)/retraitement$",
views.retraitement,
views.traitement,
kwargs={"redo": True},
name="petits-cours-demande-retraitement",
),
]

View file

@ -53,64 +53,27 @@ def traitement(request, demande_id, redo=False):
return _traitement_other(request, demande, redo)
if request.method == "POST":
return _traitement_post(request, demande)
proposals = {}
proposed_for = {}
unsatisfied = []
attribdata = {}
for matiere, candidates in demande.get_candidates(redo):
if candidates:
tuples = []
for candidate in candidates:
user = candidate.user
tuples.append(
(
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere),
)
)
tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples)
candidates = candidates[0 : min(3, len(candidates))]
attribdata[matiere.id] = []
proposals[matiere] = []
for candidate in candidates:
user = candidate.user
proposals[matiere].append(user)
attribdata[matiere.id].append(user.id)
if user not in proposed_for:
proposed_for[user] = [matiere]
else:
proposed_for[user].append(matiere)
else:
unsatisfied.append(matiere)
return _finalize_traitement(
request, demande, proposals, proposed_for, unsatisfied, attribdata, redo
)
@buro_required
def retraitement(request, demande_id):
return traitement(request, demande_id, redo=True)
proposals, unsatisfied = demande.get_proposals(redo=redo, max_candidates=3)
return _finalize_traitement(request, demande, proposals, unsatisfied, redo)
def _finalize_traitement(
request,
demande,
proposals,
proposed_for,
unsatisfied,
attribdata,
redo=False,
errors=None,
request, demande, proposals, unsatisfied, redo=False, errors=None
):
proposals = proposals.items()
proposed_for = proposed_for.items()
attribdata = list(attribdata.items())
attribdata = [
(matiere.id, [user.id for user in users])
for matiere, users in proposals.items()
]
proposed_for = {}
for matiere, users in proposals.items():
for user in users:
proposed_for.setdefault(user, []).append(matiere)
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = render_custom_mail(
"petits-cours-mail-demandeur",
{
"proposals": proposals,
"proposals": proposals.items(),
"unsatisfied": unsatisfied,
"extra": '<textarea name="extra" '
'style="width:99%; height: 90px;">'
@ -126,8 +89,8 @@ def _finalize_traitement(
{
"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
"proposed_for": proposed_for,
"proposals": proposals.items(),
"proposed_for": proposed_for.items(),
"proposed_mails": proposed_mails,
"mainmail": mainmail,
"attribdata": json.dumps(attribdata),
@ -144,7 +107,7 @@ def _generate_eleve_email(demande, proposed_for):
"petit-cours-mail-eleve", {"demande": demande, "matieres": matieres}
),
)
for user, matieres in proposed_for
for user, matieres in proposed_for.items()
]
@ -152,15 +115,12 @@ def _traitement_other_preparing(request, demande):
redo = "redo" in request.POST
unsatisfied = []
proposals = {}
proposed_for = {}
attribdata = {}
errors = []
for matiere, candidates in demande.get_candidates(redo):
if candidates:
candidates = dict(
[(candidate.user.id, candidate.user) for candidate in candidates]
)
attribdata[matiere.id] = []
proposals[matiere] = []
for choice_id in range(min(3, len(candidates))):
choice = int(
@ -183,11 +143,6 @@ def _traitement_other_preparing(request, demande):
)
continue
proposals[matiere].append(user)
attribdata[matiere.id].append(user.id)
if user not in proposed_for:
proposed_for[user] = [matiere]
else:
proposed_for[user].append(matiere)
if not proposals[matiere]:
errors.append("Aucune proposition pour {!s}".format(matiere))
elif len(proposals[matiere]) < 3:
@ -200,15 +155,7 @@ def _traitement_other_preparing(request, demande):
)
else:
unsatisfied.append(matiere)
return _finalize_traitement(
request,
demande,
proposals,
proposed_for,
unsatisfied,
attribdata,
errors=errors,
)
return _finalize_traitement(request, demande, proposals, unsatisfied, errors=errors)
def _traitement_other(request, demande, redo):
@ -217,45 +164,14 @@ def _traitement_other(request, demande, redo):
return _traitement_other_preparing(request, demande)
else:
return _traitement_post(request, demande)
proposals = {}
proposed_for = {}
unsatisfied = []
attribdata = {}
for matiere, candidates in demande.get_candidates(redo):
if candidates:
tuples = []
for candidate in candidates:
user = candidate.user
tuples.append(
(
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere),
)
)
tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples)
attribdata[matiere.id] = []
proposals[matiere] = []
for candidate in candidates:
user = candidate.user
proposals[matiere].append(user)
attribdata[matiere.id].append(user.id)
if user not in proposed_for:
proposed_for[user] = [matiere]
else:
proposed_for[user].append(matiere)
else:
unsatisfied.append(matiere)
proposals = proposals.items()
proposed_for = proposed_for.items()
proposals, unsatisfied = demande.get_proposals(redo=redo)
return render(
request,
"petitscours/traitement_demande_autre_niveau.html",
{
"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
"proposed_for": proposed_for,
"proposals": proposals.items(),
},
)
@ -280,12 +196,10 @@ def _traitement_post(request, demande):
proposed_for[user] = [matiere]
else:
proposed_for[user].append(matiere)
proposals_list = proposals.items()
proposed_for = proposed_for.items()
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail_object, mainmail_body = render_custom_mail(
"petits-cours-mail-demandeur",
{"proposals": proposals_list, "unsatisfied": unsatisfied, "extra": extra},
{"proposals": proposals.items(), "unsatisfied": unsatisfied, "extra": extra},
)
frommail = settings.MAIL_DATA["petits_cours"]["FROM"]
bccaddress = settings.MAIL_DATA["petits_cours"]["BCC"]
@ -314,8 +228,8 @@ def _traitement_post(request, demande):
connection = mail.get_connection(fail_silently=False)
connection.send_messages(mails_to_send)
with transaction.atomic():
for matiere in proposals:
for rank, user in enumerate(proposals[matiere]):
for matiere, users in proposals.items():
for rank, user in enumerate(users):
# TODO(AD): Prefer PetitCoursAttributionCounter.get_uptodate()
counter = PetitCoursAttributionCounter.objects.get(
user=user, matiere=matiere
@ -373,7 +287,7 @@ def inscription(request):
@csrf_exempt
def demande(request):
def demande(request, *, raw: bool = False):
success = False
if request.method == "POST":
form = DemandeForm(request.POST)
@ -382,21 +296,7 @@ def demande(request):
success = True
else:
form = DemandeForm()
return render(
request, "petitscours/demande.html", {"form": form, "success": success}
)
@csrf_exempt
def demande_raw(request):
success = False
if request.method == "POST":
form = DemandeForm(request.POST)
if form.is_valid():
form.save()
success = True
else:
form = DemandeForm()
return render(
request, "petitscours/demande_raw.html", {"form": form, "success": success}
)
template_name = "petitscours/demande.html"
if raw:
template_name = "petitscours/demande_raw.html"
return render(request, template_name, {"form": form, "success": success})