From 359b38924a79d43588a6a7097a154cb3d2fde62d Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Thu, 10 Feb 2011 16:48:48 -0500 Subject: [PATCH] Initial implementation of a working ICalendarFeedView based on the Django syndication framework and Penfield FeedView. --- contrib/julian/admin.py | 31 ++-- contrib/julian/feedgenerator.py | 77 +++++++++ ...add_field_event_last_modified__add_fiel.py | 138 ++++++++++++++++ .../0003_auto__add_icalendarfeedview.py | 153 ++++++++++++++++++ ...content_type__chg_field_event_location_.py | 150 +++++++++++++++++ contrib/julian/models.py | 141 +++++++++++++++- 6 files changed, 673 insertions(+), 17 deletions(-) create mode 100644 contrib/julian/feedgenerator.py create mode 100644 contrib/julian/migrations/0002_auto__add_field_event_created__add_field_event_last_modified__add_fiel.py create mode 100644 contrib/julian/migrations/0003_auto__add_icalendarfeedview.py create mode 100644 contrib/julian/migrations/0004_auto__chg_field_event_location_content_type__chg_field_event_location_.py diff --git a/contrib/julian/admin.py b/contrib/julian/admin.py index a822e1d..c8ef95a 100644 --- a/contrib/julian/admin.py +++ b/contrib/julian/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from philo.admin import EntityAdmin -from philo.contrib.julian.models import Location, Event, Calendar +from philo.contrib.julian.models import Location, Event, Calendar, ICalendarFeedView class LocationAdmin(EntityAdmin): @@ -8,17 +8,17 @@ class LocationAdmin(EntityAdmin): class EventAdmin(EntityAdmin): - fieldsets = ( - (None, { - 'fields': ('title', 'description', 'tags', 'parent_event', 'owner') - }), - ('Location', { - 'fields': ('location_content_type', 'location_pk') - }), - ('Time', { - 'fields': (('start_date', 'start_time'), ('end_date', 'end_time'),), - }) - ) + #fieldsets = ( + # (None, { + # 'fields': ('name', 'slug' 'description', 'tags', 'parent_event', 'owner') + # }), + # ('Location', { + # 'fields': ('location_content_type', 'location_pk') + # }), + # ('Time', { + # 'fields': (('start_date', 'start_time'), ('end_date', 'end_time'),), + # }) + #) related_lookup_fields = { 'generic': [["location_content_type", "location_pk"]] } @@ -28,6 +28,11 @@ class CalendarAdmin(EntityAdmin): pass +class ICalendarFeedViewAdmin(EntityAdmin): + pass + + admin.site.register(Location, LocationAdmin) admin.site.register(Event, EventAdmin) -admin.site.register(Calendar, CalendarAdmin) \ No newline at end of file +admin.site.register(Calendar, CalendarAdmin) +admin.site.register(ICalendarFeedView, ICalendarFeedViewAdmin) \ No newline at end of file diff --git a/contrib/julian/feedgenerator.py b/contrib/julian/feedgenerator.py new file mode 100644 index 0000000..274f617 --- /dev/null +++ b/contrib/julian/feedgenerator.py @@ -0,0 +1,77 @@ +from django.utils.feedgenerator import SyndicationFeed +import vobject + + +# Map the keys in the ICalendarFeed internal dictionary to the names of iCalendar attributes. +FEED_ICAL_MAP = { + 'title': 'x-wr-calname', + 'description': 'x-wr-caldesc', + #'link': ???, + #'language': ???, + #author_email + #author_name + #author_link + #subtitle + #categories + #feed_url + #feed_copyright + 'id': 'prodid', + 'ttl': 'x-published-ttl' +} + + +ITEM_ICAL_MAP = { + 'title': 'summary', + 'description': 'description', + 'link': 'url', + # author_email, author_name, and author_link need special handling. Consider them the + # 'organizer' of the event and + # construct something based on that. + 'pubdate': 'created', + 'last_modified': 'last-modified', + #'comments' require special handling as well + 'unique_id': 'uid', + 'enclosure': 'attach', # does this need special handling? + 'categories': 'categories', # does this need special handling? + # ttl is ignored. + 'start': 'dtstart', + 'end': 'dtend', +} + + +class ICalendarFeed(SyndicationFeed): + #def __init__(self, title, link, description, language=None, author_email=None, + # author_name=None, author_link=None, subtitle=None, categories=None, + # feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs): + # super(ICalendarFeed, self).__init__(title, link, description, language, + # author_email, author_name, author_link, subtitle, categories, + # feed_url, feed_copyright, feed_guid, ttl, **kwargs) + # + mime_type = 'text/calendar' + + def add_item(self, *args, **kwargs): + for kwarg in ['start', 'end', 'last_modified', 'location']: + kwargs.setdefault(kwarg, None) + super(ICalendarFeed, self).add_item(*args, **kwargs) + + def write(self, outfile, encoding): + # TODO: Use encoding... how? Just convert all values when setting them should work... + cal = vobject.iCalendar() + + # IE/Outlook needs this. See + # + cal.add('method').value = 'PUBLISH' + + for key, val in self.feed.items(): + if key in FEED_ICAL_MAP and val: + cal.add(FEED_ICAL_MAP[key]).value = val + + for item in self.items: + # TODO: handle multiple types of events. + event = cal.add('vevent') + for key, val in item.items(): + #TODO: handle the non-standard items like comments and author. + if key in ITEM_ICAL_MAP and val: + event.add(ITEM_ICAL_MAP[key]).value = val + + cal.serialize(outfile) \ No newline at end of file diff --git a/contrib/julian/migrations/0002_auto__add_field_event_created__add_field_event_last_modified__add_fiel.py b/contrib/julian/migrations/0002_auto__add_field_event_created__add_field_event_last_modified__add_fiel.py new file mode 100644 index 0000000..c0c2fad --- /dev/null +++ b/contrib/julian/migrations/0002_auto__add_field_event_created__add_field_event_last_modified__add_fiel.py @@ -0,0 +1,138 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'Event.created' + db.add_column('julian_event', 'created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, default=datetime.datetime(2011, 2, 10, 15, 14, 35, 721341), blank=True), keep_default=False) + + # Adding field 'Event.last_modified' + db.add_column('julian_event', 'last_modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, default=datetime.datetime(2011, 2, 10, 15, 14, 42, 370147), blank=True), keep_default=False) + + # Adding field 'Event.uuid' + db.add_column('julian_event', 'uuid', self.gf('django.db.models.fields.TextField')(default=''), keep_default=False) + + # Adding field 'Calendar.slug' + db.add_column('julian_calendar', 'slug', self.gf('django.db.models.fields.SlugField')(default='', max_length=100, db_index=True), keep_default=False) + + # Adding field 'Calendar.description' + db.add_column('julian_calendar', 'description', self.gf('django.db.models.fields.TextField')(default='', blank=True), keep_default=False) + + # Changing field 'Calendar.uuid' + db.alter_column('julian_calendar', 'uuid', self.gf('django.db.models.fields.TextField')(unique=True)) + + + def backwards(self, orm): + + # Deleting field 'Event.created' + db.delete_column('julian_event', 'created') + + # Deleting field 'Event.last_modified' + db.delete_column('julian_event', 'last_modified') + + # Deleting field 'Event.uuid' + db.delete_column('julian_event', 'uuid') + + # Deleting field 'Calendar.slug' + db.delete_column('julian_calendar', 'slug') + + # Deleting field 'Calendar.description' + db.delete_column('julian_calendar', 'description') + + # Changing field 'Calendar.uuid' + db.alter_column('julian_calendar', 'uuid', self.gf('django.db.models.fields.CharField')(max_length=100, unique=True)) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'julian.calendar': { + 'Meta': {'object_name': 'Calendar'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'events': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'calendars'", 'symmetrical': 'False', 'to': "orm['julian.Event']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.TextField', [], {'unique': 'True'}) + }, + 'julian.event': { + 'Meta': {'object_name': 'Event'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('philo.models.fields.TemplateField', [], {}), + 'end_date': ('django.db.models.fields.DateField', [], {}), + 'end_time': ('django.db.models.fields.TimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'location_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'location_pk': ('django.db.models.fields.TextField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'parent_event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['julian.Event']", 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'start_date': ('django.db.models.fields.DateField', [], {}), + 'start_time': ('django.db.models.fields.TimeField', [], {'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.Tag']", 'null': 'True', 'blank': 'True'}), + 'uuid': ('django.db.models.fields.TextField', [], {}) + }, + 'julian.location': { + 'Meta': {'object_name': 'Location'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.attribute': { + 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, + 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), + 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + } + } + + complete_apps = ['julian'] diff --git a/contrib/julian/migrations/0003_auto__add_icalendarfeedview.py b/contrib/julian/migrations/0003_auto__add_icalendarfeedview.py new file mode 100644 index 0000000..0f31330 --- /dev/null +++ b/contrib/julian/migrations/0003_auto__add_icalendarfeedview.py @@ -0,0 +1,153 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'ICalendarFeedView' + db.create_table('julian_icalendarfeedview', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('feed_type', self.gf('django.db.models.fields.CharField')(default='application/atom+xml', max_length=50)), + ('feed_suffix', self.gf('django.db.models.fields.CharField')(default='feed', max_length=255)), + ('feeds_enabled', self.gf('django.db.models.fields.BooleanField')(default=True, blank=True)), + ('item_title_template', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='julian_icalendarfeedview_title_related', null=True, to=orm['philo.Template'])), + ('item_description_template', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='julian_icalendarfeedview_description_related', null=True, to=orm['philo.Template'])), + ('calendar', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['julian.Calendar'])), + )) + db.send_create_signal('julian', ['ICalendarFeedView']) + + + def backwards(self, orm): + + # Deleting model 'ICalendarFeedView' + db.delete_table('julian_icalendarfeedview') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'julian.calendar': { + 'Meta': {'object_name': 'Calendar'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'events': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'calendars'", 'symmetrical': 'False', 'to': "orm['julian.Event']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.TextField', [], {'unique': 'True'}) + }, + 'julian.event': { + 'Meta': {'object_name': 'Event'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('philo.models.fields.TemplateField', [], {}), + 'end_date': ('django.db.models.fields.DateField', [], {}), + 'end_time': ('django.db.models.fields.TimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'location_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'location_pk': ('django.db.models.fields.TextField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'parent_event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['julian.Event']", 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'start_date': ('django.db.models.fields.DateField', [], {}), + 'start_time': ('django.db.models.fields.TimeField', [], {'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.Tag']", 'null': 'True', 'blank': 'True'}), + 'uuid': ('django.db.models.fields.TextField', [], {}) + }, + 'julian.icalendarfeedview': { + 'Meta': {'object_name': 'ICalendarFeedView'}, + 'calendar': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['julian.Calendar']"}), + 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}), + 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'application/atom+xml'", 'max_length': '50'}), + 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'julian_icalendarfeedview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'julian_icalendarfeedview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}) + }, + 'julian.location': { + 'Meta': {'object_name': 'Location'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.attribute': { + 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, + 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), + 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.node': { + 'Meta': {'object_name': 'Node'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'philo.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + }, + 'philo.template': { + 'Meta': {'object_name': 'Template'}, + 'code': ('philo.models.fields.TemplateField', [], {}), + 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + } + } + + complete_apps = ['julian'] diff --git a/contrib/julian/migrations/0004_auto__chg_field_event_location_content_type__chg_field_event_location_.py b/contrib/julian/migrations/0004_auto__chg_field_event_location_content_type__chg_field_event_location_.py new file mode 100644 index 0000000..42b5702 --- /dev/null +++ b/contrib/julian/migrations/0004_auto__chg_field_event_location_content_type__chg_field_event_location_.py @@ -0,0 +1,150 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Event.location_content_type' + db.alter_column('julian_event', 'location_content_type_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'], null=True, blank=True)) + + # Changing field 'Event.location_pk' + db.alter_column('julian_event', 'location_pk', self.gf('django.db.models.fields.TextField')(blank=True)) + + + def backwards(self, orm): + + # Changing field 'Event.location_content_type' + db.alter_column('julian_event', 'location_content_type_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])) + + # Changing field 'Event.location_pk' + db.alter_column('julian_event', 'location_pk', self.gf('django.db.models.fields.TextField')()) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'julian.calendar': { + 'Meta': {'object_name': 'Calendar'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'events': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'calendars'", 'symmetrical': 'False', 'to': "orm['julian.Event']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'db_index': 'True'}), + 'uuid': ('django.db.models.fields.TextField', [], {'unique': 'True'}) + }, + 'julian.event': { + 'Meta': {'object_name': 'Event'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('philo.models.fields.TemplateField', [], {}), + 'end_date': ('django.db.models.fields.DateField', [], {}), + 'end_time': ('django.db.models.fields.TimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'location_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'location_pk': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'parent_event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['julian.Event']", 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'start_date': ('django.db.models.fields.DateField', [], {}), + 'start_time': ('django.db.models.fields.TimeField', [], {'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.Tag']", 'null': 'True', 'blank': 'True'}), + 'uuid': ('django.db.models.fields.TextField', [], {}) + }, + 'julian.icalendarfeedview': { + 'Meta': {'object_name': 'ICalendarFeedView'}, + 'calendar': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['julian.Calendar']"}), + 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}), + 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'application/atom+xml'", 'max_length': '50'}), + 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'julian_icalendarfeedview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'julian_icalendarfeedview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}) + }, + 'julian.location': { + 'Meta': {'object_name': 'Location'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.attribute': { + 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, + 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), + 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.node': { + 'Meta': {'object_name': 'Node'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'philo.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + }, + 'philo.template': { + 'Meta': {'object_name': 'Template'}, + 'code': ('philo.models.fields.TemplateField', [], {}), + 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + } + } + + complete_apps = ['julian'] diff --git a/contrib/julian/models.py b/contrib/julian/models.py index 29f5fd4..9556d89 100644 --- a/contrib/julian/models.py +++ b/contrib/julian/models.py @@ -1,9 +1,15 @@ from django.conf import settings +from django.conf.urls.defaults import url, patterns, include +from django.contrib.auth.models import User from django.contrib.contenttypes.generic import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.db import models +from django.http import HttpResponse +from django.utils.encoding import force_unicode +from philo.contrib.julian.feedgenerator import ICalendarFeed +from philo.contrib.penfield.models import FeedView, FEEDS from philo.models.base import Tag, Entity from philo.models.fields import TemplateField from philo.utils import ContentTypeRegistryLimiter @@ -15,6 +21,10 @@ import re FPI_REGEX = re.compile(r"(|\+//|-//)[^/]+//[^/]+//[A-Z]{2}") +ICALENDAR = ICalendarFeed.mime_type +FEEDS[ICALENDAR] = ICalendarFeed + + location_content_type_limiter = ContentTypeRegistryLimiter() @@ -52,6 +62,12 @@ class TimedModel(models.Model): if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time: raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__) + def get_start(self): + return self.start_date + + def get_end(self): + return self.end_date + class Meta: abstract = True @@ -60,8 +76,8 @@ class Event(Entity, TimedModel): name = models.CharField(max_length=255) slug = models.SlugField(max_length=255) - location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter) - location_pk = models.TextField() + location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True) + location_pk = models.TextField(blank=True) location = GenericForeignKey('location_content_type', 'location_pk') description = TemplateField() @@ -72,13 +88,130 @@ class Event(Entity, TimedModel): owner = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User')) - # TODO: Add uid - use as pk? + created = models.DateTimeField(auto_now_add=True) + last_modified = models.DateTimeField(auto_now=True) + uuid = models.TextField() # Format? class Calendar(Entity): name = models.CharField(max_length=100) + slug = models.SlugField(max_length=100) + description = models.TextField(blank=True) #slug = models.SlugField(max_length=255, unique=True) events = models.ManyToManyField(Event, related_name='calendars') # TODO: Can we auto-generate this on save based on site id and calendar name and settings language? - uuid = models.CharField("Calendar UUID", max_length=100, unique=True, help_text="Should conform to Formal Public Identifier format. See ", validators=[RegexValidator(FPI_REGEX)]) \ No newline at end of file + uuid = models.TextField("Calendar UUID", unique=True, help_text="Should conform to Formal Public Identifier format. See <http://en.wikipedia.org/wiki/Formal_Public_Identifier>", validators=[RegexValidator(FPI_REGEX)]) + + +class ICalendarFeedView(FeedView): + calendar = models.ForeignKey(Calendar) + + item_context_var = "events" + object_attr = "calendar" + + def get_reverse_params(self, obj): + return 'feed', [], {} + + @property + def urlpatterns(self): + return patterns('', + url(r'^$', self.feed_view('get_all_events', 'feed'), name='feed') + ) + + def feed_view(self, get_items_attr, reverse_name): + """ + Returns a view function that renders a list of items as a feed. + """ + get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr) + + def inner(request, extra_context=None, *args, **kwargs): + obj = self.get_object(request, *args, **kwargs) + feed = self.get_feed(obj, request, reverse_name) + items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs) + self.populate_feed(feed, items, request) + + response = HttpResponse(mimetype=feed.mime_type) + feed.write(response, 'utf-8') + + if FEEDS[self.feed_type] == ICalendarFeed: + # Add some extra information to the response for iCalendar readers. + # + # Also, __get_dynamic_attr is protected by python - mangled. Should it + # just be private? + filename = self._FeedView__get_dynamic_attr('filename', obj) + response['Filename'] = filename + response['Content-Disposition'] = 'attachment; filename=%s' % filename + return response + + return inner + + def get_event_queryset(self): + return self.calendar.events.all() + + def get_all_events(self, request, extra_context=None): + return self.get_event_queryset(), extra_context + + def title(self, obj): + return obj.name + + def link(self, obj): + # Link is ignored anyway... + return "" + + def filename(self, obj): + return "%s.ics" % obj.slug + + def feed_guid(self, obj): + # Is this correct? Should I have a different id for different subfeeds? + return obj.uuid + + def description(self, obj): + return obj.description + + # Would this be meaningful? I think it's just ignored anyway, for ical format. + #def categories(self, obj): + # event_ct = ContentType.objects.get_for_model(Event) + # event_pks = obj.events.values_list('pk') + # return [tag.name for tag in Tag.objects.filter(content_type=event_ct, object_id__in=event_pks)] + + def item_title(self, item): + return item.name + + def item_description(self, item): + return item.description + + def item_link(self, item): + return self.reverse(item) + + def item_guid(self, item): + return item.uuid + + def item_author_name(self, item): + if item.owner: + return item.owner.get_full_name() + + def item_author_email(self, item): + return getattr(item.owner, 'email', None) or None + + def item_pubdate(self, item): + return item.created + + def item_categories(self, item): + return [tag.name for tag in item.tags.all()] + + def item_extra_kwargs(self, item): + return { + 'start': item.get_start(), + 'end': item.get_end(), + 'last_modified': item.last_modified, + # Is forcing unicode enough, or should we look for a "custom method"? + 'location': force_unicode(item.location), + } + + class Meta: + verbose_name = "iCalendar view" + +field = ICalendarFeedView._meta.get_field('feed_type') +field._choices += ((ICALENDAR, 'iCalendar'),) +field.default = ICALENDAR \ No newline at end of file -- 2.20.1