From 64b8ee41337b615af7f74d925520d54217782317 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 19 Aug 2014 12:54:22 +0200 Subject: [PATCH] =?UTF-8?q?Grod=20commit=20d=C3=A9gueux=20avec=20une=20ton?= =?UTF-8?q?ne=20de=20trucs.=20Berk.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apache/django.wsgi | 0 bda/admin.py | 68 +++++++---- bda/algorithm.py | 35 +++--- bda/models.py | 7 +- bda/views.py | 106 +++++++++++++---- gestioncof/admin.py | 6 + gestioncof/models.py | 42 +++++++ gestioncof/petits_cours_views.py | 156 ++++++++++++------------- gestioncof/shared.py | 18 ++- gestioncof/views.py | 171 +++++++++++++++++++--------- manage.py | 0 media/cof.css | 48 +++++++- templates/bda/bda-attrib.html | 1 + templates/bda/etat-places.html | 48 +++++++- templates/bda/inscription-bda.html | 6 +- templates/gestioncof/home.html | 29 ++++- templates/gestioncof/utile_cof.html | 4 +- 17 files changed, 536 insertions(+), 209 deletions(-) mode change 100755 => 100644 apache/django.wsgi mode change 100755 => 100644 manage.py diff --git a/apache/django.wsgi b/apache/django.wsgi old mode 100755 new mode 100644 diff --git a/bda/admin.py b/bda/admin.py index c934e857..c860c0f4 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -4,7 +4,7 @@ from django.core.mail import send_mail from django.contrib.contenttypes.models import ContentType from django.contrib import admin -from django.db.models import Sum +from django.db.models import Sum, Count from bda.models import Spectacle, Salle, Participant, ChoixSpectacle, Attribution class ChoixSpectacleInline(admin.TabularInline): @@ -17,13 +17,18 @@ class AttributionInline(admin.TabularInline): class ParticipantAdmin(admin.ModelAdmin): #inlines = [ChoixSpectacleInline] inlines = [AttributionInline] + def queryset(self, request): + return Participant.objects.annotate(nb_places = Count('attributions'), + total = Sum('attributions__price')) def nb_places(self, obj): - return len(obj.attribution_set.all()) + return obj.nb_places + nb_places.admin_order_field = "nb_places" nb_places.short_description = "Nombre de places" def total(self, obj): - tot = obj.attributions.aggregate(total = Sum('price'))['total'] + tot = obj.total if tot: return u"%.02f €" % tot else: return u"0 €" + total.admin_order_field = "total" total.short_description = "Total à payer" list_display = ("user", "nb_places", "total", "paid", "paymenttype") list_filter = ("paid",) @@ -56,7 +61,7 @@ Voici tes choix de spectacles tels que notre système les a enregistrés :\n\n"" Artistiquement, Le BdA""" send_mail ("Choix de spectacles (BdA du COF)", mail, - "bda@clipper.ens.fr", [member.user.email], + "bda@ens.fr", [member.user.email], fail_silently = True) count = len(queryset.all()) if count == 1: @@ -81,35 +86,48 @@ pour les spectacles suivants : %s *Paiement* -Ces spectacles sont à régler avant le vendredi 19 Octobre, pendant les -heures de permanences du COF (tous les jours de la semaine entre 12h et -14h, et entre 18h et 20h). Des facilités de paiement sont bien évidemment -possibles (encaissement échelonné des chèques). +L'intégralité de ces places de spectacles est à régler à partir du jeudi +10 octobre et AVANT le mercredi 23 octobre, au bureau du COF pendant les +heures de permanences (du lundi au vendredi entre 12h et 14h, et entre 18h +et 20h). Des facilités de paiement sont bien évidemment possibles : nous +pouvons ne pas encaisser le chèque immédiatement, ou bien découper votre +paiement en deux fois. *Mode de retrait des places* -Pour l'Opéra de Paris, le théâtre de la Colline et le théâtre du Châtelet, -les places sont à retirer au COF le jour du paiement. +Au moment du paiement, une enveloppe vous sera remise, contenant les +places pour l'Opéra de Paris, pour les premiers spectacles de la Comédie +française, certains spectacles du Châtelet et du Théâtre de la Ville. -Pour les concerts Radio France, le théâtre des Champs-Élysées et la Salle -Pleyel, les places seront nominatives et à retirer au théâtre le soir de -la représentation au moins une demi-heure avant le début du spectacle. +Pour les concerts Radio France, le Théâtre des Champs-Élysées, le théâtre +du Rond-Point, le théâtre de la Colline, le théâtre de l'Athénée, l'IRCAM, +la Cité de la musique et le 104, le Studio-Théâtre de la Comédie +française, les places seront nominatives et à retirer au théâtre le soir +de la représentation au moins une demi-heure avant le début du spectacle. -Pour le théâtre de l'Odéon, la Comédie Française, le théâtre de la Ville, -le théâtre de Chaillot et l'IRCAM, les places seront distribuées dans vos -casiers environ une semaine avant la représentation (un mail vous en -avertira). +Pour le théâtre de l'Odéon, la salle Richelieu le théâtre du Vieux +colombier de la Comédie française, certains spectacles du théâtre de la +Ville et du théâtre de Châtelet ainsi que pour le théâtre de Chaillot, les +places seront distribuées environ une semaine avant la représentation (un +mail vous en avertira). -Culturellement vôtre, +Nous vous rappelons que l'obtention de places du BdA vous engage à +respecter les règles de fonctionnement : +http://www.cof.ens.fr/bda/?page_id=1370 +Le système de revente des places via les mails BdA-revente sera très +prochainement disponible, directement sur votre compte GestioCOF. +En vous souhaitant de très beaux spectacles tout au long de l'année, -- -Le BdA""" +Le Bureau des Arts +(Chloé, Emilie, Jaime, Maxime, Olivier) +""" attribs_text = "" name = member.user.get_full_name() for attrib in attribs: attribs_text += u"- 1 place pour %s\n" % attrib mail = mail % (name, attribs_text) - send_mail ("Places de spectacle (BdA du COF)", mail, - "bda@clipper.ens.fr", [member.user.email], + send_mail ("Résultats du tirage au sort", mail, + "bda@ens.fr", [member.user.email], fail_silently = True) count = len(queryset.all()) if count == 1: @@ -136,7 +154,13 @@ class ChoixSpectacleAdmin(admin.ModelAdmin): list_filter = ("double", "autoquit") search_fields = ('participant__user__username', 'participant__user__first_name', 'participant__user__last_name') -admin.site.register(Spectacle) +class SpectacleAdmin(admin.ModelAdmin): + model = Spectacle + list_display = ("title", "date", "location", "slots", "price") + list_filter = ("location",) + search_fields = ("title", "location__name") + +admin.site.register(Spectacle, SpectacleAdmin) admin.site.register(Salle) admin.site.register(Participant, ParticipantAdmin) admin.site.register(Attribution, AttributionAdmin) diff --git a/bda/algorithm.py b/bda/algorithm.py index 31a8a99a..623f9756 100644 --- a/bda/algorithm.py +++ b/bda/algorithm.py @@ -29,25 +29,24 @@ class Algorithm(object): self.ranks = {} self.origranks = {} self.choices = {} + next_rank = {} + member_shows = {} for member in members: - ranks = {} - member_choices = {} - member_shows = {} - #next_priority = 1 - next_rank = 1 - for choice in member.choixspectacle_set.order_by('priority').all(): - if choice.spectacle in member_shows: continue - else: member_shows[choice.spectacle] = True - #assert choice.priority == next_priority - #next_priority += 1 - showdict[choice.spectacle].requests.append(member) - showdict[choice.spectacle].nrequests += 2 if choice.double else 1 - ranks[choice.spectacle] = next_rank - next_rank += 2 if choice.double else 1 - member_choices[choice.spectacle] = choice - self.ranks[member] = ranks - self.choices[member] = member_choices - self.origranks[member] = dict(ranks) + self.ranks[member] = {} + self.choices[member] = {} + next_rank[member] = 1 + member_shows[member] = {} + for choice in choices: + member = choice.participant + if choice.spectacle in member_shows[member]: continue + else: member_shows[member][choice.spectacle] = True + showdict[choice.spectacle].requests.append(member) + showdict[choice.spectacle].nrequests += 2 if choice.double else 1 + self.ranks[member][choice.spectacle] = next_rank[member] + next_rank[member] += 2 if choice.double else 1 + self.choices[member][choice.spectacle] = choice + for member in members: + self.origranks[member] = dict(self.ranks[member]) def IncrementRanks(self, member, currank, increment = 1): for show in self.ranks[member]: diff --git a/bda/models.py b/bda/models.py index ca7cb6a5..69116474 100644 --- a/bda/models.py +++ b/bda/models.py @@ -1,5 +1,7 @@ # coding: utf-8 +import calendar + from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ @@ -29,11 +31,14 @@ class Spectacle (models.Model): def __repr__ (self): return u"[%s]" % self.__unicode__() + def timestamp(self): + return "%d" % calendar.timegm(self.date.utctimetuple()) + def date_no_seconds(self): return self.date.strftime('%d %b %Y %H:%M') def __unicode__ (self): - return u"%s - %s @ %s, %.02f€" % (self.title, self.date_no_seconds(), self.location, self.price) + return u"%s - %s, %s, %.02f€" % (self.title, self.date_no_seconds(), self.location, self.price) PAYMENT_TYPES = ( ("cash",u"Cash"), diff --git a/bda/views.py b/bda/views.py index 367ae765..50e10bd8 100644 --- a/bda/views.py +++ b/bda/views.py @@ -1,17 +1,22 @@ # coding: utf-8 +from django.contrib.auth.models import User from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required from django.db import models from django.http import Http404 from django import forms from django.forms.models import inlineformset_factory, BaseInlineFormSet +from django.core import serializers +import hashlib from django.core.mail import send_mail +from datetime import datetime import time from gestioncof.decorators import cof_required, buro_required +from gestioncof.shared import send_custom_mail from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution from bda.algorithm import Algorithm @@ -42,47 +47,93 @@ def etat_places(request): total = 0 for spectacle in spectacles: spectacle.total = 0 + spectacle.ratio = -1.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/float(spectacles_dict[spectacle["spectacle"]].slots) total += spectacle["total"] for spectacle in spectacles2: spectacles_dict[spectacle["spectacle"]].total += spectacle["total"] + spectacles_dict[spectacle["spectacle"]].ratio = spectacles_dict[spectacle["spectacle"]].total/float(spectacles_dict[spectacle["spectacle"]].slots) total += spectacle["total"] return render(request, "etat-places.html", {"spectacles": spectacles, "total": total}) +def _hash_queryset(queryset): + data = serializers.serialize("json", queryset) + hasher = hashlib.sha256() + hasher.update(data) + return hasher.hexdigest() + +@cof_required +def places(request): + participant, created = Participant.objects.get_or_create(user = request.user) + 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, + "total": total, + "warning": warning}) + @cof_required def inscription(request): - if False and time.time() > 1349474400: - return render(request, "error.html", {"error_title": "C'est fini !", "error_description": u"Tirage au sort le 6 octobre dans la soirée "}) + if datetime.now() > datetime(2013, 10, 6, 23, 59): + participant, created = Participant.objects.get_or_create(user = request.user) + choices = participant.choixspectacle_set.order_by("priority").all() + return render(request, "resume_inscription.html", {"error_title": "C'est fini !", "error_description": u"Tirage au sort le 7 octobre !", "choices": choices}) BdaFormSet = inlineformset_factory(Participant, ChoixSpectacle, fields = ("spectacle","double","autoquit","priority",), formset = BaseBdaFormSet) participant, created = Participant.objects.get_or_create(user = request.user) success = False + stateerror = False if request.method == "POST": - formset = BdaFormSet(request.POST, instance = participant) - if formset.is_valid(): - #ChoixSpectacle.objects.filter(participant = participant).delete() - formset.save() - success = True + 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(): + #ChoixSpectacle.objects.filter(participant = participant).delete() + 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}) - -Spectacle.deficit = lambda x: (x.slots-x.nrequests)*x.price + return render(request, "inscription-bda.html", {"formset": formset, "success": success, "total_price": total_price, "dbstate": dbstate, "stateerror": stateerror}) def do_tirage(request): form = TokenForm(request.POST) if not form.is_valid(): return tirage(request) + start = time.time() data = {} - shows = Spectacle.objects.all() + shows = Spectacle.objects.select_related().all() members = Participant.objects.all() - choices = ChoixSpectacle.objects.all() + choices = ChoixSpectacle.objects.order_by('participant', 'priority').select_related().all() algo = Algorithm(shows, members, choices) results = algo(form.cleaned_data["token"]) total_slots = 0 @@ -99,8 +150,8 @@ def do_tirage(request): total_sold = 0 total_deficit = 0 opera_deficit = 0 - for show in shows: - deficit = show.deficit() + for (show, members, _) in results: + deficit = (show.slots - len(members)) * show.price total_sold += show.slots * show.price if deficit >= 0: if u"Opéra" in show.location.name: @@ -109,18 +160,23 @@ def do_tirage(request): 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 = {} + members_uniq = {} # Participant objects are not shared accross spectacle results, + # So assign a single object for each Participant id for (show, members, _) in results: for (member, _, _, _) in members: - if member not in members2: + 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) - if False and request.user.username == "seguin": + if False and request.user.username in ["seguin", "harazi"]: Attribution.objects.all().delete() for (show, members, _) in results: for (member, _, _, _) in members: @@ -159,15 +215,27 @@ def do_resell(request, form): spectacle = form.cleaned_data["spectacle"] count = form.cleaned_data["count"] places = "2 places" if count == "2" else "une place" + """ + send_custom_mail("bda-revente@lists.ens.fr", + "bda-revente", + {"places": places, + "spectacle": spectacle.title, + "date": spectacle.date_no_seconds(), + "lieu": spectacle.location, + "prix": spectacle.price, + "revendeur": request.user.get_full_name(), + "revendeur_mail": request.user.email}, + from_email = request.user.email) + """ mail = u"""Bonjour, Je souhaite revendre %s pour %s le %s (%s) à %.02f€. -Contactez moi par email si vous êtes intéressés ! +Contactez moi par email si vous êtes intéressé·e·s ! %s (%s)""" % (places, spectacle.title, spectacle.date_no_seconds(), spectacle.location, spectacle.price, request.user.get_full_name(), request.user.email) - send_mail("Revente de place: %s" % spectacle, mail, + send_mail("%s" % spectacle, mail, request.user.email, ["bda-revente@lists.ens.fr"], - fail_silently = True) + fail_silently = False) return render(request, "bda-success.html", {"show": spectacle, "places": places}) @login_required diff --git a/gestioncof/admin.py b/gestioncof/admin.py index b161f6de..0b8b534d 100644 --- a/gestioncof/admin.py +++ b/gestioncof/admin.py @@ -55,6 +55,9 @@ class EventOptionChoiceInline(admin.TabularInline): class EventOptionInline(admin.TabularInline): model = EventOption +class EventCommentFieldInline(admin.TabularInline): + model = EventCommentField + class EventOptionAdmin(admin.ModelAdmin): inlines = [ EventOptionChoiceInline, @@ -63,6 +66,7 @@ class EventOptionAdmin(admin.ModelAdmin): class EventAdmin(admin.ModelAdmin): inlines = [ EventOptionInline, + EventCommentFieldInline, ] #from eav.forms import BaseDynamicEntityForm @@ -253,6 +257,8 @@ admin.site.register(EventOption, EventOptionAdmin) admin.site.unregister(User) admin.site.register(User, UserProfileAdmin) admin.site.register(CofProfile) +admin.site.register(Club) +admin.site.register(CustomMail) admin.site.register(PetitCoursSubject) admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin) admin.site.register(PetitCoursAttribution, PetitCoursAttributionAdmin) diff --git a/gestioncof/models.py b/gestioncof/models.py index 92eaaad2..598b7cbc 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -24,6 +24,11 @@ TYPE_COTIZ_CHOICES = ( ('exterieur', _(u"Extérieur")), ) +TYPE_COMMENT_FIELD = ( + ('text', _(u"Texte long")), + ('char', _(u"Texte court")), +) + def choices_length (choices): return reduce (lambda m, choice: max (m, len (choice[0])), choices, 0) @@ -45,6 +50,7 @@ class CofProfile(models.Model): mailing_cof = models.BooleanField("Recevoir les mails COF", default = False) mailing_bda = models.BooleanField("Recevoir les mails BdA", default = False) mailing_bda_revente = models.BooleanField("Recevoir les mails de revente de places BdA", default = False) + comments = models.TextField("Commentaires visibles uniquement par le Buro", blank = True) is_buro = models.BooleanField("Membre du Burô", default = False) petits_cours_accept = models.BooleanField("Recevoir des petits cours", default = False) petits_cours_remarques = models.TextField(_(u"Remarques et précisions pour les petits cours"), @@ -62,6 +68,24 @@ def create_user_profile(sender, instance, created, **kwargs): CofProfile.objects.get_or_create(user = instance) post_save.connect(create_user_profile, sender = User) +class Club(models.Model): + name = models.CharField("Nom", max_length = 200) + description = models.TextField("Description") + respos = models.ManyToManyField(User, related_name = "clubs_geres") + membres = models.ManyToManyField(User, related_name = "clubs") + +class CustomMail(models.Model): + shortname = models.SlugField(max_length = 50, blank = False) + title = models.CharField("Titre", max_length = 200, blank = False) + content = models.TextField("Contenu", blank = False) + comments = models.TextField("Informations contextuelles sur le mail", blank = True) + + class Meta: + verbose_name = "Mails personnalisables" + + def __unicode__(self): + return u"%s: %s" % (self.shortname, self.title) + class Event(models.Model): title = models.CharField("Titre", max_length = 200) location = models.CharField("Lieu", max_length = 200) @@ -77,6 +101,23 @@ class Event(models.Model): def __unicode__(self): return unicode(self.title) +class EventCommentField(models.Model): + event = models.ForeignKey(Event, related_name = "commentfields") + name = models.CharField("Champ", max_length = 200) + fieldtype = models.CharField("Type", max_length = 10, choices = TYPE_COMMENT_FIELD, default = "text") + default = models.TextField("Valeur par défaut", blank = True) + + class Meta: + verbose_name = "Champ" + + def __unicode__(self): + return unicode(self.name) + +class EventCommentValue(models.Model): + commentfield = models.ForeignKey(EventCommentField, related_name = "values") + registration = models.ForeignKey("EventRegistration", related_name = "comments") + content = models.TextField("Contenu", blank = True, null = True) + class EventOption(models.Model): event = models.ForeignKey(Event, related_name = "options") name = models.CharField("Option", max_length = 200) @@ -102,6 +143,7 @@ class EventRegistration(models.Model): user = models.ForeignKey(User) event = models.ForeignKey(Event) options = models.ManyToManyField(EventOptionChoice) + filledcomments = models.ManyToManyField(EventCommentField, through = EventCommentValue) paid = models.BooleanField("A payé", default = False) class Meta: diff --git a/gestioncof/petits_cours_views.py b/gestioncof/petits_cours_views.py index 2e907058..0b9ac048 100644 --- a/gestioncof/petits_cours_views.py +++ b/gestioncof/petits_cours_views.py @@ -60,38 +60,45 @@ def _get_attrib_counter(user, matiere): counter.save() return counter - - -@buro_required -def traitement(request, demande_id): - demande = get_object_or_404(PetitCoursDemande, id = demande_id) - if demande.niveau == "other": - return _traitement_other(request, demande) - if request.method == "POST": - return _traitement_post(request, demande) - proposals = {} - proposed_for = {} - unsatisfied = [] - attribdata = {} +def _get_demande_candidates(demande, redo = False): for matiere in demande.matieres.all(): candidates = PetitCoursAbility.objects.filter(matiere = matiere, niveau = demande.niveau) candidates = candidates.filter(user__profile__is_cof = True, user__profile__petits_cours_accept = True) if demande.agrege_requis: candidates = candidates.filter(agrege = True) - candidates = candidates.values_list('user', flat = True).distinct().order_by('?').all() - tuples = [] - for candidate in candidates: - user = User.objects.get(pk = candidate) - tuples.append((candidate, _get_attrib_counter(user, matiere))) - if tuples: + if redo: + attributions = PetitCoursAttribution.objects.filter(demande = demande, + matiere = matiere).all() + for attrib in attributions: + candidates = candidates.exclude(user = attrib.user) + candidates = candidates.order_by('?').select_related().all() + yield (matiere, candidates) + +@buro_required +def traitement(request, demande_id, redo = False): + demande = get_object_or_404(PetitCoursDemande, id = demande_id) + if demande.niveau == "other": + return _traitement_other(request, demande, redo) + if request.method == "POST": + return _traitement_post(request, demande) + proposals = {} + proposed_for = {} + unsatisfied = [] + attribdata = {} + for matiere, candidates in _get_demande_candidates(demande, redo): + if candidates: + tuples = [] + for candidate in candidates: + user = candidate.user + tuples.append((candidate, _get_attrib_counter(user, matiere))) tuples = sorted(tuples, key = lambda c: c[1].count) candidates, _ = zip(*tuples) - attribdata[matiere.id] = [] candidates = candidates[0:min(3, len(candidates))] + attribdata[matiere.id] = [] proposals[matiere] = [] for candidate in candidates: - user = User.objects.get(pk = candidate) + user = candidate.user proposals[matiere].append(user) attribdata[matiere.id].append(user.id) if user not in proposed_for: @@ -100,10 +107,15 @@ def traitement(request, demande_id): proposed_for[user].append(matiere) else: unsatisfied.append(matiere) - return _finalize_traitement(request, demande, proposals, proposed_for, unsatisfied, attribdata) + return _finalize_traitement(request, demande, proposals, + proposed_for, unsatisfied, attribdata, redo) + +@buro_required +def retraitement(request, demande_id): + return traitement(request, demande_id, redo = True) def _finalize_traitement(request, demande, proposals, proposed_for, - unsatisfied, attribdata, redo = False): + unsatisfied, attribdata, redo = False, errors = None): proposals = proposals.items() proposed_for = proposed_for.items() attribdata = attribdata.items() @@ -121,6 +133,7 @@ def _finalize_traitement(request, demande, proposals, proposed_for, "mainmail": mainmail, "attribdata": base64.b64encode(simplejson.dumps(attribdata)), "redo": redo, + "errors": errors, }) def _generate_eleve_email(demande, proposed_for): @@ -130,15 +143,45 @@ def _generate_eleve_email(demande, proposed_for): proposed_mails.append((user, msg)) return proposed_mails -def _traitement_other_post(request, demande): - for matiere in demande.matieres.all(): - for choice_id in range(3): - choice = request.POST["proposal-%d-%d"] % (matiere.id, choice_id)]: - pass - return None - return _finalize_traitement(request, demande, proposals, proposed_for, unsatisfied, attribdata) +def _traitement_other_preparing(request, demande): + redo = "redo" in request.POST + unsatisfied = [] + proposals = {} + proposed_for = {} + attribdata = {} + errors = [] + for matiere, candidates in _get_demande_candidates(demande, redo): + if candidates: + candidates = dict([(candidate.user.id, candidate.user) + for candidate in candidates]) + attribdata[matiere.id] = [] + proposals[matiere] = [] + for choice_id in range(min(3, len(candidates))): + choice = int(request.POST["proposal-%d-%d" % (matiere.id, choice_id)]) + if choice == -1: + continue + if choice not in candidates: + errors.append(u"Choix invalide pour la proposition %d en %s" % (choice_id + 1, matiere)) + continue + user = candidates[choice] + if user in proposals[matiere]: + errors.append(u"La proposition %d en %s est un doublon" % (choice_id + 1, matiere)) + continue + proposals[matiere].append(user) + attribdata[matiere.id].append(user.id) + if user not in proposed_for: + proposed_for[user] = [matiere] + else: + proposed_for[user].append(matiere) + if not proposals[matiere]: + errors.append(u"Aucune proposition pour %s" % (matiere,)) + elif len(proposals[matiere]) < 3: + errors.append(u"Seulement %d proposition%s pour %s" % (len(proposals[matiere]), "s" if len(proposals[matiere]) > 1 else "", matiere)) + else: + unsatisfied.append(matiere) + return _finalize_traitement(request, demande, proposals, proposed_for, unsatisfied, attribdata, errors = errors) -def _traitement_other(request, demande): +def _traitement_other(request, demande, redo): if request.method == "POST": if "preparing" in request.POST: return _traitement_other_preparing(request, demande) @@ -148,13 +191,7 @@ def _traitement_other(request, demande): proposed_for = {} unsatisfied = [] attribdata = {} - for matiere in demande.matieres.all(): - candidates = PetitCoursAbility.objects.filter(matiere = matiere, niveau = demande.niveau) - candidates = candidates.filter(user__profile__is_cof = True, - user__profile__petits_cours_accept = True) - if demande.agrege_requis: - candidates = candidates.filter(agrege = True) - candidates = candidates.order_by('?').select_related().all() + for matiere, candidates in _get_demande_candidates(demande, redo): if candidates: tuples = [] for candidate in candidates: @@ -243,49 +280,6 @@ def _traitement_post(request, demande): "redo": redo, }) -@buro_required -def retraitement(request, demande_id): - demande = get_object_or_404(PetitCoursDemande, id = demande_id) - if request.method == "POST": - return _traitement_post(request, demande) - if demande.niveau == "other": - return _traitement_other(request, demande) - proposals = {} - proposed_for = {} - unsatisfied = [] - attribdata = {} - for matiere in demande.matieres.all(): - candidates = PetitCoursAbility.objects.filter(matiere = matiere, niveau = demande.niveau) - candidates = candidates.filter(user__profile__is_cof = True, - user__profile__petits_cours_accept = True) - if demande.agrege_requis: - candidates = candidates.filter(agrege = True) - attributions = PetitCoursAttribution.objects.filter(demande = demande, matiere = matiere).all() - for attrib in attributions: - candidates = candidates.exclude(user = attrib.user) - candidates = candidates.values_list('user', flat = True).distinct().order_by('?').all() - tuples = [] - for candidate in candidates: - user = User.objects.get(pk = candidate) - tuples.append((candidate, _get_attrib_counter(user, matiere))) - if tuples: - tuples = sorted(tuples, key = lambda c: c[1].count) - candidates, _ = zip(*tuples) - attribdata[matiere.id] = [] - candidates = candidates[0:min(3, len(candidates))] - proposals[matiere] = [] - for candidate in candidates: - user = User.objects.get(pk = candidate) - proposals[matiere].append(user) - attribdata[matiere.id].append(user.id) - if user not in proposed_for: - proposed_for[user] = [matiere] - else: - proposed_for[user].append(matiere) - else: - unsatisfied.append(matiere) - return _finalize_traitement(request, demande, proposals, proposed_for, unsatisfied, attribdata, redo = True) - class BaseMatieresFormSet(BaseInlineFormSet): def clean(self): super(BaseMatieresFormSet, self).clean() diff --git a/gestioncof/shared.py b/gestioncof/shared.py index 0b9cd462..fe8d9061 100644 --- a/gestioncof/shared.py +++ b/gestioncof/shared.py @@ -2,9 +2,12 @@ from django.contrib.sites.models import Site from django.conf import settings from django_cas.backends import CASBackend, _verify as CASverify from django_cas.models import User +from django.contrib.auth.models import User as DjangoUser from django.db import models, connection +from django.core.mail import send_mail +from django.template import Template, Context -from gestioncof.models import CofProfile +from gestioncof.models import CofProfile, CustomMail class COFCASBackend(CASBackend): def authenticate_cas(self, ticket, service, request): @@ -76,3 +79,16 @@ def unlock_tables(*models): return row unlock_table = unlock_tables + +def send_custom_mail(to, shortname, context = None, from_email = "cof@ens.fr"): + if context is None: context = {} + if isinstance(to, DjangoUser): + context["nom"] = to.get_full_name() + context["prenom"] = to.first_name + to = to.email + mail = CustomMail.objects.get(shortname = shortname) + template = Template(mail.content) + message = template.render(Context(context)) + send_mail (mail.title, message, + from_email, [to], + fail_silently = True) diff --git a/gestioncof/views.py b/gestioncof/views.py index cb38268f..9473bc53 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -15,10 +15,11 @@ from django.contrib.auth.views import login as django_login_view from gestioncof.models import Survey, SurveyQuestion, SurveyQuestionAnswer, SurveyAnswer from gestioncof.models import Event, EventOption, EventOptionChoice, EventRegistration +from gestioncof.models import EventCommentField, EventCommentValue from gestioncof.models import CofProfile, Clipper from gestioncof.decorators import buro_required, cof_required from gestioncof.widgets import TriStateCheckbox -from gestioncof.shared import lock_table, unlock_table +from gestioncof.shared import lock_table, unlock_table, send_custom_mail @login_required def home(request): @@ -205,6 +206,17 @@ def get_event_form_choices(event, form): all_choices.append(choice) return all_choices +def update_event_form_comments(event, form, registration): + for commentfield_id, value in form.comments(): + field = get_object_or_404(EventCommentField, id = commentfield_id, + event = event) + if value == field.default: + continue + (storage, _) = EventCommentValue.objects.get_or_create(commentfield = field, + registration = registration) + storage.content = value + storage.save() + @login_required def event(request, event_id): event = get_object_or_404(Event, id = event_id) @@ -426,6 +438,7 @@ class RegistrationProfileForm(forms.ModelForm): 'mailing_cof', 'mailing_bda', 'mailing_bda_revente', + 'comments' ] def save(self, *args, **kw): @@ -445,7 +458,7 @@ class RegistrationProfileForm(forms.ModelForm): class Meta: model = CofProfile - fields = ("login_clipper", "num", "phone", "occupation", "departement", "is_cof", "type_cotiz", "mailing_cof", "mailing_bda", "mailing_bda_revente",) + fields = ("login_clipper", "num", "phone", "occupation", "departement", "is_cof", "type_cotiz", "mailing_cof", "mailing_bda", "mailing_bda_revente", "comments") def registration_set_ro_fields(user_form, profile_form): user_form.fields['username'].widget.attrs['readonly'] = True @@ -487,15 +500,16 @@ def registration_form(request, login_clipper = None, username = None): return render(request, "registration_form.html", {"user_form": user_form, "profile_form": profile_form, "member": member, "login_clipper": login_clipper}) STATUS_CHOICES = (('no','Non'), - ('wait','Attente paiement'), - ('paid','Payé'),) + ('wait','Oui mais attente paiement'), + ('paid','Oui payé'),) class AdminEventForm(forms.Form): status = forms.ChoiceField(label = "Inscription", choices = STATUS_CHOICES, widget = RadioSelect) def __init__(self, *args, **kwargs): event = kwargs.pop("event") self.event = event - current_choices = kwargs.pop("current_choices", None) + registration = kwargs.pop("current_registration", None) + current_choices = registration.options.all() if registration is not None else [] paid = kwargs.pop("paid", None) if paid == True: kwargs["initial"] = {"status":"paid"} @@ -505,12 +519,12 @@ class AdminEventForm(forms.Form): kwargs["initial"] = {"status":"no"} super(AdminEventForm, self).__init__(*args, **kwargs) choices = {} - if current_choices: - for choice in current_choices.all(): - if choice.event_option.id not in choices: - choices[choice.event_option.id] = [choice.id] - else: - choices[choice.event_option.id].append(choice.id) + comments = {} + for choice in current_choices: + if choice.event_option.id not in choices: + choices[choice.event_option.id] = [choice.id] + else: + choices[choice.event_option.id].append(choice.id) all_choices = choices for option in event.options.all(): choices = [(choice.id, choice.value) for choice in option.choices.all()] @@ -530,12 +544,31 @@ class AdminEventForm(forms.Form): initial = initial) field.option_id = option.id self.fields["option_%d" % option.id] = field + for commentfield in event.commentfields.all(): + initial = commentfield.default + if registration is not None: + try: + initial = registration.comments.get(commentfield = commentfield).content + except EventCommentValue.DoesNotExist: + pass + widget = forms.Textarea if commentfield.fieldtype == "text" else forms.TextInput + field = forms.CharField(label = commentfield.name, + widget = widget, + required = False, + initial = initial) + field.comment_id = commentfield.id + self.fields["comment_%d" % commentfield.id] = field def choices(self): for name, value in self.cleaned_data.items(): if name.startswith('option_'): yield (self.fields[name].option_id, value) + def comments(self): + for name, value in self.cleaned_data.items(): + if name.startswith('comment_'): + yield (self.fields[name].comment_id, value) + @buro_required def registration_form2(request, login_clipper = None, username = None): events = Event.objects.filter(old = False).all() @@ -571,7 +604,7 @@ def registration_form2(request, login_clipper = None, username = None): for event in events: try: current_registration = EventRegistration.objects.get(user = member, event = event) - form = AdminEventForm(event = event, current_choices = current_registration.options, paid = current_registration.paid) + form = AdminEventForm(event = event, current_registration = current_registration, paid = current_registration.paid) except EventRegistration.DoesNotExist: form = AdminEventForm(event = event) event_forms.append(form) @@ -618,10 +651,14 @@ def registration(request): if user_form.is_valid() and profile_form.is_valid() and not any([not form.is_valid() for form in event_forms]): member = user_form.save() (profile, _) = CofProfile.objects.get_or_create(user = member) + was_cof = profile.is_cof request_dict["num"] = profile.num profile_form = RegistrationProfileForm(request_dict, instance = profile) profile_form.is_valid() profile_form.save() + (profile, _) = CofProfile.objects.get_or_create(user = member) + if profile.is_cof and not was_cof: + send_custom_mail(member, "bienvenue") for form in event_forms: if form.cleaned_data['status'] == 'no': try: @@ -631,10 +668,18 @@ def registration(request): pass continue all_choices = get_event_form_choices(form.event, form) - (current_registration, _) = EventRegistration.objects.get_or_create(user = member, event = form.event) + (current_registration, created_reg) = EventRegistration.objects.get_or_create(user = member, event = form.event) + update_event_form_comments(event, form, current_registration) current_registration.options = all_choices current_registration.paid = (form.cleaned_data['status'] == 'paid') current_registration.save() + if event.title == "Mega 2014" and created_reg: + field = EventCommentField.objects.get(event = event, name = "Commentaires") + try: + comments = EventCommentValue.objects.get(commentfield = field, registration = current_registration).content + except EventCommentValue.DoesNotExist: + comments = field.default + send_custom_mail(member, "mega", {"remarques": comments}) success = True return render(request, "registration_post.html", {"success": success, "user_form": user_form, "profile_form": profile_form, "member": member, "login_clipper": login_clipper, "event_forms": event_forms}) else: @@ -653,54 +698,76 @@ def export_members(request): return response +def csv_export_mega(filename, qs): + response = HttpResponse(mimetype = 'text/csv') + response['Content-Disposition'] = 'attachment; filename=' + filename + writer = unicodecsv.UnicodeWriter(response) + + for reg in qs.all(): + user = reg.user + profile = user.get_profile() + comments = "---".join([comment.content for comment in reg.comments.all()]) + bits = [user.username, user.first_name, user.last_name, user.email, profile.phone, profile.num, profile.comments if profile.comments else "", comments] + writer.writerow([unicode(bit) for bit in bits]) + + return response + +@buro_required +def export_mega_remarksonly(request): + filename = 'remarques_mega_2014.csv' + response = HttpResponse(mimetype = 'text/csv') + response['Content-Disposition'] = 'attachment; filename=' + filename + writer = unicodecsv.UnicodeWriter(response) + + event = Event.objects.get(title = "Mega 2014") + commentfield = event.commentfields.get(name = "Commentaires") + for val in commentfield.values.all(): + reg = val.registration + user = reg.user + profile = user.get_profile() + bits = [user.username, user.first_name, user.last_name, user.email, profile.phone, profile.num, profile.comments, val.content] + writer.writerow([unicode(bit) for bit in bits]) + + return response + +@buro_required +def export_mega_bytype(request, type): + types = {"orga-actif": "Orga actif", + "orga-branleur": "Orga branleur", + "conscrit-eleve": "Conscrit élève", + "conscrit-etudiant": "Conscrit étudiant"} + + if type not in types: + raise Http404 + + event = Event.objects.get(title = "Mega 2014") + type_option = event.options.get(name = "Type") + participant_type = type_option.choices.get(value = types[type]).id + qs = EventRegistration.objects.filter(event = event).filter(options__id__exact = participant_type) + return csv_export_mega(type + '_mega_2014.csv', qs) + @buro_required def export_mega_orgas(request): - response = HttpResponse(mimetype = 'text/csv') - response['Content-Disposition'] = 'attachment; filename=orgas_mega.csv' - - writer = unicodecsv.UnicodeWriter(response) - event = Event.objects.get(title = "MEGA") + event = Event.objects.get(title = "Mega 2014") type_option = event.options.get(name = "Type") - participant_type = type_option.choices.get(value = "Participant").id - for reg in EventRegistration.objects.filter(event = event).exclude(options__id__exact = participant_type).all(): - user = reg.user - profile = user.get_profile() - bits = [user.username, user.first_name, user.last_name, user.email, profile.phone, profile.num] - writer.writerow([unicode(bit) for bit in bits]) + participant_type_a = type_option.choices.get(value = "Conscrit étudiant").id + participant_type_b = type_option.choices.get(value = "Conscrit élève").id + qs = EventRegistration.objects.filter(event = event).exclude(options__id__in = (participant_type_a, participant_type_b)) + return csv_export_mega('orgas_mega_2014.csv', qs) - return response - -@buro_required def export_mega_participants(request): - response = HttpResponse(mimetype = 'text/csv') - response['Content-Disposition'] = 'attachment; filename=participants_mega.csv' - - writer = unicodecsv.UnicodeWriter(response) - event = Event.objects.get(title = "MEGA") + event = Event.objects.get(title = "Mega 2014") type_option = event.options.get(name = "Type") - participant_type = type_option.choices.get(value = "Participant").id - for reg in EventRegistration.objects.filter(event = event).filter(options__id__exact = participant_type).all(): - user = reg.user - profile = user.get_profile() - bits = [user.username, user.first_name, user.last_name, user.email, profile.phone, profile.num] - writer.writerow([unicode(bit) for bit in bits]) - - return response + participant_type_a = type_option.choices.get(value = "Conscrit étudiant").id + participant_type_b = type_option.choices.get(value = "Conscrit élève").id + qs = EventRegistration.objects.filter(event = event).filter(options__id__in = (participant_type_a, participant_type_b)) + return csv_export_mega('participants_mega_2014.csv', qs) @buro_required def export_mega(request): - response = HttpResponse(mimetype = 'text/csv') - response['Content-Disposition'] = 'attachment; filename=all_mega.csv' - - writer = unicodecsv.UnicodeWriter(response) - event = Event.objects.filter(title = "MEGA") - for reg in EventRegistration.objects.filter(event = event).order_by("user__username").all(): - user = reg.user - profile = user.get_profile() - bits = [user.username, user.first_name, user.last_name, user.email, profile.phone, profile.num] - writer.writerow([unicode(bit) for bit in bits]) - - return response + event = Event.objects.filter(title = "Mega 2014") + qs = EventRegistration.objects.filter(event = event).order_by("user__username") + return csv_export_mega('all_mega_2014.csv', qs) @buro_required def utile_cof(request): diff --git a/manage.py b/manage.py old mode 100755 new mode 100644 diff --git a/media/cof.css b/media/cof.css index 609c1ca9..a2d12fb1 100644 --- a/media/cof.css +++ b/media/cof.css @@ -176,8 +176,8 @@ fieldset legend { } #main-container { - max-width: 90%; - width: 800px; + max-width: 95%; + width: 1000px; margin: 7em auto; display: block; } @@ -588,3 +588,47 @@ pre code { max-height: 340px; overflow-y: scroll; } + + +/* Louis pour etat places*/ + + +.etat-bda td { + border:1px solid #666; + padding:4px; +} + +.etat-bda tr:nth-child(even) {background: #CCC} + +.greenratio { + background-color: #3F3; + border: 5px solid #ccc; + border: 5px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.orangeratio { + background-color: #FF3; + border: 5px solid #ccc; + border: 5px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.redratio { + background-color: #F33; + border: 5px solid #ccc; + border: 5px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +th[data-sort]{ + cursor:pointer; +} + +tr.awesome{ + color: red; +} \ No newline at end of file diff --git a/templates/bda/bda-attrib.html b/templates/bda/bda-attrib.html index 959016d1..0946a4a3 100644 --- a/templates/bda/bda-attrib.html +++ b/templates/bda/bda-attrib.html @@ -10,6 +10,7 @@

Token : {{ token }}

Placés : {{ total_slots }} ; Déçus : {{ total_losers }}

{% if user.get_profile.is_buro %}

Déficit total: {{ total_deficit }} €, Opéra: {{ opera_deficit }} €, Attribué: {{ total_sold }} €

{% endif %} +

Temps de calcul : {{ duration|floatformat }}s

{% for show, members, losers in results %}
diff --git a/templates/bda/etat-places.html b/templates/bda/etat-places.html index 086ca648..e726cde6 100644 --- a/templates/bda/etat-places.html +++ b/templates/bda/etat-places.html @@ -1,11 +1,49 @@ {% extends "base_title.html" %} {% block realcontent %} -

Etat des inscriptions BDA

- {% endif %} + + {% if user.profile.is_cof %}

BdA

- + {% endif %} +

Divers