From d5b3d3f95840c9519387aa63ebb7c157a426295f Mon Sep 17 00:00:00 2001 From: Guillaume Seguin Date: Sun, 6 Oct 2013 11:20:59 +0200 Subject: [PATCH] Initial import of petit cours stuff --- gestioncof/petits_cours_models.py | 101 ++++++++ gestioncof/petits_cours_views.py | 368 ++++++++++++++++++++++++++++++ 2 files changed, 469 insertions(+) create mode 100644 gestioncof/petits_cours_models.py create mode 100644 gestioncof/petits_cours_views.py diff --git a/gestioncof/petits_cours_models.py b/gestioncof/petits_cours_models.py new file mode 100644 index 00000000..e9b1b856 --- /dev/null +++ b/gestioncof/petits_cours_models.py @@ -0,0 +1,101 @@ +# coding: utf-8 + +from django.db import models +from django.contrib.auth.models import User +from django.utils.translation import ugettext_lazy as _ +from django.db.models.signals import post_save + +def choices_length (choices): + return reduce (lambda m, choice: max (m, len (choice[0])), choices, 0) + +LEVELS_CHOICES = ( + ('college', _(u"Collège")), + ('lycee', _(u"Lycée")), + ('prepa1styear', _(u"Prépa 1ère année")), + ('prepa2ndyear', _(u"Prépa 2ème année")), + ('other', _(u"Autre (préciser dans les commentaires)")), +) + +class PetitCoursSubject(models.Model): + name = models.CharField(_("Matière"), max_length = 30) + users = models.ManyToManyField(User, related_name = "petits_cours_matieres", + through = "PetitCoursAbility") + + class Meta: + verbose_name = "Matière de petits cours" + verbose_name_plural = "Matières des petits cours" + + def __unicode__(self): + return self.name + +class PetitCoursAbility(models.Model): + user = models.ForeignKey(User) + matiere = models.ForeignKey(PetitCoursSubject, verbose_name = _(u"Matière")) + niveau = models.CharField (_(u"Niveau"), + choices = LEVELS_CHOICES, + max_length = choices_length (LEVELS_CHOICES)) + agrege = models.BooleanField(_(u"Agrégé"), default = False) + + class Meta: + verbose_name = "Compétence petits cours" + verbose_name_plural = "Compétences des petits cours" + + def __unicode__(self): + return u"%s - %s - %s" % (self.user.username, self.matiere, self.niveau) + +class PetitCoursDemande(models.Model): + name = models.CharField(_(u"Nom/prénom"), max_length = 200) + email = models.CharField(_(u"Adresse email"), max_length = 300) + phone = models.CharField(_(u"Téléphone (facultatif)"), max_length = 20, blank = True) + quand = models.CharField(_(u"Quand ?"), help_text = _(u"Indiquez ici la période désirée pour les petits cours (vacances scolaires, semaine, week-end)."), max_length = 300, blank = True) + freq = models.CharField(_(u"Fréquence"), help_text = _(u"Indiquez ici la fréquence envisagée (hebdomadaire, 2 fois par semaine, ...)"), max_length = 300, blank = True) + lieu = models.CharField(_(u"Lieu (si préférence)"), help_text = _(u"Si vous avez avez une préférence sur le lieu."), max_length = 300, blank = True) + + matieres = models.ManyToManyField(PetitCoursSubject, verbose_name = _(u"Matières"), + related_name = "demandes") + agrege_requis = models.BooleanField(_(u"Agrégé requis"), default = False) + niveau = models.CharField (_(u"Niveau"), + default = "", + choices = LEVELS_CHOICES, + max_length = choices_length (LEVELS_CHOICES)) + + remarques = models.TextField(_(u"Remarques et précisions"), blank = True) + + traitee = models.BooleanField(_(u"Traitée"), default = False) + traitee_par = models.ForeignKey(User, blank = True, null = True) + processed = models.DateTimeField(_(u"Date de traitement"), blank = True) + created = models.DateTimeField(_(u"Date de création"), auto_now_add = True) + + class Meta: + verbose_name = "Demande de petits cours" + verbose_name_plural = "Demandes de petits cours" + + def __unicode__(self): + return u"Demande %d du %s" % (self.id, self.created.strftime("%d %b %Y")) + +class PetitCoursAttribution(models.Model): + user = models.ForeignKey(User) + demande = models.ForeignKey(PetitCoursDemande, verbose_name = _("Demande")) + matiere = models.ForeignKey(PetitCoursSubject, verbose_name = _("Matière")) + date = models.DateTimeField(_(u"Date d'attribution"), auto_now_add = True) + rank = models.IntegerField("Rang dans l'email") + selected = models.BooleanField(_(u"Sélectionné par le demandeur"), default = False) + + class Meta: + verbose_name = "Attribution de petits cours" + verbose_name_plural = "Attributions de petits cours" + + def __unicode__(self): + return u"Attribution de la demande %d à %s pour %s" % (self.demande.id, self.user.username, self.matiere) + +class PetitCoursAttributionCounter(models.Model): + user = models.ForeignKey(User) + matiere = models.ForeignKey(PetitCoursSubject, verbose_name = _("Matiere")) + count = models.IntegerField("Nombre d'envois", default = 0) + + class Meta: + verbose_name = "Compteur d'attribution de petits cours" + verbose_name_plural = "Compteurs d'attributions de petits cours" + + def __unicode__(self): + return u"%d demandes envoyées à %s pour %s" % (self.count, self.user.username, self.matiere) diff --git a/gestioncof/petits_cours_views.py b/gestioncof/petits_cours_views.py new file mode 100644 index 00000000..2e907058 --- /dev/null +++ b/gestioncof/petits_cours_views.py @@ -0,0 +1,368 @@ +# 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, Clipper +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 + + + +@buro_required +def traitement(request, demande_id): + demande = get_object_or_404(PetitCoursDemande, id = demande_id) + if demande.niveau == "other": + return _traitement_other(request, demande) + if request.method == "POST": + return _traitement_post(request, demande) + proposals = {} + proposed_for = {} + unsatisfied = [] + attribdata = {} + 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) + candidates = candidates.values_list('user', flat = True).distinct().order_by('?').all() + tuples = [] + for candidate in candidates: + user = User.objects.get(pk = candidate) + tuples.append((candidate, _get_attrib_counter(user, matiere))) + if tuples: + tuples = sorted(tuples, key = lambda c: c[1].count) + candidates, _ = zip(*tuples) + attribdata[matiere.id] = [] + candidates = candidates[0:min(3, len(candidates))] + proposals[matiere] = [] + for candidate in candidates: + user = User.objects.get(pk = candidate) + 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) + +def _finalize_traitement(request, demande, proposals, proposed_for, + unsatisfied, attribdata, redo = False): + 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, + }) + +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_post(request, demande): + for matiere in demande.matieres.all(): + for choice_id in range(3): + choice = request.POST["proposal-%d-%d"] % (matiere.id, choice_id)]: + pass + return None + return _finalize_traitement(request, demande, proposals, proposed_for, unsatisfied, attribdata) + +def _traitement_other(request, demande): + 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 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) + candidates = candidates.order_by('?').select_related().all() + 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, + }) + +@buro_required +def retraitement(request, demande_id): + demande = get_object_or_404(PetitCoursDemande, id = demande_id) + if request.method == "POST": + return _traitement_post(request, demande) + if demande.niveau == "other": + return _traitement_other(request, demande) + proposals = {} + proposed_for = {} + unsatisfied = [] + attribdata = {} + 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) + attributions = PetitCoursAttribution.objects.filter(demande = demande, matiere = matiere).all() + for attrib in attributions: + candidates = candidates.exclude(user = attrib.user) + candidates = candidates.values_list('user', flat = True).distinct().order_by('?').all() + tuples = [] + for candidate in candidates: + user = User.objects.get(pk = candidate) + tuples.append((candidate, _get_attrib_counter(user, matiere))) + if tuples: + tuples = sorted(tuples, key = lambda c: c[1].count) + candidates, _ = zip(*tuples) + attribdata[matiere.id] = [] + candidates = candidates[0:min(3, len(candidates))] + proposals[matiere] = [] + for candidate in candidates: + user = User.objects.get(pk = candidate) + 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 = True) + +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})