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.winer.models import FeedView
19 from philo.contrib.winer.feeds import registry
20 from philo.exceptions import ViewCanNotProvideSubpath
21 from philo.models import Tag, Entity, Page
22 from philo.models.fields import TemplateField
23 from philo.utils import ContentTypeRegistryLimiter
26 __all__ = ('register_location_model', 'unregister_location_model', 'Location', 'TimedModel', 'Event', 'Calendar', 'CalendarView',)
29 registry.register(ICalendarFeed, verbose_name="iCalendar")
31 DEFAULT_SITE = Site.objects.get_current()
34 _languages = dict(settings.LANGUAGES)
36 _languages[settings.LANGUAGE_CODE]
37 DEFAULT_LANGUAGE = settings.LANGUAGE_CODE
40 lang = settings.LANGUAGE_CODE.split('-')[0]
42 DEFAULT_LANGUAGE = lang
44 DEFAULT_LANGUAGE = None
47 location_content_type_limiter = ContentTypeRegistryLimiter()
50 def register_location_model(model):
51 location_content_type_limiter.register_class(model)
54 def unregister_location_model(model):
55 location_content_type_limiter.unregister_class(model)
58 class Location(Entity):
59 name = models.CharField(max_length=255)
60 slug = models.SlugField(max_length=255, unique=True)
62 def __unicode__(self):
66 register_location_model(Location)
69 class TimedModel(models.Model):
70 start_date = models.DateField(help_text="YYYY-MM-DD")
71 start_time = models.TimeField(blank=True, null=True, help_text="HH:MM:SS - 24 hour clock")
72 end_date = models.DateField()
73 end_time = models.TimeField(blank=True, null=True)
76 return self.start_time is None and self.end_time is None
79 if bool(self.start_time) != bool(self.end_time):
80 raise ValidationError("A %s must have either a start time and an end time or neither.")
82 if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time:
83 raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
86 return datetime.datetime.combine(self.start_date, self.start_time) if self.start_time else self.start_date
89 return datetime.datetime.combine(self.end_date, self.end_time) if self.end_time else self.end_date
95 class EventManager(models.Manager):
96 def get_query_set(self):
97 return EventQuerySet(self.model)
99 class EventQuerySet(QuerySet):
101 return self.filter(start_date__gte=datetime.date.today())
103 return self.filter(start_date__lte=datetime.date.today(), end_date__gte=datetime.date.today())
104 def single_day(self):
105 return self.filter(start_date__exact=models.F('end_date'))
107 return self.exclude(start_date__exact=models.F('end_date'))
109 class Event(Entity, TimedModel):
110 name = models.CharField(max_length=255)
111 slug = models.SlugField(max_length=255, unique_for_date='start_date')
113 location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
114 location_pk = models.TextField(blank=True)
115 location = GenericForeignKey('location_content_type', 'location_pk')
117 description = TemplateField()
119 tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
121 parent_event = models.ForeignKey('self', blank=True, null=True)
123 # TODO: "User module"
124 owner = models.ForeignKey(User, related_name='owned_events')
126 created = models.DateTimeField(auto_now_add=True)
127 last_modified = models.DateTimeField(auto_now=True)
129 site = models.ForeignKey(Site, default=DEFAULT_SITE)
133 return "%s@%s" % (self.created.isoformat(), getattr(self.site, 'domain', 'None'))
135 objects = EventManager()
137 def __unicode__(self):
141 unique_together = ('site', 'created')
144 class Calendar(Entity):
145 name = models.CharField(max_length=100)
146 slug = models.SlugField(max_length=100)
147 description = models.TextField(blank=True)
148 events = models.ManyToManyField(Event, related_name='calendars', blank=True)
150 site = models.ForeignKey(Site, default=DEFAULT_SITE)
151 language = models.CharField(max_length=5, choices=settings.LANGUAGES, default=DEFAULT_LANGUAGE)
153 def __unicode__(self):
158 # See http://xml.coverpages.org/tauber-fpi.html or ISO 9070:1991 for format information.
159 return "-//%s//%s//%s" % (self.site.name, self.name, self.language.split('-')[0].upper())
162 unique_together = ('name', 'site', 'language')
165 class CalendarView(FeedView):
166 calendar = models.ForeignKey(Calendar)
167 index_page = models.ForeignKey(Page, related_name="calendar_index_related")
168 event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
170 timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related", blank=True, null=True)
171 tag_page = models.ForeignKey(Page, related_name="calendar_tag_related", blank=True, null=True)
172 location_page = models.ForeignKey(Page, related_name="calendar_location_related", blank=True, null=True)
173 owner_page = models.ForeignKey(Page, related_name="calendar_owner_related", blank=True, null=True)
175 tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
176 location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
177 owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
179 tag_permalink_base = models.CharField(max_length=30, default='tags')
180 owner_permalink_base = models.CharField(max_length=30, default='owners')
181 location_permalink_base = models.CharField(max_length=30, default='locations')
182 events_per_page = models.PositiveIntegerField(blank=True, null=True)
184 item_context_var = "events"
185 object_attr = "calendar"
187 def get_reverse_params(self, obj):
188 if isinstance(obj, User):
189 return 'events_for_user', [], {'username': obj.username}
190 elif isinstance(obj, Event):
191 return 'event_detail', [], {
192 'year': str(obj.start_date.year).zfill(4),
193 'month': str(obj.start_date.month).zfill(2),
194 'day': str(obj.start_date.day).zfill(2),
197 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
198 if isinstance(obj, Tag):
200 return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
201 raise ViewCanNotProvideSubpath
203 def timespan_patterns(self, pattern, timespan_name):
204 return self.feed_patterns(pattern, 'get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
207 def urlpatterns(self):
208 # Perhaps timespans should be done with GET parameters? Or two /-separated
209 # date slugs? (e.g. 2010-02-1/2010-02-2) or a start and duration?
210 # (e.g. 2010-02-01/week/ or ?d=2010-02-01&l=week)
211 urlpatterns = self.feed_patterns(r'^', 'get_all_events', 'index_page', 'index') + \
212 self.timespan_patterns(r'^(?P<year>\d{4})', 'year') + \
213 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'month') + \
214 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'day') + \
215 self.feed_patterns(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
216 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') + \
217 self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
219 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$', self.event_detail_view, name="event_detail"),
222 # Some sort of shortcut for a location would be useful. This could be on a per-calendar
223 # or per-calendar-view basis.
224 #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
226 if self.tag_archive_page_id:
227 urlpatterns += patterns('',
228 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
231 if self.owner_archive_page_id:
232 urlpatterns += patterns('',
233 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
236 if self.location_archive_page_id:
237 urlpatterns += patterns('',
238 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
242 # Basic QuerySet fetchers.
243 def get_event_queryset(self):
244 return self.calendar.events.all()
246 def get_timespan_queryset(self, year, month=None, day=None):
247 qs = self.get_event_queryset()
248 # See python documentation for the min/max values.
249 if year and month and day:
250 year, month, day = int(year), int(month), int(day)
251 start_datetime = datetime.datetime(year, month, day, 0, 0)
252 end_datetime = datetime.datetime(year, month, day, 23, 59)
254 year, month = int(year), int(month)
255 start_datetime = datetime.datetime(year, month, 1, 0, 0)
256 end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
259 start_datetime = datetime.datetime(year, 1, 1, 0, 0)
260 end_datetime = datetime.datetime(year, 12, 31, 23, 59)
262 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)
264 def get_tag_queryset(self):
265 return Tag.objects.filter(events__calendars=self.calendar).distinct()
267 def get_location_querysets(self):
268 # Potential bottleneck?
270 locations = Event.objects.values_list('location_content_type', 'location_pk')
272 for ct, pk in locations:
273 location_map.setdefault(ct, []).append(pk)
275 location_cts = ContentType.objects.in_bulk(location_map.keys())
276 location_querysets = {}
278 for ct_pk, pks in location_map.items():
279 ct = location_cts[ct_pk]
280 location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
282 return location_querysets
284 def get_owner_queryset(self):
285 return User.objects.filter(owned_events__calendars=self.calendar).distinct()
287 # Event QuerySet parsers for a request/args/kwargs
288 def get_all_events(self, request, extra_context=None):
289 return self.get_event_queryset(), extra_context
291 def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
292 context = extra_context or {}
298 return self.get_timespan_queryset(year, month, day), context
300 def get_events_by_owner(self, request, username, extra_context=None):
302 owner = self.get_owner_queryset().get(username=username)
303 except User.DoesNotExist:
306 qs = self.get_event_queryset().filter(owner=owner)
307 context = extra_context or {}
313 def get_events_by_tag(self, request, tag_slugs, extra_context=None):
314 tag_slugs = tag_slugs.replace('+', '/').split('/')
315 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
320 # Raise a 404 on an incorrect slug.
321 found_slugs = [tag.slug for tag in tags]
322 for slug in tag_slugs:
323 if slug and slug not in found_slugs:
326 events = self.get_event_queryset()
328 events = events.filter(tags=tag)
330 context = extra_context or {}
331 context.update({'tags': tags})
333 return events, context
335 def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
337 ct = ContentType.objects.get_by_natural_key(app_label, model)
338 location = ct.model_class()._default_manager.get(pk=pk)
339 except ObjectDoesNotExist:
342 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
344 context = extra_context or {}
348 return events, context
351 def event_detail_view(self, request, year, month, day, slug, extra_context=None):
353 event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
354 except Event.DoesNotExist:
357 context = self.get_context()
358 context.update(extra_context or {})
362 return self.event_detail_page.render_to_response(request, extra_context=context)
365 def tag_archive_view(self, request, extra_context=None):
366 tags = self.get_tag_queryset()
367 context = self.get_context()
368 context.update(extra_context or {})
372 return self.tag_archive_page.render_to_response(request, extra_context=context)
374 def location_archive_view(self, request, extra_context=None):
375 # What datastructure should locations be?
376 locations = self.get_location_querysets()
377 context = self.get_context()
378 context.update(extra_context or {})
380 'locations': locations
382 return self.location_archive_page.render_to_response(request, extra_context=context)
384 def owner_archive_view(self, request, extra_context=None):
385 owners = self.get_owner_queryset()
386 context = self.get_context()
387 context.update(extra_context or {})
391 return self.owner_archive_page.render_to_response(request, extra_context=context)
394 def process_page_items(self, request, items):
395 if self.events_per_page:
396 page_num = request.GET.get('page', 1)
397 paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
399 'paginator': paginator,
400 'paginated_page': paginated_page,
401 self.item_context_var: items
405 self.item_context_var: items
407 return items, item_context
409 # Feed information hooks
410 def title(self, obj):
414 # Link is ignored anyway...
417 def feed_guid(self, obj):
420 def description(self, obj):
421 return obj.description
423 def feed_extra_kwargs(self, obj):
424 return {'filename': "%s.ics" % obj.slug}
426 def item_title(self, item):
429 def item_description(self, item):
430 return item.description
432 def item_link(self, item):
433 return self.reverse(item)
435 def item_guid(self, item):
438 def item_author_name(self, item):
440 return item.owner.get_full_name()
442 def item_author_email(self, item):
443 return getattr(item.owner, 'email', None) or None
445 def item_pubdate(self, item):
448 def item_categories(self, item):
449 return [tag.name for tag in item.tags.all()]
451 def item_extra_kwargs(self, item):
453 'start': item.get_start(),
454 'end': item.get_end(),
455 'last_modified': item.last_modified,
456 # Is forcing unicode enough, or should we look for a "custom method"?
457 'location': force_unicode(item.location),
460 def __unicode__(self):
461 return u"%s for %s" % (self.__class__.__name__, self.calendar)
463 field = CalendarView._meta.get_field('feed_type')
464 field.default = registry.get_slug(ICalendarFeed, field.default)