X-Git-Url: http://git.ithinksw.org/philo.git/blobdiff_plain/cf3eea2d358763b2408149b620a1b61dabdb228d..051e647c3f313cb4a2a51d7b17bcfda79f192b46:/contrib/julian/models.py?ds=sidebyside diff --git a/contrib/julian/models.py b/contrib/julian/models.py index 29f5fd4..14a0b17 100644 --- a/contrib/julian/models.py +++ b/contrib/julian/models.py @@ -1,13 +1,19 @@ from django.conf import settings +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 philo.models.base import Tag, Entity -from philo.models.fields import TemplateField +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.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? @@ -15,6 +21,10 @@ import re FPI_REGEX = re.compile(r"(|\+//|-//)[^/]+//[^/]+//[A-Z]{2}") +ICALENDAR = ICalendarFeed.mime_type +FEEDS[ICALENDAR] = ICalendarFeed + + location_content_type_limiter = ContentTypeRegistryLimiter() @@ -28,6 +38,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 @@ -52,33 +63,360 @@ class TimedModel(models.Model): if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time: raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__) + def get_start(self): + return self.start_date + + def get_end(self): + return self.end_date + class Meta: abstract = True 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) - location_pk = models.TextField() + location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True) + location_pk = models.TextField(blank=True) location = GenericForeignKey('location_content_type', 'location_pk') 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? - # TODO: Add uid - use as pk? + def __unicode__(self): + return self.name class Calendar(Entity): name = models.CharField(max_length=100) - #slug = models.SlugField(max_length=255, unique=True) + slug = models.SlugField(max_length=100) + description = models.TextField(blank=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.CharField("Calendar UUID", max_length=100, unique=True, help_text="Should conform to Formal Public Identifier format. See ", validators=[RegexValidator(FPI_REGEX)]) \ No newline at end of file + 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 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): + 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, timespan_name): + return self.feed_patterns('get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name) + + @property + def urlpatterns(self): + urlpatterns = patterns('', + url(r'^', include(self.feed_patterns('get_all_events', 'index_page', 'index'))), + + url(r'^(?P\d{4})', include(self.timespan_patterns('year'))), + url(r'^(?P\d{4})/(?P\d{2})', include(self.timespan_patterns('month'))), + url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})', include(self.timespan_patterns('day'))), + #url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P\d{1,2})', include(self.timespan_patterns('hour'))), + url(r'(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w-]+)', self.event_detail_view, name="event_detail"), + + url(r'^%s/(?P[^/]+)' % self.owner_permalink_base, include(self.feed_patterns('get_events_by_owner', 'owner_page', 'events_by_user'))), + + # 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, ...) + url(r'^%s/(?P\w+)/(?P\w+)/(?P[^/]+)' % self.location_permalink_base, include(self.feed_patterns('get_events_by_location', 'location_page', 'events_by_location'))), + ) + + if self.feeds_enabled: + urlpatterns += patterns('', + url(r'^%s/(?P[-\w]+[-+/\w]*)/%s$' % (self.tag_permalink_base, self.feed_suffix), self.feed_view('get_events_by_tag', 'events_by_tag_feed'), name='events_by_tag_feed'), + ) + urlpatterns += patterns('', + url(r'^%s/(?P[-\w]+[-+/\w]*)$' % self.tag_permalink_base, self.page_view('get_events_by_tag', 'tag_page'), name='events_by_tag') + ) + + 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 + + def link(self, obj): + # Link is ignored anyway... + return "" + + def feed_guid(self, obj): + # Is this correct? Should I have a different id for different subfeeds? + return obj.uuid + + def description(self, obj): + return obj.description + + def feed_extra_kwargs(self, obj): + return {'filename': "%s.ics" % obj.slug} + + def item_title(self, item): + return item.name + + def item_description(self, item): + return item.description + + def item_link(self, item): + return self.reverse(item) + + def item_guid(self, item): + return item.uuid + + def item_author_name(self, item): + if item.owner: + return item.owner.get_full_name() + + def item_author_email(self, item): + return getattr(item.owner, 'email', None) or None + + def item_pubdate(self, item): + return item.created + + def item_categories(self, item): + return [tag.name for tag in item.tags.all()] + + def item_extra_kwargs(self, item): + return { + 'start': item.get_start(), + 'end': item.get_end(), + 'last_modified': item.last_modified, + # Is forcing unicode enough, or should we look for a "custom method"? + 'location': force_unicode(item.location), + } + + def __unicode__(self): + return u"%s for %s" % (self.__class__.__name__, self.calendar) + +field = CalendarView._meta.get_field('feed_type') +field._choices += ((ICALENDAR, 'iCalendar'),) +field.default = ICALENDAR \ No newline at end of file