Initial implementation of a working ICalendarFeedView based on the Django syndication...
[philo.git] / contrib / julian / models.py
1 from django.conf import settings
2 from django.conf.urls.defaults import url, patterns, include
3 from django.contrib.auth.models import User
4 from django.contrib.contenttypes.generic import GenericForeignKey
5 from django.contrib.contenttypes.models import ContentType
6 from django.core.exceptions import ValidationError
7 from django.core.validators import RegexValidator
8 from django.db import models
9 from django.http import HttpResponse
10 from django.utils.encoding import force_unicode
11 from philo.contrib.julian.feedgenerator import ICalendarFeed
12 from philo.contrib.penfield.models import FeedView, FEEDS
13 from philo.models.base import Tag, Entity
14 from philo.models.fields import TemplateField
15 from philo.utils import ContentTypeRegistryLimiter
16 import re
17
18
19 # TODO: Could this regex more closely match the Formal Public Identifier spec?
20 # http://xml.coverpages.org/tauber-fpi.html
21 FPI_REGEX = re.compile(r"(|\+//|-//)[^/]+//[^/]+//[A-Z]{2}")
22
23
24 ICALENDAR = ICalendarFeed.mime_type
25 FEEDS[ICALENDAR] = ICalendarFeed
26
27
28 location_content_type_limiter = ContentTypeRegistryLimiter()
29
30
31 def register_location_model(model):
32         location_content_type_limiter.register_class(model)
33
34
35 def unregister_location_model(model):
36         location_content_type_limiter.unregister_class(model)
37
38
39 class Location(Entity):
40         name = models.CharField(max_length=255)
41         
42         def __unicode__(self):
43                 return self.name
44
45
46 register_location_model(Location)
47
48
49 class TimedModel(models.Model):
50         start_date = models.DateField(help_text="YYYY-MM-DD")
51         start_time = models.TimeField(blank=True, null=True, help_text="HH:MM:SS - 24 hour clock")
52         end_date = models.DateField()
53         end_time = models.TimeField(blank=True, null=True)
54         
55         def is_all_day(self):
56                 return self.start_time is None and self.end_time is None
57         
58         def clean(self):
59                 if bool(self.start_time) != bool(self.end_time):
60                         raise ValidationError("A %s must have either a start time and an end time or neither.")
61                 
62                 if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time:
63                         raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
64         
65         def get_start(self):
66                 return self.start_date
67         
68         def get_end(self):
69                 return self.end_date
70         
71         class Meta:
72                 abstract = True
73
74
75 class Event(Entity, TimedModel):
76         name = models.CharField(max_length=255)
77         slug = models.SlugField(max_length=255)
78         
79         location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
80         location_pk = models.TextField(blank=True)
81         location = GenericForeignKey('location_content_type', 'location_pk')
82         
83         description = TemplateField()
84         
85         tags = models.ManyToManyField(Tag, blank=True, null=True)
86         
87         parent_event = models.ForeignKey('self', blank=True, null=True)
88         
89         owner = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'))
90         
91         created = models.DateTimeField(auto_now_add=True)
92         last_modified = models.DateTimeField(auto_now=True)
93         uuid = models.TextField() # Format?
94
95
96 class Calendar(Entity):
97         name = models.CharField(max_length=100)
98         slug = models.SlugField(max_length=100)
99         description = models.TextField(blank=True)
100         #slug = models.SlugField(max_length=255, unique=True)
101         events = models.ManyToManyField(Event, related_name='calendars')
102         
103         # TODO: Can we auto-generate this on save based on site id and calendar name and settings language?
104         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)])
105
106
107 class ICalendarFeedView(FeedView):
108         calendar = models.ForeignKey(Calendar)
109         
110         item_context_var = "events"
111         object_attr = "calendar"
112         
113         def get_reverse_params(self, obj):
114                 return 'feed', [], {}
115         
116         @property
117         def urlpatterns(self):
118                 return patterns('',
119                         url(r'^$', self.feed_view('get_all_events', 'feed'), name='feed')
120                 )
121         
122         def feed_view(self, get_items_attr, reverse_name):
123                 """
124                 Returns a view function that renders a list of items as a feed.
125                 """
126                 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
127                 
128                 def inner(request, extra_context=None, *args, **kwargs):
129                         obj = self.get_object(request, *args, **kwargs)
130                         feed = self.get_feed(obj, request, reverse_name)
131                         items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
132                         self.populate_feed(feed, items, request)
133                         
134                         response = HttpResponse(mimetype=feed.mime_type)
135                         feed.write(response, 'utf-8')
136                         
137                         if FEEDS[self.feed_type] == ICalendarFeed:
138                                 # Add some extra information to the response for iCalendar readers.
139                                 # <http://blog.thescoop.org/archives/2007/07/31/django-ical-and-vobject/>
140                                 # Also, __get_dynamic_attr is protected by python - mangled. Should it
141                                 # just be private?
142                                 filename = self._FeedView__get_dynamic_attr('filename', obj)
143                                 response['Filename'] = filename
144                                 response['Content-Disposition'] = 'attachment; filename=%s' % filename
145                         return response
146                 
147                 return inner
148         
149         def get_event_queryset(self):
150                 return self.calendar.events.all()
151         
152         def get_all_events(self, request, extra_context=None):
153                 return self.get_event_queryset(), extra_context
154         
155         def title(self, obj):
156                 return obj.name
157         
158         def link(self, obj):
159                 # Link is ignored anyway...
160                 return ""
161         
162         def filename(self, obj):
163                 return "%s.ics" % obj.slug
164         
165         def feed_guid(self, obj):
166                 # Is this correct? Should I have a different id for different subfeeds?
167                 return obj.uuid
168         
169         def description(self, obj):
170                 return obj.description
171         
172         # Would this be meaningful? I think it's just ignored anyway, for ical format.
173         #def categories(self, obj):
174         #       event_ct = ContentType.objects.get_for_model(Event)
175         #       event_pks = obj.events.values_list('pk')
176         #       return [tag.name for tag in Tag.objects.filter(content_type=event_ct, object_id__in=event_pks)]
177         
178         def item_title(self, item):
179                 return item.name
180         
181         def item_description(self, item):
182                 return item.description
183         
184         def item_link(self, item):
185                 return self.reverse(item)
186         
187         def item_guid(self, item):
188                 return item.uuid
189         
190         def item_author_name(self, item):
191                 if item.owner:
192                         return item.owner.get_full_name()
193         
194         def item_author_email(self, item):
195                 return getattr(item.owner, 'email', None) or None
196         
197         def item_pubdate(self, item):
198                 return item.created
199         
200         def item_categories(self, item):
201                 return [tag.name for tag in item.tags.all()]
202         
203         def item_extra_kwargs(self, item):
204                 return {
205                         'start': item.get_start(),
206                         'end': item.get_end(),
207                         'last_modified': item.last_modified,
208                         # Is forcing unicode enough, or should we look for a "custom method"?
209                         'location': force_unicode(item.location),
210                 }
211         
212         class Meta:
213                 verbose_name = "iCalendar view"
214
215 field = ICalendarFeedView._meta.get_field('feed_type')
216 field._choices += ((ICALENDAR, 'iCalendar'),)
217 field.default = ICALENDAR