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