# coding: utf-8 from django.shortcuts import render, get_object_or_404, redirect from django.core import mail from django.core.mail import EmailMessage from django.forms import ModelForm from django import forms from django.forms.models import inlineformset_factory, BaseInlineFormSet from django.contrib.auth.models import User from django.views.generic import ListView from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django.template import loader, Context from django.conf import settings from django.contrib.auth.decorators import login_required from django.db.models import Min from gestioncof.models import CofProfile from gestioncof.petits_cours_models import * from gestioncof.decorators import buro_required from gestioncof.shared import lock_table, unlock_tables from captcha.fields import ReCaptchaField from datetime import datetime import base64 import simplejson def render_template(template_path, data): tmpl = loader.get_template(template_path) context = Context(data) return tmpl.render(context) class DemandeListView(ListView): model = PetitCoursDemande template_name = "petits_cours_demandes_list.html" paginate_by = 20 def get_queryset(self): return PetitCoursDemande.objects.order_by('traitee', '-id').all() @method_decorator(buro_required) def dispatch(self, *args, **kwargs): return super(DemandeListView, self).dispatch(*args, **kwargs) @buro_required def details(request, demande_id): demande = get_object_or_404(PetitCoursDemande, id=demande_id) attributions = PetitCoursAttribution.objects.filter(demande=demande).all() return render(request, "details_demande_petit_cours.html", {"demande": demande, "attributions": attributions}) def _get_attrib_counter(user, matiere): counter, created = PetitCoursAttributionCounter.\ objects.get_or_create(user=user, matiere=matiere) if created: mincount = PetitCoursAttributionCounter.objects.\ filter(matiere=matiere).exclude(user=user).all().\ aggregate(Min('count')) counter.count = mincount['count__min'] counter.save() return counter def _get_demande_candidates(demande, redo=False): for matiere in demande.matieres.all(): candidates = PetitCoursAbility.objects.filter(matiere=matiere, niveau=demande.niveau) candidates = candidates.filter(user__profile__is_cof=True, user__profile__petits_cours_accept=True) if demande.agrege_requis: candidates = candidates.filter(agrege=True) if redo: attributions = PetitCoursAttribution.objects.\ filter(demande=demande, matiere=matiere).all() for attrib in attributions: candidates = candidates.exclude(user=attrib.user) candidates = candidates.order_by('?').select_related().all() yield (matiere, candidates) @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 _get_demande_candidates(demande, redo): if candidates: tuples = [] for candidate in candidates: user = candidate.user tuples.append((candidate, _get_attrib_counter(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 = attribdata.items() proposed_mails = _generate_eleve_email(demande, proposed_for) mainmail = render_template("petits-cours-mail-demandeur.txt", {"proposals": proposals, "unsatisfied": unsatisfied, "extra": '' }) return render(request, "traitement_demande_petit_cours.html", {"demande": demande, "unsatisfied": unsatisfied, "proposals": proposals, "proposed_for": proposed_for, "proposed_mails": proposed_mails, "mainmail": mainmail, "attribdata": base64.b64encode(simplejson.dumps(attribdata)), "redo": redo, "errors": errors, }) def _generate_eleve_email(demande, proposed_for): proposed_mails = [] for user, matieres in proposed_for: msg = render_template("petits-cours-mail-eleve.txt", {"demande": demande, "matieres": matieres}) proposed_mails.append((user, msg)) return proposed_mails def _traitement_other_preparing(request, demande): redo = "redo" in request.POST unsatisfied = [] proposals = {} proposed_for = {} attribdata = {} errors = [] for matiere, candidates in _get_demande_candidates(demande, 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" % (matiere.id, choice_id)]) if choice == -1: continue if choice not in candidates: errors.append(u"Choix invalide pour la proposition %d" "en %s" % (choice_id + 1, matiere)) continue user = candidates[choice] if user in proposals[matiere]: errors.append(u"La proposition %d en %s est un doublon" % (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(u"Aucune proposition pour %s" % (matiere,)) elif len(proposals[matiere]) < 3: errors.append(u"Seulement %d proposition%s pour %s" % (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 _get_demande_candidates(demande, redo): if candidates: tuples = [] for candidate in candidates: user = candidate.user tuples.append((candidate, _get_attrib_counter(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, "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(simplejson.loads(base64.b64decode(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 = render_template("petits-cours-mail-demandeur.txt", {"proposals": proposals_list, "unsatisfied": unsatisfied, "extra": extra}) frommail = settings.PETITS_COURS_FROM bccaddress = settings.PETITS_COURS_BCC replyto = settings.PETITS_COURS_REPLYTO mails_to_send = [] for (user, msg) in proposed_mails: msg = EmailMessage("Petits cours ENS par le COF", msg, frommail, [user.email], [bccaddress], headers={'Reply-To': replyto}) mails_to_send.append(msg) mails_to_send.append(EmailMessage("Cours particuliers ENS", mainmail, frommail, [demande.email], [bccaddress], headers={'Reply-To': replyto})) connection = mail.get_connection(fail_silently=True) connection.send_messages(mails_to_send) lock_table(PetitCoursAttributionCounter, PetitCoursAttribution, User) 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() unlock_tables() demande.traitee = True demande.traitee_par = request.user demande.processed = datetime.now() demande.save() return render(request, "traitement_demande_petit_cours_success.html", {"demande": demande, "redo": redo, }) class BaseMatieresFormSet(BaseInlineFormSet): def clean(self): super(BaseMatieresFormSet, self).clean() if any(self.errors): # Don't bother validating the formset unless each form is # valid on its own return matieres = [] for i in range(0, self.total_form_count()): form = self.forms[i] if not form.cleaned_data: continue matiere = form.cleaned_data['matiere'] niveau = form.cleaned_data['niveau'] delete = form.cleaned_data['DELETE'] if not delete and (matiere, niveau) in matieres: raise forms.ValidationError( "Vous ne pouvez pas vous inscrire deux fois pour la " "même matiere avec le même niveau.") matieres.append((matiere, niveau)) @login_required def inscription(request): profile, created = CofProfile.objects.get_or_create(user=request.user) if not profile.is_cof: return redirect("cof-denied") MatieresFormSet = inlineformset_factory(User, PetitCoursAbility, fields=("matiere", "niveau", "agrege",), formset=BaseMatieresFormSet) 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() lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User, PetitCoursSubject) abilities = PetitCoursAbility.objects.\ filter(user=request.user).all() for ability in abilities: counter = _get_attrib_counter(ability.user, ability.matiere) unlock_tables() 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}) class DemandeForm(ModelForm): captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'}) def __init__(self, *args, **kwargs): super(DemandeForm, self).__init__(*args, **kwargs) self.fields['matieres'].help_text = '' class Meta: model = PetitCoursDemande fields = ('name', 'email', 'phone', 'quand', 'freq', 'lieu', 'matieres', 'agrege_requis', 'niveau', 'remarques') widgets = {'matieres': forms.CheckboxSelectMultiple} @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})