X-Git-Url: http://git.ithinksw.org/philo.git/blobdiff_plain/359b38924a79d43588a6a7097a154cb3d2fde62d..9bb81b7a041bbe45235f53d37c864d49d51eff50:/contrib/julian/models.py
diff --git a/contrib/julian/models.py b/contrib/julian/models.py
index 9556d89..333bcb3 100644
--- a/contrib/julian/models.py
+++ b/contrib/julian/models.py
@@ -3,17 +3,18 @@ from django.conf.urls.defaults import url, patterns, include
from django.contrib.auth.models import User
from django.contrib.contenttypes.generic import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
+from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import RegexValidator
from django.db import models
-from django.http import HttpResponse
+from django.db.models.query import QuerySet
+from django.http import HttpResponse, Http404
from django.utils.encoding import force_unicode
from philo.contrib.julian.feedgenerator import ICalendarFeed
from philo.contrib.penfield.models import FeedView, FEEDS
-from philo.models.base import Tag, Entity
-from philo.models.fields import TemplateField
+from philo.exceptions import ViewCanNotProvideSubpath
+from philo.models import Tag, Entity, Page, TemplateField
from philo.utils import ContentTypeRegistryLimiter
-import re
+import re, datetime, calendar
# TODO: Could this regex more closely match the Formal Public Identifier spec?
@@ -38,6 +39,7 @@ def unregister_location_model(model):
class Location(Entity):
name = models.CharField(max_length=255)
+ slug = models.SlugField(max_length=255, unique=True)
def __unicode__(self):
return self.name
@@ -72,9 +74,23 @@ class TimedModel(models.Model):
abstract = True
+class EventManager(models.Manager):
+ def get_query_set(self):
+ return EventQuerySet(self.model)
+
+class EventQuerySet(QuerySet):
+ def upcoming(self):
+ return self.filter(start_date__gte=datetime.date.today())
+ def current(self):
+ return self.filter(start_date__lte=datetime.date.today(), end_date__gte=datetime.date.today())
+ def single_day(self):
+ return self.filter(start_date__exact=models.F('end_date'))
+ def multiday(self):
+ return self.exclude(start_date__exact=models.F('end_date'))
+
class Event(Entity, TimedModel):
name = models.CharField(max_length=255)
- slug = models.SlugField(max_length=255)
+ slug = models.SlugField(max_length=255, unique_for_date='start_date')
location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
location_pk = models.TextField(blank=True)
@@ -82,76 +98,281 @@ class Event(Entity, TimedModel):
description = TemplateField()
- tags = models.ManyToManyField(Tag, blank=True, null=True)
+ tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
parent_event = models.ForeignKey('self', blank=True, null=True)
- owner = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'))
+ # TODO: "User module"
+ owner = models.ForeignKey(User, related_name='owned_events')
created = models.DateTimeField(auto_now_add=True)
last_modified = models.DateTimeField(auto_now=True)
uuid = models.TextField() # Format?
+
+ objects = EventManager()
+
+ def __unicode__(self):
+ return self.name
class Calendar(Entity):
name = models.CharField(max_length=100)
slug = models.SlugField(max_length=100)
description = models.TextField(blank=True)
- #slug = models.SlugField(max_length=255, unique=True)
events = models.ManyToManyField(Event, related_name='calendars')
# TODO: Can we auto-generate this on save based on site id and calendar name and settings language?
- uuid = models.TextField("Calendar UUID", unique=True, help_text="Should conform to Formal Public Identifier format. See <http://en.wikipedia.org/wiki/Formal_Public_Identifier>", validators=[RegexValidator(FPI_REGEX)])
+ uuid = models.TextField("Calendar UUID", unique=True, help_text="Should conform to Formal Public Identifier format. See Wikipedia.", validators=[RegexValidator(FPI_REGEX)])
+
+ def __unicode__(self):
+ return self.name
-class ICalendarFeedView(FeedView):
+class CalendarView(FeedView):
calendar = models.ForeignKey(Calendar)
+ index_page = models.ForeignKey(Page, related_name="calendar_index_related")
+ event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
+
+ timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related", blank=True, null=True)
+ tag_page = models.ForeignKey(Page, related_name="calendar_tag_related", blank=True, null=True)
+ location_page = models.ForeignKey(Page, related_name="calendar_location_related", blank=True, null=True)
+ owner_page = models.ForeignKey(Page, related_name="calendar_owner_related", blank=True, null=True)
+
+ tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
+ location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
+ owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
+
+ tag_permalink_base = models.CharField(max_length=30, default='tags')
+ owner_permalink_base = models.CharField(max_length=30, default='owners')
+ location_permalink_base = models.CharField(max_length=30, default='locations')
+ events_per_page = models.PositiveIntegerField(blank=True, null=True)
item_context_var = "events"
object_attr = "calendar"
def get_reverse_params(self, obj):
- return 'feed', [], {}
+ if isinstance(obj, User):
+ return 'events_for_user', [], {'username': obj.username}
+ elif isinstance(obj, Event):
+ return 'event_detail', [], {
+ 'year': str(obj.start_date.year).zfill(4),
+ 'month': str(obj.start_date.month).zfill(2),
+ 'day': str(obj.start_date.day).zfill(2),
+ 'slug': obj.slug
+ }
+ elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
+ if isinstance(obj, Tag):
+ obj = [obj]
+ return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
+ raise ViewCanNotProvideSubpath
+
+ def timespan_patterns(self, pattern, timespan_name):
+ return self.feed_patterns(pattern, 'get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
@property
def urlpatterns(self):
- return patterns('',
- url(r'^$', self.feed_view('get_all_events', 'feed'), name='feed')
- )
-
- def feed_view(self, get_items_attr, reverse_name):
- """
- Returns a view function that renders a list of items as a feed.
- """
- get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
-
- def inner(request, extra_context=None, *args, **kwargs):
- obj = self.get_object(request, *args, **kwargs)
- feed = self.get_feed(obj, request, reverse_name)
- items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
- self.populate_feed(feed, items, request)
-
- response = HttpResponse(mimetype=feed.mime_type)
- feed.write(response, 'utf-8')
+ # Perhaps timespans should be done with GET parameters? Or two /-separated
+ # date slugs? (e.g. 2010-02-1/2010-02-2) or a start and duration?
+ # (e.g. 2010-02-01/week/ or ?d=2010-02-01&l=week)
+ urlpatterns = self.feed_patterns(r'^', 'get_all_events', 'index_page', 'index') + \
+ self.timespan_patterns(r'^(?P\d{4})', 'year') + \
+ self.timespan_patterns(r'^(?P\d{4})/(?P\d{2})', 'month') + \
+ self.timespan_patterns(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})', 'day') + \
+ self.feed_patterns(r'^%s/(?P[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
+ self.feed_patterns(r'^%s/(?P\w+)/(?P\w+)/(?P[^/]+)' % self.location_permalink_base, 'get_events_by_location', 'location_page', 'events_by_location') + \
+ self.feed_patterns(r'^%s/(?P[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
+ patterns('',
+ url(r'(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w-]+)$', self.event_detail_view, name="event_detail"),
+ )
- if FEEDS[self.feed_type] == ICalendarFeed:
- # Add some extra information to the response for iCalendar readers.
- #
- # Also, __get_dynamic_attr is protected by python - mangled. Should it
- # just be private?
- filename = self._FeedView__get_dynamic_attr('filename', obj)
- response['Filename'] = filename
- response['Content-Disposition'] = 'attachment; filename=%s' % filename
- return response
-
- return inner
+ # Some sort of shortcut for a location would be useful. This could be on a per-calendar
+ # or per-calendar-view basis.
+ #url(r'^%s/(?P[\w-]+)' % self.location_permalink_base, ...)
+
+ if self.tag_archive_page:
+ urlpatterns += patterns('',
+ url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
+ )
+
+ if self.owner_archive_page:
+ urlpatterns += patterns('',
+ url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
+ )
+
+ if self.location_archive_page:
+ urlpatterns += patterns('',
+ url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
+ )
+ return urlpatterns
+ # Basic QuerySet fetchers.
def get_event_queryset(self):
return self.calendar.events.all()
+ def get_timespan_queryset(self, year, month=None, day=None):
+ qs = self.get_event_queryset()
+ # See python documentation for the min/max values.
+ if year and month and day:
+ year, month, day = int(year), int(month), int(day)
+ start_datetime = datetime.datetime(year, month, day, 0, 0)
+ end_datetime = datetime.datetime(year, month, day, 23, 59)
+ elif year and month:
+ year, month = int(year), int(month)
+ start_datetime = datetime.datetime(year, month, 1, 0, 0)
+ end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
+ else:
+ year = int(year)
+ start_datetime = datetime.datetime(year, 1, 1, 0, 0)
+ end_datetime = datetime.datetime(year, 12, 31, 23, 59)
+
+ 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)
+
+ def get_tag_queryset(self):
+ return Tag.objects.filter(events__calendars=self.calendar).distinct()
+
+ def get_location_querysets(self):
+ # Potential bottleneck?
+ location_map = {}
+ locations = Event.objects.values_list('location_content_type', 'location_pk')
+
+ for ct, pk in locations:
+ location_map.setdefault(ct, []).append(pk)
+
+ location_cts = ContentType.objects.in_bulk(location_map.keys())
+ location_querysets = {}
+
+ for ct_pk, pks in location_map.items():
+ ct = location_cts[ct_pk]
+ location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
+
+ return location_querysets
+
+ def get_owner_queryset(self):
+ return User.objects.filter(owned_events__calendars=self.calendar).distinct()
+
+ # Event QuerySet parsers for a request/args/kwargs
def get_all_events(self, request, extra_context=None):
return self.get_event_queryset(), extra_context
+ def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
+ context = extra_context or {}
+ context.update({
+ 'year': year,
+ 'month': month,
+ 'day': day
+ })
+ return self.get_timespan_queryset(year, month, day), context
+
+ def get_events_by_owner(self, request, username, extra_context=None):
+ try:
+ owner = self.get_owner_queryset().get(username=username)
+ except User.DoesNotExist:
+ raise Http404
+
+ qs = self.get_event_queryset().filter(owner=owner)
+ context = extra_context or {}
+ context.update({
+ 'owner': owner
+ })
+ return qs, context
+
+ def get_events_by_tag(self, request, tag_slugs, extra_context=None):
+ tag_slugs = tag_slugs.replace('+', '/').split('/')
+ tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
+
+ if not tags:
+ raise Http404
+
+ # Raise a 404 on an incorrect slug.
+ found_slugs = [tag.slug for tag in tags]
+ for slug in tag_slugs:
+ if slug and slug not in found_slugs:
+ raise Http404
+
+ events = self.get_event_queryset()
+ for tag in tags:
+ events = events.filter(tags=tag)
+
+ context = extra_context or {}
+ context.update({'tags': tags})
+
+ return events, context
+
+ def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
+ try:
+ ct = ContentType.objects.get(app_label=app_label, model=model)
+ location = ct.model_class()._default_manager.get(pk=pk)
+ except ObjectDoesNotExist:
+ raise Http404
+
+ events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
+
+ context = extra_context or {}
+ context.update({
+ 'location': location
+ })
+ return events, context
+
+ # Detail View.
+ def event_detail_view(self, request, year, month, day, slug, extra_context=None):
+ try:
+ event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
+ except Event.DoesNotExist:
+ raise Http404
+
+ context = self.get_context()
+ context.update(extra_context or {})
+ context.update({
+ 'event': event
+ })
+ return self.event_detail_page.render_to_response(request, extra_context=context)
+
+ # Archive Views.
+ def tag_archive_view(self, request, extra_context=None):
+ tags = self.get_tag_queryset()
+ context = self.get_context()
+ context.update(extra_context or {})
+ context.update({
+ 'tags': tags
+ })
+ return self.tag_archive_page.render_to_response(request, extra_context=context)
+
+ def location_archive_view(self, request, extra_context=None):
+ # What datastructure should locations be?
+ locations = self.get_location_querysets()
+ context = self.get_context()
+ context.update(extra_context or {})
+ context.update({
+ 'locations': locations
+ })
+ return self.location_archive_page.render_to_response(request, extra_context=context)
+
+ def owner_archive_view(self, request, extra_context=None):
+ owners = self.get_owner_queryset()
+ context = self.get_context()
+ context.update(extra_context or {})
+ context.update({
+ 'owners': owners
+ })
+ return self.owner_archive_page.render_to_response(request, extra_context=context)
+
+ # Process page items
+ def process_page_items(self, request, items):
+ if self.events_per_page:
+ page_num = request.GET.get('page', 1)
+ paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
+ item_context = {
+ 'paginator': paginator,
+ 'paginated_page': paginated_page,
+ self.item_context_var: items
+ }
+ else:
+ item_context = {
+ self.item_context_var: items
+ }
+ return items, item_context
+
+ # Feed information hooks
def title(self, obj):
return obj.name
@@ -159,9 +380,6 @@ class ICalendarFeedView(FeedView):
# Link is ignored anyway...
return ""
- def filename(self, obj):
- return "%s.ics" % obj.slug
-
def feed_guid(self, obj):
# Is this correct? Should I have a different id for different subfeeds?
return obj.uuid
@@ -169,11 +387,8 @@ class ICalendarFeedView(FeedView):
def description(self, obj):
return obj.description
- # Would this be meaningful? I think it's just ignored anyway, for ical format.
- #def categories(self, obj):
- # event_ct = ContentType.objects.get_for_model(Event)
- # event_pks = obj.events.values_list('pk')
- # return [tag.name for tag in Tag.objects.filter(content_type=event_ct, object_id__in=event_pks)]
+ def feed_extra_kwargs(self, obj):
+ return {'filename': "%s.ics" % obj.slug}
def item_title(self, item):
return item.name
@@ -209,9 +424,9 @@ class ICalendarFeedView(FeedView):
'location': force_unicode(item.location),
}
- class Meta:
- verbose_name = "iCalendar view"
+ def __unicode__(self):
+ return u"%s for %s" % (self.__class__.__name__, self.calendar)
-field = ICalendarFeedView._meta.get_field('feed_type')
+field = CalendarView._meta.get_field('feed_type')
field._choices += ((ICALENDAR, 'iCalendar'),)
field.default = ICALENDAR
\ No newline at end of file