import json from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required 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, redirect, render from django.template import loader 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 from gestioncof.models import CofProfile from petitscours.forms import DemandeForm, MatieresFormSet from petitscours.models import ( PetitCoursAbility, PetitCoursAttribution, PetitCoursAttributionCounter, PetitCoursDemande, ) class DemandeListView(ListView): queryset = PetitCoursDemande.objects.prefetch_related("matieres").order_by( "traitee", "-id" ) template_name = "petitscours/demande_list.html" paginate_by = 20 class DemandeDetailView(DetailView): model = PetitCoursDemande queryset = PetitCoursDemande.objects.prefetch_related( "petitcoursattribution_set", "matieres" ) template_name = "petitscours/demande_detail.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, unsatisfied = demande.get_proposals(redo=redo, max_candidates=3) return _finalize_traitement(request, demande, proposals, unsatisfied, redo) def _finalize_traitement( request, demande, proposals, unsatisfied, redo=False, errors=None ): 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 = ( "Cours particuliers ENS", loader.render_to_string( "petitscours/mails/demandeur.txt", context={ "proposals": proposals.items(), "unsatisfied": unsatisfied, "extra": '", }, ), ) if errors is not None: for error in errors: messages.error(request, error) return render( request, "petitscours/traitement_demande.html", { "demande": demande, "unsatisfied": unsatisfied, "proposals": proposals.items(), "proposed_for": proposed_for.items(), "proposed_mails": proposed_mails, "mainmail": mainmail, "attribdata": json.dumps(attribdata), "redo": redo, }, ) def _generate_eleve_email(demande, proposed_for): subject = "Petits cours ENS par le COF" return [ ( user, ( subject, loader.render_to_string( "petitscours/mails/eleve.txt", context={"demande": demande, "matieres": matieres}, ), ), ) for user, matieres in proposed_for.items() ] def _traitement_other_preparing(request, demande): redo = "redo" in request.POST unsatisfied = [] proposals = {} errors = [] for matiere, candidates in demande.get_candidates(redo): if candidates: candidates = dict( [(candidate.user.id, candidate.user) for candidate in candidates] ) 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) 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, unsatisfied, 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, unsatisfied = demande.get_proposals(redo=redo) return render( request, "petitscours/traitement_demande_autre_niveau.html", { "demande": demande, "unsatisfied": unsatisfied, "proposals": proposals.items(), }, ) 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) proposed_mails = _generate_eleve_email(demande, proposed_for) mainmail_object = "Cours particuliers ENS" mainmail_body = loader.render_to_string( "petitscours/mails/demandeur.txt", context={ "proposals": proposals.items(), "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, users in proposals.items(): for rank, user in enumerate(users): # TODO(AD): Prefer PetitCoursAttributionCounter.get_uptodate() 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, "petitscours/traitement_demande_success.html", {"demande": demande, "redo": redo}, ) @login_required def inscription(request): profile, created = CofProfile.objects.get_or_create(user=request.user) if not profile.is_cof: return redirect("cof-denied") 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, "petitscours/inscription.html", { "formset": formset, "success": success, "receive_proposals": profile.petits_cours_accept, "remarques": profile.petits_cours_remarques, }, ) @csrf_exempt def demande(request, *, raw: bool = False): success = False if request.method == "POST": form = DemandeForm(request.POST) if form.is_valid(): form.save() success = True else: form = DemandeForm() template_name = "petitscours/demande.html" if raw: template_name = "petitscours/demande_raw.html" return render(request, template_name, {"form": form, "success": success})