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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�_A9?*w?ME+iEcqU3-Z5)xd$o`E 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 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 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!toNlosX?9=DWQT}G~?a(*%jrpoOa(>smNl$+7{b|PA{KR0Lb6O^Lj^yIbD{kMC?e+zv3x4X(-uSju|&fIMwzE6ym)9;D=+;gJer7ijWniJ1| z88qx&y!pPuPfbq`E1gXD&0jXCOxWYJsk_GeiTN~z!lakUo$NQ+H+-M)cheD%n2lRD zI&JE8OEvt}$=E8)v5w&e%Oe^YYLzuTX)_Snr$w%(tF zCf(jt*C%`XzT!=|k@sngF6OTU+tYAGM&mh3^^@KBjl8@MgB|_&X x?U6{}OHglMJg{}uu8`2{tF^ZF*sUrMkJ?(JJA*O2hk=2C!PC{xWt~$(69BghLgWAd literal 0 HcmV?d00001 diff --git a/media/images/none.png b/media/images/none.png new file mode 100644 index 0000000000000000000000000000000000000000..701a5a3b42defa9f0c8c09dcf4b4a74460cf42f7 GIT binary patch literal 576 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!to*gahwLo9l8Cmrm*o+@#~ z{__uKajzKhqV512&j}ayT9tUYe0UPr#3bnN^tHUBtaq1)+^Y+&8&o`4^W(w)?>??5zyI3s%H8A1@|%0_wiw)=A-Mnk;rCulJCv@ToG_tdgUl!H1qTEt$xmiU z5KGX#ER)@T;HJZjjE3h7KF_3`)h7wCpIGndr@B%_{Muqh?$A;`#Q+sOMOO}{%_ch_Vng{7J)YtUm1P6muYd| zyL6G3+oF)*RbO?O%HG~N`EUFEMXLg?N_sB8?)BH}lK84kAybZ=l8A}lwK0=h`ZmY5 zrB9e0JifC`wpgfT6EAz`*I)i^CbJ~Un_Chj4GK635ME8^g{^AF$u}^Yg<`-fTx#a~wH%_xc^#a{Z%qCbGA~&ct2xV^?9d k;E`c?$7W-hZ(6|^{WvnHHRwbH0|Nttr>mdKI;Vst0QhAHVE_OC literal 0 HcmV?d00001 diff --git a/media/images/yes.png b/media/images/yes.png new file mode 100644 index 0000000000000000000000000000000000000000..edb3aa51d81673c7605eb14dd2ce506723c39d5e GIT binary patch literal 610 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!toG(BA$Lo9lGC);+<4Hapf zzxTZL_j9giYL3hl=v`|3qp_Fm(2Cd;0Y?Gd&&q%F|1qqNh~N&M;UUG%&8_G=WroCF zKV?ZDkx7;3<4!+PW1g<~;w9hgt>{KHY5g$9MPSHs8$InPB{9dPS+#(@RIY zeZs<4ykglH(x3i<;l{mNkItIMsVjH-7)|b&!I1dd@W6*x=d_o%e75JC>9>5(w%yHD zj>e4#@5!i5Zss_5<%a(0S^VkdW`3qOV&c^sUdg5(HoIA&*lx4JZr|1!^O;Y-=b6Uv zSXl4bOV!*BjSrXRRIdHLPg>pixRgOkDC?1_XTI%RH7{87s&(kxSF)b^w|8ATUbwyg zc+X=G`Qx`k|JJVTWBYJ?+x~)3w3Nd7(*7NjwW_z@%=G@=(ck0sWi9aQOOx(k8?e_`& z&(2j>i^S&5Ut5${%Nu$AOlwZv|IZnoHJ2C`q!!gZ=5fyU<}|*r+QO#F;pG?R|4dsL X<2Qf#Z#$WRfq}u()z4*}Q$iB}e@P~n 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')),