diff --git a/bda/admin.py b/bda/admin.py index b23d79e0..37fdf9c7 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -9,7 +9,7 @@ from django.core.mail import send_mail from django.contrib import admin from django.db.models import Sum, Count from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ - Attribution, Tirage, Quote, CategorieSpectacle + Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente from django import forms from datetime import timedelta @@ -210,6 +210,21 @@ class SalleAdmin(admin.ModelAdmin): search_fields = ('name', 'address') +class SpectacleReventeAdmin(admin.ModelAdmin): + model = SpectacleRevente + + def spectacle(self, obj): + return obj.attribution.spectacle + + list_display = ("spectacle", "seller", "date", "soldTo") + raw_id_fields = ("attribution",) + readonly_fields = ("shotgun", "expiration_time") + search_fields = ("spectacle__title", + "seller__user__username", + "seller__user__firstname", + "seller__user__lastname",) + + admin.site.register(CategorieSpectacle) admin.site.register(Spectacle, SpectacleAdmin) admin.site.register(Salle, SalleAdmin) @@ -217,3 +232,4 @@ admin.site.register(Participant, ParticipantAdmin) admin.site.register(Attribution, AttributionAdmin) admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin) admin.site.register(Tirage, TirageAdmin) +admin.site.register(SpectacleRevente, SpectacleReventeAdmin) diff --git a/bda/forms.py b/bda/forms.py index 9f5e3890..c2eec894 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -4,9 +4,13 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals +from datetime import timedelta + from django import forms from django.forms.models import BaseInlineFormSet -from bda.models import Spectacle +from django.db.models import Q +from django.utils import timezone +from bda.models import Attribution, Spectacle class BaseBdaFormSet(BaseInlineFormSet): @@ -35,17 +39,47 @@ class TokenForm(forms.Form): token = forms.CharField(widget=forms.widgets.Textarea()) -class SpectacleModelChoiceField(forms.ModelChoiceField): +class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): - return "%s le %s (%s) à %.02f€" % (obj.title, obj.date_no_seconds(), - obj.location, obj.price) + return "%s" % obj.spectacle class ResellForm(forms.Form): - count = forms.ChoiceField(choices=(("1", "1"), ("2", "2"),)) - spectacle = SpectacleModelChoiceField(queryset=Spectacle.objects.none()) + attributions = AttributionModelMultipleChoiceField( + queryset=Attribution.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False) def __init__(self, participant, *args, **kwargs): super(ResellForm, self).__init__(*args, **kwargs) - self.fields['spectacle'].queryset = participant.attributions.all() \ - .distinct() + self.fields['attributions'].queryset = participant.attribution_set\ + .filter(spectacle__date__gte=timezone.now())\ + .exclude(revente__seller=participant) + + +class AnnulForm(forms.Form): + attributions = AttributionModelMultipleChoiceField( + queryset=Attribution.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False) + + def __init__(self, participant, *args, **kwargs): + super(AnnulForm, self).__init__(*args, **kwargs) + self.fields['attributions'].queryset = participant.attribution_set\ + .filter(spectacle__date__gte=timezone.now(), + revente__isnull=False, + revente__date__gte=timezone.now()-timedelta(hours=1))\ + .filter(Q(revente__soldTo__isnull=True) | + Q(revente__soldTo=participant)) + + +class InscriptionReventeForm(forms.Form): + spectacles = forms.ModelMultipleChoiceField( + queryset=Spectacle.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False) + + def __init__(self, tirage, *args, **kwargs): + super(InscriptionReventeForm, self).__init__(*args, **kwargs) + self.fields['spectacles'].queryset = tirage.spectacle_set.filter( + date__gte=timezone.now()) diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py new file mode 100644 index 00000000..f45357b1 --- /dev/null +++ b/bda/management/commands/manage_reventes.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.core.management import BaseCommand +from django.utils import timezone +from datetime import timedelta +from bda.models import SpectacleRevente + + +class Command(BaseCommand): + help = "Envoie les mails de notification et effectue " \ + "les tirages au sort des reventes" + + def handle(self, *args, **options): + now = timezone.now() + self.stdout.write(now) + reventes = SpectacleRevente.objects.all() + for revente in reventes: + # Check si < 24h + if (revente.attribution.spectacle.date <= + revente.date + timedelta(days=1)) and \ + now >= revente.date + timedelta(minutes=15) and \ + not revente.notif_sent: + revente.mail_shotgun() + self.stdout.write("Mail de disponibilité immédiate envoyé") + # Check si délai de retrait dépassé + elif (now >= revente.date + timedelta(hours=1) and + not revente.notif_sent): + revente.send_notif() + self.stdout.write("Mail d'inscription à une revente envoyé") + # Check si tirage à faire + elif (now >= revente.expiration_time and + not revente.tirage_done): + revente.tirage() + self.stdout.write("Tirage effectué, mails envoyés") diff --git a/bda/migrations/0009_revente.py b/bda/migrations/0009_revente.py new file mode 100644 index 00000000..0c039700 --- /dev/null +++ b/bda/migrations/0009_revente.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0008_py3'), + ] + + operations = [ + migrations.CreateModel( + name='SpectacleRevente', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('date', models.DateTimeField( + default=django.utils.timezone.now, + verbose_name='Date de mise en vente')), + ('notif_sent', models.BooleanField( + default=False, verbose_name='Notification envoy\xe9e')), + ('tirage_done', models.BooleanField( + default=False, verbose_name='Tirage effectu\xe9')), + ('attribution', models.OneToOneField(related_name='revente', + to='bda.Attribution')), + ], + options={ + 'verbose_name': 'Revente', + }, + ), + migrations.AddField( + model_name='participant', + name='choicesrevente', + field=models.ManyToManyField(related_name='revente', + to='bda.Spectacle', blank=True), + ), + migrations.AddField( + model_name='spectaclerevente', + name='interested', + field=models.ManyToManyField(related_name='wanted', + to='bda.Participant', blank=True), + ), + migrations.AddField( + model_name='spectaclerevente', + name='seller', + field=models.ForeignKey(related_name='original_shows', + verbose_name='Vendeur', + to='bda.Participant'), + ), + migrations.AddField( + model_name='spectaclerevente', + name='soldTo', + field=models.ForeignKey(verbose_name='Vendue \xe0', blank=True, + to='bda.Participant', null=True), + ), + ] diff --git a/bda/models.py b/bda/models.py index 270921be..1074e58b 100644 --- a/bda/models.py +++ b/bda/models.py @@ -5,6 +5,8 @@ from __future__ import print_function from __future__ import unicode_literals import calendar +import random +from datetime import timedelta from django.db import models from django.contrib.auth.models import User @@ -158,6 +160,9 @@ class Participant(models.Model): max_length=6, choices=PAYMENT_TYPES, blank=True) tirage = models.ForeignKey(Tirage) + choicesrevente = models.ManyToManyField(Spectacle, + related_name="subscribed", + blank=True) def __str__(self): return "%s - %s" % (self.user, self.tirage.title) @@ -205,4 +210,127 @@ class Attribution(models.Model): given = models.BooleanField("Donnée", default=False) def __str__(self): - return "%s -- %s" % (self.participant, self.spectacle) + return "%s -- %s, %s" % (self.participant.user, self.spectacle.title, + self.spectacle.date) + + +@python_2_unicode_compatible +class SpectacleRevente(models.Model): + attribution = models.OneToOneField(Attribution, + related_name="revente") + date = models.DateTimeField("Date de mise en vente", + default=timezone.now) + answered_mail = models.ManyToManyField(Participant, + related_name="wanted", + blank=True) + seller = models.ForeignKey(Participant, + related_name="original_shows", + verbose_name="Vendeur") + soldTo = models.ForeignKey(Participant, blank=True, null=True, + verbose_name="Vendue à") + + notif_sent = models.BooleanField("Notification envoyée", + default=False) + tirage_done = models.BooleanField("Tirage effectué", + default=False) + + @property + def expiration_time(self): + # L'acheteur doit être connu au plus 12h avant le spectacle + remaining_time = (self.attribution.spectacle.date + - self.date - timedelta(hours=13)) + # Au minimum, on attend 2 jours avant le tirage + delay = min(remaining_time, timedelta(days=2)) + # On a aussi 1h pour changer d'avis + return self.date + delay + timedelta(hours=1) + + @property + def shotgun(self): + # Soit on a dépassé le délai du tirage, soit il reste peu de + # temps avant le spectacle + # On se laisse 5min de marge pour cron + return (timezone.now() > self.expiration_time + timedelta(minutes=5) or + (self.attribution.spectacle.date <= timezone.now() + + timedelta(days=1))) and (timezone.now() >= self.date + + timedelta(minutes=15)) + + def __str__(self): + return "%s -- %s" % (self.seller, + self.attribution.spectacle.title) + + class Meta: + verbose_name = "Revente" + + def send_notif(self): + inscrits = self.attribution.spectacle.subscribed.select_related('user') + + mails_to_send = [] + mail_object = "%s" % (self.attribution.spectacle) + for participant in inscrits: + mail_body = render_template('mail-revente.txt', { + 'user': participant.user, + 'spectacle': self.spectacle, + 'revente': self}) + mail_tot = mail.EmailMessage( + mail_object, mail_body, + settings.REVENTE_FROM, [participant.user.email], + [], headers={'Reply-To': settings.REVENTE_REPLY_TO}) + mails_to_send.append(mail_tot) + + connection = mail.get_connection() + connection.send_messages(mails_to_send) + self.notif_sent = True + self.save() + + def mail_shotgun(self): + inscrits = self.attribution.spectacle.subscribed.select_related('user') + + mails_to_send = [] + mail_object = "%s" % (self.attribution.spectacle) + for participant in inscrits: + mail_body = render_template('mail-shotgun.txt', { + 'user': participant.user, + 'spectacle': self.spectacle, + 'mail': self.attribution.participant.user.email}) + mail_tot = mail.EmailMessage( + mail_object, mail_body, + settings.REVENTE_FROM, [participant.user.email], + [], headers={'Reply-To': settings.REVENTE_REPLY_TO}) + mails_to_send.append(mail_tot) + + connection = mail.get_connection() + connection.send_messages(mails_to_send) + self.notif_sent = True + self.save() + + def tirage(self): + inscrits = self.answered_mail + spectacle = self.attribution.spectacle + seller = self.seller + if inscrits.exists(): + winner = random.choice(inscrits.all()) + self.soldTo = winner + mail_buyer = """Bonjour, + +Tu as été tiré-e au sort pour racheter une place pour %s le %s (%s) à %0.02f€. +Tu peux contacter le vendeur à l'adresse %s. + +Chaleureusement, +Le BdA""" % (spectacle.title, spectacle.date_no_seconds(), + spectacle.location, spectacle.price, seller.email) + + mail.send_mail("BdA-Revente : %s" % spectacle.title, + mail_buyer, "bda@ens.fr", [winner.user.email], + fail_silently=False) + mail_seller = """Bonjour, +La personne tirée au sort pour racheter ta place pour %s est %s. +Tu peux le/la contacter à l'adresse %s. + +Chaleureusement, +Le BdA""" % (spectacle.title, winner.user.get_full_name(), winner.user.email) + + mail.send_mail("BdA-Revente : %s" % spectacle.title, + mail_seller, "bda@ens.fr", [seller.email], + fail_silently=False) + self.tirage_done = True + self.save() diff --git a/bda/templates/bda-interested.html b/bda/templates/bda-interested.html new file mode 100644 index 00000000..acfb1d1e --- /dev/null +++ b/bda/templates/bda-interested.html @@ -0,0 +1,9 @@ +{% extends "base_title.html" %} +{% load staticfiles %} + +{% block realcontent %} +

Inscription à une revente

+

Votre inscription pour a bien été enregistrée !

+

Le tirage au sort pour cette revente ({{spectacle}}) sera effectué le {{date}}. + +{% endblock %} diff --git a/bda/templates/bda-no-revente.html b/bda/templates/bda-no-revente.html new file mode 100644 index 00000000..eabb3dc7 --- /dev/null +++ b/bda/templates/bda-no-revente.html @@ -0,0 +1,6 @@ +{% extends "base_title.html" %} + +{% block realcontent %} +

BdA-Revente

+

Il n'y a plus de places en revente pour ce spectacle, désolé !

+{% endblock %} diff --git a/bda/templates/bda-notpaid.html b/bda/templates/bda-notpaid.html index b25c2ba3..0dd4e4df 100644 --- a/bda/templates/bda-notpaid.html +++ b/bda/templates/bda-notpaid.html @@ -1,6 +1,6 @@ {% extends "base_title.html" %} {% block realcontent %} -

Nope

+

Nope

Avant de revendre des places, il faut aller les payer !

{% endblock %} diff --git a/bda/templates/bda-revente.html b/bda/templates/bda-revente.html index 10a93d43..a5f64678 100644 --- a/bda/templates/bda-revente.html +++ b/bda/templates/bda-revente.html @@ -1,26 +1,73 @@ {% extends "base_title.html" %} -{% load staticfiles %} - -{% block extra_head %} - -{% endblock %} +{% load bootstrap %} {% block realcontent %} -

Revente de place

-
+

Revente de place

+

Places non revendues

+ {% csrf_token %} -{% if form.spectacle.errors %}{% endif %} -

-

-Bonjour,
-
-Je souhaite revendre {{ form.count }} place(s) pour {{ form.spectacle }}.
-Contactez-moi par email si vous êtes intéressé !
-
-{{ user.get_full_name }} ({{ user.email }}) -
-

- +
+
+
    + {% for box in resellform.attributions %} +
  • + {{box.tag}} + {{box.choice_label}} +
  • + {% endfor %} +
+
+
+
+ +
+
+{% if annulform.attributions or overdue %} +

Places en cours de revente

+
+ {% csrf_token %} +
+
+
    + {% for box in annulform.attributions %} +
  • + {{box.tag}} + {{box.choice_label}} +
  • + {% endfor %} + {% for attrib in overdue %} +
  • + + {{attrib.spectacle}} +
  • + {% endfor %} +
+
+
+ {% if annulform.attributions %} + + {% endif %} +
+{% endif %} +
+{% if sold %} +

Places revendues

+ + {% for attrib in sold %} + + + {% csrf_token %} + + + + + + + {% endfor %} +
{{attrib.spectacle}}{{attrib.revente.soldTo.user.get_full_name}}
+{% endif %} {% endblock %} diff --git a/bda/templates/bda-success.html b/bda/templates/bda-success.html index ebcc87a7..5e970eb7 100644 --- a/bda/templates/bda-success.html +++ b/bda/templates/bda-success.html @@ -1,12 +1,8 @@ {% extends "base_title.html" %} {% load staticfiles %} -{% block extra_head %} - -{% endblock %} - {% block realcontent %} -

Revente de place

-

Votre offre de revente de {{ places }} pour {{ show.title }} le {{ show.date_no_seconds }} ({{ show.location }}) à {{ show.price }}€ a bien été envoyée.

+

Revente de place

+

Un mail a bien été envoyé à {{seller.get_full_name}} ({{seller.email}}), pour racheter une place pour {{spectacle.title}} !

{% endblock %} diff --git a/bda/templates/bda-wrongtime.html b/bda/templates/bda-wrongtime.html new file mode 100644 index 00000000..5e17926b --- /dev/null +++ b/bda/templates/bda-wrongtime.html @@ -0,0 +1,6 @@ +{% extends "base_title.html" %} + +{% block realcontent %} +

Nope

+

Cette revente n'est pas disponible actuellement, désolé !

+{% endblock %} diff --git a/bda/templates/liste-reventes.html b/bda/templates/liste-reventes.html new file mode 100644 index 00000000..2a5ddc95 --- /dev/null +++ b/bda/templates/liste-reventes.html @@ -0,0 +1,23 @@ +{% extends "base_title.html" %} +{% load bootstrap %} + +{% block realcontent %} +

Inscriptions pour BDA-Revente

+ {% if deja_revente %} +

Des reventes existent déjà pour certains de ces spectacles ; vérifie les places disponibles sans tirage !

+ {% endif %} +
+ {% csrf_token %} + {{form | bootstrap}} + +
+ + {% if shotgun %} +
+

Places disponibles immédiatement

+ {% endfor %} diff --git a/provisioning/cron.dev b/provisioning/cron.dev index d249d547..6cd2ca81 100644 --- a/provisioning/cron.dev +++ b/provisioning/cron.dev @@ -7,3 +7,4 @@ DBNAME="cof_gestion" DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" 19 */12 * * * date >> /vagrant/rappels.log ; python /vagrant/manage.py sendrappels >> /vagrant/rappels.log 2>&1 +*/5 * * * * python /vagrant/manage.py manage_revente >> /vagrant/reventes.log 2>&1 diff --git a/provisioning/cron.md b/provisioning/cron.md index 8b3f608e..840a8716 100644 --- a/provisioning/cron.md +++ b/provisioning/cron.md @@ -14,3 +14,14 @@ envoyés). - Garde les logs peut être une bonne idée. Exemple : voir le fichier `provisioning/cron.dev`. + +## Gestion des mails de revente + +Il faut effectuer très régulièrement la commande `manage_reventes` de GestioCOF, +qui gère toutes les actions associées à BdA-Revente : envoi des mails de notification, +tirages. + +- Pour l'instant un délai de 5 min est hardcodé +- Garde des logs ; ils vont finir par être assez lourds si on a beaucoup de reventes. + +Exemple : provisioning/cron.dev