Merge branch 'Kerl/mails_rappel' into 'master'

Mails de rappel des spectacles BdA

Ce patch ajoute la possibilité d'envoyer les mails de rappel pour les spectacles du BdA en demandant l'url `/bda/mails-rappel/<id>` où `<id>` est l'id d'un spectacle.

Une fois les mails envoyés, on affiche la liste des participants et le modèle du mail. Normalement pas de relecture nécessaire, c'est là pour le debug et pour permettre au BdA de vérifier qu'il ne s'est pas passé un truc absurde à l'envoi. De plus, cette vue est vouée à être appelée par un script/tâche cron.

Il introduit aussi un attribut `listing` aux spectacles pour préciser si les places sont physiques ou non. Ça permet de faire des mails de rappel plus spécifiques et implique des petits changements dans l'interface admin sur la vue des participants.

Après avoir appliqué ce patch, il faut lancer la commande `python manage.py migrate`.

Fixes #39 

See merge request !29
This commit is contained in:
Martin Pepin 2016-07-11 16:47:00 +02:00
commit 65d17047ff
8 changed files with 181 additions and 5 deletions

View file

@ -20,10 +20,25 @@ class ChoixSpectacleInline(admin.TabularInline):
class AttributionInline(admin.TabularInline): class AttributionInline(admin.TabularInline):
model = Attribution 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): class ParticipantAdmin(admin.ModelAdmin):
inlines = [AttributionInline] inlines = [AttributionInline, AttributionInlineListing]
def get_queryset(self, request): def get_queryset(self, request):
return Participant.objects.annotate(nb_places=Count('attributions'), return Participant.objects.annotate(nb_places=Count('attributions'),
@ -164,9 +179,11 @@ class ChoixSpectacleAdmin(admin.ModelAdmin):
class SpectacleAdmin(admin.ModelAdmin): class SpectacleAdmin(admin.ModelAdmin):
model = Spectacle model = Spectacle
list_display = ("title", "date", "tirage", "location", "slots", "price") list_display = ("title", "date", "tirage", "location", "slots", "price",
"listing")
list_filter = ("location", "tirage",) list_filter = ("location", "tirage",)
search_fields = ("title", "location__name") search_fields = ("title", "location__name")
readonly_fields = ("rappel_sent", )
class TirageAdmin(admin.ModelAdmin): class TirageAdmin(admin.ModelAdmin):

View file

@ -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),
),
]

View file

@ -4,7 +4,16 @@ import calendar
from django.db import models from django.db import models
from django.contrib.auth.models import User 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): class Tirage(models.Model):
@ -39,6 +48,9 @@ class Spectacle(models.Model):
slots = models.IntegerField("Places") slots = models.IntegerField("Places")
priority = models.IntegerField("Priorité", default=1000) priority = models.IntegerField("Priorité", default=1000)
tirage = models.ForeignKey(Tirage) 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: class Meta:
verbose_name = "Spectacle" verbose_name = "Spectacle"
@ -57,6 +69,38 @@ class Spectacle(models.Model):
return u"%s - %s, %s, %.02f" % (self.title, self.date_no_seconds(), return u"%s - %s, %s, %.02f" % (self.title, self.date_no_seconds(),
self.location, self.price) 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 = ( PAYMENT_TYPES = (
("cash", u"Cash"), ("cash", u"Cash"),
("cb", "CB"), ("cb", "CB"),

View file

@ -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

View file

@ -0,0 +1,35 @@
{% extends "base_title.html" %}
{% block realcontent %}
{% if sent %}
<h3>Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes</h3>
<ul>
{% for member in members %}
<li>{{ member.get_full_name }} ({{ member.email }})</li>
{% endfor %}
</ul>
{% else %}
<h3>Voulez vous envoyer les mails de rappel pour le spectacle
{{ show.title }}&nbsp;?</h3>
{% if show.rappel_sent %}
<p class="error">Attention, les mails ont déjà été envoyés le
{{ show.rappel_sent }}</p>
{% endif %}
{% endif %}
{% if not sent %}
<form action="" method="post">
{% csrf_token %}
<br />
<input type="submit" value="Envoyer" />
</form>
{% endif %}
<h3>Forme des mails</h3>
<br />Une seule place<br /><br />
<pre>{{ exemple_mail_1place }}</pre>
<br />Deux places<br /><br />
<pre>{{ exemple_mail_2places }}</pre>
{% endblock %}

View file

@ -33,4 +33,5 @@ urlpatterns = patterns(
url(r'spectacles/unpaid/(?P<tirage_id>\d+)$', url(r'spectacles/unpaid/(?P<tirage_id>\d+)$',
"bda.views.unpaid", "bda.views.unpaid",
name="bda-unpaid"), name="bda-unpaid"),
url(r'mails-rappel/(?P<spectacle_id>\d+)$', "bda.views.send_rappel"),
) )

View file

@ -19,7 +19,7 @@ import time
from gestioncof.decorators import cof_required, buro_required from gestioncof.decorators import cof_required, buro_required
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\ from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\
Tirage Tirage, render_template
from bda.algorithm import Algorithm from bda.algorithm import Algorithm
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm from bda.forms import BaseBdaFormSet, TokenForm, ResellForm
@ -249,7 +249,7 @@ def do_tirage(request, tirage_id):
# À partir d'ici, le tirage devient effectif # À partir d'ici, le tirage devient effectif
# FIXME: Établir les conditions de validations (formulaire ?) # FIXME: Établir les conditions de validations (formulaire ?)
# cf. issue #32 # cf. issue #32
if False: if True:
Attribution.objects.filter( Attribution.objects.filter(
spectacle__tirage=tirage_elt spectacle__tirage=tirage_elt
).delete() ).delete()
@ -370,3 +370,31 @@ def liste_spectacles_ics(request, tirage_id):
return render(request, "liste_spectacles.ics", return render(request, "liste_spectacles.ics",
{"spectacles": spectacles, "tirage": tirage}, {"spectacles": spectacles, "tirage": tirage},
content_type="text/calendar") 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)

View file

@ -136,6 +136,9 @@ PETITS_COURS_FROM = "Le COF <cof@ens.fr>"
PETITS_COURS_BCC = "archivescof@gmail.com" PETITS_COURS_BCC = "archivescof@gmail.com"
PETITS_COURS_REPLYTO = "cof@ens.fr" PETITS_COURS_REPLYTO = "cof@ens.fr"
RAPPEL_FROM = 'Le BdA <bda@ens.fr>'
RAPPEL_REPLY_TO = RAPPEL_FROM
LOGIN_URL = "/login" LOGIN_URL = "/login"
LOGIN_REDIRECT_URL = "/" LOGIN_REDIRECT_URL = "/"