From 2479b0a24d0b73f9c14b601a44731ae788dd7652 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Jul 2012 17:39:20 +0200 Subject: [PATCH] Major update --- bda/__init__.py | 0 bda/admin.py | 14 ++ bda/algorithm.py | 92 +++++++ bda/models.py | 44 ++++ bda/tests.py | 16 ++ bda/views.py | 40 +++ gestioncof/admin.py | 43 +++- gestioncof/decorators.py | 21 ++ gestioncof/models.py | 32 ++- gestioncof/shared.py | 19 +- gestioncof/templatetags/__init__.py | 0 gestioncof/templatetags/utils.py | 12 + gestioncof/views.py | 321 +++++++++++++++++++++--- gestioncof/widgets.py | 18 ++ media/cof.css | 234 ++++++++++++----- media/droidserif.woff | Bin 0 -> 31596 bytes media/images/no.png | Bin 0 -> 686 bytes media/images/none.png | Bin 0 -> 576 bytes media/images/yes.png | Bin 0 -> 610 bytes settings.py | 6 +- templates/bda/inscription-bda.html | 115 +++++++++ templates/bda/inscription-formset.html | 40 +++ templates/gestioncof/base.html | 1 + templates/gestioncof/base_title.html | 6 +- templates/gestioncof/event.html | 16 ++ templates/gestioncof/event_status.html | 41 +++ templates/gestioncof/home.html | 20 +- templates/gestioncof/profile.html | 15 ++ templates/gestioncof/registration.html | 15 ++ templates/gestioncof/survey.html | 7 + templates/gestioncof/survey_status.html | 41 +++ templates/gestioncof/tristate_js.html | 69 +++++ urls.py | 6 + 33 files changed, 1194 insertions(+), 110 deletions(-) create mode 100644 bda/__init__.py create mode 100644 bda/admin.py create mode 100644 bda/algorithm.py create mode 100644 bda/models.py create mode 100644 bda/tests.py create mode 100644 bda/views.py create mode 100644 gestioncof/decorators.py create mode 100644 gestioncof/templatetags/__init__.py create mode 100644 gestioncof/templatetags/utils.py create mode 100644 gestioncof/widgets.py create mode 100644 media/droidserif.woff create mode 100644 media/images/no.png create mode 100644 media/images/none.png create mode 100644 media/images/yes.png create mode 100644 templates/bda/inscription-bda.html create mode 100644 templates/bda/inscription-formset.html create mode 100644 templates/gestioncof/event.html create mode 100644 templates/gestioncof/event_status.html create mode 100644 templates/gestioncof/profile.html create mode 100644 templates/gestioncof/registration.html create mode 100644 templates/gestioncof/survey_status.html create mode 100644 templates/gestioncof/tristate_js.html diff --git a/bda/__init__.py b/bda/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bda/admin.py b/bda/admin.py new file mode 100644 index 00000000..05f500fa --- /dev/null +++ b/bda/admin.py @@ -0,0 +1,14 @@ +# coding: utf-8 + +from django.contrib import admin +from bda.models import Spectacle, Participant, ChoixSpectacle + +class ChoixSpectacleInline(admin.TabularInline): + model = ChoixSpectacle + sortable_field_name = "priority" + +class ParticipantAdmin(admin.ModelAdmin): + inlines = [ChoixSpectacleInline] + +admin.site.register(Spectacle) +admin.site.register(Participant, ParticipantAdmin) diff --git a/bda/algorithm.py b/bda/algorithm.py new file mode 100644 index 00000000..f3d2dce2 --- /dev/null +++ b/bda/algorithm.py @@ -0,0 +1,92 @@ +# coding: utf-8 + +from django.conf import settings +import random + +class Algorithm(object): + + shows = None + ranks = None + origranks = None + double = None + + def __init__(self, shows, members): + """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.shows = [] + showdict = {} + for show in shows: + showdict[show] = show + show.requests = [] + self.shows.append(show) + self.ranks = {} + self.origranks = {} + self.double = {} + 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 + self.ranks[member] = ranks + self.double[member] = double + self.origranks[member] = dict(ranks) + + def IncrementRanks(self, member, currank, increment = 1): + for show in self.ranks[member]: + if self.ranks[member][show] > currank: + self.ranks[member][show] -= increment + + def appendResult(self, l, member, show): + l.append((member, + self.ranks[member][show], + self.origranks[member][show], + self.double[member][show])) + + def __call__(self, seed): + random.seed(seed) + results = [] + for show in self.shows: + # On regroupe tous les gens ayant le même rang + groups = {} + for i in range(1, settings.NUM_CHOICES + 1): + groups[i] = [] + for member in show.requests: + groups[self.ranks[member][show]].append(member) + # On passe à l'attribution + winners = [] + losers = [] + for i in range(1, settings.NUM_CHOICES + 1): + group = list(groups[i]) + random.shuffle(group) + for member in group: + if self.double[member][show]: # 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: + self.appendResult(winners, member, show) + self.appendResult(losers, member, show) + else: + self.appendResult(losers, member, show) + self.appendResult(losers, member, show) + self.IncrementRanks(member, i, 2) + else: # simple + if len(winners) < show.slots: + self.appendResult(winners, member, show) + else: + self.appendResult(losers, member, show) + self.IncrementRanks(member, i) + results.append((show,winners,losers)) + return results diff --git a/bda/models.py b/bda/models.py new file mode 100644 index 00000000..c2480a33 --- /dev/null +++ b/bda/models.py @@ -0,0 +1,44 @@ +# coding: utf-8 + +from django.db import models +from django.contrib.auth.models import User +from django.utils.translation import ugettext_lazy as _ +from django.db.models.signals import post_save + +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) + description = models.TextField ("Description", blank = True) + slots_description = models.TextField ("Description des places", blank = True) + slots = models.IntegerField ("Places") + priority = models.IntegerField ("Priorité", default = 1000) + + class Meta: + verbose_name = "Spectacle" + ordering = ("priority", "date","title",) + + def __repr__ (self): + return u"[%s]" % self.__unicode__() + + def __unicode__ (self): + return u"%s - %s @ %s" % (self.title, self.date, self.location) + +class Participant (models.Model): + user = models.ForeignKey(User, unique = True) + choices = models.ManyToManyField(Spectacle, through = "ChoixSpectacle") + + def __unicode__ (self): + return u"%s" % (self.user) + +class ChoixSpectacle (models.Model): + participant = models.ForeignKey(Participant) + spectacle = models.ForeignKey(Spectacle, related_name = "participants") + priority = models.PositiveIntegerField("Priorité") + double = models.BooleanField("Deux places1") + autoquit = models.BooleanField("Abandon2") + class Meta: + ordering = ("priority",) + #unique_together = (("participant", "spectacle",),) + verbose_name = "voeu" diff --git a/bda/tests.py b/bda/tests.py new file mode 100644 index 00000000..501deb77 --- /dev/null +++ b/bda/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/bda/views.py b/bda/views.py new file mode 100644 index 00000000..5b8dda1a --- /dev/null +++ b/bda/views.py @@ -0,0 +1,40 @@ +# coding: utf-8 + +from django.contrib.auth.decorators import login_required +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 + +class BaseBdaFormSet(BaseInlineFormSet): + def clean(self): + """Checks that no two articles have the same title.""" + if any(self.errors): + # Don't bother validating the formset unless each form is valid on its own + return + spectacles = [] + for i in range(0, self.total_form_count()): + form = self.forms[i] + if not form.cleaned_data: + continue + spectacle = form.cleaned_data['spectacle'] + delete = form.cleaned_data['DELETE'] + if not delete and spectacle in spectacles: + raise forms.ValidationError("Vous ne pouvez pas vous inscrire deux fois pour le même spectacle.") + spectacles.append(spectacle) + +@login_required +def inscription(request): + 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(): + formset.save() + success = True + formset = BdaFormSet(instance = participant) + else: + formset = BdaFormSet(instance = participant) + return render_page(request, {"formset": formset, "success": success}, "inscription-bda.html") diff --git a/gestioncof/admin.py b/gestioncof/admin.py index 3ebc50f5..7dc05151 100644 --- a/gestioncof/admin.py +++ b/gestioncof/admin.py @@ -3,6 +3,9 @@ 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 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 @@ -41,12 +44,11 @@ class SurveyAdmin(admin.ModelAdmin): SurveyQuestionInline, ] -@add_link_field() -class EventOptionChoiceInline(admin.StackedInline): +class EventOptionChoiceInline(admin.TabularInline): model = EventOptionChoice -@add_link_field() -class EventOptionInline(admin.StackedInline): +@add_link_field(desc_text = lambda x: "Choix", link_text = lambda x: "Éditer les choix") +class EventOptionInline(admin.TabularInline): model = EventOption class EventOptionAdmin(admin.ModelAdmin): @@ -59,7 +61,40 @@ class EventAdmin(admin.ModelAdmin): EventOptionInline, ] +class CofProfileInline(admin.StackedInline): + model = CofProfile + inline_classes = ("collapse open",) + +class UserProfileAdmin(UserAdmin): + def login_clipper(self, obj): + try: + return obj.get_profile().login_clipper + except UserProfile.DoesNotExist: + return "" + def is_buro(self, obj): + try: + return obj.get_profile().is_buro + except UserProfile.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: + 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') + inlines = [ + CofProfileInline, + ] + admin.site.register(Survey, SurveyAdmin) admin.site.register(SurveyQuestion, SurveyQuestionAdmin) admin.site.register(Event, EventAdmin) admin.site.register(EventOption, EventOptionAdmin) +admin.site.unregister(User) +admin.site.register(User, UserProfileAdmin) +admin.site.register(CofProfile) diff --git a/gestioncof/decorators.py b/gestioncof/decorators.py new file mode 100644 index 00000000..6e2b3028 --- /dev/null +++ b/gestioncof/decorators.py @@ -0,0 +1,21 @@ +from django_cas.decorators import user_passes_test + +def is_cof(user): + try: + profile = user.get_profile() + return profile.is_cof + except: + return False + +def cof_required(login_url = None): + return user_passes_test(lambda u: is_cof(u), login_url=login_url) + +def is_buro(user): + try: + profile = user.get_profile() + return profile.is_buro + except: + return False + +def buro_required(login_url = None): + return user_passes_test(lambda u: is_buro(u), login_url=login_url) diff --git a/gestioncof/models.py b/gestioncof/models.py index 57e40180..a8e0e3eb 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -26,9 +26,11 @@ def choices_length (choices): return reduce (lambda m, choice: max (m, len (choice[0])), choices, 0) class CofProfile(models.Model): - user = models.OneToOneField(User) - login_clipper = models.CharField("Login clipper", max_length = 8) + 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), + phone = models.CharField("Téléphone", max_length = 20, blank = True) occupation = models.CharField (_(u"Occupation"), default = "1A", choices = OCCUPATION_CHOICES, @@ -37,20 +39,27 @@ class CofProfile(models.Model): default = "normalien", choices = TYPE_COTIZ_CHOICES, max_length = choices_length (TYPE_COTIZ_CHOICES)) - mailing_cof = models.BooleanField("Recevoir les mails COF", default = True) - mailing_bda_revente = models.BooleanField("Recevoir les mails de revente de places BDA", default = True) + 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) is_buro = models.BooleanField("Membre du Burô", default = False) + class Meta: + verbose_name = "Profil COF" + verbose_name_plural = "Profils COF" + + def __unicode__(self): + return unicode(self.user.username) + def create_user_profile(sender, instance, created, **kwargs): if created: - CofProfile.objects.create(user = instance) + CofProfile.objects.get_or_create(user = instance) post_save.connect(create_user_profile, sender = User) class Event(models.Model): title = models.CharField("Titre", max_length = 200) location = models.CharField("Lieu", max_length = 200) - start_date = models.DateField("Date de début", blank = True) - end_date = models.DateField("Date de fin", blank = True) + start_date = models.DateField("Date de début", blank = True, null = True) + 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) @@ -61,8 +70,9 @@ class Event(models.Model): return unicode(self.title) class EventOption(models.Model): - event = models.ForeignKey(Event) + event = models.ForeignKey(Event, related_name = "options") name = models.CharField("Option", max_length = 200) + multi_choices = models.BooleanField("Choix multiples", default = False) class Meta: verbose_name = "Option" @@ -71,7 +81,7 @@ class EventOption(models.Model): return unicode(self.name) class EventOptionChoice(models.Model): - event_option = models.ForeignKey(EventOption) + event_option = models.ForeignKey(EventOption, related_name = "choices") value = models.CharField("Valeur", max_length = 200) class Meta: @@ -88,6 +98,7 @@ class EventRegistration(models.Model): class Meta: verbose_name = "Inscription" + unique_together = ("user", "event") class Survey(models.Model): title = models.CharField("Titre", max_length = 200) @@ -124,7 +135,8 @@ class SurveyQuestionAnswer(models.Model): class SurveyAnswer(models.Model): user = models.ForeignKey(User) survey = models.ForeignKey(Survey) - answers = models.ManyToManyField(SurveyQuestionAnswer) + answers = models.ManyToManyField(SurveyQuestionAnswer, related_name = "selected_by") class Meta: verbose_name = "Réponses" + unique_together = ("user", "survey") diff --git a/gestioncof/shared.py b/gestioncof/shared.py index 8c316c6c..f506aed2 100644 --- a/gestioncof/shared.py +++ b/gestioncof/shared.py @@ -1,18 +1,35 @@ 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 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""" user = super(COFCASBackend, self).authenticate(ticket, service) - profile = user.get_profile() + try: + profile = user.get_profile() + except CofProfile.DoesNotExist: + profile, created = CofProfile.objects.get_or_create(user = user) + profile.save() if not profile.login_clipper: profile.login_clipper = user.username profile.save() if not user.email: user.email = settings.CAS_EMAIL_FORMAT % profile.login_clipper user.save() + if profile.is_buro and not user.is_superuser: + user.is_superuser = True + user.is_staff = True + user.save() return user def context_processor (request): diff --git a/gestioncof/templatetags/__init__.py b/gestioncof/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gestioncof/templatetags/utils.py b/gestioncof/templatetags/utils.py new file mode 100644 index 00000000..71096c75 --- /dev/null +++ b/gestioncof/templatetags/utils.py @@ -0,0 +1,12 @@ +from django import template + +register = template.Library() + +def key(d, key_name): + try: + value = d[key_name] + except KeyError: + from django.conf import settings + value = settings.TEMPLATE_STRING_IF_INVALID + return value +key = register.filter('key', key) diff --git a/gestioncof/views.py b/gestioncof/views.py index 6f1afb3c..fbdbe403 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -1,18 +1,18 @@ +# coding: utf-8 from django.shortcuts import redirect, get_object_or_404 -from django.template import RequestContext, loader -from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.http import Http404 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 gestioncof.models import Survey, SurveyQuestion, SurveyQuestionAnswer, SurveyAnswer from gestioncof.models import Event, EventOption, EventOptionChoice, EventRegistration - -def render_page (request, data, template): - template = loader.get_template (template) - context = RequestContext (request, data) - return HttpResponse (template.render (context)) +from gestioncof.models import CofProfile +from gestioncof.shared import render_page +from gestioncof.decorators import buro_required, cof_required +from gestioncof.widgets import TriStateCheckbox @login_required def home(request): @@ -27,7 +27,11 @@ def login(request): @login_required def logout(request): - if request.user.get_profile().login_clipper: + try: + profile = request.user.get_profile() + except CofProfile.DoesNotExist: + profile, created = CofProfile.objects.get_or_create(user = request.user) + if profile.login_clipper: return redirect("django_cas.views.logout") else: return redirect("django.contrib.auth.views.logout") @@ -74,35 +78,294 @@ def survey(request, survey_id): if not survey.survey_open: raise Http404 success = False + deleted = False if request.method == "POST": form = SurveyForm(request.POST, survey = survey) - if form.is_valid(): - all_answers = [] - for question_id, answers_ids in form.answers(): - question = get_object_or_404(SurveyQuestion, id = question_id, - survey = survey) - if type(answers_ids) != list: - answers_ids = [answers_ids] - if not question.multi_answers and len(answers_ids) > 1: - raise Http404 - for answer_id in answers_ids: - answer_id = int(answer_id) - answer = SurveyQuestionAnswer.objects.get( - id = answer_id, - survey_question = question) - all_answers.append(answer) + if request.POST.get('delete'): try: current_answer = SurveyAnswer.objects.get(user = request.user, survey = survey) + current_answer.delete() + current_answer = None except SurveyAnswer.DoesNotExist: - current_answer = SurveyAnswer(user = request.user, survey = survey) - current_answer.save() - current_answer.answers = all_answers - current_answer.save() + current_answer = None + form = SurveyForm(survey = survey) success = True + deleted = True + else: + if form.is_valid(): + all_answers = [] + for question_id, answers_ids in form.answers(): + question = get_object_or_404(SurveyQuestion, id = question_id, + survey = survey) + if type(answers_ids) != list: + answers_ids = [answers_ids] + if not question.multi_answers and len(answers_ids) > 1: + raise Http404 + for answer_id in answers_ids: + if not answer_id: + continue + answer_id = int(answer_id) + answer = SurveyQuestionAnswer.objects.get( + id = answer_id, + survey_question = question) + all_answers.append(answer) + try: + current_answer = SurveyAnswer.objects.get(user = request.user, survey = survey) + except SurveyAnswer.DoesNotExist: + current_answer = SurveyAnswer(user = request.user, survey = survey) + current_answer.save() + current_answer.answers = all_answers + current_answer.save() + success = True else: try: current_answer = SurveyAnswer.objects.get(user = request.user, survey = survey) form = SurveyForm(survey = survey, current_answers = current_answer.answers) except SurveyAnswer.DoesNotExist: + current_answer = None form = SurveyForm(survey = survey) - return render_page(request, {"survey": survey, "form": form, "success": success}, "survey.html") + return render_page(request, {"survey": survey, "form": form, "success": success, "deleted": deleted, "current_answer": current_answer}, "survey.html") + +class EventForm(forms.Form): + def __init__(self, *args, **kwargs): + event = kwargs.pop("event") + current_choices = kwargs.pop("current_choices", None) + super(EventForm, 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) + +@login_required +def event(request, event_id): + event = get_object_or_404(Event, id = event_id) + if not event.registration_open: + raise Http404 + success = False + 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() + current_registration.options = all_choices + current_registration.save() + success = True + else: + try: + current_registration = EventRegistration.objects.get(user = request.user, event = event) + 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") + +class SurveyStatusFilterForm(forms.Form): + def __init__(self, *args, **kwargs): + survey = kwargs.pop("survey") + super(SurveyStatusFilterForm, self).__init__(*args, **kwargs) + answers = {} + for question in survey.questions.all(): + for answer in question.answers.all(): + name = "question_%d_answer_%d" % (question.id, answer.id) + 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 = "%s : %s" % (question.question, answer.answer), + choices = [("yes", "yes"),("no","no"),("none","none")], + widget = TriStateCheckbox, + required = False, + initial = initial) + field.question_id = question.id + field.answer_id = answer.id + self.fields[name] = field + + def filters(self): + for name, value in self.cleaned_data.items(): + if name.startswith('question_'): + yield (self.fields[name].question_id, self.fields[name].answer_id, value) + +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) + 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 = "%s : %s" % (option.name, choice.value), + choices = [("yes", "yes"),("no","no"),("none","none")], + widget = TriStateCheckbox, + required = False, + initial = initial) + field.option_id = option.id + field.choice_id = choice.id + 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) + +def clean_post_for_status(initial): + d = dict(initial) + 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 + return d + +@buro_required() +def event_status(request, event_id): + event = get_object_or_404(Event, id = event_id) + registrations_query = EventRegistration.objects.filter(event = event) + post_data = clean_post_for_status(request.POST) + form = EventStatusFilterForm(post_data or None, event = event) + if form.is_valid(): + for option_id, choice_id, value in form.filters(): + choice = get_object_or_404(EventOptionChoice, id = choice_id, event_option__id = option_id) + if value == "none": + continue + if value == "yes": + registrations_query = registrations_query.filter(options__id__exact = choice.id) + elif value == "no": + registrations_query = registrations_query.exclude(options__id__exact = choice.id) + user_choices = registrations_query.prefetch_related("user").all() + options = EventOption.objects.filter(event = event).all() + choices_count = {} + for option in options: + for choice in option.choices.all(): + choices_count[choice.id] = 0 + 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") + +@buro_required() +def survey_status(request, survey_id): + survey = get_object_or_404(Survey, id = survey_id) + answers_query = SurveyAnswer.objects.filter(survey = survey) + post_data = clean_post_for_status(request.POST) + form = SurveyStatusFilterForm(post_data or None, survey = survey) + if form.is_valid(): + for question_id, answer_id, value in form.filters(): + answer = get_object_or_404(SurveyQuestionAnswer, id = answer_id, survey_question__id = question_id) + if value == "none": + continue + if value == "yes": + answers_query = answers_query.filter(answers__id__exact = answer.id) + elif value == "no": + answers_query = answers_query.exclude(answers__id__exact = answer.id) + user_answers = answers_query.prefetch_related("user").all() + questions = SurveyQuestion.objects.filter(survey = survey).all() + answers_count = {} + for question in questions: + for answer in question.answers.all(): + answers_count[answer.id] = 0 + 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") + +class UserProfileForm(forms.ModelForm): + first_name = forms.CharField(label=_(u'Prénom'), max_length=30) + last_name = forms.CharField(label=_(u'Nom'), max_length=30) + + def __init__(self, *args, **kw): + super(UserProfileForm, self).__init__(*args, **kw) + self.fields['first_name'].initial = self.instance.user.first_name + self.fields['last_name'].initial = self.instance.user.last_name + + self.fields.keyOrder = [ + 'first_name', + 'last_name', + 'phone', + 'mailing_cof', + 'mailing_bda_revente', + ] + + def save(self, *args, **kw): + super(UserProfileForm, self).save(*args, **kw) + self.instance.user.first_name = self.cleaned_data.get('first_name') + self.instance.user.last_name = self.cleaned_data.get('last_name') + self.instance.user.save() + + class Meta: + model = CofProfile + fields = ("phone", "mailing_cof", "mailing_bda_revente",) + +@login_required +def profile(request): + success = False + if request.method == "POST": + form = UserProfileForm(request.POST, instance = request.user.get_profile()) + if form.is_valid(): + form.save() + success = True + else: + form = UserProfileForm(instance = request.user.get_profile()) + return render_page(request, {"form": form, "success": success}, "profile.html") + +@login_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") diff --git a/gestioncof/widgets.py b/gestioncof/widgets.py new file mode 100644 index 00000000..e41ed3e7 --- /dev/null +++ b/gestioncof/widgets.py @@ -0,0 +1,18 @@ +from django.forms.widgets import Widget +from django.forms.util import flatatt +from django.utils.safestring import mark_safe + +class TriStateCheckbox(Widget): + + def __init__(self, attrs=None, choices=()): + super(TriStateCheckbox, self).__init__(attrs) + # choices can be any iterable, but we may need to render this widget + # multiple times. Thus, collapse it into a list so it can be consumed + # more than once. + self.choices = list(choices) + + def render(self, name, value, attrs=None, choices=()): + if value is None: value = 'none' + final_attrs = self.build_attrs(attrs, value=value) + output = [u"" % flatatt(final_attrs)] + return mark_safe('\n'.join(output)) diff --git a/media/cof.css b/media/cof.css index 6e9e6388..6779febe 100644 --- a/media/cof.css +++ b/media/cof.css @@ -12,23 +12,136 @@ html,body { clear: both; } -#cof a:link, #cof a:active, #cof a:visited { +a:link, a:active, a:visited { color: #111; background: transparent; } -#cof a:hover { +a:hover { color: #444; background: transparent; } -#cof form { - display: block; - padding: 0; - width: 100%; - } +form#profile table { + border-collapse: collapse; +} -#cof fieldset { +table#bda_formset { + width: 100%; +} + +.bda-field-spectacle { + width: 65%; +} + +.bda-field-spectacle select { + width: 94%; + margin: 0 3%; +} + +.bda-field-double, .bda-field-autoquit, .bda-field-priority { + text-align: center; +} + +.bda-field-double { + width: 15%; +} + +.bda-field-autoquit { + width: 15%; +} + +.tools-cell { + width: 32px; +} + +.tools { + width: 32px; +} + +.tools a.icon.drag-handler, .tools a.icon.delete-handler { + float: left; +} + +.tools a.icon.drag-handler { + margin-right: 5px; +} + +form#bda_form p { + font-size: 0.8em; +} + +table#bda_formset { + border-spacing: 0px 5px; +} + +tbody.bda_formset_content { +} + +tr.dynamic-form td { + border-width: 1px 1px 1px 0px; + border-style: solid; + border-color: #888; + background: #EEE; +} + +tr.dynamic-form td:first-child { + border-left-width: 1px; +} + +tr.dynamic-form.predelete td { + background-color: #FFECEC; + border-width: 1px 1px 1px 0px; + border-style: solid; + border-color: #CCC; +} + +tr.dynamic-form.predelete td:first-child { + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + border: 1px solid #CCC; +} + +tr.dynamic-form.predelete td:last-child { + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; +} + + +.ui-sortable-helper { +} + +tr.ui-sortable-placeholder td, .placeholder-cell { + background: transparent; + border-width: 1px 1px 1px 0px; + border-style: solid; + border-color: #CCC; +} + +tr.ui-sortable-placeholder td:first-child { + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + border: 1px solid #CCC; +} + +tr.ui-sortable-placeholder td:last-child { + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; +} + +form { + padding: 0; +} + +form#profile table td, form#profile table th { + width: 400px; +} + +form#profile table th { + text-align: right; +} + +fieldset { border: 0; margin: 0; padding: 0; @@ -37,16 +150,16 @@ html,body { width: auto; } -#cof fieldset legend { +fieldset legend { display: none; } -#cof #main-login-container { +#main-login-container { width: 500px; margin: 7em auto; } -#cof #main-login { +#main-login { width: 500px; border: 15px solid #333; -webkit-border-radius: 20px; @@ -54,13 +167,14 @@ html,body { border-radius: 20px; } -#cof #main-container { +#main-container { + max-width: 90%; width: 800px; margin: 7em auto; + display: block; } -#cof #main { - width: 800px; +#main { border: 15px solid #333; -webkit-border-radius: 20px; -moz-border-radius: 20px; @@ -69,61 +183,72 @@ html,body { box-shadow: 0 0 100px #AAA inset; } -#cof #main #main-content { +#main #main-content { font-size: 1.25em; margin-top: 10px; } -#cof #main #main-content ul { +#main #main-content ul { line-height: 1.3em; } -#cof #main h1 { +#main h1 { font-size: 3em; } -#cof #main h1 a { +#main h1 a { color: #333; text-decoration: none; } -#cof #main h2 { +#main h2 { font-size: 1.5em; margin-bottom: 10px; } -#cof #main h3 { +#main h3 { font-size: 1.3em; + margin-top: 5px; } -#cof #main p { +#main h4 { + margin-top: 10px; + font-weight: bold; +} + +#main p { margin-top: 8px; margin-bottom: 8px; } -#cof #main form li { +#main form li { list-style: none; } -#cof .success { +.success { font-weight: bold; color: #00B000; background-color: transparent; } -#cof #main form ul.errorlist li { +#main form ul.errorlist li { font-weight: bold; color: #B00000; background-color: transparent; list-style: disc; } -#cof #main-login.login_block { +form#bda_form ul.errorlist li { + padding-left: 0px; + list-style: none; +} + +#main-login.login_block { padding: 2em; box-shadow: 0 0 100px #AAA inset; } -#cof a#login_clipper, #cof a#login_outsider { +a#login_clipper, a#login_outsider { float: left; display: block; width: 250px; @@ -137,41 +262,41 @@ html,body { color: #FFF; } -#cof a#login_clipper { +a#login_clipper { background-color: #123E96; box-shadow: 0 0 100px #040C78 inset; } -#cof a#login_clipper:hover { +a#login_clipper:hover { background-color: #164BB6; } -#cof a#login_outsider { +a#login_outsider { background-color: #961221; box-shadow: 0 0 100px #780411 inset; } -#cof a#login_outsider:hover { +a#login_outsider:hover { background-color: #B31729; } -#cof #main-login label { +#main-login label { font-size: 11px; } -#cof #main-login label span.accesskey { +#main-login label span.accesskey { text-decoration: underline; } -#cof #main-login input { +#main-login input { letter-spacing: 1px; } -#cof #main-login .btn-row { +#main-login .btn-row { float: right; } -#cof .btn-submit { +.btn-submit, .btn-addmore { float: none; clear: none; display: inline; @@ -186,7 +311,7 @@ html,body { background: linear-gradient(center top, #EEE, #CCC); } -#cof #main-login .btn-reset { +#main-login .btn-reset { float: none; clear: none; margin-left: 5px; @@ -200,7 +325,7 @@ html,body { /* RESET --------------------------------- */ /* reset some properties for elements since defaults are not crossbrowser - http: //meyerweb.com/eric/thoughts/2007/05/01/reset-reloaded/ */ -html,body,div,span,h1,h2,h3,p,a,img,ul,li,fieldset,form,label,legend { +html,body,div,span,h1,h2,h3,h4,p,a,img,ul,li,fieldset,form,label,legend { margin: 0; padding: 0; border: 0; @@ -245,7 +370,7 @@ tt { } /* FORMS --------------------------------- */ -input { +#main-login input { border-width: 1px; font-family: Verdana,sans-serif; font-size: 1.1em; @@ -254,7 +379,7 @@ input { min-height: 1.5em; } -input[type="text"], input[type=password] { +#main-login input[type="text"], #main-login input[type=password] { border: 2px solid #888; -webkit-border-radius: 8px; -moz-border-radius: 8px; @@ -263,6 +388,15 @@ input[type="text"], input[type=password] { min-height: 2em; } +input[type="text"], input[type=password] { + border: 1px solid #888; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + box-shadow: 0 0 3px #AAA inset; + padding: 0px 2px 0px 2px; +} + hr { border: 0; height: 1px; @@ -275,24 +409,6 @@ hr { background: linear-gradient(left, hsla(0,0%,70%,0) 0%, hsla(0,0%,70%,.75) 50%, hsla(0,0%,70%,0) 100%); } -.fm-v div.row { - margin: 0; - padding: .5em 0; - width: 100%; -} - -.fm-v div.row label { - float: left; - width: 100%; - line-height: 1.5; -} - -.fm-v div.row input.btn-submit { - display: block; - margin: 0; -} - -.fm-v div.row.fl-controls-left { - width: 50%; - float: left; +.tristate:hover { + cursor: pointer; } diff --git a/media/droidserif.woff b/media/droidserif.woff new file mode 100644 index 0000000000000000000000000000000000000000..3517fef804332520134b0bec174287426935c9be GIT binary patch literal 31596 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�XWK z;yHJd;W8+YtBu{5xmx~8RusdL|HQ&e>Lj{8TS3W9KVzmUylD9oX|TsT!&f zEjp2Pg2cWSu7-DY5v%xZmlZlMwNez19iKh2P8UInspUvSj=>DiWhKg;Zo_eGBnw@; zE_`^--gc*1&Ssavh8ZjNH7+rCZ;7kpUCo+a?TvPRaED~Bj;5k&^>Y&&RJ zu53A1xe2zYu}8+vqke^dQIAS$AD!E}P&iOR=8dUS^%{RDY4joon#oNakR&?nz6)F7 zh*oie?O3^K9jQN~xri#ap(*to6^XUdWhvYtRXDw(%`{4)Hz9B*yT;VySQYpv-j1fx zkP#aLJE%vtv~jLx6N@gBwDAtlOU>CbJU5=qw8x|aHl?Wy`b5gK)HUhK(KOElD))#X ze+YfNW_+a>=G94&$(%?fMSa{DWAm`zpUq~emT{5cv`EoT|Nql=vte}xc+NR-R#-Xz zYMOE`dG;Zl#0vF~xp9`;c)BzLyVgD6!pmf}x@Y%Q6E40_L;gO&vCZ0gnDWG=N;qhA zhJW&AS9Kq&VLG@vG7IA=N2S2G<%$ylCCBg-hxQSiU@siumra~ljtCo1oiF-j>p94 zBH@WxFT_afi1C2fD>WZ@wqw82c=jc~f8ZzRpFa^1|5H#{Kv0TQMHH2$m~_RJkfEef zN-LwRa>}cqqDm^OqN-}DtD&Y^YOABJdg^PSp+*{OqN!$@YoVo9S_^5Tt#;b$prcMY z>!Pb}y6d5*Ouc03t&ePd_0!(~0}V3R5JL?!+z2C$GTInpjWgZ^6HPMN6jMzz-3&9G zaKr{1UA5Oco9wjRe#aaQ1Z@kXSZ|N-e%N83EpB-0n*)yf<)_~cJL#_5PMIagF0%BJ@C*Y?+fo0c;czk=J;y27oK@;u8%(3YM!Y17FrB({3=<16Zp4j8f|Ly=R`>xT{sAqcc@FheTsjcl6eryUh%gu2UlSa8QZs1W3^3|)V6tAYL(&!e} zV3j7912rZGYE(QQl*Xl53GWn*Vy1}YBJ5obE+u3ecE%ps6Fai3FVP5KD(jdMT){rZ zo$W%?!XsbV0m6z}Ue^1AT literal 0 HcmV?d00001 diff --git a/media/images/no.png b/media/images/no.png new file mode 100644 index 0000000000000000000000000000000000000000..8fad9e425d0df0e6bfaf1032181eb1f8a97085b1 GIT binary patch literal 686 zcmV;f0#W^mP)gl6LP*ng5RfVdm*4qARH3w=H z$iGRh#`z(M6C(kTAPH9D0RQjTyjs1F!aU;??x9-!8u?6ubBUb@X-Bj4uq2SYLb8i|OXN3}AaP>pV~GdZ2e9{${j$VJT(PvVLb8oUWik4R0fiF? zt^n8svKrv?*mES?)!KjK-;8JyZ;J%U$Cp>Y-v1Q&g2=y@+H6|7C}}G4(2_DlZa0x% zNsP#YB)57Wz&@b&;VRirvUh3Z{FLo;l3rbE@ROwN$=crCH)FqPyCpen=`)FUA|vq) z$9Li!i(Rn~#NLS{vENJb{bX&=?!RoU#@VtxOY#zu+p>O+9oyd_*-E_s-v7t`5y+_| zsz*20kz^YC0)pc~3PA!a0Q?4EtqL=YaRGqq)!Najx`irCX1jI)iQ-&zOxsT+nJ#Nz zk{uag$LCRa1;`%&5CrweLl0t17LcS8&yu_tU=Cm>fC0d@s=8EF*Q+YEj6SMx7h1J^ UEtmmUi~s-t07*qoM6N<$g0~AoAxoL^b$73I}jopMW+n5o*-^1@p0=Od8$&iqUupIIWpuiE5ACm-N7GN#Y z9BYrj*+7_Rfad^A<{LvNk`M>TuSrfNsU#QHqX7$5bq*m=BrYLE2m-StI0i7u*WHb~ z$@I2gs*8>fXcGS<)pwGJL=t9?9zSMbejZ8KX64zx1Q6_z>N4`)X*l0Yb)qUpqEu6= z^(+E*?cB-!x8I_wP}Lbpr`JmTO41jqvQ(7B${1W^v$DGSe+FTUfkZ%`03QOst^cn_bLakp z%@Ve4CfjG*cx`U`bQjKNvA>KxAN#7iaS#4}^7JJGZ6>eJ?O<{DE;Kdw6xgv=&l|u$ zvHA1i@=FHAs|duw-Phb5cQ3_$G#%Sk%wEw>2P6eJ3>*ON20S@$HFyDA<5^OLQpi{U O0000|Z+CnJeWGLwjy0Ac$v!0_tME2o6>KMt8rr@XeigL*+T zfx+G!CX<5*&eYg1%bE{oH#SZ+*j!&Hfa)D*!#3G?A%{GuKEAb>p99P943+@n6E5cI zC2g>Q;iYVOt@pkgCqu^?Fl1E)#FfnUxvHL171cXco$4J)FWb7-$8ooh$Bg4hAIIBO z{e7v92JpwWzi?GXBiJ{dgR#v})jzd#6*%VwgtNfcKy_&#PMrTCTiXZjr;I~HO9o5d zN_xAht^)62$CLe7EZKtE_pP-Lf%~>mCK<`?J9lW?n;TeqXzR=BeYL7CNy^2Sg{y7= z9{>;h#FgXYV}82(D}O!wo3^#SMoFktwy&$|$EvGT8t0<-~Nv-JNxlK=n!07*qoM6N<$f`3USmjD0& literal 0 HcmV?d00001 diff --git a/settings.py b/settings.py index eab3e36e..8427f3b7 100644 --- a/settings.py +++ b/settings.py @@ -95,7 +95,7 @@ TEMPLATE_LOADERS = ( ) TEMPLATE_CONTEXT_PROCESSORS = ( - "django.core.context_processors.auth", + "django.contrib.auth.context_processors.auth", "django.core.context_processors.debug", "django.core.context_processors.i18n", "django.core.context_processors.media", @@ -116,6 +116,7 @@ ROOT_URLCONF = 'urls' TEMPLATE_DIRS = ( "/home/gestion/www/templates/gestioncof", + "/home/gestion/www/templates/bda", ) LOGIN_URL = "/gestion/login" @@ -142,6 +143,9 @@ INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.admindocs', 'gestioncof', + 'bda', + 'pads', + 'rezo', ) # A sample logging configuration. The only tangible logging diff --git a/templates/bda/inscription-bda.html b/templates/bda/inscription-bda.html new file mode 100644 index 00000000..110218a6 --- /dev/null +++ b/templates/bda/inscription-bda.html @@ -0,0 +1,115 @@ +{% extends "base_title.html" %} + +{% block extra_head %} + + + + + +{% endblock %} + +{% block realcontent %} + + +

Inscription au tirage au sort du BDA

+ {% if success %} +

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

+ {% endif %} +
+ {% csrf_token %} + {% include "inscription-formset.html" %} + + + +
+

+ 1: demander deux places pour ce spectable
+ 2: abandonner une place si impossible d'en obtenir une seconde pour ce spectacle (si vous avez coché l'option Deux places pour ce spectacle)
+ 3: cette liste de vœu est ordonnée (du plus important au moins important), pour ajuster la priorité vous pouvez déplacer chaque vœu
+

+
+{% endblock %} diff --git a/templates/bda/inscription-formset.html b/templates/bda/inscription-formset.html new file mode 100644 index 00000000..04b68a6b --- /dev/null +++ b/templates/bda/inscription-formset.html @@ -0,0 +1,40 @@ +{{ formset.non_form_errors.as_ul }} + +{{ formset.management_form }} +{% for form in formset.forms %} + {% if forloop.first %} + + {% for field in form.visible_fields %} + {% if field.name != "DELETE" and field.name != "priority" %} + + {% endif %} + {% endfor %} + + + + {% endif %} + + {% for field in form.visible_fields %} + {% if field.name != "DELETE" and field.name != "priority" %} + + {% endif %} + {% endfor %} + + +{% endfor %} + +
{{ field.label|safe|capfirst }}3
+ {% if forloop.first %} + {{ form.non_field_errors }} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% endif %} + {{ field.errors.as_ul }} + {{ field }} +
+ + + + +
+
+
diff --git a/templates/gestioncof/base.html b/templates/gestioncof/base.html index 933417ab..ac30e575 100644 --- a/templates/gestioncof/base.html +++ b/templates/gestioncof/base.html @@ -4,6 +4,7 @@ {{ site.name }} + {% block extra_head %}{% endblock %} {% block content %}{% endblock %} diff --git a/templates/gestioncof/base_title.html b/templates/gestioncof/base_title.html index 91f6fd73..a78825c4 100644 --- a/templates/gestioncof/base_title.html +++ b/templates/gestioncof/base_title.html @@ -6,9 +6,9 @@ +
+ {% block realcontent %}{% endblock %}
diff --git a/templates/gestioncof/event.html b/templates/gestioncof/event.html new file mode 100644 index 00000000..df75f506 --- /dev/null +++ b/templates/gestioncof/event.html @@ -0,0 +1,16 @@ +{% extends "base_title.html" %} + +{% block realcontent %} +

Événement: {{ event.title }}

+ {% if success %} +

Votre inscription a bien été enregistrée ! Vous pouvez cependant la modifier jusqu'à la fin des inscriptions.

+ {% endif %} + {% if event.details %} +

{{ event.details }}

+ {% endif %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} diff --git a/templates/gestioncof/event_status.html b/templates/gestioncof/event_status.html new file mode 100644 index 00000000..ec320628 --- /dev/null +++ b/templates/gestioncof/event_status.html @@ -0,0 +1,41 @@ +{% extends "base_title.html" %} +{% load utils %} + +{% block realcontent %} +

Événement: {{ event.title }}{% if user.is_staff %} – Administration{% endif %}

+ {% if event.details %} +

{{ event.details }}

+ {% endif %} + {% include "tristate_js.html" %} +

Filtres

+
+ {% csrf_token %} + {{ form.as_p }} + +
+

Résultats globaux

+ {% for option in options %} +

{{ option.value }}

+
    + {% for choice in option.choices.all %} +
  • {{ choice.value }} : {{ choices_count|key:choice.id }}
  • + {% endfor %} +
+ {% endfor %} +

Réponses individuelles

+
    + {% for user_choice in user_choices %}{% with user_choice.user as auser %} + {% if user_choice.options.all %} +
  • + {% if auser.first_name and auser.last_name %}{{ auser.first_name }} {{ auser.last_name }} + {% else %}{{ auser.username }}{% endif %} : +
      + {% for choice in user_choice.options.all %} +
    • {{ choice.event_option.name }} : {{ choice.value }}
    • + {% endfor %} +
    +
  • + {% endif %} + {% endwith %}{% endfor %} +
+{% endblock %} diff --git a/templates/gestioncof/home.html b/templates/gestioncof/home.html index 586b6a98..b4a635b0 100644 --- a/templates/gestioncof/home.html +++ b/templates/gestioncof/home.html @@ -18,11 +18,25 @@ {% endfor %} {% endif %} + {% if user.get_profile.is_buro %} +

Administration

+ + {% endif %}

Divers

{% endblock %} diff --git a/templates/gestioncof/profile.html b/templates/gestioncof/profile.html new file mode 100644 index 00000000..a3f7281d --- /dev/null +++ b/templates/gestioncof/profile.html @@ -0,0 +1,15 @@ +{% extends "base_title.html" %} + +{% block realcontent %} +

Modifier mon profil

+ {% if success %} +

Votre profil a été mis à jour avec succès !

+ {% endif %} +
+ {% csrf_token %} + + {{ form.as_table }} +
+ +
+{% endblock %} diff --git a/templates/gestioncof/registration.html b/templates/gestioncof/registration.html new file mode 100644 index 00000000..9f8b8f41 --- /dev/null +++ b/templates/gestioncof/registration.html @@ -0,0 +1,15 @@ +{% extends "base_title.html" %} + +{% 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.html b/templates/gestioncof/survey.html index 1de9e174..29520c84 100644 --- a/templates/gestioncof/survey.html +++ b/templates/gestioncof/survey.html @@ -3,8 +3,12 @@ {% block realcontent %}

Sondage: {{ survey.title }}

{% if success %} + {% if deleted %} +

Votre réponse a bien été supprimée !

+ {% else %}

Votre réponse a bien été enregistrée ! Vous pouvez cependant la modifier jusqu'à la fin du sondage.

{% endif %} + {% endif %} {% if survey.details %}

{{ survey.details }}

{% endif %} @@ -12,5 +16,8 @@ {% csrf_token %} {{ form.as_p }} + {% if current_answer %} + + {% endif %} {% endblock %} diff --git a/templates/gestioncof/survey_status.html b/templates/gestioncof/survey_status.html new file mode 100644 index 00000000..4c1a58b1 --- /dev/null +++ b/templates/gestioncof/survey_status.html @@ -0,0 +1,41 @@ +{% extends "base_title.html" %} +{% load utils %} + +{% block realcontent %} +

Sondage: {{ survey.title }}{% if user.is_staff %} – Administration{% endif %}

+ {% if survey.details %} +

{{ survey.details }}

+ {% endif %} +

Filtres

+ {% include "tristate_js.html" %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+

Résultats globaux

+ {% for question in questions %} +

{{ question.question }}

+
    + {% for answer in question.answers.all %} +
  • {{ answer.answer }} : {{ answers_count|key:answer.id }}
  • + {% endfor %} +
+ {% endfor %} +

Réponses individuelles

+
    + {% for user_answer in user_answers %}{% with user_answer.user as auser %} + {% if user_answer.answers.all %} +
  • + {% if auser.first_name and auser.last_name %}{{ auser.first_name }} {{ auser.last_name }} + {% else %}{{ auser.username }}{% endif %} : +
      + {% for answer in user_answer.answers.all %} +
    • {{ answer.survey_question.question }} : {{ answer.answer }}
    • + {% endfor %} +
    +
  • + {% endif %} + {% endwith %}{% endfor %} +
+{% endblock %} diff --git a/templates/gestioncof/tristate_js.html b/templates/gestioncof/tristate_js.html new file mode 100644 index 00000000..4e292f9d --- /dev/null +++ b/templates/gestioncof/tristate_js.html @@ -0,0 +1,69 @@ + diff --git a/urls.py b/urls.py index c3f23942..a07109d2 100644 --- a/urls.py +++ b/urls.py @@ -10,7 +10,13 @@ urlpatterns = patterns('', 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')),