Améliore les mails automatiques du BdA Les mails du BdA sont maintenant tous chargés depuis des templates gérés par le système de templates de Django, et plus par de l'interpolation de chaîne de caractères. Ceci permet en particulier d'utiliser (et de configurer) la localisation de Django afin d'afficher les dates de façon uniforme (et sans "hack" à la `date_no_seconds`) dans un format comportant un "à" entre le jour et l'heure. See merge request !113
414 lines
16 KiB
Python
414 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
|
|
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
|
|
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 PetitCoursDemande, \
|
|
PetitCoursAttribution, PetitCoursAttributionCounter, PetitCoursAbility, \
|
|
PetitCoursSubject
|
|
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 json
|
|
|
|
|
|
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 = list(attribdata.items())
|
|
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
|
mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", {
|
|
"proposals": proposals,
|
|
"unsatisfied": unsatisfied,
|
|
"extra":
|
|
'<textarea name="extra" '
|
|
'style="width:99%; height: 90px;">'
|
|
'</textarea>'
|
|
})
|
|
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(json.dumps(attribdata)
|
|
.encode('utf_8')),
|
|
"redo": redo,
|
|
"errors": errors,
|
|
})
|
|
|
|
|
|
def _generate_eleve_email(demande, proposed_for):
|
|
proposed_mails = []
|
|
for user, matieres in proposed_for:
|
|
msg = loader.render_to_string("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("Choix invalide pour la proposition %d"
|
|
"en %s" % (choice_id + 1, matiere))
|
|
continue
|
|
user = candidates[choice]
|
|
if user in proposals[matiere]:
|
|
errors.append("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("Aucune proposition pour %s" % (matiere,))
|
|
elif len(proposals[matiere]) < 3:
|
|
errors.append("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(json.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 = loader.render_to_string("petits-cours-mail-demandeur.txt", {
|
|
"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, 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:
|
|
_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})
|