Initial implementation of a working ICalendarFeedView based on the Django syndication...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 10 Feb 2011 21:48:48 +0000 (16:48 -0500)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 10 Feb 2011 21:48:48 +0000 (16:48 -0500)
contrib/julian/admin.py
contrib/julian/feedgenerator.py [new file with mode: 0644]
contrib/julian/migrations/0002_auto__add_field_event_created__add_field_event_last_modified__add_fiel.py [new file with mode: 0644]
contrib/julian/migrations/0003_auto__add_icalendarfeedview.py [new file with mode: 0644]
contrib/julian/migrations/0004_auto__chg_field_event_location_content_type__chg_field_event_location_.py [new file with mode: 0644]
contrib/julian/models.py

index a822e1d..c8ef95a 100644 (file)
@@ -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 (file)
index 0000000..274f617
--- /dev/null
@@ -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 <http://tools.ietf.org/html/rfc5545#section-3.8.4.3> and
+       # construct something based on that.
+       'pubdate': 'created',
+       'last_modified': 'last-modified',
+       #'comments' require special handling as well <http://tools.ietf.org/html/rfc5545#section-3.8.1.4>
+       '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
+               # <http://blog.thescoop.org/archives/2007/07/31/django-ical-and-vobject/>
+               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 (file)
index 0000000..c0c2fad
--- /dev/null
@@ -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 (file)
index 0000000..0f31330
--- /dev/null
@@ -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 (file)
index 0000000..42b5702
--- /dev/null
@@ -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']
index 29f5fd4..9556d89 100644 (file)
@@ -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 <http://en.wikipedia.org/wiki/Formal_Public_Identifier>", 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 &lt;http://en.wikipedia.org/wiki/Formal_Public_Identifier&gt;", 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.
+                               # <http://blog.thescoop.org/archives/2007/07/31/django-ical-and-vobject/>
+                               # 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