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:
commit
d8eb5786a8
21 changed files with 648 additions and 46 deletions
18
bda/admin.py
18
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)
|
||||
|
|
50
bda/forms.py
50
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())
|
||||
|
|
36
bda/management/commands/manage_reventes.py
Normal file
36
bda/management/commands/manage_reventes.py
Normal 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")
|
59
bda/migrations/0009_revente.py
Normal file
59
bda/migrations/0009_revente.py
Normal 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),
|
||||
),
|
||||
]
|
130
bda/models.py
130
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()
|
||||
|
|
9
bda/templates/bda-interested.html
Normal file
9
bda/templates/bda-interested.html
Normal 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 %}
|
6
bda/templates/bda-no-revente.html
Normal file
6
bda/templates/bda-no-revente.html
Normal 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 %}
|
|
@ -1,6 +1,6 @@
|
|||
{% extends "base_title.html" %}
|
||||
|
||||
{% block realcontent %}
|
||||
<h2><strong>Nope</strong></h1>
|
||||
<h2><strong>Nope</strong></h2>
|
||||
<p>Avant de revendre des places, il faut aller les payer !</p>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,26 +1,73 @@
|
|||
{% extends "base_title.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||
{% endblock %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block realcontent %}
|
||||
|
||||
<h2>Revente de place</h1>
|
||||
<form action="" method="post" id="resellform">
|
||||
<h2>Revente de place</h2>
|
||||
<h3>Places non revendues</h3>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.spectacle.errors %}<ul class="errorlist"><li>Sélectionnez un spetacle</li></ul>{% endif %}
|
||||
<p>
|
||||
<pre>
|
||||
Bonjour,<br />
|
||||
<br />
|
||||
Je souhaite revendre {{ form.count }} place(s) pour {{ form.spectacle }}.<br />
|
||||
Contactez-moi par email si vous êtes intéressé !<br />
|
||||
<br />
|
||||
{{ user.get_full_name }} ({{ user.email }})
|
||||
</pre>
|
||||
</p>
|
||||
<input class="btn btn-primary" type="submit" value="Envoyer" />
|
||||
<div class="form-group">
|
||||
<div class="multiple-checkbox">
|
||||
<ul>
|
||||
{% for box in resellform.attributions %}
|
||||
<li>
|
||||
{{box.tag}}
|
||||
{{box.choice_label}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
|
||||
</div>
|
||||
</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 %}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
{% extends "base_title.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block realcontent %}
|
||||
|
||||
<h1>Revente de place</h1>
|
||||
<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>
|
||||
<h2>Revente de place</h2>
|
||||
<p class="success">Un mail a bien été envoyé à {{seller.get_full_name}} ({{seller.email}}), pour racheter une place pour {{spectacle.title}} !</p>
|
||||
{% endblock %}
|
||||
|
|
6
bda/templates/bda-wrongtime.html
Normal file
6
bda/templates/bda-wrongtime.html
Normal 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 %}
|
23
bda/templates/liste-reventes.html
Normal file
23
bda/templates/liste-reventes.html
Normal 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 %}
|
8
bda/templates/mail-revente.txt
Normal file
8
bda/templates/mail-revente.txt
Normal 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
|
8
bda/templates/mail-shotgun.txt
Normal file
8
bda/templates/mail-shotgun.txt
Normal 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
|
20
bda/templates/revente-confirm.html
Normal file
20
bda/templates/revente-confirm.html
Normal 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%}
|
|
@ -32,6 +32,15 @@ urlpatterns = [
|
|||
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
|
||||
views.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'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
|
||||
name='bda-descriptions'),
|
||||
|
|
201
bda/views.py
201
bda/views.py
|
@ -4,13 +4,16 @@ from __future__ import division
|
|||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import random
|
||||
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
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.forms.models import inlineformset_factory
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
import hashlib
|
||||
|
||||
from django.core.mail import send_mail
|
||||
|
@ -18,13 +21,15 @@ from django.utils import timezone
|
|||
from django.views.generic.list import ListView
|
||||
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
from gestioncof.decorators import cof_required, buro_required
|
||||
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\
|
||||
Tirage, render_template
|
||||
Tirage, render_template, SpectacleRevente
|
||||
from bda.algorithm import Algorithm
|
||||
|
||||
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm
|
||||
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\
|
||||
InscriptionReventeForm
|
||||
|
||||
|
||||
@cof_required
|
||||
|
@ -231,6 +236,11 @@ def do_tirage(request, tirage_id):
|
|||
Attribution(spectacle=show, participant=member)
|
||||
for show, members, _ in results
|
||||
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)
|
||||
else:
|
||||
return render(request, "bda-attrib.html", data)
|
||||
|
@ -277,14 +287,185 @@ def revente(request, tirage_id):
|
|||
user=request.user, tirage=tirage)
|
||||
if not participant.paid:
|
||||
return render(request, "bda-notpaid.html", {})
|
||||
if request.POST:
|
||||
form = ResellForm(participant, request.POST)
|
||||
if form.is_valid():
|
||||
return do_resell(request, form)
|
||||
if request.method == 'POST':
|
||||
if 'resell' in request.POST:
|
||||
resellform = ResellForm(participant, request.POST, prefix='resell')
|
||||
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:
|
||||
resellform = ResellForm(participant, prefix='resell')
|
||||
annulform = AnnulForm(participant, prefix='annul')
|
||||
else:
|
||||
form = ResellForm(participant)
|
||||
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",
|
||||
{"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
|
||||
|
|
|
@ -152,6 +152,9 @@ PETITS_COURS_REPLYTO = "cof@ens.fr"
|
|||
RAPPEL_FROM = 'Le BdA <bda@ens.fr>'
|
||||
RAPPEL_REPLY_TO = RAPPEL_FROM
|
||||
|
||||
REVENTE_FROM = 'BDA-Revente <bda-revente@ens.fr>'
|
||||
REVENTE_REPLY_TO = REVENTE_FROM
|
||||
|
||||
LOGIN_URL = "/gestion/login"
|
||||
LOGIN_REDIRECT_URL = "/gestion/"
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
{% else %}
|
||||
<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-liste-revente" tirage.id %}">S'inscrire à BDA-Revente</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue