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 00000000..3517fef8 Binary files /dev/null and b/media/droidserif.woff differ diff --git a/media/images/no.png b/media/images/no.png new file mode 100644 index 00000000..8fad9e42 Binary files /dev/null and b/media/images/no.png differ diff --git a/media/images/none.png b/media/images/none.png new file mode 100644 index 00000000..701a5a3b Binary files /dev/null and b/media/images/none.png differ diff --git a/media/images/yes.png b/media/images/yes.png new file mode 100644 index 00000000..edb3aa51 Binary files /dev/null and b/media/images/yes.png differ 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 }}

+ + {% endfor %} +

Réponses individuelles

+ +{% 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 }}

+ + {% endfor %} +

Réponses individuelles

+ +{% 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')),