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, pattern, timespan_name):
153 return self.feed_patterns(pattern, 'get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
156 def urlpatterns(self):
157 # Perhaps timespans should be done with GET parameters? Or two /-separated
158 # date slugs? (e.g. 2010-02-1/2010-02-2) or a start and duration?
159 # (e.g. 2010-02-01/week/ or ?d=2010-02-01&l=week)
160 urlpatterns = self.feed_patterns(r'^', 'get_all_events', 'index_page', 'index') + \
161 self.timespan_patterns(r'^(?P<year>\d{4})', 'year') + \
162 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'month') + \
163 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'day') + \
164 self.feed_patterns(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
165 self.feed_patterns(r'^%s/(?P<app_label>\w+)/(?P<model>\w+)/(?P<pk>[^/]+)' % self.location_permalink_base, 'get_events_by_location', 'location_page', 'events_by_location') + \
166 self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
168 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$', self.event_detail_view, name="event_detail"),
171 # Some sort of shortcut for a location would be useful. This could be on a per-calendar
172 # or per-calendar-view basis.
173 #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
175 if self.tag_archive_page:
176 urlpatterns += patterns('',
177 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
180 if self.owner_archive_page:
181 urlpatterns += patterns('',
182 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
185 if self.location_archive_page:
186 urlpatterns += patterns('',
187 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
191 # Basic QuerySet fetchers.
192 def get_event_queryset(self):
193 return self.calendar.events.all()
195 def get_timespan_queryset(self, year, month=None, day=None):
196 qs = self.get_event_queryset()
197 # See python documentation for the min/max values.
198 if year and month and day:
199 year, month, day = int(year), int(month), int(day)
200 start_datetime = datetime.datetime(year, month, day, 0, 0)
201 end_datetime = datetime.datetime(year, month, day, 23, 59)
203 year, month = int(year), int(month)
204 start_datetime = datetime.datetime(year, month, 1, 0, 0)
205 end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
208 start_datetime = datetime.datetime(year, 1, 1, 0, 0)
209 end_datetime = datetime.datetime(year, 12, 31, 23, 59)
211 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)
213 def get_tag_queryset(self):
214 return Tag.objects.filter(events__calendars=self.calendar).distinct()
216 def get_location_querysets(self):
217 # Potential bottleneck?
219 locations = Event.objects.values_list('location_content_type', 'location_pk')
221 for ct, pk in locations:
222 location_map.setdefault(ct, []).append(pk)
224 location_cts = ContentType.objects.in_bulk(location_map.keys())
225 location_querysets = {}
227 for ct_pk, pks in location_map.items():
228 ct = location_cts[ct_pk]
229 location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
231 return location_querysets
233 def get_owner_queryset(self):
234 return User.objects.filter(owned_events__calendars=self.calendar).distinct()
236 # Event QuerySet parsers for a request/args/kwargs
237 def get_all_events(self, request, extra_context=None):
238 return self.get_event_queryset(), extra_context
240 def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
241 context = extra_context or {}
247 return self.get_timespan_queryset(year, month, day), context
249 def get_events_by_owner(self, request, username, extra_context=None):
251 owner = self.get_owner_queryset().get(username=username)
252 except User.DoesNotExist:
255 qs = self.get_event_queryset().filter(owner=owner)
256 context = extra_context or {}
262 def get_events_by_tag(self, request, tag_slugs, extra_context=None):
263 tag_slugs = tag_slugs.replace('+', '/').split('/')
264 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
269 # Raise a 404 on an incorrect slug.
270 found_slugs = [tag.slug for tag in tags]
271 for slug in tag_slugs:
272 if slug and slug not in found_slugs:
275 events = self.get_event_queryset()
277 events = events.filter(tags=tag)
279 context = extra_context or {}
280 context.update({'tags': tags})
282 return events, context
284 def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
286 ct = ContentType.objects.get(app_label=app_label, model=model)
287 location = ct.model_class()._default_manager.get(pk=pk)
288 except ObjectDoesNotExist:
291 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
293 context = extra_context or {}
297 return events, context
300 def event_detail_view(self, request, year, month, day, slug, extra_context=None):
302 event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
303 except Event.DoesNotExist:
306 context = self.get_context()
307 context.update(extra_context or {})
311 return self.event_detail_page.render_to_response(request, extra_context=context)
314 def tag_archive_view(self, request, extra_context=None):
315 tags = self.get_tag_queryset()
316 context = self.get_context()
317 context.update(extra_context or {})
321 return self.tag_archive_page.render_to_response(request, extra_context=context)
323 def location_archive_view(self, request, extra_context=None):
324 # What datastructure should locations be?
325 locations = self.get_location_querysets()
326 context = self.get_context()
327 context.update(extra_context or {})
329 'locations': locations
331 return self.location_archive_page.render_to_response(request, extra_context=context)
333 def owner_archive_view(self, request, extra_context=None):
334 owners = self.get_owner_queryset()
335 context = self.get_context()
336 context.update(extra_context or {})
340 return self.owner_archive_page.render_to_response(request, extra_context=context)
343 def process_page_items(self, request, items):
344 if self.events_per_page:
345 page_num = request.GET.get('page', 1)
346 paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
348 'paginator': paginator,
349 'paginated_page': paginated_page,
350 self.item_context_var: items
354 self.item_context_var: items
356 return items, item_context
358 # Feed information hooks
359 def title(self, obj):
363 # Link is ignored anyway...
366 def feed_guid(self, obj):
367 # Is this correct? Should I have a different id for different subfeeds?
370 def description(self, obj):
371 return obj.description
373 def feed_extra_kwargs(self, obj):
374 return {'filename': "%s.ics" % obj.slug}
376 def item_title(self, item):
379 def item_description(self, item):
380 return item.description
382 def item_link(self, item):
383 return self.reverse(item)
385 def item_guid(self, item):
388 def item_author_name(self, item):
390 return item.owner.get_full_name()
392 def item_author_email(self, item):
393 return getattr(item.owner, 'email', None) or None
395 def item_pubdate(self, item):
398 def item_categories(self, item):
399 return [tag.name for tag in item.tags.all()]
401 def item_extra_kwargs(self, item):
403 'start': item.get_start(),
404 'end': item.get_end(),
405 'last_modified': item.last_modified,
406 # Is forcing unicode enough, or should we look for a "custom method"?
407 'location': force_unicode(item.location),
410 def __unicode__(self):
411 return u"%s for %s" % (self.__class__.__name__, self.calendar)
413 field = CalendarView._meta.get_field('feed_type')
414 field._choices += ((ICALENDAR, 'iCalendar'),)
415 field.default = ICALENDAR