# -*- coding: utf-8 -*- from __future__ import division from __future__ import print_function from __future__ import unicode_literals import random from custommail.utils import send_mass_custom_mail, send_custom_mail from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required from django.db import models, transaction from django.db.models import Count, Q from django.core import serializers from django.forms.models import inlineformset_factory from django.http import HttpResponseBadRequest, HttpResponseRedirect from django.core.urlresolvers import reverse from django.conf import settings import hashlib from django.template import loader from django.utils import timezone from django.views.generic.list import ListView import time from datetime import timedelta from gestioncof.decorators import cof_required, buro_required from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\ Tirage, SpectacleRevente from bda.algorithm import Algorithm from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\ InscriptionReventeForm @cof_required def etat_places(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) spectacles1 = ChoixSpectacle.objects \ .filter(spectacle__tirage=tirage) \ .filter(double_choice="1") \ .all() \ .values('spectacle', 'spectacle__title') \ .annotate(total=models.Count('spectacle')) spectacles2 = ChoixSpectacle.objects \ .filter(spectacle__tirage=tirage) \ .exclude(double_choice="1") \ .all() \ .values('spectacle', 'spectacle__title') \ .annotate(total=models.Count('spectacle')) spectacles = tirage.spectacle_set.all() spectacles_dict = {} total = 0 for spectacle in spectacles: spectacle.total = 0 spectacle.ratio = 0.0 spectacles_dict[spectacle.id] = spectacle for spectacle in spectacles1: spectacles_dict[spectacle["spectacle"]].total += spectacle["total"] spectacles_dict[spectacle["spectacle"]].ratio = \ spectacles_dict[spectacle["spectacle"]].total / \ spectacles_dict[spectacle["spectacle"]].slots total += spectacle["total"] for spectacle in spectacles2: spectacles_dict[spectacle["spectacle"]].total += 2*spectacle["total"] spectacles_dict[spectacle["spectacle"]].ratio = \ spectacles_dict[spectacle["spectacle"]].total / \ spectacles_dict[spectacle["spectacle"]].slots total += spectacle["total"] return render(request, "etat-places.html", {"spectacles": spectacles, "total": total, 'tirage': tirage}) 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, created = Participant.objects.get_or_create( user=request.user, tirage=tirage) places = participant.attribution_set.order_by( "spectacle__date", "spectacle").all() 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) return render(request, "resume_places.html", {"participant": participant, "places": filtered_places, "tirage": tirage, "total": total, "warning": warning}) @cof_required def inscription(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) if timezone.now() < tirage.ouverture: error_desc = tirage.ouverture.strftime('Ouverture le %d %b %Y à %H:%M') return render(request, 'resume_inscription.html', {"error_title": "Le tirage n'est pas encore ouvert !", "error_description": error_desc}) if timezone.now() > tirage.fermeture: participant, created = Participant.objects.get_or_create( user=request.user, tirage=tirage) choices = participant.choixspectacle_set.order_by("priority").all() return render(request, "resume_inscription.html", {"error_title": "C'est fini !", "error_description": "Tirage au sort dans la journée !", "choices": choices}) def formfield_callback(f, **kwargs): if f.name == "spectacle": kwargs['queryset'] = tirage.spectacle_set return f.formfield(**kwargs) BdaFormSet = inlineformset_factory( Participant, ChoixSpectacle, fields=("spectacle", "double_choice", "priority"), formset=BaseBdaFormSet, formfield_callback=formfield_callback) participant, created = Participant.objects.get_or_create( user=request.user, tirage=tirage) success = False stateerror = False if request.method == "POST": 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) dbstate = _hash_queryset(participant.choixspectacle_set.all()) total_price = 0 for choice in participant.choixspectacle_set.all(): total_price += choice.spectacle.price if choice.double: total_price += choice.spectacle.price return render(request, "inscription-bda.html", {"formset": formset, "success": success, "total_price": total_price, "dbstate": dbstate, 'tirage': tirage, "stateerror": stateerror}) def do_tirage(request, tirage_id): tirage_elt = get_object_or_404(Tirage, id=tirage_id) form = TokenForm(request.POST) if not form.is_valid(): return tirage(request, tirage_id) start = time.time() data = {} shows = tirage_elt.spectacle_set.select_related().all() members = tirage_elt.participant_set.all() choices = ChoixSpectacle.objects.filter(spectacle__tirage=tirage_elt) \ .order_by('participant', 'priority').select_related().all() algo = Algorithm(shows, members, choices) results = algo(form.cleaned_data["token"]) total_slots = 0 total_losers = 0 for (_, members, losers) in results: total_slots += len(members) total_losers += len(losers) data["total_slots"] = total_slots data["total_losers"] = total_losers data["shows"] = shows data["token"] = form.cleaned_data["token"] data["members"] = members data["results"] = results total_sold = 0 total_deficit = 0 opera_deficit = 0 for (show, members, _) in results: deficit = (show.slots - len(members)) * show.price total_sold += show.slots * show.price if deficit >= 0: if "Opéra" in show.location.name: opera_deficit += deficit total_deficit += deficit data["total_sold"] = total_sold - total_deficit data["total_deficit"] = total_deficit data["opera_deficit"] = opera_deficit data["duration"] = time.time() - start if request.user.is_authenticated(): members2 = {} # Participant objects are not shared accross spectacle results, # so assign a single object for each Participant id members_uniq = {} 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 Attribution.objects.filter(spectacle__tirage=tirage_elt).delete() tirage_elt.tokens += "%s\n\"\"\"%s\"\"\"\n" % ( timezone.now().strftime("%y-%m-%d %H:%M:%S"), form.cleaned_data['token']) tirage_elt.enable_do_tirage = False tirage_elt.save() 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 for (show, _, losers) in results: for (loser, _, _, _) in losers: loser.choicesrevente.add(show) loser.save() return render(request, "bda-attrib-extra.html", data) else: return render(request, "bda-attrib.html", 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(): return do_tirage(request, tirage_id) else: form = TokenForm() return render(request, "bda-token.html", {"form": form}) @login_required def revente(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, created = Participant.objects.get_or_create( user=request.user, tirage=tirage) if not participant.paid: return render(request, "bda-notpaid.html", {}) if request.method == 'POST': # On met en vente une place if 'resell' in request.POST: resellform = ResellForm(participant, request.POST, prefix='resell') annulform = AnnulForm(participant, prefix='annul') if resellform.is_valid(): mails_data = [] 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.seller = participant revente.date = timezone.now() revente.soldTo = None revente.notif_sent = False revente.tirage_done = False revente.shotgun = False context = { 'vendeur': participant.user, 'show': attribution.spectacle, 'revente': revente } mails_data.append(participant.user.email, context) revente.save() send_mass_custom_mail( 'bda-revente-new', mails_data, from_email=settings.MAIL_DATA['revente']['FROM'] ) # On annule une revente elif 'annul' in request.POST: annulform = AnnulForm(participant, request.POST, prefix='annul') resellform = ResellForm(participant, prefix='resell') if annulform.is_valid(): attributions = annulform.cleaned_data["attributions"] for attribution in attributions: attribution.revente.delete() # On confirme une vente en transférant la place à la personne qui a # gagné le tirage elif 'transfer' in request.POST: resellform = ResellForm(participant, prefix='resell') annulform = AnnulForm(participant, prefix='annul') revente_id = request.POST['transfer'][0] rev = SpectacleRevente.objects.filter(soldTo__isnull=False, id=revente_id) if rev.exists(): revente = rev.get() attrib = revente.attribution attrib.participant = revente.soldTo attrib.save() # On annule la revente après le tirage au sort (par exemple si # la personne qui a gagné le tirage ne se manifeste pas). La place est # alors remise en vente elif 'reinit' in request.POST: resellform = ResellForm(participant, prefix='resell') annulform = AnnulForm(participant, prefix='annul') revente_id = request.POST['reinit'][0] rev = SpectacleRevente.objects.filter(soldTo__isnull=False, id=revente_id) if rev.exists(): revente = rev.get() if revente.attribution.spectacle.date > timezone.now(): revente.date = timezone.now() - timedelta(hours=1) revente.soldTo = None revente.notif_sent = False revente.tirage_done = False revente.shotgun = False if revente.answered_mail: revente.answered_mail.clear() revente.save() else: resellform = ResellForm(participant, prefix='resell') annulform = AnnulForm(participant, prefix='annul') else: resellform = ResellForm(participant, prefix='resell') annulform = AnnulForm(participant, prefix='annul') overdue = participant.attribution_set.filter( spectacle__date__gte=timezone.now(), revente__isnull=False, revente__seller=participant, revente__date__lte=timezone.now()-timedelta(hours=1)).filter( Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant)) sold = participant.attribution_set.filter( spectacle__date__gte=timezone.now(), revente__isnull=False, revente__soldTo__isnull=False) return render(request, "bda-revente.html", {'tirage': tirage, 'overdue': overdue, "sold": sold, "annulform": annulform, "resellform": resellform}) @login_required def revente_interested(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) participant, created = Participant.objects.get_or_create( user=request.user, tirage=revente.attribution.spectacle.tirage) if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun: return render(request, "bda-wrongtime.html", {"revente": revente}) revente.answered_mail.add(participant) return render(request, "bda-interested.html", {"spectacle": revente.attribution.spectacle, "date": revente.date_tirage}) @login_required def list_revente(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, created = Participant.objects.get_or_create( user=request.user, tirage=tirage) deja_revente = False success = False inscrit_revente = False 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('answered_mail')) .order_by('nb_subscribers') .first() ) if min_resell is not None: min_resell.answered_mail.add(participant) min_resell.save() inscrit_revente = True success = True else: form = InscriptionReventeForm( tirage, initial={'spectacles': participant.choicesrevente.all()}) return render(request, "liste-reventes.html", {"form": form, "deja_revente": deja_revente, "success": success, "inscrit_revente": inscrit_revente}) @login_required def buy_revente(request, spectacle_id): spectacle = get_object_or_404(Spectacle, id=spectacle_id) tirage = spectacle.tirage participant, created = 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. if reventes.filter(seller=participant).exists(): revente = reventes.filter(seller=participant)[0] revente.delete() return HttpResponseRedirect(reverse("bda-shotgun", args=[tirage.id])) reventes_shotgun = list(reventes.filter(shotgun=True).all()) if not reventes_shotgun: return render(request, "bda-no-revente.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_custom_mail( revente.seller.user.email, 'bda-buy-shotgun', context=context, from_email='bda@ens.fr' ) return render(request, "bda-success.html", {"seller": revente.attribution.participant.user, "spectacle": spectacle}) return render(request, "revente-confirm.html", {"spectacle": spectacle, "user": request.user}) @login_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()) shotgun = [] for spectacle in spectacles: reventes = SpectacleRevente.objects.filter( attribution__spectacle=spectacle, shotgun=True, soldTo__isnull=True) if reventes.exists(): shotgun.append(spectacle) return render(request, "bda-shotgun.html", {"shotgun": 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.all() 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': participant.paid, 'nb_places': 1} if participant.id in participants: participants[participant.id]['nb_places'] += 1 participants[participant.id]['given'] += attrib.given 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.all() return categories def get_context_data(self, **kwargs): context = super(SpectacleListView, self).get_context_data(**kwargs) context['tirage_id'] = self.tirage.id context['tirage_name'] = self.tirage.title return context @buro_required def unpaid(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) unpaid = tirage.participant_set \ .annotate(nb_attributions=Count('attribution')) \ .filter(paid=False, nb_attributions__gt=0).all() return render(request, "bda-unpaid.html", {"unpaid": unpaid}) @buro_required def send_rappel(request, spectacle_id): show = get_object_or_404(Spectacle, id=spectacle_id) # Mails d'exemples exemple_mail_1place = render_mail('bda-rappel', { 'member': request.user, 'show': show, 'nb_attr': 1 }) exemple_mail_2place = render_mail('bda-rappel', { 'member': request.user, 'show': show, 'nb_attr': 2 }) # Contexte ctxt = {'show': show, 'exemple_mail_1place': exemple_mail_1place, 'exemple_mail_2places': exemple_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 return render(request, "mails-rappel.html", ctxt) def descriptions_spectacles(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) shows_qs = tirage.spectacle_set category_name = request.GET.get('category', '') location_id = request.GET.get('location', '') if category_name: shows_qs = shows_qs.filter(category__name=category_name) if location_id: try: shows_qs = shows_qs.filter(location__id=int(location_id)) except ValueError: return HttpResponseBadRequest( "La variable GET 'location' doit contenir un entier") return render(request, 'descriptions.html', {'shows': shows_qs.all()})