Initial implementation of a working ICalendarFeedView based on the Django syndication...
[philo.git] / contrib / julian / models.py
index c9bd6da..9556d89 100644 (file)
@@ -1,15 +1,18 @@
 from django.conf import settings
 from django.conf import settings
-from django.contrib.localflavor.us.models import USStateField
-from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation
+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.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
-from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator
+from django.core.validators import RegexValidator
 from django.db import models
 from django.db import models
-from philo.contrib.julian.fields import USZipCodeField
-from philo.models.base import Tag, Entity, Titled
+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.models.fields import TemplateField
-from philo.utils import ContentTypeSubclassLimiter
-import datetime
+from philo.utils import ContentTypeRegistryLimiter
 import re
 
 
 import re
 
 
@@ -18,53 +21,29 @@ import re
 FPI_REGEX = re.compile(r"(|\+//|-//)[^/]+//[^/]+//[A-Z]{2}")
 
 
 FPI_REGEX = re.compile(r"(|\+//|-//)[^/]+//[^/]+//[A-Z]{2}")
 
 
-class Location(Entity):
-       name = models.CharField(max_length=150, blank=True)
-       description = models.TextField(blank=True)
-       
-       longitude = models.FloatField(blank=True, validators=[MinValueValidator(-180), MaxValueValidator(180)])
-       latitude = models.FloatField(blank=True, validators=[MinValueValidator(-90), MaxValueValidator(90)])
-       
-       events = GenericRelation('Event')
-       
-       def clean(self):
-               if not (self.name or self.description) or (self.longitude is None and self.latitude is None):
-                       raise ValidationError("Either a name and description or a latitude and longitude must be defined.")
-       
-       class Meta:
-               abstract = True
+ICALENDAR = ICalendarFeed.mime_type
+FEEDS[ICALENDAR] = ICalendarFeed
 
 
 
 
-_location_content_type_limiter = ContentTypeSubclassLimiter(Location)
+location_content_type_limiter = ContentTypeRegistryLimiter()
 
 
 
 
-# TODO: Can we track when a building is open? Hmm...
-class Building(Location):
-       """A building is a location with a street address."""
-       address = models.CharField(max_length=255)
-       city = models.CharField(max_length=150)
-       
-       class Meta:
-               abstract = True
+def register_location_model(model):
+       location_content_type_limiter.register_class(model)
 
 
 
 
-_building_content_type_limiter = ContentTypeSubclassLimiter(Building)
+def unregister_location_model(model):
+       location_content_type_limiter.unregister_class(model)
 
 
 
 
-class USBuilding(Building):
-       state = USStateField()
-       zipcode = USZipCodeField()
+class Location(Entity):
+       name = models.CharField(max_length=255)
        
        
-       class Meta:
-               verbose_name = "Building (US)"
-               verbose_name_plural = "Buildings (US)"
+       def __unicode__(self):
+               return self.name
 
 
 
 
-class Venue(Location):
-       """A venue is a location inside a building"""
-       building_content_type = models.ForeignKey(ContentType, limit_choices_to=_building_content_type_limiter)
-       building_pk = models.TextField()
-       building = GenericForeignKey('building_content_type', 'building_pk')
+register_location_model(Location)
 
 
 class TimedModel(models.Model):
 
 
 class TimedModel(models.Model):
@@ -76,13 +55,29 @@ class TimedModel(models.Model):
        def is_all_day(self):
                return self.start_time is None and self.end_time is None
        
        def is_all_day(self):
                return self.start_time is None and self.end_time is None
        
+       def clean(self):
+               if bool(self.start_time) != bool(self.end_time):
+                       raise ValidationError("A %s must have either a start time and an end time or neither.")
+               
+               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
 
 
        class Meta:
                abstract = True
 
 
-class Event(Entity, Titled, TimedModel):
-       location_content_type = models.ForeignKey(ContentType, limit_choices_to=_location_content_type_limiter)
-       location_pk = models.TextField()
+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, blank=True, null=True)
+       location_pk = models.TextField(blank=True)
        location = GenericForeignKey('location_content_type', 'location_pk')
        
        description = TemplateField()
        location = GenericForeignKey('location_content_type', 'location_pk')
        
        description = TemplateField()
@@ -92,12 +87,131 @@ class Event(Entity, Titled, TimedModel):
        parent_event = models.ForeignKey('self', blank=True, null=True)
        
        owner = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'))
        parent_event = models.ForeignKey('self', blank=True, null=True)
        
        owner = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'))
+       
+       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)
 
 
 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?
        #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