# -*- coding: utf-8 -*- from __future__ import division from __future__ import print_function from __future__ import unicode_literals import random 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, mail 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.core.mail import send_mail 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 = [] 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 mail_subject = "BdA-Revente : {:s}".format( attribution.spectacle.title) mail_body = loader.render_to_string( 'bda/mails/revente-new.txt', {'vendeur': participant.user, 'spectacle': attribution.spectacle, 'revente': revente} ) mails.append(mail.EmailMessage( mail_subject, mail_body, from_email=settings.MAIL_DATA['revente']['FROM'], to=[participant.user.email], reply_to=[ settings.MAIL_DATA['revente']['REPLYTO'] ], )) revente.save() mail.get_connection().send_messages(mails) # 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() mail = loader.render_to_string('bda/mails/buy-shotgun.txt', { 'spectacle': spectacle, 'acheteur': request.user, 'vendeur': revente.seller.user, }) send_mail("BdA-Revente : %s" % spectacle.title, mail, request.user.email, [revente.seller.user.email], fail_silently=False) 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 fake_member = request.user fake_member.nb_attr = 1 exemple_mail_1place = loader.render_to_string('bda/mails/rappel.txt', { 'member': fake_member, 'show': show}) fake_member.nb_attr = 2 exemple_mail_2places = loader.render_to_string('bda/mails/rappel.txt', { 'member': fake_member, 'show': show}) # 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()})