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
est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer mercurial, pip, les librairies de développement de
python, un client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et
dérivées (Ubuntu, ...) :
Il vous faudra installer pip, les librairies de développement de python, un
client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et dérivées
(Ubuntu, ...) :
sudo apt-get install mercurial python-pip python-dev libmysqlclient-dev
redis-server
sudo apt-get install python-pip python-dev libmysqlclient-dev redis-server
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(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
dans le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis les fichiers
`requirements.txt` et `requirements-devel.txt` :
Vous pouvez maintenant installer les dépendances Python depuis le fichier
`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`.

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

View file

@ -1,37 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import json
from datetime import datetime
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.generic import ListView, DetailView
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.petits_cours_models import (
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursAbility, PetitCoursSubject
)
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
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
@ -41,47 +31,17 @@ class DemandeListView(ListView):
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)
class DemandeDetailView(DetailView):
model = PetitCoursDemande
template_name = "gestioncof/details_demande_petit_cours.html"
context_object_name = "demande"
@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)
def get_context_data(self, **kwargs):
context = super(DemandeDetailView, self).get_context_data(**kwargs)
obj = self.object
context['attributions'] = obj.petitcoursattribution_set.all()
return context
@buro_required
@ -95,12 +55,15 @@ def traitement(request, demande_id, redo=False):
proposed_for = {}
unsatisfied = []
attribdata = {}
for matiere, candidates in _get_demande_candidates(demande, redo):
for matiere, candidates in demande.get_candidates(redo):
if candidates:
tuples = []
for candidate in candidates:
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)
candidates, _ = zip(*tuples)
candidates = candidates[0:min(3, len(candidates))]
@ -170,7 +133,7 @@ def _traitement_other_preparing(request, demande):
proposed_for = {}
attribdata = {}
errors = []
for matiere, candidates in _get_demande_candidates(demande, redo):
for matiere, candidates in demande.get_candidates(redo):
if candidates:
candidates = dict([(candidate.user.id, candidate.user)
for candidate in candidates])
@ -178,17 +141,19 @@ def _traitement_other_preparing(request, demande):
proposals[matiere] = []
for choice_id in range(min(3, len(candidates))):
choice = int(
request.POST["proposal-%d-%d" % (matiere.id, choice_id)])
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" % (choice_id + 1, matiere))
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"
% (choice_id + 1, 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)
@ -197,12 +162,13 @@ def _traitement_other_preparing(request, demande):
else:
proposed_for[user].append(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:
errors.append("Seulement %d proposition%s pour %s"
% (len(proposals[matiere]),
"s" if len(proposals[matiere]) > 1 else "",
matiere))
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,
@ -219,12 +185,15 @@ def _traitement_other(request, demande, redo):
proposed_for = {}
unsatisfied = []
attribdata = {}
for matiere, candidates in _get_demande_candidates(demande, redo):
for matiere, candidates in demande.get_candidates(redo):
if candidates:
tuples = []
for candidate in candidates:
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)
candidates, _ = zip(*tuples)
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
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)
@ -354,10 +297,14 @@ def inscription(request):
profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User,
PetitCoursSubject)
abilities = PetitCoursAbility.objects \
.filter(user=request.user).all()
abilities = (
PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities:
_get_attrib_counter(ability.user, ability.matiere)
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
unlock_tables()
success = True
formset = MatieresFormSet(instance=request.user)
@ -369,20 +316,6 @@ def inscription(request):
"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

View file

@ -1,12 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
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.decorators import buro_required
export_patterns = [
url(r'^members$', views.export_members),
@ -24,10 +21,11 @@ petitcours_patterns = [
name='petits-cours-demande'),
url(r'^demande-raw$', petits_cours_views.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'),
url(r'^demandes/(?P<demande_id>\d+)$',
petits_cours_views.details,
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,