Repository: dotkom/onlineweb4

Issue:
Calender sync
Google cal, ical and the likes. Ref the feature from ow3. 



Discussion:
Would it be possible to have a per user calendar that is dynamically updated when participating in events?

I noticed that ntnu.1024.no does this for lectures and Google Calendar automatically adds the changes when the ics file changes. 

Then we should also offer an "all events" calendar. 

I recommend just making the "everything" calendar first, since users are missing this feature, and then offer the specialized calendar later.


Resolving patch:
diff --git a/apps/events/urls.py b/apps/events/urls.py
--- a/apps/events/urls.py
+++ b/apps/events/urls.py
@@ -9,4 +9,8 @@
     url(r'^(?P<event_id>\d+)/unattend/$', 'unattendEvent', name='unattend_event'),
     url(r'^(?P<event_id>\d+)/(?P<event_slug>[a-zA-Z0-9_-]+)/$', 'details', name='events_details'), 
     url(r'^search/.*$', 'search_events', name="search_events"),
+    # iCalendar
+    url(r'^events.ics$', 'calendar_export', name='events_ics'),
+    url(r'^(?P<event_id>\d+).ics$', 'calendar_export', name='event_ics'),
+    url(r'^user/(?P<user>[\w:-]+).ics$', 'calendar_export', name='events_personal_ics'),
 )
diff --git a/apps/events/views.py b/apps/events/views.py
--- a/apps/events/views.py
+++ b/apps/events/views.py
@@ -7,20 +7,28 @@
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.decorators import login_required, user_passes_test
+from django.core.signing import Signer, BadSignature
 from django.core.urlresolvers import reverse
-from django.http import HttpResponseRedirect
+from django.http import HttpResponse, HttpResponseRedirect
 from django.shortcuts import render, get_object_or_404, redirect
 from django.utils.translation import ugettext as _
 
+import icalendar
 import watson
 
+from apps.authentication.models import OnlineUser as User
 from apps.events.forms import CaptchaForm
 from apps.events.models import Event, AttendanceEvent, Attendee
 from apps.events.pdf_generator import EventPDF
 
 
 def index(request):
-    return render(request, 'events/index.html', {})
+    context = {}
+    if request.user and request.user.is_authenticated():
+        signer = Signer()
+        context['signer_value'] = signer.sign(request.user.username)
+        context['personal_ics_path'] = request.build_absolute_uri(reverse('events_personal_ics', args=(context['signer_value'],)))
+    return render(request, 'events/index.html', context)
 
 def details(request, event_id, event_slug):
     event = get_object_or_404(Event, pk=event_id)
@@ -58,10 +66,10 @@ def details(request, event_id, event_slug):
     except AttendanceEvent.DoesNotExist:
         pass
 
+    context = {'event': event, 'ics_path': request.build_absolute_uri(reverse('event_ics', args=(event.id,)))}
     if is_attendance_event:
-        context = {
+        context.update({
                 'now': timezone.now(),
-                'event': event,
                 'attendance_event': attendance_event,
                 'user_anonymous': user_anonymous,
                 'user_attending': user_attending,
@@ -71,12 +79,8 @@ def details(request, event_id, event_slug):
                 'place_on_wait_list': int(place_on_wait_list),
                 #'position_in_wait_list': position_in_wait_list,
                 'captcha_form': form,
-        }
-        
-        return render(request, 'events/details.html', context)
-    else:
-        return render(request, 'events/details.html', {'event': event})
-
+        })
+    return render(request, 'events/details.html', context)
 
 def get_attendee(attendee_id):
     return get_object_or_404(Attendee, pk=attendee_id)
@@ -183,3 +187,54 @@ def generate_pdf(request, event_id):
             return redirect(event)
 
     return EventPDF(event).render_pdf()
+
+def calendar_export(request, event_id=None, user=None):
+    cal = icalendar.Calendar()
+    cal.add('prodid', '-//Online//Onlineweb//EN')
+    cal.add('version', '2.0')
+    filename = 'online'
+    if event_id:
+        # Single event
+        try:
+            events = [Event.objects.get(id=event_id)]
+        except Event.DoesNotExist:
+            events = []
+        filename = str(event_id)
+    elif user:
+        # Personalized calendar
+        # This calendar is publicly available, but the url is not guessable so data should not be leaked to everyone
+        signer = Signer()
+        try:
+            username = signer.unsign(user)
+            user = User.objects.get(username=username)
+        except BadSignature, User.DoesNotExist:
+            user = None
+        if user:
+            # Getting all events that the user has/is participating in
+            events = Event.objects.filter(attendance_event__attendees__user=user).order_by('event_start').prefetch_related(
+            'attendance_event', 'attendance_event__attendees')
+            filename = username
+        else:
+            events = []
+    else:
+        # All events that haven't ended yet
+        events = Event.objects.filter(event_end__gt=timezone.now()).order_by('event_start')
+        filename = 'events'
+
+    for event in events:
+        cal_event = icalendar.Event()
+
+        cal_event.add('dtstart', event.event_start)
+        cal_event.add('dtend', event.event_end)
+        cal_event.add('location', event.location)
+        cal_event.add('summary', event.title)
+        cal_event.add('description', event.ingress_short)
+        cal_event.add('uid', 'event-' + str(event.id) + '@online.ntnu.no')
+
+        cal.add_component(cal_event)
+
+    response = HttpResponse(cal.to_ical(), mimetype='text/calendar')
+    response['Content-Type'] = 'text/calendar; charset=utf-8';
+    response['Content-Disposition'] = 'attachment; filename=' + filename + '.ics'
+
+    return response


Test patch: