diff --git a/bda/admin.py b/bda/admin.py index a9b3c51f..0e9b683b 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -225,7 +225,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin): list_display = ("spectacle", "seller", "date", "soldTo") raw_id_fields = ("attribution",) - readonly_fields = ("shotgun", "expiration_time") + readonly_fields = ("date_tirage",) search_fields = ['attribution__spectacle__title', 'seller__user__username', 'seller__user__first_name', diff --git a/bda/forms.py b/bda/forms.py index fe20e565..352914e4 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -68,9 +68,8 @@ class AnnulForm(forms.Form): self.fields['attributions'].queryset = participant.attribution_set\ .filter(spectacle__date__gte=timezone.now(), revente__isnull=False, - revente__date__gt=timezone.now()-timedelta(hours=1))\ - .filter(Q(revente__soldTo__isnull=True) | - Q(revente__soldTo=participant)) + revente__date__gt=timezone.now()-timedelta(hours=1), + revente__soldTo__isnull=True) class InscriptionReventeForm(forms.Form): diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index 4b90bc57..0302ec4b 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -36,7 +36,7 @@ class Command(BaseCommand): revente.send_notif() self.stdout.write("Mail d'inscription à une revente envoyé") # Check si tirage à faire - elif (now >= revente.expiration_time and + elif (now >= revente.date_tirage and not revente.tirage_done): self.stdout.write(str(now)) revente.tirage() diff --git a/bda/migrations/0010_spectaclerevente_shotgun.py b/bda/migrations/0010_spectaclerevente_shotgun.py new file mode 100644 index 00000000..35b4da8a --- /dev/null +++ b/bda/migrations/0010_spectaclerevente_shotgun.py @@ -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), + ] diff --git a/bda/models.py b/bda/models.py index 85c7fa4d..5b814d3d 100644 --- a/bda/models.py +++ b/bda/models.py @@ -229,32 +229,20 @@ class SpectacleRevente(models.Model): default=False) tirage_done = models.BooleanField("Tirage effectué", default=False) + shotgun = models.BooleanField("Disponible immédiatement", + default=False) @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 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 + # Le vendeur a aussi 1h pour changer d'avis 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): return "%s -- %s" % (self.seller, self.attribution.spectacle.title) @@ -312,6 +300,9 @@ class SpectacleRevente(models.Model): connection = mail.get_connection() connection.send_messages(mails_to_send) self.notif_sent = True + # Flag inutile, sauf si l'horloge interne merde + self.tirage_done = True + self.shotgun = True self.save() def tirage(self): @@ -371,5 +362,10 @@ class SpectacleRevente(models.Model): reply_to=[settings.MAIL_DATA['revente']['REPLYTO']], )) 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.save() diff --git a/bda/templates/bda-interested.html b/bda/templates/bda-interested.html index acfb1d1e..780330bd 100644 --- a/bda/templates/bda-interested.html +++ b/bda/templates/bda-interested.html @@ -3,7 +3,7 @@ {% block realcontent %}

Inscription à une revente

-

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

+

Votre inscription a bien été enregistrée !

Le tirage au sort pour cette revente ({{spectacle}}) sera effectué le {{date}}. {% endblock %} diff --git a/bda/templates/bda-wrongtime.html b/bda/templates/bda-wrongtime.html index 5e17926b..dfafb05f 100644 --- a/bda/templates/bda-wrongtime.html +++ b/bda/templates/bda-wrongtime.html @@ -1,6 +1,13 @@ {% extends "base_title.html" %} {% block realcontent %} -

Nope

-

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

+

Nope

+ {% if revente.shotgun %} +

Le tirage au sort de cette revente a déjà été effectué !

+ +

Si personne n'était intéressé, elle est maintenant disponible + ici.

+ {% else %} +

Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !

+ {% endif %} {% endblock %} diff --git a/bda/templates/bda/mails/revente-new.txt b/bda/templates/bda/mails/revente-new.txt index ffba3083..b4815e84 100644 --- a/bda/templates/bda/mails/revente-new.txt +++ b/bda/templates/bda/mails/revente-new.txt @@ -2,7 +2,7 @@ Bonjour {{ vendeur.first_name }}, Tu t’es 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 {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}). Si personne ne s’est inscrit pour racheter la place, celle-ci apparaitra parmi diff --git a/bda/templates/bda/mails/revente.txt b/bda/templates/bda/mails/revente.txt index 397a58d8..7efc02e5 100644 --- a/bda/templates/bda/mails/revente.txt +++ b/bda/templates/bda/mails/revente.txt @@ -3,10 +3,12 @@ Bonjour {{ user.first_name }} Une place pour le spectacle {{ spectacle.title }} ({{ spectacle.date }}) 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 sur ce lien : http://{{ domain }}{% url "bda-revente-interested" revente.id %}. 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, Le BdA diff --git a/bda/templates/liste-reventes.html b/bda/templates/liste-reventes.html index d08d4010..c0bf8ff0 100644 --- a/bda/templates/liste-reventes.html +++ b/bda/templates/liste-reventes.html @@ -8,7 +8,10 @@ {% endif %} {% if deja_revente %}

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

+ {% elif inscrit_revente %} +

Tu as été inscrit à une revente en cours pour ce spectacle !

{% endif %} +
{% csrf_token %}
diff --git a/bda/views.py b/bda/views.py index 72fd5dd2..99168787 100644 --- a/bda/views.py +++ b/bda/views.py @@ -271,6 +271,7 @@ def revente(request, tirage_id): if not participant.paid: return render(request, "bda-notpaid.html", {}) if request.method == 'POST': + # On met en vente une place if 'resell' in request.POST: resellform = ResellForm(participant, request.POST, prefix='resell') annulform = AnnulForm(participant, prefix='annul') @@ -279,27 +280,36 @@ def revente(request, tirage_id): attributions = resellform.cleaned_data["attributions"] with transaction.atomic(): for attribution in attributions: - revente, created = SpectacleRevente.objects.get_or_create( - attribution=attribution, - defaults={'seller': participant}) + revente, created = \ + SpectacleRevente.objects.get_or_create( + attribution=attribution, + defaults={'seller': participant}) if not created: revente.seller = participant revente.date = timezone.now() - 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, - 'revente': revente, - }) + revente.soldTo = None + revente.notif_sent = False + 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, + 'revente': revente} + ) mails.append(mail.EmailMessage( mail_subject, mail_body, from_email=settings.MAIL_DATA['revente']['FROM'], to=[participant.user.email], - reply_to=[settings.MAIL_DATA['revente']['REPLYTO']], + reply_to=[ + settings.MAIL_DATA['revente']['REPLYTO'] + ], )) revente.save() mail.get_connection().send_messages(mails) - + # On annule une revente elif 'annul' in request.POST: annulform = AnnulForm(participant, request.POST, prefix='annul') resellform = ResellForm(participant, prefix='resell') @@ -307,7 +317,8 @@ def revente(request, tirage_id): attributions = annulform.cleaned_data["attributions"] for attribution in attributions: attribution.revente.delete() - + # On confirme une vente en transférant la place à la personne qui a + # gagné le tirage elif 'transfer' in request.POST: resellform = ResellForm(participant, prefix='resell') annulform = AnnulForm(participant, prefix='annul') @@ -320,7 +331,9 @@ def revente(request, tirage_id): attrib = revente.attribution attrib.participant = revente.soldTo 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: resellform = ResellForm(participant, prefix='resell') annulform = AnnulForm(participant, prefix='annul') @@ -334,6 +347,7 @@ def revente(request, tirage_id): revente.soldTo = None revente.notif_sent = False revente.tirage_done = False + revente.shotgun = False if revente.answered_mail: revente.answered_mail.clear() revente.save() @@ -354,8 +368,7 @@ def revente(request, tirage_id): sold = participant.attribution_set.filter( spectacle__date__gte=timezone.now(), revente__isnull=False, - revente__soldTo__isnull=False).exclude( - revente__soldTo=participant) + revente__soldTo__isnull=False) return render(request, "bda-revente.html", {'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) 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", {}) + if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun: + return render(request, "bda-wrongtime.html", + {"revente": revente}) revente.answered_mail.add(participant) return render(request, "bda-interested.html", {"spectacle": revente.attribution.spectacle, - "date": revente.expiration_time}) + "date": revente.date_tirage}) @login_required @@ -381,23 +395,9 @@ 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 success = 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) - + inscrit_revente = False if request.method == 'POST': form = InscriptionReventeForm(tirage, request.POST) if form.is_valid(): @@ -407,15 +407,24 @@ def list_revente(request, tirage_id): 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) - revente.save() - break + if qset.filter(shotgun=True, soldTo__isnull=True).exists(): + # Une place est disponible au shotgun, on suggère à + # l'utilisateur d'aller la récupérer + deja_revente = True + else: + # La place n'est pas disponible au shotgun, si des reventes + # pour ce spectacle existent déjà, on inscrit la personne à + # 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 else: form = InscriptionReventeForm( @@ -423,8 +432,9 @@ def list_revente(request, tirage_id): initial={'spectacles': participant.choicesrevente.all()}) return render(request, "liste-reventes.html", - {"form": form, 'shotgun': shotgun, - "deja_revente": deja_revente, "success": success}) + {"form": form, + "deja_revente": deja_revente, "success": success, + "inscrit_revente": inscrit_revente}) @login_required @@ -436,15 +446,16 @@ def buy_revente(request, spectacle_id): reventes = SpectacleRevente.objects.filter( attribution__spectacle=spectacle, 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(): revente = reventes.filter(seller=participant)[0] revente.delete() return HttpResponseRedirect(reverse("bda-shotgun", args=[tirage.id])) - reventes_shotgun = [] - for revente in reventes.all(): - if revente.shotgun: - reventes_shotgun.append(revente) + + reventes_shotgun = list(reventes.filter(shotgun=True).all()) if not reventes_shotgun: return render(request, "bda-no-revente.html", {}) @@ -478,14 +489,11 @@ def revente_shotgun(request, tirage_id): date__gte=timezone.now()) shotgun = [] for spectacle in spectacles: - revente_objects = SpectacleRevente.objects.filter( + reventes = SpectacleRevente.objects.filter( attribution__spectacle=spectacle, + shotgun=True, soldTo__isnull=True) - revente_count = 0 - for revente in revente_objects: - if revente.shotgun: - revente_count += 1 - if revente_count: + if reventes.exists(): shotgun.append(spectacle) return render(request, "bda-shotgun.html",