From 21a2199a2b91086d6cfefb93975566598db4149f Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Mon, 14 Feb 2011 18:02:51 -0500 Subject: [PATCH] Reinstated EventAdmin fieldsets and added some options. Filled out the page requirements, get_reverse_params, urls, and object-fetchers for a CalendarView. Moved header declaration for ICalendarFeed into the feed's write method. --- contrib/julian/admin.py | 30 +++-- contrib/julian/feedgenerator.py | 9 +- contrib/julian/models.py | 231 +++++++++++++++++++++++++------- 3 files changed, 208 insertions(+), 62 deletions(-) diff --git a/contrib/julian/admin.py b/contrib/julian/admin.py index c8ef95a..f976e82 100644 --- a/contrib/julian/admin.py +++ b/contrib/julian/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from philo.admin import EntityAdmin +from philo.admin import EntityAdmin, COLLAPSE_CLASSES from philo.contrib.julian.models import Location, Event, Calendar, ICalendarFeedView @@ -8,20 +8,26 @@ class LocationAdmin(EntityAdmin): class EventAdmin(EntityAdmin): - #fieldsets = ( - # (None, { - # 'fields': ('name', 'slug' 'description', 'tags', 'parent_event', 'owner') - # }), - # ('Location', { - # 'fields': ('location_content_type', 'location_pk') - # }), - # ('Time', { - # 'fields': (('start_date', 'start_time'), ('end_date', 'end_time'),), - # }) - #) + fieldsets = ( + (None, { + 'fields': ('name', 'slug', 'description', 'tags', 'owner') + }), + ('Location', { + 'fields': ('location_content_type', 'location_pk') + }), + ('Time', { + 'fields': (('start_date', 'start_time'), ('end_date', 'end_time'),), + }), + ('Advanced', { + 'fields': ('parent_event', 'uuid',), + 'classes': COLLAPSE_CLASSES + }) + ) related_lookup_fields = { 'generic': [["location_content_type", "location_pk"]] } + filter_horizontal = ['tags'] + raw_id_fields = ['parent_event'] class CalendarAdmin(EntityAdmin): diff --git a/contrib/julian/feedgenerator.py b/contrib/julian/feedgenerator.py index 274f617..23efdca 100644 --- a/contrib/julian/feedgenerator.py +++ b/contrib/julian/feedgenerator.py @@ -1,3 +1,4 @@ +from django.http import HttpResponse from django.utils.feedgenerator import SyndicationFeed import vobject @@ -74,4 +75,10 @@ class ICalendarFeed(SyndicationFeed): if key in ITEM_ICAL_MAP and val: event.add(ITEM_ICAL_MAP[key]).value = val - cal.serialize(outfile) \ No newline at end of file + cal.serialize(outfile) + + # Some special handling for HttpResponses. See link above. + if isinstance(outfile, HttpResponse): + filename = self.feed.get('filename', 'filename.ics') + outfile['Filename'] = filename + response['Content-Disposition'] = 'attachment; filename=%s' % filename \ No newline at end of file diff --git a/contrib/julian/models.py b/contrib/julian/models.py index 9556d89..7faf4f0 100644 --- a/contrib/julian/models.py +++ b/contrib/julian/models.py @@ -3,17 +3,17 @@ 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.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 +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 @@ -74,7 +75,7 @@ class TimedModel(models.Model): 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='created') 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 +83,214 @@ 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) created = models.DateTimeField(auto_now_add=True) last_modified = models.DateTimeField(auto_now=True) uuid = models.TextField() # Format? + + 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)]) -class ICalendarFeedView(FeedView): +class CalendarView(FeedView): calendar = models.ForeignKey(Calendar) + index_page = models.ForeignKey(Page, related_name="calendar_index_related") + timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related") + event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related") + tag_page = models.ForeignKey(Page, related_name="calendar_tag_related") + location_page = models.ForeignKey(Page, related_name="calendar_location_related") + owner_page = models.ForeignKey(Page, related_name="calendar_owner_related") + + 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='owner') + location_permalink_base = models.CharField(max_length=30, default='location') 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': obj.start_date.year, + 'month': obj.start_date.month, + 'day': obj.start_date.day, + '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): + urlpatterns = patterns('', + ) + self.feed_patterns('get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name) + return urlpatterns @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) + 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"), - response = HttpResponse(mimetype=feed.mime_type) - feed.write(response, 'utf-8') + url(r'^%s/(?P[^/]+)' % self.owner_permalink_base, include(self.feed_patterns('get_events_by_user', 'owner_page', 'events_by_user'))), - 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 + # 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') + ) - return inner + if self.owner_archive_page: + urlpatterns += patterns('', + url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive') + ) + + if self.owner_archive_page: + urlpatterns += patterns('', + url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive') + ) + return urlpatterns 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: + start_datetime = datetime.datetime(year, month, day, 0, 0) + end_datetime = datetime.datetime(year, month, day, 23, 59) + elif year and month: + start_datetime = datetime.datetime(year, month, 1, 0, 0) + end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59) + else: + 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.time()).exclude(start_date__gt=end_datetime, start_time__gt=end_datetime.time(), start_time__isnull=False).exclude(start_time__isnull=True, start_date__gt=end_datetime.time()) + + def get_tag_queryset(self): + return Tag.objects.filter(events__calendar=self.calendar).distinct() + + # Event fetchers. 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_user(self, request, username, extra_context=None): + try: + user = User.objects.get(username) + except User.DoesNotExist: + raise Http404 + + qs = self.event_queryset().filter(owner=user) + context = extra_context or {} + context.update({ + 'user': user + }) + 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. TODO: fill this out. + def event_detail_view(self, request, year, month, day, slug, extra_context=None): + pass + + # Archive Views. TODO: fill these out. + def tag_archive_view(self, request, extra_context=None): + pass + + def location_archive_view(self, request, extra_context=None): + pass + + def owner_archive_view(self, request, extra_context=None): + pass + + # Feed information hooks def title(self, obj): return obj.name @@ -159,9 +298,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 +305,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 +342,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 -- 2.20.1