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 self.model.QuerySet(self.model)
81 class Event(Entity, TimedModel):
82 name = models.CharField(max_length=255)
83 slug = models.SlugField(max_length=255, unique_for_date='start_date')
85 location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
86 location_pk = models.TextField(blank=True)
87 location = GenericForeignKey('location_content_type', 'location_pk')
89 description = TemplateField()
91 tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
93 parent_event = models.ForeignKey('self', blank=True, null=True)
96 owner = models.ForeignKey(User, related_name='owned_events')
98 created = models.DateTimeField(auto_now_add=True)
99 last_modified = models.DateTimeField(auto_now=True)
100 uuid = models.TextField() # Format?
102 objects = EventManager()
104 class QuerySet(QuerySet):
107 return self.filter(start_date__gte=datetime.date.today())
110 return self.filter(start_date__lte=datetime.date.today(), end_date__gte=datetime.date.today())
112 def single_day(self):
113 return self.filter(start_date__exact=models.F('end_date'))
116 return self.exclude(start_date__exact=models.F('end_date'))
118 def __unicode__(self):
122 class Calendar(Entity):
123 name = models.CharField(max_length=100)
124 slug = models.SlugField(max_length=100)
125 description = models.TextField(blank=True)
126 events = models.ManyToManyField(Event, related_name='calendars')
128 # TODO: Can we auto-generate this on save based on site id and calendar name and settings language?
129 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)])
131 def __unicode__(self):
135 class CalendarView(FeedView):
136 calendar = models.ForeignKey(Calendar)
137 index_page = models.ForeignKey(Page, related_name="calendar_index_related")
138 event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
140 timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related", blank=True, null=True)
141 tag_page = models.ForeignKey(Page, related_name="calendar_tag_related", blank=True, null=True)
142 location_page = models.ForeignKey(Page, related_name="calendar_location_related", blank=True, null=True)
143 owner_page = models.ForeignKey(Page, related_name="calendar_owner_related", blank=True, null=True)
145 tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
146 location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
147 owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
149 tag_permalink_base = models.CharField(max_length=30, default='tags')
150 owner_permalink_base = models.CharField(max_length=30, default='owners')
151 location_permalink_base = models.CharField(max_length=30, default='locations')
152 events_per_page = models.PositiveIntegerField(blank=True, null=True)
154 item_context_var = "events"
155 object_attr = "calendar"
157 def get_reverse_params(self, obj):
158 if isinstance(obj, User):
159 return 'events_for_user', [], {'username': obj.username}
160 elif isinstance(obj, Event):
161 return 'event_detail', [], {
162 'year': str(obj.start_date.year).zfill(4),
163 'month': str(obj.start_date.month).zfill(2),
164 'day': str(obj.start_date.day).zfill(2),
167 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
168 if isinstance(obj, Tag):
170 return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
171 raise ViewCanNotProvideSubpath
173 def timespan_patterns(self, pattern, timespan_name):
174 return self.feed_patterns(pattern, 'get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
177 def urlpatterns(self):
178 # Perhaps timespans should be done with GET parameters? Or two /-separated
179 # date slugs? (e.g. 2010-02-1/2010-02-2) or a start and duration?
180 # (e.g. 2010-02-01/week/ or ?d=2010-02-01&l=week)
181 urlpatterns = self.feed_patterns(r'^', 'get_all_events', 'index_page', 'index') + \
182 self.timespan_patterns(r'^(?P<year>\d{4})', 'year') + \
183 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'month') + \
184 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'day') + \
185 self.feed_patterns(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
186 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') + \
187 self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
189 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$', self.event_detail_view, name="event_detail"),
192 # Some sort of shortcut for a location would be useful. This could be on a per-calendar
193 # or per-calendar-view basis.
194 #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
196 if self.tag_archive_page:
197 urlpatterns += patterns('',
198 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
201 if self.owner_archive_page:
202 urlpatterns += patterns('',
203 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
206 if self.location_archive_page:
207 urlpatterns += patterns('',
208 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
212 # Basic QuerySet fetchers.
213 def get_event_queryset(self):
214 return self.calendar.events.all()
216 def get_timespan_queryset(self, year, month=None, day=None):
217 qs = self.get_event_queryset()
218 # See python documentation for the min/max values.
219 if year and month and day:
220 year, month, day = int(year), int(month), int(day)
221 start_datetime = datetime.datetime(year, month, day, 0, 0)
222 end_datetime = datetime.datetime(year, month, day, 23, 59)
224 year, month = int(year), int(month)
225 start_datetime = datetime.datetime(year, month, 1, 0, 0)
226 end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
229 start_datetime = datetime.datetime(year, 1, 1, 0, 0)
230 end_datetime = datetime.datetime(year, 12, 31, 23, 59)
232 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)
234 def get_tag_queryset(self):
235 return Tag.objects.filter(events__calendars=self.calendar).distinct()
237 def get_location_querysets(self):
238 # Potential bottleneck?
240 locations = Event.objects.values_list('location_content_type', 'location_pk')
242 for ct, pk in locations:
243 location_map.setdefault(ct, []).append(pk)
245 location_cts = ContentType.objects.in_bulk(location_map.keys())
246 location_querysets = {}
248 for ct_pk, pks in location_map.items():
249 ct = location_cts[ct_pk]
250 location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
252 return location_querysets
254 def get_owner_queryset(self):
255 return User.objects.filter(owned_events__calendars=self.calendar).distinct()
257 # Event QuerySet parsers for a request/args/kwargs
258 def get_all_events(self, request, extra_context=None):
259 return self.get_event_queryset(), extra_context
261 def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
262 context = extra_context or {}
268 return self.get_timespan_queryset(year, month, day), context
270 def get_events_by_owner(self, request, username, extra_context=None):
272 owner = self.get_owner_queryset().get(username=username)
273 except User.DoesNotExist:
276 qs = self.get_event_queryset().filter(owner=owner)
277 context = extra_context or {}
283 def get_events_by_tag(self, request, tag_slugs, extra_context=None):
284 tag_slugs = tag_slugs.replace('+', '/').split('/')
285 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
290 # Raise a 404 on an incorrect slug.
291 found_slugs = [tag.slug for tag in tags]
292 for slug in tag_slugs:
293 if slug and slug not in found_slugs:
296 events = self.get_event_queryset()
298 events = events.filter(tags=tag)
300 context = extra_context or {}
301 context.update({'tags': tags})
303 return events, context
305 def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
307 ct = ContentType.objects.get(app_label=app_label, model=model)
308 location = ct.model_class()._default_manager.get(pk=pk)
309 except ObjectDoesNotExist:
312 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
314 context = extra_context or {}
318 return events, context
321 def event_detail_view(self, request, year, month, day, slug, extra_context=None):
323 event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
324 except Event.DoesNotExist:
327 context = self.get_context()
328 context.update(extra_context or {})
332 return self.event_detail_page.render_to_response(request, extra_context=context)
335 def tag_archive_view(self, request, extra_context=None):
336 tags = self.get_tag_queryset()
337 context = self.get_context()
338 context.update(extra_context or {})
342 return self.tag_archive_page.render_to_response(request, extra_context=context)
344 def location_archive_view(self, request, extra_context=None):
345 # What datastructure should locations be?
346 locations = self.get_location_querysets()
347 context = self.get_context()
348 context.update(extra_context or {})
350 'locations': locations
352 return self.location_archive_page.render_to_response(request, extra_context=context)
354 def owner_archive_view(self, request, extra_context=None):
355 owners = self.get_owner_queryset()
356 context = self.get_context()
357 context.update(extra_context or {})
361 return self.owner_archive_page.render_to_response(request, extra_context=context)
364 def process_page_items(self, request, items):
365 if self.events_per_page:
366 page_num = request.GET.get('page', 1)
367 paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
369 'paginator': paginator,
370 'paginated_page': paginated_page,
371 self.item_context_var: items
375 self.item_context_var: items
377 return items, item_context
379 # Feed information hooks
380 def title(self, obj):
384 # Link is ignored anyway...
387 def feed_guid(self, obj):
388 # Is this correct? Should I have a different id for different subfeeds?
391 def description(self, obj):
392 return obj.description
394 def feed_extra_kwargs(self, obj):
395 return {'filename': "%s.ics" % obj.slug}
397 def item_title(self, item):
400 def item_description(self, item):
401 return item.description
403 def item_link(self, item):
404 return self.reverse(item)
406 def item_guid(self, item):
409 def item_author_name(self, item):
411 return item.owner.get_full_name()
413 def item_author_email(self, item):
414 return getattr(item.owner, 'email', None) or None
416 def item_pubdate(self, item):
419 def item_categories(self, item):
420 return [tag.name for tag in item.tags.all()]
422 def item_extra_kwargs(self, item):
424 'start': item.get_start(),
425 'end': item.get_end(),
426 'last_modified': item.last_modified,
427 # Is forcing unicode enough, or should we look for a "custom method"?
428 'location': force_unicode(item.location),
431 def __unicode__(self):
432 return u"%s for %s" % (self.__class__.__name__, self.calendar)
434 field = CalendarView._meta.get_field('feed_type')
435 field._choices += ((ICALENDAR, 'iCalendar'),)
436 field.default = ICALENDAR