diff --git a/README.md b/README.md index b304772c..15f0ec0d 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ Il ne vous reste plus qu'à initialiser les modèles de Django avec la commande Une base de donnée pré-remplie est disponible en lançant la commande : - python manage.py loaddata users bda gestion + python manage.py loaddata users root bda gestion sites Vous êtes prêts à développer ! Lancer GestioCOF en faisant @@ -171,6 +171,6 @@ Pour mettre à jour les modèles après une migration, il faut ensuite faire : ## Documentation utilisateur -Une brève documentation utilisateur pour se faliliariser plus vite avec l'outil +Une brève documentation utilisateur pour se familiariser plus vite avec l'outil est accessible sur le [wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home). diff --git a/bda/admin.py b/bda/admin.py index eb8d3106..b23d79e0 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -9,7 +9,7 @@ from django.core.mail import send_mail from django.contrib import admin from django.db.models import Sum, Count from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ - Attribution, Tirage + Attribution, Tirage, Quote, CategorieSpectacle from django import forms from datetime import timedelta @@ -182,7 +182,12 @@ class ChoixSpectacleAdmin(admin.ModelAdmin): 'spectacle__title') +class QuoteInline(admin.TabularInline): + model = Quote + + class SpectacleAdmin(admin.ModelAdmin): + inlines = [QuoteInline] model = Spectacle list_display = ("title", "date", "tirage", "location", "slots", "price", "listing") @@ -194,7 +199,7 @@ class SpectacleAdmin(admin.ModelAdmin): class TirageAdmin(admin.ModelAdmin): model = Tirage list_display = ("title", "ouverture", "fermeture", "active", - "enable_do_tirage") + "enable_do_tirage") readonly_fields = ("tokens", ) list_filter = ("active", ) search_fields = ("title", ) @@ -205,6 +210,7 @@ class SalleAdmin(admin.ModelAdmin): search_fields = ('name', 'address') +admin.site.register(CategorieSpectacle) admin.site.register(Spectacle, SpectacleAdmin) admin.site.register(Salle, SalleAdmin) admin.site.register(Participant, ParticipantAdmin) diff --git a/bda/fixtures/bda.json b/bda/fixtures/bda.json index d9bc1155..bb9fd73d 100644 --- a/bda/fixtures/bda.json +++ b/bda/fixtures/bda.json @@ -74,7 +74,6 @@ "description": "Jazz / Funk", "title": "Un super concert", "price": 10.0, - "priority": 1000, "rappel_sent": null, "location": 2, "date": "2016-09-30T18:00:00Z", @@ -91,7 +90,6 @@ "description": "Homemade", "title": "Une super pi\u00e8ce", "price": 10.0, - "priority": 1000, "rappel_sent": null, "location": 3, "date": "2016-09-29T14:00:00Z", @@ -108,7 +106,6 @@ "description": "Plein air, soleil, bonne musique", "title": "Concert pour la f\u00eate de la musique", "price": 5.0, - "priority": 1000, "rappel_sent": null, "location": 1, "date": "2016-09-21T15:00:00Z", @@ -125,7 +122,6 @@ "description": "Sous le regard s\u00e9v\u00e8re de Louis Pasteur", "title": "Op\u00e9ra sans d\u00e9cors", "price": 5.0, - "priority": 1000, "rappel_sent": null, "location": 4, "date": "2016-10-06T19:00:00Z", @@ -142,7 +138,6 @@ "description": "Buffet \u00e0 la fin", "title": "Concert Trouv\u00e8re", "price": 20.0, - "priority": 1000, "rappel_sent": null, "location": 5, "date": "2016-11-30T12:00:00Z", @@ -159,7 +154,6 @@ "description": "Vive les maths", "title": "Dessin \u00e0 la craie sur tableau noir", "price": 10.0, - "priority": 1000, "rappel_sent": null, "location": 6, "date": "2016-12-15T07:00:00Z", @@ -176,7 +170,6 @@ "description": "Une pi\u00e8ce \u00e0 un personnage", "title": "D\u00e9cors, d\u00e9montage en musique", "price": 0.0, - "priority": 1000, "rappel_sent": null, "location": 3, "date": "2016-12-26T07:00:00Z", @@ -193,7 +186,6 @@ "description": "Annulera, annulera pas\u00a0?", "title": "La Nuit", "price": 27.0, - "priority": 1000, "rappel_sent": null, "location": 1, "date": "2016-11-14T23:00:00Z", @@ -210,7 +202,6 @@ "description": "Le boum fait sa carte blanche", "title": "Turbomix", "price": 10.0, - "priority": 1000, "rappel_sent": null, "location": 2, "date": "2017-01-10T20:00:00Z", @@ -227,7 +218,6 @@ "description": "Unique repr\u00e9sentation", "title": "Carinettes et trombone", "price": 15.0, - "priority": 1000, "rappel_sent": null, "location": 5, "date": "2017-01-02T14:00:00Z", @@ -244,7 +234,6 @@ "description": "Suivi d'une jam session", "title": "Percussion sur rondins", "price": 5.0, - "priority": 1000, "rappel_sent": null, "location": 4, "date": "2017-01-13T14:00:00Z", @@ -261,7 +250,6 @@ "description": "\u00c9preuve sportive et artistique", "title": "Bassin aux ernests, nage libre", "price": 5.0, - "priority": 1000, "rappel_sent": null, "location": 1, "date": "2016-11-17T09:00:00Z", @@ -278,7 +266,6 @@ "description": "Sonore", "title": "Chant du barde", "price": 13.0, - "priority": 1000, "rappel_sent": null, "location": 2, "date": "2017-02-26T07:00:00Z", @@ -295,7 +282,6 @@ "description": "Cocorico", "title": "Chant du coq", "price": 4.0, - "priority": 1000, "rappel_sent": null, "location": 1, "date": "2016-12-17T04:00:00Z", diff --git a/bda/migrations/0007_extends_spectacle.py b/bda/migrations/0007_extends_spectacle.py new file mode 100644 index 00000000..b95c18de --- /dev/null +++ b/bda/migrations/0007_extends_spectacle.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0006_add_tirage_switch'), + ] + + operations = [ + migrations.CreateModel( + name='CategorieSpectacle', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=100, verbose_name='Nom', + unique=True)), + ], + options={ + 'verbose_name': 'Cat\xe9gorie', + }, + ), + migrations.CreateModel( + name='Quote', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('text', models.TextField(verbose_name='Citation')), + ('author', models.CharField(max_length=200, + verbose_name='Auteur')), + ], + ), + migrations.AlterModelOptions( + name='spectacle', + options={'ordering': ('date', 'title'), + 'verbose_name': 'Spectacle'}, + ), + migrations.RemoveField( + model_name='spectacle', + name='priority', + ), + migrations.AddField( + model_name='spectacle', + name='ext_link', + field=models.CharField( + max_length=500, + verbose_name='Lien vers le site du spectacle', + blank=True), + ), + migrations.AddField( + model_name='spectacle', + name='image', + field=models.ImageField(upload_to='imgs/shows/', null=True, + verbose_name='Image', blank=True), + ), + migrations.AlterField( + model_name='tirage', + name='enable_do_tirage', + field=models.BooleanField( + default=False, + verbose_name='Le tirage peut \xeatre lanc\xe9'), + ), + migrations.AlterField( + model_name='tirage', + name='tokens', + field=models.TextField(verbose_name='Graine(s) du tirage', + blank=True), + ), + migrations.AddField( + model_name='spectacle', + name='category', + field=models.ForeignKey(blank=True, to='bda.CategorieSpectacle', + null=True), + ), + migrations.AddField( + model_name='spectacle', + name='vips', + field=models.TextField(verbose_name='Personnalit\xe9s', + blank=True), + ), + migrations.AddField( + model_name='quote', + name='spectacle', + field=models.ForeignKey(to='bda.Spectacle'), + ), + ] diff --git a/bda/models.py b/bda/models.py index ba72416a..8a7d0814 100644 --- a/bda/models.py +++ b/bda/models.py @@ -29,7 +29,7 @@ class Tirage(models.Model): tokens = models.TextField("Graine(s) du tirage", blank=True) active = models.BooleanField("Tirage actif", default=False) enable_do_tirage = models.BooleanField("Le tirage peut être lancé", - default=False) + default=False) def date_no_seconds(self): return self.fermeture.strftime('%d %b %Y %H:%M') @@ -47,16 +47,32 @@ class Salle(models.Model): return self.name +@python_2_unicode_compatible +class CategorieSpectacle(models.Model): + name = models.CharField('Nom', max_length=100, unique=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Catégorie" + + @python_2_unicode_compatible class Spectacle(models.Model): title = models.CharField("Titre", max_length=300) + category = models.ForeignKey(CategorieSpectacle, blank=True, null=True) date = models.DateTimeField("Date & heure") location = models.ForeignKey(Salle) + vips = models.TextField('Personnalités', blank=True) description = models.TextField("Description", blank=True) slots_description = models.TextField("Description des places", blank=True) + image = models.ImageField('Image', blank=True, null=True, + upload_to='imgs/shows/') + ext_link = models.CharField('Lien vers le site du spectacle', blank=True, + max_length=500) price = models.FloatField("Prix d'une place") slots = models.IntegerField("Places") - priority = models.IntegerField("Priorité", default=1000) tirage = models.ForeignKey(Tirage) listing = models.BooleanField("Les places sont sur listing") rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, @@ -64,7 +80,7 @@ class Spectacle(models.Model): class Meta: verbose_name = "Spectacle" - ordering = ("priority", "date", "title",) + ordering = ("date", "title",) def __repr__(self): return "[%s]" % self @@ -111,6 +127,13 @@ class Spectacle(models.Model): # On renvoie la liste des destinataires return members.values() + +class Quote(models.Model): + spectacle = models.ForeignKey(Spectacle) + text = models.TextField('Citation') + author = models.CharField('Auteur', max_length=200) + + PAYMENT_TYPES = ( ("cash", "Cash"), ("cb", "CB"), diff --git a/bda/static/fonts/josefinsans.ttf b/bda/static/fonts/josefinsans.ttf new file mode 100644 index 00000000..d234c43c Binary files /dev/null and b/bda/static/fonts/josefinsans.ttf differ diff --git a/bda/templates/descriptions.html b/bda/templates/descriptions.html new file mode 100644 index 00000000..3ab514f2 --- /dev/null +++ b/bda/templates/descriptions.html @@ -0,0 +1,71 @@ +{% load staticfiles %} + + + + + + + + + {% for show in shows %} + + + + + + + + + + + + + + + + + + + + {% if show.image %} + + + + {% endif %} + +

{{ show.title }}

{{ show.location }}

{{ show.category }}

{{ show.date }}

{{ show.slots }} place{{ show.slots|pluralize}} {% if show.slots_description != "" %}({{ show.slots_description }}){% endif %}- {{ show.price }}

{{ show.category }}

+

{{ show.description }}

+ {% for quote in show.quote_set.all %} +

«{{ quote.text }}»{% if show.quote.author %} - {{ quote.author }}{% endif %}

+ {% endfor %} +

{{ show.title }}

+ {% endfor %} + + diff --git a/bda/templates/spectacle_list.html b/bda/templates/spectacle_list.html index 0c3c7317..816461db 100644 --- a/bda/templates/spectacle_list.html +++ b/bda/templates/spectacle_list.html @@ -26,7 +26,7 @@ {% endfor %} - + @@ -39,14 +39,15 @@

Exports

{% endblock %} diff --git a/bda/urls.py b/bda/urls.py index 8ec8f277..d0f8ec83 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -5,6 +5,7 @@ from __future__ import print_function from __future__ import unicode_literals from django.conf.urls import url +from gestioncof.decorators import buro_required from bda.views import SpectacleListView from bda import views @@ -23,7 +24,7 @@ urlpatterns = [ name='bda-etat-places'), url(r'^tirage/(?P\d+)$', views.tirage), url(r'^spectacles/(?P\d+)$', - SpectacleListView.as_view(), + buro_required(SpectacleListView.as_view()), name="bda-liste-spectacles"), url(r'^spectacles/(?P\d+)/(?P\d+)$', views.spectacle, @@ -32,4 +33,6 @@ urlpatterns = [ views.unpaid, name="bda-unpaid"), url(r'^mails-rappel/(?P\d+)$', views.send_rappel), + url(r'^descriptions/(?P\d+)$', views.descriptions_spectacles, + name='bda-descriptions'), ] diff --git a/bda/views.py b/bda/views.py index ededccd7..4ea0df32 100644 --- a/bda/views.py +++ b/bda/views.py @@ -10,13 +10,13 @@ from django.db import models from django.db.models import Count from django.core import serializers from django.forms.models import inlineformset_factory +from django.http import HttpResponseBadRequest import hashlib from django.core.mail import send_mail from django.utils import timezone from django.views.generic.list import ListView -from datetime import timedelta import time from gestioncof.decorators import cof_required, buro_required @@ -110,8 +110,7 @@ def places(request, tirage_id): def inscription(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) if timezone.now() < tirage.ouverture: - error_desc = "Ouverture le %s" % ( - tirage.ouverture.strftime('%d %b %Y à %H:%M')) + error_desc = tirage.ouverture.strftime('Ouverture le %d %b %Y à %H:%M') return render(request, 'resume_inscription.html', {"error_title": "Le tirage n'est pas encore ouvert !", "error_description": error_desc}) @@ -366,3 +365,19 @@ def send_rappel(request, spectacle_id): else: ctxt['sent'] = False return render(request, "mails-rappel.html", ctxt) + + +def descriptions_spectacles(request, tirage_id): + tirage = get_object_or_404(Tirage, id=tirage_id) + shows_qs = tirage.spectacle_set + category_name = request.GET.get('category', '') + location_id = request.GET.get('location', '') + if category_name: + shows_qs = shows_qs.filter(category__name=category_name) + if location_id: + try: + shows_qs = shows_qs.filter(location__id=int(location_id)) + except ValueError: + return HttpResponseBadRequest( + "La variable GET 'location' doit contenir un entier") + return render(request, 'descriptions.html', {'shows': shows_qs.all()}) diff --git a/cof/urls.py b/cof/urls.py index 64af9eda..ca7ea247 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -16,7 +16,8 @@ from django.contrib.auth import views as django_views from django_cas_ng import views as django_cas_views from gestioncof import views as gestioncof_views, csv_views from gestioncof.urls import export_patterns, petitcours_patterns, \ - surveys_patterns, events_patterns, calendar_patterns + surveys_patterns, events_patterns, calendar_patterns, \ + clubs_patterns from gestioncof.autocomplete import autocomplete @@ -38,6 +39,8 @@ urlpatterns = [ url(r'^event/', include(events_patterns)), # Calendrier url(r'^calendar/', include(calendar_patterns)), + # Clubs + url(r'^clubs/', include(clubs_patterns)), # Authentification url(r'^cof/denied$', TemplateView.as_view(template_name='cof-denied.html'), name="cof-denied"), diff --git a/gestioncof/admin.py b/gestioncof/admin.py index 342317f3..7aedf089 100644 --- a/gestioncof/admin.py +++ b/gestioncof/admin.py @@ -4,6 +4,7 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals +from django import forms from django.contrib import admin from gestioncof.models import SurveyQuestionAnswer, SurveyQuestion, \ CofProfile, EventOption, EventOptionChoice, Event, Club, CustomMail, \ @@ -232,6 +233,25 @@ class PetitCoursDemandeAdmin(admin.ModelAdmin): class CustomMailAdmin(admin.ModelAdmin): search_fields = ('shortname', 'title') + +class ClubAdminForm(forms.ModelForm): + def clean(self): + cleaned_data = super(ClubAdminForm, self).clean() + respos = cleaned_data.get('respos') + members = cleaned_data.get('membres') + for respo in respos.all(): + if respo not in members: + raise forms.ValidationError( + "Erreur : le respo %s n'est pas membre du club." + % respo.get_full_name()) + return cleaned_data + + +class ClubAdmin(admin.ModelAdmin): + list_display = ['name'] + form = ClubAdminForm + + admin.site.register(Survey, SurveyAdmin) admin.site.register(SurveyQuestion, SurveyQuestionAdmin) admin.site.register(Event, EventAdmin) @@ -239,7 +259,7 @@ admin.site.register(EventOption, EventOptionAdmin) admin.site.unregister(User) admin.site.register(User, UserProfileAdmin) admin.site.register(CofProfile) -admin.site.register(Club) +admin.site.register(Club, ClubAdmin) admin.site.register(CustomMail) admin.site.register(PetitCoursSubject) admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin) diff --git a/gestioncof/fixtures/sites.json b/gestioncof/fixtures/sites.json new file mode 100644 index 00000000..a0d8c271 --- /dev/null +++ b/gestioncof/fixtures/sites.json @@ -0,0 +1,10 @@ +[ +{ + "fields": { + "domain": "localhost", + "name": "GestioCOF - dev - local" + }, + "model": "sites.site", + "pk": 1 +} +] diff --git a/gestioncof/forms.py b/gestioncof/forms.py index 15db25ce..8a64825f 100644 --- a/gestioncof/forms.py +++ b/gestioncof/forms.py @@ -8,10 +8,12 @@ from django import forms from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User from django.forms.widgets import RadioSelect, CheckboxSelectMultiple +from django.forms.formsets import BaseFormSet, formset_factory from django.db.models import Max +from django.core.validators import MinLengthValidator from gestioncof.models import CofProfile, EventCommentValue, \ - CalendarSubscription + CalendarSubscription, Club from gestioncof.widgets import TriStateCheckbox from gestioncof.shared import lock_table, unlock_table @@ -183,14 +185,6 @@ class UserProfileForm(forms.ModelForm): 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', - 'mailing_bda_revente', - ] def save(self, *args, **kw): super(UserProfileForm, self).save(*args, **kw) @@ -200,8 +194,8 @@ class UserProfileForm(forms.ModelForm): class Meta: model = CofProfile - fields = ("phone", "mailing_cof", "mailing_bda", - "mailing_bda_revente", ) + fields = ["first_name", "last_name", "phone", "mailing_cof", + "mailing_bda", "mailing_bda_revente"] class RegistrationUserForm(forms.ModelForm): @@ -209,11 +203,40 @@ class RegistrationUserForm(forms.ModelForm): super(RegistrationUserForm, self).__init__(*args, **kw) self.fields['username'].help_text = "" + def force_long_username(self): + self.fields['username'].validators = [MinLengthValidator(9)] + class Meta: model = User fields = ("username", "first_name", "last_name", "email") +class RegistrationPassUserForm(RegistrationUserForm): + """ + Formulaire pour changer le mot de passe d'un utilisateur. + """ + password1 = forms.CharField(label=_('Mot de passe'), + widget=forms.PasswordInput) + password2 = forms.CharField(label=_('Confirmation du mot de passe'), + widget=forms.PasswordInput) + + def clean_password2(self): + pass1 = self.cleaned_data['password1'] + pass2 = self.cleaned_data['password2'] + if pass1 and pass2: + if pass1 != pass2: + raise forms.ValidationError(_('Mots de passe non identiques.')) + return pass2 + + def save(self, commit=True, *args, **kwargs): + user = super(RegistrationPassUserForm, self).save(commit, *args, + **kwargs) + user.set_password(self.cleaned_data['password2']) + if commit: + user.save() + return user + + class RegistrationProfileForm(forms.ModelForm): def __init__(self, *args, **kw): super(RegistrationProfileForm, self).__init__(*args, **kw) @@ -263,17 +286,15 @@ STATUS_CHOICES = (('no', 'Non'), class AdminEventForm(forms.Form): - status = forms.ChoiceField(label="Inscription", + status = forms.ChoiceField(label="Inscription", initial="no", choices=STATUS_CHOICES, widget=RadioSelect) def __init__(self, *args, **kwargs): - event = kwargs.pop("event") - self.event = event + self.event = kwargs.pop("event") registration = kwargs.pop("current_registration", None) - current_choices = \ - registration.options.all() if registration is not None\ - else [] - paid = kwargs.pop("paid", None) + current_choices, paid = \ + (registration.options.all(), registration.paid) \ + if registration is not None else ([], None) if paid is True: kwargs["initial"] = {"status": "paid"} elif paid is False: @@ -288,7 +309,7 @@ class AdminEventForm(forms.Form): else: choices[choice.event_option.id].append(choice.id) all_choices = choices - for option in event.options.all(): + for option in self.event.options.all(): choices = [(choice.id, choice.value) for choice in option.choices.all()] if option.multi_choices: @@ -310,7 +331,7 @@ class AdminEventForm(forms.Form): initial=initial) field.option_id = option.id self.fields["option_%d" % option.id] = field - for commentfield in event.commentfields.all(): + for commentfield in self.event.commentfields.all(): initial = commentfield.default if registration is not None: try: @@ -338,6 +359,22 @@ class AdminEventForm(forms.Form): yield (self.fields[name].comment_id, value) +class BaseEventRegistrationFormset(BaseFormSet): + def __init__(self, *args, **kwargs): + self.events = kwargs.pop('events') + self.current_registrations = kwargs.pop('current_registrations', None) + self.extra = len(self.events) + super(BaseEventRegistrationFormset, self).__init__(*args, **kwargs) + + def _construct_form(self, index, **kwargs): + kwargs['event'] = self.events[index] + if self.current_registrations is not None: + kwargs['current_registration'] = self.current_registrations[index] + return super(BaseEventRegistrationFormset, self)._construct_form( + index, **kwargs) +EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset) + + class CalendarForm(forms.ModelForm): subscribe_to_events = forms.BooleanField( initial=True, @@ -348,9 +385,21 @@ class CalendarForm(forms.ModelForm): other_shows = forms.ModelMultipleChoiceField( label="Spectacles supplémentaires.", queryset=Spectacle.objects.filter(tirage__active=True), - widget=forms.CheckboxSelectMultiple) + widget=forms.CheckboxSelectMultiple, + required=False) class Meta: model = CalendarSubscription fields = ['subscribe_to_events', 'subscribe_to_my_shows', 'other_shows'] + + +class ClubsForm(forms.Form): + """ + Formulaire d'inscription d'un membre à plusieurs clubs du COF. + """ + clubs = forms.ModelMultipleChoiceField( + label="Inscriptions aux clubs du COF", + queryset=Club.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False) diff --git a/gestioncof/migrations/0007_alter_club.py b/gestioncof/migrations/0007_alter_club.py new file mode 100644 index 00000000..324c59a6 --- /dev/null +++ b/gestioncof/migrations/0007_alter_club.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('gestioncof', '0006_add_calendar'), + ] + + operations = [ + migrations.AlterField( + model_name='club', + name='name', + field=models.CharField(unique=True, max_length=200, + verbose_name='Nom') + ), + migrations.AlterField( + model_name='club', + name='description', + field=models.TextField(verbose_name='Description', blank=True) + ), + migrations.AlterField( + model_name='club', + name='membres', + field=models.ManyToManyField(related_name='clubs', + to=settings.AUTH_USER_MODEL, + blank=True), + ), + migrations.AlterField( + model_name='club', + name='respos', + field=models.ManyToManyField(related_name='clubs_geres', + to=settings.AUTH_USER_MODEL, + blank=True), + ), + migrations.AlterField( + model_name='event', + name='start_date', + field=models.DateTimeField(null=True, + verbose_name='Date de d\xe9but', + blank=True), + ), + ] diff --git a/gestioncof/models.py b/gestioncof/models.py index 95837a3a..382a5750 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -86,10 +86,11 @@ post_save.connect(create_user_profile, sender=User) @python_2_unicode_compatible class Club(models.Model): - name = models.CharField("Nom", max_length=200) - description = models.TextField("Description") - respos = models.ManyToManyField(User, related_name="clubs_geres") - membres = models.ManyToManyField(User, related_name="clubs") + name = models.CharField("Nom", max_length=200, unique=True) + description = models.TextField("Description", blank=True) + respos = models.ManyToManyField(User, related_name="clubs_geres", + blank=True) + membres = models.ManyToManyField(User, related_name="clubs", blank=True) def __str__(self): return self.name diff --git a/gestioncof/static/css/cof.css b/gestioncof/static/css/cof.css index 5db3c9bb..b8f6e7f8 100644 --- a/gestioncof/static/css/cof.css +++ b/gestioncof/static/css/cof.css @@ -825,6 +825,16 @@ input#search_autocomplete:focus { color: #343a4a; } +input[type=number][readonly] { + -moz-appearance:textfield; +} + +input[type=number][readonly]::-webkit-inner-spin-button, +input[type=number][readonly]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + .autocomplete { margin-bottom:5px; } diff --git a/gestioncof/templates/calendar_subscription.html b/gestioncof/templates/calendar_subscription.html index 52f6d492..5f0bc988 100644 --- a/gestioncof/templates/calendar_subscription.html +++ b/gestioncof/templates/calendar_subscription.html @@ -37,7 +37,7 @@ souscrire aux événements du COF et/ou aux spectacles BdA.
{% csrf_token %} {{ form.as_p }} - +
{% endblock %} diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index bcec273c..0907adc6 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -37,26 +37,29 @@ {% for tirage in open_tirages %} {% endfor %} {% endif %} - {% endif %}

Divers

+ {% endif %} {% if user.profile.is_buro %}
@@ -67,6 +70,7 @@
  • Administration générale
  • Demandes de petits cours
  • Inscription d'un nouveau membre
  • +
  • Gestion des clubs
    • Évènements & Sondages

      @@ -80,12 +84,15 @@

    Gestion tirages BDA

    - {% if open_tirages %} - {% for tirage in open_tirages %} + {% if active_tirages %} + {% for tirage in active_tirages %} {% endfor %} {% else %} diff --git a/gestioncof/templates/liste_clubs.html b/gestioncof/templates/liste_clubs.html new file mode 100644 index 00000000..c248a7a6 --- /dev/null +++ b/gestioncof/templates/liste_clubs.html @@ -0,0 +1,25 @@ +{% extends "base_title.html" %} + +{% block page_size %}col-sm-8{% endblock %} + +{% block realcontent %} +

    Clubs enregistrés sur GestioCOF

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

    {{ club }}

    + + +{% if club.respos.exists %} +

    Respo{{ club.respos.all|pluralize }}

    + +{% for member in club.respos.all %} + + + + + +{% endfor %} +
    {{ member }}{{ member.email }}
    +{% else %} +

    Pas de respo

    +{% endif %} + + +{% if club.membres.exists %} +

    Liste des membres

    + +{% for member in members_no_respo %} + + + + + +{% endfor %} +
    {{ member }}{{ member.email }}
    +{% else %} +Ce club ne comporte actuellement aucun membre. +{% endif %} + +{% endblock %} diff --git a/gestioncof/templates/registration_form.html b/gestioncof/templates/registration_form.html index b9699647..8668152b 100644 --- a/gestioncof/templates/registration_form.html +++ b/gestioncof/templates/registration_form.html @@ -12,16 +12,19 @@ {{ user_form | bootstrap }} {{ profile_form | bootstrap }} - {% if event_forms %}
    - {% for event_form in event_forms %} +
    + + {{ clubs_form | bootstrap }} +
    + {{ event_formset.management_form }} + {% for event_form in event_formset %}
    -

    Inscription {{ event_form.event.title }} :

    +

    Inscription {{ event_form.event.title }} :

    - {{ event_form | bootstrap }} + {{ event_form | bootstrap }}
    {% endfor %} - {% endif %} {% if login_clipper or member %} {% endif %} diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 89cd5aa8..ad108005 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -52,3 +52,10 @@ calendar_patterns = [ url(r'^(?P[a-z0-9-]+)/calendar.ics$', 'gestioncof.views.calendar_ics') ] + +clubs_patterns = [ + url(r'^membres/(?P\w+)', views.membres_club, name='membres-club'), + url(r'^liste', views.liste_clubs, name='liste-clubs'), + url(r'^change_respo/(?P\w+)/(?P\d+)', + views.change_respo, name='change-respo'), +] diff --git a/gestioncof/views.py b/gestioncof/views.py index 378cb44d..3148308a 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -10,10 +10,11 @@ from datetime import timedelta from icalendar import Calendar, Event as Vevent from django.shortcuts import redirect, get_object_or_404, render -from django.http import Http404, HttpResponse +from django.http import Http404, HttpResponse, HttpResponseForbidden from django.contrib.auth.decorators import login_required from django.contrib.auth.views import login as django_login_view from django.contrib.auth.models import User +from django.utils import timezone import django.utils.six as six from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \ @@ -23,11 +24,12 @@ from gestioncof.models import Event, EventRegistration, EventOption, \ from gestioncof.models import EventCommentField, EventCommentValue, \ CalendarSubscription from gestioncof.shared import send_custom_mail -from gestioncof.models import CofProfile, Clipper +from gestioncof.models import CofProfile, Clipper, Club from gestioncof.decorators import buro_required, cof_required from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \ SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \ - RegistrationProfileForm, AdminEventForm, EventForm, CalendarForm + RegistrationProfileForm, EventForm, CalendarForm, EventFormset, \ + RegistrationPassUserForm, ClubsForm from bda.models import Tirage, Spectacle @@ -40,7 +42,11 @@ def home(request): Survey.objects.filter(survey_open=True, old=False).all(), "open_events": Event.objects.filter(registration_open=True, old=False).all(), - "open_tirages": Tirage.objects.filter(active=True).all()} + "active_tirages": Tirage.objects.filter(active=True).all(), + "open_tirages": + Tirage.objects.filter(active=True, + ouverture__lte=timezone.now()).all(), + "now": timezone.now()} return render(request, "home.html", data) @@ -91,7 +97,7 @@ def logout(request): @login_required def survey(request, survey_id): survey = get_object_or_404(Survey, id=survey_id) - if not survey.survey_open: + if not survey.survey_open or survey.old: raise Http404 success = False deleted = False @@ -188,7 +194,7 @@ def update_event_form_comments(event, form, registration): @login_required def event(request, event_id): event = get_object_or_404(Event, id=event_id) - if not event.registration_open: + if (not event.registration_open) or event.old: raise Http404 success = False if request.method == "POST": @@ -295,7 +301,7 @@ def survey_status(request, survey_id): "form": form}) -@login_required +@cof_required def profile(request): success = False if request.method == "POST": @@ -313,45 +319,6 @@ def registration_set_ro_fields(user_form, profile_form): profile_form.fields['login_clipper'].widget.attrs['readonly'] = True -@buro_required -def registration_form(request, login_clipper=None, username=None): - member = None - if login_clipper: - clipper = get_object_or_404(Clipper, username=login_clipper) - try: # check if the given user is already registered - member = User.objects.filter(username=login_clipper).get() - username = member.username - login_clipper = None - except User.DoesNotExist: - # new user, but prefill - user_form = RegistrationUserForm() - profile_form = RegistrationProfileForm() - user_form.fields['username'].initial = login_clipper - user_form.fields['email'].initial = \ - login_clipper + "@clipper.ens.fr" - profile_form.fields['login_clipper'].initial = login_clipper - if clipper.fullname: - bits = clipper.fullname.split(" ") - user_form.fields['first_name'].initial = bits[0] - if len(bits) > 1: - user_form.fields['last_name'].initial = " ".join(bits[1:]) - registration_set_ro_fields(user_form, profile_form) - if username: - member = get_object_or_404(User, username=username) - (profile, _) = CofProfile.objects.get_or_create(user=member) - # already existing, prefill - user_form = RegistrationUserForm(instance=member) - profile_form = RegistrationProfileForm(instance=profile) - registration_set_ro_fields(user_form, profile_form) - elif not login_clipper: - # new user - user_form = RegistrationUserForm() - profile_form = RegistrationProfileForm() - return render(request, "registration_form.html", - {"user_form": user_form, "profile_form": profile_form, - "member": member, "login_clipper": login_clipper}) - - @buro_required def registration_form2(request, login_clipper=None, username=None): events = Event.objects.filter(old=False).all() @@ -359,24 +326,27 @@ def registration_form2(request, login_clipper=None, username=None): if login_clipper: clipper = get_object_or_404(Clipper, username=login_clipper) try: # check if the given user is already registered - member = User.objects.filter(username=login_clipper).get() + member = User.objects.get(username=login_clipper) username = member.username login_clipper = None except User.DoesNotExist: # new user, but prefill - user_form = RegistrationUserForm() - profile_form = RegistrationProfileForm() - event_forms = [AdminEventForm(event=event) for event in events] - user_form.fields['username'].initial = login_clipper - user_form.fields['email'].initial = \ - login_clipper + "@clipper.ens.fr" - profile_form.fields['login_clipper'].initial = login_clipper + # user + user_form = RegistrationUserForm(initial={ + 'username': login_clipper, + 'email': "%s@clipper.ens.fr" % login_clipper}) if clipper.fullname: bits = clipper.fullname.split(" ") user_form.fields['first_name'].initial = bits[0] if len(bits) > 1: user_form.fields['last_name'].initial = " ".join(bits[1:]) + # profile + profile_form = RegistrationProfileForm(initial={ + 'login_clipper': login_clipper}) registration_set_ro_fields(user_form, profile_form) + # events & clubs + event_formset = EventFormset(events=events, prefix='events') + clubs_form = ClubsForm(initial={'clubs': member.clubs.all()}) if username: member = get_object_or_404(User, username=username) (profile, _) = CofProfile.objects.get_or_create(user=member) @@ -384,121 +354,185 @@ def registration_form2(request, login_clipper=None, username=None): user_form = RegistrationUserForm(instance=member) profile_form = RegistrationProfileForm(instance=profile) registration_set_ro_fields(user_form, profile_form) - event_forms = [] + # events + current_registrations = [] for event in events: try: - current_registration = EventRegistration.objects.get( - user=member, event=event) - form = AdminEventForm( - event=event, - current_registration=current_registration, - paid=current_registration.paid) + current_registrations.append( + EventRegistration.objects.get(user=member, event=event)) except EventRegistration.DoesNotExist: - form = AdminEventForm(event=event) - event_forms.append(form) + current_registrations.append(None) + event_formset = EventFormset( + events=events, prefix='events', + current_registrations=current_registrations) + # Clubs + clubs_form = ClubsForm(initial={'clubs': member.clubs.all()}) elif not login_clipper: # new user - user_form = RegistrationUserForm() + user_form = RegistrationPassUserForm() + user_form.force_long_username() profile_form = RegistrationProfileForm() - event_forms = [AdminEventForm(event=event) for event in events] + event_formset = EventFormset(events=events, prefix='events') + clubs_form = ClubsForm() return render(request, "registration_form.html", - {"user_form": user_form, "profile_form": profile_form, - "member": member, "login_clipper": login_clipper, - "event_forms": event_forms}) + {"member": member, "login_clipper": login_clipper, + "user_form": user_form, + "profile_form": profile_form, + "event_formset": event_formset, + "clubs_form": clubs_form}) @buro_required def registration(request): if request.POST: request_dict = request.POST.copy() + # num ne peut pas être défini manuellement if "num" in request_dict: del request_dict["num"] - success = False - user_form = RegistrationUserForm(request_dict) - profile_form = RegistrationProfileForm(request_dict) - events = Event.objects.filter(old=False).all() - event_forms = \ - [AdminEventForm(request_dict, event=event) for event in events] - user_form.is_valid() - profile_form.is_valid() - for event_form in event_forms: - event_form.is_valid() member = None login_clipper = None + success = False + + # ----- + # Remplissage des formulaires + # ----- + + if 'password1' in request_dict or 'password2' in request_dict: + user_form = RegistrationPassUserForm(request_dict) + else: + user_form = RegistrationUserForm(request_dict) + profile_form = RegistrationProfileForm(request_dict) + clubs_form = ClubsForm(request_dict) + events = Event.objects.filter(old=False).all() + event_formset = EventFormset(events=events, data=request_dict, + prefix='events') if "user_exists" in request_dict and request_dict["user_exists"]: username = request_dict["username"] try: - member = User.objects.filter(username=username).get() - (profile, _) = CofProfile.objects.get_or_create(user=member) + member = User.objects.get(username=username) user_form = RegistrationUserForm(request_dict, instance=member) - profile_form = RegistrationProfileForm(request_dict, - instance=profile) except User.DoesNotExist: try: - clipper = Clipper.objects.filter(username=username).get() + clipper = Clipper.objects.get(username=username) login_clipper = clipper.username except Clipper.DoesNotExist: - pass - for form in event_forms: - if not form.is_valid(): - break - if form.cleaned_data['status'] == 'no': - continue - all_choices = get_event_form_choices(form.event, form) - if user_form.is_valid() and profile_form.is_valid() \ - and not any([not form.is_valid() for form in event_forms]): + user_form.force_long_username() + else: + user_form.force_long_username() + + # ----- + # Validation des formulaires + # ----- + + if user_form.is_valid(): member = user_form.save() - (profile, _) = CofProfile.objects.get_or_create(user=member) + profile, _ = CofProfile.objects.get_or_create(user=member) was_cof = profile.is_cof request_dict["num"] = profile.num + # Maintenant on remplit le formulaire de profil profile_form = RegistrationProfileForm(request_dict, instance=profile) - profile_form.is_valid() - profile_form.save() - (profile, _) = CofProfile.objects.get_or_create(user=member) - if profile.is_cof and not was_cof: - send_custom_mail(member, "bienvenue") - for form in event_forms: - if form.cleaned_data['status'] == 'no': - try: - current_registration = EventRegistration.objects.get( + if (profile_form.is_valid() and event_formset.is_valid() + and clubs_form.is_valid()): + # Enregistrement du profil + profile = profile_form.save() + if profile.is_cof and not was_cof: + send_custom_mail(member, "bienvenue") + # Enregistrement des inscriptions aux événements + for form in event_formset: + if 'status' not in form.cleaned_data: + form.cleaned_data['status'] = 'no' + if form.cleaned_data['status'] == 'no': + try: + current_registration = EventRegistration.objects \ + .get(user=member, event=form.event) + current_registration.delete() + except EventRegistration.DoesNotExist: + pass + continue + all_choices = get_event_form_choices(form.event, form) + (current_registration, created_reg) = \ + EventRegistration.objects.get_or_create( user=member, event=form.event) - current_registration.delete() - except EventRegistration.DoesNotExist: - pass - continue - all_choices = get_event_form_choices(form.event, form) - (current_registration, created_reg) = \ - EventRegistration.objects.get_or_create(user=member, - event=form.event) - update_event_form_comments(form.event, form, - current_registration) - current_registration.options = all_choices - current_registration.paid = \ - (form.cleaned_data['status'] == 'paid') - current_registration.save() - if form.event.title == "Mega 15" and created_reg: - field = EventCommentField.objects.get(event=form.event, - name="Commentaires") - try: - comments = EventCommentValue.objects.get( - commentfield=field, - registration=current_registration).content - except EventCommentValue.DoesNotExist: - comments = field.default - send_custom_mail(member, "mega", {"remarques": comments}) - success = True + update_event_form_comments(form.event, form, + current_registration) + current_registration.options = all_choices + current_registration.paid = \ + (form.cleaned_data['status'] == 'paid') + current_registration.save() + if form.event.title == "Mega 15" and created_reg: + field = EventCommentField.objects.get( + event=form.event, name="Commentaires") + try: + comments = EventCommentValue.objects.get( + commentfield=field, + registration=current_registration).content + except EventCommentValue.DoesNotExist: + comments = field.default + send_custom_mail(member, "mega", + {"remarques": comments}) + # Enregistrement des inscriptions aux clubs + member.clubs.clear() + for club in clubs_form.cleaned_data['clubs']: + club.membres.add(member) + club.save() + success = True return render(request, "registration_post.html", {"success": success, "user_form": user_form, "profile_form": profile_form, "member": member, "login_clipper": login_clipper, - "event_forms": event_forms}) + "event_formset": event_formset, + "clubs_form": clubs_form}) else: return render(request, "registration.html") +# ----- +# Clubs +# ----- + + +@login_required +def membres_club(request, name): + # Vérification des permissions : l'utilisateur doit être membre du burô + # ou respo du club. + user = request.user + club = get_object_or_404(Club, name=name) + if not request.user.profile.is_buro \ + and club not in user.clubs_geres.all(): + return HttpResponseForbidden('

    Permission denied

    ') + members_no_respo = club.membres.exclude(clubs_geres=club).all() + return render(request, 'membres_clubs.html', + {'club': club, + 'members_no_respo': members_no_respo}) + + +@buro_required +def change_respo(request, club_name, user_id): + club = get_object_or_404(Club, name=club_name) + user = get_object_or_404(User, id=user_id) + if user in club.respos.all(): + club.respos.remove(user) + elif user in club.membres.all(): + club.respos.add(user) + else: + raise Http404 + return redirect('membres-club', name=club_name) + + +@cof_required +def liste_clubs(request): + clubs = Club.objects + if request.user.profile.is_buro: + data = {'owned_clubs': clubs.all()} + else: + data = {'owned_clubs': request.user.clubs_geres, + 'other_clubs': clubs.exclude(respos=request.user)} + return render(request, 'liste_clubs.html', data) + + @buro_required def export_members(request): response = HttpResponse(content_type='text/csv') diff --git a/provisioning/prepare_django.sh b/provisioning/prepare_django.sh index 5c661cc8..ef26235e 100644 --- a/provisioning/prepare_django.sh +++ b/provisioning/prepare_django.sh @@ -1,4 +1,4 @@ # Doit être lancé par bootstrap.sh python manage.py migrate -python manage.py loaddata users root bda gestion +python manage.py loaddata users root bda gestion sites