Skip to content

Commit

Permalink
Merge pull request #309 from maximz/calexport
Browse files Browse the repository at this point in the history
Calendar export to GCal (including bump to Django 1.8)
  • Loading branch information
maximz committed Feb 6, 2016
2 parents de0d42b + d97c9f0 commit fb8ca1a
Show file tree
Hide file tree
Showing 16 changed files with 491 additions and 56 deletions.
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"tests"
],
"dependencies": {
"angular-animate": "^1.3.0 || >1.4.0-beta.0",
"angular-animate": "~1.4.9",
"angular-aria": "~1.4.1",
"angular-bootstrap": "~0.13.0",
"angular-hotkeys": "chieffancypants/angular-hotkeys#~1.4.5",
Expand Down
36 changes: 36 additions & 0 deletions course_selection/migrations/0023_auto_20160201_2156.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models
import uuid



class Migration(migrations.Migration):
"""
NOTE: ical_uuid must be migrated in a careful way to create unique field.
See https://github.com/django/django/commit/1f9e44030e9c5300b97ef7b029f482c53a66f13b and https://docs.djangoproject.com/en/1.9/howto/writing-migrations/#migrations-that-add-unique-fields
"""


dependencies = [
('course_selection', '0022_remove_friend_request_status'),
]

operations = [
migrations.AddField(
model_name='schedule',
name='ical_uuid',
field=models.UUIDField(default=uuid.uuid4, null=True),
),
migrations.AlterField(
model_name='nice_user',
name='last_login',
field=models.DateTimeField(null=True, verbose_name='last login', blank=True),
),
migrations.AlterField(
model_name='semester',
name='term_code',
field=models.CharField(default=1162, unique=True, max_length=4, db_index=True),
),
]
24 changes: 24 additions & 0 deletions course_selection/migrations/0024_auto_20160204_1722.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models
import uuid

def gen_uuid(apps, schema_editor):
Schedule = apps.get_model('course_selection', 'Schedule')
for row in Schedule.objects.all():
row.ical_uuid = uuid.uuid4()
row.save()


class Migration(migrations.Migration):

dependencies = [
('course_selection', '0023_auto_20160201_2156'),
]

operations = [
# omit reverse_code=... if you don't want the migration to be reversible.
migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),

]
20 changes: 20 additions & 0 deletions course_selection/migrations/0025_auto_20160204_1722.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

dependencies = [
('course_selection', '0024_auto_20160204_1722'),
]

operations = [
migrations.AlterField(
model_name='schedule',
name='ical_uuid',
field=models.UUIDField(default=uuid.uuid4, unique=True, null=False),
),
]
9 changes: 8 additions & 1 deletion course_selection/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.db import models
from django.db.models.signals import post_save
from django.contrib.auth.models import AbstractBaseUser, User
import settings.common as settings
import settings.prod as settings
import uuid


class Semester(models.Model):
Expand Down Expand Up @@ -148,6 +149,11 @@ class Meta:


class Schedule(models.Model):
"""
NOTE: ical_uuid must be migrated in a careful way to create unique field.
See https://github.com/django/django/commit/1f9e44030e9c5300b97ef7b029f482c53a66f13b and https://docs.djangoproject.com/en/1.9/howto/writing-migrations/#migrations-that-add-unique-fields
It is done in migrations 23-25. Note that you have to split data from schema migrations. So we do a schema migration, a data migration, and then a schema migration.
"""
# relationships
semester = models.ForeignKey(Semester)
user = models.ForeignKey('Nice_User')
Expand All @@ -156,6 +162,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
130 changes: 85 additions & 45 deletions course_selection/pdf.py
Original file line number Diff line number Diff line change
@@ -1,76 +1,112 @@
# retrieved from: https://gist.github.com/zyegfryed/918403
# retrieved from: https://gist.github.com/zyegfryed/918403, https://gist.github.com/grantmcconnaughey/ce90a689050c07c61c96
# used for creating pdf files to be served using django

# -*- coding: utf-8 -*-
import codecs
import subprocess
from fdfgen import forge_fdf
from django.core.exceptions import ImproperlyConfigured
from django.template import engines
from django.template.backends.base import BaseEngine
from django.template.engine import Engine, _dirs_undefined

from django.template import Template, loader
from django.template.loader import find_template, LoaderOrigin


class PdfTemplateError(Exception):
pass

class PdftkEngine(BaseEngine):

class PdfTemplate(Template):
# Going ahead and defining this, but really PDFs should still be placed
# in the templates directory of an app because the loader checks templates
app_dirname = 'pdfs'

def __init__(self, template_string, origin=None, name='<Unknown Template>'):
self.origin = origin
def __init__(self, params):
params = params.copy()
options = params.pop('OPTIONS').copy()
super(PdftkEngine, self).__init__(params)
self.engine = self._Engine(self.dirs, self.app_dirs, **options)

def get_template(self, template_name, dirs=_dirs_undefined):
return PdfTemplate(self.engine.get_template(template_name, dirs))

class _Engine(Engine):
def make_origin(self, display_name, loader, name, dirs):
# Always return an Origin object, because PDFTemplate need it to
# render the PDF Form file.
from django.template.loader import LoaderOrigin
return LoaderOrigin(display_name, loader, name, dirs)


class PdfTemplate(object):
pdftk_bin = None

def __init__(self, template):
self.template = template
self.set_pdftk_bin()

@property
def origin(self):
return self.template.origin

def render(self, context=None, request=None):
if context is None:
context = {}

def render(self, context):
context = context.items()
output, err = self.fill_form(context, self.origin.name)
if err:
raise PdfTemplateError(err)
return output

def fill_form(self, fields, src, pdftk_bin=None):
if pdftk_bin is None:
from django.conf import settings
assert hasattr(settings, 'PDFTK_BIN'), "PDF generation requires pdftk (http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/). Edit your PDFTK_BIN settings accordingly."
assert len(settings.PDFTK_BIN) > 0
pdftk_bin = settings.PDFTK_BIN

fdf_stream = forge_fdf(fdf_data_strings=fields)

cmd = [
pdftk_bin,
src,
'fill_form',
'-',
'output',
'-',
'flatten',
]
cmd = [self.pdftk_bin, src, 'fill_form', '-', 'output', '-', 'flatten']
cmd = ' '.join(cmd)

return self.run_cmd(cmd, fdf_stream)

def dump_data_fields(self):
cmd = [self.pdftk_bin, self.origin.name, 'dump_data_fields']
cmd = ' '.join(cmd)

output, err = self.run_cmd(cmd, None)
if err:
raise PdfTemplateError(err)
return output

def run_cmd(self, cmd, input_data):
try:
process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, shell=True)
return process.communicate(input=fdf_stream)
except OSError:
return None


def get_template_from_string(source, origin=None, name=None):
"""
Returns a compiled Template object for the given template code,
handling template inheritance recursively.
"""
if name and name.endswith('.pdf'):
return PdfTemplate('pdf', origin, name)
return Template(source, origin, name)
loader.get_template_from_string = get_template_from_string
if input_data:
return process.communicate(input=input_data)
else:
return process.communicate()
except OSError, e:
return None, e

def set_pdftk_bin(self):
if self.pdftk_bin is None:
from django.conf import settings
if not hasattr(settings, 'PDFTK_BIN'):
msg = "PDF generation requires pdftk " \
"(http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit). " \
"Edit your PDFTK_BIN settings accordingly."
raise ImproperlyConfigured(msg)
self.pdftk_bin = settings.PDFTK_BIN

return self.pdftk_bin

def make_origin(display_name, loader, name, dirs):
# Always return an Origin object, because PDFTemplate need it to render
# the PDF Form file.
return LoaderOrigin(display_name, loader, name, dirs)
loader.make_origin = make_origin
def version(self):
cmd = [self.pdftk_bin, '--version']
cmd = ' '.join(cmd)

output, err = self.run_cmd(cmd, None)
if err:
raise PdfTemplateError(err)
return output

def get_template(template_name):
"""
Expand All @@ -87,9 +123,13 @@ def fake_strict_errors(exception):
# Loading hacks
# Ignore UnicodeError, due to PDF file read
codecs.register_error('strict', fake_strict_errors)
# --//--
template, origin = find_template(template_name)

if template_name.endswith('.pdf'):
template = engines['pdf'].get_template(template_name)
else:
template = engines['django'].get_template(template_name)

# Loading hacks
codecs.register_error('strict', strict_errors)
# --//--
return template

return template
56 changes: 54 additions & 2 deletions course_selection/static/js/controllers/CourseSearchCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ICourse = require('../interfaces/ICourse');
import Course = require('../models/Course');
import SearchCtrl = require('./SearchCtrl');
import IScheduleManager = require('../interfaces/IScheduleManager');
import ExportScheduleModalCtrl = require('./ExportScheduleModalCtrl');
import Utils = require('../Utils');

'use strict';
Expand All @@ -18,7 +19,9 @@ class CourseSearchCtrl {
public static $inject = [
'$scope',
'$sce',
'$filter'
'$filter',
'$modal',
'$http'
];

private _scheduleManager: IScheduleManager;
Expand All @@ -29,7 +32,9 @@ class CourseSearchCtrl {
constructor(
private $scope,
private $sce,
private $filter
private $filter,
private $modal,
private $http
) {

this.$scope.vm = this;
Expand Down Expand Up @@ -184,6 +189,53 @@ class CourseSearchCtrl {
return 'blue';
}
}

// export feature
public exportToGoogleCalendar(index: number) {
// TODO: fix this validation
/* if (this.$scope.selectedSchedule != index) {
return;
} */

// make ajax call to get the url -- don't call something in ExportScheduleModalCtrl?
var ical_api_url = '/icalapi/geturl/';
var ical_api_url_makenew = '/icalapi/regenerate/';

//this.$http.get(ical_api_url + index).then( function(response) {
this.$http.get(ical_api_url + index).then((function($modal) { // have to pass in $modal to callback (http://stackoverflow.com/questions/19116815/how-to-pass-a-value-to-an-angularjs-http-success-callback)
return function(response) {
console.log('Ical api url: ' + response.data);
var icalurl = response.data;


var modalInstance = $modal.open({
templateUrl: '/static/templates/exportModal.html',
controller: ExportScheduleModalCtrl,
keyboard: true,
resolve: {
id: () => {
return index; //this.schedules[index].scheduleObject.id;
},
url: () => {
return icalurl;
},
apiurl: () => {
return ical_api_url_makenew;
}
},
backdropClass: 'modal-backdrop',
//windowClass: 'center-modal',
size: 'lg'
});

modalInstance.result.then(() => {

});
}
})(this.$modal));


}
}

export = CourseSearchCtrl;
Loading

0 comments on commit fb8ca1a

Please sign in to comment.