from collections import defaultdict import random import hashlib import time import json 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 from django.contrib.auth.decorators import login_required 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 ) from django.core.urlresolvers import reverse from django.conf import settings from django.utils import timezone, formats from django.views.generic.list import ListView from gestioncof.decorators import cof_required, buro_required from bda.models import ( Spectacle, Participant, ChoixSpectacle, Attribution, Tirage, SpectacleRevente, Salle, CategorieSpectacle ) from bda.algorithm import Algorithm from bda.forms import ( TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm, InscriptionInlineFormSet, ReventeTirageForm, ReventeTirageAnnulForm ) from utils.views.autocomplete 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, ) success = False stateerror = False if request.method == "POST": # use *this* queryset dbstate = _hash_queryset(participant.choixspectacle_set.all()) if "dbstate" in request.POST and dbstate != request.POST["dbstate"]: stateerror = True formset = BdaFormSet(instance=participant) else: formset = BdaFormSet(request.POST, instance=participant) if formset.is_valid(): formset.save() success = True formset = BdaFormSet(instance=participant) 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 # Messages if success: messages.success(request, "Votre inscription a été mise à jour avec " "succès !") if stateerror: messages.error(request, "Impossible d'enregistrer vos modifications " ": vous avez apporté d'autres modifications " "entre temps.") return render(request, "bda/inscription-tirage.html", {"formset": formset, "total_price": total_price, "dbstate": dbstate, 'tirage': tirage}) 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}) @login_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.get_or_create( user=request.user, tirage=tirage) if not participant.paid: return render(request, "bda/revente/notpaid.html", {}) 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(): datatuple = [] 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 } datatuple.append(( 'bda-revente-new', context, settings.MAIL_DATA['revente']['FROM'], [participant.user.email] )) revente.save() send_mass_custom_mail(datatuple) # 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) overdue = participant.attribution_set.filter( spectacle__date__gte=timezone.now(), revente__isnull=False, revente__seller=participant, revente__notif_sent=True)\ .filter( Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant)) return render(request, "bda/revente/manage.html", {'tirage': tirage, 'overdue': overdue, "soldform": soldform, "annulform": annulform, "resellform": resellform}) @login_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)) ) 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) 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}) @login_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 = 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, "Ton inscription a bien été prise en compte") if deja_revente: messages.info(request, "Des reventes existent déjà pour certains de " "ces spectacles, vérifie les places " "disponibles sans tirage !") if inscrit_revente: shows = map("