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)
124 uuid = models.TextField() # Format?
126 objects = EventManager()
128 def __unicode__(self):
132 class Calendar(Entity):
133 name = models.CharField(max_length=100)
134 slug = models.SlugField(max_length=100)
135 description = models.TextField(blank=True)
136 events = models.ManyToManyField(Event, related_name='calendars', blank=True)
138 site = models.ForeignKey(Site, default=DEFAULT_SITE)
139 language = models.CharField(max_length=5, choices=settings.LANGUAGES, default=DEFAULT_LANGUAGE)
141 def __unicode__(self):
145 unique_together = ('name', 'site', 'language')
148 class CalendarView(FeedView):
149 calendar = models.ForeignKey(Calendar)
150 index_page = models.ForeignKey(Page, related_name="calendar_index_related")
151 event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
153 timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related", blank=True, null=True)
154 tag_page = models.ForeignKey(Page, related_name="calendar_tag_related", blank=True, null=True)
155 location_page = models.ForeignKey(Page, related_name="calendar_location_related", blank=True, null=True)
156 owner_page = models.ForeignKey(Page, related_name="calendar_owner_related", blank=True, null=True)
158 tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
159 location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
160 owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
162 tag_permalink_base = models.CharField(max_length=30, default='tags')
163 owner_permalink_base = models.CharField(max_length=30, default='owners')
164 location_permalink_base = models.CharField(max_length=30, default='locations')
165 events_per_page = models.PositiveIntegerField(blank=True, null=True)
167 item_context_var = "events"
168 object_attr = "calendar"
170 def get_reverse_params(self, obj):
171 if isinstance(obj, User):
172 return 'events_for_user', [], {'username': obj.username}
173 elif isinstance(obj, Event):
174 return 'event_detail', [], {
175 'year': str(obj.start_date.year).zfill(4),
176 'month': str(obj.start_date.month).zfill(2),
177 'day': str(obj.start_date.day).zfill(2),
180 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
181 if isinstance(obj, Tag):
183 return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
184 raise ViewCanNotProvideSubpath
186 def timespan_patterns(self, pattern, timespan_name):
187 return self.feed_patterns(pattern, 'get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
190 def urlpatterns(self):
191 # Perhaps timespans should be done with GET parameters? Or two /-separated
192 # date slugs? (e.g. 2010-02-1/2010-02-2) or a start and duration?
193 # (e.g. 2010-02-01/week/ or ?d=2010-02-01&l=week)
194 urlpatterns = self.feed_patterns(r'^', 'get_all_events', 'index_page', 'index') + \
195 self.timespan_patterns(r'^(?P<year>\d{4})', 'year') + \
196 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'month') + \
197 self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'day') + \
198 self.feed_patterns(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
199 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') + \
200 self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
202 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$', self.event_detail_view, name="event_detail"),
205 # Some sort of shortcut for a location would be useful. This could be on a per-calendar
206 # or per-calendar-view basis.
207 #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
209 if self.tag_archive_page:
210 urlpatterns += patterns('',
211 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
214 if self.owner_archive_page:
215 urlpatterns += patterns('',
216 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
219 if self.location_archive_page:
220 urlpatterns += patterns('',
221 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
225 # Basic QuerySet fetchers.
226 def get_event_queryset(self):
227 return self.calendar.events.all()
229 def get_timespan_queryset(self, year, month=None, day=None):
230 qs = self.get_event_queryset()
231 # See python documentation for the min/max values.
232 if year and month and day:
233 year, month, day = int(year), int(month), int(day)
234 start_datetime = datetime.datetime(year, month, day, 0, 0)
235 end_datetime = datetime.datetime(year, month, day, 23, 59)
237 year, month = int(year), int(month)
238 start_datetime = datetime.datetime(year, month, 1, 0, 0)
239 end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
242 start_datetime = datetime.datetime(year, 1, 1, 0, 0)
243 end_datetime = datetime.datetime(year, 12, 31, 23, 59)
245 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)
247 def get_tag_queryset(self):
248 return Tag.objects.filter(events__calendars=self.calendar).distinct()
250 def get_location_querysets(self):
251 # Potential bottleneck?
253 locations = Event.objects.values_list('location_content_type', 'location_pk')
255 for ct, pk in locations:
256 location_map.setdefault(ct, []).append(pk)
258 location_cts = ContentType.objects.in_bulk(location_map.keys())
259 location_querysets = {}
261 for ct_pk, pks in location_map.items():
262 ct = location_cts[ct_pk]
263 location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
265 return location_querysets
267 def get_owner_queryset(self):
268 return User.objects.filter(owned_events__calendars=self.calendar).distinct()
270 # Event QuerySet parsers for a request/args/kwargs
271 def get_all_events(self, request, extra_context=None):
272 return self.get_event_queryset(), extra_context
274 def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
275 context = extra_context or {}
281 return self.get_timespan_queryset(year, month, day), context
283 def get_events_by_owner(self, request, username, extra_context=None):
285 owner = self.get_owner_queryset().get(username=username)
286 except User.DoesNotExist:
289 qs = self.get_event_queryset().filter(owner=owner)
290 context = extra_context or {}
296 def get_events_by_tag(self, request, tag_slugs, extra_context=None):
297 tag_slugs = tag_slugs.replace('+', '/').split('/')
298 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
303 # Raise a 404 on an incorrect slug.
304 found_slugs = [tag.slug for tag in tags]
305 for slug in tag_slugs:
306 if slug and slug not in found_slugs:
309 events = self.get_event_queryset()
311 events = events.filter(tags=tag)
313 context = extra_context or {}
314 context.update({'tags': tags})
316 return events, context
318 def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
320 ct = ContentType.objects.get(app_label=app_label, model=model)
321 location = ct.model_class()._default_manager.get(pk=pk)
322 except ObjectDoesNotExist:
325 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
327 context = extra_context or {}
331 return events, context
334 def event_detail_view(self, request, year, month, day, slug, extra_context=None):
336 event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
337 except Event.DoesNotExist:
340 context = self.get_context()
341 context.update(extra_context or {})
345 return self.event_detail_page.render_to_response(request, extra_context=context)
348 def tag_archive_view(self, request, extra_context=None):
349 tags = self.get_tag_queryset()
350 context = self.get_context()
351 context.update(extra_context or {})
355 return self.tag_archive_page.render_to_response(request, extra_context=context)
357 def location_archive_view(self, request, extra_context=None):
358 # What datastructure should locations be?
359 locations = self.get_location_querysets()
360 context = self.get_context()
361 context.update(extra_context or {})
363 'locations': locations
365 return self.location_archive_page.render_to_response(request, extra_context=context)
367 def owner_archive_view(self, request, extra_context=None):
368 owners = self.get_owner_queryset()
369 context = self.get_context()
370 context.update(extra_context or {})
374 return self.owner_archive_page.render_to_response(request, extra_context=context)
377 def process_page_items(self, request, items):
378 if self.events_per_page:
379 page_num = request.GET.get('page', 1)
380 paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
382 'paginator': paginator,
383 'paginated_page': paginated_page,
384 self.item_context_var: items
388 self.item_context_var: items
390 return items, item_context
392 # Feed information hooks
393 def title(self, obj):
397 # Link is ignored anyway...
400 def feed_guid(self, obj):
401 # Is this correct? Should I have a different id for different subfeeds?
402 # See http://xml.coverpages.org/tauber-fpi.html for format.
403 return "-//%s//%s %s//%s" % (obj.site.name.upper(), self.feed_type.upper(), obj.name.upper(), obj.language.upper())
405 def description(self, obj):
406 return obj.description
408 def feed_extra_kwargs(self, obj):
409 return {'filename': "%s.ics" % obj.slug}
411 def item_title(self, item):
414 def item_description(self, item):
415 return item.description
417 def item_link(self, item):
418 return self.reverse(item)
420 def item_guid(self, item):
423 def item_author_name(self, item):
425 return item.owner.get_full_name()
427 def item_author_email(self, item):
428 return getattr(item.owner, 'email', None) or None
430 def item_pubdate(self, item):
433 def item_categories(self, item):
434 return [tag.name for tag in item.tags.all()]
436 def item_extra_kwargs(self, item):
438 'start': item.get_start(),
439 'end': item.get_end(),
440 'last_modified': item.last_modified,
441 # Is forcing unicode enough, or should we look for a "custom method"?
442 'location': force_unicode(item.location),
445 def __unicode__(self):
446 return u"%s for %s" % (self.__class__.__name__, self.calendar)
448 field = CalendarView._meta.get_field('feed_type')
449 field._choices += ((ICALENDAR, 'iCalendar'),)
450 field.default = ICALENDAR