forked from DGNum/gestioCOF
911 lines
32 KiB
Python
911 lines
32 KiB
Python
import hashlib
|
|
import json
|
|
import random
|
|
import time
|
|
from collections import defaultdict
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.core import serializers
|
|
from django.core.exceptions import NON_FIELD_ERRORS
|
|
from django.core.mail import send_mail, send_mass_mail
|
|
from django.db import transaction
|
|
from django.db.models import Count, Prefetch
|
|
from django.forms.models import inlineformset_factory
|
|
from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
|
|
from django.shortcuts import get_object_or_404, render
|
|
from django.template import loader
|
|
from django.template.defaultfilters import pluralize
|
|
from django.urls import reverse
|
|
from django.utils import formats, timezone
|
|
from django.views.generic.list import ListView
|
|
|
|
from bda.algorithm import Algorithm
|
|
from bda.forms import (
|
|
AnnulForm,
|
|
InscriptionInlineFormSet,
|
|
InscriptionReventeForm,
|
|
ResellForm,
|
|
ReventeTirageAnnulForm,
|
|
ReventeTirageForm,
|
|
SoldForm,
|
|
TokenForm,
|
|
)
|
|
from bda.models import (
|
|
Attribution,
|
|
CategorieSpectacle,
|
|
ChoixSpectacle,
|
|
Participant,
|
|
Salle,
|
|
Spectacle,
|
|
SpectacleRevente,
|
|
Tirage,
|
|
)
|
|
from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required
|
|
from shared.views import Select2QuerySetView
|
|
|
|
|
|
@cof_required
|
|
def etat_places(request, tirage_id):
|
|
"""
|
|
Résumé des spectacles d'un tirage avec pour chaque spectacle :
|
|
- Le nombre de places en jeu
|
|
- Le nombre de demandes
|
|
- Le ratio demandes/places
|
|
Et le total de toutes les demandes
|
|
"""
|
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
|
|
|
spectacles = tirage.spectacle_set.select_related("location")
|
|
spectacles_dict = {} # index of spectacle by id
|
|
|
|
for spectacle in spectacles:
|
|
spectacle.total = 0 # init total requests
|
|
spectacles_dict[spectacle.id] = spectacle
|
|
|
|
choices = (
|
|
ChoixSpectacle.objects.filter(spectacle__in=spectacles)
|
|
.values("spectacle")
|
|
.annotate(total=Count("spectacle"))
|
|
)
|
|
|
|
# choices *by spectacles* whose only 1 place is requested
|
|
choices1 = choices.filter(double_choice="1")
|
|
# choices *by spectacles* whose 2 places is requested
|
|
choices2 = choices.exclude(double_choice="1")
|
|
|
|
for spectacle in choices1:
|
|
pk = spectacle["spectacle"]
|
|
spectacles_dict[pk].total += spectacle["total"]
|
|
for spectacle in choices2:
|
|
pk = spectacle["spectacle"]
|
|
spectacles_dict[pk].total += 2 * spectacle["total"]
|
|
|
|
# here, each spectacle.total contains the number of requests
|
|
|
|
slots = 0 # proposed slots
|
|
total = 0 # requests
|
|
for spectacle in spectacles:
|
|
slots += spectacle.slots
|
|
total += spectacle.total
|
|
spectacle.ratio = spectacle.total / spectacle.slots
|
|
|
|
context = {
|
|
"proposed": slots,
|
|
"spectacles": spectacles,
|
|
"total": total,
|
|
"tirage": tirage,
|
|
}
|
|
return render(request, "bda/etat-places.html", context)
|
|
|
|
|
|
def _hash_queryset(queryset):
|
|
data = serializers.serialize("json", queryset).encode("utf-8")
|
|
hasher = hashlib.sha256()
|
|
hasher.update(data)
|
|
return hasher.hexdigest()
|
|
|
|
|
|
@cof_required
|
|
def places(request, tirage_id):
|
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
|
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
|
|
places = participant.attribution_set.order_by(
|
|
"spectacle__date", "spectacle"
|
|
).select_related("spectacle", "spectacle__location")
|
|
total = sum(place.spectacle.price for place in places)
|
|
filtered_places = []
|
|
places_dict = {}
|
|
spectacles = []
|
|
dates = []
|
|
warning = False
|
|
for place in places:
|
|
if place.spectacle in spectacles:
|
|
places_dict[place.spectacle].double = True
|
|
else:
|
|
place.double = False
|
|
places_dict[place.spectacle] = place
|
|
spectacles.append(place.spectacle)
|
|
filtered_places.append(place)
|
|
date = place.spectacle.date.date()
|
|
if date in dates:
|
|
warning = True
|
|
else:
|
|
dates.append(date)
|
|
# On prévient l'utilisateur s'il a deux places à la même date
|
|
if warning:
|
|
messages.warning(
|
|
request,
|
|
"Attention, vous avez reçu des places pour "
|
|
"des spectacles différents à la même date.",
|
|
)
|
|
return render(
|
|
request,
|
|
"bda/resume_places.html",
|
|
{
|
|
"participant": participant,
|
|
"places": filtered_places,
|
|
"tirage": tirage,
|
|
"total": total,
|
|
},
|
|
)
|
|
|
|
|
|
@cof_required
|
|
def inscription(request, tirage_id):
|
|
"""
|
|
Vue d'inscription à un tirage BdA.
|
|
- On vérifie qu'on se situe bien entre la date d'ouverture et la date de
|
|
fermeture des inscriptions.
|
|
- On vérifie que l'inscription n'a pas été modifiée entre le moment où le
|
|
client demande le formulaire et le moment où il soumet son inscription
|
|
(autre session par exemple).
|
|
"""
|
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
|
if timezone.now() < tirage.ouverture:
|
|
# Le tirage n'est pas encore ouvert.
|
|
opening = formats.localize(timezone.template_localtime(tirage.ouverture))
|
|
messages.error(
|
|
request,
|
|
"Le tirage n'est pas encore ouvert : " "ouverture le {:s}".format(opening),
|
|
)
|
|
return render(request, "bda/resume-inscription-tirage.html", {})
|
|
|
|
participant, _ = Participant.objects.select_related("tirage").get_or_create(
|
|
user=request.user, tirage=tirage
|
|
)
|
|
|
|
if timezone.now() > tirage.fermeture:
|
|
# Le tirage est fermé.
|
|
choices = participant.choixspectacle_set.order_by("priority")
|
|
messages.error(request, " C'est fini : tirage au sort dans la journée !")
|
|
return render(
|
|
request, "bda/resume-inscription-tirage.html", {"choices": choices}
|
|
)
|
|
|
|
BdaFormSet = inlineformset_factory(
|
|
Participant,
|
|
ChoixSpectacle,
|
|
fields=("spectacle", "double_choice", "priority"),
|
|
formset=InscriptionInlineFormSet,
|
|
error_messages={
|
|
NON_FIELD_ERRORS: {
|
|
"unique_together": "Vous avez déjà demandé ce voeu plus haut !"
|
|
}
|
|
},
|
|
)
|
|
|
|
if request.method == "POST":
|
|
# use *this* queryset
|
|
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
|
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
|
|
formset = BdaFormSet(instance=participant)
|
|
messages.error(
|
|
request,
|
|
"Impossible d'enregistrer vos modifications "
|
|
": vous avez apporté d'autres modifications "
|
|
"entre temps.",
|
|
)
|
|
else:
|
|
formset = BdaFormSet(request.POST, instance=participant)
|
|
if formset.is_valid():
|
|
formset.save()
|
|
formset = BdaFormSet(instance=participant)
|
|
participant.accepte_charte = True
|
|
participant.save()
|
|
messages.success(
|
|
request, "Votre inscription a été mise à jour avec succès !"
|
|
)
|
|
else:
|
|
messages.error(
|
|
request,
|
|
"Une erreur s'est produite lors de l'enregistrement de vos vœux. "
|
|
"Avez-vous demandé plusieurs fois le même spectacle ?",
|
|
)
|
|
else:
|
|
formset = BdaFormSet(instance=participant)
|
|
# use *this* queryset
|
|
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
|
total_price = 0
|
|
choices = participant.choixspectacle_set.select_related("spectacle")
|
|
for choice in choices:
|
|
total_price += choice.spectacle.price
|
|
if choice.double:
|
|
total_price += choice.spectacle.price
|
|
return render(
|
|
request,
|
|
"bda/inscription-tirage.html",
|
|
{
|
|
"formset": formset,
|
|
"total_price": total_price,
|
|
"dbstate": dbstate,
|
|
"tirage": tirage,
|
|
"charte": participant.accepte_charte,
|
|
},
|
|
)
|
|
|
|
|
|
def do_tirage(tirage_elt, token):
|
|
"""
|
|
Fonction auxiliaire à la vue ``tirage`` qui lance effectivement le tirage
|
|
après qu'on a vérifié que c'est légitime et que le token donné en argument
|
|
est correct.
|
|
Rend les résultats
|
|
"""
|
|
# Initialisation du dictionnaire data qui va contenir les résultats
|
|
start = time.time()
|
|
data = {
|
|
"shows": tirage_elt.spectacle_set.select_related("location"),
|
|
"token": token,
|
|
"members": tirage_elt.participant_set.select_related("user"),
|
|
"total_slots": 0,
|
|
"total_losers": 0,
|
|
"total_sold": 0,
|
|
"total_deficit": 0,
|
|
"opera_deficit": 0,
|
|
}
|
|
|
|
# On lance le tirage
|
|
choices = (
|
|
ChoixSpectacle.objects.filter(spectacle__tirage=tirage_elt)
|
|
.order_by("participant", "priority")
|
|
.select_related("participant", "participant__user", "spectacle")
|
|
)
|
|
results = Algorithm(data["shows"], data["members"], choices)(token)
|
|
|
|
# On compte les places attribuées et les déçus
|
|
for (_, members, losers) in results:
|
|
data["total_slots"] += len(members)
|
|
data["total_losers"] += len(losers)
|
|
|
|
# On calcule le déficit et les bénéfices pour le BdA
|
|
# FIXME: le traitement de l'opéra est sale
|
|
for (show, members, _) in results:
|
|
deficit = (show.slots - len(members)) * show.price
|
|
data["total_sold"] += show.slots * show.price
|
|
if deficit >= 0:
|
|
if "Opéra" in show.location.name:
|
|
data["opera_deficit"] += deficit
|
|
data["total_deficit"] += deficit
|
|
data["total_sold"] -= data["total_deficit"]
|
|
|
|
# Participant objects are not shared accross spectacle results,
|
|
# so assign a single object for each Participant id
|
|
members_uniq = {}
|
|
members2 = {}
|
|
for (show, members, _) in results:
|
|
for (member, _, _, _) in members:
|
|
if member.id not in members_uniq:
|
|
members_uniq[member.id] = member
|
|
members2[member] = []
|
|
member.total = 0
|
|
member = members_uniq[member.id]
|
|
members2[member].append(show)
|
|
member.total += show.price
|
|
members2 = members2.items()
|
|
data["members2"] = sorted(members2, key=lambda m: m[0].user.last_name)
|
|
|
|
# ---
|
|
# À partir d'ici, le tirage devient effectif
|
|
# ---
|
|
|
|
# On suppression les vieilles attributions, on sauvegarde le token et on
|
|
# désactive le tirage
|
|
Attribution.objects.filter(spectacle__tirage=tirage_elt).delete()
|
|
tirage_elt.tokens += '{:s}\n"""{:s}"""\n'.format(
|
|
timezone.now().strftime("%y-%m-%d %H:%M:%S"), token
|
|
)
|
|
tirage_elt.enable_do_tirage = False
|
|
tirage_elt.save()
|
|
|
|
# On enregistre les nouvelles attributions
|
|
Attribution.objects.bulk_create(
|
|
[
|
|
Attribution(spectacle=show, participant=member)
|
|
for show, members, _ in results
|
|
for member, _, _, _ in members
|
|
]
|
|
)
|
|
|
|
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
|
|
ChoixRevente = Participant.choicesrevente.through
|
|
|
|
# Suppression des reventes demandées/enregistrées
|
|
# (si le tirage est relancé)
|
|
(ChoixRevente.objects.filter(spectacle__tirage=tirage_elt).delete())
|
|
(
|
|
SpectacleRevente.objects.filter(
|
|
attribution__spectacle__tirage=tirage_elt
|
|
).delete()
|
|
)
|
|
|
|
lost_by = defaultdict(set)
|
|
for show, _, losers in results:
|
|
for loser, _, _, _ in losers:
|
|
lost_by[loser].add(show)
|
|
|
|
ChoixRevente.objects.bulk_create(
|
|
ChoixRevente(participant=member, spectacle=show)
|
|
for member, shows in lost_by.items()
|
|
for show in shows
|
|
)
|
|
|
|
data["duration"] = time.time() - start
|
|
data["results"] = results
|
|
return data
|
|
|
|
|
|
@buro_required
|
|
def tirage(request, tirage_id):
|
|
tirage_elt = get_object_or_404(Tirage, id=tirage_id)
|
|
if not (tirage_elt.enable_do_tirage and tirage_elt.fermeture < timezone.now()):
|
|
return render(request, "tirage-failed.html", {"tirage": tirage_elt})
|
|
if request.POST:
|
|
form = TokenForm(request.POST)
|
|
if form.is_valid():
|
|
results = do_tirage(tirage_elt, form.cleaned_data["token"])
|
|
return render(request, "bda-attrib-extra.html", results)
|
|
else:
|
|
form = TokenForm()
|
|
return render(request, "bda-token.html", {"form": form})
|
|
|
|
|
|
@cof_required
|
|
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)
|
|
participant, created = Participant.objects.annotate_paid().get_or_create(
|
|
user=request.user, tirage=tirage
|
|
)
|
|
|
|
resellform = ResellForm(participant, prefix="resell")
|
|
annulform = AnnulForm(participant, prefix="annul")
|
|
soldform = SoldForm(participant, prefix="sold")
|
|
|
|
if request.method == "POST":
|
|
# On met en vente une place
|
|
if "resell" in request.POST:
|
|
resellform = ResellForm(participant, request.POST, prefix="resell")
|
|
if resellform.is_valid():
|
|
mails = []
|
|
attributions = resellform.cleaned_data["attributions"]
|
|
with transaction.atomic():
|
|
for attribution in attributions:
|
|
revente, created = SpectacleRevente.objects.get_or_create(
|
|
attribution=attribution, defaults={"seller": participant}
|
|
)
|
|
if not created:
|
|
revente.reset()
|
|
|
|
context = {
|
|
"vendeur": participant.user,
|
|
"show": attribution.spectacle,
|
|
"revente": revente,
|
|
}
|
|
mails.append(
|
|
(
|
|
"BdA-Revente : {}".format(attribution.spectacle),
|
|
loader.render_to_string(
|
|
"bda/mails/revente-seller.txt", context=context
|
|
),
|
|
settings.MAIL_DATA["revente"]["FROM"],
|
|
[participant.user.email],
|
|
)
|
|
)
|
|
send_mass_mail(mails)
|
|
# On annule une revente
|
|
elif "annul" in request.POST:
|
|
annulform = AnnulForm(participant, request.POST, prefix="annul")
|
|
if annulform.is_valid():
|
|
reventes = annulform.cleaned_data["reventes"]
|
|
for revente in reventes:
|
|
revente.delete()
|
|
# On confirme une vente en transférant la place à la personne qui a
|
|
# gagné le tirage
|
|
elif "transfer" in request.POST:
|
|
soldform = SoldForm(participant, request.POST, prefix="sold")
|
|
if soldform.is_valid():
|
|
reventes = soldform.cleaned_data["reventes"]
|
|
for revente in reventes:
|
|
revente.attribution.participant = revente.soldTo
|
|
revente.attribution.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:
|
|
soldform = SoldForm(participant, request.POST, prefix="sold")
|
|
if soldform.is_valid():
|
|
reventes = soldform.cleaned_data["reventes"]
|
|
for revente in reventes:
|
|
if revente.attribution.spectacle.date > timezone.now():
|
|
# On antidate pour envoyer le mail plus vite
|
|
new_date = timezone.now() - SpectacleRevente.remorse_time
|
|
revente.reset(new_date=new_date)
|
|
|
|
sold_exists = soldform.fields["reventes"].queryset.exists()
|
|
annul_exists = annulform.fields["reventes"].queryset.exists()
|
|
resell_exists = resellform.fields["attributions"].queryset.exists()
|
|
|
|
return render(
|
|
request,
|
|
"bda/revente/manage.html",
|
|
{
|
|
"tirage": tirage,
|
|
"soldform": soldform,
|
|
"annulform": annulform,
|
|
"resellform": resellform,
|
|
"sold_exists": sold_exists,
|
|
"annul_exists": annul_exists,
|
|
"resell_exists": resell_exists,
|
|
},
|
|
)
|
|
|
|
|
|
@cof_required
|
|
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)
|
|
),
|
|
)
|
|
|
|
annul_exists = annulform.fields["reventes"].queryset.exists()
|
|
sub_exists = subform.fields["reventes"].queryset.exists()
|
|
|
|
return render(
|
|
request,
|
|
"bda/revente/tirages.html",
|
|
{
|
|
"annulform": annulform,
|
|
"subform": subform,
|
|
"annul_exists": annul_exists,
|
|
"sub_exists": sub_exists,
|
|
},
|
|
)
|
|
|
|
|
|
@cof_required
|
|
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 not revente.notif_sent or revente.shotgun:
|
|
return render(request, "bda/revente/wrongtime.html", {"revente": revente})
|
|
|
|
revente.confirmed_entry.add(participant)
|
|
return render(
|
|
request,
|
|
"bda/revente/confirmed.html",
|
|
{"spectacle": revente.attribution.spectacle, "date": revente.date_tirage},
|
|
)
|
|
|
|
|
|
@cof_required
|
|
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 où il ajoute un
|
|
spectacle à la liste des spectacles qui l'intéressent.
|
|
"""
|
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
|
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
|
|
deja_revente = False
|
|
success = False
|
|
inscrit_revente = []
|
|
if request.method == "POST":
|
|
form = InscriptionReventeForm(tirage, request.POST)
|
|
if form.is_valid():
|
|
choices = form.cleaned_data["spectacles"]
|
|
participant.choicesrevente.set(choices)
|
|
participant.save()
|
|
for spectacle in choices:
|
|
qset = SpectacleRevente.objects.filter(attribution__spectacle=spectacle)
|
|
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("confirmed_entry"))
|
|
.order_by("nb_subscribers")
|
|
.first()
|
|
)
|
|
if min_resell is not None:
|
|
min_resell.confirmed_entry.add(participant)
|
|
inscrit_revente.append(spectacle)
|
|
success = True
|
|
else:
|
|
form = InscriptionReventeForm(
|
|
tirage, initial={"spectacles": participant.choicesrevente.all()}
|
|
)
|
|
# Messages
|
|
if success:
|
|
messages.success(request, "Votre inscription a bien été prise en compte")
|
|
if deja_revente:
|
|
messages.info(
|
|
request,
|
|
"Des reventes existent déjà pour certains de "
|
|
"ces spectacles, vérifiez les places "
|
|
"disponibles sans tirage !",
|
|
)
|
|
if inscrit_revente:
|
|
shows = map("<li>{!s}</li>".format, inscrit_revente)
|
|
msg = (
|
|
"Vous avez été inscrit·e à des reventes en cours pour les spectacles "
|
|
"<ul>{:s}</ul>".format("\n".join(shows))
|
|
)
|
|
messages.info(request, msg, extra_tags="safe")
|
|
|
|
return render(request, "bda/revente/subscribe.html", {"form": form})
|
|
|
|
|
|
@cof_required
|
|
def revente_buy(request, spectacle_id):
|
|
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
|
|
tirage = spectacle.tirage
|
|
participant, _ = Participant.objects.get_or_create(user=request.user, tirage=tirage)
|
|
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.
|
|
own_reventes = reventes.filter(seller=participant)
|
|
if len(own_reventes) > 0:
|
|
own_reventes[0].delete()
|
|
return HttpResponseRedirect(reverse("bda-revente-shotgun", args=[tirage.id]))
|
|
|
|
reventes_shotgun = reventes.filter(shotgun=True)
|
|
|
|
if not reventes_shotgun:
|
|
return render(request, "bda/revente/none.html", {})
|
|
|
|
if request.POST:
|
|
revente = random.choice(reventes_shotgun)
|
|
revente.soldTo = participant
|
|
revente.save()
|
|
context = {
|
|
"show": spectacle,
|
|
"acheteur": request.user,
|
|
"vendeur": revente.seller.user,
|
|
}
|
|
|
|
send_mail(
|
|
"BdA-Revente : {}".format(spectacle.title),
|
|
loader.render_to_string(
|
|
"bda/mails/revente-shotgun-seller.txt", context=context
|
|
),
|
|
request.user.email,
|
|
[revente.seller.user.email],
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
"bda/revente/mail-success.html",
|
|
{"seller": revente.attribution.participant.user, "spectacle": spectacle},
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
"bda/revente/confirm-shotgun.html",
|
|
{"spectacle": spectacle, "user": request.user},
|
|
)
|
|
|
|
|
|
@cof_required
|
|
def revente_shotgun(request, tirage_id):
|
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
|
spectacles = (
|
|
tirage.spectacle_set.filter(date__gte=timezone.now())
|
|
.select_related("location")
|
|
.prefetch_related(
|
|
Prefetch(
|
|
"attribues",
|
|
queryset=(
|
|
Attribution.objects.filter(
|
|
revente__shotgun=True, revente__soldTo__isnull=True
|
|
)
|
|
),
|
|
to_attr="shotguns",
|
|
)
|
|
)
|
|
)
|
|
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
|
|
|
|
return render(request, "bda/revente/shotgun.html", {"spectacles": shotgun})
|
|
|
|
|
|
@buro_required
|
|
def spectacle(request, tirage_id, spectacle_id):
|
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
|
spectacle = get_object_or_404(Spectacle, id=spectacle_id, tirage=tirage)
|
|
attributions = spectacle.attribues.select_related(
|
|
"participant", "participant__user"
|
|
)
|
|
participants = {}
|
|
for attrib in attributions:
|
|
participant = attrib.participant
|
|
participant_info = {
|
|
"lastname": participant.user.last_name,
|
|
"name": participant.user.get_full_name,
|
|
"username": participant.user.username,
|
|
"email": participant.user.email,
|
|
"given": int(attrib.given),
|
|
"paid": attrib.paid,
|
|
"nb_places": 1,
|
|
}
|
|
if participant.id in participants:
|
|
participants[participant.id]["nb_places"] += 1
|
|
participants[participant.id]["given"] += attrib.given
|
|
participants[participant.id]["paid"] &= attrib.paid
|
|
else:
|
|
participants[participant.id] = participant_info
|
|
|
|
participants_info = sorted(participants.values(), key=lambda part: part["lastname"])
|
|
return render(
|
|
request,
|
|
"bda/participants.html",
|
|
{"spectacle": spectacle, "participants": participants_info},
|
|
)
|
|
|
|
|
|
class SpectacleListView(ListView):
|
|
model = Spectacle
|
|
template_name = "spectacle_list.html"
|
|
|
|
def get_queryset(self):
|
|
self.tirage = get_object_or_404(Tirage, id=self.kwargs["tirage_id"])
|
|
categories = self.tirage.spectacle_set.select_related("location")
|
|
return categories
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["tirage_id"] = self.tirage.id
|
|
context["tirage_name"] = self.tirage.title
|
|
return context
|
|
|
|
|
|
class UnpaidParticipants(BuroRequiredMixin, ListView):
|
|
context_object_name = "unpaid"
|
|
template_name = "bda-unpaid.html"
|
|
|
|
def get_queryset(self):
|
|
return (
|
|
Participant.objects.annotate_paid()
|
|
.filter(tirage__id=self.kwargs["tirage_id"], paid=False)
|
|
.select_related("user")
|
|
)
|
|
|
|
|
|
@buro_required
|
|
def send_rappel(request, spectacle_id):
|
|
show = get_object_or_404(Spectacle, id=spectacle_id)
|
|
# Mails d'exemples
|
|
subject = show.title
|
|
body_mail_1place = loader.render_to_string(
|
|
"bda/mails/rappel.txt",
|
|
context={"member": request.user, "show": show, "nb_attr": 1},
|
|
)
|
|
body_mail_2places = loader.render_to_string(
|
|
"bda/mails/rappel.txt",
|
|
context={"member": request.user, "show": show, "nb_attr": 2},
|
|
)
|
|
|
|
# Contexte
|
|
ctxt = {
|
|
"show": show,
|
|
"exemple_mail_1place": (subject, body_mail_1place),
|
|
"exemple_mail_2places": (subject, body_mail_2places),
|
|
}
|
|
# Envoi confirmé
|
|
if request.method == "POST":
|
|
members = show.send_rappel()
|
|
ctxt["sent"] = True
|
|
ctxt["members"] = members
|
|
# Demande de confirmation
|
|
else:
|
|
ctxt["sent"] = False
|
|
if show.rappel_sent:
|
|
messages.warning(
|
|
request,
|
|
"Attention, un mail de rappel pour ce spectale a déjà été "
|
|
"envoyé le {}".format(
|
|
formats.localize(timezone.template_localtime(show.rappel_sent))
|
|
),
|
|
)
|
|
return render(request, "bda/mails-rappel.html", ctxt)
|
|
|
|
|
|
def catalogue(request, request_type):
|
|
"""
|
|
Vue destinée à communiquer avec un client AJAX, fournissant soit :
|
|
- la liste des tirages
|
|
- les catégories et salles d'un tirage
|
|
- les descriptions d'un tirage (filtrées selon la catégorie et la salle)
|
|
"""
|
|
if request_type == "list":
|
|
# Dans ce cas on retourne la liste des tirages et de leur id en JSON
|
|
data_return = list(
|
|
Tirage.objects.filter(appear_catalogue=True).values("id", "title")
|
|
)
|
|
return JsonResponse(data_return, safe=False)
|
|
if request_type == "details":
|
|
# Dans ce cas on retourne une liste des catégories et des salles
|
|
tirage_id = request.GET.get("id", None)
|
|
if tirage_id is None:
|
|
return HttpResponseBadRequest("Missing GET parameter: id <int>")
|
|
try:
|
|
tirage = get_object_or_404(Tirage, id=int(tirage_id))
|
|
except ValueError:
|
|
return HttpResponseBadRequest("Bad format: int expected for `id`")
|
|
shows = tirage.spectacle_set.values_list("id", flat=True)
|
|
categories = list(
|
|
CategorieSpectacle.objects.filter(spectacle__in=shows)
|
|
.distinct()
|
|
.values("id", "name")
|
|
)
|
|
locations = list(
|
|
Salle.objects.filter(spectacle__in=shows).distinct().values("id", "name")
|
|
)
|
|
data_return = {"categories": categories, "locations": locations}
|
|
return JsonResponse(data_return, safe=False)
|
|
if request_type == "descriptions":
|
|
# Ici on retourne les descriptions correspondant à la catégorie et
|
|
# à la salle spécifiées
|
|
|
|
tirage_id = request.GET.get("id", "")
|
|
categories = request.GET.get("category", "[]")
|
|
locations = request.GET.get("location", "[]")
|
|
try:
|
|
tirage_id = int(tirage_id)
|
|
categories_id = json.loads(categories)
|
|
locations_id = json.loads(locations)
|
|
# Integers expected
|
|
if not all(isinstance(id, int) for id in categories_id):
|
|
raise ValueError
|
|
if not all(isinstance(id, int) for id in locations_id):
|
|
raise ValueError
|
|
except ValueError: # Contient JSONDecodeError
|
|
return HttpResponseBadRequest(
|
|
"Parse error, please ensure the GET parameters have the "
|
|
"following types:\n"
|
|
"id: int, category: [int], location: [int]\n"
|
|
"Data received:\n"
|
|
"id = {}, category = {}, locations = {}".format(
|
|
request.GET.get("id", ""),
|
|
request.GET.get("category", "[]"),
|
|
request.GET.get("location", "[]"),
|
|
)
|
|
)
|
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
|
|
|
shows_qs = tirage.spectacle_set.select_related("location").prefetch_related(
|
|
"quote_set"
|
|
)
|
|
if categories_id and 0 not in categories_id:
|
|
shows_qs = shows_qs.filter(category__id__in=categories_id)
|
|
if locations_id and 0 not in locations_id:
|
|
shows_qs = shows_qs.filter(location__id__in=locations_id)
|
|
|
|
# On convertit les descriptions à envoyer en une liste facilement
|
|
# JSONifiable (il devrait y avoir un moyen plus efficace en
|
|
# redéfinissant le serializer de JSON)
|
|
data_return = [
|
|
{
|
|
"title": spectacle.title,
|
|
"category": str(spectacle.category),
|
|
"date": str(
|
|
formats.date_format(
|
|
timezone.localtime(spectacle.date), "SHORT_DATETIME_FORMAT"
|
|
)
|
|
),
|
|
"location": str(spectacle.location),
|
|
"vips": spectacle.vips,
|
|
"description": spectacle.description,
|
|
"slots_description": spectacle.slots_description,
|
|
"quotes": [
|
|
dict(author=quote.author, text=quote.text)
|
|
for quote in spectacle.quote_set.all()
|
|
],
|
|
"image": spectacle.getImgUrl(),
|
|
"ext_link": spectacle.ext_link,
|
|
"price": spectacle.price,
|
|
"slots": spectacle.slots,
|
|
}
|
|
for spectacle in shows_qs
|
|
]
|
|
return JsonResponse(data_return, safe=False)
|
|
# Si la requête n'est pas de la forme attendue, on quitte avec une erreur
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
##
|
|
# Autocomplete views
|
|
#
|
|
# https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#create-an-autocomplete-view
|
|
##
|
|
|
|
|
|
class ParticipantAutocomplete(Select2QuerySetView):
|
|
model = Participant
|
|
search_fields = ("user__username", "user__first_name", "user__last_name")
|
|
|
|
|
|
participant_autocomplete = buro_required(ParticipantAutocomplete.as_view())
|
|
|
|
|
|
class SpectacleAutocomplete(Select2QuerySetView):
|
|
model = Spectacle
|
|
search_fields = ("title",)
|
|
|
|
|
|
spectacle_autocomplete = buro_required(SpectacleAutocomplete.as_view())
|