diff --git a/bda/templates/liste_spectacles.ics b/bda/templates/liste_spectacles.ics deleted file mode 100644 index 1ce599f3..00000000 --- a/bda/templates/liste_spectacles.ics +++ /dev/null @@ -1,10 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//GESTIOCOF/bda//EN -{% for spectacle in spectacles %}BEGIN:VEVENT -DTSTART;TZID=Europe/Paris:{{ spectacle.date|date:'Ymd\\THis' }} -DTEND;TZID=Europe/Paris:{{ spectacle.dtend|date:'Ymd\\THis' }} -SUMMARY:{{ spectacle.title|safe }} -LOCATION:{{ spectacle.location.name|safe }} -END:VEVENT -{% endfor %}END:VCALENDAR \ No newline at end of file diff --git a/bda/templates/resume_places.html b/bda/templates/resume_places.html index 4ed33e72..9ca5c78f 100644 --- a/bda/templates/resume_places.html +++ b/bda/templates/resume_places.html @@ -18,7 +18,9 @@

Total à payer : {{ total|floatformat }}€


-

Exporter au format calendrier (.ics, compatible avec tous les logiciels d'agenda)

+

Ne manque pas un spectacle avec le + calendrier + automatique !

{% else %}

Vous n'avez aucune place :(

{% endif %} diff --git a/bda/templates/resume_places.ics b/bda/templates/resume_places.ics deleted file mode 100644 index 72aa4347..00000000 --- a/bda/templates/resume_places.ics +++ /dev/null @@ -1,10 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//GESTIOCOF/bda//EN -{% for place in places %}BEGIN:VEVENT -DTSTART;TZID=Europe/Paris:{{ place.spectacle.date|date:'Ymd\\THis' }} -DTEND;TZID=Europe/Paris:{{ place.spectacle.dtend|date:'Ymd\\THis' }} -SUMMARY:{{ place.spectacle.title|safe }}{% if place.double %} (deux places){% endif %} -LOCATION:{{ place.spectacle.location.name|safe }} -END:VEVENT -{% endfor %}END:VCALENDAR \ No newline at end of file diff --git a/bda/templates/spectacle_list.html b/bda/templates/spectacle_list.html index dedc655d..0c3c7317 100644 --- a/bda/templates/spectacle_list.html +++ b/bda/templates/spectacle_list.html @@ -48,6 +48,5 @@

Exports

{% endblock %} diff --git a/bda/urls.py b/bda/urls.py index 4e5811a1..e3c5a9fb 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -15,9 +15,6 @@ urlpatterns = patterns( url(r'^places/(?P\d+)$', 'bda.views.places', name="bda-places-attribuees"), - url(r'^places/(?P\d+)/places_bda.ics$', - 'bda.views.places_ics', - name="bda-places-attribuees-ics"), url(r'^revente/(?P\d+)$', 'bda.views.revente', name='bda-revente'), @@ -31,9 +28,6 @@ urlpatterns = patterns( url(r'^spectacles/(?P\d+)/(?P\d+)$', "bda.views.spectacle", name="bda-spectacle"), - url(r'^spectacles-ics/(?P\d+)$', - 'bda.views.liste_spectacles_ics', - name="bda-liste-spectacles-ics"), url(r'^spectacles/unpaid/(?P\d+)$', "bda.views.unpaid", name="bda-unpaid"), diff --git a/bda/views.py b/bda/views.py index bf666f08..ededccd7 100644 --- a/bda/views.py +++ b/bda/views.py @@ -106,31 +106,6 @@ def places(request, tirage_id): "warning": warning}) -@cof_required -def places_ics(request, tirage_id): - tirage = get_object_or_404(Tirage, id=tirage_id) - participant, created = Participant.objects.get_or_create( - user=request.user, tirage=tirage) - places = participant.attribution_set.order_by( - "spectacle__date", "spectacle").all() - filtered_places = [] - places_dict = {} - spectacles = [] - for place in places: - if place.spectacle in spectacles: - places_dict[place.spectacle].double = True - else: - place.double = False - place.spectacle.dtend = place.spectacle.date \ - + timedelta(seconds=7200) - places_dict[place.spectacle] = place - spectacles.append(place.spectacle) - filtered_places.append(place) - return render(request, "resume_places.ics", - {"participant": participant, - "places": filtered_places}, content_type="text/calendar") - - @cof_required def inscription(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) @@ -365,17 +340,6 @@ def unpaid(request, tirage_id): return render(request, "bda-unpaid.html", {"unpaid": unpaid}) -@buro_required -def liste_spectacles_ics(request, tirage_id): - tirage = get_object_or_404(Tirage, id=tirage_id) - spectacles = tirage.spectacle_set.order_by("date").all() - for spectacle in spectacles: - spectacle.dtend = spectacle.date + timedelta(seconds=7200) - return render(request, "liste_spectacles.ics", - {"spectacles": spectacles, "tirage": tirage}, - content_type="text/calendar") - - @buro_required def send_rappel(request, spectacle_id): show = get_object_or_404(Spectacle, id=spectacle_id) diff --git a/cof/urls.py b/cof/urls.py index ee9d8457..79177170 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -13,7 +13,7 @@ from django.views.generic.base import TemplateView import autocomplete_light from gestioncof.urls import export_patterns, petitcours_patterns, \ - surveys_patterns, events_patterns + surveys_patterns, events_patterns, calendar_patterns autocomplete_light.autodiscover() admin.autodiscover() @@ -32,6 +32,8 @@ urlpatterns = patterns( url(r'^survey/', include(surveys_patterns)), # Evenements url(r'^event/', include(events_patterns)), + # Calendrier + url(r'^calendar/', include(calendar_patterns)), # Authentification url(r'^cof/denied$', TemplateView.as_view(template_name='cof-denied.html'), name="cof-denied"), diff --git a/gestioncof/forms.py b/gestioncof/forms.py index 170ebcd0..15db25ce 100644 --- a/gestioncof/forms.py +++ b/gestioncof/forms.py @@ -10,10 +10,13 @@ from django.contrib.auth.models import User from django.forms.widgets import RadioSelect, CheckboxSelectMultiple from django.db.models import Max -from gestioncof.models import CofProfile, EventCommentValue +from gestioncof.models import CofProfile, EventCommentValue, \ + CalendarSubscription from gestioncof.widgets import TriStateCheckbox from gestioncof.shared import lock_table, unlock_table +from bda.models import Spectacle + class EventForm(forms.Form): def __init__(self, *args, **kwargs): @@ -333,3 +336,21 @@ class AdminEventForm(forms.Form): for name, value in self.cleaned_data.items(): if name.startswith('comment_'): yield (self.fields[name].comment_id, value) + + +class CalendarForm(forms.ModelForm): + subscribe_to_events = forms.BooleanField( + initial=True, + label="Événements du COF.") + subscribe_to_my_shows = forms.BooleanField( + initial=True, + label="Les spectacles pour lesquels j'ai obtenu une place.") + other_shows = forms.ModelMultipleChoiceField( + label="Spectacles supplémentaires.", + queryset=Spectacle.objects.filter(tirage__active=True), + widget=forms.CheckboxSelectMultiple) + + class Meta: + model = CalendarSubscription + fields = ['subscribe_to_events', 'subscribe_to_my_shows', + 'other_shows'] diff --git a/gestioncof/migrations/0005_add_calendar.py b/gestioncof/migrations/0005_add_calendar.py new file mode 100644 index 00000000..5d894f13 --- /dev/null +++ b/gestioncof/migrations/0005_add_calendar.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0004_mails-rappel'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('gestioncof', '0004_registration_mail'), + ] + + operations = [ + migrations.CreateModel( + name='CalendarSubscription', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('token', models.UUIDField()), + ('subscribe_to_events', models.BooleanField(default=True)), + ('subscribe_to_my_shows', models.BooleanField(default=True)), + ('other_shows', models.ManyToManyField(to='bda.Spectacle')), + ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AlterModelOptions( + name='custommail', + options={'verbose_name': 'Mail personnalisable', + 'verbose_name_plural': 'Mails personnalisables'}, + ), + migrations.AlterModelOptions( + name='eventoptionchoice', + options={'verbose_name': 'Choix', 'verbose_name_plural': 'Choix'}, + ), + migrations.AlterField( + model_name='event', + name='end_date', + field=models.DateTimeField(null=True, verbose_name=b'Date de fin', + blank=True), + ), + migrations.AlterField( + model_name='event', + name='start_date', + field=models.DateTimeField( + null=True, verbose_name=b'Date de d\xc3\xa9but', blank=True), + ), + ] diff --git a/gestioncof/models.py b/gestioncof/models.py index 7df0fb00..95837a3a 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -13,6 +13,8 @@ from django.db.models.signals import post_save from gestioncof.petits_cours_models import choices_length +from bda.models import Spectacle + OCCUPATION_CHOICES = ( ('exterieur', _("Extérieur")), ('1A', _("1A")), @@ -113,8 +115,8 @@ class CustomMail(models.Model): 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, null=True) - end_date = models.DateField("Date de fin", blank=True, null=True) + start_date = models.DateTimeField("Date de début", blank=True, null=True) + end_date = models.DateTimeField("Date de fin", blank=True, null=True) description = models.TextField("Description", blank=True) image = models.ImageField("Image", blank=True, null=True, upload_to="imgs/events/") @@ -262,3 +264,15 @@ class Clipper(models.Model): def __str__(self): return "Clipper %s" % self.username + + +@python_2_unicode_compatible +class CalendarSubscription(models.Model): + token = models.UUIDField() + user = models.OneToOneField(User) + other_shows = models.ManyToManyField(Spectacle) + subscribe_to_events = models.BooleanField(default=True) + subscribe_to_my_shows = models.BooleanField(default=True) + + def __str__(self): + return "Calendrier de %s" % self.user.get_full_name() diff --git a/gestioncof/templates/calendar_subscription.html b/gestioncof/templates/calendar_subscription.html new file mode 100644 index 00000000..52f6d492 --- /dev/null +++ b/gestioncof/templates/calendar_subscription.html @@ -0,0 +1,43 @@ +{% extends "base_title.html" %} + +{% block realcontent %} + +

Calendrier dynamique

+ +{% if success %} +

Calendrier mis à jour avec succès

+{% endif %} + +{% if error %} +

{{ error }}

+{% endif %} + +

Ce formulaire vous permet de définir un calendrier dynamique compatible avec +n'importe quel logiciel ou application d'agenda. Vous pouvez choisir de +souscrire aux événements du COF et/ou aux spectacles BdA. +

+ +{% if token %} +

Votre calendrier (compatible avec toutes les applications d'agenda) se trouve à +cette adresse.

+ +
    +
  • Pour l'ajouter à Thunderbird (lightning), il faut copier ce lien et aller + dans Fichier > Nouveau > Agenda puis choisir Sur le + réseau et le format .ics.
  • +
  • Avec Apple, il suffit de cliquer sur lien et d'ouvrir le fichier avec + l'application calendrier.
  • +
  • Google Agenda permet d'importer le fichier au format .ics depuis le menu + Préférences.
  • +
+{% endif %} + +
+ +
+{% csrf_token %} +{{ form.as_p }} + +
+ +{% endblock %} diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index f1983992..bcec273c 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -50,7 +50,7 @@

Divers

    - +
  • Calendrier dynamique
  • {% if user.profile.is_cof %}
  • Inscription pour donner des petits cours
  • {% endif %}
  • Éditer mon profil
  • diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 5cea2f8e..bf4089db 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -45,3 +45,9 @@ events_patterns = [ url(r'^(?P\d+)$', 'gestioncof.views.event'), url(r'^(?P\d+)/status$', 'gestioncof.views.event_status'), ] + +calendar_patterns = [ + url(r'^subscription$', 'gestioncof.views.calendar'), + url(r'^(?P[a-z0-9-]+)/calendar.ics$', + 'gestioncof.views.calendar_ics') +] diff --git a/gestioncof/views.py b/gestioncof/views.py index 2d5f0487..378cb44d 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -5,6 +5,9 @@ from __future__ import print_function from __future__ import unicode_literals import unicodecsv +import uuid +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 @@ -17,15 +20,16 @@ from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \ SurveyQuestionAnswer from gestioncof.models import Event, EventRegistration, EventOption, \ EventOptionChoice -from gestioncof.models import EventCommentField, EventCommentValue +from gestioncof.models import EventCommentField, EventCommentValue, \ + CalendarSubscription from gestioncof.shared import send_custom_mail from gestioncof.models import CofProfile, Clipper -from gestioncof.decorators import buro_required +from gestioncof.decorators import buro_required, cof_required from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \ SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \ - RegistrationProfileForm, AdminEventForm, EventForm + RegistrationProfileForm, AdminEventForm, EventForm, CalendarForm -from bda.models import Tirage +from bda.models import Tirage, Spectacle @login_required @@ -633,3 +637,61 @@ def liste_diffcof(request): personnes = CofProfile.objects.filter(mailing_cof=True, is_cof=True).all() return render(request, "liste_mails.html", {"titre": titre, "personnes": personnes}) + + +@cof_required +def calendar(request): + try: + instance = CalendarSubscription.objects.get(user=request.user) + except CalendarSubscription.DoesNotExist: + instance = None + if request.method == 'POST': + form = CalendarForm(request.POST, instance=instance) + if form.is_valid(): + subscription = form.save(commit=False) + if instance is None: + subscription.user = request.user + subscription.token = uuid.uuid4() + subscription.save() + form.save_m2m() + return render(request, "calendar_subscription.html", + {'form': form, + 'success': True, + 'token': str(subscription.token)}) + else: + return render(request, "calendar_subscription.html", + {'form': form, 'error': "Formulaire incorrect"}) + else: + return render(request, "calendar_subscription.html", + {'form': CalendarForm(instance=instance), + 'token': instance.token if instance else None}) + + +def calendar_ics(request, token): + subscription = get_object_or_404(CalendarSubscription, token=token) + shows = subscription.other_shows.all() + if subscription.subscribe_to_my_shows: + shows |= Spectacle.objects.filter( + attribues__participant__user=subscription.user, + tirage__active=True) + shows = shows.distinct() + vcal = Calendar() + for show in shows: + vevent = Vevent() + vevent.add('dtstart', show.date) + vevent.add('dtend', show.date + timedelta(seconds=7200)) + vevent.add('summary', show.title) + vevent.add('location', show.location.name) + vcal.add_component(vevent) + if subscription.subscribe_to_events: + for event in Event.objects.filter(old=False).all(): + vevent = Vevent() + vevent.add('dtstart', event.start_date) + vevent.add('dtend', event.end_date) + vevent.add('summary', event.title) + vevent.add('location', event.location) + vevent.add('description', event.description) + vcal.add_component(vevent) + response = HttpResponse(content=vcal.to_ical()) + response['Content-Type'] = "text/calendar" + return response diff --git a/requirements.txt b/requirements.txt index 43d30dc9..b6f8cc6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ Pillow==2.9.0 simplejson==3.8.2 six==1.10.0 unicodecsv==0.14.1 +icalendar==3.10 django-bootstrap-form==3.2.1