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
16 from taggit.managers import TaggableManager
18 from philo.contrib.julian.feedgenerator import ICalendarFeed
19 from philo.contrib.winer.models import FeedView
20 from philo.contrib.winer.feeds import registry
21 from philo.exceptions import ViewCanNotProvideSubpath
22 from philo.models import Tag, Entity, Page
23 from philo.models.fields import TemplateField
24 from philo.utils import ContentTypeRegistryLimiter
27 __all__ = ('register_location_model', 'unregister_location_model', 'Location', 'TimedModel', 'Event', 'Calendar', 'CalendarView',)
30 registry.register(ICalendarFeed, verbose_name="iCalendar")
32 DEFAULT_SITE = Site.objects.get_current()
35 _languages = dict(settings.LANGUAGES)
37 _languages[settings.LANGUAGE_CODE]
38 DEFAULT_LANGUAGE = settings.LANGUAGE_CODE
41 lang = settings.LANGUAGE_CODE.split('-')[0]
43 DEFAULT_LANGUAGE = lang
45 DEFAULT_LANGUAGE = None
48 location_content_type_limiter = ContentTypeRegistryLimiter()
51 def register_location_model(model):
52 location_content_type_limiter.register_class(model)
55 def unregister_location_model(model):
56 location_content_type_limiter.unregister_class(model)
59 class Location(Entity):
60 name = models.CharField(max_length=255)
61 slug = models.SlugField(max_length=255, unique=True)
63 def __unicode__(self):
67 register_location_model(Location)
70 class TimedModel(models.Model):
71 start_date = models.DateField(help_text="YYYY-MM-DD")
72 start_time = models.TimeField(blank=True, null=True, help_text="HH:MM:SS - 24 hour clock")
73 end_date = models.DateField()
74 end_time = models.TimeField(blank=True, null=True)
77 return self.start_time is None and self.end_time is None
80 if bool(self.start_time) != bool(self.end_time):
81 raise ValidationError("A %s must have either a start time and an end time or neither.")
83 if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time:
84 raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
87 return datetime.datetime.combine(self.start_date, self.start_time) if self.start_time else self.start_date
90 return datetime.datetime.combine(self.end_date, self.end_time) if self.end_time else self.end_date
96 class EventManager(models.Manager):
97 def get_query_set(self):
98 return EventQuerySet(self.model)
100 class EventQuerySet(QuerySet):
102 return self.filter(start_date__gte=datetime.date.today())
104 return self.filter(start_date__lte=datetime.date.today(), end_date__gte=datetime.date.today())
105 def single_day(self):
106 return self.filter(start_date__exact=models.F('end_date'))
108 return self.exclude(start_date__exact=models.F('end_date'))
110 class Event(Entity, TimedModel):
111 name = models.CharField(max_length=255)
112 slug = models.SlugField(max_length=255, unique_for_date='start_date')
114 location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
115 location_pk = models.TextField(blank=True)
116 location = GenericForeignKey('location_content_type', 'location_pk')
118 description = TemplateField()
120 tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
122 parent_event = models.ForeignKey('self', blank=True, null=True)
124 # TODO: "User module"
125 owner = models.ForeignKey(User, related_name='owned_events')
127 created = models.DateTimeField(auto_now_add=True)
128 last_modified = models.DateTimeField(auto_now=True)
130 site = models.ForeignKey(Site, default=DEFAULT_SITE)
134 return "%s@%s" % (self.created.isoformat(), getattr(self.site, 'domain', 'None'))
136 objects = EventManager()
138 def __unicode__(self):
142 unique_together = ('site', 'created')
145 class Calendar(Entity):
146 name = models.CharField(max_length=100)
147 slug = models.SlugField(max_length=100)
148 description = models.TextField(blank=True)
149 events = models.ManyToManyField(Event, related_name='calendars', blank=True)
151 site = models.ForeignKey(Site, default=DEFAULT_SITE)
152 language = models.CharField(max_length=5, choices=settings.LANGUAGES, default=DEFAULT_LANGUAGE)
154 def __unicode__(self):
159 # See http://xml.coverpages.org/tauber-fpi.html or ISO 9070:1991 for format information.
160 return "-//%s//%s//%s" % (self.site.name, self.name, self.language.split('-')[0].upper())
163 unique_together = ('name', 'site', 'language')
166 class CalendarView(FeedView):
167 calendar = models.ForeignKey(Calendar)
168 index_page = models.ForeignKey(Page, related_name="calendar_index_related")
169 event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
171 timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related", blank=True, null=True)
172 tag_page = models.ForeignKey(Page, related_name="calendar_tag_related", blank=True, null=True)
173 location_page = models.ForeignKey(Page, related_name="calendar_location_related", blank=True, null=True)
174 owner_page = models.ForeignKey(Page, related_name="calendar_owner_related", blank=True, null=True)
176 tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
177 location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
178 owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
180 tag_permalink_base = models.CharField(max_length=30, default='tags')
181 owner_permalink_base = models.CharField(max_length=30, default='owners')
182 location_permalink_base = models.CharField(max_length=30, default='locations')
183 events_per_page = models.PositiveIntegerField(blank=True, null=True)
185 item_context_var = "events"
186 object_attr = "calendar"
188 def get_reverse_params(self, obj):
189 if isinstance(obj, User):
190 return 'events_for_user', [], {'username': obj.username}
191 elif isinstance(obj, Event):
192 return 'event_detail', [], {
193 'year': str(obj.start_date.year).zfill(4),
194 'month': str(obj.start_date.month).zfill(2),
195 'day': str(obj.start_date.day).zfill(2),
198 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
199 if isinstance(obj, Tag):
201 return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
202 raise ViewCanNotProvideSubpath
204 def timespan_patterns(self, pattern, timespan_name):
205 return self.feed_patterns(pattern, 'get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
208 def urlpatterns(self):
209 # Perhaps timespans should be done with GET parameters? Or two /-separated
210 # date slugs? (e.g. 2010-02-1/2010-02-2) or a start and duration?
211 # (e.g. 2010-02-01/week/ or ?d=2010-02-01&l=week)
212 urlpatterns = self.feed_patterns(r'^', 'get_all_events', 'index_page', 'index') + \
213 self.timespan_patterns(r'^(?P<year>\d{4})', 'year') + \
214 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'month') + \
215 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'day') + \
216 self.feed_patterns(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
217 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') + \
218 self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
220 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$', self.event_detail_view, name="event_detail"),
223 # Some sort of shortcut for a location would be useful. This could be on a per-calendar
224 # or per-calendar-view basis.
225 #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
227 if self.tag_archive_page_id:
228 urlpatterns += patterns('',
229 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
232 if self.owner_archive_page_id:
233 urlpatterns += patterns('',
234 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
237 if self.location_archive_page_id:
238 urlpatterns += patterns('',
239 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
243 # Basic QuerySet fetchers.
244 def get_event_queryset(self):
245 return self.calendar.events.all()
247 def get_timespan_queryset(self, year, month=None, day=None):
248 qs = self.get_event_queryset()
249 # See python documentation for the min/max values.
250 if year and month and day:
251 year, month, day = int(year), int(month), int(day)
252 start_datetime = datetime.datetime(year, month, day, 0, 0)
253 end_datetime = datetime.datetime(year, month, day, 23, 59)
255 year, month = int(year), int(month)
256 start_datetime = datetime.datetime(year, month, 1, 0, 0)
257 end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
260 start_datetime = datetime.datetime(year, 1, 1, 0, 0)
261 end_datetime = datetime.datetime(year, 12, 31, 23, 59)
263 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)
265 def get_tag_queryset(self):
266 return Tag.objects.filter(events__calendars=self.calendar).distinct()
268 def get_location_querysets(self):
269 # Potential bottleneck?
271 locations = Event.objects.values_list('location_content_type', 'location_pk')
273 for ct, pk in locations:
274 location_map.setdefault(ct, []).append(pk)
276 location_cts = ContentType.objects.in_bulk(location_map.keys())
277 location_querysets = {}
279 for ct_pk, pks in location_map.items():
280 ct = location_cts[ct_pk]
281 location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
283 return location_querysets
285 def get_owner_queryset(self):
286 return User.objects.filter(owned_events__calendars=self.calendar).distinct()
288 # Event QuerySet parsers for a request/args/kwargs
289 def get_all_events(self, request, extra_context=None):
290 return self.get_event_queryset(), extra_context
292 def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
293 context = extra_context or {}
299 return self.get_timespan_queryset(year, month, day), context
301 def get_events_by_owner(self, request, username, extra_context=None):
303 owner = self.get_owner_queryset().get(username=username)
304 except User.DoesNotExist:
307 qs = self.get_event_queryset().filter(owner=owner)
308 context = extra_context or {}
314 def get_events_by_tag(self, request, tag_slugs, extra_context=None):
315 tag_slugs = tag_slugs.replace('+', '/').split('/')
316 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
321 # Raise a 404 on an incorrect slug.
322 found_slugs = [tag.slug for tag in tags]
323 for slug in tag_slugs:
324 if slug and slug not in found_slugs:
327 events = self.get_event_queryset()
329 events = events.filter(tags=tag)
331 context = extra_context or {}
332 context.update({'tags': tags})
334 return events, context
336 def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
338 ct = ContentType.objects.get_by_natural_key(app_label, model)
339 location = ct.model_class()._default_manager.get(pk=pk)
340 except ObjectDoesNotExist:
343 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
345 context = extra_context or {}
349 return events, context
352 def event_detail_view(self, request, year, month, day, slug, extra_context=None):
354 event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
355 except Event.DoesNotExist:
358 context = self.get_context()
359 context.update(extra_context or {})
363 return self.event_detail_page.render_to_response(request, extra_context=context)
366 def tag_archive_view(self, request, extra_context=None):
367 tags = self.get_tag_queryset()
368 context = self.get_context()
369 context.update(extra_context or {})
373 return self.tag_archive_page.render_to_response(request, extra_context=context)
375 def location_archive_view(self, request, extra_context=None):
376 # What datastructure should locations be?
377 locations = self.get_location_querysets()
378 context = self.get_context()
379 context.update(extra_context or {})
381 'locations': locations
383 return self.location_archive_page.render_to_response(request, extra_context=context)
385 def owner_archive_view(self, request, extra_context=None):
386 owners = self.get_owner_queryset()
387 context = self.get_context()
388 context.update(extra_context or {})
392 return self.owner_archive_page.render_to_response(request, extra_context=context)
395 def process_page_items(self, request, items):
396 if self.events_per_page:
397 page_num = request.GET.get('page', 1)
398 paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
400 'paginator': paginator,
401 'paginated_page': paginated_page,
402 self.item_context_var: items
406 self.item_context_var: items
408 return items, item_context
410 # Feed information hooks
411 def title(self, obj):
415 # Link is ignored anyway...
418 def feed_guid(self, obj):
421 def description(self, obj):
422 return obj.description
424 def feed_extra_kwargs(self, obj):
425 return {'filename': "%s.ics" % obj.slug}
427 def item_title(self, item):
430 def item_description(self, item):
431 return item.description
433 def item_link(self, item):
434 return self.reverse(item)
436 def item_guid(self, item):
439 def item_author_name(self, item):
441 return item.owner.get_full_name()
443 def item_author_email(self, item):
444 return getattr(item.owner, 'email', None) or None
446 def item_pubdate(self, item):
449 def item_categories(self, item):
450 return [tag.name for tag in item.tags.all()]
452 def item_extra_kwargs(self, item):
454 'start': item.get_start(),
455 'end': item.get_end(),
456 'last_modified': item.last_modified,
457 # Is forcing unicode enough, or should we look for a "custom method"?
458 'location': force_unicode(item.location),
461 def __unicode__(self):
462 return u"%s for %s" % (self.__class__.__name__, self.calendar)
464 field = CalendarView._meta.get_field('feed_type')
465 field.default = registry.get_slug(ICalendarFeed, field.default)