diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1af5efa7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 cof-geek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c5c212e3..1a3d575e 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,9 @@ Il ne vous reste plus qu'à initialiser les modèles de Django avec la commande python manage.py migrate +Charger les mails indispensables au bon fonctionnement de GestioCOF : + + python manage.py syncmails Une base de donnée pré-remplie est disponible en lançant les commandes : diff --git a/bda/admin.py b/bda/admin.py index 0e9b683b..fc10c326 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals +import autocomplete_light +from datetime import timedelta +from custommail.shortcuts import send_mass_custom_mail -from django.core.mail import send_mail from django.contrib import admin from django.db.models import Sum, Count from django.template.defaultfilters import pluralize @@ -13,10 +12,6 @@ from django import forms from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente -from datetime import timedelta - -import autocomplete_light - class ChoixSpectacleInline(admin.TabularInline): model = ChoixSpectacle @@ -72,66 +67,20 @@ class ParticipantAdmin(admin.ModelAdmin): readonly_fields = ("total",) def send_attribs(self, request, queryset): + datatuple = [] for member in queryset.all(): attribs = member.attributions.all() + context = {'member': member.user} + shortname = "" if len(attribs) == 0: - mail = """Cher-e %s, - -Tu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as -obtenu aucune place. - -Nous proposons cependant de nombreuses offres hors-tirage tout au long de -l'année, et nous t'invitons à nous contacter si l'une d'entre elles -t'intéresse ! --- -Le Bureau des Arts - -""" - name = member.user.get_full_name() - mail = mail % name + shortname = "bda-attributions-decus" else: - mail = """Cher-e %s, - -Tu t'es inscrit-e pour le tirage au sort du BdA. Tu as été sélectionné-e -pour les spectacles suivants : - -%s - -*Paiement* -L'intégralité de ces places de spectacles est à régler dès maintenant et AVANT -le %s, au bureau du COF pendant les heures de permanences (du lundi au vendredi -entre 12h et 14h, et entre 18h et 20h). Des facilités de paiement sont bien -évidemment possibles : nous pouvons ne pas encaisser le chèque immédiatement, -ou bien découper votre paiement en deux fois. Pour ceux qui ne pourraient pas -venir payer au bureau, merci de nous contacter par mail. - -*Mode de retrait des places* -Au moment du paiement, certaines places vous seront remises directement, -d'autres seront à récupérer au cours de l'année, d'autres encore seront -nominatives et à retirer le soir même dans les theâtres correspondants. -Pour chaque spectacle, vous recevrez un mail quelques jours avant la -représentation vous indiquant le mode de retrait. - -Nous vous rappelons que l'obtention de places du BdA vous engage à -respecter les règles de fonctionnement : -http://www.cof.ens.fr/bda/?page_id=1370 -Le système de revente des places via les mails BdA-revente sera très -prochainement disponible, directement sur votre compte GestioCOF. - -En vous souhaitant de très beaux spectacles tout au long de l'année, --- -Le Bureau des Arts -""" - attribs_text = "" - name = member.user.get_full_name() - for attrib in attribs: - attribs_text += "- 1 place pour %s\n" % attrib - deadline = member.tirage.fermeture + timedelta(days=7) - mail = mail % (name, attribs_text, - deadline.strftime('%d %b %Y')) - send_mail("Résultats du tirage au sort", mail, - "bda@ens.fr", [member.user.email], - fail_silently=True) + shortname = "bda-attributions" + context['places'] = attribs + print(context) + datatuple.append((shortname, context, "bda@ens.fr", + [member.user.email])) + send_mass_custom_mail(datatuple) count = len(queryset.all()) if count == 1: message_bit = "1 membre a" diff --git a/bda/forms.py b/bda/forms.py index 352914e4..e2a1961b 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -8,7 +8,6 @@ from datetime import timedelta from django import forms from django.forms.models import BaseInlineFormSet -from django.db.models import Q from django.utils import timezone from bda.models import Attribution, Spectacle diff --git a/bda/management/commands/loadbdadevdata.py b/bda/management/commands/loadbdadevdata.py index f348b310..69eab6fc 100644 --- a/bda/management/commands/loadbdadevdata.py +++ b/bda/management/commands/loadbdadevdata.py @@ -6,7 +6,7 @@ import os import random from django.utils import timezone -from django.contrib.auth.models import User +from django.contrib.auth.models import Group from cof.management.base import MyBaseCommand from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle @@ -77,7 +77,8 @@ class Command(MyBaseCommand): self.stdout.write("Inscription des utilisateurs aux tirages") ChoixSpectacle.objects.all().delete() choices = [] - for user in User.objects.filter(profile__cof__is_cof=True): + cof_members = Group.objects.get(name="cof_members") + for user in cof_members.user_set.all(): for tirage in tirages: part, _ = Participant.objects.get_or_create( user=user, diff --git a/bda/models.py b/bda/models.py index f9087ac1..a405a665 100644 --- a/bda/models.py +++ b/bda/models.py @@ -3,12 +3,11 @@ import calendar import random from datetime import timedelta +from custommail.shortcuts import send_mass_custom_mail from django.contrib.sites.models import Site from django.db import models from django.contrib.auth.models import User -from django.template import loader -from django.core import mail from django.conf import settings from django.utils import timezone, formats @@ -91,33 +90,30 @@ class Spectacle(models.Model): if member.id in members: members[member.id][1] = 2 else: - members[member.id] = [member.first_name, 1, member.email] - # Pour le BdA - members[0] = ['BdA', 1, 'bda@ens.fr'] - members[-1] = ['BdA', 2, 'bda@ens.fr'] + members[member.id] = [member, 1] + # FIXME : faire quelque chose de ça, un utilisateur bda_generic ? + # # Pour le BdA + # members[0] = ['BdA', 1, 'bda@ens.fr'] + # members[-1] = ['BdA', 2, 'bda@ens.fr'] # On écrit un mail personnalisé à chaque participant - mails_to_send = [] - mail_object = str(self) - for member in members.values(): - mail_body = loader.render_to_string('bda/mails/rappel.txt', { - 'name': member[0], - 'nb_attr': member[1], - 'show': self}) - mail_tot = mail.EmailMessage( - mail_object, mail_body, - settings.MAIL_DATA['rappels']['FROM'], [member[2]], - [], headers={ - 'Reply-To': settings.MAIL_DATA['rappels']['REPLYTO']}) - mails_to_send.append(mail_tot) - # On envoie les mails - connection = mail.get_connection() - connection.send_messages(mails_to_send) + datatuple = [( + 'bda-rappel', + {'member': member[0], 'nb_attr': member[1], 'show': self}, + settings.MAIL_DATA['rappels']['FROM'], + [member[0].email]) + for member in members.values() + ] + send_mass_custom_mail(datatuple) # On enregistre le fait que l'envoi a bien eu lieu self.rappel_sent = timezone.now() self.save() # On renvoie la liste des destinataires return members.values() + @property + def is_past(self): + return self.date < timezone.now() + class Quote(models.Model): spectacle = models.ForeignKey(Spectacle) @@ -238,26 +234,24 @@ class SpectacleRevente(models.Model): verbose_name = "Revente" def send_notif(self): + """ + Envoie une notification pour indiquer la mise en vente d'une place sur + BdA-Revente à tous les intéressés. + """ inscrits = self.attribution.spectacle.subscribed.select_related('user') - - mails_to_send = [] - mail_object = "%s" % (self.attribution.spectacle) - for participant in inscrits: - mail_body = loader.render_to_string('bda/mails/revente.txt', { - 'user': participant.user, - 'spectacle': self.attribution.spectacle, + datatuple = [( + 'bda-revente', + { + 'member': participant.user, + 'show': self.attribution.spectacle, 'revente': self, - 'domain': Site.objects.get_current().domain}) - mail_tot = mail.EmailMessage( - mail_object, mail_body, - settings.MAIL_DATA['revente']['FROM'], - [participant.user.email], - [], headers={ - 'Reply-To': settings.MAIL_DATA['revente']['REPLYTO']}) - mails_to_send.append(mail_tot) - - connection = mail.get_connection() - connection.send_messages(mails_to_send) + 'site': Site.objects.get_current() + }, + settings.MAIL_DATA['revente']['FROM'], + [participant.user.email]) + for participant in inscrits + ] + send_mass_custom_mail(datatuple) self.notif_sent = True self.save() @@ -267,25 +261,18 @@ class SpectacleRevente(models.Model): leur indiquer qu'il est désormais disponible au shotgun. """ inscrits = self.attribution.spectacle.subscribed.select_related('user') - - mails_to_send = [] - mail_object = "%s" % (self.attribution.spectacle) - for participant in inscrits: - mail_body = loader.render_to_string('bda/mails/shotgun.txt', { - 'user': participant.user, - 'spectacle': self.attribution.spectacle, - 'domain': Site.objects.get_current(), - 'mail': self.attribution.participant.user.email}) - mail_tot = mail.EmailMessage( - mail_object, mail_body, - settings.MAIL_DATA['revente']['FROM'], - [participant.user.email], - [], headers={ - 'Reply-To': settings.MAIL_DATA['revente']['REPLYTO']}) - mails_to_send.append(mail_tot) - - connection = mail.get_connection() - connection.send_messages(mails_to_send) + datatuple = [( + 'bda-shotgun', + { + 'member': participant.user, + 'show': self.attribution.spectacle, + 'site': Site.objects.get_current(), + }, + settings.MAIL_DATA['revente']['FROM'], + [participant.user.email]) + for participant in inscrits + ] + send_mass_custom_mail(datatuple) self.notif_sent = True # Flag inutile, sauf si l'horloge interne merde self.tirage_done = True @@ -303,56 +290,41 @@ class SpectacleRevente(models.Model): seller = self.seller if inscrits: - mails = [] - mail_subject = "BdA-Revente : {:s}".format(spectacle.title) - # Envoie un mail au gagnant et au vendeur winner = random.choice(inscrits) self.soldTo = winner + datatuple = [] context = { 'acheteur': winner.user, 'vendeur': seller.user, - 'spectacle': spectacle, + 'show': spectacle, } - mails.append(mail.EmailMessage( - mail_subject, - loader.render_to_string('bda/mails/revente-winner.txt', - context), - from_email=settings.MAIL_DATA['revente']['FROM'], - to=[winner.user.email], - reply_to=[seller.user.email], + datatuple.append(( + 'bda-revente-winner', + context, + settings.MAIL_DATA['revente']['FROM'], + [winner.user.email], )) - mails.append(mail.EmailMessage( - mail_subject, - loader.render_to_string('bda/mails/revente-seller.txt', - context), - from_email=settings.MAIL_DATA['revente']['FROM'], - to=[seller.user.email], - reply_to=[winner.user.email], + datatuple.append(( + 'bda-revente-seller', + context, + settings.MAIL_DATA['revente']['FROM'], + [seller.user.email] )) # Envoie un mail aux perdants for inscrit in inscrits: - if inscrit == winner: - continue - - mail_body = loader.render_to_string( - 'bda/mails/revente-loser.txt', - {'acheteur': inscrit.user, - 'vendeur': seller.user, - 'spectacle': spectacle} - ) - mails.append(mail.EmailMessage( - mail_subject, mail_body, - from_email=settings.MAIL_DATA['revente']['FROM'], - to=[inscrit.user.email], - reply_to=[settings.MAIL_DATA['revente']['REPLYTO']], - )) - mail.get_connection().send_messages(mails) - + if inscrit != winner: + context['acheteur'] = inscrit.user + datatuple.append(( + 'bda-revente-loser', + context, + settings.MAIL_DATA['revente']['FROM'], + [inscrit.user.email] + )) + send_mass_custom_mail(datatuple) # Si personne ne veut de la place, elle part au shotgun else: self.shotgun = True - self.tirage_done = True self.save() diff --git a/bda/static/bda/js/jquery-1.6.2.min.js b/bda/static/bda/js/jquery-1.6.2.min.js deleted file mode 100644 index 48590ecb..00000000 --- a/bda/static/bda/js/jquery-1.6.2.min.js +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * jQuery JavaScript Library v1.6.2 - * http://jquery.com/ - * - * Copyright 2011, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Thu Jun 30 14:16:56 2011 -0400 - */ -(function(a,b){function cv(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cs(a){if(!cg[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ch||(ch=c.createElement("iframe"),ch.frameBorder=ch.width=ch.height=0),b.appendChild(ch);if(!ci||!ch.createElement)ci=(ch.contentWindow||ch.contentDocument).document,ci.write((c.compatMode==="CSS1Compat"?"":"")+"
"),ci.close();d=ci.createElement(a),ci.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ch)}cg[a]=e}return cg[a]}function cr(a,b){var c={};f.each(cm.concat.apply([],cm.slice(0,b)),function(){c[this]=a});return c}function cq(){cn=b}function cp(){setTimeout(cq,0);return cn=f.now()}function cf(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ce(){try{return new a.XMLHttpRequest}catch(b){}}function b$(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bx(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function bm(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(be,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bl(a){f.nodeName(a,"input")?bk(a):"getElementsByTagName"in a&&f.grep(a.getElementsByTagName("input"),bk)}function bk(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bj(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bi(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bh(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;it |
{{ spectacle.title }} | -{{ spectacle.date }} | +{{ spectacle.date }} | {{ spectacle.location }} |
{{ spectacle.price |floatformat }}€
diff --git a/bda/views.py b/bda/views.py
index 18fdc3bc..110343bc 100644
--- a/bda/views.py
+++ b/bda/views.py
@@ -3,20 +3,20 @@
import random
import hashlib
import time
-
from datetime import timedelta
+from custommail.shortcuts import (
+ send_mass_custom_mail, send_custom_mail, render_custom_mail
+)
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.db import models, transaction
+from django.core import serializers
from django.db.models import Count, Q, Sum
-from django.core import serializers, mail
from django.forms.models import inlineformset_factory
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.conf import settings
-from django.core.mail import send_mail
-from django.template import loader
from django.utils import timezone
from django.views.generic.list import ListView
@@ -300,7 +300,7 @@ def revente(request, tirage_id):
resellform = ResellForm(participant, request.POST, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
if resellform.is_valid():
- mails = []
+ datatuple = []
attributions = resellform.cleaned_data["attributions"]
with transaction.atomic():
for attribution in attributions:
@@ -315,24 +315,18 @@ def revente(request, tirage_id):
revente.notif_sent = False
revente.tirage_done = False
revente.shotgun = False
- mail_subject = "BdA-Revente : {:s}".format(
- attribution.spectacle.title)
- mail_body = loader.render_to_string(
- 'bda/mails/revente-new.txt',
- {'vendeur': participant.user,
- 'spectacle': attribution.spectacle,
- 'revente': revente}
- )
- mails.append(mail.EmailMessage(
- mail_subject, mail_body,
- from_email=settings.MAIL_DATA['revente']['FROM'],
- to=[participant.user.email],
- reply_to=[
- settings.MAIL_DATA['revente']['REPLYTO']
- ],
+ context = {
+ 'vendeur': participant.user,
+ 'show': attribution.spectacle,
+ 'revente': revente
+ }
+ datatuple.append((
+ 'bda-revente-new', context,
+ settings.MAIL_DATA['revente']['FROM'],
+ [participant.user.email]
))
revente.save()
- mail.get_connection().send_messages(mails)
+ send_mass_custom_mail(datatuple)
# On annule une revente
elif 'annul' in request.POST:
annulform = AnnulForm(participant, request.POST, prefix='annul')
@@ -384,15 +378,15 @@ def revente(request, tirage_id):
annulform = AnnulForm(participant, prefix='annul')
overdue = participant.attribution_set.filter(
- spectacle__date__gte=timezone.now(),
- revente__isnull=False,
- revente__seller=participant,
- revente__date__lte=timezone.now()-timedelta(hours=1)).filter(
+ spectacle__date__gte=timezone.now(),
+ revente__isnull=False,
+ revente__seller=participant,
+ revente__date__lte=timezone.now()-timedelta(hours=1)).filter(
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
sold = participant.attribution_set.filter(
- spectacle__date__gte=timezone.now(),
- revente__isnull=False,
- revente__soldTo__isnull=False)
+ spectacle__date__gte=timezone.now(),
+ revente__isnull=False,
+ revente__soldTo__isnull=False)
return render(request, "bda-revente.html",
{'tirage': tirage, 'overdue': overdue, "sold": sold,
@@ -402,7 +396,7 @@ def revente(request, tirage_id):
@login_required
def revente_interested(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id)
- participant, created = Participant.objects.get_or_create(
+ participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=revente.attribution.spectacle.tirage)
if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun:
return render(request, "bda-wrongtime.html",
@@ -417,8 +411,8 @@ def revente_interested(request, revente_id):
@login_required
def list_revente(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
- participant, created = Participant.objects.get_or_create(
- user=request.user, tirage=tirage)
+ participant, _ = Participant.objects.get_or_create(
+ user=request.user, tirage=tirage)
deja_revente = False
success = False
inscrit_revente = False
@@ -430,7 +424,7 @@ def list_revente(request, tirage_id):
participant.save()
for spectacle in choices:
qset = SpectacleRevente.objects.filter(
- attribution__spectacle=spectacle)
+ attribution__spectacle=spectacle)
if qset.filter(shotgun=True, soldTo__isnull=True).exists():
# Une place est disponible au shotgun, on suggère à
# l'utilisateur d'aller la récupérer
@@ -452,24 +446,24 @@ def list_revente(request, tirage_id):
success = True
else:
form = InscriptionReventeForm(
- tirage,
- initial={'spectacles': participant.choicesrevente.all()})
+ tirage,
+ initial={'spectacles': participant.choicesrevente.all()})
return render(request, "liste-reventes.html",
{"form": form,
- "deja_revente": deja_revente, "success": success,
- "inscrit_revente": inscrit_revente})
+ "deja_revente": deja_revente, "success": success,
+ "inscrit_revente": inscrit_revente})
@login_required
def buy_revente(request, spectacle_id):
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
tirage = spectacle.tirage
- participant, created = Participant.objects.get_or_create(
- user=request.user, tirage=tirage)
+ participant, _ = Participant.objects.get_or_create(
+ user=request.user, tirage=tirage)
reventes = SpectacleRevente.objects.filter(
- attribution__spectacle=spectacle,
- soldTo__isnull=True)
+ attribution__spectacle=spectacle,
+ soldTo__isnull=True)
# Si l'utilisateur veut racheter une place qu'il est en train de revendre,
# on supprime la revente en question.
@@ -488,15 +482,17 @@ def buy_revente(request, spectacle_id):
revente = random.choice(reventes_shotgun)
revente.soldTo = participant
revente.save()
- mail = loader.render_to_string('bda/mails/buy-shotgun.txt', {
- 'spectacle': spectacle,
+ context = {
+ 'show': spectacle,
'acheteur': request.user,
- 'vendeur': revente.seller.user,
- })
- send_mail("BdA-Revente : %s" % spectacle.title, mail,
- request.user.email,
- [revente.seller.user.email],
- fail_silently=False)
+ 'vendeur': revente.seller.user
+ }
+ send_custom_mail(
+ 'bda-buy-shotgun',
+ 'bda@ens.fr',
+ [revente.seller.user.email],
+ context=context,
+ )
return render(request, "bda-success.html",
{"seller": revente.attribution.participant.user,
"spectacle": spectacle})
@@ -510,13 +506,13 @@ def buy_revente(request, spectacle_id):
def revente_shotgun(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
spectacles = tirage.spectacle_set.filter(
- date__gte=timezone.now())
+ date__gte=timezone.now())
shotgun = []
for spectacle in spectacles:
reventes = SpectacleRevente.objects.filter(
- attribution__spectacle=spectacle,
- shotgun=True,
- soldTo__isnull=True)
+ attribution__spectacle=spectacle,
+ shotgun=True,
+ soldTo__isnull=True)
if reventes.exists():
shotgun.append(spectacle)
@@ -580,15 +576,16 @@ def unpaid(request, tirage_id):
def send_rappel(request, spectacle_id):
show = get_object_or_404(Spectacle, id=spectacle_id)
# Mails d'exemples
- fake_member = request.user
- fake_member.nb_attr = 1
- exemple_mail_1place = loader.render_to_string('bda/mails/rappel.txt', {
- 'member': fake_member,
- 'show': show})
- fake_member.nb_attr = 2
- exemple_mail_2places = loader.render_to_string('bda/mails/rappel.txt', {
- 'member': fake_member,
- 'show': show})
+ exemple_mail_1place = render_custom_mail('bda-rappel', {
+ 'member': request.user,
+ 'show': show,
+ 'nb_attr': 1
+ })
+ exemple_mail_2places = render_custom_mail('bda-rappel', {
+ 'member': request.user,
+ 'show': show,
+ 'nb_attr': 2
+ })
# Contexte
ctxt = {'show': show,
'exemple_mail_1place': exemple_mail_1place,
@@ -601,7 +598,7 @@ def send_rappel(request, spectacle_id):
# Demande de confirmation
else:
ctxt['sent'] = False
- return render(request, "mails-rappel.html", ctxt)
+ return render(request, "bda/mails-rappel.html", ctxt)
def descriptions_spectacles(request, tirage_id):
@@ -616,5 +613,5 @@ def descriptions_spectacles(request, tirage_id):
shows_qs = shows_qs.filter(location__id=int(location_id))
except ValueError:
return HttpResponseBadRequest(
- "La variable GET 'location' doit contenir un entier")
+ "La variable GET 'location' doit contenir un entier")
return render(request, 'descriptions.html', {'shows': shows_qs.all()})
diff --git a/cof/admin.py b/cof/admin.py
index fe531853..95ac77ed 100644
--- a/cof/admin.py
+++ b/cof/admin.py
@@ -7,12 +7,6 @@ from __future__ import unicode_literals
from django import forms
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
-from .models import SurveyQuestionAnswer, SurveyQuestion, \
- CofProfile, EventOption, EventOptionChoice, Event, Club, CustomMail, \
- Survey, EventCommentField, EventRegistration
-from .petits_cours_models import PetitCoursDemande, \
- PetitCoursSubject, PetitCoursAbility, PetitCoursAttribution, \
- PetitCoursAttributionCounter
from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.admin import UserAdmin
from django.core.urlresolvers import reverse
@@ -22,6 +16,17 @@ import django.utils.six as six
import autocomplete_light
+from .petits_cours_models import PetitCoursDemande, \
+ PetitCoursSubject, PetitCoursAbility, PetitCoursAttribution, \
+ PetitCoursAttributionCounter
+from .models import (
+ SurveyQuestionAnswer, SurveyQuestion, CofProfile, EventOption,
+ EventOptionChoice, Event, Club, EventCommentField, EventRegistration,
+ Survey
+)
+
+from gestion.models import Profile
+
def add_link_field(target_model='', field='', link_text=six.text_type,
desc_text=six.text_type):
@@ -140,7 +145,7 @@ def ProfileInfo(field, short_description, boolean=False):
User.profile_login_clipper = FkeyLookup("profile__login_clipper",
"Login clipper")
User.profile_num = FkeyLookup("profile__cofprofile__num",
- "Numéro")
+ "Numéro")
User.profile_phone = ProfileInfo("phone", "Téléphone")
User.profile_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement")
@@ -271,10 +276,6 @@ class PetitCoursDemandeAdmin(admin.ModelAdmin):
search_fields = ('name', 'email', 'phone', 'lieu', 'remarques')
-class CustomMailAdmin(admin.ModelAdmin):
- search_fields = ('shortname', 'title')
-
-
class ClubAdminForm(forms.ModelForm):
def clean(self):
cleaned_data = super(ClubAdminForm, self).clean()
@@ -301,7 +302,6 @@ admin.site.unregister(User)
admin.site.register(User, UserProfileAdmin)
admin.site.register(CofProfile)
admin.site.register(Club, ClubAdmin)
-admin.site.register(CustomMail)
admin.site.register(PetitCoursSubject)
admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin)
admin.site.register(PetitCoursAttribution, PetitCoursAttributionAdmin)
diff --git a/cof/autocomplete.py b/cof/autocomplete.py
index 2c05c701..37a08c32 100644
--- a/cof/autocomplete.py
+++ b/cof/autocomplete.py
@@ -1,16 +1,20 @@
# -*- coding: utf-8 -*-
-from __future__ import division
-from __future__ import print_function
-from __future__ import unicode_literals
+from ldap3 import Connection
from django import shortcuts
from django.http import Http404
from django.db.models import Q
-
from django.contrib.auth.models import User
-from .models import CofProfile, Clipper
+from .models import CofProfile
from .decorators import buro_required
+from django.conf import settings
+
+
+class Clipper(object):
+ def __init__(self, clipper, fullname):
+ self.clipper = clipper
+ self.fullname = fullname
@buro_required
@@ -25,9 +29,9 @@ def autocomplete(request):
queries = {}
bits = q.split()
- queries['members'] = CofProfile.objects.filter(Q(is_cof=True))
- queries['users'] = User.objects.filter(Q(profile__cof__is_cof=False))
- queries['clippers'] = Clipper.objects
+ # Fetching data from User and Profile tables
+ queries['members'] = CofProfile.objects.filter(is_cof=True)
+ queries['users'] = User.objects.filter(profile__cof__is_cof=False)
for bit in bits:
queries['members'] = queries['members'].filter(
Q(profile__user__first_name__icontains=bit)
@@ -35,27 +39,41 @@ def autocomplete(request):
| Q(profile__user__username__icontains=bit)
| Q(profile__login_clipper__icontains=bit))
queries['users'] = queries['users'].filter(
- Q(first_name__icontains=bit)
- | Q(last_name__icontains=bit)
- | Q(username__icontains=bit))
- queries['clippers'] = queries['clippers'].filter(
- Q(fullname__icontains=bit)
- | Q(username__icontains=bit))
+ Q(first_name__icontains=bit)
+ | Q(last_name__icontains=bit)
+ | Q(username__icontains=bit))
queries['members'] = queries['members'].distinct()
queries['users'] = queries['users'].distinct()
- usernames = list(queries['members'].values_list('profile__login_clipper',
- flat='True')) \
- + list(queries['users'].values_list('profile__login_clipper',
- flat='True'))
- queries['clippers'] = queries['clippers'] \
- .exclude(username__in=usernames).distinct()
- # add clippers
+ # Clearing redundancies
+ usernames = (
+ set(queries['members'].values_list('login_clipper', flat='True'))
+ | set(queries['users'].values_list('profile__login_clipper',
+ flat='True'))
+ )
+
+ # Fetching data from the SPI
+ if hasattr(settings, 'LDAP_SERVER_URL'):
+ # Fetching
+ ldap_query = '(|{:s})'.format(''.join(
+ ['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(**{"bit": bit})
+ for bit in bits]
+ ))
+ with Connection(settings.LDAP_SERVER_URL) as conn:
+ conn.search(
+ 'dc=spi,dc=ens,dc=fr', ldap_query,
+ attributes=['uid', 'cn']
+ )
+ queries['clippers'] = conn.entries
+ # Clearing redundancies
+ queries['clippers'] = [
+ Clipper(clipper.uid, clipper.cn)
+ for clipper in queries['clippers']
+ if str(clipper.uid) not in usernames
+ ]
+
+ # Resulting data
data.update(queries)
-
- options = 0
- for query in queries.values():
- options += len(query)
- data['options'] = options
+ data['options'] = sum(len(query) for query in queries)
return shortcuts.render(request, "autocomplete_user.html", data)
diff --git a/cof/autocomplete_light_registry.py b/cof/autocomplete_light_registry.py
index f2a2ca6e..4c62d995 100644
--- a/cof/autocomplete_light_registry.py
+++ b/cof/autocomplete_light_registry.py
@@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
-from __future__ import division
-from __future__ import print_function
-from __future__ import unicode_literals
-
import autocomplete_light
from django.contrib.auth.models import User
autocomplete_light.register(
User, search_fields=('username', 'first_name', 'last_name'),
- autocomplete_js_attributes={'placeholder': 'membre...'})
+ attrs={'placeholder': 'membre...'}
+)
diff --git a/cof/management/commands/loaddevdata.py b/cof/management/commands/loaddevdata.py
index 1525152c..aac77299 100644
--- a/cof/management/commands/loaddevdata.py
+++ b/cof/management/commands/loaddevdata.py
@@ -49,10 +49,10 @@ class Command(MyBaseCommand):
# Gaulois
gaulois = self.from_json('gaulois.json', DATA_DIR, User)
for user in gaulois:
- CofProfile.objects.create(
+ cofprofile = CofProfile.objects.create(
profile=user.profile,
- is_cof=True
)
+ cofprofile.is_cof = True
# Romains
self.from_json('romains.json', DATA_DIR, User)
@@ -113,3 +113,9 @@ class Command(MyBaseCommand):
# ---
call_command('loadbdadevdata')
+
+ # ---
+ # La K-Fêt
+ # ---
+
+ call_command('loadkfetdevdata')
diff --git a/cof/models.py b/cof/models.py
index d05fec3a..06e6b073 100644
--- a/cof/models.py
+++ b/cof/models.py
@@ -1,16 +1,10 @@
# -*- coding: utf-8 -*-
-from __future__ import division
-from __future__ import print_function
-from __future__ import unicode_literals
-
from django.db import models
-from django.dispatch import receiver
-from django.contrib.auth.models import Group, Permission, User
+from django.contrib.auth.models import Group, User
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
import django.utils.six as six
-from django.db.models.signals import post_save, post_delete
from gestion.models import Profile
from bda.models import Spectacle
@@ -54,7 +48,6 @@ class CofProfile(models.Model):
_("Remarques et précisions pour les petits cours"),
blank=True, default="")
-
# is_cof = models.BooleanField("Membre du COF", default=False)
@property
def is_cof(self):
@@ -64,8 +57,7 @@ class CofProfile(models.Model):
def is_cof(self, really):
if really:
g = Group.objects.get(name='cof_members')
- self.groups.add(g)
-
+ self.profile.user.groups.add(g)
class Meta:
verbose_name = "Profil COF"
@@ -87,22 +79,6 @@ class Club(models.Model):
return self.name
-@python_2_unicode_compatible
-class CustomMail(models.Model):
- shortname = models.SlugField(max_length=50, blank=False)
- title = models.CharField("Titre", max_length=200, blank=False)
- content = models.TextField("Contenu", blank=False)
- comments = models.TextField("Informations contextuelles sur le mail",
- blank=True)
-
- class Meta:
- verbose_name = "Mail personnalisable"
- verbose_name_plural = "Mails personnalisables"
-
- def __str__(self):
- return "%s: %s" % (self.shortname, self.title)
-
-
@python_2_unicode_compatible
class Event(models.Model):
title = models.CharField("Titre", max_length=200)
@@ -249,15 +225,6 @@ class SurveyAnswer(models.Model):
self.survey.title)
-#XXX. this needs to be removed according to Martin
-class Clipper(models.Model):
- username = models.CharField("Identifiant", max_length=20)
- fullname = models.CharField("Nom complet", max_length=200)
-
- def __str__(self):
- return "Clipper %s" % self.username
-
-
@python_2_unicode_compatible
class CalendarSubscription(models.Model):
token = models.UUIDField()
diff --git a/cof/petits_cours_forms.py b/cof/petits_cours_forms.py
new file mode 100644
index 00000000..c6ad03df
--- /dev/null
+++ b/cof/petits_cours_forms.py
@@ -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 .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
+)
diff --git a/cof/petits_cours_models.py b/cof/petits_cours_models.py
index 4428b78c..753e8674 100644
--- a/cof/petits_cours_models.py
+++ b/cof/petits_cours_models.py
@@ -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
+ )
diff --git a/cof/petits_cours_views.py b/cof/petits_cours_views.py
index 9dc8a576..6bcb5cb8 100644
--- a/cof/petits_cours_views.py
+++ b/cof/petits_cours_views.py
@@ -1,36 +1,25 @@
# -*- coding: utf-8 -*-
-from __future__ import division
-from __future__ import print_function
-from __future__ import unicode_literals
+import json
+from datetime import datetime
+from custommail.shortcuts import render_custom_mail
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 .models import CofProfile
-from .petits_cours_models import PetitCoursDemande, \
- PetitCoursAttribution, PetitCoursAttributionCounter, PetitCoursAbility, \
- PetitCoursSubject
+from .petits_cours_models import (
+ PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
+ PetitCoursAbility, PetitCoursSubject
+)
from .decorators import buro_required
from .shared import lock_table, unlock_tables
-
-from captcha.fields import ReCaptchaField
-
-from datetime import datetime
-import base64
-import json
+from .petits_cours_forms import DemandeForm, MatieresFormSet
class DemandeListView(ListView):
@@ -41,47 +30,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 = "cof/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 +54,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))]
@@ -131,7 +93,7 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
proposed_for = proposed_for.items()
attribdata = list(attribdata.items())
proposed_mails = _generate_eleve_email(demande, proposed_for)
- mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", {
+ mainmail = render_custom_mail("petits-cours-mail-demandeur", {
"proposals": proposals,
"unsatisfied": unsatisfied,
"extra":
@@ -139,7 +101,7 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
'style="width:99%; height: 90px;">'
''
})
- return render(request, "traitement_demande_petit_cours.html",
+ return render(request, "gestioncof/traitement_demande_petit_cours.html",
{"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
@@ -153,14 +115,16 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
def _generate_eleve_email(demande, proposed_for):
- proposed_mails = []
- for user, matieres in proposed_for:
- msg = loader.render_to_string("petits-cours-mail-eleve.txt", {
- "demande": demande,
- "matieres": matieres
- })
- proposed_mails.append((user, msg))
- return proposed_mails
+ 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):
@@ -170,7 +134,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 +142,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 +163,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 +186,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] = []
@@ -241,7 +211,8 @@ def _traitement_other(request, demande, redo):
unsatisfied.append(matiere)
proposals = proposals.items()
proposed_for = proposed_for.items()
- return render(request, "traitement_demande_petit_cours_autre_niveau.html",
+ return render(request,
+ "gestiocof/traitement_demande_petit_cours_autre_niveau.html",
{"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
@@ -272,7 +243,7 @@ def _traitement_post(request, demande):
proposals_list = proposals.items()
proposed_for = proposed_for.items()
proposed_mails = _generate_eleve_email(demande, proposed_for)
- mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", {
+ mainmail = render_custom_mail("petits-cours-mail-demandeur", {
"proposals": proposals_list,
"unsatisfied": unsatisfied,
"extra": extra,
@@ -282,14 +253,14 @@ def _traitement_post(request, demande):
replyto = settings.MAIL_DATA['petits_cours']['REPLYTO']
mails_to_send = []
for (user, msg) in proposed_mails:
- msg = EmailMessage("Petits cours ENS par le COF", msg,
- frommail, [user.email],
- [bccaddress], headers={'Reply-To': replyto})
+ msg = mail.EmailMessage("Petits cours ENS par le COF", msg,
+ frommail, [user.email],
+ [bccaddress], headers={'Reply-To': replyto})
mails_to_send.append(msg)
- mails_to_send.append(EmailMessage("Cours particuliers ENS", mainmail,
- frommail, [demande.email],
- [bccaddress],
- headers={'Reply-To': replyto}))
+ mails_to_send.append(mail.EmailMessage("Cours particuliers ENS", mainmail,
+ frommail, [demande.email],
+ [bccaddress],
+ headers={'Reply-To': replyto}))
connection = mail.get_connection(fail_silently=True)
connection.send_messages(mails_to_send)
lock_table(PetitCoursAttributionCounter, PetitCoursAttribution, User)
@@ -307,43 +278,18 @@ def _traitement_post(request, demande):
demande.traitee_par = request.user
demande.processed = datetime.now()
demande.save()
- return render(request, "traitement_demande_petit_cours_success.html",
+ return render(request,
+ "gestioncof/traitement_demande_petit_cours_success.html",
{"demande": demande,
"redo": redo,
})
-class BaseMatieresFormSet(BaseInlineFormSet):
- def clean(self):
- super(BaseMatieresFormSet, self).clean()
- if any(self.errors):
- # Don't bother validating the formset unless each form is
- # valid on its own
- return
- matieres = []
- for i in range(0, self.total_form_count()):
- form = self.forms[i]
- if not form.cleaned_data:
- continue
- matiere = form.cleaned_data['matiere']
- niveau = form.cleaned_data['niveau']
- delete = form.cleaned_data['DELETE']
- if not delete and (matiere, niveau) in matieres:
- raise forms.ValidationError(
- "Vous ne pouvez pas vous inscrire deux fois pour la "
- "même matiere avec le même niveau.")
- matieres.append((matiere, niveau))
-
-
@login_required
def inscription(request):
profile, created = CofProfile.objects.get_or_create(user=request.user)
if not profile.is_cof:
return redirect("cof-denied")
- MatieresFormSet = inlineformset_factory(User, PetitCoursAbility,
- fields=("matiere", "niveau",
- "agrege",),
- formset=BaseMatieresFormSet)
success = False
if request.method == "POST":
formset = MatieresFormSet(request.POST, instance=request.user)
@@ -354,10 +300,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 +319,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
diff --git a/cof/shared.py b/cof/shared.py
index 7c8b089b..6b1a7705 100644
--- a/cof/shared.py
+++ b/cof/shared.py
@@ -9,12 +9,9 @@ from django.conf import settings
from django_cas_ng.backends import CASBackend
from django_cas_ng.utils import get_cas_client
from django.contrib.auth import get_user_model
-from django.contrib.auth.models import User as DjangoUser
from django.db import connection
-from django.core.mail import send_mail
-from django.template import Template, Context
-from .models import CofProfile, CustomMail
+from .models import CofProfile
User = get_user_model()
@@ -73,9 +70,9 @@ class COFCASBackend(CASBackend):
def context_processor(request):
'''Append extra data to the context of the given request'''
data = {
- "user": request.user,
- "site": Site.objects.get_current(),
- }
+ "user": request.user,
+ "site": Site.objects.get_current(),
+ }
return data
@@ -99,18 +96,3 @@ def unlock_tables(*models):
return row
unlock_table = unlock_tables
-
-
-def send_custom_mail(to, shortname, context=None, from_email="cof@ens.fr"):
- if context is None:
- context = {}
- if isinstance(to, DjangoUser):
- context["nom"] = to.get_full_name()
- context["prenom"] = to.first_name
- to = to.email
- mail = CustomMail.objects.get(shortname=shortname)
- template = Template(mail.content)
- message = template.render(Context(context))
- send_mail(mail.title, message,
- from_email, [to],
- fail_silently=True)
diff --git a/cof/static/css/cof.css b/cof/static/css/cof.css
index b8f6e7f8..9b888c4f 100644
--- a/cof/static/css/cof.css
+++ b/cof/static/css/cof.css
@@ -1088,3 +1088,8 @@ tr.awesome{
color: white;
padding: 20px;
}
+
+.petitcours-raw {
+ padding:20px;
+ background:#fff;
+}
diff --git a/cof/templates/autocomplete_user.html b/cof/templates/autocomplete_user.html
index f5802c5d..17d55c66 100644
--- a/cof/templates/autocomplete_user.html
+++ b/cof/templates/autocomplete_user.html
@@ -15,7 +15,7 @@
{% if clippers %}
Mails pour les membres proposés :{% for proposeduser, mail in proposed_mails %}Pour {{ proposeduser }}:-{{ mail }}+ {% with object=mail.0 content=mail.1 %} + {{ object }}+ {{ content }}+ {% endwith %} {% endfor %} Mail pour l'auteur de la demande :-{{ mainmail|safe }}+ {% with object=mainmail.0 content=mainmail.1 %} + {{ object }}+ {{ content|safe }}+ {% endwith %} {% if redo %}{% endif %} diff --git a/cof/templates/traitement_demande_petit_cours_autre_niveau.html b/cof/templates/cof/traitement_demande_petit_cours_autre_niveau.html similarity index 100% rename from cof/templates/traitement_demande_petit_cours_autre_niveau.html rename to cof/templates/cof/traitement_demande_petit_cours_autre_niveau.html diff --git a/cof/templates/traitement_demande_petit_cours_success.html b/cof/templates/cof/traitement_demande_petit_cours_success.html similarity index 100% rename from cof/templates/traitement_demande_petit_cours_success.html rename to cof/templates/cof/traitement_demande_petit_cours_success.html diff --git a/cof/templates/demande-petit-cours-raw.html b/cof/templates/demande-petit-cours-raw.html index 7aab243a..000df8f3 100644 --- a/cof/templates/demande-petit-cours-raw.html +++ b/cof/templates/demande-petit-cours-raw.html @@ -1,11 +1,19 @@ +{% extends "base.html" %} + +{% load bootstrap %} + +{% block content %} +
{% if success %}
+{% endblock %}
diff --git a/cof/templates/inscription-petit-cours.html b/cof/templates/inscription-petit-cours.html
index 97182760..11b92aed 100644
--- a/cof/templates/inscription-petit-cours.html
+++ b/cof/templates/inscription-petit-cours.html
@@ -2,11 +2,11 @@
{% load staticfiles %}
{% block extra_head %}
-
-
-
-
-
+
+
+
+
+
{% endblock %}
{% block realcontent %}
diff --git a/cof/templates/petits-cours-mail-demandeur.txt b/cof/templates/petits-cours-mail-demandeur.txt
deleted file mode 100644
index 8c20834e..00000000
--- a/cof/templates/petits-cours-mail-demandeur.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-Bonjour,
-
-Je vous contacte au sujet de votre annonce passée sur le site du COF pour rentrer en contact avec un élève normalien pour des cours particuliers. Voici les coordonnées d'élèves qui sont motivés par de tels cours et correspondent aux critères que vous nous aviez transmis :
-
-{% for matiere, proposed in proposals %}¤ {{ matiere }} :{% for user in proposed %}
- ¤ {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}
-
-{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'élève disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.
-
-{% endif %}Si pour une raison ou une autre ces numéros ne suffisaient pas, n'hésitez pas à répondre à cet e-mail et je vous en ferai parvenir d'autres sans problème.
-{% if extra|length > 0 %}
-{{ extra|safe }}
-{% endif %}
-Cordialement,
-
---
-Le COF, BdE de l'ENS
diff --git a/cof/templates/petits-cours-mail-eleve.txt b/cof/templates/petits-cours-mail-eleve.txt
deleted file mode 100644
index f75fb33f..00000000
--- a/cof/templates/petits-cours-mail-eleve.txt
+++ /dev/null
@@ -1,28 +0,0 @@
-Salut,
-
-Le COF a reçu une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonnées, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les numéros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :
-
-¤ Nom : {{ demande.name }}
-
-¤ Période : {{ demande.quand }}
-
-¤ Fréquence : {{ demande.freq }}
-
-¤ Lieu (si préféré) : {{ demande.lieu }}
-
-¤ Niveau : {{ demande.get_niveau_display }}
-
-¤ Remarques diverses (désolé pour les balises HTML) : {{ demande.remarques }}
-
-{% if matieres|length > 1 %}¤ Matières :
-{% for matiere in matieres %} ¤ {{ matiere }}
-{% endfor %}{% else %}¤ Matière : {% for matiere in matieres %}{{ matiere }}
-{% endfor %}{% endif %}
-Voilà, cette personne te contactera peut-être sous peu, tu pourras voir les détails directement avec elle (prix, modalités, ...). Pour indication, 30 Euro/h semble être la moyenne.
-
-Si tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, ça serait cool que tu décoches la case "Recevoir des propositions de petits cours" sur GestioCOF. Ensuite dès que tu voudras réapparaître tu pourras recocher la case et tu seras à nouveau sur la liste.
-
-À bientôt,
-
---
-Le COF, pour les petits cours
diff --git a/cof/templatetags/utils.py b/cof/templatetags/utils.py
index 16a1f4e3..90855165 100644
--- a/cof/templatetags/utils.py
+++ b/cof/templatetags/utils.py
@@ -43,7 +43,7 @@ def highlight_user(user, q):
@register.filter
def highlight_clipper(clipper, q):
if clipper.fullname:
- text = "%s (%s)" % (clipper.fullname, clipper.username)
+ text = "%s (%s)" % (clipper.fullname, clipper.clipper)
else:
- text = clipper.username
+ text = clipper.clipper
return highlight_text(text, q)
diff --git a/cof/urls.py b/cof/urls.py
index 2f02354d..ca310382 100644
--- a/cof/urls.py
+++ b/cof/urls.py
@@ -1,13 +1,11 @@
# -*- coding: utf-8 -*-
-from __future__ import division
-from __future__ import print_function
-from __future__ import unicode_literals
-
from django.conf.urls import url
-from .petits_cours_views import DemandeListView
+from .petits_cours_views import DemandeListView, DemandeDetailView
+from .decorators import buro_required
from . import views, petits_cours_views
+
export_patterns = [
url(r'^members$', views.export_members),
url(r'^mega/avecremarques$', views.export_mega_remarksonly),
@@ -24,10 +22,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/(?PVotre demande a été enregistrée avec succès ! {% else %} {% endif %} + |