Merge branch 'master' into aureplop/site-cof

This commit is contained in:
Aurélien Delobelle 2019-01-06 00:56:21 +01:00
commit 84c88dfd5e
37 changed files with 1706 additions and 244 deletions

View file

@ -20,7 +20,7 @@ from gestioncof.models import (
SurveyQuestion,
SurveyQuestionAnswer,
)
from gestioncof.petits_cours_models import (
from petitscours.models import (
PetitCoursAbility,
PetitCoursAttribution,
PetitCoursAttributionCounter,

View file

@ -1,23 +1,55 @@
from django.contrib.auth.decorators import user_passes_test
from functools import wraps
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.exceptions import PermissionDenied
from django.shortcuts import render
def is_cof(user):
try:
profile = user.profile
return profile.is_cof
except Exception:
return False
def cof_required(view_func):
"""Décorateur qui vérifie que l'utilisateur est connecté et membre du COF.
- Si l'utilisteur n'est pas connecté, il est redirigé vers la page de
connexion
- Si l'utilisateur est connecté mais pas membre du COF, il obtient une
page d'erreur lui demandant de s'inscrire au COF
"""
def is_cof(user):
try:
return user.profile.is_cof
except AttributeError:
return False
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if is_cof(request.user):
return view_func(request, *args, **kwargs)
return render(request, "cof-denied.html", status=403)
return login_required(_wrapped_view)
cof_required = user_passes_test(is_cof)
def buro_required(view_func):
"""Décorateur qui vérifie que l'utilisateur est connecté et membre du burô.
- Si l'utilisateur n'est pas connecté, il est redirigé vers la page de
connexion
- Si l'utilisateur est connecté mais pas membre du burô, il obtient une
page d'erreur 403 Forbidden
"""
def is_buro(user):
try:
profile = user.profile
return profile.is_buro
except Exception:
return False
def is_buro(user):
try:
return user.profile.is_buro
except AttributeError:
return False
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if is_buro(request.user):
return view_func(request, *args, **kwargs)
buro_required = user_passes_test(is_buro)
return render(request, "buro-denied.html", status=403)
return login_required(_wrapped_view)

View file

@ -14,7 +14,7 @@ from django.contrib.auth.models import User
from django.core.management import call_command
from gestioncof.management.base import MyBaseCommand
from gestioncof.petits_cours_models import (
from petitscours.models import (
LEVELS_CHOICES,
PetitCoursAbility,
PetitCoursAttributionCounter,

View file

@ -5,7 +5,7 @@ from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from bda.models import Spectacle
from gestioncof.petits_cours_models import choices_length
from petitscours.models import choices_length
TYPE_COMMENT_FIELD = (("text", _("Texte long")), ("char", _("Texte court")))

View file

@ -1,62 +0,0 @@
from captcha.fields import ReCaptchaField
from django import forms
from django.contrib.auth.models import User
from django.forms import ModelForm
from django.forms.models import BaseInlineFormSet, inlineformset_factory
from gestioncof.petits_cours_models import PetitCoursAbility, PetitCoursDemande
class BaseMatieresFormSet(BaseInlineFormSet):
def clean(self):
super().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))
class DemandeForm(ModelForm):
captcha = ReCaptchaField(attrs={"theme": "clean", "lang": "fr"})
def __init__(self, *args, **kwargs):
super().__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}
MatieresFormSet = inlineformset_factory(
User,
PetitCoursAbility,
fields=("matiere", "niveau", "agrege"),
formset=BaseMatieresFormSet,
)

View file

@ -1,191 +0,0 @@
from functools import reduce
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Min
from django.utils.translation import ugettext_lazy as _
def choices_length(choices):
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
LEVELS_CHOICES = (
("college", _("Collège")),
("lycee", _("Lycée")),
("prepa1styear", _("Prépa 1ère année / L1")),
("prepa2ndyear", _("Prépa 2ème année / L2")),
("licence3", _("Licence 3")),
("other", _("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 __str__(self):
return self.name
class PetitCoursAbility(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
matiere = models.ForeignKey(
PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matière")
)
niveau = models.CharField(
_("Niveau"), choices=LEVELS_CHOICES, max_length=choices_length(LEVELS_CHOICES)
)
agrege = models.BooleanField(_("Agrégé"), default=False)
class Meta:
verbose_name = "Compétence petits cours"
verbose_name_plural = "Compétences des petits cours"
def __str__(self):
return "{:s} - {!s} - {:s}".format(
self.user.username, self.matiere, self.niveau
)
class PetitCoursDemande(models.Model):
name = models.CharField(_("Nom/prénom"), max_length=200)
email = models.CharField(_("Adresse email"), max_length=300)
phone = models.CharField(_("Téléphone (facultatif)"), max_length=20, blank=True)
quand = models.CharField(
_("Quand ?"),
help_text=_(
"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(
_("Fréquence"),
help_text=_(
"Indiquez ici la fréquence envisagée "
"(hebdomadaire, 2 fois par semaine, ...)"
),
max_length=300,
blank=True,
)
lieu = models.CharField(
_("Lieu (si préférence)"),
help_text=_("Si vous avez avez une préférence sur le lieu."),
max_length=300,
blank=True,
)
matieres = models.ManyToManyField(
PetitCoursSubject, verbose_name=_("Matières"), related_name="demandes"
)
agrege_requis = models.BooleanField(_("Agrégé requis"), default=False)
niveau = models.CharField(
_("Niveau"),
default="",
choices=LEVELS_CHOICES,
max_length=choices_length(LEVELS_CHOICES),
)
remarques = models.TextField(_("Remarques et précisions"), blank=True)
traitee = models.BooleanField(_("Traitée"), default=False)
traitee_par = models.ForeignKey(
User, on_delete=models.CASCADE, blank=True, null=True
)
processed = models.DateTimeField(_("Date de traitement"), blank=True, null=True)
created = models.DateTimeField(_("Date de création"), auto_now_add=True)
def get_candidates(self, redo=False):
"""
Donne la liste des profs disponibles pour chaque matière de la demande.
- On ne donne que les agrégés si c'est demandé
- Si ``redo`` vaut ``True``, cela signifie qu'on retraite la demande et
il ne faut pas proposer à nouveau des noms qui ont déjà été proposés
"""
for matiere in self.matieres.all():
candidates = PetitCoursAbility.objects.filter(
matiere=matiere,
niveau=self.niveau,
user__profile__is_cof=True,
user__profile__petits_cours_accept=True,
)
if self.agrege_requis:
candidates = candidates.filter(agrege=True)
if redo:
attrs = self.petitcoursattribution_set.filter(matiere=matiere)
already_proposed = [attr.user for attr in attrs]
candidates = candidates.exclude(user__in=already_proposed)
candidates = candidates.order_by("?").select_related().all()
yield (matiere, candidates)
class Meta:
verbose_name = "Demande de petits cours"
verbose_name_plural = "Demandes de petits cours"
def __str__(self):
return "Demande {:d} du {:s}".format(self.id, self.created.strftime("%d %b %Y"))
class PetitCoursAttribution(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
demande = models.ForeignKey(
PetitCoursDemande, on_delete=models.CASCADE, verbose_name=_("Demande")
)
matiere = models.ForeignKey(
PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matière")
)
date = models.DateTimeField(_("Date d'attribution"), auto_now_add=True)
rank = models.IntegerField("Rang dans l'email")
selected = models.BooleanField(_("Sélectionné par le demandeur"), default=False)
class Meta:
verbose_name = "Attribution de petits cours"
verbose_name_plural = "Attributions de petits cours"
def __str__(self):
return "Attribution de la demande {:d} à {:s} pour {!s}".format(
self.demande.id, self.user.username, self.matiere
)
class PetitCoursAttributionCounter(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
matiere = models.ForeignKey(
PetitCoursSubject, on_delete=models.CASCADE, verbose_name=_("Matiere")
)
count = models.IntegerField("Nombre d'envois", default=0)
@classmethod
def get_uptodate(cls, user, matiere):
"""
Donne le compteur de l'utilisateur pour cette matière. Si le compteur
n'existe pas encore, il est initialisé avec le minimum des valeurs des
compteurs de tout le monde.
"""
counter, created = cls.objects.get_or_create(user=user, matiere=matiere)
if created:
mincount = (
cls.objects.filter(matiere=matiere)
.exclude(user=user)
.aggregate(Min("count"))["count__min"]
)
counter.count = mincount or 0
counter.save()
return counter
class Meta:
verbose_name = "Compteur d'attribution de petits cours"
verbose_name_plural = "Compteurs d'attributions de petits cours"
def __str__(self):
return "{:d} demandes envoyées à {:s} pour {!s}".format(
self.count, self.user.username, self.matiere
)

View file

@ -1,401 +0,0 @@
import json
from custommail.shortcuts import render_custom_mail
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.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 gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
from gestioncof.petits_cours_models import (
PetitCoursAbility,
PetitCoursAttribution,
PetitCoursAttributionCounter,
PetitCoursDemande,
)
class DemandeListView(ListView):
queryset = PetitCoursDemande.objects.prefetch_related("matieres").order_by(
"traitee", "-id"
)
template_name = "petits_cours_demandes_list.html"
paginate_by = 20
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"
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 = {}
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)
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 = 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,
},
)
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
]
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,
)
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 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)
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,
},
)
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)
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"]
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 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()
demande.traitee = True
demande.traitee_par = request.user
demande.processed = timezone.now()
demande.save()
return render(
request,
"gestioncof/traitement_demande_petit_cours_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,
"inscription-petit-cours.html",
{
"formset": formset,
"success": success,
"receive_proposals": profile.petits_cours_accept,
"remarques": profile.petits_cours_remarques,
},
)
@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}
)

View file

@ -15,7 +15,7 @@ def messages_on_out_login(request, user, **kwargs):
@receiver(cas_user_authenticated)
def mesagges_on_cas_login(request, user, **kwargs):
def messages_on_cas_login(request, user, **kwargs):
msg = _("Connexion à GestioCOF par CAS réussie. Bienvenue {}.").format(
user.get_short_name()
)

View file

@ -1,2 +0,0 @@
{% extends "base_title.html" %}

View file

@ -0,0 +1,5 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Section réservée au Burô.</h2>
{% endblock %}

View file

@ -1,19 +0,0 @@
{% extends "base.html" %}
{% load bootstrap %}
{% block content %}
<div class="petitcours-raw">
{% if success %}
<p class="success">Votre demande a été enregistrée avec succès !</p>
{% else %}
<form id="demandecours" method="post" action="{% url "petits-cours-demande-raw" %}">
{% csrf_token %}
<table>
{{ form | bootstrap }}
</table>
<input type="submit" class="btn-submit" value="Enregistrer" />
</form>
{% endif %}
</div>
{% endblock %}

View file

@ -1,16 +0,0 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Demande de petits cours</h2>
{% if success %}
<p class="success">Votre demande a été enregistrée avec succès !</p>
{% else %}
<form id="demandecours" method="post" action="{% url "petits-cours-demande" %}">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" class="btn-submit" value="Enregistrer" />
</form>
{% endif %}
{% endblock %}

View file

@ -1,14 +0,0 @@
{% load staticfiles %}
<table class="table table-striped">
<tr class="danger"><td><strong>Date</strong></td><td> {{ demande.created }}</td></tr>
<tr class="warning"><td><strong>Nom/prénom</strong></td><td> {{ demande.name }}</td></tr>
<tr><td><strong>Email</strong></td><td> {{ demande.email }}</td></tr>
<tr><td><strong>Téléphone</strong></td><td> {{ demande.phone }}</td></tr>
<tr><td><strong>Lieu</strong></td><td> {{ demande.lieu }}</td></tr>
<tr><td><strong>Quand</strong></td><td> {{ demande.quand }}</td></tr>
<tr><td><strong>Fréquence</strong></td><td> {{ demande.freq }}</td></tr>
<tr><td><strong>Matières</strong></td><td> {% for matiere in demande.matieres.all %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}</td></tr>
<tr><td><strong>Niveau souhaité</strong></td><td> {{ demande.get_niveau_display }}</td></tr>
<tr><td><strong>Agrégé requis</strong></td><td> <img src="{% if demande.agrege_requis %}{% static "images/yes.png" %}{% else %}{% static "images/no.png" %}{% endif %}" /></td></tr>
<tr><td><strong>Remarques</strong></td><td> {{ demande.remarques }}</td></tr>
</table>

View file

@ -1,39 +0,0 @@
{% extends "base_title_petitscours.html" %}
{% load staticfiles %}
{% block page_size %}col-sm-8{% endblock %}
{% block realcontent %}
<h2>Demande de petits cours</h2>
{% include "details_demande_petit_cours_infos.html" %}
<hr />
<table class="table table-striped">
<tr><td><strong>Traitée</strong></td><td> <img src="{% if demande.traitee %}{% static "images/yes.png" %}{% else %}{% static "images/no.png" %}{% endif %}" /></td></tr>
{% if demande.traitee %}
<tr><td><strong>Traitée par</strong></td><td> {{ demande.traitee_par }}</td></tr>
<tr><td><strong>Traitée le</strong></td><td> {{ demande.processed }}</td></tr>
<tr><td>
<strong>Attributions</strong></td><td>
<ul>
{% for attrib in attributions %}
<li>{{ attrib.user.get_full_name }} pour {{ attrib.matiere }}, {{ attrib.date|date:"d E Y" }}</li>
{% endfor %}
</ul>
</td>
</tr>
{% endif %}
</table>
{% if demande.traitee %}
<div style="text-align: right;">
<form action="{% url "petits-cours-demande-retraitement" demande.id %}">
<input class="btn btn-primary" type="submit" value="Retraiter">
</form>
</div>
{% else %}
<div style="text-align: right;">
<form action="{% url "petits-cours-demande-traitement" demande.id %}">
<input class="btn btn-primary" type="submit" value="Traiter">
</form>
</div>
{% endif %}
{% endblock %}

View file

@ -1,51 +0,0 @@
{% extends "base_title_petitscours.html" %}
{% block realcontent %}
<h2>Traitement de la demande de petits cours {{ demande.id }}</h2>
{% include "details_demande_petit_cours_infos.html" %}
<hr />
{% if errors %}
<div class="error">
Attention:
<ul>{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>
</div>
{% endif %}
{% if unsatisfied %}
<div class="error">
Attention: Impossible de trouver des propositions pour les matières suivantes:
<ul>
{% for matiere in unsatisfied %}<li>{{ matiere }}</li>{% endfor %}
</ul>
</div>
{% endif %}
{% if proposals %}
<form method="post">
{% csrf_token %}
Propositions:
<ul>
{% for proposeduser, matieres in proposed_for %}
<li>{{ proposeduser }} pour {% for matiere in matieres %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}</li>
{% endfor %}
</ul>
<h4>Mails pour les membres proposés :</h4>
{% for proposeduser, mail in proposed_mails %}
<h5>Pour {{ proposeduser }}:</h5>
{% with object=mail.0 content=mail.1 %}
<pre>{{ object }}</pre>
<pre>{{ content }}</pre>
{% endwith %}
{% endfor %}
<h4>Mail pour l'auteur de la demande :</h4>
{% with object=mainmail.0 content=mainmail.1 %}
<pre style="margin-top: 15px;">{{ object }}</pre>
<pre style="margin-top: 15px;">{{ content|safe }}</pre>
{% endwith %}
<input type="hidden" name="attribdata" value="{{ attribdata }}" />
{% if redo %}<input type="hidden" name="redo" value="1" />{% endif %}
<input class="btn btn-primary pull-right" type="submit" value="Valider le {% if redo %}re{% endif %}traitement de la demande" />
</form>
{% else %}
<h3>Impossible de trouver des propositions pour cette demande</h3>
<div class="error" style="font-size: 1.6em; margin-top: 10px;">Traitement manuel obligatoire !</div>
{% endif %}
{% endblock %}

View file

@ -1,58 +0,0 @@
{% extends "base_title_petitscours.html" %}
{% load staticfiles %}
{% block realcontent %}
<h2>Traitement de la demande de petits cours {{ demande.id }}</h2>
{% include "details_demande_petit_cours_infos.html" %}
<hr />
<div class="error">
Attention: demande de petits cours spécifiant le niveau "Autre niveau": choisissez les candidats correspondant aux remarques de la demande. S'il y a moins de 3 candidats adaptés, ne mettre que ceux qui conviennent, pas besoin de faire du bourrage :)
</div>
{% if unsatisfied %}
<div class="error">
Attention: Impossible de trouver des propositions pour les matières suivantes:
<ul>
{% for matiere in unsatisfied %}<li>{{ matiere }}</li>{% endfor %}
</ul>
</div>
{% endif %}
{% if proposals %}
<form method="post">
{% csrf_token %}
{% for matiere, users in proposals %}
<h4>{{ matiere }}</h4>
{% for i in "012"|make_list %}{% if i|add:"0" < users|length %}
<div style="float: left; width: 340px;">
Proposition {{ i|add:"1" }}:
<select id="proposal-{{ matiere.id }}-{{ i }}" name="proposal-{{ matiere.id }}-{{ i }}">
<option value="-1" data-description="">-------</option>
{% for user in users %}
<option value="{{ user.id }}" data-description="{{ user.profile.petits_cours_remarques }}">{{ user }}</option>
{% endfor %}
</select>
</div>
<div style="float: right; width: 580px;"><p id="proposal-description-{{ matiere.id }}-{{ i }}"></p></div>
<div class="spacer"></div>
<script type="text/javascript">
$('#proposal-{{ matiere.id }}-{{ i }}').change(function(){
var selected = $(this).find(':selected');
var content = "";
if (selected.val() != "-1")
content = (selected.data('description')) ? selected.data('description'): "Pas de remarques";
$('#proposal-description-{{ matiere.id }}-{{ i }}').html(content);
}).trigger('change');
</script>
<hr />
{% endif %}
{% endfor %}
{% endfor %}
<input type="hidden" name="preparing" value="1" />
{% if redo %}<input type="hidden" name="redo" value="1" />{% endif %}
<input type="submit" value="Préparer le {% if redo %}re{% endif %}traitement de la demande" />
</form>
{% else %}
<h3>Impossible de trouver des propositions pour cette demande</h3>
<div class="error" style="font-size: 1.6em; margin-top: 10px;">Traitement manuel obligatoire !</div>
{% endif %}
{% endblock %}

View file

@ -1,7 +0,0 @@
{% extends "base_title_petitscours.html" %}
{% block realcontent %}
<h2>Traitement de la demande de petits cours {{ demande.id }}</h2>
<div class="success" style="margin: 15px 0px; font-size: 1.4em;">Demande {{ demande.id }} de {{ demande.name }} {% if redo %}re{% endif %}traitée avec succès !</div>
<a href="{% url "petits-cours-demandes-list" %}">Retour à la liste des demandes</a>
{% endblock %}

View file

@ -1,40 +0,0 @@
{% load bootstrap %}
{{ formset.non_form_errors.as_ul }}
<table id="bda_formset" class="form table">
{{ formset.management_form }}
{% for form in formset.forms %}
{% if forloop.first %}
<thead><tr>
{% for field in form.visible_fields %}
<th class="bda-field-{{ field.name }}">
{% if field.name != "DELETE" %}
{{ field.label|safe|capfirst }}
{% endif %}
<sup>{{ forloop.counter }}</sup>
</th>
{% endfor %}
</tr></thead>
<tbody class="bda_formset_content">
{% endif %}
<tr class="{% cycle 'row1' 'row2' %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
{% for field in form.visible_fields %}
{% if field.name != "DELETE" and field.name != "priority" %}
<td class="bda-field-{{ field.name }}">
{% if forloop.first %}
{{ form.non_field_errors }}
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
{% endif %}
{{ field | bootstrap }}
</td>
{% endif %}
{% endfor %}
<td class="tools-cell"><div class="tools">
<input type="checkbox" name="{{ form.DELETE.html_name }}" style="display: none;" />
<a href="javascript://" class="glyphicon glyphicon-remove remove-btn" title="Supprimer"></a>
</div>
<div class="spacer"></div>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -1,117 +0,0 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block extra_head %}
<script src="{% static "js/jquery-ui.min.js" %}" type="text/javascript"></script>
<script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
<link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
{% endblock %}
{% block realcontent %}
<script type="text/javascript">
var django = {
"jQuery": jQuery.noConflict(true)
};
(function($) {
cloneMore = function(selector, type) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
$(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}
deleteButtonHandler = function(elem) {
elem.bind("click", function() {
var deleteInput = $(this).prev(),
form = $(this).parents(".dynamic-form").first();
// callback
// toggle options.predeleteCssClass and toggle checkbox
if (form.hasClass("has_original")) {
form.toggleClass("predelete");
if (deleteInput.attr("checked")) {
deleteInput.attr("checked", false);
} else {
deleteInput.attr("checked", true);
}
}
// callback
});
};
$(document).ready(function($) {
deleteButtonHandler($("table#bda_formset tbody.bda_formset_content").find("a.remove-btn"));
$("table#bda_formset tbody.bda_formset_content").sortable({
handle: "a.drag-handler",
items: "tr",
axis: "y",
appendTo: 'body',
forceHelperSize: true,
placeholder: 'ui-sortable-placeholder',
forcePlaceholderSize: true,
containment: 'form#bda_form',
tolerance: 'pointer',
start: function(evt, ui) {
var template = "",
len = ui.item.children("td").length;
for (var i = 0; i < len; i++) {
template += "<td style='height:" + (ui.item.outerHeight() + 12 ) + "px' class='placeholder-cell'>&nbsp;</td>"
}
template += "";
ui.placeholder.html(template);
},
stop: function(evt, ui) {
// Toggle div.table twice to remove webkits border-spacing bug
$("table#bda_formset").toggle().toggle();
},
});
$("#bda_form").bind("submit", function(){
var sortable_field_name = "priority";
var i = 1;
$(".bda_formset_content").find("tr").each(function(){
var fields = $(this).find("td :input[value]"),
select = $(this).find("td select");
if (select.val() && fields.serialize()) {
$(this).find("input[name$='"+sortable_field_name+"']").val(i);
i++;
}
});
});
});
})(django.jQuery);
</script>
<h2 class="no-bottom-margin">Inscription pour donner des cours particuliers</h2>
{% if success %}
<p class="success table-top">Votre inscription a été mise à jour avec succès !</p>
{% endif %}
<form class="form-horizontal petit-cours_form" id="bda_form" method="post" action="{% url 'petits-cours-inscription' %}">
{% csrf_token %}
<div class="table-top" style="margin-left:0px; margin-right:0px; font-size: 1.25em; font-weight: bold; color: #DE826B;"><input type="checkbox" name="receive_proposals" {% if receive_proposals %}checked="checked"{% endif %} /> Recevoir des propositions de petits cours</div>
{% include "inscription-petit-cours-formset.html" %}
<div class="inscription-bottom">
<input type="button" class="btn btn-default pull-right" value="Ajouter une autre matière" id="add_more" />
<script>
django.jQuery('#add_more').click(function() {
cloneMore('tbody.bda_formset_content tr:last-child', 'petitcoursability_set');
});
</script><br />
<div style="margin: 10px 0px;">
<span style="vertical-align: top; font-weight: bold;">Remarques:</span> <textarea name="remarques" style="width: 60%; height: 60px;">{{ remarques }}</textarea>
</div>
<input type="submit" class="btn btn-primary pull-right" value="Enregistrer" />
<p class="footnotes">
<sup>1</sup>: spécifiez les matières pour lesquelles vous êtes compétent<br />
<sup>2</sup>: spécifiez pour chaque matière le ou les niveaux pour lesquels vous souhaitez recevoir des demandes<br />
<sup>3</sup>: spécifiez si vous êtes titulaire de l'agrégation pour cette matière<br />
<sup>4</sup>: pour supprimer une ligne, cliquez sur la croix puis appuyer sur <tt>Enregistrer</tt><br />
</p>
</div>
</form>
{% endblock %}

View file

@ -1,47 +0,0 @@
{% extends "base_title_petitscours.html" %}
{% load staticfiles %}
{% block realcontent %}
<h2>Demandes de petits cours</h2>
{% if object_list %}
<table class="table table-stripped">
<thead>
<th>Nom</th>
<th>Matières</th>
<th>Date</th>
<th>Traitée</th>
<th>par</th>
<th></th>
</thead>
<tbody>
{% for demande in object_list %}
<tr>
<td>{{ demande.name }}</td>
<td>{% for matiere in demande.matieres.all %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}</td>
<td>{{ demande.created|date:"d E Y" }}</td>
<td style="text-align: center;"><img src="{% if demande.traitee %}{% static "images/yes.png" %}{% else %}{% static "images/no.png" %}{% endif %}" /></td>
<td>{% if demande.traitee_par %}{{ demande.traitee_par.username }}{% else %}<span class="glyphicon glyphicon-ban-circle"></span>{% endif %}</td>
<td><a href="{% url "petits-cours-demande-details" demande.id %}" class="see_detail">Détails</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="{% url "petits-cours-demandes-list" %}?page={{ page_obj.previous_page_number }}">&lt;</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="{% url "petits-cours-demandes-list" %}?page={{ page_obj.next_page_number }}">&gt;</a>
{% endif %}
</span>
</div>
{% endif %}
{% else %}
<p>Aucune demande :(</p>
{% endif %}
{% endblock %}

View file

@ -76,7 +76,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
"last_name": "last",
"email": "username@mail.net",
"is_cof": "1",
}
},
),
)
@ -111,7 +111,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
"email": "user@mail.net",
"is_cof": "1",
"user_exists": "1",
}
},
),
)
@ -137,7 +137,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
data = dict(
self._minimal_data,
**{"username": u.username, "email": "user@mail.net", "user_exists": "1"}
**{"username": u.username, "email": "user@mail.net", "user_exists": "1"},
)
if is_cof:
data["is_cof"] = "1"
@ -197,7 +197,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
"events-0-option_{}".format(o2.pk): [str(oc3.pk)],
"events-0-comment_{}".format(cf1.pk): "comment 1",
"events-0-comment_{}".format(cf2.pk): "",
}
},
),
)

View file

@ -1,8 +1,7 @@
from django.conf.urls import url
from gestioncof import petits_cours_views, views
from gestioncof import views
from gestioncof.decorators import buro_required
from gestioncof.petits_cours_views import DemandeDetailView, DemandeListView
export_patterns = [
url(r"^members$", views.export_members, name="cof.membres_export"),
@ -21,40 +20,6 @@ export_patterns = [
url(r"^mega$", views.export_mega, name="cof.mega_export"),
]
petitcours_patterns = [
url(
r"^inscription$",
petits_cours_views.inscription,
name="petits-cours-inscription",
),
url(r"^demande$", petits_cours_views.demande, name="petits-cours-demande"),
url(
r"^demande-raw$",
petits_cours_views.demande_raw,
name="petits-cours-demande-raw",
),
url(
r"^demandes$",
buro_required(DemandeListView.as_view()),
name="petits-cours-demandes-list",
),
url(
r"^demandes/(?P<pk>\d+)$",
buro_required(DemandeDetailView.as_view()),
name="petits-cours-demande-details",
),
url(
r"^demandes/(?P<demande_id>\d+)/traitement$",
petits_cours_views.traitement,
name="petits-cours-demande-traitement",
),
url(
r"^demandes/(?P<demande_id>\d+)/retraitement$",
petits_cours_views.retraitement,
name="petits-cours-demande-retraitement",
),
]
surveys_patterns = [
url(
r"^(?P<survey_id>\d+)/status$",