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

+ +{% 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 %} +
+ {% csrf_token %} +
+ +
+{% 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']