Reinstated EventAdmin fieldsets and added some options. Filled out the page requireme...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Mon, 14 Feb 2011 23:02:51 +0000 (18:02 -0500)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Mon, 14 Feb 2011 23:06:14 +0000 (18:06 -0500)
contrib/julian/admin.py
contrib/julian/feedgenerator.py
contrib/julian/models.py

index c8ef95a..f976e82 100644 (file)
@@ -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):
index 274f617..23efdca 100644 (file)
@@ -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
index 9556d89..7faf4f0 100644 (file)
@@ -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 &lt;http://en.wikipedia.org/wiki/Formal_Public_Identifier&gt;", 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<year>\d{4})', include(self.timespan_patterns('year'))),
+                       url(r'^(?P<year>\d{4})/(?P<month>\d{2})', include(self.timespan_patterns('month'))),
+                       url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', include(self.timespan_patterns('day'))),
+                       #url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<hour>\d{1,2})', include(self.timespan_patterns('hour'))),
+                       url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)', self.event_detail_view, name="event_detail"),
                        
-                       response = HttpResponse(mimetype=feed.mime_type)
-                       feed.write(response, 'utf-8')
+                       url(r'^%s/(?P<username>[^/]+)' % 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.
-                               # <http://blog.thescoop.org/archives/2007/07/31/django-ical-and-vobject/>
-                               # 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<slug>[\w-]+)' % self.location_permalink_base, ...)
+                       url(r'^%s/(?P<app_label>\w+)/(?P<model>\w+)/(?P<pk>[^/]+)' % 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<tag_slugs>[-\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<tag_slugs>[-\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