Reinstated EventAdmin fieldsets and added some options. Filled out the page requireme...
[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.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
17
18
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}")
22
23
24 ICALENDAR = ICalendarFeed.mime_type
25 FEEDS[ICALENDAR] = ICalendarFeed
26
27
28 location_content_type_limiter = ContentTypeRegistryLimiter()
29
30
31 def register_location_model(model):
32         location_content_type_limiter.register_class(model)
33
34
35 def unregister_location_model(model):
36         location_content_type_limiter.unregister_class(model)
37
38
39 class Location(Entity):
40         name = models.CharField(max_length=255)
41         slug = models.SlugField(max_length=255, unique=True)
42         
43         def __unicode__(self):
44                 return self.name
45
46
47 register_location_model(Location)
48
49
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)
55         
56         def is_all_day(self):
57                 return self.start_time is None and self.end_time is None
58         
59         def clean(self):
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.")
62                 
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__)
65         
66         def get_start(self):
67                 return self.start_date
68         
69         def get_end(self):
70                 return self.end_date
71         
72         class Meta:
73                 abstract = True
74
75
76 class Event(Entity, TimedModel):
77         name = models.CharField(max_length=255)
78         slug = models.SlugField(max_length=255, unique_for_date='created')
79         
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')
83         
84         description = TemplateField()
85         
86         tags = models.ManyToManyField(Tag, related_name='events', blank=True, null=True)
87         
88         parent_event = models.ForeignKey('self', blank=True, null=True)
89         
90         # TODO: "User module"
91         owner = models.ForeignKey(User)
92         
93         created = models.DateTimeField(auto_now_add=True)
94         last_modified = models.DateTimeField(auto_now=True)
95         uuid = models.TextField() # Format?
96         
97         def __unicode__(self):
98                 return self.name
99
100
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')
106         
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)])
109
110
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")
119         
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)
123         
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')
127         
128         item_context_var = "events"
129         object_attr = "calendar"
130         
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,
139                                 'slug': obj.slug
140                         }
141                 elif isinstance(obj, Tag) or isinstance(obj, models.query.QuerySet) and obj.model == Tag:
142                         if isinstance(obj, Tag):
143                                 obj = [obj]
144                         return 'entries_by_tag', [], {'tag_slugs': '/'.join(obj)}
145                 raise ViewCanNotProvideSubpath
146         
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)
150                 return urlpatterns
151         
152         @property
153         def urlpatterns(self):
154                 urlpatterns = patterns('',
155                         url(r'^', include(self.feed_patterns('get_all_events', 'index_page', 'index'))),
156                         
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"),
162                         
163                         url(r'^%s/(?P<username>[^/]+)' % self.owner_permalink_base, include(self.feed_patterns('get_events_by_user', 'owner_page', 'events_by_user'))),
164                         
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'))),
169                 )
170                 
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'),
174                         )
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')
177                 )
178                 
179                 if self.tag_archive_page:
180                         urlpatterns += patterns('',
181                                 url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
182                         )
183                 
184                 if self.owner_archive_page:
185                         urlpatterns += patterns('',
186                                 url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
187                         )
188                 
189                 if self.owner_archive_page:
190                         urlpatterns += patterns('',
191                                 url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
192                         )
193                 return urlpatterns
194         
195         def get_event_queryset(self):
196                 return self.calendar.events.all()
197         
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)
204                 elif year and month:
205                         start_datetime = datetime.datetime(year, month, 1, 0, 0)
206                         end_datetime = datetime.datetime(year, month, calendar.monthrange(year, month)[1], 23, 59)
207                 else:
208                         start_datetime = datetime.datetime(year, 1, 1, 0, 0)
209                         end_datetime = datetime.datetime(year, 12, 31, 23, 59)
210                 
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())
212         
213         def get_tag_queryset(self):
214                 return Tag.objects.filter(events__calendar=self.calendar).distinct()
215         
216         # Event fetchers.
217         def get_all_events(self, request, extra_context=None):
218                 return self.get_event_queryset(), extra_context
219         
220         def get_events_by_timespan(self, request, year, month=None, day=None, extra_context=None):
221                 context = extra_context or {}
222                 context.update({
223                         'year': year,
224                         'month': month,
225                         'day': day
226                 })
227                 return self.get_timespan_queryset(year, month, day), context
228         
229         def get_events_by_user(self, request, username, extra_context=None):
230                 try:
231                         user = User.objects.get(username)
232                 except User.DoesNotExist:
233                         raise Http404
234                 
235                 qs = self.event_queryset().filter(owner=user)
236                 context = extra_context or {}
237                 context.update({
238                         'user': user
239                 })
240                 return qs, context
241         
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)
245                 
246                 if not tags:
247                         raise Http404
248                 
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:
253                                 raise Http404
254
255                 events = self.get_event_queryset()
256                 for tag in tags:
257                         events = events.filter(tags=tag)
258                 
259                 context = extra_context or {}
260                 context.update({'tags': tags})
261                 
262                 return events, context
263         
264         def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
265                 try:
266                         ct = ContentType.objects.get(app_label=app_label, model=model)
267                         location = ct.model_class()._default_manager.get(pk=pk)
268                 except ObjectDoesNotExist:
269                         raise Http404
270                 
271                 events = self.get_event_queryset().filter(location_content_type=ct, location_pk=location.pk)
272                 
273                 context = extra_context or {}
274                 context.update({
275                         'location': location
276                 })
277                 return events, context
278         
279         # Detail View. TODO: fill this out.
280         def event_detail_view(self, request, year, month, day, slug, extra_context=None):
281                 pass
282         
283         # Archive Views. TODO: fill these out.
284         def tag_archive_view(self, request, extra_context=None):
285                 pass
286         
287         def location_archive_view(self, request, extra_context=None):
288                 pass
289         
290         def owner_archive_view(self, request, extra_context=None):
291                 pass
292         
293         # Feed information hooks
294         def title(self, obj):
295                 return obj.name
296         
297         def link(self, obj):
298                 # Link is ignored anyway...
299                 return ""
300         
301         def feed_guid(self, obj):
302                 # Is this correct? Should I have a different id for different subfeeds?
303                 return obj.uuid
304         
305         def description(self, obj):
306                 return obj.description
307         
308         def feed_extra_kwargs(self, obj):
309                 return {'filename': "%s.ics" % obj.slug}
310         
311         def item_title(self, item):
312                 return item.name
313         
314         def item_description(self, item):
315                 return item.description
316         
317         def item_link(self, item):
318                 return self.reverse(item)
319         
320         def item_guid(self, item):
321                 return item.uuid
322         
323         def item_author_name(self, item):
324                 if item.owner:
325                         return item.owner.get_full_name()
326         
327         def item_author_email(self, item):
328                 return getattr(item.owner, 'email', None) or None
329         
330         def item_pubdate(self, item):
331                 return item.created
332         
333         def item_categories(self, item):
334                 return [tag.name for tag in item.tags.all()]
335         
336         def item_extra_kwargs(self, item):
337                 return {
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),
343                 }
344         
345         def __unicode__(self):
346                 return u"%s for %s" % (self.__class__.__name__, self.calendar)
347
348 field = CalendarView._meta.get_field('feed_type')
349 field._choices += ((ICALENDAR, 'iCalendar'),)
350 field.default = ICALENDAR