Merge branch 'Aufinal/bda_revente' into 'master'

Aufinal/bda revente

Nouvelle interface BDA-Revente:
- on s'inscrit pour recevoir les reventes sur `liste-revente`, où on peut aussi récupérer les places au shotgun
- on coche les places qu'on veut revendre sur `bda/places`, et on dispose d'une heure pour changer d'avis
- quand une place est revendue, on envoie un mail à tous les gens inscrits ; ils ont un délai pour répondre (dépendant du temps restant avant le spectacle)
- on fait un tirage au sort entre ceux ayant répondu

See merge request !68
This commit is contained in:
Martin Pepin 2016-09-27 15:46:19 +02:00
commit d8eb5786a8
21 changed files with 648 additions and 46 deletions

View file

@ -9,7 +9,7 @@ from django.core.mail import send_mail
from django.contrib import admin from django.contrib import admin
from django.db.models import Sum, Count from django.db.models import Sum, Count
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
Attribution, Tirage, Quote, CategorieSpectacle Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
from django import forms from django import forms
from datetime import timedelta from datetime import timedelta
@ -210,6 +210,21 @@ class SalleAdmin(admin.ModelAdmin):
search_fields = ('name', 'address') 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(CategorieSpectacle)
admin.site.register(Spectacle, SpectacleAdmin) admin.site.register(Spectacle, SpectacleAdmin)
admin.site.register(Salle, SalleAdmin) admin.site.register(Salle, SalleAdmin)
@ -217,3 +232,4 @@ admin.site.register(Participant, ParticipantAdmin)
admin.site.register(Attribution, AttributionAdmin) admin.site.register(Attribution, AttributionAdmin)
admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin) admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin)
admin.site.register(Tirage, TirageAdmin) admin.site.register(Tirage, TirageAdmin)
admin.site.register(SpectacleRevente, SpectacleReventeAdmin)

View file

@ -4,9 +4,13 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import timedelta
from django import forms from django import forms
from django.forms.models import BaseInlineFormSet 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): class BaseBdaFormSet(BaseInlineFormSet):
@ -35,17 +39,47 @@ class TokenForm(forms.Form):
token = forms.CharField(widget=forms.widgets.Textarea()) token = forms.CharField(widget=forms.widgets.Textarea())
class SpectacleModelChoiceField(forms.ModelChoiceField): class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj): def label_from_instance(self, obj):
return "%s le %s (%s) à %.02f" % (obj.title, obj.date_no_seconds(), return "%s" % obj.spectacle
obj.location, obj.price)
class ResellForm(forms.Form): class ResellForm(forms.Form):
count = forms.ChoiceField(choices=(("1", "1"), ("2", "2"),)) attributions = AttributionModelMultipleChoiceField(
spectacle = SpectacleModelChoiceField(queryset=Spectacle.objects.none()) queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super(ResellForm, self).__init__(*args, **kwargs) super(ResellForm, self).__init__(*args, **kwargs)
self.fields['spectacle'].queryset = participant.attributions.all() \ self.fields['attributions'].queryset = participant.attribution_set\
.distinct() .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())

View file

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

View file

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

View file

@ -5,6 +5,8 @@ from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
import calendar import calendar
import random
from datetime import timedelta
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -158,6 +160,9 @@ class Participant(models.Model):
max_length=6, choices=PAYMENT_TYPES, max_length=6, choices=PAYMENT_TYPES,
blank=True) blank=True)
tirage = models.ForeignKey(Tirage) tirage = models.ForeignKey(Tirage)
choicesrevente = models.ManyToManyField(Spectacle,
related_name="subscribed",
blank=True)
def __str__(self): def __str__(self):
return "%s - %s" % (self.user, self.tirage.title) return "%s - %s" % (self.user, self.tirage.title)
@ -205,4 +210,127 @@ class Attribution(models.Model):
given = models.BooleanField("Donnée", default=False) given = models.BooleanField("Donnée", default=False)
def __str__(self): 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()

View file

@ -0,0 +1,9 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block realcontent %}
<h2>Inscription à une revente</h2>
<p class="success"> Votre inscription pour a bien été enregistrée !</p>
<p>Le tirage au sort pour cette revente ({{spectacle}}) sera effectué le {{date}}.
{% endblock %}

View file

@ -0,0 +1,6 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>BdA-Revente</h2>
<p>Il n'y a plus de places en revente pour ce spectacle, désolé !</p>
{% endblock %}

View file

@ -1,6 +1,6 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% block realcontent %} {% block realcontent %}
<h2><strong>Nope</strong></h1> <h2><strong>Nope</strong></h2>
<p>Avant de revendre des places, il faut aller les payer !</p> <p>Avant de revendre des places, il faut aller les payer !</p>
{% endblock %} {% endblock %}

View file

@ -1,26 +1,73 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load bootstrap %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %} {% block realcontent %}
<h2>Revente de place</h1> <h2>Revente de place</h2>
<form action="" method="post" id="resellform"> <h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
{% csrf_token %} {% csrf_token %}
{% if form.spectacle.errors %}<ul class="errorlist"><li>Sélectionnez un spetacle</li></ul>{% endif %} <div class="form-group">
<p> <div class="multiple-checkbox">
<pre> <ul>
Bonjour,<br /> {% for box in resellform.attributions %}
<br /> <li>
Je souhaite revendre {{ form.count }} place(s) pour {{ form.spectacle }}.<br /> {{box.tag}}
Contactez-moi par email si vous êtes intéressé !<br /> {{box.choice_label}}
<br /> </li>
{{ user.get_full_name }} ({{ user.email }}) {% endfor %}
</pre> </ul>
</p> </div>
<input class="btn btn-primary" type="submit" value="Envoyer" /> </div>
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form> </form>
<br>
{% if annulform.attributions or overdue %}
<h3>Places en cours de revente</h3>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<div class="multiple-checkbox">
<ul>
{% for box in annulform.attributions %}
<li>
{{box.tag}}
{{box.choice_label}}
</li>
{% endfor %}
{% for attrib in overdue %}
<li>
<input type="checkbox" style="visibility:hidden">
{{attrib.spectacle}}
</li>
{% endfor %}
</ul>
</div>
</div>
{% if annulform.attributions %}
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
{% endif %}
</form>
{% endif %}
<br>
{% if sold %}
<h3>Places revendues</h3>
<table class="table">
{% for attrib in sold %}
<tr>
<form action="" method="post">
{% csrf_token %}
<td>{{attrib.spectacle}}</td>
<td>{{attrib.revente.soldTo.user.get_full_name}}</td>
<td><button type="submit" class="btn btn-primary" name="transfer"
value={{attrib.revente.id}}>Transférer</button></td>
<td><button type="submit" class="btn btn-primary" name="reinit"
value={{attrib.revente.id}}>Réinitialiser</button></td>
</form>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -1,12 +1,8 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %} {% block realcontent %}
<h1>Revente de place</h1> <h2>Revente de place</h2>
<p class="success">Votre offre de revente de {{ places }} pour {{ show.title }} le {{ show.date_no_seconds }} ({{ show.location }}) à {{ show.price }}€ a bien été envoyée.</p> <p class="success">Un mail a bien été envoyé à {{seller.get_full_name}} ({{seller.email}}), pour racheter une place pour {{spectacle.title}} !</p>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,6 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Nope</h2>
<p>Cette revente n'est pas disponible actuellement, désolé !</p>
{% endblock %}

View file

@ -0,0 +1,23 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Inscriptions pour BDA-Revente</h2>
{% if deja_revente %}
<p class="success">Des reventes existent déjà pour certains de ces spectacles ; vérifie les places disponibles sans tirage !</p>
{% endif %}
<form action="" class="form-horizontal" method="post">
{% csrf_token %}
{{form | bootstrap}}
<input type="submit" class="btn btn-primary" value="S'inscrire pour les places sélectionnées">
</form>
{% if shotgun %}
<br>
<h3>Places disponibles immédiatement</h3>
<ul class="list-unstyled">
{% for spectacle in shotgun %}
<li><a href="{% url "bda-buy-revente" spectacle.id %}">{{spectacle}}</a></li>
{% endfor %}
{% endif %}
{% endblock %}

View file

@ -0,0 +1,8 @@
Bonjour {{ user.get_full_name }}
Une place pour le spectacle {{ spectacle.title }} ({{spectacle.date_no_seconds}}) a été postée sur BdA-Revente.
Si ce spectacle t'intéresse toujours, merci de nous le signaler en cliquant sur ce lien : {% url "bda-interested-revente" revente.id %}. Dans le cas où plusieurs personnes seraient intéressées, nous procèderons à un tirage au sort le {{revente.date_no_seconds}}
Chaleureusement,
Le BdA

View file

@ -0,0 +1,8 @@
Bonjour {{ user.get_full_name }}
Une place pour le spectacle {{ spectacle.title }} ({{spectacle.date_no_seconds}}) a été postée sur BdA-Revente.
Puisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour cette place : elle est disponible immédiatement à l'addresse {{url "bda-buy-revente" spectacle.id}}, à la disposition de tous.
Chaleureusement,
Le BdA

View file

@ -0,0 +1,20 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{%block realcontent %}
<h2>Rachat d'une place</h2>
<form action="" method="post">
{% csrf_token %}
<pre>
Bonjour !
Je souhaiterais racheter ta place pour {{spectacle.title}} le {{spectacle.date}} ({{spectacle.location}}) à {{spectacle.price}}€.
Contacte-moi si tu es toujours intéressé-e !
{{user.get_full_name}} ({{user.email}})
</pre>
<input type="submit" class="btn btn-primary pull-right" value="Envoyer">
</form>
<p class="bda-prix">Note : ce mail sera envoyé à une personne au hasard revendant sa place.</p>
{%endblock%}

View file

@ -32,6 +32,15 @@ urlpatterns = [
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$', url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
views.unpaid, views.unpaid,
name="bda-unpaid"), name="bda-unpaid"),
url(r'^liste-revente/(?P<tirage_id>\d+)$',
"bda.views.list_revente",
name="bda-liste-revente"),
url(r'^buy-revente/(?P<spectacle_id>\d+)$',
"bda.views.buy_revente",
name="bda-buy-revente"),
url(r'^revente-interested/(?P<revente_id>\d+)$',
"bda.views.revente_interested",
name='bda-revente-interested'),
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel), url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel),
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles, url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
name='bda-descriptions'), name='bda-descriptions'),

View file

@ -4,13 +4,16 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
import random
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db import models from django.db import models
from django.db.models import Count from django.db.models import Count, Q
from django.core import serializers from django.core import serializers
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.core.urlresolvers import reverse
import hashlib import hashlib
from django.core.mail import send_mail from django.core.mail import send_mail
@ -18,13 +21,15 @@ from django.utils import timezone
from django.views.generic.list import ListView from django.views.generic.list import ListView
import time import time
from datetime import timedelta
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, render_template Tirage, render_template, SpectacleRevente
from bda.algorithm import Algorithm from bda.algorithm import Algorithm
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\
InscriptionReventeForm
@cof_required @cof_required
@ -231,6 +236,11 @@ def do_tirage(request, tirage_id):
Attribution(spectacle=show, participant=member) Attribution(spectacle=show, participant=member)
for show, members, _ in results for show, members, _ in results
for member, _, _, _ in members]) for member, _, _, _ in members])
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
for (show, _, losers) in results:
for (loser, _, _, _) in losers:
loser.choicesrevente.add(show)
loser.save()
return render(request, "bda-attrib-extra.html", data) return render(request, "bda-attrib-extra.html", data)
else: else:
return render(request, "bda-attrib.html", data) return render(request, "bda-attrib.html", data)
@ -277,14 +287,185 @@ def revente(request, tirage_id):
user=request.user, tirage=tirage) user=request.user, tirage=tirage)
if not participant.paid: if not participant.paid:
return render(request, "bda-notpaid.html", {}) return render(request, "bda-notpaid.html", {})
if request.POST: if request.method == 'POST':
form = ResellForm(participant, request.POST) if 'resell' in request.POST:
if form.is_valid(): resellform = ResellForm(participant, request.POST, prefix='resell')
return do_resell(request, form) annulform = AnnulForm(participant, prefix='annul')
if resellform.is_valid():
attributions = resellform.cleaned_data["attributions"]
for attribution in attributions:
revente, created = SpectacleRevente.objects.get_or_create(
attribution=attribution,
defaults={'seller': participant})
if not created:
revente.seller = participant
revente.date = timezone.now()
revente.save()
elif 'annul' in request.POST:
annulform = AnnulForm(participant, request.POST, prefix='annul')
resellform = ResellForm(participant, prefix='resell')
if annulform.is_valid():
attributions = annulform.cleaned_data["attributions"]
for attribution in attributions:
attribution.revente.delete()
elif 'transfer' in request.POST:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
revente_id = request.POST['transfer'][0]
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
id=revente_id)
if rev.exists():
revente = rev.get()
attrib = revente.attribution
attrib.participant = revente.soldTo
attrib.save()
elif 'reinit' in request.POST:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
revente_id = request.POST['reinit'][0]
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
id=revente_id)
if rev.exists():
revente = rev.get()
revente.date = timezone.now() - timedelta(hours=1)
revente.soldTo = None
revente.answered_mail = None
else: else:
form = ResellForm(participant) resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
else:
resellform = ResellForm(participant, prefix='resell')
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(
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).exclude(
revente__soldTo=participant)
return render(request, "bda-revente.html", return render(request, "bda-revente.html",
{"form": form, 'tirage': tirage}) {'tirage': tirage, 'overdue': overdue, "sold": sold,
"annulform": annulform, "resellform": resellform})
@login_required
def revente_interested(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id)
participant, created = 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", {})
revente.answered_mail.add(participant)
return render(request, "bda-interested.html",
{"spectacle": revente.attribution.spectacle,
"date": revente.expiration_time})
@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)
spectacles = tirage.spectacle_set.filter(
date__gte=timezone.now())
shotgun = []
deja_revente = False
for spectacle in spectacles:
revente_objects = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle,
soldTo__isnull=True)
revente_count = 0
for revente in revente_objects:
if revente.shotgun:
revente_count += 1
if revente_count:
spectacle.revente_count = revente_count
shotgun.append(spectacle)
if request.method == 'POST':
form = InscriptionReventeForm(tirage, request.POST)
if form.is_valid():
choices = form.cleaned_data['spectacles']
participant.choicesrevente = choices
participant.save()
for spectacle in choices:
qset = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle)
if qset.exists():
# On l'inscrit à l'un des tirages au sort
for revente in qset.all():
if revente.shotgun and not revente.soldTo:
deja_revente = True
else:
revente.answered_mail.add(participant)
break
else:
form = InscriptionReventeForm(
tirage,
initial={'spectacles': participant.choicesrevente.all()})
return render(request, "liste-reventes.html",
{"form": form, 'shotgun': shotgun,
"deja_revente": deja_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)
reventes = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle,
soldTo__isnull=True)
if reventes.filter(seller=participant).exists():
revente = reventes.filter(seller=participant)[0]
revente.delete()
return HttpResponseRedirect(reverse("bda-liste-revente",
args=[tirage.id]))
reventes_shotgun = []
for revente in reventes.all():
if revente.shotgun:
reventes_shotgun.append(revente)
if reventes_shotgun.empty():
return render(request, "bda-no-revente.html", {})
if request.POST:
revente = random.choice(reventes_shotgun)
revente.soldTo = participant
revente.save()
mail = """Bonjour !
Je souhaiterais racheter ta place pour %s le %s (%s) à %.02f.
Contacte-moi si tu es toujours intéressé·e !
%s (%s)""" % (spectacle.title, spectacle.date_no_seconds(),
spectacle.location, spectacle.price,
request.user.get_full_name(), request.user.email)
send_mail("BdA-Revente : %s" % spectacle.title, mail,
request.user.email,
[revente.seller.user.email],
fail_silently=False)
return render(request, "bda-success.html",
{"seller": revente.attribution.participant.user,
"spectacle": spectacle})
return render(request, "revente-confirm.html",
{"spectacle": spectacle,
"user": request.user})
@buro_required @buro_required

View file

@ -152,6 +152,9 @@ PETITS_COURS_REPLYTO = "cof@ens.fr"
RAPPEL_FROM = 'Le BdA <bda@ens.fr>' RAPPEL_FROM = 'Le BdA <bda@ens.fr>'
RAPPEL_REPLY_TO = RAPPEL_FROM RAPPEL_REPLY_TO = RAPPEL_FROM
REVENTE_FROM = 'BDA-Revente <bda-revente@ens.fr>'
REVENTE_REPLY_TO = REVENTE_FROM
LOGIN_URL = "/gestion/login" LOGIN_URL = "/gestion/login"
LOGIN_REDIRECT_URL = "/gestion/" LOGIN_REDIRECT_URL = "/gestion/"

View file

@ -43,6 +43,7 @@
{% else %} {% else %}
<li><a href="{% url "bda-places-attribuees" tirage.id %}">Mes places</a></li> <li><a href="{% url "bda-places-attribuees" tirage.id %}">Mes places</a></li>
<li><a href="{% url "bda-revente" tirage.id %}">Revendre une place</a></li> <li><a href="{% url "bda-revente" tirage.id %}">Revendre une place</a></li>
<li><a href="{% url "bda-liste-revente" tirage.id %}">S'inscrire à BDA-Revente</a></li>
{% endif %} {% endif %}
</ul> </ul>
{% endfor %} {% endfor %}

View file

@ -7,3 +7,4 @@ DBNAME="cof_gestion"
DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
19 */12 * * * date >> /vagrant/rappels.log ; python /vagrant/manage.py sendrappels >> /vagrant/rappels.log 2>&1 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

View file

@ -14,3 +14,14 @@ envoyés).
- Garde les logs peut être une bonne idée. - Garde les logs peut être une bonne idée.
Exemple : voir le fichier `provisioning/cron.dev`. 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