Merge branch 'Aufinal/clean_code' into 'master'

Rend le code de BdA-Revente plus robuste

- `shotgun` devient un champ, et nécessite que `tirage_done` vaille `True` (plus de places au shotgun avant que le tirage au sort ne soit fait)
- suppression de code mort
- correction d'un bug sur les reventes de reventes
- améliorations diverses : commentaires, messages d'erreur
Selon moi, ça règle le 3e point de #101.

See merge request !114
This commit is contained in:
Martin Pepin 2016-12-21 17:17:36 +01:00
commit 6e5c3c8c33
11 changed files with 129 additions and 81 deletions

View file

@ -225,7 +225,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
list_display = ("spectacle", "seller", "date", "soldTo") list_display = ("spectacle", "seller", "date", "soldTo")
raw_id_fields = ("attribution",) raw_id_fields = ("attribution",)
readonly_fields = ("shotgun", "expiration_time") readonly_fields = ("date_tirage",)
search_fields = ['attribution__spectacle__title', search_fields = ['attribution__spectacle__title',
'seller__user__username', 'seller__user__username',
'seller__user__first_name', 'seller__user__first_name',

View file

@ -68,9 +68,8 @@ class AnnulForm(forms.Form):
self.fields['attributions'].queryset = participant.attribution_set\ self.fields['attributions'].queryset = participant.attribution_set\
.filter(spectacle__date__gte=timezone.now(), .filter(spectacle__date__gte=timezone.now(),
revente__isnull=False, revente__isnull=False,
revente__date__gt=timezone.now()-timedelta(hours=1))\ revente__date__gt=timezone.now()-timedelta(hours=1),
.filter(Q(revente__soldTo__isnull=True) | revente__soldTo__isnull=True)
Q(revente__soldTo=participant))
class InscriptionReventeForm(forms.Form): class InscriptionReventeForm(forms.Form):

View file

@ -36,7 +36,7 @@ class Command(BaseCommand):
revente.send_notif() revente.send_notif()
self.stdout.write("Mail d'inscription à une revente envoyé") self.stdout.write("Mail d'inscription à une revente envoyé")
# Check si tirage à faire # Check si tirage à faire
elif (now >= revente.expiration_time and elif (now >= revente.date_tirage and
not revente.tirage_done): not revente.tirage_done):
self.stdout.write(str(now)) self.stdout.write(str(now))
revente.tirage() revente.tirage()

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.utils import timezone
from datetime import timedelta
def forwards_func(apps, schema_editor):
SpectacleRevente = apps.get_model("bda", "SpectacleRevente")
for revente in SpectacleRevente.objects.all():
is_expired = timezone.now() > revente.date_tirage()
is_direct = (revente.attribution.spectacle.date >= revente.date and
timezone.now() > revente.date + timedelta(minutes=15))
revente.shotgun = is_expired or is_direct
revente.save()
class Migration(migrations.Migration):
dependencies = [
('bda', '0009_revente'),
]
operations = [
migrations.AddField(
model_name='spectaclerevente',
name='shotgun',
field=models.BooleanField(default=False, verbose_name='Disponible imm\xe9diatement'),
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
]

View file

@ -229,32 +229,20 @@ class SpectacleRevente(models.Model):
default=False) default=False)
tirage_done = models.BooleanField("Tirage effectué", tirage_done = models.BooleanField("Tirage effectué",
default=False) default=False)
shotgun = models.BooleanField("Disponible immédiatement",
default=False)
@property @property
def expiration_time(self): def date_tirage(self):
"""Renvoie la date du tirage au sort de la revente."""
# L'acheteur doit être connu au plus 12h avant le spectacle # L'acheteur doit être connu au plus 12h avant le spectacle
remaining_time = (self.attribution.spectacle.date remaining_time = (self.attribution.spectacle.date
- self.date - timedelta(hours=13)) - self.date - timedelta(hours=13))
# Au minimum, on attend 2 jours avant le tirage # Au minimum, on attend 2 jours avant le tirage
delay = min(remaining_time, timedelta(days=2)) delay = min(remaining_time, timedelta(days=2))
# On a aussi 1h pour changer d'avis # Le vendeur a aussi 1h pour changer d'avis
return self.date + delay + timedelta(hours=1) return self.date + delay + timedelta(hours=1)
def expiration_time_str(self):
return self.expiration_time \
.astimezone(timezone.get_current_timezone()) \
.strftime('%d/%m/%y à %H:%M')
@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): def __str__(self):
return "%s -- %s" % (self.seller, return "%s -- %s" % (self.seller,
self.attribution.spectacle.title) self.attribution.spectacle.title)
@ -312,6 +300,9 @@ class SpectacleRevente(models.Model):
connection = mail.get_connection() connection = mail.get_connection()
connection.send_messages(mails_to_send) connection.send_messages(mails_to_send)
self.notif_sent = True self.notif_sent = True
# Flag inutile, sauf si l'horloge interne merde
self.tirage_done = True
self.shotgun = True
self.save() self.save()
def tirage(self): def tirage(self):
@ -371,5 +362,10 @@ class SpectacleRevente(models.Model):
reply_to=[settings.MAIL_DATA['revente']['REPLYTO']], reply_to=[settings.MAIL_DATA['revente']['REPLYTO']],
)) ))
mail.get_connection().send_messages(mails) mail.get_connection().send_messages(mails)
# Si personne ne veut de la place, elle part au shotgun
else:
self.shotgun = True
self.tirage_done = True self.tirage_done = True
self.save() self.save()

View file

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

View file

@ -1,6 +1,13 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% block realcontent %} {% block realcontent %}
<h2>Nope</h2> <h2>Nope</h2>
<p>Cette revente n'est pas disponible actuellement, désolé !</p> {% if revente.shotgun %}
<p>Le tirage au sort de cette revente a déjà été effectué !</p>
<p>Si personne n'était intéressé, elle est maintenant disponible
<a href="{% url "bda-buy-revente" revente.attribution.spectacle.id %}">ici</a>.</p>
{% else %}
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -2,7 +2,7 @@ Bonjour {{ vendeur.first_name }},
Tu tes bien inscrit-e pour la revente de {{ spectacle.title }}. Tu tes bien inscrit-e pour la revente de {{ spectacle.title }}.
{% with revente.expiration_time as time %} {% with revente.date_tirage as time %}
Le tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu Le tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu
le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}). le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}).
Si personne ne sest inscrit pour racheter la place, celle-ci apparaitra parmi Si personne ne sest inscrit pour racheter la place, celle-ci apparaitra parmi

View file

@ -3,10 +3,12 @@ Bonjour {{ user.first_name }}
Une place pour le spectacle {{ spectacle.title }} ({{ spectacle.date }}) Une place pour le spectacle {{ spectacle.title }} ({{ spectacle.date }})
a été postée sur BdA-Revente. a été postée sur BdA-Revente.
{% with revente.date_tirage as time %}
Si ce spectacle t'intéresse toujours, merci de nous le signaler en cliquant Si ce spectacle t'intéresse toujours, merci de nous le signaler en cliquant
sur ce lien : http://{{ domain }}{% url "bda-revente-interested" revente.id %}. sur ce lien : http://{{ domain }}{% url "bda-revente-interested" revente.id %}.
Dans le cas où plusieurs personnes seraient intéressées, nous procèderons à Dans le cas où plusieurs personnes seraient intéressées, nous procèderons à
un tirage au sort le {{ revente.expiration_time_str }}. un tirage au sort le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil}}).
{% endwith %}
Chaleureusement, Chaleureusement,
Le BdA Le BdA

View file

@ -8,7 +8,10 @@
{% endif %} {% endif %}
{% if deja_revente %} {% if deja_revente %}
<p class="success">Des reventes existent déjà pour certains de ces spectacles ; vérifie les places disponibles sans tirage !</p> <p class="success">Des reventes existent déjà pour certains de ces spectacles ; vérifie les places disponibles sans tirage !</p>
{% elif inscrit_revente %}
<p class="success">Tu as été inscrit à une revente en cours pour ce spectacle !</p>
{% endif %} {% endif %}
<form action="" class="form-horizontal" method="post"> <form action="" class="form-horizontal" method="post">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">

View file

@ -271,6 +271,7 @@ def revente(request, tirage_id):
if not participant.paid: if not participant.paid:
return render(request, "bda-notpaid.html", {}) return render(request, "bda-notpaid.html", {})
if request.method == 'POST': if request.method == 'POST':
# On met en vente une place
if 'resell' in request.POST: if 'resell' in request.POST:
resellform = ResellForm(participant, request.POST, prefix='resell') resellform = ResellForm(participant, request.POST, prefix='resell')
annulform = AnnulForm(participant, prefix='annul') annulform = AnnulForm(participant, prefix='annul')
@ -279,27 +280,36 @@ def revente(request, tirage_id):
attributions = resellform.cleaned_data["attributions"] attributions = resellform.cleaned_data["attributions"]
with transaction.atomic(): with transaction.atomic():
for attribution in attributions: for attribution in attributions:
revente, created = SpectacleRevente.objects.get_or_create( revente, created = \
SpectacleRevente.objects.get_or_create(
attribution=attribution, attribution=attribution,
defaults={'seller': participant}) defaults={'seller': participant})
if not created: if not created:
revente.seller = participant revente.seller = participant
revente.date = timezone.now() revente.date = timezone.now()
mail_subject = "BdA-Revente : {:s}".format(attribution.spectacle.title) revente.soldTo = None
mail_body = loader.render_to_string('bda/mails/revente-new.txt', { revente.notif_sent = False
'vendeur': participant.user, revente.tirage_done = False
revente.shotgun = False
mail_subject = "BdA-Revente : {:s}".format(
attribution.spectacle.title)
mail_body = loader.render_to_string(
'bda/mails/revente-new.txt',
{'vendeur': participant.user,
'spectacle': attribution.spectacle, 'spectacle': attribution.spectacle,
'revente': revente, 'revente': revente}
}) )
mails.append(mail.EmailMessage( mails.append(mail.EmailMessage(
mail_subject, mail_body, mail_subject, mail_body,
from_email=settings.MAIL_DATA['revente']['FROM'], from_email=settings.MAIL_DATA['revente']['FROM'],
to=[participant.user.email], to=[participant.user.email],
reply_to=[settings.MAIL_DATA['revente']['REPLYTO']], reply_to=[
settings.MAIL_DATA['revente']['REPLYTO']
],
)) ))
revente.save() revente.save()
mail.get_connection().send_messages(mails) mail.get_connection().send_messages(mails)
# On annule une revente
elif 'annul' in request.POST: elif 'annul' in request.POST:
annulform = AnnulForm(participant, request.POST, prefix='annul') annulform = AnnulForm(participant, request.POST, prefix='annul')
resellform = ResellForm(participant, prefix='resell') resellform = ResellForm(participant, prefix='resell')
@ -307,7 +317,8 @@ def revente(request, tirage_id):
attributions = annulform.cleaned_data["attributions"] attributions = annulform.cleaned_data["attributions"]
for attribution in attributions: for attribution in attributions:
attribution.revente.delete() attribution.revente.delete()
# On confirme une vente en transférant la place à la personne qui a
# gagné le tirage
elif 'transfer' in request.POST: elif 'transfer' in request.POST:
resellform = ResellForm(participant, prefix='resell') resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul') annulform = AnnulForm(participant, prefix='annul')
@ -320,7 +331,9 @@ def revente(request, tirage_id):
attrib = revente.attribution attrib = revente.attribution
attrib.participant = revente.soldTo attrib.participant = revente.soldTo
attrib.save() attrib.save()
# On annule la revente après le tirage au sort (par exemple si
# la personne qui a gagné le tirage ne se manifeste pas). La place est
# alors remise en vente
elif 'reinit' in request.POST: elif 'reinit' in request.POST:
resellform = ResellForm(participant, prefix='resell') resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul') annulform = AnnulForm(participant, prefix='annul')
@ -334,6 +347,7 @@ def revente(request, tirage_id):
revente.soldTo = None revente.soldTo = None
revente.notif_sent = False revente.notif_sent = False
revente.tirage_done = False revente.tirage_done = False
revente.shotgun = False
if revente.answered_mail: if revente.answered_mail:
revente.answered_mail.clear() revente.answered_mail.clear()
revente.save() revente.save()
@ -354,8 +368,7 @@ def revente(request, tirage_id):
sold = participant.attribution_set.filter( sold = participant.attribution_set.filter(
spectacle__date__gte=timezone.now(), spectacle__date__gte=timezone.now(),
revente__isnull=False, revente__isnull=False,
revente__soldTo__isnull=False).exclude( revente__soldTo__isnull=False)
revente__soldTo=participant)
return render(request, "bda-revente.html", return render(request, "bda-revente.html",
{'tirage': tirage, 'overdue': overdue, "sold": sold, {'tirage': tirage, 'overdue': overdue, "sold": sold,
@ -367,13 +380,14 @@ def revente_interested(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id) revente = get_object_or_404(SpectacleRevente, id=revente_id)
participant, created = Participant.objects.get_or_create( participant, created = Participant.objects.get_or_create(
user=request.user, tirage=revente.attribution.spectacle.tirage) user=request.user, tirage=revente.attribution.spectacle.tirage)
if timezone.now() < revente.date + timedelta(hours=1) or revente.shotgun: if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun:
return render(request, "bda-wrongtime.html", {}) return render(request, "bda-wrongtime.html",
{"revente": revente})
revente.answered_mail.add(participant) revente.answered_mail.add(participant)
return render(request, "bda-interested.html", return render(request, "bda-interested.html",
{"spectacle": revente.attribution.spectacle, {"spectacle": revente.attribution.spectacle,
"date": revente.expiration_time}) "date": revente.date_tirage})
@login_required @login_required
@ -381,23 +395,9 @@ def list_revente(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
participant, created = Participant.objects.get_or_create( participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage) user=request.user, tirage=tirage)
spectacles = tirage.spectacle_set.filter(
date__gte=timezone.now())
shotgun = []
deja_revente = False deja_revente = False
success = False success = False
for spectacle in spectacles: inscrit_revente = False
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': if request.method == 'POST':
form = InscriptionReventeForm(tirage, request.POST) form = InscriptionReventeForm(tirage, request.POST)
if form.is_valid(): if form.is_valid():
@ -407,15 +407,24 @@ def list_revente(request, tirage_id):
for spectacle in choices: for spectacle in choices:
qset = SpectacleRevente.objects.filter( qset = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle) attribution__spectacle=spectacle)
if qset.exists(): if qset.filter(shotgun=True, soldTo__isnull=True).exists():
# On l'inscrit à l'un des tirages au sort # Une place est disponible au shotgun, on suggère à
for revente in qset.all(): # l'utilisateur d'aller la récupérer
if revente.shotgun and not revente.soldTo:
deja_revente = True deja_revente = True
else: else:
revente.answered_mail.add(participant) # La place n'est pas disponible au shotgun, si des reventes
revente.save() # pour ce spectacle existent déjà, on inscrit la personne à
break # la revente ayant le moins d'inscrits
min_resell = (
qset.filter(shotgun=False)
.annotate(nb_subscribers=Count('answered_mail'))
.order_by('nb_subscribers')
.first()
)
if min_resell is not None:
min_resell.answered_mail.add(participant)
min_resell.save()
inscrit_revente = True
success = True success = True
else: else:
form = InscriptionReventeForm( form = InscriptionReventeForm(
@ -423,8 +432,9 @@ def list_revente(request, tirage_id):
initial={'spectacles': participant.choicesrevente.all()}) initial={'spectacles': participant.choicesrevente.all()})
return render(request, "liste-reventes.html", return render(request, "liste-reventes.html",
{"form": form, 'shotgun': shotgun, {"form": form,
"deja_revente": deja_revente, "success": success}) "deja_revente": deja_revente, "success": success,
"inscrit_revente": inscrit_revente})
@login_required @login_required
@ -436,15 +446,16 @@ def buy_revente(request, spectacle_id):
reventes = SpectacleRevente.objects.filter( reventes = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle, attribution__spectacle=spectacle,
soldTo__isnull=True) soldTo__isnull=True)
# Si l'utilisateur veut racheter une place qu'il est en train de revendre,
# on supprime la revente en question.
if reventes.filter(seller=participant).exists(): if reventes.filter(seller=participant).exists():
revente = reventes.filter(seller=participant)[0] revente = reventes.filter(seller=participant)[0]
revente.delete() revente.delete()
return HttpResponseRedirect(reverse("bda-shotgun", return HttpResponseRedirect(reverse("bda-shotgun",
args=[tirage.id])) args=[tirage.id]))
reventes_shotgun = []
for revente in reventes.all(): reventes_shotgun = list(reventes.filter(shotgun=True).all())
if revente.shotgun:
reventes_shotgun.append(revente)
if not reventes_shotgun: if not reventes_shotgun:
return render(request, "bda-no-revente.html", {}) return render(request, "bda-no-revente.html", {})
@ -478,14 +489,11 @@ def revente_shotgun(request, tirage_id):
date__gte=timezone.now()) date__gte=timezone.now())
shotgun = [] shotgun = []
for spectacle in spectacles: for spectacle in spectacles:
revente_objects = SpectacleRevente.objects.filter( reventes = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle, attribution__spectacle=spectacle,
shotgun=True,
soldTo__isnull=True) soldTo__isnull=True)
revente_count = 0 if reventes.exists():
for revente in revente_objects:
if revente.shotgun:
revente_count += 1
if revente_count:
shotgun.append(spectacle) shotgun.append(spectacle)
return render(request, "bda-shotgun.html", return render(request, "bda-shotgun.html",