From 732e47707e96bb7047438edb1791511e89918fa1 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 17:25:58 +0200 Subject: [PATCH 01/15] Add unsubscribe option + list of current draws --- bda/forms.py | 41 +++++++++++++++++++-- bda/templates/bda/revente-tirages.html | 28 +++++++++++++++ bda/urls.py | 11 +++--- bda/views.py | 50 +++++++++++++++++++++++++- gestioncof/templates/home.html | 7 ++-- 5 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 bda/templates/bda/revente-tirages.html diff --git a/bda/forms.py b/bda/forms.py index c0417d1e..139ef45d 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -4,7 +4,7 @@ from django import forms from django.forms.models import BaseInlineFormSet from django.utils import timezone -from bda.models import Attribution, Spectacle +from bda.models import Attribution, Spectacle, SpectacleRevente class InscriptionInlineFormSet(BaseInlineFormSet): @@ -45,6 +45,9 @@ class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): return "%s" % str(obj.spectacle) +class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField): + def label_from_instance(self, obj): + return "%s" % str(obj.attribution.spectacle) class ResellForm(forms.Form): attributions = AttributionModelMultipleChoiceField( @@ -63,7 +66,6 @@ class ResellForm(forms.Form): 'participant__user') ) - class AnnulForm(forms.Form): attributions = AttributionModelMultipleChoiceField( label='', @@ -83,7 +85,6 @@ class AnnulForm(forms.Form): 'participant__user') ) - class InscriptionReventeForm(forms.Form): spectacles = forms.ModelMultipleChoiceField( queryset=Spectacle.objects.none(), @@ -98,6 +99,40 @@ class InscriptionReventeForm(forms.Form): .filter(date__gte=timezone.now()) ) +class ReventeTirageAnnulForm(forms.Form): + reventes = ReventeModelMultipleChoiceField( + label='', + queryset=SpectacleRevente.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False + ) + + def __init__(self, participant, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['reventes'].queryset = ( + participant.wanted.filter(soldTo__isnull=True) + .select_related('attribution__spectacle') + ) + + +class ReventeTirageForm(forms.Form): + reventes = ReventeModelMultipleChoiceField( + label='', + queryset=SpectacleRevente.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False + ) + + def __init__(self, participant, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['reventes'].queryset = ( + SpectacleRevente.objects.filter( + notif_sent=True, + shotgun=False, + tirage_done=False + ).exclude(answered_mail=participant) + .select_related('attribution__spectacle') + ) class SoldForm(forms.Form): attributions = AttributionModelMultipleChoiceField( diff --git a/bda/templates/bda/revente-tirages.html b/bda/templates/bda/revente-tirages.html new file mode 100644 index 00000000..bd738673 --- /dev/null +++ b/bda/templates/bda/revente-tirages.html @@ -0,0 +1,28 @@ +{% extends "base_title.html" %} +{% load bootstrap %} + +{% block realcontent %} + +

Tirages au sort de reventes

+{% if annulform.reventes %} +

Mes inscriptions

+
+ {% csrf_token %} + {{annulform|bootstrap}} +
+ +
+
+{% endif %} +
+{% if subform.reventes %} +

Tirages en cours

+
+ {% csrf_token %} + {{subform|bootstrap}} +
+ +
+
+{% endif %} +{% endblock %} diff --git a/bda/urls.py b/bda/urls.py index 876c84ea..51dd4235 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -32,16 +32,19 @@ urlpatterns = [ url(r'^spectacles/unpaid/(?P\d+)$', views.unpaid, name="bda-unpaid"), - url(r'^liste-revente/(?P\d+)$', + url(r'^revente/(?P\d+)/list$', views.list_revente, name="bda-liste-revente"), - url(r'^buy-revente/(?P\d+)$', + url(r'^revente/(?P\d+)/tirages$', + views.revente_tirages, + name="bda-revente-tirages"), + url(r'^revente/(?P\d+)/buy$', views.buy_revente, name="bda-buy-revente"), - url(r'^revente-interested/(?P\d+)$', + url(r'^revente/(?P\d+)/interested$', views.revente_interested, name='bda-revente-interested'), - url(r'^revente-immediat/(?P\d+)$', + url(r'^revente/(?P\d+)/immediat$', views.revente_shotgun, name="bda-shotgun"), url(r'^mails-rappel/(?P\d+)$', diff --git a/bda/views.py b/bda/views.py index 84b6c9d3..4b75c116 100644 --- a/bda/views.py +++ b/bda/views.py @@ -30,7 +30,7 @@ from bda.models import ( from bda.algorithm import Algorithm from bda.forms import ( TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm, - InscriptionInlineFormSet, + InscriptionInlineFormSet, ReventeTirageForm, ReventeTirageAnnulForm ) @@ -377,6 +377,7 @@ def revente(request, tirage_id): if not created: revente.seller = participant revente.date = timezone.now() + revente.wanted = Participant.objects.none() revente.soldTo = None revente.notif_sent = False revente.tirage_done = False @@ -442,6 +443,53 @@ def revente(request, tirage_id): "annulform": annulform, "resellform": resellform}) +@login_required +def revente_tirages(request, tirage_id): + tirage = get_object_or_404(Tirage, id=tirage_id) + participant, _ = Participant.objects.get_or_create( + user=request.user, tirage=tirage) + unsub = 0 + subform = ReventeTirageForm(participant, prefix="subscribe") + annulform = ReventeTirageAnnulForm(participant, prefix="annul") + + if request.method == 'POST': + if "subscribe" in request.POST: + subform = ReventeTirageForm(participant, request.POST, + prefix="subscribe") + if subform.is_valid(): + sub = 0 + reventes = subform.cleaned_data['reventes'] + for revente in reventes: + revente.answered_mail.add(participant) + sub += 1 + if sub > 0: + plural = "s" if sub > 1 else "" + messages.success( + request, + "Tu as bien été inscrit à {} revente{}" + .format(sub, plural) + ) + elif "annul" in request.POST: + annulform = ReventeTirageAnnulForm(participant, request.POST, + prefix="annul") + if annulform.is_valid(): + unsub = 0 + reventes = annulform.cleaned_data['reventes'] + for revente in reventes: + revente.answered_mail.remove(participant) + unsub += 1 + if unsub > 0: + plural = "s" if unsub > 1 else "" + messages.success( + request, + "Tu as bien été désinscrit de {} revente{}" + .format(unsub, plural) + ) + + return render(request, "bda/revente-tirages.html", + {"annulform": annulform, "subform": subform}) + + @login_required def revente_interested(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index acc04f30..f7ca57b5 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -43,9 +43,10 @@
  • État des demandes
  • {% else %}
  • Mes places
  • -
  • Revendre une place
  • -
  • S'inscrire à BdA-Revente
  • -
  • Places disponibles immédiatement
  • +
  • Gestion de mes reventes
  • +
  • Reventes en cours
  • +
  • S'inscrire à BdA-Revente
  • +
  • Places disponibles immédiatement
  • {% endif %} {% endfor %} From e74dbb11f1556a4ffb3cc42f83686a3a27cf5f45 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 18:39:45 +0200 Subject: [PATCH 02/15] Organize revente files and function names --- .../revente/confirm-shotgun.html} | 0 .../revente/confirmed.html} | 0 .../revente/mail-success.html} | 0 .../{reventes.html => revente/manage.html} | 0 .../revente/none.html} | 0 .../revente/notpaid.html} | 0 .../revente/shotgun.html} | 2 +- .../subscribe.html} | 0 .../tirages.html} | 0 .../revente/wrongtime.html} | 2 +- bda/urls.py | 30 +++++++++++-------- bda/views.py | 30 +++++++++---------- gestioncof/management/data/custommail.json | 2 +- gestioncof/templates/home.html | 6 ++-- 14 files changed, 38 insertions(+), 34 deletions(-) rename bda/templates/{revente-confirm.html => bda/revente/confirm-shotgun.html} (100%) rename bda/templates/{bda-interested.html => bda/revente/confirmed.html} (100%) rename bda/templates/{bda-success.html => bda/revente/mail-success.html} (100%) rename bda/templates/bda/{reventes.html => revente/manage.html} (100%) rename bda/templates/{bda-no-revente.html => bda/revente/none.html} (100%) rename bda/templates/{bda-notpaid.html => bda/revente/notpaid.html} (100%) rename bda/templates/{bda-shotgun.html => bda/revente/shotgun.html} (83%) rename bda/templates/bda/{liste-reventes.html => revente/subscribe.html} (100%) rename bda/templates/bda/{revente-tirages.html => revente/tirages.html} (100%) rename bda/templates/{bda-wrongtime.html => bda/revente/wrongtime.html} (86%) diff --git a/bda/templates/revente-confirm.html b/bda/templates/bda/revente/confirm-shotgun.html similarity index 100% rename from bda/templates/revente-confirm.html rename to bda/templates/bda/revente/confirm-shotgun.html diff --git a/bda/templates/bda-interested.html b/bda/templates/bda/revente/confirmed.html similarity index 100% rename from bda/templates/bda-interested.html rename to bda/templates/bda/revente/confirmed.html diff --git a/bda/templates/bda-success.html b/bda/templates/bda/revente/mail-success.html similarity index 100% rename from bda/templates/bda-success.html rename to bda/templates/bda/revente/mail-success.html diff --git a/bda/templates/bda/reventes.html b/bda/templates/bda/revente/manage.html similarity index 100% rename from bda/templates/bda/reventes.html rename to bda/templates/bda/revente/manage.html diff --git a/bda/templates/bda-no-revente.html b/bda/templates/bda/revente/none.html similarity index 100% rename from bda/templates/bda-no-revente.html rename to bda/templates/bda/revente/none.html diff --git a/bda/templates/bda-notpaid.html b/bda/templates/bda/revente/notpaid.html similarity index 100% rename from bda/templates/bda-notpaid.html rename to bda/templates/bda/revente/notpaid.html diff --git a/bda/templates/bda-shotgun.html b/bda/templates/bda/revente/shotgun.html similarity index 83% rename from bda/templates/bda-shotgun.html rename to bda/templates/bda/revente/shotgun.html index e10fae00..fae36c04 100644 --- a/bda/templates/bda-shotgun.html +++ b/bda/templates/bda/revente/shotgun.html @@ -5,7 +5,7 @@ {% if shotgun %}
      {% for spectacle in shotgun %} -
    • {{spectacle}}
    • +
    • {{spectacle}}
    • {% endfor %} {% else %}

      Pas de places disponibles immédiatement, désolé !

      diff --git a/bda/templates/bda/liste-reventes.html b/bda/templates/bda/revente/subscribe.html similarity index 100% rename from bda/templates/bda/liste-reventes.html rename to bda/templates/bda/revente/subscribe.html diff --git a/bda/templates/bda/revente-tirages.html b/bda/templates/bda/revente/tirages.html similarity index 100% rename from bda/templates/bda/revente-tirages.html rename to bda/templates/bda/revente/tirages.html diff --git a/bda/templates/bda-wrongtime.html b/bda/templates/bda/revente/wrongtime.html similarity index 86% rename from bda/templates/bda-wrongtime.html rename to bda/templates/bda/revente/wrongtime.html index dfafb05f..18c417a2 100644 --- a/bda/templates/bda-wrongtime.html +++ b/bda/templates/bda/revente/wrongtime.html @@ -6,7 +6,7 @@

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

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

      + ici.

      {% else %}

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

      {% endif %} diff --git a/bda/urls.py b/bda/urls.py index 51dd4235..7588187c 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -16,9 +16,6 @@ urlpatterns = [ url(r'^places/(?P\d+)$', views.places, name="bda-places-attribuees"), - url(r'^revente/(?P\d+)$', - views.revente, - name='bda-revente'), url(r'^etat-places/(?P\d+)$', views.etat_places, name='bda-etat-places'), @@ -32,21 +29,28 @@ urlpatterns = [ url(r'^spectacles/unpaid/(?P\d+)$', views.unpaid, name="bda-unpaid"), - url(r'^revente/(?P\d+)/list$', - views.list_revente, - name="bda-liste-revente"), + + # Urls BdA-Revente + + url(r'^revente/(?P\d+)/manage$', + views.revente_manage, + name='bda-revente-manage'), + url(r'^revente/(?P\d+)/subscribe$', + views.revente_subscribe, + name="bda-revente-subscribe"), url(r'^revente/(?P\d+)/tirages$', views.revente_tirages, name="bda-revente-tirages"), url(r'^revente/(?P\d+)/buy$', - views.buy_revente, - name="bda-buy-revente"), - url(r'^revente/(?P\d+)/interested$', - views.revente_interested, - name='bda-revente-interested'), - url(r'^revente/(?P\d+)/immediat$', + views.revente_buy, + name="bda-revente-buy"), + url(r'^revente/(?P\d+)/confirm$', + views.revente_confirm, + name='bda-revente-confirm'), + url(r'^revente/(?P\d+)/shotgun$', views.revente_shotgun, - name="bda-shotgun"), + name="bda-revente-shotgun"), + url(r'^mails-rappel/(?P\d+)$', views.send_rappel, name="bda-rappels" diff --git a/bda/views.py b/bda/views.py index 4b75c116..c0e64230 100644 --- a/bda/views.py +++ b/bda/views.py @@ -349,13 +349,13 @@ def tirage(request, tirage_id): @login_required -def revente(request, tirage_id): +def revente_manage(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, created = Participant.objects.get_or_create( user=request.user, tirage=tirage) if not participant.paid: - return render(request, "bda-notpaid.html", {}) + return render(request, "bda/revente/notpaid.html", {}) resellform = ResellForm(participant, prefix='resell') annulform = AnnulForm(participant, prefix='annul') @@ -438,7 +438,7 @@ def revente(request, tirage_id): .filter( Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant)) - return render(request, "bda/reventes.html", + return render(request, "bda/revente/manage.html", {'tirage': tirage, 'overdue': overdue, "soldform": soldform, "annulform": annulform, "resellform": resellform}) @@ -486,27 +486,27 @@ def revente_tirages(request, tirage_id): .format(unsub, plural) ) - return render(request, "bda/revente-tirages.html", + return render(request, "bda/revente/tirages.html", {"annulform": annulform, "subform": subform}) @login_required -def revente_interested(request, revente_id): +def revente_confirm(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) participant, _ = 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", + return render(request, "bda/revente/wrongtime.html", {"revente": revente}) revente.answered_mail.add(participant) - return render(request, "bda-interested.html", + return render(request, "bda/revente/confirmed.html", {"spectacle": revente.attribution.spectacle, "date": revente.date_tirage}) @login_required -def list_revente(request, tirage_id): +def revente_subscribe(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=tirage) @@ -560,11 +560,11 @@ def list_revente(request, tirage_id): ) messages.info(request, msg, extra_tags="safe") - return render(request, "bda/liste-reventes.html", {"form": form}) + return render(request, "bda/revente/subscribe.html", {"form": form}) @login_required -def buy_revente(request, spectacle_id): +def revente_buy(request, spectacle_id): spectacle = get_object_or_404(Spectacle, id=spectacle_id) tirage = spectacle.tirage participant, _ = Participant.objects.get_or_create( @@ -578,13 +578,13 @@ def buy_revente(request, spectacle_id): own_reventes = reventes.filter(seller=participant) if len(own_reventes) > 0: own_reventes[0].delete() - return HttpResponseRedirect(reverse("bda-shotgun", + return HttpResponseRedirect(reverse("bda-revente-shotgun", args=[tirage.id])) reventes_shotgun = reventes.filter(shotgun=True) if not reventes_shotgun: - return render(request, "bda-no-revente.html", {}) + return render(request, "bda/revente/none.html", {}) if request.POST: revente = random.choice(reventes_shotgun) @@ -601,11 +601,11 @@ def buy_revente(request, spectacle_id): [revente.seller.user.email], context=context, ) - return render(request, "bda-success.html", + return render(request, "bda/revente/mail-success.html", {"seller": revente.attribution.participant.user, "spectacle": spectacle}) - return render(request, "revente-confirm.html", + return render(request, "bda/revente/confirm-shotgun.html", {"spectacle": spectacle, "user": request.user}) @@ -629,7 +629,7 @@ def revente_shotgun(request, tirage_id): ) shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0] - return render(request, "bda-shotgun.html", + return render(request, "bda/revente/shotgun.html", {"shotgun": shotgun}) diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json index 9ee9b1ea..bf59e5f6 100644 --- a/gestioncof/management/data/custommail.json +++ b/gestioncof/management/data/custommail.json @@ -151,7 +151,7 @@ "shortname": "bda-revente", "subject": "{{ show }}", "description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente.", - "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA" + "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-confirm\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA" } }, { diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index f7ca57b5..943ef780 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -43,10 +43,10 @@
    • État des demandes
    • {% else %}
    • Mes places
    • -
    • Gestion de mes reventes
    • +
    • Gestion de mes reventes
    • Reventes en cours
    • -
    • S'inscrire à BdA-Revente
    • -
    • Places disponibles immédiatement
    • +
    • S'inscrire à BdA-Revente
    • +
    • Places disponibles immédiatement
    • {% endif %}
    {% endfor %} From 919bcd197d077767bdc07355a70a84d39f8ebecf Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 18:59:30 +0200 Subject: [PATCH 03/15] Small code QoL improvements --- bda/models.py | 10 ++++++++++ bda/views.py | 10 ++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/bda/models.py b/bda/models.py index 41462d70..5533e3bb 100644 --- a/bda/models.py +++ b/bda/models.py @@ -252,6 +252,16 @@ class SpectacleRevente(models.Model): class Meta: verbose_name = "Revente" + def reset(self): + """Réinitialise la revente pour permettre une remise sur le marché""" + self.seller = self.attribution.participant + self.date = timezone.now() + self.answered_mail.clear() + self.soldTo = None + self.notif_sent = False + self.tirage_done = False + self.shotgun = False + def send_notif(self): """ Envoie une notification pour indiquer la mise en vente d'une place sur diff --git a/bda/views.py b/bda/views.py index c0e64230..311d530a 100644 --- a/bda/views.py +++ b/bda/views.py @@ -375,13 +375,7 @@ def revente_manage(request, tirage_id): attribution=attribution, defaults={'seller': participant}) if not created: - revente.seller = participant - revente.date = timezone.now() - revente.wanted = Participant.objects.none() - revente.soldTo = None - revente.notif_sent = False - revente.tirage_done = False - revente.shotgun = False + revente.reset() context = { 'vendeur': participant.user, 'show': attribution.spectacle, @@ -495,7 +489,7 @@ def revente_confirm(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=revente.attribution.spectacle.tirage) - if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun: + if not revente.notif_sent or revente.shotgun: return render(request, "bda/revente/wrongtime.html", {"revente": revente}) From 1b0e4285ecbc7ea224cb7fad9c725365d4a9ba01 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 20:26:07 +0200 Subject: [PATCH 04/15] Reverse match fix --- gestioncof/management/data/custommail.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json index bf59e5f6..029c03e0 100644 --- a/gestioncof/management/data/custommail.json +++ b/gestioncof/management/data/custommail.json @@ -161,7 +161,7 @@ "shortname": "bda-shotgun", "subject": "{{ show }}", "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.", - "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA" + "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-revente-buy\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA" } }, { From 684603709e90e471f5528ab35f1efc2011145fb5 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 20:30:11 +0200 Subject: [PATCH 05/15] Class attributes and properties + more verbose log SpectacleRevente gets brand new properties and attributes to simplify code ; also, manage_reventes command output is more verbose --- bda/management/commands/manage_reventes.py | 48 ++++++++++++++-------- bda/models.py | 45 +++++++++++++++++--- 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index 0302ec4b..5a767604 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -6,7 +6,6 @@ Gestion en ligne de commande des reventes. from __future__ import unicode_literals -from datetime import timedelta from django.core.management import BaseCommand from django.utils import timezone from bda.models import SpectacleRevente @@ -21,23 +20,36 @@ class Command(BaseCommand): now = timezone.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: - self.stdout.write(str(now)) - 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): + # Le spectacle est bientôt et on a pas encore envoyé de mail : + # on met la place au shotgun et on prévient. + if revente.is_urgent and not revente.notif_sent: + if revente.can_notif: + self.stdout.write(str(now)) + revente.mail_shotgun() + self.stdout.write( + "Mails de disponibilité immédiate envoyés " + "pour la revente [%s]" % revente + ) + + # Le spectacle est dans plus longtemps : on prévient + elif (revente.can_notif and not revente.notif_sent): self.stdout.write(str(now)) revente.send_notif() - self.stdout.write("Mail d'inscription à une revente envoyé") - # Check si tirage à faire - elif (now >= revente.date_tirage and - not revente.tirage_done): + self.stdout.write( + "Mails d'inscription à la revente [%s] envoyés" + % revente + ) + + # On fait le tirage + elif (now >= revente.date_tirage and not revente.tirage_done): self.stdout.write(str(now)) - revente.tirage() - self.stdout.write("Tirage effectué, mails envoyés") + winner = revente.tirage() + self.stdout.write( + "Tirage effectué pour la revente [%s]" + % revente + ) + + if winner: + self.stdout.write("Gagnant : %s" % winner.user) + else: + self.stdout.write("Pas de gagnant ; place au shotgun") diff --git a/bda/models.py b/bda/models.py index 5533e3bb..b2882900 100644 --- a/bda/models.py +++ b/bda/models.py @@ -233,17 +233,46 @@ class SpectacleRevente(models.Model): default=False) shotgun = models.BooleanField("Disponible immédiatement", default=False) + #### + # Some class attributes + ### + # TODO : settings ? + + # Temps minimum entre le tirage et le spectacle + min_margin = timedelta(days=5) + + # Temps entre la création d'une revente et l'envoi du mail + remorse_time = timedelta(hours=1) + + # Temps min/max d'attente avant le tirage + max_wait_time = timedelta(days=3) + min_wait_time = timedelta(days=1) @property 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 + notif_time = self.date + self.remorse_time + 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)) - # Le vendeur a aussi 1h pour changer d'avis - return self.date + delay + timedelta(hours=1) + - notif_time - self.min_margin) + + delay = min(remaining_time, self.max_wait_time) + + return notif_time + delay + + @property + def is_urgent(self): + """ + Renvoie True iff la revente doit être mise au shotgun directement. + Plus précisément, on doit avoir min_margin + min_wait_time de marge. + """ + spectacle_date = self.attribution.spectacle.date + return (spectacle_date <= timezone.now() + self.min_margin + + self.min_wait_time) + + @property + def can_notif(self): + return (timezone.now() >= self.date + self.remorse_time) def __str__(self): return "%s -- %s" % (self.seller, @@ -353,8 +382,12 @@ class SpectacleRevente(models.Model): [inscrit.user.email] )) send_mass_custom_mail(datatuple) + + return winner + # Si personne ne veut de la place, elle part au shotgun else: self.shotgun = True + return None self.tirage_done = True self.save() From 6a6549e0d72937d9f5adaf7bf43ff090150b4891 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 20:52:25 +0200 Subject: [PATCH 06/15] Add notif time In case of a gestioCOF bug, we keep the notification time in memory to still do the drawing 1-3 days after. --- bda/admin.py | 6 +++--- bda/forms.py | 4 ++-- bda/migrations/0012_notif_time.py | 28 ++++++++++++++++++++++++++++ bda/models.py | 29 +++++++++++++++++++++-------- bda/views.py | 14 +++++++------- 5 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 bda/migrations/0012_notif_time.py diff --git a/bda/admin.py b/bda/admin.py index 60d3c1ba..4f5d821a 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -225,7 +225,7 @@ class SpectacleReventeAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['answered_mail'].queryset = ( + self.fields['confirmed_entry'].queryset = ( Participant.objects .select_related('user', 'tirage') ) @@ -292,8 +292,8 @@ class SpectacleReventeAdmin(admin.ModelAdmin): revente.soldTo = None revente.notif_sent = False revente.tirage_done = False - if revente.answered_mail: - revente.answered_mail.clear() + if revente.confirmed_entry: + revente.confirmed_entry.clear() revente.save() self.message_user( request, diff --git a/bda/forms.py b/bda/forms.py index 139ef45d..11d05b0e 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -110,7 +110,7 @@ class ReventeTirageAnnulForm(forms.Form): def __init__(self, participant, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['reventes'].queryset = ( - participant.wanted.filter(soldTo__isnull=True) + participant.entered.filter(soldTo__isnull=True) .select_related('attribution__spectacle') ) @@ -130,7 +130,7 @@ class ReventeTirageForm(forms.Form): notif_sent=True, shotgun=False, tirage_done=False - ).exclude(answered_mail=participant) + ).exclude(confirmed_entry=participant) .select_related('attribution__spectacle') ) diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py new file mode 100644 index 00000000..be66efd1 --- /dev/null +++ b/bda/migrations/0012_notif_time.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0011_tirage_appear_catalogue'), + ] + + operations = [ + migrations.RemoveField( + model_name='spectaclerevente', + name='answered_mail', + ), + migrations.AddField( + model_name='spectaclerevente', + name='confirmed_entry', + field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'), + ), + migrations.AddField( + model_name='spectaclerevente', + name='notif_time', + field=models.DateTimeField(blank=True, verbose_name="Moment d'envoi de la notification", null=True), + ), + ] diff --git a/bda/models.py b/bda/models.py index b2882900..2ad47dbf 100644 --- a/bda/models.py +++ b/bda/models.py @@ -218,9 +218,9 @@ class SpectacleRevente(models.Model): related_name="revente") date = models.DateTimeField("Date de mise en vente", default=timezone.now) - answered_mail = models.ManyToManyField(Participant, - related_name="wanted", - blank=True) + confirmed_entry = models.ManyToManyField(Participant, + related_name="entered", + blank=True) seller = models.ForeignKey(Participant, related_name="original_shows", verbose_name="Vendeur") @@ -229,8 +229,13 @@ class SpectacleRevente(models.Model): notif_sent = models.BooleanField("Notification envoyée", default=False) + + notif_time = models.DateTimeField("Moment d'envoi de la notification", + blank=True, null=True) + tirage_done = models.BooleanField("Tirage effectué", default=False) + shotgun = models.BooleanField("Disponible immédiatement", default=False) #### @@ -248,17 +253,23 @@ class SpectacleRevente(models.Model): max_wait_time = timedelta(days=3) min_wait_time = timedelta(days=1) + @property + def real_notif_time(self): + if self.notif_time: + return self.notif_time + else: + return self.date + self.remorse_time + @property def date_tirage(self): """Renvoie la date du tirage au sort de la revente.""" - notif_time = self.date + self.remorse_time remaining_time = (self.attribution.spectacle.date - - notif_time - self.min_margin) + - self.real_notif_time - self.min_margin) delay = min(remaining_time, self.max_wait_time) - return notif_time + delay + return self.real_notif_time + delay @property def is_urgent(self): @@ -285,7 +296,7 @@ class SpectacleRevente(models.Model): """Réinitialise la revente pour permettre une remise sur le marché""" self.seller = self.attribution.participant self.date = timezone.now() - self.answered_mail.clear() + self.confirmed_entry.clear() self.soldTo = None self.notif_sent = False self.tirage_done = False @@ -311,6 +322,7 @@ class SpectacleRevente(models.Model): ] send_mass_custom_mail(datatuple) self.notif_sent = True + self.notif_time = timezone.now() self.save() def mail_shotgun(self): @@ -332,6 +344,7 @@ class SpectacleRevente(models.Model): ] send_mass_custom_mail(datatuple) self.notif_sent = True + self.notif_time = timezone.now() # Flag inutile, sauf si l'horloge interne merde self.tirage_done = True self.shotgun = True @@ -343,7 +356,7 @@ class SpectacleRevente(models.Model): parmis les personnes intéressées par le spectacle. Les personnes sont ensuites prévenues par mail du résultat du tirage. """ - inscrits = list(self.answered_mail.all()) + inscrits = list(self.confirmed_entry.all()) spectacle = self.attribution.spectacle seller = self.seller diff --git a/bda/views.py b/bda/views.py index 311d530a..6ed22b21 100644 --- a/bda/views.py +++ b/bda/views.py @@ -420,8 +420,8 @@ def revente_manage(request, tirage_id): revente.notif_sent = False revente.tirage_done = False revente.shotgun = False - if revente.answered_mail: - revente.answered_mail.clear() + if revente.confirmed_entry: + revente.confirmed_entry.clear() revente.save() overdue = participant.attribution_set.filter( @@ -454,7 +454,7 @@ def revente_tirages(request, tirage_id): sub = 0 reventes = subform.cleaned_data['reventes'] for revente in reventes: - revente.answered_mail.add(participant) + revente.confirmed_entry.add(participant) sub += 1 if sub > 0: plural = "s" if sub > 1 else "" @@ -470,7 +470,7 @@ def revente_tirages(request, tirage_id): unsub = 0 reventes = annulform.cleaned_data['reventes'] for revente in reventes: - revente.answered_mail.remove(participant) + revente.confirmed_entry.remove(participant) unsub += 1 if unsub > 0: plural = "s" if unsub > 1 else "" @@ -493,7 +493,7 @@ def revente_confirm(request, revente_id): return render(request, "bda/revente/wrongtime.html", {"revente": revente}) - revente.answered_mail.add(participant) + revente.confirmed_entry.add(participant) return render(request, "bda/revente/confirmed.html", {"spectacle": revente.attribution.spectacle, "date": revente.date_tirage}) @@ -526,12 +526,12 @@ def revente_subscribe(request, tirage_id): # la revente ayant le moins d'inscrits min_resell = ( qset.filter(shotgun=False) - .annotate(nb_subscribers=Count('answered_mail')) + .annotate(nb_subscribers=Count('confirmed_entry')) .order_by('nb_subscribers') .first() ) if min_resell is not None: - min_resell.answered_mail.add(participant) + min_resell.confirmed_entry.add(participant) inscrit_revente.append(spectacle) success = True else: From 785555c05cc874dfc2a9542608c0e94baffccc2e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 26 Oct 2017 12:40:11 +0200 Subject: [PATCH 07/15] Misc fixes --- bda/forms.py | 10 ++++++++-- bda/migrations/0012_notif_time.py | 7 ++++--- bda/models.py | 11 ++++++----- bda/views.py | 33 +++++++++++-------------------- 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/bda/forms.py b/bda/forms.py index 11d05b0e..2929f771 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -43,11 +43,13 @@ class TokenForm(forms.Form): class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): - return "%s" % str(obj.spectacle) + return str(obj.spectacle) + class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): - return "%s" % str(obj.attribution.spectacle) + return str(obj.attribution.spectacle) + class ResellForm(forms.Form): attributions = AttributionModelMultipleChoiceField( @@ -66,6 +68,7 @@ class ResellForm(forms.Form): 'participant__user') ) + class AnnulForm(forms.Form): attributions = AttributionModelMultipleChoiceField( label='', @@ -85,6 +88,7 @@ class AnnulForm(forms.Form): 'participant__user') ) + class InscriptionReventeForm(forms.Form): spectacles = forms.ModelMultipleChoiceField( queryset=Spectacle.objects.none(), @@ -99,6 +103,7 @@ class InscriptionReventeForm(forms.Form): .filter(date__gte=timezone.now()) ) + class ReventeTirageAnnulForm(forms.Form): reventes = ReventeModelMultipleChoiceField( label='', @@ -134,6 +139,7 @@ class ReventeTirageForm(forms.Form): .select_related('attribution__spectacle') ) + class SoldForm(forms.Form): attributions = AttributionModelMultipleChoiceField( label='', diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py index be66efd1..ee777e35 100644 --- a/bda/migrations/0012_notif_time.py +++ b/bda/migrations/0012_notif_time.py @@ -11,11 +11,12 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( + migrations.RenameField( model_name='spectaclerevente', - name='answered_mail', + old_name='answered_mail', + new_name='confirmed_entry', ), - migrations.AddField( + migrations.AlterField( model_name='spectaclerevente', name='confirmed_entry', field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'), diff --git a/bda/models.py b/bda/models.py index 2ad47dbf..59827621 100644 --- a/bda/models.py +++ b/bda/models.py @@ -168,6 +168,7 @@ class Participant(models.Model): def __str__(self): return "%s - %s" % (self.user, self.tirage.title) + DOUBLE_CHOICES = ( ("1", "1 place"), ("autoquit", "2 places si possible, 1 sinon"), @@ -292,15 +293,16 @@ class SpectacleRevente(models.Model): class Meta: verbose_name = "Revente" - def reset(self): + def reset(self, new_date=timezone.now()): """Réinitialise la revente pour permettre une remise sur le marché""" self.seller = self.attribution.participant - self.date = timezone.now() + self.date = new_date self.confirmed_entry.clear() self.soldTo = None self.notif_sent = False self.tirage_done = False self.shotgun = False + self.save() def send_notif(self): """ @@ -396,11 +398,10 @@ class SpectacleRevente(models.Model): )) send_mass_custom_mail(datatuple) - return winner - # Si personne ne veut de la place, elle part au shotgun else: + winner = None self.shotgun = True - return None self.tirage_done = True self.save() + return winner diff --git a/bda/views.py b/bda/views.py index 6ed22b21..fb1a2e82 100644 --- a/bda/views.py +++ b/bda/views.py @@ -5,7 +5,6 @@ import random import hashlib import time import json -from datetime import timedelta from custommail.shortcuts import send_mass_custom_mail, send_custom_mail from custommail.models import CustomMail from django.shortcuts import render, get_object_or_404 @@ -14,6 +13,7 @@ from django.contrib import messages from django.db import transaction from django.core import serializers from django.db.models import Count, Q, Prefetch +from django.template.defaultfilters import pluralize from django.forms.models import inlineformset_factory from django.http import ( HttpResponseBadRequest, HttpResponseRedirect, JsonResponse @@ -376,6 +376,7 @@ def revente_manage(request, tirage_id): defaults={'seller': participant}) if not created: revente.reset() + context = { 'vendeur': participant.user, 'show': attribution.spectacle, @@ -414,15 +415,10 @@ def revente_manage(request, tirage_id): attributions = soldform.cleaned_data['attributions'] for attribution in attributions: if attribution.spectacle.date > timezone.now(): - revente = attribution.revente - revente.date = timezone.now() - timedelta(minutes=65) - revente.soldTo = None - revente.notif_sent = False - revente.tirage_done = False - revente.shotgun = False - if revente.confirmed_entry: - revente.confirmed_entry.clear() - revente.save() + # On antidate pour envoyer le mail plus vite + new_date = (timezone.now() + - SpectacleRevente.remorse_time) + revente.reset(new_date=new_date) overdue = participant.attribution_set.filter( spectacle__date__gte=timezone.now(), @@ -442,7 +438,6 @@ def revente_tirages(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=tirage) - unsub = 0 subform = ReventeTirageForm(participant, prefix="subscribe") annulform = ReventeTirageAnnulForm(participant, prefix="annul") @@ -451,33 +446,29 @@ def revente_tirages(request, tirage_id): subform = ReventeTirageForm(participant, request.POST, prefix="subscribe") if subform.is_valid(): - sub = 0 reventes = subform.cleaned_data['reventes'] + count = reventes.count() for revente in reventes: revente.confirmed_entry.add(participant) - sub += 1 - if sub > 0: - plural = "s" if sub > 1 else "" + if count > 0: messages.success( request, "Tu as bien été inscrit à {} revente{}" - .format(sub, plural) + .format(count, pluralize(count)) ) elif "annul" in request.POST: annulform = ReventeTirageAnnulForm(participant, request.POST, prefix="annul") if annulform.is_valid(): - unsub = 0 reventes = annulform.cleaned_data['reventes'] + count = reventes.count() for revente in reventes: revente.confirmed_entry.remove(participant) - unsub += 1 - if unsub > 0: - plural = "s" if unsub > 1 else "" + if count > 0: messages.success( request, "Tu as bien été désinscrit de {} revente{}" - .format(unsub, plural) + .format(count, pluralize(count)) ) return render(request, "bda/revente/tirages.html", From f18959c0a1d643fab3b08921a004f158d4ba4720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 1 Nov 2017 17:26:40 +0100 Subject: [PATCH 08/15] BdA-Revente: meaningful names, some help tests --- bda/templates/bda/revente/manage.html | 58 +++++++++++++++++++----- bda/templates/bda/revente/subscribe.html | 39 ++++++++++------ bda/templates/bda/revente/tirages.html | 40 ++++++++++++---- bda/views.py | 18 ++++++++ gestioncof/static/css/cof.css | 11 +++++ gestioncof/templates/home.html | 6 +-- 6 files changed, 136 insertions(+), 36 deletions(-) diff --git a/bda/templates/bda/revente/manage.html b/bda/templates/bda/revente/manage.html index 0912babb..8162d55d 100644 --- a/bda/templates/bda/revente/manage.html +++ b/bda/templates/bda/revente/manage.html @@ -3,50 +3,84 @@ {% block realcontent %} -

    Revente de place

    +

    Gestion des places que je revends

    {% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %} {% if resellform.attributions %} +
    +

    Places non revendues

    - {% csrf_token %} - {{resellform|bootstrap}} +
    + + Cochez les places que vous souhaitez revendre, et validez. Vous aurez + ensuite 1h pour changer d'avis avant que la revente soit confirmée et + que les notifications soient envoyées aux intéressé·e·s. +
    +
    + {% csrf_token %} + {{ resellform|bootstrap }} +
    + +
    {% endif %} -
    + {% if annul_attributions or overdue %}

    Places en cours de revente

    + {% if annul_attributions %} +
    + + Vous pouvez annuler les places mises en vente il y a moins d'une heure. +
    + {% endif %} {% csrf_token %}
      {% for attrib in annul_attributions %} -
    • {{attrib.tag}} {{attrib.choice_label}}
    • +
    • {{ attrib.tag }} {{ attrib.choice_label }}
    • {% endfor %} {% for attrib in overdue %}
    • - {{attrib.spectacle}} + {{ attrib.spectacle }}
    • {% endfor %} +
    +
    +
    {% if annul_attributions %} {% endif %}
    + +
    {% endif %} -
    + {% if sold_attributions %}

    Places revendues

    -
    + +
    + + Pour chaque revente, vous devez soit l'annuler soit la confirmer pour + transférer la place la place à la personne tirée au sort. + + L'annulation sert par exemple à pouvoir remettre la place en jeu si + vous ne parvenez pas à entrer en contact avec la personne tirée au + sort. +
    +
    {% csrf_token %} - {{soldform|bootstrap}} - - - + {{ soldform|bootstrap }} +
    + + + {% endif %} {% if not resell_attributions and not annul_attributions and not overdue and not sold_attributions %}

    Plus de reventes possibles !

    diff --git a/bda/templates/bda/revente/subscribe.html b/bda/templates/bda/revente/subscribe.html index fcf57345..9a193908 100644 --- a/bda/templates/bda/revente/subscribe.html +++ b/bda/templates/bda/revente/subscribe.html @@ -4,28 +4,41 @@ {% block realcontent %}

    Inscriptions pour BdA-Revente

    +
    + + Cochez les spectacles pour lesquels vous souhaitez recevoir un + notification quand une place est disponible en revente.
    + Lorsque vous validez vos choix, si un tirage au sort est en cours pour + un des spectacles que vous avez sélectionné, vous serez automatiquement + inscrit à ce tirage. +
    +
    {% csrf_token %}
    -

    Spectacles

    -
    - - + + -
    -
      - {% for checkbox in form.spectacles %} -
    • {{checkbox}}
    • - {%endfor%} -
    -
    +
    +
      + {% for checkbox in form.spectacles %} +
    • {{ checkbox }}
    • + {% endfor %} +
    +
    - +