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
7 from django.core.validators import RegexValidator
8 from django.db import models
9 from django.http import HttpResponse
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.models.base import Tag, Entity
14 from philo.models.fields import TemplateField
15 from philo.utils import ContentTypeRegistryLimiter
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)
42 def __unicode__(self):
46 register_location_model(Location)
49 class TimedModel(models.Model):
50 start_date = models.DateField(help_text="YYYY-MM-DD")
51 start_time = models.TimeField(blank=True, null=True, help_text="HH:MM:SS - 24 hour clock")
52 end_date = models.DateField()
53 end_time = models.TimeField(blank=True, null=True)
56 return self.start_time is None and self.end_time is None
59 if bool(self.start_time) != bool(self.end_time):
60 raise ValidationError("A %s must have either a start time and an end time or neither.")
62 if self.start_date > self.end_date or self.start_date == self.end_date and self.start_time > self.end_time:
63 raise ValidationError("A %s cannot end before it starts." % self.__class__.__name__)
66 return self.start_date
75 class Event(Entity, TimedModel):
76 name = models.CharField(max_length=255)
77 slug = models.SlugField(max_length=255)
79 location_content_type = models.ForeignKey(ContentType, limit_choices_to=location_content_type_limiter, blank=True, null=True)
80 location_pk = models.TextField(blank=True)
81 location = GenericForeignKey('location_content_type', 'location_pk')
83 description = TemplateField()
85 tags = models.ManyToManyField(Tag, blank=True, null=True)
87 parent_event = models.ForeignKey('self', blank=True, null=True)
89 owner = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'))
91 created = models.DateTimeField(auto_now_add=True)
92 last_modified = models.DateTimeField(auto_now=True)
93 uuid = models.TextField() # Format?
96 class Calendar(Entity):
97 name = models.CharField(max_length=100)
98 slug = models.SlugField(max_length=100)
99 description = models.TextField(blank=True)
100 #slug = models.SlugField(max_length=255, unique=True)
101 events = models.ManyToManyField(Event, related_name='calendars')
103 # TODO: Can we auto-generate this on save based on site id and calendar name and settings language?
104 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)])
107 class ICalendarFeedView(FeedView):
108 calendar = models.ForeignKey(Calendar)
110 item_context_var = "events"
111 object_attr = "calendar"
113 def get_reverse_params(self, obj):
114 return 'feed', [], {}
117 def urlpatterns(self):
119 url(r'^$', self.feed_view('get_all_events', 'feed'), name='feed')
122 def feed_view(self, get_items_attr, reverse_name):
124 Returns a view function that renders a list of items as a feed.
126 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
128 def inner(request, extra_context=None, *args, **kwargs):
129 obj = self.get_object(request, *args, **kwargs)
130 feed = self.get_feed(obj, request, reverse_name)
131 items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
132 self.populate_feed(feed, items, request)
134 response = HttpResponse(mimetype=feed.mime_type)
135 feed.write(response, 'utf-8')
137 if FEEDS[self.feed_type] == ICalendarFeed:
138 # Add some extra information to the response for iCalendar readers.
139 # <http://blog.thescoop.org/archives/2007/07/31/django-ical-and-vobject/>
140 # Also, __get_dynamic_attr is protected by python - mangled. Should it
142 filename = self._FeedView__get_dynamic_attr('filename', obj)
143 response['Filename'] = filename
144 response['Content-Disposition'] = 'attachment; filename=%s' % filename
149 def get_event_queryset(self):
150 return self.calendar.events.all()
152 def get_all_events(self, request, extra_context=None):
153 return self.get_event_queryset(), extra_context
155 def title(self, obj):
159 # Link is ignored anyway...
162 def filename(self, obj):
163 return "%s.ics" % obj.slug
165 def feed_guid(self, obj):
166 # Is this correct? Should I have a different id for different subfeeds?
169 def description(self, obj):
170 return obj.description
172 # Would this be meaningful? I think it's just ignored anyway, for ical format.
173 #def categories(self, obj):
174 # event_ct = ContentType.objects.get_for_model(Event)
175 # event_pks = obj.events.values_list('pk')
176 # return [tag.name for tag in Tag.objects.filter(content_type=event_ct, object_id__in=event_pks)]
178 def item_title(self, item):
181 def item_description(self, item):
182 return item.description
184 def item_link(self, item):
185 return self.reverse(item)
187 def item_guid(self, item):
190 def item_author_name(self, item):
192 return item.owner.get_full_name()
194 def item_author_email(self, item):
195 return getattr(item.owner, 'email', None) or None
197 def item_pubdate(self, item):
200 def item_categories(self, item):
201 return [tag.name for tag in item.tags.all()]
203 def item_extra_kwargs(self, item):
205 'start': item.get_start(),
206 'end': item.get_end(),
207 'last_modified': item.last_modified,
208 # Is forcing unicode enough, or should we look for a "custom method"?
209 'location': force_unicode(item.location),
213 verbose_name = "iCalendar view"
215 field = ICalendarFeedView._meta.get_field('feed_type')
216 field._choices += ((ICALENDAR, 'iCalendar'),)
217 field.default = ICALENDAR