1 from django.conf import settings
2 from django.conf.urls.defaults import url, patterns, include
3 from django.contrib.auth.models import User
4 from django.contrib.contenttypes.generic import GenericForeignKey
5 from django.contrib.contenttypes.models import ContentType
6 from django.core.exceptions import ValidationError, ObjectDoesNotExist
7 from django.core.validators import RegexValidator
8 from django.db import models
9 from django.http import HttpResponse, Http404
10 from django.utils.encoding import force_unicode
11 from philo.contrib.julian.feedgenerator import ICalendarFeed
12 from philo.contrib.penfield.models import FeedView, FEEDS
13 from philo.exceptions import ViewCanNotProvideSubpath
14 from philo.models import Tag, Entity, Page, TemplateField
15 from philo.utils import ContentTypeRegistryLimiter
16 import re, datetime, calendar
19 # TODO: Could this regex more closely match the Formal Public Identifier spec?
20 # http://xml.coverpages.org/tauber-fpi.html
21 FPI_REGEX = re.compile(r"(|\+//|-//)[^/]+//[^/]+//[A-Z]{2}")
24 ICALENDAR = ICalendarFeed.mime_type
25 FEEDS[ICALENDAR] = ICalendarFeed
28 location_content_type_limiter = ContentTypeRegistryLimiter()
31 def register_location_model(model):
32 location_content_type_limiter.register_class(model)
35 def unregister_location_model(model):
36 location_content_type_limiter.unregister_class(model)
39 class Location(Entity):
40 name = models.CharField(max_length=255)
41 slug = models.SlugField(max_length=255, unique=True)
43 def __unicode__(self):
47 register_location_model(Location)
50 class TimedModel(models.Model):
51 start_date = models.DateField(help_text="YYYY-MM-DD")
52 start_time = models.TimeField(blank=True, null=True, help_text="HH:MM:SS - 24 hour clock")
53 end_date = models.DateField()
54 end_time = models.TimeField(blank=True, null=True)
57 return self.start_time is None and self.end_time is None
60 if bool(self.start_time) != bool(self.end_time):
61 raise ValidationError("A %s must have either a start time and an end time or neither.")
63 if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time:
64 raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
67 return self.start_date
76 class Event(Entity, TimedModel):
77 name = models.CharField(max_length=255)
78 slug = models.SlugField(max_length=255, unique_for_date='created')
80 location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
81 location_pk = models.TextField(blank=True)
82 location = GenericForeignKey('location_content_type', 'location_pk')
84 description = TemplateField()
86 tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
88 parent_event = models.ForeignKey('self', blank=True, null=True)
91 owner = models.ForeignKey(User)
93 created = models.DateTimeField(auto_now_add=True)
94 last_modified = models.DateTimeField(auto_now=True)
95 uuid = models.TextField() # Format?
97 def __unicode__(self):
101 class Calendar(Entity):
102 name = models.CharField(max_length=100)
103 slug = models.SlugField(max_length=100)
104 description = models.TextField(blank=True)
105 events = models.ManyToManyField(Event, related_name='calendars')
107 # TODO: Can we auto-generate this on save based on site id and calendar name and settings language?
108 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)])
111 class CalendarView(FeedView):
112 calendar = models.ForeignKey(Calendar)
113 index_page = models.ForeignKey(Page, related_name="calendar_index_related")
114 timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related")
115 event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
116 tag_page = models.ForeignKey(Page, related_name="calendar_tag_related")
117 location_page = models.ForeignKey(Page, related_name="calendar_location_related")
118 owner_page = models.ForeignKey(Page, related_name="calendar_owner_related")
120 tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
121 location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
122 owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
124 tag_permalink_base = models.CharField(max_length=30, default='tags')
125 owner_permalink_base = models.CharField(max_length=30, default='owner')
126 location_permalink_base = models.CharField(max_length=30, default='location')
128 item_context_var = "events"
129 object_attr = "calendar"
131 def get_reverse_params(self, obj):
132 if isinstance(obj, User):
133 return 'events_for_user', [], {'username': obj.username}
134 elif isinstance(obj, Event):
135 return 'event_detail', [], {
136 'year': obj.start_date.year,
137 'month': obj.start_date.month,
138 'day': obj.start_date.day,
141 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
142 if isinstance(obj, Tag):
144 return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
145 raise ViewCanNotProvideSubpath
147 def timespan_patterns(self, timespan_name):
148 urlpatterns = patterns('',
149 ) + self.feed_patterns('get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
153 def urlpatterns(self):
154 urlpatterns = patterns('',
155 url(r'^', include(self.feed_patterns('get_all_events', 'index_page', 'index'))),
157 url(r'^(?P<year>\d{4})', include(self.timespan_patterns('year'))),
158 url(r'^(?P<year>\d{4})/(?P<month>\d{2})', include(self.timespan_patterns('month'))),
159 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', include(self.timespan_patterns('day'))),
160 #url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<hour>\d{1,2})', include(self.timespan_patterns('hour'))),
161 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)', self.event_detail_view, name="event_detail"),
163 url(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, include(self.feed_patterns('get_events_by_user', 'owner_page', 'events_by_user'))),
165 # Some sort of shortcut for a location would be useful. This could be on a per-calendar
166 # or per-calendar-view basis.
167 #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
168 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'))),
171 if self.feeds_enabled:
172 urlpatterns += patterns('',
173 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'),
175 urlpatterns += patterns('',
176 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')
179 if self.tag_archive_page:
180 urlpatterns += patterns('',
181 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
184 if self.owner_archive_page:
185 urlpatterns += patterns('',
186 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
189 if self.owner_archive_page:
190 urlpatterns += patterns('',
191 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
195 def get_event_queryset(self):
196 return self.calendar.events.all()
198 def get_timespan_queryset(self, year, month=None, day=None):
199 qs = self.get_event_queryset()
200 # See python documentation for the min/max values.
201 if year and month and day:
202 start_datetime = datetime.datetime(year, month, day, 0, 0)
203 end_datetime = datetime.datetime(year, month, day, 23, 59)
205 start_datetime = datetime.datetime(year, month, 1, 0, 0)
206 end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
208 start_datetime = datetime.datetime(year, 1, 1, 0, 0)
209 end_datetime = datetime.datetime(year, 12, 31, 23, 59)
211 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())
213 def get_tag_queryset(self):
214 return Tag.objects.filter(events__calendar=self.calendar).distinct()
217 def get_all_events(self, request, extra_context=None):
218 return self.get_event_queryset(), extra_context
220 def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
221 context = extra_context or {}
227 return self.get_timespan_queryset(year, month, day), context
229 def get_events_by_user(self, request, username, extra_context=None):
231 user = User.objects.get(username)
232 except User.DoesNotExist:
235 qs = self.event_queryset().filter(owner=user)
236 context = extra_context or {}
242 def get_events_by_tag(self, request, tag_slugs, extra_context=None):
243 tag_slugs = tag_slugs.replace('+', '/').split('/')
244 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
249 # Raise a 404 on an incorrect slug.
250 found_slugs = [tag.slug for tag in tags]
251 for slug in tag_slugs:
252 if slug and slug not in found_slugs:
255 events = self.get_event_queryset()
257 events = events.filter(tags=tag)
259 context = extra_context or {}
260 context.update({'tags': tags})
262 return events, context
264 def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
266 ct = ContentType.objects.get(app_label=app_label, model=model)
267 location = ct.model_class()._default_manager.get(pk=pk)
268 except ObjectDoesNotExist:
271 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
273 context = extra_context or {}
277 return events, context
279 # Detail View. TODO: fill this out.
280 def event_detail_view(self, request, year, month, day, slug, extra_context=None):
283 # Archive Views. TODO: fill these out.
284 def tag_archive_view(self, request, extra_context=None):
287 def location_archive_view(self, request, extra_context=None):
290 def owner_archive_view(self, request, extra_context=None):
293 # Feed information hooks
294 def title(self, obj):
298 # Link is ignored anyway...
301 def feed_guid(self, obj):
302 # Is this correct? Should I have a different id for different subfeeds?
305 def description(self, obj):
306 return obj.description
308 def feed_extra_kwargs(self, obj):
309 return {'filename': "%s.ics" % obj.slug}
311 def item_title(self, item):
314 def item_description(self, item):
315 return item.description
317 def item_link(self, item):
318 return self.reverse(item)
320 def item_guid(self, item):
323 def item_author_name(self, item):
325 return item.owner.get_full_name()
327 def item_author_email(self, item):
328 return getattr(item.owner, 'email', None) or None
330 def item_pubdate(self, item):
333 def item_categories(self, item):
334 return [tag.name for tag in item.tags.all()]
336 def item_extra_kwargs(self, item):
338 'start': item.get_start(),
339 'end': item.get_end(),
340 'last_modified': item.last_modified,
341 # Is forcing unicode enough, or should we look for a "custom method"?
342 'location': force_unicode(item.location),
345 def __unicode__(self):
346 return u"%s for %s" % (self.__class__.__name__, self.calendar)
348 field = CalendarView._meta.get_field('feed_type')
349 field._choices += ((ICALENDAR, 'iCalendar'),)
350 field.default = ICALENDAR