Skip to content

Commit

Permalink
started calendar export
Browse files Browse the repository at this point in the history
  • Loading branch information
maximz committed Jan 31, 2016
1 parent de0d42b commit df7704f
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 0 deletions.
2 changes: 2 additions & 0 deletions course_selection/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db.models.signals import post_save
from django.contrib.auth.models import AbstractBaseUser, User
import settings.common as settings
import uuid


class Semester(models.Model):
Expand Down Expand Up @@ -156,6 +157,7 @@ class Schedule(models.Model):
available_colors = models.TextField(null=True)
enrollments = models.TextField(null=True)
title = models.CharField(max_length=100, default="schedule")
ical_uuid = models.UUIDField(default=uuid.uuid4, unique=True) # uuid.uuid4 generates a random UUID (Universally Unique ID)

# class Enrollment(models.Model):
# # each course enrollment has
Expand Down
2 changes: 2 additions & 0 deletions course_selection/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@

url(r'^api/worksheet/(?P<schedule_id>\d+)$',
views.get_worksheet_pdf, name='get-worksheet-pdf'),

url(r'^ical/(?P<cal_id>).*\.ics$', views.ical_feed, name='ical-feed'),
)
114 changes: 114 additions & 0 deletions course_selection/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.db.models import Q
from django.core.cache import caches
from django.views.decorators.cache import cache_page
from django.urls import reverse
# send regardless of whether Django thinks we should

from view_cache_utils import cache_page_with_prefix
Expand Down Expand Up @@ -329,3 +330,116 @@ def fill_out_course(context, idx, enrollment):
context[checkbox_name] = checkbox_val
context[course_name] = course.primary_listing()
return context




## CALENDAR EXPORT
from icalendar import Calendar, Event, vCalAddress, vText, vDatetime, vRecur
from datetime import datetime, timedelta
from time import time
from dateutil import parser as dt_parser
import pytz
import re

@require_GET
@never_cache
def ical_feed(request, cal_id):
"""
iCal feed
Kept up-to-date
Parameter: cal_id, which is a guid that is 1:1 with schedules in our database
"""
cal = Calendar()
cal.add('prodid', '-//Recal Course Planner//recal.io//')
cal.add('version', '2.0')

sched = Schedule.objects.get(Q(ical_uuid=cal_id))
semester = sched.semester

cal.add('X-WR-CALNAME', 'ReCal %s (%s)' % (unicode(semester), sched.user.netid))
cal.add('X-WR-CALDESC', sched.title) #'ReCal Schedule'
cal.add('X-PUBLISHED-TTL', 'PT15M') # https://msdn.microsoft.com/en-us/library/ee178699(v=exchg.80).aspx. 15 minute updates.

tz = pytz.timezone("US/Eastern") # pytz.utc
# recurrence
ical_days = {
0: 'MO',
1: 'TU',
2: 'WE',
3: 'TH',
4: 'FR'
}
builtin_days = {
'M': 0,
'T': 1,
'W': 2,
'Th': 3,
'F': 4
}

#data = [hydrate_course_dict(Course.objects.get(Q(id=course['course_id']))) for course in json.loads(sched.enrollments)]

day_of_week_semester_start = semester.start_date.weekday() # 0-6, monday is 0, sunday is 6. we will have values of 0 (Monday) or 2 (Wednesday)

for course_obj in json.loads(sched.enrollments):
#course = Course.objects.get(Q(id=course_obj['course_id'])) # course_obj is json object; course is model
for section_id in course_obj['sections']:
section = Section.objects.get(Q(id=section_id))
for meeting in section.meetings.all():
event = Event()
event.add('summary', unicode(section)) # name of the event
event.add('location', vText(meeting.location + ', Princeton, NJ'))

## compute first meeting date.
# days when the class meets. convert them to day difference relative to first date of the semester
daysofweek = [builtin_days[i] for i in meeting.days.split() ] # split by space. format: 0-4. monday is 0, friday is 4. matches python weekday() format.
dayofweek_relative_to_semester_start = []
for dow in daysofweek:
diff = dow - day_of_week_semester_start
if diff < 0:
diff += 7 # add a week
dayofweek_relative_to_semester_start.append(diff)
assert all([d > 0 for d in dayofweek_relative_to_semester_start]) # all must be positive
first_meeting_dayofweek = min(dayofweek_relative_to_semester_start) # a T,Th class will have first meeting on T if semester starts on M, or on Th if semester starts on Wed.

## get meeting time
# meeting.start_time, meeting.end_time examples: "03:20 PM", "10:00 AM"
start_time = dt_parser.parse(meeting.start_time)
end_time = dt_parser.parse(meeting.end_time)

## add event time.
event.add('dtstart', tz.localize(datetime(semester.start_date.year, semester.start_date.month , semester.start_date.day , start_time.hour, start_time.minute , 0) + timedelta(days=first_meeting_dayofweek))) #year,month,day, hour,min,second in ET
event.add('dtend', tz.localize(datetime(semester.start_date.year, semester.start_date.month , semester.start_date.day , end_time.hour, end_time.minute , 0) + timedelta(days=first_meeting_dayofweek)))
event.add('dtstamp', tz.localize(datetime(semester.start_date.year, semester.start_date.month, semester.start_date.day, 0,0,0))) # "property specifies the DATE-TIME that iCalendar object was created". per 3.8.7.2 of RFC 5545, must be in UTC

## recurring event config
# producing e.g.: RRULE:FREQ=WEEKLY;UNTIL=[LAST DAY OF SEMESTER + 1];WKST=SU;BYDAY=TU,TH
selected_days = [ical_days[i] for i in sorted(daysofweek) ] # formatted for ical
end_date = tz.localize(datetime(semester.end_date.year, semester.end_date.month, semester.end_date.day, 0, 0, 0) + timedelta(days=1)) #[LAST DAY OF SEMESTER + 1]
event.add('rrule', vRecur({'FREQ': 'WEEKLY', 'UNTIL': end_date, 'WKST': 'SU', 'BYDAY': selected_days}))
cal.add_component(event)

ical = cal.to_ical()

# filter out blank lines
#filtered = filter(lambda x: not re.match(r'^\s*$', x), ical)
#print filtered
return HttpResponse(ical, 'text/calendar', status=200)

import uuid
def get_ical_url_for_schedule(request, schedule_id, make_new=False):
"""
Returns ical feed url for a particular schedule
Parameter: schedule_id
We look up the UUID that is 1:1 to this schedule. Each schedule has a UUID always (it is auto-created.)
If make_new, then we create a new UUID for the schedule.
Then we return the url with it
"""
schedule = Schedule.objects.get(Q(id=schedule_id))
if make_new:
schedule.ical_uuid = uuid.uuid4()
schedule.save()
return request.build_absolute_uri(reverse('ical-feed', args=(schedule.ical_uuid,)))


0 comments on commit df7704f

Please sign in to comment.