From 9c989d886b72c3b57c4f6c39a33e6d1b59b29f51 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 5 Sep 2013 22:20:52 +0200 Subject: [PATCH] Update while working on Django 1.5 upgrade --- __init__.py | 0 apache/django.wsgi | 2 +- bda/admin.py | 133 ++++++- bda/algorithm.py | 59 ++-- bda/models.py | 41 ++- bda/views.py | 161 ++++++++- gestioncof/admin.py | 165 ++++++++- gestioncof/decorators.py | 6 +- gestioncof/models.py | 36 +- gestioncof/shared.py | 21 +- gestioncof/templatetags/utils.py | 26 +- gestioncof/views.py | 442 +++++++++++++++++++++--- manage.py | 18 +- media/cof.css | 50 +++ settings.py | 172 --------- templates/bda/inscription-bda.html | 5 +- templates/gestioncof/base.html | 2 +- templates/gestioncof/base_title.html | 4 +- templates/gestioncof/event_status.html | 4 + templates/gestioncof/home.html | 49 ++- templates/gestioncof/login.html | 2 +- templates/gestioncof/login_switch.html | 2 +- templates/gestioncof/registration.html | 40 ++- templates/gestioncof/survey_status.html | 5 +- urls.py | 24 -- 25 files changed, 1112 insertions(+), 357 deletions(-) delete mode 100644 __init__.py mode change 100644 => 100755 manage.py delete mode 100644 settings.py delete mode 100644 urls.py diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apache/django.wsgi b/apache/django.wsgi index 9e0290f7..7b9c271d 100755 --- a/apache/django.wsgi +++ b/apache/django.wsgi @@ -1,6 +1,6 @@ import os, sys sys.path.append (os.path.expanduser ('~gestion/www')) -os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' +os.environ['DJANGO_SETTINGS_MODULE'] = 'cof.settings' import django.core.handlers.wsgi diff --git a/bda/admin.py b/bda/admin.py index 05f500fa..c934e857 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -1,14 +1,143 @@ # coding: utf-8 +from django.core.mail import send_mail +from django.contrib.contenttypes.models import ContentType + from django.contrib import admin -from bda.models import Spectacle, Participant, ChoixSpectacle +from django.db.models import Sum +from bda.models import Spectacle, Salle, Participant, ChoixSpectacle, Attribution class ChoixSpectacleInline(admin.TabularInline): model = ChoixSpectacle sortable_field_name = "priority" +class AttributionInline(admin.TabularInline): + model = Attribution + class ParticipantAdmin(admin.ModelAdmin): - inlines = [ChoixSpectacleInline] + #inlines = [ChoixSpectacleInline] + inlines = [AttributionInline] + def nb_places(self, obj): + return len(obj.attribution_set.all()) + nb_places.short_description = "Nombre de places" + def total(self, obj): + tot = obj.attributions.aggregate(total = Sum('price'))['total'] + if tot: return u"%.02f €" % tot + else: return u"0 €" + total.short_description = "Total à payer" + list_display = ("user", "nb_places", "total", "paid", "paymenttype") + list_filter = ("paid",) + search_fields = ('user__username', 'user__first_name', 'user__last_name') + actions = ['send_attribs',] + actions_on_bottom = True + list_per_page = 400 + + def send_choices(self, request, queryset): + for member in queryset.all(): + choices = member.choixspectacle_set.order_by('priority').all() + if len(choices) == 0: + continue + mail = u"""Cher(e) %s, +Voici tes choix de spectacles tels que notre système les a enregistrés :\n\n""" % member.user.get_full_name() + next_rank = 1 + member_shows = {} + for choice in choices: + if choice.spectacle in member_shows: continue + else: member_shows[choice.spectacle] = True + extra = "" + if choice.double: + extra += u" ; deux places" + if choice.autoquit: + extra += u" ; désistement automatique" + mail += u"- Choix %d : %s%s\n" % (next_rank, choice.spectacle, extra) + next_rank += 1 + mail += u"""\nSi cette liste est incorrecte, merci de nous contacter au plus vite (avant samedi 6 octobre 18h). + +Artistiquement, +Le BdA""" + send_mail ("Choix de spectacles (BdA du COF)", mail, + "bda@clipper.ens.fr", [member.user.email], + fail_silently = True) + count = len(queryset.all()) + if count == 1: + message_bit = u"1 membre a" + plural = "" + else: + message_bit = u"%d membres ont" % count + plural = "s" + self.message_user(request, u"%s été informé%s avec succès." % (message_bit, plural)) + send_choices.short_description = u"Envoyer les choix par mail" + + def send_attribs(self, request, queryset): + for member in queryset.all(): + attribs = member.attributions.all() + if len(attribs) == 0: + continue + mail = u"""Cher(e) %s, + +Tu t'es inscrit(e) pour le tirage au sort du BdA. Tu as été sélectionné(e) +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). + +*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. + +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 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). + +Culturellement vôtre, + +-- +Le BdA""" + 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], + fail_silently = True) + count = len(queryset.all()) + if count == 1: + message_bit = u"1 membre a" + plural = "" + else: + message_bit = u"%d membres ont" % count + plural = "s" + self.message_user(request, u"%s été informé%s avec succès." % (message_bit, plural)) + send_attribs.short_description = u"Envoyer les résultats par mail" + +class AttributionAdmin(admin.ModelAdmin): + def paid(self, obj): + return obj.participant.paid + paid.short_description = 'A payé' + paid.boolean = True + list_display = ("id", "spectacle", "participant", "given", "paid") + search_fields = ('spectacle__title', 'participant__user__username', 'participant__user__first_name', 'participant__user__last_name') + +import autocomplete_light +class ChoixSpectacleAdmin(admin.ModelAdmin): + form = autocomplete_light.modelform_factory(ChoixSpectacle) + list_display = ("participant", "spectacle", "priority", "double", "autoquit") + list_filter = ("double", "autoquit") + search_fields = ('participant__user__username', 'participant__user__first_name', 'participant__user__last_name') admin.site.register(Spectacle) +admin.site.register(Salle) admin.site.register(Participant, ParticipantAdmin) +admin.site.register(Attribution, AttributionAdmin) +admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin) diff --git a/bda/algorithm.py b/bda/algorithm.py index f3d2dce2..31a8a99a 100644 --- a/bda/algorithm.py +++ b/bda/algorithm.py @@ -1,6 +1,8 @@ # coding: utf-8 from django.conf import settings +from django.db.models import Max + import random class Algorithm(object): @@ -10,37 +12,41 @@ class Algorithm(object): origranks = None double = None - def __init__(self, shows, members): + def __init__(self, shows, members, choices): """Initialisation : - on aggrège toutes les demandes pour chaque spectacle dans show.requests - on crée des tables de demandes pour chaque personne, afin de pouvoir modifier les rankings""" + self.max_group = 2 * choices.aggregate(Max('priority'))['priority__max'] self.shows = [] showdict = {} for show in shows: + show.nrequests = 0 showdict[show] = show show.requests = [] self.shows.append(show) self.ranks = {} self.origranks = {} - self.double = {} + self.choices = {} for member in members: ranks = {} - double = {} - for i in range(1, settings.NUM_CHOICES + 1): - choice = getattr(member, "choice%d" % i) - if not choice: - continue - # Noter les doubles demandes - if choice in double: - double[choice] = True - else: - showdict[choice].requests.append(member) - ranks[choice] = i - double[choice] = False + 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.double[member] = double + self.choices[member] = member_choices self.origranks[member] = dict(ranks) def IncrementRanks(self, member, currank, increment = 1): @@ -52,30 +58,37 @@ class Algorithm(object): l.append((member, self.ranks[member][show], self.origranks[member][show], - self.double[member][show])) + self.choices[member][show].double)) + + """ + Pour les 2 Walkyries: c'est Sandefer + + Pour les 4 places pour l'Oratorio c'est Maxence Arutkin + """ def __call__(self, seed): random.seed(seed) results = [] - for show in self.shows: + shows = sorted(self.shows, key = lambda x: float(x.nrequests) / x.slots, reverse = True) + for show in shows: # On regroupe tous les gens ayant le même rang - groups = {} - for i in range(1, settings.NUM_CHOICES + 1): - groups[i] = [] + groups = dict([(i, []) for i in range(1, self.max_group + 1)]) for member in show.requests: + if self.ranks[member][show] == 0: + raise RuntimeError, (member, show.title) groups[self.ranks[member][show]].append(member) # On passe à l'attribution winners = [] losers = [] - for i in range(1, settings.NUM_CHOICES + 1): + for i in range(1, self.max_group + 1): group = list(groups[i]) random.shuffle(group) for member in group: - if self.double[member][show]: # double + if self.choices[member][show].double: # double if len(winners) + 1 < show.slots: self.appendResult(winners, member, show) self.appendResult(winners, member, show) - elif not member.autoquit and len(winners) < show.slots: + elif not self.choices[member][show].autoquit and len(winners) < show.slots: self.appendResult(winners, member, show) self.appendResult(losers, member, show) else: diff --git a/bda/models.py b/bda/models.py index c2480a33..b91da5ef 100644 --- a/bda/models.py +++ b/bda/models.py @@ -5,13 +5,20 @@ from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ from django.db.models.signals import post_save +class Salle (models.Model): + name = models.CharField ("Nom", max_length = 300) + address = models.TextField ("Adresse") + + def __unicode__ (self): + return self.name + class Spectacle (models.Model): title = models.CharField ("Titre", max_length = 300) date = models.DateTimeField ("Date & heure") - location = models.CharField ("Lieu", max_length = 300, - blank = True, null = True) + location = models.ForeignKey(Salle) description = models.TextField ("Description", blank = True) - slots_description = models.TextField ("Description des places", blank = True) + #slots_description = models.TextField ("Description des places", blank = True) + price = models.FloatField("Prix d'une place", blank = True) slots = models.IntegerField ("Places") priority = models.IntegerField ("Priorité", default = 1000) @@ -22,12 +29,25 @@ class Spectacle (models.Model): def __repr__ (self): return u"[%s]" % self.__unicode__() + def date_no_seconds(self): + return self.date.strftime('%d %b %Y %H:%M') + def __unicode__ (self): - return u"%s - %s @ %s" % (self.title, self.date, self.location) + return u"%s - %s @ %s, %.02f€" % (self.title, self.date_no_seconds(), self.location, self.price) + +PAYMENT_TYPES = ( +("cash",u"Cash"), +("cb","CB"), +("cheque",u"Chèque"), +("autre",u"Autre"), +) class Participant (models.Model): user = models.ForeignKey(User, unique = True) - choices = models.ManyToManyField(Spectacle, through = "ChoixSpectacle") + choices = models.ManyToManyField(Spectacle, through = "ChoixSpectacle", related_name = "chosen_by") + attributions = models.ManyToManyField(Spectacle, through = "Attribution", related_name = "attributed_to") + paid = models.BooleanField (u"A payé", default = False) + paymenttype = models.CharField(u"Moyen de paiement", max_length = 6, choices = PAYMENT_TYPES, blank = True) def __unicode__ (self): return u"%s" % (self.user) @@ -40,5 +60,14 @@ class ChoixSpectacle (models.Model): autoquit = models.BooleanField("Abandon2") class Meta: ordering = ("priority",) - #unique_together = (("participant", "spectacle",),) + unique_together = (("participant", "spectacle",),) verbose_name = "voeu" + verbose_name_plural = "voeux" + +class Attribution (models.Model): + participant = models.ForeignKey(Participant) + spectacle = models.ForeignKey(Spectacle, related_name = "attribues") + given = models.BooleanField(u"Donnée", default = False) + + def __unicode__ (self): + return u"%s -- %s" % (self.participant, self.spectacle) diff --git a/bda/views.py b/bda/views.py index 5b8dda1a..d99a3abe 100644 --- a/bda/views.py +++ b/bda/views.py @@ -1,15 +1,24 @@ # coding: utf-8 +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 gestioncof.shared import render_page -from bda.models import Spectacle, Participant, ChoixSpectacle +from django.core.mail import send_mail + +import time + +from gestioncof.decorators import cof_required, buro_required +from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution +from bda.algorithm import Algorithm class BaseBdaFormSet(BaseInlineFormSet): def clean(self): """Checks that no two articles have the same title.""" + super(BaseBdaFormSet, self).clean() if any(self.errors): # Don't bother validating the formset unless each form is valid on its own return @@ -24,17 +33,161 @@ class BaseBdaFormSet(BaseInlineFormSet): raise forms.ValidationError("Vous ne pouvez pas vous inscrire deux fois pour le même spectacle.") spectacles.append(spectacle) -@login_required +@buro_required +def etat_places(request): + spectacles1 = ChoixSpectacle.objects.all().values('spectacle','spectacle__title').annotate(total = models.Count('spectacle')) + spectacles2 = ChoixSpectacle.objects.filter(double = True).all().values('spectacle','spectacle__title').annotate(total = models.Count('spectacle')) + spectacles = Spectacle.objects.all() + spectacles_dict = {} + total = 0 + for spectacle in spectacles: + spectacle.total = 0 + spectacles_dict[spectacle.id] = spectacle + for spectacle in spectacles1: + spectacles_dict[spectacle["spectacle"]].total += spectacle["total"] + total += spectacle["total"] + for spectacle in spectacles2: + spectacles_dict[spectacle["spectacle"]].total += spectacle["total"] + total += spectacle["total"] + return render(request, "etat-places.html", {"spectacles": spectacles, "total": total}) + +@cof_required def inscription(request): + if 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 "}) BdaFormSet = inlineformset_factory(Participant, ChoixSpectacle, fields = ("spectacle","double","autoquit","priority",), formset = BaseBdaFormSet) participant, created = Participant.objects.get_or_create(user = request.user) success = 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 formset = BdaFormSet(instance = participant) else: formset = BdaFormSet(instance = participant) - return render_page(request, {"formset": formset, "success": success}, "inscription-bda.html") + 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 + +def do_tirage(request): + form = TokenForm(request.POST) + if not form.is_valid(): + return tirage(request) + data = {} + shows = Spectacle.objects.all() + members = Participant.objects.all() + choices = ChoixSpectacle.objects.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 in shows: + deficit = show.deficit() + total_sold += show.slots * show.price + if deficit >= 0: + if u"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 + if request.user.is_authenticated(): + members2 = {} + for (show, members, _) in results: + for (member, _, _, _) in members: + if member not in members2: + members2[member] = [] + member.total = 0 + 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": + Attribution.objects.all().delete() + for (show, members, _) in results: + for (member, _, _, _) in members: + attrib = Attribution(spectacle = show, participant = member) + attrib.save() + return render(request, "bda-attrib-extra.html", data) + else: + return render(request, "bda-attrib.html", data) + +class TokenForm(forms.Form): + token = forms.CharField(widget = forms.widgets.Textarea()) + +@login_required +def tirage(request): + if request.POST: + form = TokenForm(request.POST) + if form.is_valid(): + return do_tirage(request) + else: + form = TokenForm() + return render(request, "bda-token.html", {"form": form}) + +class SpectacleModelChoiceField(forms.ModelChoiceField): + def label_from_instance(self, obj): + return u"%s le %s (%s) à %.02f€" % (obj.title, obj.date_no_seconds(), obj.location, obj.price) + +class ResellForm(forms.Form): + count = forms.ChoiceField(choices = (("1","1"),("2","2"),)) + spectacle = SpectacleModelChoiceField(queryset = Spectacle.objects.none()) + + def __init__(self, participant, *args, **kwargs): + super(ResellForm, self).__init__(*args, **kwargs) + self.fields['spectacle'].queryset = participant.attributions.all().distinct() + +def do_resell(request, form): + spectacle = form.cleaned_data["spectacle"] + count = form.cleaned_data["count"] + places = "2 places" if count == "2" else "une place" + mail = u"""Bonjour, + +Je souhaite revendre %s pour %s le %s (%s) à %.02f€. +Contactez moi par email si vous êtes intéressé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, + request.user.email, ["bda-revente@lists.ens.fr"], + fail_silently = True) + return render(request, "bda-success.html", {"show": spectacle, "places": places}) + +@login_required +def revente(request): + participant, created = Participant.objects.get_or_create(user = request.user) + if not participant.paid: + return render(request, "bda-notpaid.html", {}) + if request.POST: + form = ResellForm(participant, request.POST) + if form.is_valid(): + return do_resell(request, form) + else: + form = ResellForm(participant) + return render(request, "bda-revente.html", {"form": form}) + +@buro_required +def spectacle(request, spectacle_id): + spectacle = get_object_or_404(Spectacle, id = spectacle_id) + return render(request, "bda-emails.html", {"spectacle": spectacle}) + +@buro_required +def unpaid(request): + return render(request, "bda-unpaid.html", {"unpaid": Participant.objects.filter(paid = False).all()}) diff --git a/gestioncof/admin.py b/gestioncof/admin.py index 7dc05151..9fe8c82b 100644 --- a/gestioncof/admin.py +++ b/gestioncof/admin.py @@ -1,13 +1,17 @@ # coding: utf-8 from django.contrib import admin -from gestioncof.models import Survey, SurveyQuestion, SurveyQuestionAnswer -from gestioncof.models import Event, EventOption, EventOptionChoice -from gestioncof.models import CofProfile +from gestioncof.models import * +from gestioncof.petits_cours_models import * from django.contrib.auth.models import User from django.contrib.auth.admin import UserAdmin from django.core.urlresolvers import reverse from django.utils.safestring import mark_safe +import django.forms as forms +#import eav.admin +from django.utils.translation import ugettext_lazy as _ +from django.contrib.admin import SimpleListFilter +from django.db.models import Q def add_link_field(target_model = '', field = '', link_text = unicode, desc_text = unicode): def add_link(cls): @@ -61,36 +65,170 @@ class EventAdmin(admin.ModelAdmin): EventOptionInline, ] +#from eav.forms import BaseDynamicEntityForm + +#class CofProfileInline(eav.admin.BaseEntityInline, admin.StackedInline): class CofProfileInline(admin.StackedInline): model = CofProfile + #form = BaseDynamicEntityForm inline_classes = ("collapse open",) -class UserProfileAdmin(UserAdmin): - def login_clipper(self, obj): +class FkeyLookup(object): + def __init__(self, fkeydecl, short_description=None, admin_order_field=None): + self.fk, fkattrs = fkeydecl.split('__', 1) + self.fkattrs = fkattrs.split('__') + + self.short_description = short_description or self.fkattrs[-1] + self.admin_order_field = admin_order_field or fkeydecl + + def __get__(self, obj, klass): + if obj is None: + return self # hack required to make Django validate (if obj is None, then we're a class, and classes are callable ) + + item = getattr(obj, self.fk) + for attr in self.fkattrs: + item = getattr(item, attr) + return item + +def ProfileInfo(field, short_description, boolean = False): + def getter(self): try: - return obj.get_profile().login_clipper - except UserProfile.DoesNotExist: + return getattr(self.get_profile(), field) + except CofProfile.DoesNotExist: return "" + getter.short_description = short_description + getter.boolean = boolean + return getter + +User.profile_login_clipper = FkeyLookup("profile__login_clipper", "Login clipper") +User.profile_num = FkeyLookup("profile__num", "Numéro") +User.profile_phone = ProfileInfo("phone", "Téléphone") +User.profile_occupation = ProfileInfo("occupation", "Occupation") +User.profile_departement = ProfileInfo("departement", "Departement") +User.profile_mailing_cof = ProfileInfo("mailing_cof", "ML COF", True) +User.profile_mailing_bda = ProfileInfo("mailing_bda", "ML BDA", True) +User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente", "ML BDA-R", True) + +class UserProfileAdmin(UserAdmin): def is_buro(self, obj): try: return obj.get_profile().is_buro - except UserProfile.DoesNotExist: + except CofProfile.DoesNotExist: return False is_buro.short_description = 'Membre du Buro' is_buro.boolean = True def is_cof(self, obj): try: return obj.get_profile().is_cof - except UserProfile.DoesNotExist: + except CofProfile.DoesNotExist: return False is_cof.short_description = 'Membre du COF' is_cof.boolean = True - list_display = UserAdmin.list_display + ('login_clipper','is_cof','is_buro',) - list_filter = UserAdmin.list_filter + ('profile__is_cof', 'profile__is_buro') + list_display = ('profile_num',) + UserAdmin.list_display + ('profile_login_clipper','profile_phone','profile_occupation','profile_mailing_cof','profile_mailing_bda','profile_mailing_bda_revente','is_cof','is_buro',) + list_display_links = ('username','email','first_name','last_name') + list_filter = UserAdmin.list_filter + ('profile__is_cof', 'profile__is_buro', 'profile__mailing_cof', 'profile__mailing_bda') inlines = [ CofProfileInline, ] +import autocomplete_light +def user_unicode(self): + if self.first_name and self.last_name: + return u"%s %s (%s)" % (self.first_name, self.last_name, self.username) + else: + return self.username +User.__unicode__ = user_unicode +class EventRegistrationAdmin(admin.ModelAdmin): + form = autocomplete_light.modelform_factory(EventRegistration) + list_display = ('__unicode__','event','user','paid') + list_filter = ('paid',) + search_fields = ('user__username', 'user__first_name', 'user__last_name', 'user__email', 'event__title') + +""" +class VoterProfileInlineForm(BaseDynamicEntityForm): + def __init__(self, data=None, *args, **kwargs): + super(BaseDynamicEntityForm, self).__init__(data, *args, **kwargs) + config_cls = self.instance._eav_config_cls + self.entity = getattr(self.instance, config_cls.eav_attr) + self.base_fields = {} + self._build_dynamic_fields() + +class VoterProfileInline(eav.admin.BaseEntityInline, admin.StackedInline): + model = CofProfile + form = VoterProfileInlineForm + inline_classes = ("collapse open",) + fields = None + fieldsets = None + + def get_fieldsets(self, request, obj=None): + formset = self.get_formset(request) + fk_name = self.fk_name or formset.fk.name + kw = {fk_name: obj} if obj else {} + instance = self.model(**kw) + form = formset.form(request.POST, instance=instance) + + return [(None, {'fields': form.fields.keys()})] + +class VotedListFilter(SimpleListFilter): + # Human-readable title which will be displayed in the + # right admin sidebar just above the filter options. + title = _(u'A voté') + + # Parameter for the filter that will be used in the URL query. + parameter_name = 'voted' + + def lookups(self, request, model_admin): + return ( + ('1', _('Yes')), + ('0', _('No')), + ) + + def queryset(self, request, queryset): + # Returns the filtered queryset based on the value + # provided in the query string and retrievable via + # `self.value()`. + # + # Compare the requested value (either '80s' or '90s') + # to decide how to filter the queryset. + if self.value() == '1': + qs2 = User.objects.filter(profile__eav__a_vot = True) + return queryset.filter(pk__in = qs2.values_list('id', flat = True)) + return voters + if self.value() == '0': + qs2 = User.objects.filter(profile__eav__a_vot = False) + return queryset.filter(pk__in = qs2.values_list('id', flat = True)) + +class VoterAdmin(UserProfileAdmin): + + form = forms.ModelForm + fields = ('username','first_name','last_name') + readonly_fields = ('username','first_name','last_name') + fieldsets = None + + def is_cof(self, obj): + try: + return obj.get_profile().is_cof + except CofProfile.DoesNotExist: + return False + is_cof.short_description = 'Membre du COF' + is_cof.boolean = True + def a_vote(self, obj): + try: + if not obj.get_profile().eav.a_vot: + return False + else: + return True + except CofProfile.DoesNotExist: + return False + a_vote.short_description = 'A voté' + a_vote.boolean = True + list_display = ('profile_num',) + UserAdmin.list_display + ('is_cof','a_vote') + list_filter = ('profile__is_cof', 'profile__is_buro', VotedListFilter) + inlines = [ + VoterProfileInline, + ] +""" + admin.site.register(Survey, SurveyAdmin) admin.site.register(SurveyQuestion, SurveyQuestionAdmin) admin.site.register(Event, EventAdmin) @@ -98,3 +236,8 @@ admin.site.register(EventOption, EventOptionAdmin) admin.site.unregister(User) admin.site.register(User, UserProfileAdmin) admin.site.register(CofProfile) +admin.site.register(PetitCoursSubject) +admin.site.register(PetitCoursAbility) +admin.site.register(PetitCoursDemande) +#admin.site.register(Voter, VoterAdmin) +admin.site.register(EventRegistration, EventRegistrationAdmin) diff --git a/gestioncof/decorators.py b/gestioncof/decorators.py index 6e2b3028..6ffb2d39 100644 --- a/gestioncof/decorators.py +++ b/gestioncof/decorators.py @@ -7,8 +7,7 @@ def is_cof(user): except: return False -def cof_required(login_url = None): - return user_passes_test(lambda u: is_cof(u), login_url=login_url) +cof_required = user_passes_test(lambda u: is_cof(u)) def is_buro(user): try: @@ -17,5 +16,4 @@ def is_buro(user): except: return False -def buro_required(login_url = None): - return user_passes_test(lambda u: is_buro(u), login_url=login_url) +buro_required = user_passes_test(lambda u: is_buro(u)) diff --git a/gestioncof/models.py b/gestioncof/models.py index a8e0e3eb..2d251181 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -5,6 +5,8 @@ from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ from django.db.models.signals import post_save +from petits_cours_models import * + OCCUPATION_CHOICES = ( ('exterieur', _(u"Extérieur")), ('1A', _(u"1A")), @@ -29,19 +31,23 @@ class CofProfile(models.Model): user = models.OneToOneField(User, related_name = "profile") login_clipper = models.CharField("Login clipper", max_length = 8, blank = True) is_cof = models.BooleanField("Membre du COF", default = False) - num = models.IntegerField ("Numéro d'adhérent", blank = True, default = 0), + num = models.IntegerField ("Numéro d'adhérent", blank = True, default = 0) phone = models.CharField("Téléphone", max_length = 20, blank = True) occupation = models.CharField (_(u"Occupation"), default = "1A", choices = OCCUPATION_CHOICES, max_length = choices_length (OCCUPATION_CHOICES)) + departement = models.CharField (_(u"Département"), max_length = 50, blank = True) type_cotiz = models.CharField (_(u"Type de cotisation"), default = "normalien", choices = TYPE_COTIZ_CHOICES, max_length = choices_length (TYPE_COTIZ_CHOICES)) mailing_cof = models.BooleanField("Recevoir les mails COF", default = False) - mailing_bda_revente = models.BooleanField("Recevoir les mails de revente de places BDA", 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) is_buro = models.BooleanField("Membre du Burô", default = False) + petits_cours_accept = models.BooleanField("Recevoir des petits cours", default = False) + petits_cours_sent = models.IntegerField("Nombre de propositions petits cours reçues", default = 0) class Meta: verbose_name = "Profil COF" @@ -62,6 +68,7 @@ class Event(models.Model): end_date = models.DateField("Date de fin", blank = True, null = True) description = models.TextField("Description", blank = True) registration_open = models.BooleanField("Inscriptions ouvertes", default = True) + old = models.BooleanField("Archiver (événement fini)", default = False) class Meta: verbose_name = "Événement" @@ -100,10 +107,14 @@ class EventRegistration(models.Model): verbose_name = "Inscription" unique_together = ("user", "event") + def __unicode__(self): + return u"Inscription de %s à %s" % (unicode(self.user), unicode(self.event.title)) + class Survey(models.Model): title = models.CharField("Titre", max_length = 200) details = models.TextField("Détails", blank = True) survey_open = models.BooleanField("Sondage ouvert", default = True) + old = models.BooleanField("Archiver (sondage fini)", default = False) class Meta: verbose_name = "Sondage" @@ -140,3 +151,24 @@ class SurveyAnswer(models.Model): class Meta: verbose_name = "Réponses" unique_together = ("user", "survey") + +class Clipper(models.Model): + username = models.CharField("Identifiant", max_length = 20) + fullname = models.CharField("Nom complet", max_length = 200) + +""" +class Voter(User): + class Meta: + proxy = True + verbose_name = "voteur" + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) + +import eav +eav.register(CofProfile) + +class ManagerOnlyEavConfig(eav.registry.EavConfig): + manager_only = True + +eav.register(User, ManagerOnlyEavConfig) +""" diff --git a/gestioncof/shared.py b/gestioncof/shared.py index f506aed2..efcd0e50 100644 --- a/gestioncof/shared.py +++ b/gestioncof/shared.py @@ -1,16 +1,10 @@ from django.contrib.sites.models import Site from django.conf import settings from django_cas.backends import CASBackend -from django.template import RequestContext, loader -from django.http import HttpResponse +from django.db import models, connection from gestioncof.models import CofProfile -def render_page (request, data, template): - template = loader.get_template (template) - context = RequestContext (request, data) - return HttpResponse (template.render (context)) - class COFCASBackend(CASBackend): def authenticate(self, ticket, service): """Authenticates CAS ticket and retrieves user data""" @@ -39,3 +33,16 @@ def context_processor (request): "site": Site.objects.get_current(), } return data + +def lock_table(model): + cursor = connection.cursor() + table = model._meta.db_table + cursor.execute("LOCK TABLES %s WRITE" % table) + row = cursor.fetchone() + return row + +def unlock_table(model): + cursor = connection.cursor() + cursor.execute("UNLOCK TABLES") + row = cursor.fetchone() + return row diff --git a/gestioncof/templatetags/utils.py b/gestioncof/templatetags/utils.py index 71096c75..9c439dd3 100644 --- a/gestioncof/templatetags/utils.py +++ b/gestioncof/templatetags/utils.py @@ -1,7 +1,11 @@ from django import template +from django.utils.safestring import mark_safe + +import re register = template.Library() +@register.filter def key(d, key_name): try: value = d[key_name] @@ -9,4 +13,24 @@ def key(d, key_name): from django.conf import settings value = settings.TEMPLATE_STRING_IF_INVALID return value -key = register.filter('key', key) + +def highlight_text(text, q): + q2 = "|".join(q.split()) + pattern = re.compile(r"(?P%s)" % q2, re.IGNORECASE) + return mark_safe(re.sub(pattern, r"\g", text)) + +@register.filter +def highlight_user(user, q): + if user.first_name and user.last_name: + text = u"%s %s (%s)" % (user.first_name, user.last_name, user.username) + else: + text = user.username + return highlight_text(text, q) + +@register.filter +def highlight_clipper(clipper, q): + if clipper.fullname: + text = u"%s (%s)" % (clipper.fullname, clipper.username) + else: + text = clipper.username + return highlight_text(text, q) diff --git a/gestioncof/views.py b/gestioncof/views.py index fbdbe403..0adda9f8 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -1,29 +1,51 @@ # coding: utf-8 -from django.shortcuts import redirect, get_object_or_404 -from django.http import Http404 + +import unicodecsv + +from django.shortcuts import redirect, get_object_or_404, render +from django.http import Http404, HttpResponse from django.core.urlresolvers import reverse +from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required from django import forms from django.forms.widgets import RadioSelect, CheckboxSelectMultiple from django.utils.translation import ugettext_lazy as _ +from django.db.models import Max +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 CofProfile -from gestioncof.shared import render_page +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 @login_required def home(request): - data = {"surveys": Survey.objects.filter(survey_open = True).all(), - "events": Event.objects.filter(registration_open = True).all()} - return render_page(request, data, "home.html") + data = {"surveys": Survey.objects.filter(old = False).all(), + "events": Event.objects.filter(old = False).all(), + "open_surveys": Survey.objects.filter(survey_open = True, old = False).all(), + "open_events": Event.objects.filter(registration_open = True, old = False).all()} + return render(request, "home.html", data) def login(request): if request.user.is_authenticated(): return redirect("gestioncof.views.home") - return render_page(request, {}, "login_switch.html") + return render(request, "login_switch.html", {}) + +def login_ext(request): + if request.method == "POST" and "username" in request.POST: + try: + user = User.objects.get(username = request.POST["username"]) + if not user.has_usable_password() or user.password in ("","!"): + profile, created = CofProfile.objects.get_or_create(user = user) + if profile.login_clipper: + return render(request, "error.html", {"error_type": "use_clipper_login"}) + else: + return render(request, "error.html", {"error_type": "no_password"}) + except User.DoesNotExist: + pass + return django_login_view(request, template_name = 'login.html') @login_required def logout(request): @@ -124,11 +146,12 @@ def survey(request, survey_id): except SurveyAnswer.DoesNotExist: current_answer = None form = SurveyForm(survey = survey) - return render_page(request, {"survey": survey, "form": form, "success": success, "deleted": deleted, "current_answer": current_answer}, "survey.html") + return render(request, "survey.html", {"survey": survey, "form": form, "success": success, "deleted": deleted, "current_answer": current_answer}) class EventForm(forms.Form): def __init__(self, *args, **kwargs): event = kwargs.pop("event") + self.event = event current_choices = kwargs.pop("current_choices", None) super(EventForm, self).__init__(*args, **kwargs) choices = {} @@ -163,6 +186,25 @@ class EventForm(forms.Form): if name.startswith('option_'): yield (self.fields[name].option_id, value) +def get_event_form_choices(event, form): + all_choices = [] + for option_id, choices_ids in form.choices(): + option = get_object_or_404(EventOption, id = option_id, + event = event) + if type(choices_ids) != list: + choices_ids = [choices_ids] + if not option.multi_choices and len(choices_ids) > 1: + raise Http404 + for choice_id in choices_ids: + if not choice_id: + continue + choice_id = int(choice_id) + choice = EventOptionChoice.objects.get( + id = choice_id, + event_option = option) + all_choices.append(choice) + return all_choices + @login_required def event(request, event_id): event = get_object_or_404(Event, id = event_id) @@ -172,27 +214,8 @@ def event(request, event_id): if request.method == "POST": form = EventForm(request.POST, event = event) if form.is_valid(): - all_choices = [] - for option_id, choices_ids in form.choices(): - option = get_object_or_404(EventOption, id = option_id, - event = event) - if type(choices_ids) != list: - choices_ids = [choices_ids] - if not option.multi_choices and len(choices_ids) > 1: - raise Http404 - for choice_id in choices_ids: - if not choice_id: - continue - choice_id = int(choice_id) - choice = EventOptionChoice.objects.get( - id = choice_id, - event_option = option) - all_choices.append(choice) - try: - current_registration = EventRegistration.objects.get(user = request.user, event = event) - except EventRegistration.DoesNotExist: - current_registration = EventRegistration(user = request.user, event = event) - current_registration.save() + all_choices = get_event_form_choices(event, form) + (current_registration, _) = EventRegistration.objects.get_or_create(user = request.user, event = event) current_registration.options = all_choices current_registration.save() success = True @@ -202,13 +225,7 @@ def event(request, event_id): form = EventForm(event = event, current_choices = current_registration.options) except EventRegistration.DoesNotExist: form = EventForm(event = event) - return render_page(request, {"event": event, "form": form, "success": success}, "event.html") - -@buro_required() -def event_status(request, event_id): - event = get_object_or_404(Event, id = event_id) - registrants = EventRegistration.objects.filter(event = event).all() - return render_page(request, {"event": event, "registrants": registrants}, "event_status.html") + return render(request, "event.html", {"event": event, "form": form, "success": success}) class SurveyStatusFilterForm(forms.Form): def __init__(self, *args, **kwargs): @@ -240,7 +257,6 @@ class EventStatusFilterForm(forms.Form): def __init__(self, *args, **kwargs): event = kwargs.pop("event") super(EventStatusFilterForm, self).__init__(*args, **kwargs) - choices = {} for option in event.options.all(): for choice in option.choices.all(): name = "option_%d_choice_%d" % (option.id, choice.id) @@ -256,24 +272,35 @@ class EventStatusFilterForm(forms.Form): field.option_id = option.id field.choice_id = choice.id self.fields[name] = field + # has_paid + name = "event_has_paid" + if self.is_bound and self.data.get(self.add_prefix(name), None): + initial = self.data.get(self.add_prefix(name), None) + else: + initial = "none" + field = forms.ChoiceField(label = "Événement payé", + choices = [("yes", "yes"),("no","no"),("none","none")], + widget = TriStateCheckbox, + required = False, + initial = initial) + self.fields[name] = field def filters(self): for name, value in self.cleaned_data.items(): if name.startswith('option_'): yield (self.fields[name].option_id, self.fields[name].choice_id, value) + elif name == "event_has_paid": + yield ("has_paid", None, value) def clean_post_for_status(initial): - d = dict(initial) + d = initial.copy() for k, v in d.items(): if k.startswith("id_"): del d[k] - if type(v) == list and len(v) >= 1: - d[k[3:]] = v[0] - else: - d[k[3:]] = v + d[k[3:]] = v return d -@buro_required() +@buro_required def event_status(request, event_id): event = get_object_or_404(Event, id = event_id) registrations_query = EventRegistration.objects.filter(event = event) @@ -281,6 +308,12 @@ def event_status(request, event_id): form = EventStatusFilterForm(post_data or None, event = event) if form.is_valid(): for option_id, choice_id, value in form.filters(): + if option_id == "has_paid": + if value == "yes": + registrations_query = registrations_query.filter(paid = True) + elif value == "no": + registrations_query = registrations_query.filter(paid = False) + continue choice = get_object_or_404(EventOptionChoice, id = choice_id, event_option__id = option_id) if value == "none": continue @@ -297,9 +330,9 @@ def event_status(request, event_id): for user_choice in user_choices: for choice in user_choice.options.all(): choices_count[choice.id] += 1 - return render_page(request, {"event": event, "user_choices": user_choices, "options": options, "choices_count": choices_count, "form": form}, "event_status.html") + return render(request, "event_status.html", {"event": event, "user_choices": user_choices, "options": options, "choices_count": choices_count, "form": form}) -@buro_required() +@buro_required def survey_status(request, survey_id): survey = get_object_or_404(Survey, id = survey_id) answers_query = SurveyAnswer.objects.filter(survey = survey) @@ -323,7 +356,7 @@ def survey_status(request, survey_id): for user_answer in user_answers: for answer in user_answer.answers.all(): answers_count[answer.id] += 1 - return render_page(request, {"survey": survey, "user_answers": user_answers, "questions": questions, "answers_count": answers_count, "form": form}, "survey_status.html") + return render(request, "survey_status.html", {"survey": survey, "user_answers": user_answers, "questions": questions, "answers_count": answers_count, "form": form}) class UserProfileForm(forms.ModelForm): first_name = forms.CharField(label=_(u'Prénom'), max_length=30) @@ -339,6 +372,7 @@ class UserProfileForm(forms.ModelForm): 'last_name', 'phone', 'mailing_cof', + 'mailing_bda', 'mailing_bda_revente', ] @@ -350,7 +384,7 @@ class UserProfileForm(forms.ModelForm): class Meta: model = CofProfile - fields = ("phone", "mailing_cof", "mailing_bda_revente",) + fields = ("phone", "mailing_cof", "mailing_bda", "mailing_bda_revente",) @login_required def profile(request): @@ -362,10 +396,312 @@ def profile(request): success = True else: form = UserProfileForm(instance = request.user.get_profile()) - return render_page(request, {"form": form, "success": success}, "profile.html") + return render(request, "profile.html", {"form": form, "success": success}) -@login_required +class RegistrationUserForm(forms.ModelForm): + def __init__(self, *args, **kw): + super(RegistrationUserForm, self).__init__(*args, **kw) + self.fields['username'].help_text = "" + + class Meta: + model = User + fields = ("username", "first_name", "last_name", "email") + +class RegistrationProfileForm(forms.ModelForm): + def __init__(self, *args, **kw): + super(RegistrationProfileForm, self).__init__(*args, **kw) + self.fields['mailing_cof'].initial = True + self.fields['mailing_bda'].initial = True + self.fields['mailing_bda_revente'].initial = True + self.fields['num'].widget.attrs['readonly'] = True + + self.fields.keyOrder = [ + 'login_clipper', + 'phone', + 'occupation', + 'departement', + 'is_cof', + 'num', + 'type_cotiz', + 'mailing_cof', + 'mailing_bda', + 'mailing_bda_revente', + ] + + def save(self, *args, **kw): + instance = super(RegistrationProfileForm, self).save(*args, **kw) + if instance.is_cof and not instance.num: + # Generate new number + try: + lock_table(CofProfile) + aggregate = CofProfile.objects.aggregate(Max('num')) + instance.num = aggregate['num__max'] + 1 + instance.save() + self.cleaned_data['num'] = instance.num + self.data['num'] = instance.num + finally: + unlock_table(CofProfile) + return instance + + class Meta: + model = CofProfile + fields = ("login_clipper", "num", "phone", "occupation", "departement", "is_cof", "type_cotiz", "mailing_cof", "mailing_bda", "mailing_bda_revente",) + +def registration_set_ro_fields(user_form, profile_form): + user_form.fields['username'].widget.attrs['readonly'] = True + profile_form.fields['login_clipper'].widget.attrs['readonly'] = True + +@buro_required +def registration_form(request, login_clipper = None, username = None): + member = None + if login_clipper: + clipper = get_object_or_404(Clipper, username = login_clipper) + try: # check if the given user is already registered + member = User.objects.filter(username = login_clipper).get() + username = member.username + login_clipper = None + except User.DoesNotExist: + # new user, but prefill + user_form = RegistrationUserForm() + profile_form = RegistrationProfileForm() + user_form.fields['username'].initial = login_clipper + user_form.fields['email'].initial = login_clipper + "@clipper.ens.fr" + profile_form.fields['login_clipper'].initial = login_clipper + if clipper.fullname: + bits = clipper.fullname.split(" ") + user_form.fields['first_name'].initial = bits[0] + if len(bits) > 1: + user_form.fields['last_name'].initial = " ".join(bits[1:]) + registration_set_ro_fields(user_form, profile_form) + if username: + member = get_object_or_404(User, username = username) + (profile, _) = CofProfile.objects.get_or_create(user = member) + # already existing, prefill + user_form = RegistrationUserForm(instance = member) + profile_form = RegistrationProfileForm(instance = profile) + registration_set_ro_fields(user_form, profile_form) + elif not login_clipper: + # new user + user_form = RegistrationUserForm() + profile_form = RegistrationProfileForm() + 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é'),) +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) + paid = kwargs.pop("paid", None) + if paid == True: + kwargs["initial"] = {"status":"paid"} + elif paid == False: + kwargs["initial"] = {"status":"wait"} + else: + 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) + all_choices = choices + for option in event.options.all(): + choices = [(choice.id, choice.value) for choice in option.choices.all()] + if option.multi_choices: + initial = [] if option.id not in all_choices else all_choices[option.id] + field = forms.MultipleChoiceField(label = option.name, + choices = choices, + widget = CheckboxSelectMultiple, + required = False, + initial = initial) + else: + initial = None if option.id not in all_choices else all_choices[option.id][0] + field = forms.ChoiceField(label = option.name, + choices = choices, + widget = RadioSelect, + required = False, + initial = initial) + field.option_id = option.id + self.fields["option_%d" % option.id] = field + + def choices(self): + for name, value in self.cleaned_data.items(): + if name.startswith('option_'): + yield (self.fields[name].option_id, value) + +@buro_required +def registration_form2(request, login_clipper = None, username = None): + events = Event.objects.filter(old = False).all() + member = None + if login_clipper: + clipper = get_object_or_404(Clipper, username = login_clipper) + try: # check if the given user is already registered + member = User.objects.filter(username = login_clipper).get() + username = member.username + login_clipper = None + except User.DoesNotExist: + # new user, but prefill + user_form = RegistrationUserForm() + profile_form = RegistrationProfileForm() + event_forms = [AdminEventForm(event = event) for event in events] + user_form.fields['username'].initial = login_clipper + user_form.fields['email'].initial = login_clipper + "@clipper.ens.fr" + profile_form.fields['login_clipper'].initial = login_clipper + if clipper.fullname: + bits = clipper.fullname.split(" ") + user_form.fields['first_name'].initial = bits[0] + if len(bits) > 1: + user_form.fields['last_name'].initial = " ".join(bits[1:]) + registration_set_ro_fields(user_form, profile_form) + if username: + member = get_object_or_404(User, username = username) + (profile, _) = CofProfile.objects.get_or_create(user = member) + # already existing, prefill + user_form = RegistrationUserForm(instance = member) + profile_form = RegistrationProfileForm(instance = profile) + registration_set_ro_fields(user_form, profile_form) + event_forms = [] + 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) + except EventRegistration.DoesNotExist: + form = AdminEventForm(event = event) + event_forms.append(form) + elif not login_clipper: + # new user + user_form = RegistrationUserForm() + profile_form = RegistrationProfileForm() + event_forms = [AdminEventForm(event = event) for event in events] + return render(request, "registration_form.html", {"user_form": user_form, "profile_form": profile_form, "member": member, "login_clipper": login_clipper, "event_forms": event_forms}) + +@buro_required def registration(request): - data = {"surveys": Survey.objects.filter(survey_open = True).all(), - "events": Event.objects.filter(registration_open = True).all()} - return render_page(request, data, "registration.html") + if request.POST: + request_dict = request.POST.copy() + if "num" in request_dict: + del request_dict["num"] + success = False + user_form = RegistrationUserForm(request_dict) + profile_form = RegistrationProfileForm(request_dict) + events = Event.objects.filter(old = False).all() + event_forms = [AdminEventForm(request_dict, event = event) for event in events] + user_form.is_valid() + profile_form.is_valid() + for event_form in event_forms: event_form.is_valid() + member = None + login_clipper = None + if "user_exists" in request_dict and request_dict["user_exists"]: + username = request_dict["username"] + try: + member = User.objects.filter(username = username).get() + (profile, _) = CofProfile.objects.get_or_create(user = member) + user_form = RegistrationUserForm(request_dict, instance = member) + profile_form = RegistrationProfileForm(request_dict, instance = profile) + except User.DoesNotExist: + try: + clipper = Clipper.objects.filter(username = username).get() + login_clipper = clipper.username + except Clipper.DoesNotExist: + pass + for form in event_forms: + if not form.is_valid(): break + if form.cleaned_data['status'] == 'no': continue + all_choices = get_event_form_choices(form.event, form) + 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) + request_dict["num"] = profile.num + profile_form = RegistrationProfileForm(request_dict, instance = profile) + profile_form.is_valid() + profile_form.save() + for form in event_forms: + if form.cleaned_data['status'] == 'no': + try: + current_registration = EventRegistration.objects.get(user = member, event = form.event) + current_registration.delete() + except EventRegistration.DoesNotExist: + 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.options = all_choices + current_registration.paid = (form.cleaned_data['status'] == 'paid') + current_registration.save() + 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: + return render(request, "registration.html") + +@buro_required +def export_members(request): + response = HttpResponse(mimetype = 'text/csv') + response['Content-Disposition'] = 'attachment; filename=membres_cof.csv' + + writer = unicodecsv.UnicodeWriter(response) + for profile in CofProfile.objects.filter(is_cof = True).all(): + user = profile.user + bits = [profile.num, user.username, user.first_name, user.last_name, user.email, profile.phone, profile.occupation, profile.departement, profile.type_cotiz] + writer.writerow([unicode(bit) for bit in bits]) + + return response + +@buro_required +def export_mega_orgas(request): + response = HttpResponse(mimetype = 'text/csv') + response['Content-Disposition'] = 'attachment; filename=participants_mega.csv' + + writer = unicodecsv.UnicodeWriter(response) + event = Event.objects.filter(title = "MEGA") + for reg in EventRegistration.objects.filter(event = event).exclude(options__id__exact = 3).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 + +@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.filter(title = "MEGA") + for reg in EventRegistration.objects.filter(event = event).filter(options__id__exact = 3).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 + +@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 + +@buro_required +def utile_cof(request): + return render(request, "utile_cof.html", {}) + +@buro_required +def utile_bda(request): + return render(request, "utile_bda.html", {}) \ No newline at end of file diff --git a/manage.py b/manage.py old mode 100644 new mode 100755 index 3e4eedc9..7f4e79f6 --- a/manage.py +++ b/manage.py @@ -1,14 +1,10 @@ #!/usr/bin/env python -from django.core.management import execute_manager -import imp -try: - imp.find_module('settings') # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) - sys.exit(1) - -import settings +import os +import sys if __name__ == "__main__": - execute_manager(settings) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/media/cof.css b/media/cof.css index 6779febe..f709b7fe 100644 --- a/media/cof.css +++ b/media/cof.css @@ -133,6 +133,14 @@ form { padding: 0; } +#form-placeholder { + margin-top: 15px; +} + +#form-placeholder form#profile { + margin-top: 10px; +} + form#profile table td, form#profile table th { width: 400px; } @@ -358,6 +366,15 @@ body { font-weight: bold; font-size: 18px; margin-bottom: 5px; + float: left; +} + +#main-login #header h1 { + float: none; +} + +#header #homelink { + float: right; } tt { @@ -388,6 +405,10 @@ tt { min-height: 2em; } +input[disabled], input[readonly] { + color: #BBB; +} + input[type="text"], input[type=password] { border: 1px solid #888; -webkit-border-radius: 3px; @@ -397,6 +418,35 @@ input[type="text"], input[type=password] { padding: 0px 2px 0px 2px; } +input#search_autocomplete { + width: 600px; + font-size: 14px; + height: 20px; + padding: 10px 8px; + margin: 0 auto; + display: block; + color: #aaa; +} + +input#search_autocomplete:focus { + font-size: 18px; + color: #343a4a; +} + +.autocomplete {margin-bottom:5px;} +.yourlabs-autocomplete ul {list-style-type: none; padding: 0; margin: 0; font-size: 1.5em;} +.yourlabs-autocomplete li {padding: 6px 5px; display: block;} +.yourlabs-autocomplete li a {text-decoration: none;} +.yourlabs-autocomplete li a span.highlight {text-decoration: underline; font-weight: bold;} +.yourlabs-autocomplete li.autocomplete-header {background-color: #FFEF9E; padding: 3px 5px; color: #000;} +.yourlabs-autocomplete li.autocomplete-value {cursor: pointer;} +.yourlabs-autocomplete li.hilight {background-color: #EA4839; color: #FFF; } +.yourlabs-autocomplete li.hilight a {color: #FFF; } +.yourlabs-autocomplete { border: none; background: none; } +.yourlabs-autocomplete.outer-container { position: relative; } +.yourlabs-autocomplete.inner-container { position: absolute; left: -1px; top: 2px; border: 1px solid #B7B7B7; width: 618px; background: #fff; } +.yourlabs-autocomplete.inner-container { margin: 0; padding: 0; overflow-x: hidden; overflow-y: auto; position: relative; list-style-type: none; } + hr { border: 0; height: 1px; diff --git a/settings.py b/settings.py deleted file mode 100644 index 8427f3b7..00000000 --- a/settings.py +++ /dev/null @@ -1,172 +0,0 @@ -# Django settings for cof project. - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - ('Guillaume Seguin', 'guillaume@segu.in'), -) - -MANAGERS = ADMINS - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': 'cof_gestion', # Or path to database file if using sqlite3. - 'USER': 'cof_gestion', # Not used with sqlite3. - 'PASSWORD': '1OjKotIbmyro', # Not used with sqlite3. - 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3. - } -} - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'Europe/Paris' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'fr-fr' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale -USE_L10N = True - -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/home/media/media.lawrence.com/media/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash. -# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" -MEDIA_URL = '/gestion/media/' - -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/home/media/media.lawrence.com/static/" -STATIC_ROOT = '/home/gestion/www/static' - -# URL prefix for static files. -# Example: "http://media.lawrence.com/static/" -STATIC_URL = '/gestion/static/' - -# URL prefix for admin static files -- CSS, JavaScript and images. -# Make sure to use a trailing slash. -# Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = '/gestion/static/grappelli/' -GRAPPELLI_ADMIN_HEADLINE = "GestioCOF" -GRAPPELLI_ADMIN_TITLE = "GestioCOF" - -# Additional locations of static files -STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# Make this unique, and don't share it with anybody. -SECRET_KEY = '1d%3zqeyj8dk^@afz9=q12gs&&5k@4qx)5%uc_(&%01)d&74af' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -TEMPLATE_CONTEXT_PROCESSORS = ( - "django.contrib.auth.context_processors.auth", - "django.core.context_processors.debug", - "django.core.context_processors.i18n", - "django.core.context_processors.media", - "django.core.context_processors.static", - "gestioncof.shared.context_processor", -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', -# 'django_cas.middleware.CASMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -) - -ROOT_URLCONF = 'urls' - -TEMPLATE_DIRS = ( - "/home/gestion/www/templates/gestioncof", - "/home/gestion/www/templates/bda", -) - -LOGIN_URL = "/gestion/login" -LOGIN_REDIRECT_URL = "/gestion/" - -CAS_SERVER_URL = 'https://cas.eleves.ens.fr/' -CAS_IGNORE_REFERER = True -CAS_REDIRECT_URL = '/gestion/' -CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" -AUTH_PROFILE_MODULE = 'gestioncof.CofProfile' -AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'gestioncof.shared.COFCASBackend', -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'grappelli', - 'django.contrib.admin', - 'django.contrib.admindocs', - 'gestioncof', - 'bda', - 'pads', - 'rezo', -) - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} diff --git a/templates/bda/inscription-bda.html b/templates/bda/inscription-bda.html index 110218a6..0bc3daeb 100644 --- a/templates/bda/inscription-bda.html +++ b/templates/bda/inscription-bda.html @@ -33,7 +33,7 @@ var django = { } deleteButtonHandler = function(elem) { elem.bind("click", function() { - var deleteInput = $(this).prev(), + var deleteInput = $(this).prev().prev(), form = $(this).parents(".dynamic-form").first(); // callback // toggle options.predeleteCssClass and toggle checkbox @@ -95,7 +95,7 @@ var django = { {% if success %}

Votre inscription a été mise à jour avec succès !

{% endif %} -
+ {% csrf_token %} {% include "inscription-formset.html" %} @@ -105,6 +105,7 @@ var django = { }); + Prix total actuel : {{ total_price }}€

1: demander deux places pour ce spectable
diff --git a/templates/gestioncof/base.html b/templates/gestioncof/base.html index ac30e575..862830e4 100644 --- a/templates/gestioncof/base.html +++ b/templates/gestioncof/base.html @@ -2,7 +2,7 @@ {{ site.name }} - + {% block extra_head %}{% endblock %} diff --git a/templates/gestioncof/base_title.html b/templates/gestioncof/base_title.html index a78825c4..5acee8a3 100644 --- a/templates/gestioncof/base_title.html +++ b/templates/gestioncof/base_title.html @@ -4,7 +4,9 @@

diff --git a/templates/gestioncof/event_status.html b/templates/gestioncof/event_status.html index ec320628..09487c1a 100644 --- a/templates/gestioncof/event_status.html +++ b/templates/gestioncof/event_status.html @@ -38,4 +38,8 @@ {% endif %} {% endwith %}{% endfor %} +

Mailing list

+ {% endblock %} diff --git a/templates/gestioncof/home.html b/templates/gestioncof/home.html index b4a635b0..a53a06c6 100644 --- a/templates/gestioncof/home.html +++ b/templates/gestioncof/home.html @@ -1,42 +1,53 @@ {% extends "base_title.html" %} +{% block homelink %} +{% endblock %} + {% block realcontent %}

Bienvenue, {% if user.first_name %}{{ user.first_name }}{% else %}{{ user.username }}{% endif %}

- {% if events %} + {% if open_events %}

Événements

{% endif %} - {% if surveys %} -

Sondages

+ {% if open_surveys %} +

Sondages en cours

{% endif %} + +

Divers

+ + {% if user.get_profile.is_buro %}

Administration

{% endif %} -

Divers

- + +

Pour tout problème : cof@ens.fr.

{% endblock %} diff --git a/templates/gestioncof/login.html b/templates/gestioncof/login.html index e075b546..4d9e7bf1 100644 --- a/templates/gestioncof/login.html +++ b/templates/gestioncof/login.html @@ -10,7 +10,7 @@

Identifiants incorrects.

{% endif %} - + {% csrf_token %} diff --git a/templates/gestioncof/login_switch.html b/templates/gestioncof/login_switch.html index 1497b1d8..8478518e 100644 --- a/templates/gestioncof/login_switch.html +++ b/templates/gestioncof/login_switch.html @@ -6,7 +6,7 @@ Compte clipper - + Extérieur
diff --git a/templates/gestioncof/registration.html b/templates/gestioncof/registration.html index 9f8b8f41..ea34cc77 100644 --- a/templates/gestioncof/registration.html +++ b/templates/gestioncof/registration.html @@ -1,15 +1,35 @@ {% extends "base_title.html" %} +{% block extra_head %} + + +{% endblock %} + {% block realcontent %}

Inscription d'un nouveau membre

- {% if success %} -

{{ member.first_name }} {{ member.last_name }} a été inscrit avec succès en tant que membre n°{{ member.get_profile.num }}!

- {% endif %} - - {% csrf_token %} -
- {{ form.as_table }} -
- - + +
+ {% endblock %} diff --git a/templates/gestioncof/survey_status.html b/templates/gestioncof/survey_status.html index 4c1a58b1..fa50ce94 100644 --- a/templates/gestioncof/survey_status.html +++ b/templates/gestioncof/survey_status.html @@ -37,5 +37,8 @@ {% endif %} {% endwith %}{% endfor %} - +

Mailing list

+ {% endblock %} diff --git a/urls.py b/urls.py deleted file mode 100644 index a07109d2..00000000 --- a/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.conf.urls.defaults import patterns, include, url -from django.contrib import admin -admin.autodiscover() - -urlpatterns = patterns('', - url(r'^$', 'gestioncof.views.home', name='home'), - url(r'^cas/login$', 'django_cas.views.login'), - url(r'^cas/logout$', 'django_cas.views.logout'), - url(r'^outsider/login$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}), - url(r'^outsider/logout$', 'django.contrib.auth.views.logout', {'next_page': '/gestion/'}), - url(r'^login$', 'gestioncof.views.login'), - url(r'^logout$', 'gestioncof.views.logout'), - url(r'^profile$', 'gestioncof.views.profile'), - url(r'^registration$', 'gestioncof.views.registration'), - url(r'^bda/inscription$', 'bda.views.inscription'), - url(r'^survey/(?P\d+)$', 'gestioncof.views.survey'), - url(r'^event/(?P\d+)$', 'gestioncof.views.event'), - url(r'^survey/(?P\d+)/status$', 'gestioncof.views.survey_status'), - url(r'^event/(?P\d+)/status$', 'gestioncof.views.event_status'), - url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - url(r'^admin/', include(admin.site.urls)), - url(r'^grappelli/', include('grappelli.urls')), -) -