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, ObjectDoesNotExist
7 from django.core.validators import RegexValidator
8 from django.db import models
9 from django.http import HttpResponse, Http404
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.exceptions import ViewCanNotProvideSubpath
14 from philo.models import Tag, Entity, Page, TemplateField
15 from philo.utils import ContentTypeRegistryLimiter
16 import re, datetime, calendar
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}")
24 ICALENDAR = ICalendarFeed.mime_type
25 FEEDS[ICALENDAR] = ICalendarFeed
28 location_content_type_limiter = ContentTypeRegistryLimiter()
31 def register_location_model(model):
32 location_content_type_limiter.register_class(model)
35 def unregister_location_model(model):
36 location_content_type_limiter.unregister_class(model)
39 class Location(Entity):
40 name = models.CharField(max_length=255)
41 slug = models.SlugField(max_length=255, unique=True)
43 def __unicode__(self):
47 register_location_model(Location)
50 class TimedModel(models.Model):
51 start_date = models.DateField(help_text="YYYY-MM-DD")
52 start_time = models.TimeField(blank=True, null=True, help_text="HH:MM:SS - 24 hour clock")
53 end_date = models.DateField()
54 end_time = models.TimeField(blank=True, null=True)
57 return self.start_time is None and self.end_time is None
60 if bool(self.start_time) != bool(self.end_time):
61 raise ValidationError("A %s must have either a start time and an end time or neither.")
63 if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time:
64 raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
67 return self.start_date
76 class Event(Entity, TimedModel):
77 name = models.CharField(max_length=255)
78 slug = models.SlugField(max_length=255, unique_for_date='start_date')
80 location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
81 location_pk = models.TextField(blank=True)
82 location = GenericForeignKey('location_content_type', 'location_pk')
84 description = TemplateField()
86 tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
88 parent_event = models.ForeignKey('self', blank=True, null=True)
91 owner = models.ForeignKey(User, related_name='owned_events')
93 created = models.DateTimeField(auto_now_add=True)
94 last_modified = models.DateTimeField(auto_now=True)
95 uuid = models.TextField() # Format?
97 def __unicode__(self):
101 class Calendar(Entity):
102 name = models.CharField(max_length=100)
103 slug = models.SlugField(max_length=100)
104 description = models.TextField(blank=True)
105 events = models.ManyToManyField(Event, related_name='calendars')
107 # TODO: Can we auto-generate this on save based on site id and calendar name and settings language?
108 uuid = models.TextField("Calendar UUID", unique=True, help_text="Should conform to Formal Public Identifier format. See <a href='http://en.wikipedia.org/wiki/Formal_Public_Identifier'>Wikipedia</a>.", validators=[RegexValidator(FPI_REGEX)])
110 def __unicode__(self):
114 class CalendarView(FeedView):
115 calendar = models.ForeignKey(Calendar)
116 index_page = models.ForeignKey(Page, related_name="calendar_index_related")
117 event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
119 timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related", blank=True, null=True)
120 tag_page = models.ForeignKey(Page, related_name="calendar_tag_related", blank=True, null=True)
121 location_page = models.ForeignKey(Page, related_name="calendar_location_related", blank=True, null=True)
122 owner_page = models.ForeignKey(Page, related_name="calendar_owner_related", blank=True, null=True)
124 tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
125 location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
126 owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
128 tag_permalink_base = models.CharField(max_length=30, default='tags')
129 owner_permalink_base = models.CharField(max_length=30, default='owners')
130 location_permalink_base = models.CharField(max_length=30, default='locations')
131 events_per_page = models.PositiveIntegerField(blank=True, null=True)
133 item_context_var = "events"
134 object_attr = "calendar"
136 def get_reverse_params(self, obj):
137 if isinstance(obj, User):
138 return 'events_for_user', [], {'username': obj.username}
139 elif isinstance(obj, Event):
140 return 'event_detail', [], {
141 'year': str(obj.start_date.year).zfill(4),
142 'month': str(obj.start_date.month).zfill(2),
143 'day': str(obj.start_date.day).zfill(2),
146 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
147 if isinstance(obj, Tag):
149 return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
150 raise ViewCanNotProvideSubpath
152 def timespan_patterns(self, timespan_name):
153 return self.feed_patterns('get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
156 def urlpatterns(self):
157 urlpatterns = patterns('',
158 url(r'^', include(self.feed_patterns('get_all_events', 'index_page', 'index'))),
160 url(r'^(?P<year>\d{4})', include(self.timespan_patterns('year'))),
161 url(r'^(?P<year>\d{4})/(?P<month>\d{2})', include(self.timespan_patterns('month'))),
162 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', include(self.timespan_patterns('day'))),
163 #url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<hour>\d{1,2})', include(self.timespan_patterns('hour'))),
164 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)', self.event_detail_view, name="event_detail"),
166 url(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, include(self.feed_patterns('get_events_by_owner', 'owner_page', 'events_by_user'))),
168 # Some sort of shortcut for a location would be useful. This could be on a per-calendar
169 # or per-calendar-view basis.
170 #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
171 url(r'^%s/(?P<app_label>\w+)/(?P<model>\w+)/(?P<pk>[^/]+)' % self.location_permalink_base, include(self.feed_patterns('get_events_by_location', 'location_page', 'events_by_location'))),
174 if self.feeds_enabled:
175 urlpatterns += patterns('',
176 url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)/%s$' % (self.tag_permalink_base, self.feed_suffix), self.feed_view('get_events_by_tag', 'events_by_tag_feed'), name='events_by_tag_feed'),
178 urlpatterns += patterns('',
179 url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)$' % self.tag_permalink_base, self.page_view('get_events_by_tag', 'tag_page'), name='events_by_tag')
182 if self.tag_archive_page:
183 urlpatterns += patterns('',
184 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
187 if self.owner_archive_page:
188 urlpatterns += patterns('',
189 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
192 if self.location_archive_page:
193 urlpatterns += patterns('',
194 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
198 # Basic QuerySet fetchers.
199 def get_event_queryset(self):
200 return self.calendar.events.all()
202 def get_timespan_queryset(self, year, month=None, day=None):
203 qs = self.get_event_queryset()
204 # See python documentation for the min/max values.
205 if year and month and day:
206 year, month, day = int(year), int(month), int(day)
207 start_datetime = datetime.datetime(year, month, day, 0, 0)
208 end_datetime = datetime.datetime(year, month, day, 23, 59)
210 year, month = int(year), int(month)
211 start_datetime = datetime.datetime(year, month, 1, 0, 0)
212 end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
215 start_datetime = datetime.datetime(year, 1, 1, 0, 0)
216 end_datetime = datetime.datetime(year, 12, 31, 23, 59)
218 return qs.exclude(end_date__lt=start_datetime, end_time__lt=start_datetime).exclude(start_date__gt=end_datetime, start_time__gt=end_datetime, start_time__isnull=False).exclude(start_time__isnull=True, start_date__gt=end_datetime)
220 def get_tag_queryset(self):
221 return Tag.objects.filter(events__calendars=self.calendar).distinct()
223 def get_location_querysets(self):
224 # Potential bottleneck?
226 locations = Event.objects.values_list('location_content_type', 'location_pk')
228 for ct, pk in locations:
229 location_map.setdefault(ct, []).append(pk)
231 location_cts = ContentType.objects.in_bulk(location_map.keys())
232 location_querysets = {}
234 for ct_pk, pks in location_map.items():
235 ct = location_cts[ct_pk]
236 location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
238 return location_querysets
240 def get_owner_queryset(self):
241 return User.objects.filter(owned_events__calendars=self.calendar).distinct()
243 # Event QuerySet parsers for a request/args/kwargs
244 def get_all_events(self, request, extra_context=None):
245 return self.get_event_queryset(), extra_context
247 def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
248 context = extra_context or {}
254 return self.get_timespan_queryset(year, month, day), context
256 def get_events_by_owner(self, request, username, extra_context=None):
258 owner = self.get_owner_queryset().get(username=username)
259 except User.DoesNotExist:
262 qs = self.get_event_queryset().filter(owner=owner)
263 context = extra_context or {}
269 def get_events_by_tag(self, request, tag_slugs, extra_context=None):
270 tag_slugs = tag_slugs.replace('+', '/').split('/')
271 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
276 # Raise a 404 on an incorrect slug.
277 found_slugs = [tag.slug for tag in tags]
278 for slug in tag_slugs:
279 if slug and slug not in found_slugs:
282 events = self.get_event_queryset()
284 events = events.filter(tags=tag)
286 context = extra_context or {}
287 context.update({'tags': tags})
289 return events, context
291 def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
293 ct = ContentType.objects.get(app_label=app_label, model=model)
294 location = ct.model_class()._default_manager.get(pk=pk)
295 except ObjectDoesNotExist:
298 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
300 context = extra_context or {}
304 return events, context
307 def event_detail_view(self, request, year, month, day, slug, extra_context=None):
309 event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
310 except Event.DoesNotExist:
313 context = self.get_context()
314 context.update(extra_context or {})
318 return self.event_detail_page.render_to_response(request, extra_context=context)
321 def tag_archive_view(self, request, extra_context=None):
322 tags = self.get_tag_queryset()
323 context = self.get_context()
324 context.update(extra_context or {})
328 return self.tag_archive_page.render_to_response(request, extra_context=context)
330 def location_archive_view(self, request, extra_context=None):
331 # What datastructure should locations be?
332 locations = self.get_location_querysets()
333 context = self.get_context()
334 context.update(extra_context or {})
336 'locations': locations
338 return self.location_archive_page.render_to_response(request, extra_context=context)
340 def owner_archive_view(self, request, extra_context=None):
341 owners = self.get_owner_queryset()
342 context = self.get_context()
343 context.update(extra_context or {})
347 return self.owner_archive_page.render_to_response(request, extra_context=context)
350 def process_page_items(self, request, items):
351 if self.events_per_page:
352 page_num = request.GET.get('page', 1)
353 paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
355 'paginator': paginator,
356 'paginated_page': paginated_page,
357 self.item_context_var: items
361 self.item_context_var: items
363 return items, item_context
365 # Feed information hooks
366 def title(self, obj):
370 # Link is ignored anyway...
373 def feed_guid(self, obj):
374 # Is this correct? Should I have a different id for different subfeeds?
377 def description(self, obj):
378 return obj.description
380 def feed_extra_kwargs(self, obj):
381 return {'filename': "%s.ics" % obj.slug}
383 def item_title(self, item):
386 def item_description(self, item):
387 return item.description
389 def item_link(self, item):
390 return self.reverse(item)
392 def item_guid(self, item):
395 def item_author_name(self, item):
397 return item.owner.get_full_name()
399 def item_author_email(self, item):
400 return getattr(item.owner, 'email', None) or None
402 def item_pubdate(self, item):
405 def item_categories(self, item):
406 return [tag.name for tag in item.tags.all()]
408 def item_extra_kwargs(self, item):
410 'start': item.get_start(),
411 'end': item.get_end(),
412 'last_modified': item.last_modified,
413 # Is forcing unicode enough, or should we look for a "custom method"?
414 'location': force_unicode(item.location),
417 def __unicode__(self):
418 return u"%s for %s" % (self.__class__.__name__, self.calendar)
420 field = CalendarView._meta.get_field('feed_type')
421 field._choices += ((ICALENDAR, 'iCalendar'),)
422 field.default = ICALENDAR