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.contrib.sites.models import Site
7 from django.core.exceptions import ValidationError, ObjectDoesNotExist
8 from django.core.validators import RegexValidator
9 from django.db import models
10 from django.db.models.query import QuerySet
11 from django.http import HttpResponse, Http404
12 from django.utils.encoding import force_unicode
13 from philo.contrib.julian.feedgenerator import ICalendarFeed
14 from philo.contrib.penfield.models import FeedView, FEEDS
15 from philo.exceptions import ViewCanNotProvideSubpath
16 from philo.models import Tag, Entity, Page, TemplateField
17 from philo.utils import ContentTypeRegistryLimiter
18 import datetime, calendar
21 __all__ = ('register_location_model', 'unregister_location_model', 'Location', 'TimedModel', 'Event', 'Calendar', 'CalendarView',)
24 ICALENDAR = ICalendarFeed.mime_type
25 FEEDS[ICALENDAR] = ICalendarFeed
27 DEFAULT_SITE = Site.objects.get_current()
30 _languages = dict(settings.LANGUAGES)
32 _languages[settings.LANGUAGE_CODE]
33 DEFAULT_LANGUAGE = settings.LANGUAGE_CODE
36 lang = settings.LANGUAGE_CODE.split('-')[0]
38 DEFAULT_LANGUAGE = lang
40 DEFAULT_LANGUAGE = None
43 location_content_type_limiter = ContentTypeRegistryLimiter()
46 def register_location_model(model):
47 location_content_type_limiter.register_class(model)
50 def unregister_location_model(model):
51 location_content_type_limiter.unregister_class(model)
54 class Location(Entity):
55 name = models.CharField(max_length=255)
56 slug = models.SlugField(max_length=255, unique=True)
58 def __unicode__(self):
62 register_location_model(Location)
65 class TimedModel(models.Model):
66 start_date = models.DateField(help_text="YYYY-MM-DD")
67 start_time = models.TimeField(blank=True, null=True, help_text="HH:MM:SS - 24 hour clock")
68 end_date = models.DateField()
69 end_time = models.TimeField(blank=True, null=True)
72 return self.start_time is None and self.end_time is None
75 if bool(self.start_time) != bool(self.end_time):
76 raise ValidationError("A %s must have either a start time and an end time or neither.")
78 if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time:
79 raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
82 return self.start_date
91 class EventManager(models.Manager):
92 def get_query_set(self):
93 return EventQuerySet(self.model)
95 class EventQuerySet(QuerySet):
97 return self.filter(start_date__gte=datetime.date.today())
99 return self.filter(start_date__lte=datetime.date.today(), end_date__gte=datetime.date.today())
100 def single_day(self):
101 return self.filter(start_date__exact=models.F('end_date'))
103 return self.exclude(start_date__exact=models.F('end_date'))
105 class Event(Entity, TimedModel):
106 name = models.CharField(max_length=255)
107 slug = models.SlugField(max_length=255, unique_for_date='start_date')
109 location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
110 location_pk = models.TextField(blank=True)
111 location = GenericForeignKey('location_content_type', 'location_pk')
113 description = TemplateField()
115 tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
117 parent_event = models.ForeignKey('self', blank=True, null=True)
119 # TODO: "User module"
120 owner = models.ForeignKey(User, related_name='owned_events')
122 created = models.DateTimeField(auto_now_add=True)
123 last_modified = models.DateTimeField(auto_now=True)
125 site = models.ForeignKey(Site, default=DEFAULT_SITE)
129 return "%s@%s" % (self.created.isoformat(), getattr(self.site, 'domain', 'None'))
131 objects = EventManager()
133 def __unicode__(self):
137 unique_together = ('site', 'created')
140 class Calendar(Entity):
141 name = models.CharField(max_length=100)
142 slug = models.SlugField(max_length=100)
143 description = models.TextField(blank=True)
144 events = models.ManyToManyField(Event, related_name='calendars', blank=True)
146 site = models.ForeignKey(Site, default=DEFAULT_SITE)
147 language = models.CharField(max_length=5, choices=settings.LANGUAGES, default=DEFAULT_LANGUAGE)
149 def __unicode__(self):
154 # See http://xml.coverpages.org/tauber-fpi.html or ISO 9070:1991 for format information.
155 return "-//%s//%s//%s" % (self.site.name, self.name, self.language.split('-')[0].upper())
158 unique_together = ('name', 'site', 'language')
161 class CalendarView(FeedView):
162 calendar = models.ForeignKey(Calendar)
163 index_page = models.ForeignKey(Page, related_name="calendar_index_related")
164 event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
166 timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related", blank=True, null=True)
167 tag_page = models.ForeignKey(Page, related_name="calendar_tag_related", blank=True, null=True)
168 location_page = models.ForeignKey(Page, related_name="calendar_location_related", blank=True, null=True)
169 owner_page = models.ForeignKey(Page, related_name="calendar_owner_related", blank=True, null=True)
171 tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
172 location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
173 owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
175 tag_permalink_base = models.CharField(max_length=30, default='tags')
176 owner_permalink_base = models.CharField(max_length=30, default='owners')
177 location_permalink_base = models.CharField(max_length=30, default='locations')
178 events_per_page = models.PositiveIntegerField(blank=True, null=True)
180 item_context_var = "events"
181 object_attr = "calendar"
183 def get_reverse_params(self, obj):
184 if isinstance(obj, User):
185 return 'events_for_user', [], {'username': obj.username}
186 elif isinstance(obj, Event):
187 return 'event_detail', [], {
188 'year': str(obj.start_date.year).zfill(4),
189 'month': str(obj.start_date.month).zfill(2),
190 'day': str(obj.start_date.day).zfill(2),
193 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
194 if isinstance(obj, Tag):
196 return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
197 raise ViewCanNotProvideSubpath
199 def timespan_patterns(self, pattern, timespan_name):
200 return self.feed_patterns(pattern, 'get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
203 def urlpatterns(self):
204 # Perhaps timespans should be done with GET parameters? Or two /-separated
205 # date slugs? (e.g. 2010-02-1/2010-02-2) or a start and duration?
206 # (e.g. 2010-02-01/week/ or ?d=2010-02-01&l=week)
207 urlpatterns = self.feed_patterns(r'^', 'get_all_events', 'index_page', 'index') + \
208 self.timespan_patterns(r'^(?P<year>\d{4})', 'year') + \
209 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'month') + \
210 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'day') + \
211 self.feed_patterns(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
212 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') + \
213 self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
215 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$', self.event_detail_view, name="event_detail"),
218 # Some sort of shortcut for a location would be useful. This could be on a per-calendar
219 # or per-calendar-view basis.
220 #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
222 if self.tag_archive_page:
223 urlpatterns += patterns('',
224 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
227 if self.owner_archive_page:
228 urlpatterns += patterns('',
229 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
232 if self.location_archive_page:
233 urlpatterns += patterns('',
234 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
238 # Basic QuerySet fetchers.
239 def get_event_queryset(self):
240 return self.calendar.events.all()
242 def get_timespan_queryset(self, year, month=None, day=None):
243 qs = self.get_event_queryset()
244 # See python documentation for the min/max values.
245 if year and month and day:
246 year, month, day = int(year), int(month), int(day)
247 start_datetime = datetime.datetime(year, month, day, 0, 0)
248 end_datetime = datetime.datetime(year, month, day, 23, 59)
250 year, month = int(year), int(month)
251 start_datetime = datetime.datetime(year, month, 1, 0, 0)
252 end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
255 start_datetime = datetime.datetime(year, 1, 1, 0, 0)
256 end_datetime = datetime.datetime(year, 12, 31, 23, 59)
258 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)
260 def get_tag_queryset(self):
261 return Tag.objects.filter(events__calendars=self.calendar).distinct()
263 def get_location_querysets(self):
264 # Potential bottleneck?
266 locations = Event.objects.values_list('location_content_type', 'location_pk')
268 for ct, pk in locations:
269 location_map.setdefault(ct, []).append(pk)
271 location_cts = ContentType.objects.in_bulk(location_map.keys())
272 location_querysets = {}
274 for ct_pk, pks in location_map.items():
275 ct = location_cts[ct_pk]
276 location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
278 return location_querysets
280 def get_owner_queryset(self):
281 return User.objects.filter(owned_events__calendars=self.calendar).distinct()
283 # Event QuerySet parsers for a request/args/kwargs
284 def get_all_events(self, request, extra_context=None):
285 return self.get_event_queryset(), extra_context
287 def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
288 context = extra_context or {}
294 return self.get_timespan_queryset(year, month, day), context
296 def get_events_by_owner(self, request, username, extra_context=None):
298 owner = self.get_owner_queryset().get(username=username)
299 except User.DoesNotExist:
302 qs = self.get_event_queryset().filter(owner=owner)
303 context = extra_context or {}
309 def get_events_by_tag(self, request, tag_slugs, extra_context=None):
310 tag_slugs = tag_slugs.replace('+', '/').split('/')
311 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
316 # Raise a 404 on an incorrect slug.
317 found_slugs = [tag.slug for tag in tags]
318 for slug in tag_slugs:
319 if slug and slug not in found_slugs:
322 events = self.get_event_queryset()
324 events = events.filter(tags=tag)
326 context = extra_context or {}
327 context.update({'tags': tags})
329 return events, context
331 def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
333 ct = ContentType.objects.get(app_label=app_label, model=model)
334 location = ct.model_class()._default_manager.get(pk=pk)
335 except ObjectDoesNotExist:
338 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
340 context = extra_context or {}
344 return events, context
347 def event_detail_view(self, request, year, month, day, slug, extra_context=None):
349 event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
350 except Event.DoesNotExist:
353 context = self.get_context()
354 context.update(extra_context or {})
358 return self.event_detail_page.render_to_response(request, extra_context=context)
361 def tag_archive_view(self, request, extra_context=None):
362 tags = self.get_tag_queryset()
363 context = self.get_context()
364 context.update(extra_context or {})
368 return self.tag_archive_page.render_to_response(request, extra_context=context)
370 def location_archive_view(self, request, extra_context=None):
371 # What datastructure should locations be?
372 locations = self.get_location_querysets()
373 context = self.get_context()
374 context.update(extra_context or {})
376 'locations': locations
378 return self.location_archive_page.render_to_response(request, extra_context=context)
380 def owner_archive_view(self, request, extra_context=None):
381 owners = self.get_owner_queryset()
382 context = self.get_context()
383 context.update(extra_context or {})
387 return self.owner_archive_page.render_to_response(request, extra_context=context)
390 def process_page_items(self, request, items):
391 if self.events_per_page:
392 page_num = request.GET.get('page', 1)
393 paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
395 'paginator': paginator,
396 'paginated_page': paginated_page,
397 self.item_context_var: items
401 self.item_context_var: items
403 return items, item_context
405 # Feed information hooks
406 def title(self, obj):
410 # Link is ignored anyway...
413 def feed_guid(self, obj):
416 def description(self, obj):
417 return obj.description
419 def feed_extra_kwargs(self, obj):
420 return {'filename': "%s.ics" % obj.slug}
422 def item_title(self, item):
425 def item_description(self, item):
426 return item.description
428 def item_link(self, item):
429 return self.reverse(item)
431 def item_guid(self, item):
434 def item_author_name(self, item):
436 return item.owner.get_full_name()
438 def item_author_email(self, item):
439 return getattr(item.owner, 'email', None) or None
441 def item_pubdate(self, item):
444 def item_categories(self, item):
445 return [tag.name for tag in item.tags.all()]
447 def item_extra_kwargs(self, item):
449 'start': item.get_start(),
450 'end': item.get_end(),
451 'last_modified': item.last_modified,
452 # Is forcing unicode enough, or should we look for a "custom method"?
453 'location': force_unicode(item.location),
456 def __unicode__(self):
457 return u"%s for %s" % (self.__class__.__name__, self.calendar)
459 field = CalendarView._meta.get_field('feed_type')
460 field._choices += ((ICALENDAR, 'iCalendar'),)
461 field.default = ICALENDAR