Merge branch 'Aufinal/bda_fixes' into 'master'

Fix pour BdA-Revente

See merge request cof-geek/gestioCOF!263
This commit is contained in:
Martin Pepin 2018-04-07 14:55:33 +02:00
commit 25f4c64835
25 changed files with 657 additions and 261 deletions

1
TODO_PROD.md Normal file
View file

@ -0,0 +1 @@
- Changer les urls dans les mails "bda-revente" et "bda-shotgun"

View file

@ -236,7 +236,7 @@ class SpectacleReventeAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['answered_mail'].queryset = ( self.fields['confirmed_entry'].queryset = (
Participant.objects Participant.objects
.select_related('user', 'tirage') .select_related('user', 'tirage')
) )
@ -299,13 +299,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
count = queryset.count() count = queryset.count()
for revente in queryset.filter( for revente in queryset.filter(
attribution__spectacle__date__gte=timezone.now()): attribution__spectacle__date__gte=timezone.now()):
revente.date = timezone.now() - timedelta(hours=1) revente.reset(new_date=timezone.now() - timedelta(hours=1))
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
self.message_user( self.message_user(
request, request,
"%d attribution%s %s été réinitialisée%s avec succès." % ( "%d attribution%s %s été réinitialisée%s avec succès." % (

View file

@ -4,7 +4,7 @@ from django import forms
from django.forms.models import BaseInlineFormSet from django.forms.models import BaseInlineFormSet
from django.utils import timezone from django.utils import timezone
from bda.models import Attribution, Spectacle from bda.models import Attribution, Spectacle, SpectacleRevente
class InscriptionInlineFormSet(BaseInlineFormSet): class InscriptionInlineFormSet(BaseInlineFormSet):
@ -43,7 +43,33 @@ class TokenForm(forms.Form):
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj): def label_from_instance(self, obj):
return "%s" % str(obj.spectacle) return str(obj.spectacle)
class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def __init__(self, *args, own=True, **kwargs):
super().__init__(*args, **kwargs)
self.own = own
def label_from_instance(self, obj):
label = "{show}{suffix}"
suffix = ""
if self.own:
# C'est notre propre revente : pas besoin de spécifier le vendeur
if obj.soldTo is not None:
suffix = " -- Vendue à {firstname} {lastname}".format(
firstname=obj.soldTo.user.first_name,
lastname=obj.soldTo.user.last_name,
)
else:
# Ce n'est pas à nous : on ne voit jamais l'acheteur
suffix = " -- Vendue par {firstname} {lastname}".format(
firstname=obj.seller.user.first_name,
lastname=obj.seller.user.last_name,
)
return label.format(show=str(obj.attribution.spectacle),
suffix=suffix)
class ResellForm(forms.Form): class ResellForm(forms.Form):
@ -65,7 +91,8 @@ class ResellForm(forms.Form):
class AnnulForm(forms.Form): class AnnulForm(forms.Form):
attributions = AttributionModelMultipleChoiceField( reventes = ReventeModelMultipleChoiceField(
own=True,
label='', label='',
queryset=Attribution.objects.none(), queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
@ -73,14 +100,13 @@ class AnnulForm(forms.Form):
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super(AnnulForm, self).__init__(*args, **kwargs) super(AnnulForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = ( self.fields['reventes'].queryset = (
participant.attribution_set participant.original_shows
.filter(spectacle__date__gte=timezone.now(), .filter(attribution__spectacle__date__gte=timezone.now(),
revente__isnull=False, notif_sent=False,
revente__notif_sent=False, soldTo__isnull=True)
revente__soldTo__isnull=True) .select_related('attribution__spectacle',
.select_related('spectacle', 'spectacle__location', 'attribution__spectacle__location')
'participant__user')
) )
@ -99,19 +125,58 @@ class InscriptionReventeForm(forms.Form):
) )
class ReventeTirageAnnulForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=False,
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.entered.filter(soldTo__isnull=True)
.select_related('attribution__spectacle',
'seller__user')
)
class ReventeTirageForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=False,
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(confirmed_entry=participant)
.select_related('attribution__spectacle')
)
class SoldForm(forms.Form): class SoldForm(forms.Form):
attributions = AttributionModelMultipleChoiceField( reventes = ReventeModelMultipleChoiceField(
own=True,
label='', label='',
queryset=Attribution.objects.none(), queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple) widget=forms.CheckboxSelectMultiple)
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super(SoldForm, self).__init__(*args, **kwargs) super(SoldForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = ( self.fields['reventes'].queryset = (
participant.attribution_set participant.original_shows
.filter(revente__isnull=False, .filter(soldTo__isnull=False)
revente__soldTo__isnull=False) .exclude(soldTo=participant)
.exclude(revente__soldTo=participant) .select_related('attribution__spectacle',
.select_related('spectacle', 'spectacle__location', 'attribution__spectacle__location')
'participant__user')
) )

View file

@ -6,7 +6,6 @@ Gestion en ligne de commande des reventes.
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import timedelta
from django.core.management import BaseCommand from django.core.management import BaseCommand
from django.utils import timezone from django.utils import timezone
from bda.models import SpectacleRevente from bda.models import SpectacleRevente
@ -21,23 +20,36 @@ class Command(BaseCommand):
now = timezone.now() now = timezone.now()
reventes = SpectacleRevente.objects.all() reventes = SpectacleRevente.objects.all()
for revente in reventes: for revente in reventes:
# Check si < 24h # Le spectacle est bientôt et on a pas encore envoyé de mail :
if (revente.attribution.spectacle.date <= # on met la place au shotgun et on prévient.
revente.date + timedelta(days=1)) and \ if revente.is_urgent and not revente.notif_sent:
now >= revente.date + timedelta(minutes=15) and \ if revente.can_notif:
not revente.notif_sent: self.stdout.write(str(now))
self.stdout.write(str(now)) revente.mail_shotgun()
revente.mail_shotgun() self.stdout.write(
self.stdout.write("Mail de disponibilité immédiate envoyé") "Mails de disponibilité immédiate envoyés "
# Check si délai de retrait dépassé "pour la revente [%s]" % revente
elif (now >= revente.date + timedelta(hours=1) and )
not revente.notif_sent):
# Le spectacle est dans plus longtemps : on prévient
elif (revente.can_notif and not revente.notif_sent):
self.stdout.write(str(now)) self.stdout.write(str(now))
revente.send_notif() revente.send_notif()
self.stdout.write("Mail d'inscription à une revente envoyé") self.stdout.write(
# Check si tirage à faire "Mails d'inscription à la revente [%s] envoyés"
elif (now >= revente.date_tirage and % revente
not revente.tirage_done): )
# On fait le tirage
elif (now >= revente.date_tirage and not revente.tirage_done):
self.stdout.write(str(now)) self.stdout.write(str(now))
revente.tirage() winner = revente.tirage()
self.stdout.write("Tirage effectué, mails envoyés") 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")

View file

@ -0,0 +1,29 @@
# -*- 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.RenameField(
model_name='spectaclerevente',
old_name='answered_mail',
new_name='confirmed_entry',
),
migrations.AlterField(
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),
),
]

View file

@ -174,6 +174,7 @@ class Participant(models.Model):
def __str__(self): def __str__(self):
return "%s - %s" % (self.user, self.tirage.title) return "%s - %s" % (self.user, self.tirage.title)
DOUBLE_CHOICES = ( DOUBLE_CHOICES = (
("1", "1 place"), ("1", "1 place"),
("autoquit", "2 places si possible, 1 sinon"), ("autoquit", "2 places si possible, 1 sinon"),
@ -232,9 +233,9 @@ class SpectacleRevente(models.Model):
) )
date = models.DateTimeField("Date de mise en vente", date = models.DateTimeField("Date de mise en vente",
default=timezone.now) default=timezone.now)
answered_mail = models.ManyToManyField(Participant, confirmed_entry = models.ManyToManyField(Participant,
related_name="wanted", related_name="entered",
blank=True) blank=True)
seller = models.ForeignKey( seller = models.ForeignKey(
Participant, on_delete=models.CASCADE, Participant, on_delete=models.CASCADE,
verbose_name="Vendeur", verbose_name="Vendeur",
@ -248,21 +249,61 @@ class SpectacleRevente(models.Model):
notif_sent = models.BooleanField("Notification envoyée", notif_sent = models.BooleanField("Notification envoyée",
default=False) default=False)
notif_time = models.DateTimeField("Moment d'envoi de la notification",
blank=True, null=True)
tirage_done = models.BooleanField("Tirage effectué", tirage_done = models.BooleanField("Tirage effectué",
default=False) default=False)
shotgun = models.BooleanField("Disponible immédiatement", shotgun = models.BooleanField("Disponible immédiatement",
default=False) 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 real_notif_time(self):
if self.notif_time:
return self.notif_time
else:
return self.date + self.remorse_time
@property @property
def date_tirage(self): def date_tirage(self):
"""Renvoie la date du tirage au sort de la revente.""" """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 remaining_time = (self.attribution.spectacle.date
- self.date - timedelta(hours=13)) - self.real_notif_time - self.min_margin)
# Au minimum, on attend 2 jours avant le tirage
delay = min(remaining_time, timedelta(days=2)) delay = min(remaining_time, self.max_wait_time)
# Le vendeur a aussi 1h pour changer d'avis
return self.date + delay + timedelta(hours=1) return self.real_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): def __str__(self):
return "%s -- %s" % (self.seller, return "%s -- %s" % (self.seller,
@ -271,6 +312,18 @@ class SpectacleRevente(models.Model):
class Meta: class Meta:
verbose_name = "Revente" verbose_name = "Revente"
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 = new_date
self.confirmed_entry.clear()
self.soldTo = None
self.notif_sent = False
self.notif_time = None
self.tirage_done = False
self.shotgun = False
self.save()
def send_notif(self): def send_notif(self):
""" """
Envoie une notification pour indiquer la mise en vente d'une place sur Envoie une notification pour indiquer la mise en vente d'une place sur
@ -291,6 +344,7 @@ class SpectacleRevente(models.Model):
] ]
send_mass_custom_mail(datatuple) send_mass_custom_mail(datatuple)
self.notif_sent = True self.notif_sent = True
self.notif_time = timezone.now()
self.save() self.save()
def mail_shotgun(self): def mail_shotgun(self):
@ -312,76 +366,79 @@ class SpectacleRevente(models.Model):
] ]
send_mass_custom_mail(datatuple) send_mass_custom_mail(datatuple)
self.notif_sent = True self.notif_sent = True
self.notif_time = timezone.now()
# Flag inutile, sauf si l'horloge interne merde # Flag inutile, sauf si l'horloge interne merde
self.tirage_done = True self.tirage_done = True
self.shotgun = True self.shotgun = True
self.save() self.save()
def tirage(self): def tirage(self, send_mails=True):
""" """
Lance le tirage au sort associé à la revente. Un gagnant est choisi Lance le tirage au sort associé à la revente. Un gagnant est choisi
parmis les personnes intéressées par le spectacle. Les personnes sont parmis les personnes intéressées par le spectacle. Les personnes sont
ensuites prévenues par mail du résultat du tirage. 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 spectacle = self.attribution.spectacle
seller = self.seller seller = self.seller
winner = None
if inscrits: if inscrits:
# Envoie un mail au gagnant et au vendeur # Envoie un mail au gagnant et au vendeur
winner = random.choice(inscrits) winner = random.choice(inscrits)
self.soldTo = winner self.soldTo = winner
if send_mails:
mails = []
mails = [] context = {
'acheteur': winner.user,
'vendeur': seller.user,
'show': spectacle,
}
context = { c_mails_qs = CustomMail.objects.filter(shortname__in=[
'acheteur': winner.user, 'bda-revente-winner', 'bda-revente-loser',
'vendeur': seller.user, 'bda-revente-seller',
'show': spectacle, ])
}
c_mails_qs = CustomMail.objects.filter(shortname__in=[ c_mails = {cm.shortname: cm for cm in c_mails_qs}
'bda-revente-winner', 'bda-revente-loser',
'bda-revente-seller',
])
c_mails = {cm.shortname: cm for cm in c_mails_qs} mails.append(
c_mails['bda-revente-winner'].get_message(
mails.append( context,
c_mails['bda-revente-winner'].get_message( from_email=settings.MAIL_DATA['revente']['FROM'],
context, to=[winner.user.email],
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[winner.user.email],
)
)
mails.append(
c_mails['bda-revente-seller'].get_message(
context,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[seller.user.email],
reply_to=[winner.user.email],
)
)
# Envoie un mail aux perdants
for inscrit in inscrits:
if inscrit != winner:
new_context = dict(context)
new_context['acheteur'] = inscrit.user
mails.append(
c_mails['bda-revente-loser'].get_message(
new_context,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[inscrit.user.email],
)
) )
)
mail_conn = mail.get_connection() mails.append(
mail_conn.send_messages(mails) c_mails['bda-revente-seller'].get_message(
context,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[seller.user.email],
reply_to=[winner.user.email],
)
)
# Envoie un mail aux perdants
for inscrit in inscrits:
if inscrit != winner:
new_context = dict(context)
new_context['acheteur'] = inscrit.user
mails.append(
c_mails['bda-revente-loser'].get_message(
new_context,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[inscrit.user.email],
)
)
mail_conn = mail.get_connection()
mail_conn.send_messages(mails)
# Si personne ne veut de la place, elle part au shotgun # Si personne ne veut de la place, elle part au shotgun
else: else:
self.shotgun = True self.shotgun = True
self.tirage_done = True self.tirage_done = True
self.save() self.save()
return winner

View file

@ -1,33 +0,0 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2>
<form action="" class="form-horizontal" method="post">
{% csrf_token %}
<div class="form-group">
<h3>Spectacles</h3>
<br/>
<button type="button" class="btn btn-primary" onClick="select(true)">Tout sélectionner</button>
<button type="button" class="btn btn-primary" onClick="select(false)">Tout désélectionner</button>
<div class="multiple-checkbox">
<ul>
{% for checkbox in form.spectacles %}
<li>{{checkbox}}</li>
{%endfor%}
</ul>
</div>
</div>
<input type="submit" class="btn btn-primary" value="S'inscrire pour les places sélectionnées">
</form>
<script language="JavaScript">
function select(check) {
checkboxes = document.getElementsByName("spectacles");
for(var i=0, n=checkboxes.length;i<n;i++) {
checkboxes[i].checked = check;
}
}
</script>
{% endblock %}

View file

@ -0,0 +1,90 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Gestion des places que je revends</h2>
{% with resell_attributions=resellform.attributions annul_reventes=annulform.reventes sold_reventes=soldform.reventes %}
{% if resellform.attributions %}
<br />
<h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
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.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ resellform|bootstrap }}
</div>
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form>
<hr />
{% endif %}
{% if annul_reventes or overdue %}
<h3>Places en cours de revente</h3>
<form action="" method="post">
{% if annul_reventes %}
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Vous pouvez annuler les places mises en vente il y a moins d'une heure.
</div>
{% endif %}
{% csrf_token %}
<div class='form-group'>
<div class='multiple-checkbox'>
<ul>
{% for revente in annul_reventes %}
<li>{{ revente.tag }} {{ revente.choice_label }}</li>
{% endfor %}
{% for attrib in overdue %}
<li>
<input type="checkbox" style="visibility:hidden">
{{ attrib.spectacle }}
</li>
{% endfor %}
</ul>
</div>
</div>
{% if annul_reventes %}
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
{% endif %}
</form>
<hr />
{% endif %}
{% if sold_reventes %}
<h3>Places revendues</h3>
<form action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
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.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ soldform|bootstrap }}
</div>
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
</form>
{% endif %}
{% if not resell_attributions and not annul_attributions and not overdue and not sold_reventes %}
<p>Plus de reventes possibles !</p>
{% endif %}
{% endwith %}
{% endblock %}

View file

@ -5,7 +5,7 @@
{% if shotgun %} {% if shotgun %}
<ul class="list-unstyled"> <ul class="list-unstyled">
{% for spectacle in shotgun %} {% for spectacle in shotgun %}
<li><a href="{% url "bda-buy-revente" spectacle.id %}">{{spectacle}}</a></li> <li><a href="{% url "bda-revente-buy" spectacle.id %}">{{spectacle}}</a></li>
{% endfor %} {% endfor %}
{% else %} {% else %}
<p> Pas de places disponibles immédiatement, désolé !</p> <p> Pas de places disponibles immédiatement, désolé !</p>

View file

@ -0,0 +1,46 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2>
<form action="" class="form-horizontal" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Cochez les spectacles pour lesquels vous souhaitez recevoir un
notification quand une place est disponible en revente. <br />
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.
</div>
<br />
{% csrf_token %}
<div class="form-group">
<button type="button"
class="btn btn-primary"
onClick="select(true)">Tout sélectionner</button>
<button type="button"
class="btn btn-primary"
onClick="select(false)">Tout désélectionner</button>
<div class="multiple-checkbox">
<ul>
{% for checkbox in form.spectacles %}
<li>{{ checkbox }}</li>
{% endfor %}
</ul>
</div>
</div>
<input type="submit"
class="btn btn-primary"
value="S'inscrire pour les places sélectionnées">
</form>
<script language="JavaScript">
function select(check) {
checkboxes = document.getElementsByName("spectacles");
for(var i=0, n=checkboxes.length; i < n; i++) {
checkboxes[i].checked = check;
}
}
</script>
{% endblock %}

View file

@ -0,0 +1,52 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Tirages au sort de reventes</h2>
{% if annulform.reventes %}
<h3>Les reventes auxquelles vous êtes inscrit·e</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Vous pouvez vous désinscrire des reventes suivantes tant que le tirage n'a
pas eu lieu.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ annulform|bootstrap }}
</div>
<div class="form-actions">
<input type="submit"
class="btn btn-primary"
name="annul"
value="Se désinscrire des tirages sélectionnés">
</div>
</form>
<hr />
{% endif %}
{% if subform.reventes %}
<h3>Tirages en cours</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Vous pouvez vous inscrire aux tirage en cours suivants.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ subform|bootstrap }}
</div>
<div class="form-actions">
<input type="submit"
class="btn btn-primary"
name="subscribe"
value="S'inscrire aux tirages sélectionnés">
</div>
</form>
{% endif %}
{% endblock %}

View file

@ -6,7 +6,7 @@
<p>Le tirage au sort de cette revente a déjà été effectué !</p> <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 <p>Si personne n'était intéressé, elle est maintenant disponible
<a href="{% url "bda-buy-revente" revente.attribution.spectacle.id %}">ici</a>.</p> <a href="{% url "bda-revente-buy" revente.attribution.spectacle.id %}">ici</a>.</p>
{% else %} {% else %}
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p> <p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
{% endif %} {% endif %}

View file

@ -1,56 +0,0 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Revente de place</h2>
{% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %}
{% if resellform.attributions %}
<h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
{{resellform|bootstrap}}
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form>
{% endif %}
<br>
{% if annul_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 attrib in annul_attributions %}
<li>{{attrib.tag}} {{attrib.choice_label}}</li>
{% endfor %}
{% for attrib in overdue %}
<li>
<input type="checkbox" style="visibility:hidden">
{{attrib.spectacle}}
</li>
{% endfor %}
{% if annul_attributions %}
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
{% endif %}
</form>
{% endif %}
<br>
{% if sold_attributions %}
<h3>Places revendues</h3>
<form action="" method="post">
{% csrf_token %}
{{soldform|bootstrap}}
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
</form>
{% endif %}
{% if not resell_attributions and not annul_attributions and not overdue and not sold_attributions %}
<p>Plus de reventes possibles !</p>
{% endif %}
{% endwith %}
{% endblock %}

View file

@ -67,7 +67,7 @@ class SpectacleReventeTests(TestCase):
revente = self.rev revente = self.rev
wanted_by = [self.p1, self.p2, self.p3] wanted_by = [self.p1, self.p2, self.p3]
revente.answered_mail = wanted_by revente.confirmed_entry = wanted_by
with mock.patch('bda.models.random.choice') as mc: with mock.patch('bda.models.random.choice') as mc:
# Set winner to self.p1. # Set winner to self.p1.

69
bda/tests/test_revente.py Normal file
View file

@ -0,0 +1,69 @@
from django.contrib.auth.models import User
from django.test import TestCase, Client
from django.utils import timezone
from datetime import timedelta
from bda.models import (Tirage, Spectacle, Salle, CategorieSpectacle,
SpectacleRevente, Attribution, Participant)
class TestModels(TestCase):
def setUp(self):
self.tirage = Tirage.objects.create(
title="Tirage test",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now()
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
self.spectacle_soon = Spectacle.objects.create(
title="foo", date=timezone.now()+timedelta(days=1),
location=self.location, price=0, slots=42,
tirage=self.tirage, listing=False, category=self.category
)
self.spectacle_later = Spectacle.objects.create(
title="bar", date=timezone.now()+timedelta(days=30),
location=self.location, price=0, slots=42,
tirage=self.tirage, listing=False, category=self.category
)
user_buyer = User.objects.create_user(
username="bda_buyer", password="testbuyer"
)
user_seller = User.objects.create_user(
username="bda_seller", password="testseller"
)
self.buyer = Participant.objects.create(
user=user_buyer, tirage=self.tirage
)
self.seller = Participant.objects.create(
user=user_seller, tirage=self.tirage
)
self.attr_soon = Attribution.objects.create(
participant=self.seller, spectacle=self.spectacle_soon
)
self.attr_later = Attribution.objects.create(
participant=self.seller, spectacle=self.spectacle_later
)
self.revente_soon = SpectacleRevente.objects.create(
seller=self.seller,
attribution=self.attr_soon
)
self.revente_later = SpectacleRevente.objects.create(
seller=self.seller,
attribution=self.attr_later
)
def test_urgent(self):
self.assertTrue(self.revente_soon.is_urgent)
self.assertFalse(self.revente_later.is_urgent)
def test_tirage(self):
self.revente_soon.confirmed_entry.add(self.buyer)
self.assertEqual(self.revente_soon.tirage(send_mails=False),
self.buyer)
self.assertIsNone(self.revente_later.tirage(send_mails=False))

View file

@ -16,9 +16,6 @@ urlpatterns = [
url(r'^places/(?P<tirage_id>\d+)$', url(r'^places/(?P<tirage_id>\d+)$',
views.places, views.places,
name="bda-places-attribuees"), name="bda-places-attribuees"),
url(r'^revente/(?P<tirage_id>\d+)$',
views.revente,
name='bda-revente'),
url(r'^etat-places/(?P<tirage_id>\d+)$', url(r'^etat-places/(?P<tirage_id>\d+)$',
views.etat_places, views.etat_places,
name='bda-etat-places'), name='bda-etat-places'),
@ -38,18 +35,28 @@ urlpatterns = [
url(r'^participants/autocomplete$', url(r'^participants/autocomplete$',
views.participant_autocomplete, views.participant_autocomplete,
name="bda-participant-autocomplete"), name="bda-participant-autocomplete"),
url(r'^liste-revente/(?P<tirage_id>\d+)$',
views.list_revente, # Urls BdA-Revente
name="bda-liste-revente"),
url(r'^buy-revente/(?P<spectacle_id>\d+)$', url(r'^revente/(?P<tirage_id>\d+)/manage$',
views.buy_revente, views.revente_manage,
name="bda-buy-revente"), name='bda-revente-manage'),
url(r'^revente-interested/(?P<revente_id>\d+)$', url(r'^revente/(?P<tirage_id>\d+)/subscribe$',
views.revente_interested, views.revente_subscribe,
name='bda-revente-interested'), name="bda-revente-subscribe"),
url(r'^revente-immediat/(?P<tirage_id>\d+)$', url(r'^revente/(?P<tirage_id>\d+)/tirages$',
views.revente_tirages,
name="bda-revente-tirages"),
url(r'^revente/(?P<spectacle_id>\d+)/buy$',
views.revente_buy,
name="bda-revente-buy"),
url(r'^revente/(?P<revente_id>\d+)/confirm$',
views.revente_confirm,
name='bda-revente-confirm'),
url(r'^revente/(?P<tirage_id>\d+)/shotgun$',
views.revente_shotgun, views.revente_shotgun,
name="bda-shotgun"), name="bda-revente-shotgun"),
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', url(r'^mails-rappel/(?P<spectacle_id>\d+)$',
views.send_rappel, views.send_rappel,
name="bda-rappels" name="bda-rappels"

View file

@ -5,7 +5,6 @@ import random
import hashlib import hashlib
import time import time
import json import json
from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail, send_custom_mail from custommail.shortcuts import send_mass_custom_mail, send_custom_mail
from custommail.models import CustomMail from custommail.models import CustomMail
from django.shortcuts import render, get_object_or_404 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.db import transaction
from django.core import serializers from django.core import serializers
from django.db.models import Count, Q, Prefetch from django.db.models import Count, Q, Prefetch
from django.template.defaultfilters import pluralize
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django.http import ( from django.http import (
HttpResponseBadRequest, HttpResponseRedirect, JsonResponse HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
@ -30,7 +30,7 @@ from bda.models import (
from bda.algorithm import Algorithm from bda.algorithm import Algorithm
from bda.forms import ( from bda.forms import (
TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm, TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm,
InscriptionInlineFormSet, InscriptionInlineFormSet, ReventeTirageForm, ReventeTirageAnnulForm
) )
from utils.views.autocomplete import Select2QuerySetView from utils.views.autocomplete import Select2QuerySetView
@ -351,13 +351,21 @@ def tirage(request, tirage_id):
@login_required @login_required
def revente(request, tirage_id): def revente_manage(request, tirage_id):
"""
Gestion de ses propres reventes :
- Création d'une revente
- Annulation d'une revente
- Confirmation d'une revente = transfert de la place à la personne qui
rachète
- Annulation d'une revente après que le tirage a eu lieu
"""
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)
if not participant.paid: if not participant.paid:
return render(request, "bda-notpaid.html", {}) return render(request, "bda/revente/notpaid.html", {})
resellform = ResellForm(participant, prefix='resell') resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul') annulform = AnnulForm(participant, prefix='annul')
@ -377,12 +385,8 @@ def revente(request, tirage_id):
attribution=attribution, attribution=attribution,
defaults={'seller': participant}) defaults={'seller': participant})
if not created: if not created:
revente.seller = participant revente.reset()
revente.date = timezone.now()
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
revente.shotgun = False
context = { context = {
'vendeur': participant.user, 'vendeur': participant.user,
'show': attribution.spectacle, 'show': attribution.spectacle,
@ -399,18 +403,18 @@ def revente(request, tirage_id):
elif 'annul' in request.POST: elif 'annul' in request.POST:
annulform = AnnulForm(participant, request.POST, prefix='annul') annulform = AnnulForm(participant, request.POST, prefix='annul')
if annulform.is_valid(): if annulform.is_valid():
attributions = annulform.cleaned_data["attributions"] reventes = annulform.cleaned_data["reventes"]
for attribution in attributions: for revente in reventes:
attribution.revente.delete() revente.delete()
# On confirme une vente en transférant la place à la personne qui a # On confirme une vente en transférant la place à la personne qui a
# gagné le tirage # gagné le tirage
elif 'transfer' in request.POST: elif 'transfer' in request.POST:
soldform = SoldForm(participant, request.POST, prefix='sold') soldform = SoldForm(participant, request.POST, prefix='sold')
if soldform.is_valid(): if soldform.is_valid():
attributions = soldform.cleaned_data['attributions'] reventes = soldform.cleaned_data['reventes']
for attribution in attributions: for reventes in reventes:
attribution.participant = attribution.revente.soldTo revente.attribution.participant = revente.soldTo
attribution.save() revente.attribution.save()
# On annule la revente après le tirage au sort (par exemple si # 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 # la personne qui a gagné le tirage ne se manifeste pas). La place est
@ -418,18 +422,13 @@ def revente(request, tirage_id):
elif 'reinit' in request.POST: elif 'reinit' in request.POST:
soldform = SoldForm(participant, request.POST, prefix='sold') soldform = SoldForm(participant, request.POST, prefix='sold')
if soldform.is_valid(): if soldform.is_valid():
attributions = soldform.cleaned_data['attributions'] reventes = soldform.cleaned_data['reventes']
for attribution in attributions: for revente in reventes:
if attribution.spectacle.date > timezone.now(): if revente.attribution.spectacle.date > timezone.now():
revente = attribution.revente # On antidate pour envoyer le mail plus vite
revente.date = timezone.now() - timedelta(minutes=65) new_date = (timezone.now()
revente.soldTo = None - SpectacleRevente.remorse_time)
revente.notif_sent = False revente.reset(new_date=new_date)
revente.tirage_done = False
revente.shotgun = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
overdue = participant.attribution_set.filter( overdue = participant.attribution_set.filter(
spectacle__date__gte=timezone.now(), spectacle__date__gte=timezone.now(),
@ -439,28 +438,80 @@ def revente(request, tirage_id):
.filter( .filter(
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant)) 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, {'tirage': tirage, 'overdue': overdue, "soldform": soldform,
"annulform": annulform, "resellform": resellform}) "annulform": annulform, "resellform": resellform})
@login_required @login_required
def revente_interested(request, revente_id): def revente_tirages(request, tirage_id):
"""
Affiche à un participant la liste de toutes les reventes en cours (pour un
tirage donné) et lui permet de s'inscrire et se désinscrire à ces reventes.
"""
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
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():
reventes = subform.cleaned_data['reventes']
count = reventes.count()
for revente in reventes:
revente.confirmed_entry.add(participant)
if count > 0:
messages.success(
request,
"Tu as bien été inscrit à {} revente{}"
.format(count, pluralize(count))
)
elif "annul" in request.POST:
annulform = ReventeTirageAnnulForm(participant, request.POST,
prefix="annul")
if annulform.is_valid():
reventes = annulform.cleaned_data['reventes']
count = reventes.count()
for revente in reventes:
revente.confirmed_entry.remove(participant)
if count > 0:
messages.success(
request,
"Tu as bien été désinscrit de {} revente{}"
.format(count, pluralize(count))
)
return render(request, "bda/revente/tirages.html",
{"annulform": annulform, "subform": subform})
@login_required
def revente_confirm(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id) revente = get_object_or_404(SpectacleRevente, id=revente_id)
participant, _ = Participant.objects.get_or_create( participant, _ = 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 not revente.notif_sent or revente.shotgun:
return render(request, "bda-wrongtime.html", return render(request, "bda/revente/wrongtime.html",
{"revente": revente}) {"revente": revente})
revente.answered_mail.add(participant) revente.confirmed_entry.add(participant)
return render(request, "bda-interested.html", return render(request, "bda/revente/confirmed.html",
{"spectacle": revente.attribution.spectacle, {"spectacle": revente.attribution.spectacle,
"date": revente.date_tirage}) "date": revente.date_tirage})
@login_required @login_required
def list_revente(request, tirage_id): def revente_subscribe(request, tirage_id):
"""
Permet à un participant de sélectionner ses préférences pour les reventes.
Il recevra des notifications pour les spectacles qui l'intéressent et il
est automatiquement inscrit aux reventes en cours au moment il ajoute un
spectacle à la liste des spectacles qui l'intéressent.
"""
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
participant, _ = Participant.objects.get_or_create( participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=tirage) user=request.user, tirage=tirage)
@ -486,12 +537,12 @@ def list_revente(request, tirage_id):
# la revente ayant le moins d'inscrits # la revente ayant le moins d'inscrits
min_resell = ( min_resell = (
qset.filter(shotgun=False) qset.filter(shotgun=False)
.annotate(nb_subscribers=Count('answered_mail')) .annotate(nb_subscribers=Count('confirmed_entry'))
.order_by('nb_subscribers') .order_by('nb_subscribers')
.first() .first()
) )
if min_resell is not None: if min_resell is not None:
min_resell.answered_mail.add(participant) min_resell.confirmed_entry.add(participant)
inscrit_revente.append(spectacle) inscrit_revente.append(spectacle)
success = True success = True
else: else:
@ -514,11 +565,11 @@ def list_revente(request, tirage_id):
) )
messages.info(request, msg, extra_tags="safe") 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 @login_required
def buy_revente(request, spectacle_id): def revente_buy(request, spectacle_id):
spectacle = get_object_or_404(Spectacle, id=spectacle_id) spectacle = get_object_or_404(Spectacle, id=spectacle_id)
tirage = spectacle.tirage tirage = spectacle.tirage
participant, _ = Participant.objects.get_or_create( participant, _ = Participant.objects.get_or_create(
@ -532,13 +583,13 @@ def buy_revente(request, spectacle_id):
own_reventes = reventes.filter(seller=participant) own_reventes = reventes.filter(seller=participant)
if len(own_reventes) > 0: if len(own_reventes) > 0:
own_reventes[0].delete() own_reventes[0].delete()
return HttpResponseRedirect(reverse("bda-shotgun", return HttpResponseRedirect(reverse("bda-revente-shotgun",
args=[tirage.id])) args=[tirage.id]))
reventes_shotgun = reventes.filter(shotgun=True) reventes_shotgun = reventes.filter(shotgun=True)
if not reventes_shotgun: if not reventes_shotgun:
return render(request, "bda-no-revente.html", {}) return render(request, "bda/revente/none.html", {})
if request.POST: if request.POST:
revente = random.choice(reventes_shotgun) revente = random.choice(reventes_shotgun)
@ -555,11 +606,11 @@ def buy_revente(request, spectacle_id):
[revente.seller.user.email], [revente.seller.user.email],
context=context, context=context,
) )
return render(request, "bda-success.html", return render(request, "bda/revente/mail-success.html",
{"seller": revente.attribution.participant.user, {"seller": revente.attribution.participant.user,
"spectacle": spectacle}) "spectacle": spectacle})
return render(request, "revente-confirm.html", return render(request, "bda/revente/confirm-shotgun.html",
{"spectacle": spectacle, {"spectacle": spectacle,
"user": request.user}) "user": request.user})
@ -583,7 +634,7 @@ def revente_shotgun(request, tirage_id):
) )
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0] 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}) {"shotgun": shotgun})

View file

@ -159,23 +159,23 @@
}, },
{ {
"model": "custommail.custommail", "model": "custommail.custommail",
"pk": 3,
"fields": { "fields": {
"shortname": "bda-revente", "shortname": "bda-revente",
"subject": "{{ show }}", "subject": "{{ show }}",
"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", "description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour leur signaler qu'une place vient d'\u00eatre mise en vente.",
"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-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"
}, }
"pk": 3
}, },
{ {
"model": "custommail.custommail", "model": "custommail.custommail",
"pk": 4,
"fields": { "fields": {
"shortname": "bda-shotgun", "shortname": "bda-shotgun",
"subject": "{{ show }}", "subject": "{{ show }}",
"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", "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.",
"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-revente-buy\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
}, }
"pk": 4
}, },
{ {
"model": "custommail.custommail", "model": "custommail.custommail",

View file

@ -1140,3 +1140,14 @@ p.help-block {
margin: 5px auto; margin: 5px auto;
width: 90%; width: 90%;
} }
div.bg-info {
border-radius: 3px;
padding: 0.3em 1em;
margin-left: 1em;
margin-right: 1em;
}
.bootstrap-form-reduce > .form-group {
margin-top: -16px;
}

View file

@ -43,9 +43,10 @@
<li><a href="{% url "bda-etat-places" tirage.id %}">État des demandes</a></li> <li><a href="{% url "bda-etat-places" tirage.id %}">État des demandes</a></li>
{% else %} {% else %}
<li><a href="{% url "bda-places-attribuees" tirage.id %}">Mes places</a></li> <li><a href="{% url "bda-places-attribuees" tirage.id %}">Mes places</a></li>
<li><a href="{% url "bda-revente" tirage.id %}">Revendre une place</a></li> <li><a href="{% url "bda-revente-manage" tirage.id %}">Gérer les places que je revends</a></li>
<li><a href="{% url "bda-liste-revente" tirage.id %}">S'inscrire à BdA-Revente</a></li> <li><a href="{% url "bda-revente-tirages" tirage.id %}">Voir les reventes en cours</a></li>
<li><a href="{% url "bda-shotgun" tirage.id %}">Places disponibles immédiatement</a></li> <li><a href="{% url "bda-revente-subscribe" tirage.id %}">Indiquer les spectacles qui m'intéressent</a></li>
<li><a href="{% url "bda-revente-shotgun" tirage.id %}">Places disponibles immédiatement</a></li>
{% endif %} {% endif %}
</ul> </ul>
{% endfor %} {% endfor %}