Merge branch 'Kerl/modernize_petitscours'

This commit is contained in:
Qwann 2017-02-11 02:54:22 +01:00
commit 5136e394d4
6 changed files with 179 additions and 152 deletions

View file

@ -84,29 +84,30 @@ visualiser la dernière version du code.
Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il
est fortement conseillé d'utiliser un environnement virtuel pour Python. est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer mercurial, pip, les librairies de développement de Il vous faudra installer pip, les librairies de développement de python, un
python, un client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et dérivées
dérivées (Ubuntu, ...) : (Ubuntu, ...) :
sudo apt-get install mercurial python-pip python-dev libmysqlclient-dev sudo apt-get install python-pip python-dev libmysqlclient-dev redis-server
redis-server
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant : (le dossier où se trouve ce README), et créez-le maintenant :
virtualenv env virtualenv env -p $(which python3)
Pour l'activer, il faut faire L'option `-p` sert à préciser l'exécutable python à utiliser. Vous devez choisir
python3, si c'est la version de python par défaut sur votre système, ceci n'est
pas nécessaire. Pour l'activer, il faut faire
. env/bin/activate . env/bin/activate
dans le même dossier. dans le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis les fichiers Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements.txt` et `requirements-devel.txt` : `requirements-devel.txt` :
pip install -r requirements.txt -r requirements-devel.txt pip install -r requirements-devel.txt
Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`. Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`.

View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from captcha.fields import ReCaptchaField
from django import forms
from django.forms import ModelForm
from django.forms.models import inlineformset_factory, BaseInlineFormSet
from django.contrib.auth.models import User
from gestioncof.petits_cours_models import PetitCoursDemande, PetitCoursAbility
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))
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}
MatieresFormSet = inlineformset_factory(
User,
PetitCoursAbility,
fields=("matiere", "niveau", "agrege"),
formset=BaseMatieresFormSet
)

View file

@ -1,14 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division from functools import reduce
from __future__ import print_function
from __future__ import unicode_literals
from django.db import models from django.db import models
from django.db.models import Min
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from django.utils.six.moves import reduce
def choices_length(choices): def choices_length(choices):
@ -24,7 +21,6 @@ LEVELS_CHOICES = (
) )
@python_2_unicode_compatible
class PetitCoursSubject(models.Model): class PetitCoursSubject(models.Model):
name = models.CharField(_("Matière"), max_length=30) name = models.CharField(_("Matière"), max_length=30)
users = models.ManyToManyField(User, related_name="petits_cours_matieres", users = models.ManyToManyField(User, related_name="petits_cours_matieres",
@ -38,7 +34,6 @@ class PetitCoursSubject(models.Model):
return self.name return self.name
@python_2_unicode_compatible
class PetitCoursAbility(models.Model): class PetitCoursAbility(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière")) matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière"))
@ -52,11 +47,11 @@ class PetitCoursAbility(models.Model):
verbose_name_plural = "Compétences des petits cours" verbose_name_plural = "Compétences des petits cours"
def __str__(self): def __str__(self):
return "%s - %s - %s" % (self.user.username, return "{:s} - {!s} - {:s}".format(
self.matiere, self.niveau) self.user.username, self.matiere, self.niveau
)
@python_2_unicode_compatible
class PetitCoursDemande(models.Model): class PetitCoursDemande(models.Model):
name = models.CharField(_("Nom/prénom"), max_length=200) name = models.CharField(_("Nom/prénom"), max_length=200)
email = models.CharField(_("Adresse email"), max_length=300) email = models.CharField(_("Adresse email"), max_length=300)
@ -70,7 +65,7 @@ class PetitCoursDemande(models.Model):
freq = models.CharField( freq = models.CharField(
_("Fréquence"), _("Fréquence"),
help_text=_("Indiquez ici la fréquence envisagée " help_text=_("Indiquez ici la fréquence envisagée "
+ "(hebdomadaire, 2 fois par semaine, ...)"), "(hebdomadaire, 2 fois par semaine, ...)"),
max_length=300, blank=True) max_length=300, blank=True)
lieu = models.CharField( lieu = models.CharField(
_("Lieu (si préférence)"), _("Lieu (si préférence)"),
@ -94,16 +89,42 @@ class PetitCoursDemande(models.Model):
blank=True, null=True) blank=True, null=True)
created = models.DateTimeField(_("Date de création"), auto_now_add=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: class Meta:
verbose_name = "Demande de petits cours" verbose_name = "Demande de petits cours"
verbose_name_plural = "Demandes de petits cours" verbose_name_plural = "Demandes de petits cours"
def __str__(self): def __str__(self):
return "Demande %d du %s" % (self.id, return "Demande {:d} du {:s}".format(
self.created.strftime("%d %b %Y")) self.id, self.created.strftime("%d %b %Y")
)
@python_2_unicode_compatible
class PetitCoursAttribution(models.Model): class PetitCoursAttribution(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
demande = models.ForeignKey(PetitCoursDemande, verbose_name=_("Demande")) demande = models.ForeignKey(PetitCoursDemande, verbose_name=_("Demande"))
@ -118,20 +139,40 @@ class PetitCoursAttribution(models.Model):
verbose_name_plural = "Attributions de petits cours" verbose_name_plural = "Attributions de petits cours"
def __str__(self): def __str__(self):
return "Attribution de la demande %d à %s pour %s" \ return "Attribution de la demande {:d} à {:s} pour {!s}".format(
% (self.demande.id, self.user.username, self.matiere) self.demande.id, self.user.username, self.matiere
)
@python_2_unicode_compatible
class PetitCoursAttributionCounter(models.Model): class PetitCoursAttributionCounter(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matiere")) matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matiere"))
count = models.IntegerField("Nombre d'envois", default=0) 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
counter.save()
return counter
class Meta: class Meta:
verbose_name = "Compteur d'attribution de petits cours" verbose_name = "Compteur d'attribution de petits cours"
verbose_name_plural = "Compteurs d'attributions de petits cours" verbose_name_plural = "Compteurs d'attributions de petits cours"
def __str__(self): def __str__(self):
return "%d demandes envoyées à %s pour %s" \ return "{:d} demandes envoyées à {:s} pour {!s}".format(
% (self.count, self.user.username, self.matiere) self.count, self.user.username, self.matiere
)

View file

@ -1,37 +1,27 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division import json
from __future__ import print_function from datetime import datetime
from __future__ import unicode_literals
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.core import mail from django.core import mail
from django.core.mail import EmailMessage 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.contrib.auth.models import User
from django.views.generic import ListView from django.views.generic import ListView, DetailView
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.template import loader from django.template import loader
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Min
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from gestioncof.petits_cours_models import PetitCoursDemande, \ from gestioncof.petits_cours_models import (
PetitCoursAttribution, PetitCoursAttributionCounter, PetitCoursAbility, \ PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursSubject PetitCoursAbility, PetitCoursSubject
)
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
from gestioncof.shared import lock_table, unlock_tables from gestioncof.shared import lock_table, unlock_tables
from captcha.fields import ReCaptchaField
from datetime import datetime
import base64
import json
class DemandeListView(ListView): class DemandeListView(ListView):
model = PetitCoursDemande model = PetitCoursDemande
@ -41,47 +31,17 @@ class DemandeListView(ListView):
def get_queryset(self): def get_queryset(self):
return PetitCoursDemande.objects.order_by('traitee', '-id').all() return PetitCoursDemande.objects.order_by('traitee', '-id').all()
@method_decorator(buro_required)
def dispatch(self, *args, **kwargs):
return super(DemandeListView, self).dispatch(*args, **kwargs)
class DemandeDetailView(DetailView):
model = PetitCoursDemande
template_name = "gestioncof/details_demande_petit_cours.html"
context_object_name = "demande"
@buro_required def get_context_data(self, **kwargs):
def details(request, demande_id): context = super(DemandeDetailView, self).get_context_data(**kwargs)
demande = get_object_or_404(PetitCoursDemande, id=demande_id) obj = self.object
attributions = PetitCoursAttribution.objects.filter(demande=demande).all() context['attributions'] = obj.petitcoursattribution_set.all()
return render(request, "details_demande_petit_cours.html", return context
{"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 @buro_required
@ -95,12 +55,15 @@ def traitement(request, demande_id, redo=False):
proposed_for = {} proposed_for = {}
unsatisfied = [] unsatisfied = []
attribdata = {} attribdata = {}
for matiere, candidates in _get_demande_candidates(demande, redo): for matiere, candidates in demande.get_candidates(redo):
if candidates: if candidates:
tuples = [] tuples = []
for candidate in candidates: for candidate in candidates:
user = candidate.user user = candidate.user
tuples.append((candidate, _get_attrib_counter(user, matiere))) tuples.append((
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere)
))
tuples = sorted(tuples, key=lambda c: c[1].count) tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples) candidates, _ = zip(*tuples)
candidates = candidates[0:min(3, len(candidates))] candidates = candidates[0:min(3, len(candidates))]
@ -170,7 +133,7 @@ def _traitement_other_preparing(request, demande):
proposed_for = {} proposed_for = {}
attribdata = {} attribdata = {}
errors = [] errors = []
for matiere, candidates in _get_demande_candidates(demande, redo): for matiere, candidates in demande.get_candidates(redo):
if candidates: if candidates:
candidates = dict([(candidate.user.id, candidate.user) candidates = dict([(candidate.user.id, candidate.user)
for candidate in candidates]) for candidate in candidates])
@ -178,17 +141,19 @@ def _traitement_other_preparing(request, demande):
proposals[matiere] = [] proposals[matiere] = []
for choice_id in range(min(3, len(candidates))): for choice_id in range(min(3, len(candidates))):
choice = int( choice = int(
request.POST["proposal-%d-%d" % (matiere.id, choice_id)]) request.POST["proposal-{:d}-{:d}"
.format(matiere.id, choice_id)]
)
if choice == -1: if choice == -1:
continue continue
if choice not in candidates: if choice not in candidates:
errors.append("Choix invalide pour la proposition %d" errors.append("Choix invalide pour la proposition {:d}"
"en %s" % (choice_id + 1, matiere)) "en {!s}".format(choice_id + 1, matiere))
continue continue
user = candidates[choice] user = candidates[choice]
if user in proposals[matiere]: if user in proposals[matiere]:
errors.append("La proposition %d en %s est un doublon" errors.append("La proposition {:d} en {!s} est un doublon"
% (choice_id + 1, matiere)) .format(choice_id + 1, matiere))
continue continue
proposals[matiere].append(user) proposals[matiere].append(user)
attribdata[matiere.id].append(user.id) attribdata[matiere.id].append(user.id)
@ -197,12 +162,13 @@ def _traitement_other_preparing(request, demande):
else: else:
proposed_for[user].append(matiere) proposed_for[user].append(matiere)
if not proposals[matiere]: if not proposals[matiere]:
errors.append("Aucune proposition pour %s" % (matiere,)) errors.append("Aucune proposition pour {!s}".format(matiere))
elif len(proposals[matiere]) < 3: elif len(proposals[matiere]) < 3:
errors.append("Seulement %d proposition%s pour %s" errors.append("Seulement {:d} proposition{:s} pour {!s}"
% (len(proposals[matiere]), .format(
"s" if len(proposals[matiere]) > 1 else "", len(proposals[matiere]),
matiere)) "s" if len(proposals[matiere]) > 1 else "",
matiere))
else: else:
unsatisfied.append(matiere) unsatisfied.append(matiere)
return _finalize_traitement(request, demande, proposals, proposed_for, return _finalize_traitement(request, demande, proposals, proposed_for,
@ -219,12 +185,15 @@ def _traitement_other(request, demande, redo):
proposed_for = {} proposed_for = {}
unsatisfied = [] unsatisfied = []
attribdata = {} attribdata = {}
for matiere, candidates in _get_demande_candidates(demande, redo): for matiere, candidates in demande.get_candidates(redo):
if candidates: if candidates:
tuples = [] tuples = []
for candidate in candidates: for candidate in candidates:
user = candidate.user user = candidate.user
tuples.append((candidate, _get_attrib_counter(user, matiere))) tuples.append((
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere)
))
tuples = sorted(tuples, key=lambda c: c[1].count) tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples) candidates, _ = zip(*tuples)
attribdata[matiere.id] = [] attribdata[matiere.id] = []
@ -313,37 +282,11 @@ def _traitement_post(request, demande):
}) })
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 @login_required
def inscription(request): def inscription(request):
profile, created = CofProfile.objects.get_or_create(user=request.user) profile, created = CofProfile.objects.get_or_create(user=request.user)
if not profile.is_cof: if not profile.is_cof:
return redirect("cof-denied") return redirect("cof-denied")
MatieresFormSet = inlineformset_factory(User, PetitCoursAbility,
fields=("matiere", "niveau",
"agrege",),
formset=BaseMatieresFormSet)
success = False success = False
if request.method == "POST": if request.method == "POST":
formset = MatieresFormSet(request.POST, instance=request.user) formset = MatieresFormSet(request.POST, instance=request.user)
@ -354,10 +297,14 @@ def inscription(request):
profile.save() profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User, lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User,
PetitCoursSubject) PetitCoursSubject)
abilities = PetitCoursAbility.objects \ abilities = (
.filter(user=request.user).all() PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities: for ability in abilities:
_get_attrib_counter(ability.user, ability.matiere) PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
unlock_tables() unlock_tables()
success = True success = True
formset = MatieresFormSet(instance=request.user) formset = MatieresFormSet(instance=request.user)
@ -369,20 +316,6 @@ def inscription(request):
"remarques": profile.petits_cours_remarques}) "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 @csrf_exempt
def demande(request): def demande(request):
success = False success = False

View file

@ -1,12 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from gestioncof.petits_cours_views import DemandeListView from gestioncof.petits_cours_views import DemandeListView, DemandeDetailView
from gestioncof import views, petits_cours_views from gestioncof import views, petits_cours_views
from gestioncof.decorators import buro_required
export_patterns = [ export_patterns = [
url(r'^members$', views.export_members), url(r'^members$', views.export_members),
@ -24,10 +21,11 @@ petitcours_patterns = [
name='petits-cours-demande'), name='petits-cours-demande'),
url(r'^demande-raw$', petits_cours_views.demande_raw, url(r'^demande-raw$', petits_cours_views.demande_raw,
name='petits-cours-demande-raw'), name='petits-cours-demande-raw'),
url(r'^demandes$', DemandeListView.as_view(), url(r'^demandes$',
buro_required(DemandeListView.as_view()),
name='petits-cours-demandes-list'), name='petits-cours-demandes-list'),
url(r'^demandes/(?P<demande_id>\d+)$', url(r'^demandes/(?P<pk>\d+)$',
petits_cours_views.details, buro_required(DemandeDetailView.as_view()),
name='petits-cours-demande-details'), name='petits-cours-demande-details'),
url(r'^demandes/(?P<demande_id>\d+)/traitement$', url(r'^demandes/(?P<demande_id>\d+)/traitement$',
petits_cours_views.traitement, petits_cours_views.traitement,