Modified the get_start and get_end methods to return datetimes if times are specified.
[philo.git] / contrib / julian / models.py
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.db.models.query import QuerySet
10 from django.http import HttpResponse, Http404
11 from django.utils.encoding import force_unicode
12 from philo.contrib.julian.feedgenerator import ICalendarFeed
13 from philo.contrib.penfield.models import FeedView, FEEDS
14 from philo.exceptions import ViewCanNotProvideSubpath
15 from philo.models import Tag, Entity, Page, TemplateField
16 from philo.utils import ContentTypeRegistryLimiter
17 import re, datetime, calendar
18
19
20 # TODO: Could this regex more closely match the Formal Public Identifier spec?
21 # http://xml.coverpages.org/tauber-fpi.html
22 FPI_REGEX = re.compile(r"(|\+//|-//)[^/]+//[^/]+//[A-Z]{2}")
23
24
25 ICALENDAR = ICalendarFeed.mime_type
26 FEEDS[ICALENDAR] = ICalendarFeed
27
28
29 location_content_type_limiter = ContentTypeRegistryLimiter()
30
31
32 def register_location_model(model):
33         location_content_type_limiter.register_class(model)
34
35
36 def unregister_location_model(model):
37         location_content_type_limiter.unregister_class(model)
38
39
40 class Location(Entity):
41         name = models.CharField(max_length=255)
42         slug = models.SlugField(max_length=255, unique=True)
43         
44         def __unicode__(self):
45                 return self.name
46
47
48 register_location_model(Location)
49
50
51 class TimedModel(models.Model):
52         start_date = models.DateField(help_text="YYYY-MM-DD")
53         start_time = models.TimeField(blank=True, null=True, help_text="HH:MM:SS - 24 hour clock")
54         end_date = models.DateField()
55         end_time = models.TimeField(blank=True, null=True)
56         
57         def is_all_day(self):
58                 return self.start_time is None and self.end_time is None
59         
60         def clean(self):
61                 if bool(self.start_time) != bool(self.end_time):
62                         raise ValidationError("A %s must have either a start time and an end time or neither.")
63                 
64                 if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time:
65                         raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
66         
67         def get_start(self):
68                 return datetime.datetime.combine(self.start_date, self.start_time) if self.start_time else self.start_date
69         
70         def get_end(self):
71                 return datetime.datetime.combine(self.end_date, self.end_time) if self.end_time else self.end_date
72         
73         class Meta:
74                 abstract = True
75
76
77 class EventManager(models.Manager):
78         def get_query_set(self):
79                 return EventQuerySet(self.model)
80
81 class EventQuerySet(QuerySet):
82         def upcoming(self):
83                 return self.filter(start_date__gte=datetime.date.today())
84         def current(self):
85                 return self.filter(start_date__lte=datetime.date.today(), end_date__gte=datetime.date.today())
86         def single_day(self):
87                 return self.filter(start_date__exact=models.F('end_date'))
88         def multiday(self):
89                 return self.exclude(start_date__exact=models.F('end_date'))
90
91 class Event(Entity, TimedModel):
92         name = models.CharField(max_length=255)
93         slug = models.SlugField(max_length=255, unique_for_date='start_date')
94         
95         location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
96         location_pk = models.TextField(blank=True)
97         location = GenericForeignKey('location_content_type', 'location_pk')
98         
99         description = TemplateField()
100         
101         tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
102         
103         parent_event = models.ForeignKey('self', blank=True, null=True)
104         
105         # TODO: "User module"
106         owner = models.ForeignKey(User, related_name='owned_events')
107         
108         created = models.DateTimeField(auto_now_add=True)
109         last_modified = models.DateTimeField(auto_now=True)
110         uuid = models.TextField() # Format?
111         
112         objects = EventManager()
113         
114         def __unicode__(self):
115                 return self.name
116
117
118 class Calendar(Entity):
119         name = models.CharField(max_length=100)
120         slug = models.SlugField(max_length=100)
121         description = models.TextField(blank=True)
122         events = models.ManyToManyField(Event, related_name='calendars')
123         
124         # TODO: Can we auto-generate this on save based on site id and calendar name and settings language?
125         uuid = models.TextField("Calendar UUID", unique=True, help_text="Should conform to Formal Public Identifier format. See <a href='http://en.wikipedia.org/wiki/Formal_Public_Identifier'>Wikipedia</a>.", validators=[RegexValidator(FPI_REGEX)])
126         
127         def __unicode__(self):
128                 return self.name
129
130
131 class CalendarView(FeedView):
132         calendar = models.ForeignKey(Calendar)
133         index_page = models.ForeignKey(Page, related_name="calendar_index_related")
134         event_detail_page = models.ForeignKey(Page, related_name="calendar_detail_related")
135         
136         timespan_page = models.ForeignKey(Page, related_name="calendar_timespan_related", blank=True, null=True)
137         tag_page = models.ForeignKey(Page, related_name="calendar_tag_related", blank=True, null=True)
138         location_page = models.ForeignKey(Page, related_name="calendar_location_related", blank=True, null=True)
139         owner_page = models.ForeignKey(Page, related_name="calendar_owner_related", blank=True, null=True)
140         
141         tag_archive_page = models.ForeignKey(Page, related_name="calendar_tag_archive_related", blank=True, null=True)
142         location_archive_page = models.ForeignKey(Page, related_name="calendar_location_archive_related", blank=True, null=True)
143         owner_archive_page = models.ForeignKey(Page, related_name="calendar_owner_archive_related", blank=True, null=True)
144         
145         tag_permalink_base = models.CharField(max_length=30, default='tags')
146         owner_permalink_base = models.CharField(max_length=30, default='owners')
147         location_permalink_base = models.CharField(max_length=30, default='locations')
148         events_per_page = models.PositiveIntegerField(blank=True, null=True)
149         
150         item_context_var = "events"
151         object_attr = "calendar"
152         
153         def get_reverse_params(self, obj):
154                 if isinstance(obj, User):
155                         return 'events_for_user', [], {'username': obj.username}
156                 elif isinstance(obj, Event):
157                         return 'event_detail', [], {
158                                 'year': str(obj.start_date.year).zfill(4),
159                                 'month': str(obj.start_date.month).zfill(2),
160                                 'day': str(obj.start_date.day).zfill(2),
161                                 'slug': obj.slug
162                         }
163                 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
164                         if isinstance(obj, Tag):
165                                 obj = [obj]
166                         return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
167                 raise ViewCanNotProvideSubpath
168         
169         def timespan_patterns(self, pattern, timespan_name):
170                 return self.feed_patterns(pattern, 'get_events_by_timespan', 'timespan_page', "events_by_%s" % timespan_name)
171         
172         @property
173         def urlpatterns(self):
174                 # Perhaps timespans should be done with GET parameters? Or two /-separated
175                 # date slugs? (e.g. 2010-02-1/2010-02-2) or a start and duration?
176                 # (e.g. 2010-02-01/week/ or ?d=2010-02-01&l=week)
177                 urlpatterns = self.feed_patterns(r'^', 'get_all_events', 'index_page', 'index') + \
178                         self.timespan_patterns(r'^(?P<year>\d{4})', 'year') + \
179                         self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'month') + \
180                         self.timespan_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'day') + \
181                         self.feed_patterns(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, 'get_events_by_owner', 'owner_page', 'events_by_user') + \
182                         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') + \
183                         self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_events_by_tag', 'tag_page', 'events_by_tag') + \
184                         patterns('',
185                                 url(r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$', self.event_detail_view, name="event_detail"),
186                         )
187                         
188                         # Some sort of shortcut for a location would be useful. This could be on a per-calendar
189                         # or per-calendar-view basis.
190                         #url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
191                 
192                 if self.tag_archive_page:
193                         urlpatterns += patterns('',
194                                 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
195                         )
196                 
197                 if self.owner_archive_page:
198                         urlpatterns += patterns('',
199                                 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
200                         )
201                 
202                 if self.location_archive_page:
203                         urlpatterns += patterns('',
204                                 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
205                         )
206                 return urlpatterns
207         
208         # Basic QuerySet fetchers.
209         def get_event_queryset(self):
210                 return self.calendar.events.all()
211         
212         def get_timespan_queryset(self, year, month=None, day=None):
213                 qs = self.get_event_queryset()
214                 # See python documentation for the min/max values.
215                 if year and month and day:
216                         year, month, day = int(year), int(month), int(day)
217                         start_datetime = datetime.datetime(year, month, day, 0, 0)
218                         end_datetime = datetime.datetime(year, month, day, 23, 59)
219                 elif year and month:
220                         year, month = int(year), int(month)
221                         start_datetime = datetime.datetime(year, month, 1, 0, 0)
222                         end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
223                 else:
224                         year = int(year)
225                         start_datetime = datetime.datetime(year, 1, 1, 0, 0)
226                         end_datetime = datetime.datetime(year, 12, 31, 23, 59)
227                 
228                 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)
229         
230         def get_tag_queryset(self):
231                 return Tag.objects.filter(events__calendars=self.calendar).distinct()
232         
233         def get_location_querysets(self):
234                 # Potential bottleneck?
235                 location_map = {}
236                 locations = Event.objects.values_list('location_content_type', 'location_pk')
237                 
238                 for ct, pk in locations:
239                         location_map.setdefault(ct, []).append(pk)
240                 
241                 location_cts = ContentType.objects.in_bulk(location_map.keys())
242                 location_querysets = {}
243                 
244                 for ct_pk, pks in location_map.items():
245                         ct = location_cts[ct_pk]
246                         location_querysets[ct] = ct.model_class()._default_manager.filter(pk__in=pks)
247                 
248                 return location_querysets
249         
250         def get_owner_queryset(self):
251                 return User.objects.filter(owned_events__calendars=self.calendar).distinct()
252         
253         # Event QuerySet parsers for a request/args/kwargs
254         def get_all_events(self, request, extra_context=None):
255                 return self.get_event_queryset(), extra_context
256         
257         def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
258                 context = extra_context or {}
259                 context.update({
260                         'year': year,
261                         'month': month,
262                         'day': day
263                 })
264                 return self.get_timespan_queryset(year, month, day), context
265         
266         def get_events_by_owner(self, request, username, extra_context=None):
267                 try:
268                         owner = self.get_owner_queryset().get(username=username)
269                 except User.DoesNotExist:
270                         raise Http404
271                 
272                 qs = self.get_event_queryset().filter(owner=owner)
273                 context = extra_context or {}
274                 context.update({
275                         'owner': owner
276                 })
277                 return qs, context
278         
279         def get_events_by_tag(self, request, tag_slugs, extra_context=None):
280                 tag_slugs = tag_slugs.replace('+', '/').split('/')
281                 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
282                 
283                 if not tags:
284                         raise Http404
285                 
286                 # Raise a 404 on an incorrect slug.
287                 found_slugs = [tag.slug for tag in tags]
288                 for slug in tag_slugs:
289                         if slug and slug not in found_slugs:
290                                 raise Http404
291
292                 events = self.get_event_queryset()
293                 for tag in tags:
294                         events = events.filter(tags=tag)
295                 
296                 context = extra_context or {}
297                 context.update({'tags': tags})
298                 
299                 return events, context
300         
301         def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
302                 try:
303                         ct = ContentType.objects.get(app_label=app_label, model=model)
304                         location = ct.model_class()._default_manager.get(pk=pk)
305                 except ObjectDoesNotExist:
306                         raise Http404
307                 
308                 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
309                 
310                 context = extra_context or {}
311                 context.update({
312                         'location': location
313                 })
314                 return events, context
315         
316         # Detail View.
317         def event_detail_view(self, request, year, month, day, slug, extra_context=None):
318                 try:
319                         event = Event.objects.select_related('parent_event').get(start_date__year=year, start_date__month=month, start_date__day=day, slug=slug)
320                 except Event.DoesNotExist:
321                         raise Http404
322                 
323                 context = self.get_context()
324                 context.update(extra_context or {})
325                 context.update({
326                         'event': event
327                 })
328                 return self.event_detail_page.render_to_response(request, extra_context=context)
329         
330         # Archive Views.
331         def tag_archive_view(self, request, extra_context=None):
332                 tags = self.get_tag_queryset()
333                 context = self.get_context()
334                 context.update(extra_context or {})
335                 context.update({
336                         'tags': tags
337                 })
338                 return self.tag_archive_page.render_to_response(request, extra_context=context)
339         
340         def location_archive_view(self, request, extra_context=None):
341                 # What datastructure should locations be?
342                 locations = self.get_location_querysets()
343                 context = self.get_context()
344                 context.update(extra_context or {})
345                 context.update({
346                         'locations': locations
347                 })
348                 return self.location_archive_page.render_to_response(request, extra_context=context)
349         
350         def owner_archive_view(self, request, extra_context=None):
351                 owners = self.get_owner_queryset()
352                 context = self.get_context()
353                 context.update(extra_context or {})
354                 context.update({
355                         'owners': owners
356                 })
357                 return self.owner_archive_page.render_to_response(request, extra_context=context)
358         
359         # Process page items
360         def process_page_items(self, request, items):
361                 if self.events_per_page:
362                         page_num = request.GET.get('page', 1)
363                         paginator, paginated_page, items = paginate(items, self.events_per_page, page_num)
364                         item_context = {
365                                 'paginator': paginator,
366                                 'paginated_page': paginated_page,
367                                 self.item_context_var: items
368                         }
369                 else:
370                         item_context = {
371                                 self.item_context_var: items
372                         }
373                 return items, item_context
374         
375         # Feed information hooks
376         def title(self, obj):
377                 return obj.name
378         
379         def link(self, obj):
380                 # Link is ignored anyway...
381                 return ""
382         
383         def feed_guid(self, obj):
384                 # Is this correct? Should I have a different id for different subfeeds?
385                 return obj.uuid
386         
387         def description(self, obj):
388                 return obj.description
389         
390         def feed_extra_kwargs(self, obj):
391                 return {'filename': "%s.ics" % obj.slug}
392         
393         def item_title(self, item):
394                 return item.name
395         
396         def item_description(self, item):
397                 return item.description
398         
399         def item_link(self, item):
400                 return self.reverse(item)
401         
402         def item_guid(self, item):
403                 return item.uuid
404         
405         def item_author_name(self, item):
406                 if item.owner:
407                         return item.owner.get_full_name()
408         
409         def item_author_email(self, item):
410                 return getattr(item.owner, 'email', None) or None
411         
412         def item_pubdate(self, item):
413                 return item.created
414         
415         def item_categories(self, item):
416                 return [tag.name for tag in item.tags.all()]
417         
418         def item_extra_kwargs(self, item):
419                 return {
420                         'start': item.get_start(),
421                         'end': item.get_end(),
422                         'last_modified': item.last_modified,
423                         # Is forcing unicode enough, or should we look for a "custom method"?
424                         'location': force_unicode(item.location),
425                 }
426         
427         def __unicode__(self):
428                 return u"%s for %s" % (self.__class__.__name__, self.calendar)
429
430 field = CalendarView._meta.get_field('feed_type')
431 field._choices += ((ICALENDAR, 'iCalendar'),)
432 field.default = ICALENDAR