4 from django.conf import settings
5 from django.conf.urls.defaults import url, patterns, include
6 from django.contrib.auth.models import User
7 from django.contrib.contenttypes.generic import GenericForeignKey
8 from django.contrib.contenttypes.models import ContentType
9 from django.contrib.sites.models import Site
10 from django.core.exceptions import ValidationError, ObjectDoesNotExist
11 from django.core.validators import RegexValidator
12 from django.db import models
13 from django.db.models.query import QuerySet
14 from django.http import HttpResponse, Http404
15 from django.utils.encoding import force_unicode
17 from philo.contrib.julian.feedgenerator import ICalendarFeed
18 from philo.contrib.penfield.models import FeedView, FEEDS
19 from philo.exceptions import ViewCanNotProvideSubpath
20 from philo.models import Tag, Entity, Page, TemplateField
21 from philo.utils import ContentTypeRegistryLimiter
24 __all__ = ('register_location_model', 'unregister_location_model', 'Location', 'TimedModel', 'Event', 'Calendar', 'CalendarView',)
27 ICALENDAR = ICalendarFeed.mime_type
28 FEEDS[ICALENDAR] = ICalendarFeed
30 DEFAULT_SITE = Site.objects.get_current()
33 _languages = dict(settings.LANGUAGES)
35 _languages[settings.LANGUAGE_CODE]
36 DEFAULT_LANGUAGE = settings.LANGUAGE_CODE
39 lang = settings.LANGUAGE_CODE.split('-')[0]
41 DEFAULT_LANGUAGE = lang
43 DEFAULT_LANGUAGE = None
46 location_content_type_limiter = ContentTypeRegistryLimiter()
49 def register_location_model(model):
50 location_content_type_limiter.register_class(model)
53 def unregister_location_model(model):
54 location_content_type_limiter.unregister_class(model)
57 class Location(Entity):
58 name = models.CharField(max_length=255)
59 slug = models.SlugField(max_length=255, unique=True)
61 def __unicode__(self):
65 register_location_model(Location)
68 class TimedModel(models.Model):
69 start_date = models.DateField(help_text="YYYY-MM-DD")
70 start_time = models.TimeField(blank=True, null=True, help_text="HH:MM:SS - 24 hour clock")
71 end_date = models.DateField()
72 end_time = models.TimeField(blank=True, null=True)
75 return self.start_time is None and self.end_time is None
78 if bool(self.start_time) != bool(self.end_time):
79 raise ValidationError("A %s must have either a start time and an end time or neither.")
81 if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time:
82 raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
85 return datetime.datetime.combine(self.start_date, self.start_time) if self.start_time else self.start_date
88 return datetime.datetime.combine(self.end_date, self.end_time) if self.end_time else self.end_date
94 class EventManager(models.Manager):
95 def get_query_set(self):
96 return EventQuerySet(self.model)
98 class EventQuerySet(QuerySet):
100 return self.filter(start_date__gte=datetime.date.today())
102 return self.filter(start_date__lte=datetime.date.today(), end_date__gte=datetime.date.today())
103 def single_day(self):
104 return self.filter(start_date__exact=models.F('end_date'))
106 return self.exclude(start_date__exact=models.F('end_date'))
108 class Event(Entity, TimedModel):
109 name = models.CharField(max_length=255)
110 slug = models.SlugField(max_length=255, unique_for_date='start_date')
112 location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
113 location_pk = models.TextField(blank=True)
114 location = GenericForeignKey('location_content_type', 'location_pk')
116 description = TemplateField()
118 tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
120 parent_event = models.ForeignKey('self', blank=True, null=True)
122 # TODO: "User module"
123 owner = models.ForeignKey(User, related_name='owned_events')
125 created = models.DateTimeField(auto_now_add=True)
126 last_modified = models.DateTimeField(auto_now=True)
128 site = models.ForeignKey(Site, default=DEFAULT_SITE)
132 return "%s@%s" % (self.created.isoformat(), getattr(self.site, 'domain', 'None'))
134 objects = EventManager()
136 def __unicode__(self):
140 unique_together = ('site', 'created')
143 class Calendar(Entity):
144 name = models.CharField(max_length=100)
145 slug = models.SlugField(max_length=100)
146 description = models.TextField(blank=True)
147 events = models.ManyToManyField(Event, related_name='calendars', blank=True)
149 site = models.ForeignKey(Site, default=DEFAULT_SITE)
150 language = models.CharField(max_length=5, choices=settings.LANGUAGES, default=DEFAULT_LANGUAGE)
152 def __unicode__(self):
157 # See http://xml.coverpages.org/tauber-fpi.html or ISO 9070:1991 for format information.
158 return "-//%s//%s//%s" % (self.site.name, self.name, self.language.split('-')[0].upper())
161 unique_together = ('name', 'site', 'language')
164 class CalendarView(FeedView):
165 calendar = models.ForeignKey(Calendar)
166 index_page = models.ForeignKey(Page, related_name="calendar_index_related")
167 event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
169 timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related", blank=True, null=True)
170 tag_page = models.ForeignKey(Page, related_name="calendar_tag_related", blank=True, null=True)
171 location_page = models.ForeignKey(Page, related_name="calendar_location_related", blank=True, null=True)
172 owner_page = models.ForeignKey(Page, related_name="calendar_owner_related", blank=True, null=True)
174 tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
175 location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
176 owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
178 tag_permalink_base = models.CharField(max_length=30, default='tags')
179 owner_permalink_base = models.CharField(max_length=30, default='owners')
180 location_permalink_base = models.CharField(max_length=30, default='locations')
181 events_per_page = models.PositiveIntegerField(blank=True, null=True)
183 item_context_var = "events"
184 object_attr = "calendar"
186 def get_reverse_params(self, obj):
187 if isinstance(obj, User):
188 return 'events_for_user', [], {'username': obj.username}
189 elif isinstance(obj, Event):
190 return 'event_detail', [], {
191 'year': str(obj.start_date.year).zfill(4),
192 'month': str(obj.start_date.month).zfill(2),
193 'day': str(obj.start_date.day).zfill(2),
196 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
197 if isinstance(obj, Tag):
199 return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
200 raise ViewCanNotProvideSubpath
202 def timespan_patterns(self, pattern, timespan_name):
203 return self.feed_patterns(pattern, 'get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
206 def urlpatterns(self):
207 # Perhaps timespans should be done with GET parameters? Or two /-separated
208 # date slugs? (e.g. 2010-02-1/2010-02-2) or a start and duration?
209 # (e.g. 2010-02-01/week/ or ?d=2010-02-01&l=week)
210 urlpatterns = self.feed_patterns(r'^', 'get_all_events', 'index_page', 'index') + \
211 self.timespan_patterns(r'^(?P<year>\d{4})', 'year') + \
212 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'month') + \
213 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'day') + \
214 self.feed_patterns(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
215 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') + \
216 self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
218 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$', self.event_detail_view, name="event_detail"),
221 # Some sort of shortcut for a location would be useful. This could be on a per-calendar
222 # or per-calendar-view basis.
223 #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
225 if self.tag_archive_page:
226 urlpatterns += patterns('',
227 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
230 if self.owner_archive_page:
231 urlpatterns += patterns('',
232 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
235 if self.location_archive_page:
236 urlpatterns += patterns('',
237 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
241 # Basic QuerySet fetchers.
242 def get_event_queryset(self):
243 return self.calendar.events.all()
245 def get_timespan_queryset(self, year, month=None, day=None):
246 qs = self.get_event_queryset()
247 # See python documentation for the min/max values.
248 if year and month and day:
249 year, month, day = int(year), int(month), int(day)
250 start_datetime = datetime.datetime(year, month, day, 0, 0)
251 end_datetime = datetime.datetime(year, month, day, 23, 59)
253 year, month = int(year), int(month)
254 start_datetime = datetime.datetime(year, month, 1, 0, 0)
255 end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
258 start_datetime = datetime.datetime(year, 1, 1, 0, 0)
259 end_datetime = datetime.datetime(year, 12, 31, 23, 59)
261 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)
263 def get_tag_queryset(self):
264 return Tag.objects.filter(events__calendars=self.calendar).distinct()
266 def get_location_querysets(self):
267 # Potential bottleneck?
269 locations = Event.objects.values_list('location_content_type', 'location_pk')
271 for ct, pk in locations:
272 location_map.setdefault(ct, []).append(pk)
274 location_cts = ContentType.objects.in_bulk(location_map.keys())
275 location_querysets = {}
277 for ct_pk, pks in location_map.items():
278 ct = location_cts[ct_pk]
279 location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
281 return location_querysets
283 def get_owner_queryset(self):
284 return User.objects.filter(owned_events__calendars=self.calendar).distinct()
286 # Event QuerySet parsers for a request/args/kwargs
287 def get_all_events(self, request, extra_context=None):
288 return self.get_event_queryset(), extra_context
290 def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
291 context = extra_context or {}
297 return self.get_timespan_queryset(year, month, day), context
299 def get_events_by_owner(self, request, username, extra_context=None):
301 owner = self.get_owner_queryset().get(username=username)
302 except User.DoesNotExist:
305 qs = self.get_event_queryset().filter(owner=owner)
306 context = extra_context or {}
312 def get_events_by_tag(self, request, tag_slugs, extra_context=None):
313 tag_slugs = tag_slugs.replace('+', '/').split('/')
314 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
319 # Raise a 404 on an incorrect slug.
320 found_slugs = [tag.slug for tag in tags]
321 for slug in tag_slugs:
322 if slug and slug not in found_slugs:
325 events = self.get_event_queryset()
327 events = events.filter(tags=tag)
329 context = extra_context or {}
330 context.update({'tags': tags})
332 return events, context
334 def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
336 ct = ContentType.objects.get(app_label=app_label, model=model)
337 location = ct.model_class()._default_manager.get(pk=pk)
338 except ObjectDoesNotExist:
341 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
343 context = extra_context or {}
347 return events, context
350 def event_detail_view(self, request, year, month, day, slug, extra_context=None):
352 event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
353 except Event.DoesNotExist:
356 context = self.get_context()
357 context.update(extra_context or {})
361 return self.event_detail_page.render_to_response(request, extra_context=context)
364 def tag_archive_view(self, request, extra_context=None):
365 tags = self.get_tag_queryset()
366 context = self.get_context()
367 context.update(extra_context or {})
371 return self.tag_archive_page.render_to_response(request, extra_context=context)
373 def location_archive_view(self, request, extra_context=None):
374 # What datastructure should locations be?
375 locations = self.get_location_querysets()
376 context = self.get_context()
377 context.update(extra_context or {})
379 'locations': locations
381 return self.location_archive_page.render_to_response(request, extra_context=context)
383 def owner_archive_view(self, request, extra_context=None):
384 owners = self.get_owner_queryset()
385 context = self.get_context()
386 context.update(extra_context or {})
390 return self.owner_archive_page.render_to_response(request, extra_context=context)
393 def process_page_items(self, request, items):
394 if self.events_per_page:
395 page_num = request.GET.get('page', 1)
396 paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
398 'paginator': paginator,
399 'paginated_page': paginated_page,
400 self.item_context_var: items
404 self.item_context_var: items
406 return items, item_context
408 # Feed information hooks
409 def title(self, obj):
413 # Link is ignored anyway...
416 def feed_guid(self, obj):
419 def description(self, obj):
420 return obj.description
422 def feed_extra_kwargs(self, obj):
423 return {'filename': "%s.ics" % obj.slug}
425 def item_title(self, item):
428 def item_description(self, item):
429 return item.description
431 def item_link(self, item):
432 return self.reverse(item)
434 def item_guid(self, item):
437 def item_author_name(self, item):
439 return item.owner.get_full_name()
441 def item_author_email(self, item):
442 return getattr(item.owner, 'email', None) or None
444 def item_pubdate(self, item):
447 def item_categories(self, item):
448 return [tag.name for tag in item.tags.all()]
450 def item_extra_kwargs(self, item):
452 'start': item.get_start(),
453 'end': item.get_end(),
454 'last_modified': item.last_modified,
455 # Is forcing unicode enough, or should we look for a "custom method"?
456 'location': force_unicode(item.location),
459 def __unicode__(self):
460 return u"%s for %s" % (self.__class__.__name__, self.calendar)
462 field = CalendarView._meta.get_field('feed_type')
463 field._choices += ((ICALENDAR, 'iCalendar'),)
464 field.default = ICALENDAR