Merge branch 'master' of git://github.com/melinath/philo
[philo.git] / contrib / julian / models.py
index 7faf4f0..5c49c7e 100644 (file)
@@ -3,9 +3,11 @@ 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.contrib.auth.models import User
 from django.contrib.contenttypes.generic import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.sites.models import Site
 from django.core.exceptions import ValidationError, ObjectDoesNotExist
 from django.core.validators import RegexValidator
 from django.db import models
 from django.core.exceptions import ValidationError, ObjectDoesNotExist
 from django.core.validators import RegexValidator
 from django.db import models
+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 django.http import HttpResponse, Http404
 from django.utils.encoding import force_unicode
 from philo.contrib.julian.feedgenerator import ICalendarFeed
@@ -13,16 +15,29 @@ 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
 from philo.exceptions import ViewCanNotProvideSubpath
 from philo.models import Tag, Entity, Page, TemplateField
 from philo.utils import ContentTypeRegistryLimiter
-import re, datetime, calendar
+import datetime, calendar
 
 
 
 
-# TODO: Could this regex more closely match the Formal Public Identifier spec?
-# http://xml.coverpages.org/tauber-fpi.html
-FPI_REGEX = re.compile(r"(|\+//|-//)[^/]+//[^/]+//[A-Z]{2}")
+__all__ = ('register_location_model', 'unregister_location_model', 'Location', 'TimedModel', 'Event', 'Calendar', 'CalendarView',)
 
 
 ICALENDAR = ICalendarFeed.mime_type
 FEEDS[ICALENDAR] = ICalendarFeed
 
 
 ICALENDAR = ICalendarFeed.mime_type
 FEEDS[ICALENDAR] = ICalendarFeed
+try:
+       DEFAULT_SITE = Site.objects.get_current()
+except:
+       DEFAULT_SITE = None
+_languages = dict(settings.LANGUAGES)
+try:
+       _languages[settings.LANGUAGE_CODE]
+       DEFAULT_LANGUAGE = settings.LANGUAGE_CODE
+except KeyError:
+       try:
+               lang = settings.LANGUAGE_CODE.split('-')[0]
+               _languages[lang]
+               DEFAULT_LANGUAGE = lang
+       except KeyError:
+               DEFAULT_LANGUAGE = None
 
 
 location_content_type_limiter = ContentTypeRegistryLimiter()
 
 
 location_content_type_limiter = ContentTypeRegistryLimiter()
@@ -64,18 +79,32 @@ class TimedModel(models.Model):
                        raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
        
        def get_start(self):
                        raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
        
        def get_start(self):
-               return self.start_date
+               return datetime.datetime.combine(self.start_date, self.start_time) if self.start_time else self.start_date
        
        def get_end(self):
        
        def get_end(self):
-               return self.end_date
+               return datetime.datetime.combine(self.end_date, self.end_time) if self.end_time else self.end_date
        
        class Meta:
                abstract = True
 
 
        
        class Meta:
                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)
 class Event(Entity, TimedModel):
        name = models.CharField(max_length=255)
-       slug = models.SlugField(max_length=255, unique_for_date='created')
+       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)
        
        location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
        location_pk = models.TextField(blank=True)
@@ -88,42 +117,65 @@ class Event(Entity, TimedModel):
        parent_event = models.ForeignKey('self', blank=True, null=True)
        
        # TODO: "User module"
        parent_event = models.ForeignKey('self', blank=True, null=True)
        
        # TODO: "User module"
-       owner = models.ForeignKey(User)
+       owner = models.ForeignKey(User, related_name='owned_events')
        
        created = models.DateTimeField(auto_now_add=True)
        last_modified = models.DateTimeField(auto_now=True)
        
        created = models.DateTimeField(auto_now_add=True)
        last_modified = models.DateTimeField(auto_now=True)
-       uuid = models.TextField() # Format?
+       
+       site = models.ForeignKey(Site, default=DEFAULT_SITE)
+       
+       @property
+       def uuid(self):
+               return "%s@%s" % (self.created.isoformat(), getattr(self.site, 'domain', 'None'))
+       
+       objects = EventManager()
        
        def __unicode__(self):
                return self.name
        
        def __unicode__(self):
                return self.name
+       
+       class Meta:
+               unique_together = ('site', 'created')
 
 
 class Calendar(Entity):
        name = models.CharField(max_length=100)
        slug = models.SlugField(max_length=100)
        description = models.TextField(blank=True)
 
 
 class Calendar(Entity):
        name = models.CharField(max_length=100)
        slug = models.SlugField(max_length=100)
        description = models.TextField(blank=True)
-       events = models.ManyToManyField(Event, related_name='calendars')
+       events = models.ManyToManyField(Event, related_name='calendars', blank=True)
+       
+       site = models.ForeignKey(Site, default=DEFAULT_SITE)
+       language = models.CharField(max_length=5, choices=settings.LANGUAGES, default=DEFAULT_LANGUAGE)
+       
+       def __unicode__(self):
+               return self.name
+       
+       @property
+       def fpi(self):
+               # See http://xml.coverpages.org/tauber-fpi.html or ISO 9070:1991 for format information.
+               return "-//%s//%s//%s" % (self.site.name, self.name, self.language.split('-')[0].upper())
        
        
-       # 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 Meta:
+               unique_together = ('name', 'site', 'language')
 
 
 class CalendarView(FeedView):
        calendar = models.ForeignKey(Calendar)
        index_page = models.ForeignKey(Page, related_name="calendar_index_related")
 
 
 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")
        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")
+       
+       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')
        
        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')
+       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"
        
        item_context_var = "events"
        object_attr = "calendar"
@@ -133,9 +185,9 @@ class CalendarView(FeedView):
                        return 'events_for_user', [], {'username': obj.username}
                elif isinstance(obj, Event):
                        return 'event_detail', [], {
                        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,
+                               '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:
                                'slug': obj.slug
                        }
                elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
@@ -144,37 +196,28 @@ class CalendarView(FeedView):
                        return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
                raise ViewCanNotProvideSubpath
        
                        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
+       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):
        
        @property
        def urlpatterns(self):
-               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"),
-                       
-                       url(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, include(self.feed_patterns('get_events_by_user', 'owner_page', 'events_by_user'))),
+               # 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<year>\d{4})', 'year') + \
+                       self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'month') + \
+                       self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'day') + \
+                       self.feed_patterns(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
+                       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') + \
+                       self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
+                       patterns('',
+                               url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$', self.event_detail_view, name="event_detail"),
+                       )
                        
                        # 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, ...)
                        
                        # 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('',
                
                if self.tag_archive_page:
                        urlpatterns += patterns('',
@@ -186,12 +229,13 @@ class CalendarView(FeedView):
                                url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
                        )
                
                                url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
                        )
                
-               if self.owner_archive_page:
+               if self.location_archive_page:
                        urlpatterns += patterns('',
                                url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
                        )
                return urlpatterns
        
                        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_event_queryset(self):
                return self.calendar.events.all()
        
@@ -199,21 +243,44 @@ class CalendarView(FeedView):
                qs = self.get_event_queryset()
                # See python documentation for the min/max values.
                if year and month and day:
                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:
                        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:
                        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)
                
                        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())
+               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):
        
        def get_tag_queryset(self):
-               return Tag.objects.filter(events__calendar=self.calendar).distinct()
+               return Tag.objects.filter(events__calendars=self.calendar).distinct()
        
        
-       # Event fetchers.
+       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_all_events(self, request, extra_context=None):
                return self.get_event_queryset(), extra_context
        
@@ -226,16 +293,16 @@ class CalendarView(FeedView):
                })
                return self.get_timespan_queryset(year, month, day), context
        
                })
                return self.get_timespan_queryset(year, month, day), context
        
-       def get_events_by_user(self, request, username, extra_context=None):
+       def get_events_by_owner(self, request, username, extra_context=None):
                try:
                try:
-                       user = User.objects.get(username)
+                       owner = self.get_owner_queryset().get(username=username)
                except User.DoesNotExist:
                        raise Http404
                
                except User.DoesNotExist:
                        raise Http404
                
-               qs = self.event_queryset().filter(owner=user)
+               qs = self.get_event_queryset().filter(owner=owner)
                context = extra_context or {}
                context.update({
                context = extra_context or {}
                context.update({
-                       'user': user
+                       'owner': owner
                })
                return qs, context
        
                })
                return qs, context
        
@@ -276,19 +343,64 @@ class CalendarView(FeedView):
                })
                return events, context
        
                })
                return events, context
        
-       # Detail View. TODO: fill this out.
+       # Detail View.
        def event_detail_view(self, request, year, month, day, slug, extra_context=None):
        def event_detail_view(self, request, year, month, day, slug, extra_context=None):
-               pass
+               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. TODO: fill these out.
+       # Archive Views.
        def tag_archive_view(self, request, extra_context=None):
        def tag_archive_view(self, request, extra_context=None):
-               pass
+               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):
        
        def location_archive_view(self, request, extra_context=None):
-               pass
+               # 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):
        
        def owner_archive_view(self, request, extra_context=None):
-               pass
+               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):
        
        # Feed information hooks
        def title(self, obj):
@@ -299,8 +411,7 @@ class CalendarView(FeedView):
                return ""
        
        def feed_guid(self, obj):
                return ""
        
        def feed_guid(self, obj):
-               # Is this correct? Should I have a different id for different subfeeds?
-               return obj.uuid
+               return obj.fpi
        
        def description(self, obj):
                return obj.description
        
        def description(self, obj):
                return obj.description