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.db.models.query import QuerySet
10 from django.http import HttpResponse, Http404
11 from django.utils.encoding import force_unicode
12 from philo.contrib.julian.feedgenerator import ICalendarFeed
13 from philo.contrib.penfield.models import FeedView, FEEDS
14 from philo.exceptions import ViewCanNotProvideSubpath
15 from philo.models import Tag, Entity, Page, TemplateField
16 from philo.utils import ContentTypeRegistryLimiter
17 import re, datetime, calendar
20 # TODO: Could this regex more closely match the Formal Public Identifier spec?
21 # http://xml.coverpages.org/tauber-fpi.html
22 FPI_REGEX = re.compile(r"(|\+//|-//)[^/]+//[^/]+//[A-Z]{2}")
25 ICALENDAR = ICalendarFeed.mime_type
26 FEEDS[ICALENDAR] = ICalendarFeed
29 location_content_type_limiter = ContentTypeRegistryLimiter()
32 def register_location_model(model):
33 location_content_type_limiter.register_class(model)
36 def unregister_location_model(model):
37 location_content_type_limiter.unregister_class(model)
40 class Location(Entity):
41 name = models.CharField(max_length=255)
42 slug = models.SlugField(max_length=255, unique=True)
44 def __unicode__(self):
48 register_location_model(Location)
51 class TimedModel(models.Model):
52 start_date = models.DateField(help_text="YYYY-MM-DD")
53 start_time = models.TimeField(blank=True, null=True, help_text="HH:MM:SS - 24 hour clock")
54 end_date = models.DateField()
55 end_time = models.TimeField(blank=True, null=True)
58 return self.start_time is None and self.end_time is None
61 if bool(self.start_time) != bool(self.end_time):
62 raise ValidationError("A %s must have either a start time and an end time or neither.")
64 if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time:
65 raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
68 return self.start_date
77 class EventManager(models.Manager):
78 def get_query_set(self):
79 return EventQuerySet(self.model)
81 class EventQuerySet(QuerySet):
83 return self.filter(start_date__gte=datetime.date.today())
85 return self.filter(start_date__lte=datetime.date.today(), end_date__gte=datetime.date.today())
87 return self.filter(start_date__exact=models.F('end_date'))
89 return self.exclude(start_date__exact=models.F('end_date'))
91 class Event(Entity, TimedModel):
92 name = models.CharField(max_length=255)
93 slug = models.SlugField(max_length=255, unique_for_date='start_date')
95 location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
96 location_pk = models.TextField(blank=True)
97 location = GenericForeignKey('location_content_type', 'location_pk')
99 description = TemplateField()
101 tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
103 parent_event = models.ForeignKey('self', blank=True, null=True)
105 # TODO: "User module"
106 owner = models.ForeignKey(User, related_name='owned_events')
108 created = models.DateTimeField(auto_now_add=True)
109 last_modified = models.DateTimeField(auto_now=True)
110 uuid = models.TextField() # Format?
112 objects = EventManager()
114 def __unicode__(self):
118 class Calendar(Entity):
119 name = models.CharField(max_length=100)
120 slug = models.SlugField(max_length=100)
121 description = models.TextField(blank=True)
122 events = models.ManyToManyField(Event, related_name='calendars')
124 # TODO: Can we auto-generate this on save based on site id and calendar name and settings language?
125 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)])
127 def __unicode__(self):
131 class CalendarView(FeedView):
132 calendar = models.ForeignKey(Calendar)
133 index_page = models.ForeignKey(Page, related_name="calendar_index_related")
134 event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
136 timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related", blank=True, null=True)
137 tag_page = models.ForeignKey(Page, related_name="calendar_tag_related", blank=True, null=True)
138 location_page = models.ForeignKey(Page, related_name="calendar_location_related", blank=True, null=True)
139 owner_page = models.ForeignKey(Page, related_name="calendar_owner_related", blank=True, null=True)
141 tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
142 location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
143 owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
145 tag_permalink_base = models.CharField(max_length=30, default='tags')
146 owner_permalink_base = models.CharField(max_length=30, default='owners')
147 location_permalink_base = models.CharField(max_length=30, default='locations')
148 events_per_page = models.PositiveIntegerField(blank=True, null=True)
150 item_context_var = "events"
151 object_attr = "calendar"
153 def get_reverse_params(self, obj):
154 if isinstance(obj, User):
155 return 'events_for_user', [], {'username': obj.username}
156 elif isinstance(obj, Event):
157 return 'event_detail', [], {
158 'year': str(obj.start_date.year).zfill(4),
159 'month': str(obj.start_date.month).zfill(2),
160 'day': str(obj.start_date.day).zfill(2),
163 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
164 if isinstance(obj, Tag):
166 return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
167 raise ViewCanNotProvideSubpath
169 def timespan_patterns(self, pattern, timespan_name):
170 return self.feed_patterns(pattern, 'get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
173 def urlpatterns(self):
174 # Perhaps timespans should be done with GET parameters? Or two /-separated
175 # date slugs? (e.g. 2010-02-1/2010-02-2) or a start and duration?
176 # (e.g. 2010-02-01/week/ or ?d=2010-02-01&l=week)
177 urlpatterns = self.feed_patterns(r'^', 'get_all_events', 'index_page', 'index') + \
178 self.timespan_patterns(r'^(?P<year>\d{4})', 'year') + \
179 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'month') + \
180 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'day') + \
181 self.feed_patterns(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
182 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') + \
183 self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
185 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$', self.event_detail_view, name="event_detail"),
188 # Some sort of shortcut for a location would be useful. This could be on a per-calendar
189 # or per-calendar-view basis.
190 #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
192 if self.tag_archive_page:
193 urlpatterns += patterns('',
194 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
197 if self.owner_archive_page:
198 urlpatterns += patterns('',
199 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
202 if self.location_archive_page:
203 urlpatterns += patterns('',
204 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
208 # Basic QuerySet fetchers.
209 def get_event_queryset(self):
210 return self.calendar.events.all()
212 def get_timespan_queryset(self, year, month=None, day=None):
213 qs = self.get_event_queryset()
214 # See python documentation for the min/max values.
215 if year and month and day:
216 year, month, day = int(year), int(month), int(day)
217 start_datetime = datetime.datetime(year, month, day, 0, 0)
218 end_datetime = datetime.datetime(year, month, day, 23, 59)
220 year, month = int(year), int(month)
221 start_datetime = datetime.datetime(year, month, 1, 0, 0)
222 end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
225 start_datetime = datetime.datetime(year, 1, 1, 0, 0)
226 end_datetime = datetime.datetime(year, 12, 31, 23, 59)
228 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)
230 def get_tag_queryset(self):
231 return Tag.objects.filter(events__calendars=self.calendar).distinct()
233 def get_location_querysets(self):
234 # Potential bottleneck?
236 locations = Event.objects.values_list('location_content_type', 'location_pk')
238 for ct, pk in locations:
239 location_map.setdefault(ct, []).append(pk)
241 location_cts = ContentType.objects.in_bulk(location_map.keys())
242 location_querysets = {}
244 for ct_pk, pks in location_map.items():
245 ct = location_cts[ct_pk]
246 location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
248 return location_querysets
250 def get_owner_queryset(self):
251 return User.objects.filter(owned_events__calendars=self.calendar).distinct()
253 # Event QuerySet parsers for a request/args/kwargs
254 def get_all_events(self, request, extra_context=None):
255 return self.get_event_queryset(), extra_context
257 def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
258 context = extra_context or {}
264 return self.get_timespan_queryset(year, month, day), context
266 def get_events_by_owner(self, request, username, extra_context=None):
268 owner = self.get_owner_queryset().get(username=username)
269 except User.DoesNotExist:
272 qs = self.get_event_queryset().filter(owner=owner)
273 context = extra_context or {}
279 def get_events_by_tag(self, request, tag_slugs, extra_context=None):
280 tag_slugs = tag_slugs.replace('+', '/').split('/')
281 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
286 # Raise a 404 on an incorrect slug.
287 found_slugs = [tag.slug for tag in tags]
288 for slug in tag_slugs:
289 if slug and slug not in found_slugs:
292 events = self.get_event_queryset()
294 events = events.filter(tags=tag)
296 context = extra_context or {}
297 context.update({'tags': tags})
299 return events, context
301 def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
303 ct = ContentType.objects.get(app_label=app_label, model=model)
304 location = ct.model_class()._default_manager.get(pk=pk)
305 except ObjectDoesNotExist:
308 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
310 context = extra_context or {}
314 return events, context
317 def event_detail_view(self, request, year, month, day, slug, extra_context=None):
319 event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
320 except Event.DoesNotExist:
323 context = self.get_context()
324 context.update(extra_context or {})
328 return self.event_detail_page.render_to_response(request, extra_context=context)
331 def tag_archive_view(self, request, extra_context=None):
332 tags = self.get_tag_queryset()
333 context = self.get_context()
334 context.update(extra_context or {})
338 return self.tag_archive_page.render_to_response(request, extra_context=context)
340 def location_archive_view(self, request, extra_context=None):
341 # What datastructure should locations be?
342 locations = self.get_location_querysets()
343 context = self.get_context()
344 context.update(extra_context or {})
346 'locations': locations
348 return self.location_archive_page.render_to_response(request, extra_context=context)
350 def owner_archive_view(self, request, extra_context=None):
351 owners = self.get_owner_queryset()
352 context = self.get_context()
353 context.update(extra_context or {})
357 return self.owner_archive_page.render_to_response(request, extra_context=context)
360 def process_page_items(self, request, items):
361 if self.events_per_page:
362 page_num = request.GET.get('page', 1)
363 paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
365 'paginator': paginator,
366 'paginated_page': paginated_page,
367 self.item_context_var: items
371 self.item_context_var: items
373 return items, item_context
375 # Feed information hooks
376 def title(self, obj):
380 # Link is ignored anyway...
383 def feed_guid(self, obj):
384 # Is this correct? Should I have a different id for different subfeeds?
387 def description(self, obj):
388 return obj.description
390 def feed_extra_kwargs(self, obj):
391 return {'filename': "%s.ics" % obj.slug}
393 def item_title(self, item):
396 def item_description(self, item):
397 return item.description
399 def item_link(self, item):
400 return self.reverse(item)
402 def item_guid(self, item):
405 def item_author_name(self, item):
407 return item.owner.get_full_name()
409 def item_author_email(self, item):
410 return getattr(item.owner, 'email', None) or None
412 def item_pubdate(self, item):
415 def item_categories(self, item):
416 return [tag.name for tag in item.tags.all()]
418 def item_extra_kwargs(self, item):
420 'start': item.get_start(),
421 'end': item.get_end(),
422 'last_modified': item.last_modified,
423 # Is forcing unicode enough, or should we look for a "custom method"?
424 'location': force_unicode(item.location),
427 def __unicode__(self):
428 return u"%s for %s" % (self.__class__.__name__, self.calendar)
430 field = CalendarView._meta.get_field('feed_type')
431 field._choices += ((ICALENDAR, 'iCalendar'),)
432 field.default = ICALENDAR