gestioCOF/gestioncof/petits_cours_views.py

399 lines
13 KiB
Python
Raw Normal View History

import json
from custommail.shortcuts import render_custom_mail
2013-10-06 11:20:59 +02:00
from django.conf import settings
2017-01-30 13:50:24 +01:00
from django.contrib import messages
from django.contrib.auth.models import User
from django.core import mail
from django.db import transaction
core -- Install django-allauth-ens 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.
2017-10-19 01:12:52 +02:00
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
2013-10-06 11:20:59 +02:00
core -- Install django-allauth-ens 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.
2017-10-19 01:12:52 +02:00
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,
)
2013-10-06 11:20:59 +02:00
2013-10-06 11:20:59 +02:00
class DemandeListView(ListView):
queryset = PetitCoursDemande.objects.prefetch_related("matieres").order_by(
"traitee", "-id"
)
2013-10-06 11:20:59 +02:00
template_name = "petits_cours_demandes_list.html"
paginate_by = 20
2017-02-05 17:35:41 +01:00
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"
2017-02-05 17:35:41 +01:00
def get_context_data(self, **kwargs):
2018-01-16 16:22:52 +01:00
context = super().get_context_data(**kwargs)
obj = self.object
context["attributions"] = obj.petitcoursattribution_set.all()
2017-02-05 17:35:41 +01:00
return context
2013-10-06 11:20:59 +02:00
2013-10-06 11:20:59 +02:00
@buro_required
def traitement(request, demande_id, redo=False):
demande = get_object_or_404(PetitCoursDemande, id=demande_id)
2013-10-06 11:20:59 +02:00
if demande.niveau == "other":
return _traitement_other(request, demande, redo)
2013-10-06 11:20:59 +02:00
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)
2013-10-06 11:20:59 +02:00
candidates, _ = zip(*tuples)
candidates = candidates[0 : min(3, len(candidates))]
attribdata[matiere.id] = []
2013-10-06 11:20:59 +02:00
proposals[matiere] = []
for candidate in candidates:
user = candidate.user
2013-10-06 11:20:59 +02:00
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)
2013-10-06 11:20:59 +02:00
def _finalize_traitement(
request,
demande,
proposals,
proposed_for,
unsatisfied,
attribdata,
redo=False,
errors=None,
):
2013-10-06 11:20:59 +02:00
proposals = proposals.items()
proposed_for = proposed_for.items()
2016-09-22 14:01:01 +02:00
attribdata = list(attribdata.items())
2013-10-06 11:20:59 +02:00
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,
},
)
2013-10-06 11:20:59 +02:00
2013-10-06 11:20:59 +02:00
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
]
2013-10-06 11:20:59 +02:00
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,
)
2013-10-06 11:20:59 +02:00
def _traitement_other(request, demande, redo):
2013-10-06 11:20:59 +02:00
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):
2013-10-06 11:20:59 +02:00
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)
2013-10-06 11:20:59 +02:00
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,
},
)
2013-10-06 11:20:59 +02:00
2013-10-06 11:20:59 +02:00
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))
2013-10-06 11:20:59 +02:00
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)
2013-10-06 11:20:59 +02:00
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"]
2013-10-06 11:20:59 +02:00
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},
)
2013-10-06 11:20:59 +02:00
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)
2013-10-06 11:20:59 +02:00
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()
2013-10-06 11:20:59 +02:00
demande.traitee = True
demande.traitee_par = request.user
demande.processed = timezone.now()
2013-10-06 11:20:59 +02:00
demande.save()
return render(
request,
"gestioncof/traitement_demande_petit_cours_success.html",
{"demande": demande, "redo": redo},
)
2013-10-06 11:20:59 +02:00
core -- Install django-allauth-ens 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.
2017-10-19 01:12:52 +02:00
@cof_required
2013-10-06 11:20:59 +02:00
def inscription(request):
profile, created = CofProfile.objects.get_or_create(user=request.user)
2013-10-06 11:20:59 +02:00
success = False
if request.method == "POST":
formset = MatieresFormSet(request.POST, instance=request.user)
2013-10-06 11:20:59 +02:00
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
)
2013-10-06 11:20:59 +02:00
success = True
formset = MatieresFormSet(instance=request.user)
2013-10-06 11:20:59 +02:00
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,
},
)
2013-10-06 11:20:59 +02:00
@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}
)
2013-10-06 11:20:59 +02:00
2013-10-06 11:20:59 +02:00
@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}
)