diff --git a/bda/admin.py b/bda/admin.py
index 48d0c955..35f79b8c 100644
--- a/bda/admin.py
+++ b/bda/admin.py
@@ -20,10 +20,25 @@ class ChoixSpectacleInline(admin.TabularInline):
class AttributionInline(admin.TabularInline):
model = Attribution
+ extra = 0
+
+ def get_queryset(self, request):
+ qs = super(AttributionInline, self).get_queryset(request)
+ return qs.filter(spectacle__listing=False)
+
+
+class AttributionInlineListing(admin.TabularInline):
+ model = Attribution
+ exclude = ('given', )
+ extra = 0
+
+ def get_queryset(self, request):
+ qs = super(AttributionInlineListing, self).get_queryset(request)
+ return qs.filter(spectacle__listing=True)
class ParticipantAdmin(admin.ModelAdmin):
- inlines = [AttributionInline]
+ inlines = [AttributionInline, AttributionInlineListing]
def get_queryset(self, request):
return Participant.objects.annotate(nb_places=Count('attributions'),
@@ -159,14 +174,17 @@ class ChoixSpectacleAdmin(admin.ModelAdmin):
list_filter = ("double_choice", "participant__tirage")
search_fields = ('participant__user__username',
'participant__user__first_name',
- 'participant__user__last_name')
+ 'participant__user__last_name',
+ 'spectacle__title')
class SpectacleAdmin(admin.ModelAdmin):
model = Spectacle
- list_display = ("title", "date", "tirage", "location", "slots", "price")
+ list_display = ("title", "date", "tirage", "location", "slots", "price",
+ "listing")
list_filter = ("location", "tirage",)
search_fields = ("title", "location__name")
+ readonly_fields = ("rappel_sent", )
class TirageAdmin(admin.ModelAdmin):
@@ -177,8 +195,14 @@ class TirageAdmin(admin.ModelAdmin):
list_filter = ("active", )
search_fields = ("title", )
+
+class SalleAdmin(admin.ModelAdmin):
+ model = Salle
+ search_fields = ('name', 'address')
+
+
admin.site.register(Spectacle, SpectacleAdmin)
-admin.site.register(Salle)
+admin.site.register(Salle, SalleAdmin)
admin.site.register(Participant, ParticipantAdmin)
admin.site.register(Attribution, AttributionAdmin)
admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin)
diff --git a/bda/migrations/0004_mails-rappel.py b/bda/migrations/0004_mails-rappel.py
new file mode 100644
index 00000000..f17b711f
--- /dev/null
+++ b/bda/migrations/0004_mails-rappel.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bda', '0003_update_tirage_and_spectacle'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='spectacle',
+ name='listing',
+ field=models.BooleanField(default=False, verbose_name=b'Les places sont sur listing'),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='spectacle',
+ name='rappel_sent',
+ field=models.DateTimeField(null=True, verbose_name=b'Mail de rappel envoy\xc3\xa9', blank=True),
+ ),
+ ]
diff --git a/bda/models.py b/bda/models.py
index 8e235509..37c43479 100644
--- a/bda/models.py
+++ b/bda/models.py
@@ -4,7 +4,16 @@ import calendar
from django.db import models
from django.contrib.auth.models import User
-from django.utils.translation import ugettext_lazy as _
+from django.template import loader, Context
+from django.core import mail
+from django.conf import settings
+from django.utils import timezone
+
+
+def render_template(template_name, data):
+ tmpl = loader.get_template(template_name)
+ ctxt = Context(data)
+ return tmpl.render(ctxt)
class Tirage(models.Model):
@@ -41,6 +50,9 @@ class Spectacle(models.Model):
slots = models.IntegerField("Places")
priority = models.IntegerField("Priorité", default=1000)
tirage = models.ForeignKey(Tirage)
+ listing = models.BooleanField("Les places sont sur listing")
+ rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True,
+ null=True)
class Meta:
verbose_name = "Spectacle"
@@ -59,6 +71,38 @@ class Spectacle(models.Model):
return u"%s - %s, %s, %.02f€" % (self.title, self.date_no_seconds(),
self.location, self.price)
+ def send_rappel(self):
+ # On récupère la liste des participants
+ members = {}
+ for attr in Attribution.objects.filter(spectacle=self).all():
+ member = attr.participant.user
+ if member.id in members:
+ members[member.id].nb_attr = 2
+ else:
+ member.nb_attr = 1
+ members[member.id] = member
+ # On écrit un mail personnalisé à chaque participant
+ mails_to_send = []
+ mail_object = "%s - %s - %s" % (self.title, self.date_no_seconds(),
+ self.location)
+ for member in members.values():
+ mail_body = render_template('mail-rappel.txt', {
+ 'member': member,
+ 'show': self})
+ mail_tot = mail.EmailMessage(
+ mail_object, mail_body,
+ settings.RAPPEL_FROM, [member.email],
+ [], headers={'Reply-To': settings.RAPPEL_REPLY_TO})
+ mails_to_send.append(mail_tot)
+ # On envoie les mails
+ connection = mail.get_connection()
+ connection.send_messages(mails_to_send)
+ # 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()
+
PAYMENT_TYPES = (
("cash", u"Cash"),
("cb", "CB"),
diff --git a/bda/templates/mail-rappel.txt b/bda/templates/mail-rappel.txt
new file mode 100644
index 00000000..5152b1db
--- /dev/null
+++ b/bda/templates/mail-rappel.txt
@@ -0,0 +1,23 @@
+Bonjour {{ member.get_full_name }},
+
+Nous te rappellons que tu as eu la chance d'obtenir {{ member.nb_attr|pluralize:"une place,deux places" }}
+pour {{ show.title }}, le {{ show.date_no_seconds }} au {{ show.location }}. N'oublie pas de t'y rendre !
+{% if member.nb_attr == 2 %}
+Tu as obtenu deux places pour ce spectacle. Nous te rappelons que
+ces places sont strictement réservées aux personnes de moins de 28 ans.
+{% endif %}
+{% if show.listing %}Pour ce spectacle, tu as reçu des places sur
+listing. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la représentation
+pour retirer {{ member.nb_attr|pluralize:"ta place,tes places" }}.
+{% else %}Pour assister à ce spectacle, tu dois présenter les billets qui ont
+été distribués au burô.
+{% endif %}
+
+Si tu ne peux plus assister à cette représentation, tu peux
+revendre ta place via BdA-revente, accessible directement sur
+GestioCOF (lien "revendre une place du premier tirage" sur la page
+d'accueil https://www.cof.ens.fr/gestion/).
+
+En te souhaitant un excellent spectacle,
+
+Le Bureau des Arts
diff --git a/bda/templates/mails-rappel.html b/bda/templates/mails-rappel.html
new file mode 100644
index 00000000..3fc15fa2
--- /dev/null
+++ b/bda/templates/mails-rappel.html
@@ -0,0 +1,35 @@
+{% extends "base_title.html" %}
+
+{% block realcontent %}
+{% if sent %}
+
Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes
+
+{% for member in members %}
+- {{ member.get_full_name }} ({{ member.email }})
+{% endfor %}
+
+{% else %}
+Voulez vous envoyer les mails de rappel pour le spectacle
+ {{ show.title }} ?
+ {% if show.rappel_sent %}
+ Attention, les mails ont déjà été envoyés le
+ {{ show.rappel_sent }}
+ {% endif %}
+{% endif %}
+
+{% if not sent %}
+
+{% endif %}
+
+Forme des mails
+
+
Une seule place
+{{ exemple_mail_1place }}
+
+
Deux places
+{{ exemple_mail_2places }}
+{% endblock %}
diff --git a/bda/urls.py b/bda/urls.py
index 4557ec86..268bb352 100644
--- a/bda/urls.py
+++ b/bda/urls.py
@@ -5,32 +5,33 @@ from bda.views import SpectacleListView
urlpatterns = patterns(
'',
- url(r'inscription/(?P\d+)$',
+ url(r'^inscription/(?P\d+)$',
'bda.views.inscription',
name='bda-tirage-inscription'),
- url(r'places/(?P\d+)$',
+ url(r'^places/(?P\d+)$',
'bda.views.places',
name="bda-places-attribuees"),
- url(r'places/(?P\d+)/places_bda.ics$',
+ url(r'^places/(?P\d+)/places_bda.ics$',
'bda.views.places_ics',
name="bda-places-attribuees-ics"),
- url(r'revente/(?P\d+)$',
+ url(r'^revente/(?P\d+)$',
'bda.views.revente',
name='bda-revente'),
- url(r'etat-places/(?P\d+)$',
+ url(r'^etat-places/(?P\d+)$',
'bda.views.etat_places',
name='bda-etat-places'),
- url(r'tirage/(?P\d+)$', 'bda.views.tirage'),
- url(r'spectacles/(?P\d+)$',
+ url(r'^tirage/(?P\d+)$', 'bda.views.tirage'),
+ url(r'^spectacles/(?P\d+)$',
SpectacleListView.as_view(),
name="bda-liste-spectacles"),
- url(r'spectacles/(?P\d+)/(?P\d+)$',
+ url(r'^spectacles/(?P\d+)/(?P\d+)$',
"bda.views.spectacle",
name="bda-spectacle"),
- url(r'spectacles-ics/(?P\d+)$',
+ url(r'^spectacles-ics/(?P\d+)$',
'bda.views.liste_spectacles_ics',
name="bda-liste-spectacles-ics"),
- url(r'spectacles/unpaid/(?P\d+)$',
+ url(r'^spectacles/unpaid/(?P\d+)$',
"bda.views.unpaid",
name="bda-unpaid"),
+ url(r'^mails-rappel/(?P\d+)$', "bda.views.send_rappel"),
)
diff --git a/bda/views.py b/bda/views.py
index 846aaeae..d9f89b2d 100644
--- a/bda/views.py
+++ b/bda/views.py
@@ -19,7 +19,7 @@ import time
from gestioncof.decorators import cof_required, buro_required
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\
- Tirage
+ Tirage, render_template
from bda.algorithm import Algorithm
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm
@@ -371,3 +371,31 @@ def liste_spectacles_ics(request, tirage_id):
return render(request, "liste_spectacles.ics",
{"spectacles": spectacles, "tirage": tirage},
content_type="text/calendar")
+
+
+@buro_required
+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 = render_template('mail-rappel.txt', {
+ 'member': fake_member,
+ 'show': show})
+ fake_member.nb_attr = 2
+ exemple_mail_2places = render_template('mail-rappel.txt', {
+ 'member': fake_member,
+ 'show': show})
+ # Contexte
+ ctxt = {'show': show,
+ 'exemple_mail_1place': exemple_mail_1place,
+ 'exemple_mail_2places': exemple_mail_2places}
+ # Envoi confirmé
+ if request.method == 'POST':
+ members = show.send_rappel()
+ ctxt['sent'] = True
+ ctxt['members'] = members
+ # Demande de confirmation
+ else:
+ ctxt['sent'] = False
+ return render(request, "mails-rappel.html", ctxt)
diff --git a/cof/settings_dev.py b/cof/settings_dev.py
index 36cd8a3a..1ef05f8c 100644
--- a/cof/settings_dev.py
+++ b/cof/settings_dev.py
@@ -136,6 +136,9 @@ PETITS_COURS_FROM = "Le COF "
PETITS_COURS_BCC = "archivescof@gmail.com"
PETITS_COURS_REPLYTO = "cof@ens.fr"
+RAPPEL_FROM = 'Le BdA '
+RAPPEL_REPLY_TO = RAPPEL_FROM
+
LOGIN_URL = "/login"
LOGIN_REDIRECT_URL = "/"
diff --git a/gestioncof/admin.py b/gestioncof/admin.py
index bb03bbd4..8a1fb431 100644
--- a/gestioncof/admin.py
+++ b/gestioncof/admin.py
@@ -46,12 +46,14 @@ class SurveyQuestionInline(admin.TabularInline):
class SurveyQuestionAdmin(admin.ModelAdmin):
+ search_fields = ('survey__title', 'answer')
inlines = [
SurveyQuestionAnswerInline,
]
class SurveyAdmin(admin.ModelAdmin):
+ search_fields = ('title', 'details')
inlines = [
SurveyQuestionInline,
]
@@ -72,12 +74,14 @@ class EventCommentFieldInline(admin.TabularInline):
class EventOptionAdmin(admin.ModelAdmin):
+ search_fields = ('event__title', 'name')
inlines = [
EventOptionChoiceInline,
]
class EventAdmin(admin.ModelAdmin):
+ search_fields = ('title', 'location', 'description')
inlines = [
EventOptionInline,
EventCommentFieldInline,
@@ -189,6 +193,7 @@ class PetitCoursAbilityAdmin(admin.ModelAdmin):
class PetitCoursAttributionAdmin(admin.ModelAdmin):
list_display = ('user', 'demande', 'matiere', 'rank', )
+ search_fields = ('user__username', 'matiere__name')
class PetitCoursAttributionCounterAdmin(admin.ModelAdmin):
@@ -208,6 +213,11 @@ class PetitCoursDemandeAdmin(admin.ModelAdmin):
list_display = ('name', 'email', 'agrege_requis', 'niveau', 'created',
'traitee', 'processed')
list_filter = ('traitee', 'niveau')
+ search_fields = ('name', 'email', 'phone', 'lieu', 'remarques')
+
+
+class CustomMailAdmin(admin.ModelAdmin):
+ search_fields = ('shortname', 'title')
admin.site.register(Survey, SurveyAdmin)
admin.site.register(SurveyQuestion, SurveyQuestionAdmin)
diff --git a/gestioncof/migrations/0004_registration_mail.py b/gestioncof/migrations/0004_registration_mail.py
new file mode 100644
index 00000000..d72900bf
--- /dev/null
+++ b/gestioncof/migrations/0004_registration_mail.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+def create_mail(apps, schema_editor):
+ CustomMail = apps.get_model("gestioncof", "CustomMail")
+ db_alias = schema_editor.connection.alias
+ if CustomMail.objects.filter(shortname="bienvenue").count() == 0:
+ CustomMail.objects.using(db_alias).bulk_create([
+ CustomMail(
+ shortname="bienvenue",
+ title="Bienvenue au COF",
+ content="Mail de bienvenue au COF, envoyé automatiquement à " \
+ + "l'inscription.\n\n" \
+ + "Les balises {{ ... }} sont interprétées comme expliqué " \
+ + "ci-dessous à l'envoi.",
+ comments="{{ nom }} \t fullname de la personne.\n"\
+ + "{{ prenom }} \t prénom de la personne.")
+ ])
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gestioncof', '0003_event_image'),
+ ]
+
+ operations = [
+ # Pas besoin de supprimer le mail lors de la migration dans l'autre
+ # sens.
+ migrations.RunPython(create_mail, migrations.RunPython.noop),
+ ]
diff --git a/gestioncof/models.py b/gestioncof/models.py
index badf7533..540145d5 100644
--- a/gestioncof/models.py
+++ b/gestioncof/models.py
@@ -94,7 +94,8 @@ class CustomMail(models.Model):
blank=True)
class Meta:
- verbose_name = "Mails personnalisables"
+ verbose_name = "Mail personnalisable"
+ verbose_name_plural = "Mails personnalisables"
def __unicode__(self):
return u"%s: %s" % (self.shortname, self.title)
@@ -158,6 +159,7 @@ class EventOptionChoice(models.Model):
class Meta:
verbose_name = "Choix"
+ verbose_name_plural = "Choix"
def __unicode__(self):
return unicode(self.value)
diff --git a/tirage_bda.py b/tirage_bda.py
deleted file mode 100644
index 502cb45e..00000000
--- a/tirage_bda.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-import os
-import sys
-import time
-
-if __name__ == "__main__":
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
- from django.conf import settings
- settings.DEBUG = True
- from bda.models import Spectacle, Participant, ChoixSpectacle
- from bda.algorithm import Algorithm
- from django.db.models import Sum
- from django.db import connection
- start = time.time()
- shows = Spectacle.objects.all()
- members = Participant.objects.all()
- choices = ChoixSpectacle.objects.order_by('participant', 'priority') \
- .select_related().all()
- available_slots = Spectacle.objects.aggregate(Sum('slots'))['slots__sum']
- cursor = connection.cursor()
- cursor.execute(
- "SELECT SUM(`slots` * `price`) AS `total` FROM `bda_spectacle`;")
- total_price = cursor.fetchone()[0]
- print "%d spectacles" % len(shows)
- print "%d places" % available_slots
- print "%d participants" % len(members)
- print "%d demandes" % len(choices)
- print "%d places demandées" % (len(choices)
- + len(choices.filter(double=True).all()))
- print "%.02f€ à brasser" % total_price
- print "Récupération: %.2fs" % (time.time() - start)
- start_init = time.time()
- algo = Algorithm(shows, members, choices)
- print "Initialisation: %.2fs" % (time.time() - start_init)
- start_algo = time.time()
- results = algo(sys.argv[1])
- print "Traitement: %.2fs" % (time.time() - start_algo)
- print len(connection.queries), "requêtes SQL effectuées"
- queries = list(connection.queries)
- total_slots = 0
- total_losers = 0
- for (_, members, losers) in results:
- total_slots += len(members)
- total_losers += len(losers)
- print "Placés %d\nDécus %d" % (total_slots, total_losers)
- print "Total %d" % (total_slots + total_losers)
- members2 = {}
- members_uniq = {}
- for (show, members, _) in results:
- for (member, _, _, _) in members:
- if member.id not in members_uniq:
- members_uniq[member.id] = member
- members2[member] = []
- member.total = 0
- member = members_uniq[member.id]
- members2[member].append(show)
- member.total += show.price
- if len(members) < show.slots:
- print "%d place(s) invendue(s) pour %s" \
- % (show.slots - len(members), show)
- members2 = members2.items()
- print "Temps total: %.2fs" % (time.time() - start)
- print "Requêtes SQL:"
- for query in queries:
- print query['sql']