05eeb6a25c
Refer to allauth doc for an accurate features list: http://django-allauth.readthedocs.io/en/latest/ Users can now change their password, ask for a password reset, or set one if they don't have one. In particular, it allows users whose account has been created via a clipper authentication to configure a password before losing their clipper. Even if they have already lost it, they are able to get one using the "Reset password" functionality. Allauth multiple emails management is deactivated. Requests to the related url redirect to the home page. All the login and logout views are replaced by the allauth' ones. It also concerns the Django and Wagtail admin sites. Note that users are no longer logged out of the clipper CAS server when they authenticated via this server. Instead a message suggests the user to disconnect. Clipper connections and `login_clipper` --------------------------------------- - Non-empty `login_clipper` are now unique among `CofProfile` instances. - They are created once for users with a non-empty 'login_clipper' (with the data migration 0014_create_clipper_connections). - The `login_clipper` of CofProfile instances are sync with their clipper connections: * `CofProfile.sync_clipper_connections` method updates the connections based on `login_clipper`. * Signals receivers `sync_clipper…` update `login_clipper` based on connections creations/updates/deletions. Misc ---- - Add NullCharField (model field) which allows to use `unique=True` on CharField (even with empty strings). - Parts of kfet mixins for TestCase are now in shared.tests.testcase, as they are used elsewhere than in the kfet app.
398 lines
13 KiB
Python
398 lines
13 KiB
Python
import json
|
|
|
|
from custommail.shortcuts import render_custom_mail
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.contrib.auth.models import User
|
|
from django.core import mail
|
|
from django.db import transaction
|
|
from django.shortcuts import get_object_or_404, render
|
|
from django.utils import timezone
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.views.generic import DetailView, ListView
|
|
|
|
from gestioncof.decorators import buro_required, cof_required
|
|
from gestioncof.models import CofProfile
|
|
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
|
|
from gestioncof.petits_cours_models import (
|
|
PetitCoursAbility,
|
|
PetitCoursAttribution,
|
|
PetitCoursAttributionCounter,
|
|
PetitCoursDemande,
|
|
)
|
|
|
|
|
|
class DemandeListView(ListView):
|
|
queryset = PetitCoursDemande.objects.prefetch_related("matieres").order_by(
|
|
"traitee", "-id"
|
|
)
|
|
template_name = "petits_cours_demandes_list.html"
|
|
paginate_by = 20
|
|
|
|
|
|
class DemandeDetailView(DetailView):
|
|
model = PetitCoursDemande
|
|
queryset = PetitCoursDemande.objects.prefetch_related(
|
|
"petitcoursattribution_set", "matieres"
|
|
)
|
|
template_name = "gestioncof/details_demande_petit_cours.html"
|
|
context_object_name = "demande"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
obj = self.object
|
|
context["attributions"] = obj.petitcoursattribution_set.all()
|
|
return context
|
|
|
|
|
|
@buro_required
|
|
def traitement(request, demande_id, redo=False):
|
|
demande = get_object_or_404(PetitCoursDemande, id=demande_id)
|
|
if demande.niveau == "other":
|
|
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)
|
|
|
|
|
|
def _finalize_traitement(
|
|
request,
|
|
demande,
|
|
proposals,
|
|
proposed_for,
|
|
unsatisfied,
|
|
attribdata,
|
|
redo=False,
|
|
errors=None,
|
|
):
|
|
proposals = proposals.items()
|
|
proposed_for = proposed_for.items()
|
|
attribdata = list(attribdata.items())
|
|
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
|
mainmail = render_custom_mail(
|
|
"petits-cours-mail-demandeur",
|
|
{
|
|
"proposals": proposals,
|
|
"unsatisfied": unsatisfied,
|
|
"extra": '<textarea name="extra" '
|
|
'style="width:99%; height: 90px;">'
|
|
"</textarea>",
|
|
},
|
|
)
|
|
if errors is not None:
|
|
for error in errors:
|
|
messages.error(request, error)
|
|
return render(
|
|
request,
|
|
"gestioncof/traitement_demande_petit_cours.html",
|
|
{
|
|
"demande": demande,
|
|
"unsatisfied": unsatisfied,
|
|
"proposals": proposals,
|
|
"proposed_for": proposed_for,
|
|
"proposed_mails": proposed_mails,
|
|
"mainmail": mainmail,
|
|
"attribdata": json.dumps(attribdata),
|
|
"redo": redo,
|
|
},
|
|
)
|
|
|
|
|
|
def _generate_eleve_email(demande, proposed_for):
|
|
return [
|
|
(
|
|
user,
|
|
render_custom_mail(
|
|
"petit-cours-mail-eleve", {"demande": demande, "matieres": matieres}
|
|
),
|
|
)
|
|
for user, matieres in proposed_for
|
|
]
|
|
|
|
|
|
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(
|
|
request.POST["proposal-{:d}-{:d}".format(matiere.id, choice_id)]
|
|
)
|
|
if choice == -1:
|
|
continue
|
|
if choice not in candidates:
|
|
errors.append(
|
|
"Choix invalide pour la proposition {:d}"
|
|
"en {!s}".format(choice_id + 1, matiere)
|
|
)
|
|
continue
|
|
user = candidates[choice]
|
|
if user in proposals[matiere]:
|
|
errors.append(
|
|
"La proposition {:d} en {!s} est un doublon".format(
|
|
choice_id + 1, matiere
|
|
)
|
|
)
|
|
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:
|
|
errors.append(
|
|
"Seulement {:d} proposition{:s} pour {!s}".format(
|
|
len(proposals[matiere]),
|
|
"s" if len(proposals[matiere]) > 1 else "",
|
|
matiere,
|
|
)
|
|
)
|
|
else:
|
|
unsatisfied.append(matiere)
|
|
return _finalize_traitement(
|
|
request,
|
|
demande,
|
|
proposals,
|
|
proposed_for,
|
|
unsatisfied,
|
|
attribdata,
|
|
errors=errors,
|
|
)
|
|
|
|
|
|
def _traitement_other(request, demande, redo):
|
|
if request.method == "POST":
|
|
if "preparing" in request.POST:
|
|
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()
|
|
return render(
|
|
request,
|
|
"gestioncof/traitement_demande_petit_cours_autre_niveau.html",
|
|
{
|
|
"demande": demande,
|
|
"unsatisfied": unsatisfied,
|
|
"proposals": proposals,
|
|
"proposed_for": proposed_for,
|
|
},
|
|
)
|
|
|
|
|
|
def _traitement_post(request, demande):
|
|
proposals = {}
|
|
proposed_for = {}
|
|
unsatisfied = []
|
|
extra = request.POST["extra"].strip()
|
|
redo = "redo" in request.POST
|
|
attribdata = request.POST["attribdata"]
|
|
attribdata = dict(json.loads(attribdata))
|
|
for matiere in demande.matieres.all():
|
|
if matiere.id not in attribdata:
|
|
unsatisfied.append(matiere)
|
|
else:
|
|
proposals[matiere] = []
|
|
for user_id in attribdata[matiere.id]:
|
|
user = User.objects.get(pk=user_id)
|
|
proposals[matiere].append(user)
|
|
if user not in proposed_for:
|
|
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},
|
|
)
|
|
frommail = settings.MAIL_DATA["petits_cours"]["FROM"]
|
|
bccaddress = settings.MAIL_DATA["petits_cours"]["BCC"]
|
|
replyto = settings.MAIL_DATA["petits_cours"]["REPLYTO"]
|
|
mails_to_send = []
|
|
for (user, (mail_object, body)) in proposed_mails:
|
|
msg = mail.EmailMessage(
|
|
mail_object,
|
|
body,
|
|
frommail,
|
|
[user.email],
|
|
[bccaddress],
|
|
headers={"Reply-To": replyto},
|
|
)
|
|
mails_to_send.append(msg)
|
|
mails_to_send.append(
|
|
mail.EmailMessage(
|
|
mainmail_object,
|
|
mainmail_body,
|
|
frommail,
|
|
[demande.email],
|
|
[bccaddress],
|
|
headers={"Reply-To": replyto},
|
|
)
|
|
)
|
|
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]):
|
|
counter = PetitCoursAttributionCounter.objects.get(
|
|
user=user, matiere=matiere
|
|
)
|
|
counter.count += 1
|
|
counter.save()
|
|
attrib = PetitCoursAttribution(
|
|
user=user, matiere=matiere, demande=demande, rank=rank + 1
|
|
)
|
|
attrib.save()
|
|
demande.traitee = True
|
|
demande.traitee_par = request.user
|
|
demande.processed = timezone.now()
|
|
demande.save()
|
|
return render(
|
|
request,
|
|
"gestioncof/traitement_demande_petit_cours_success.html",
|
|
{"demande": demande, "redo": redo},
|
|
)
|
|
|
|
|
|
@cof_required
|
|
def inscription(request):
|
|
profile, created = CofProfile.objects.get_or_create(user=request.user)
|
|
success = False
|
|
if request.method == "POST":
|
|
formset = MatieresFormSet(request.POST, instance=request.user)
|
|
if formset.is_valid():
|
|
formset.save()
|
|
profile.petits_cours_accept = "receive_proposals" in request.POST
|
|
profile.petits_cours_remarques = request.POST["remarques"]
|
|
profile.save()
|
|
with transaction.atomic():
|
|
abilities = PetitCoursAbility.objects.filter(user=request.user).all()
|
|
for ability in abilities:
|
|
PetitCoursAttributionCounter.get_uptodate(
|
|
ability.user, ability.matiere
|
|
)
|
|
success = True
|
|
formset = MatieresFormSet(instance=request.user)
|
|
else:
|
|
formset = MatieresFormSet(instance=request.user)
|
|
return render(
|
|
request,
|
|
"inscription-petit-cours.html",
|
|
{
|
|
"formset": formset,
|
|
"success": success,
|
|
"receive_proposals": profile.petits_cours_accept,
|
|
"remarques": profile.petits_cours_remarques,
|
|
},
|
|
)
|
|
|
|
|
|
@csrf_exempt
|
|
def demande(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, "demande-petit-cours.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, "demande-petit-cours-raw.html", {"form": form, "success": success}
|
|
)
|