Merge branch 'Elarnon/petitscours_proposals_cleanup' into 'master'

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

See merge request klub-dev-ens/gestioCOF!332
This commit is contained in:
Ludovic Stephan 2018-12-03 21:15:17 +01:00
commit dee2f4badc
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.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models import Min from django.db.models import Min
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -55,6 +56,12 @@ class PetitCoursAbility(models.Model):
self.user.username, self.matiere, self.niveau 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): class PetitCoursDemande(models.Model):
name = models.CharField(_("Nom/prénom"), max_length=200) name = models.CharField(_("Nom/prénom"), max_length=200)
@ -128,6 +135,42 @@ class PetitCoursDemande(models.Model):
candidates = candidates.order_by("?").select_related().all() candidates = candidates.order_by("?").select_related().all()
yield (matiere, candidates) 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: class Meta:
app_label = "gestioncof" app_label = "gestioncof"
verbose_name = "Demande de petits cours" verbose_name = "Demande de petits cours"

View file

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

View file

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