Merge branch 'shipherd_hotfix'
[philo.git] / contrib / penfield / models.py
1 from django.conf import settings
2 from django.conf.urls.defaults import url, patterns, include
3 from django.contrib.sites.models import Site, RequestSite
4 from django.contrib.syndication.views import add_domain
5 from django.db import models
6 from django.http import Http404, HttpResponse
7 from django.template import RequestContext, Template as DjangoTemplate
8 from django.utils import feedgenerator, tzinfo
9 from django.utils.datastructures import SortedDict
10 from django.utils.encoding import smart_unicode, force_unicode
11 from django.utils.html import escape
12 from datetime import date, datetime
13 from philo.contrib.penfield.exceptions import HttpNotAcceptable
14 from philo.contrib.penfield.middleware import http_not_acceptable
15 from philo.contrib.penfield.validators import validate_pagination_count
16 from philo.exceptions import ViewCanNotProvideSubpath
17 from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField, Template
18 from philo.utils import paginate
19 try:
20         import mimeparse
21 except:
22         mimeparse = None
23
24
25 ATOM = feedgenerator.Atom1Feed.mime_type
26 RSS = feedgenerator.Rss201rev2Feed.mime_type
27 FEEDS = SortedDict([
28         (ATOM, feedgenerator.Atom1Feed),
29         (RSS, feedgenerator.Rss201rev2Feed),
30 ])
31 FEED_CHOICES = (
32         (ATOM, "Atom"),
33         (RSS, "RSS"),
34 )
35
36
37 class FeedView(MultiView):
38         """
39         The FeedView expects to handle a number of different feeds for the
40         same object - i.e. patterns for a blog to handle all entries or
41         just entries for a certain year/month/day.
42         
43         This class would subclass django.contrib.syndication.views.Feed, but
44         that would make it callable, which causes problems.
45         """
46         feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM)
47         feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
48         feeds_enabled = models.BooleanField(default=True)
49         feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.")
50         
51         item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
52         item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
53         
54         item_context_var = 'items'
55         object_attr = 'object'
56         
57         description = ""
58         
59         def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
60                 """
61                 Given the name to be used to reverse this view and the names of
62                 the attributes for the function that fetches the objects, returns
63                 patterns suitable for inclusion in urlpatterns.
64                 """
65                 urlpatterns = patterns('')
66                 if self.feeds_enabled:
67                         feed_reverse_name = "%s_feed" % reverse_name
68                         feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name))
69                         feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
70                         urlpatterns += patterns('',
71                                 url(feed_pattern, feed_view, name=feed_reverse_name),
72                         )
73                 urlpatterns += patterns('',
74                         url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
75                 )
76                 return urlpatterns
77         
78         def get_object(self, request, **kwargs):
79                 return getattr(self, self.object_attr)
80         
81         def feed_view(self, get_items_attr, reverse_name):
82                 """
83                 Returns a view function that renders a list of items as a feed.
84                 """
85                 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
86                 
87                 def inner(request, extra_context=None, *args, **kwargs):
88                         obj = self.get_object(request, *args, **kwargs)
89                         feed = self.get_feed(obj, request, reverse_name)
90                         items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
91                         self.populate_feed(feed, items, request)
92                         
93                         response = HttpResponse(mimetype=feed.mime_type)
94                         feed.write(response, 'utf-8')
95                         return response
96                 
97                 return inner
98         
99         def page_view(self, get_items_attr, page_attr):
100                 """
101                 Returns a view function that renders a list of items as a page.
102                 """
103                 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
104                 page = isinstance(page_attr, Page) and page_attr or getattr(self, page_attr)
105                 
106                 def inner(request, extra_context=None, *args, **kwargs):
107                         items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
108                         items, item_context = self.process_page_items(request, items)
109                         
110                         context = self.get_context()
111                         context.update(extra_context or {})
112                         context.update(item_context or {})
113                         
114                         return page.render_to_response(request, extra_context=context)
115                 return inner
116         
117         def process_page_items(self, request, items):
118                 """
119                 Hook for handling any extra processing of items based on a
120                 request, such as pagination or searching. This method is
121                 expected to return a list of items and a dictionary to be
122                 added to the page context.
123                 """
124                 item_context = {
125                         self.item_context_var: items
126                 }
127                 return items, item_context
128         
129         def get_feed_type(self, request):
130                 feed_type = self.feed_type
131                 if feed_type not in FEEDS:
132                         feed_type = FEEDS.keys()[0]
133                 accept = request.META.get('HTTP_ACCEPT')
134                 if accept and feed_type not in accept and "*/*" not in accept and "%s/*" % feed_type.split("/")[0] not in accept:
135                         # Wups! They aren't accepting the chosen format. Is there another format we can use?
136                         if mimeparse:
137                                 feed_type = mimeparse.best_match(FEEDS.keys(), accept)
138                         else:
139                                 for feed_type in FEEDS.keys():
140                                         if feed_type in accept or "%s/*" % feed_type.split("/")[0] in accept:
141                                                 break
142                                 else:
143                                         feed_type = None
144                         if not feed_type:
145                                 raise HttpNotAcceptable
146                 return FEEDS[feed_type]
147         
148         def get_feed(self, obj, request, reverse_name):
149                 """
150                 Returns an unpopulated feedgenerator.DefaultFeed object for this object.
151                 """
152                 try:
153                         current_site = Site.objects.get_current()
154                 except Site.DoesNotExist:
155                         current_site = RequestSite(request)
156                 
157                 feed_type = self.get_feed_type(request)
158                 node = request.node
159                 link = node.get_absolute_url(with_domain=True, request=request, secure=request.is_secure())
160                 
161                 feed = feed_type(
162                         title = self.__get_dynamic_attr('title', obj),
163                         subtitle = self.__get_dynamic_attr('subtitle', obj),
164                         link = link,
165                         description = self.__get_dynamic_attr('description', obj),
166                         language = settings.LANGUAGE_CODE.decode(),
167                         feed_url = add_domain(
168                                 current_site.domain,
169                                 self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node.subpath, with_domain=True, request=request, secure=request.is_secure()),
170                                 request.is_secure()
171                         ),
172                         author_name = self.__get_dynamic_attr('author_name', obj),
173                         author_link = self.__get_dynamic_attr('author_link', obj),
174                         author_email = self.__get_dynamic_attr('author_email', obj),
175                         categories = self.__get_dynamic_attr('categories', obj),
176                         feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
177                         feed_guid = self.__get_dynamic_attr('feed_guid', obj),
178                         ttl = self.__get_dynamic_attr('ttl', obj),
179                         **self.feed_extra_kwargs(obj)
180                 )
181                 return feed
182         
183         def populate_feed(self, feed, items, request):
184                 if self.item_title_template:
185                         title_template = DjangoTemplate(self.item_title_template.code)
186                 else:
187                         title_template = None
188                 if self.item_description_template:
189                         description_template = DjangoTemplate(self.item_description_template.code)
190                 else:
191                         description_template = None
192                 
193                 node = request.node
194                 try:
195                         current_site = Site.objects.get_current()
196                 except Site.DoesNotExist:
197                         current_site = RequestSite(request)
198                 
199                 if self.feed_length is not None:
200                         items = items[:self.feed_length]
201                 
202                 for item in items:
203                         if title_template is not None:
204                                 title = title_template.render(RequestContext(request, {'obj': item}))
205                         else:
206                                 title = self.__get_dynamic_attr('item_title', item)
207                         if description_template is not None:
208                                 description = description_template.render(RequestContext(request, {'obj': item}))
209                         else:
210                                 description = self.__get_dynamic_attr('item_description', item)
211                         
212                         link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
213                         
214                         enc = None
215                         enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
216                         if enc_url:
217                                 enc = feedgenerator.Enclosure(
218                                         url = smart_unicode(add_domain(
219                                                         current_site.domain,
220                                                         enc_url,
221                                                         request.is_secure()
222                                         )),
223                                         length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
224                                         mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
225                                 )
226                         author_name = self.__get_dynamic_attr('item_author_name', item)
227                         if author_name is not None:
228                                 author_email = self.__get_dynamic_attr('item_author_email', item)
229                                 author_link = self.__get_dynamic_attr('item_author_link', item)
230                         else:
231                                 author_email = author_link = None
232                         
233                         pubdate = self.__get_dynamic_attr('item_pubdate', item)
234                         if pubdate and not pubdate.tzinfo:
235                                 ltz = tzinfo.LocalTimezone(pubdate)
236                                 pubdate = pubdate.replace(tzinfo=ltz)
237                         
238                         feed.add_item(
239                                 title = title,
240                                 link = link,
241                                 description = description,
242                                 unique_id = self.__get_dynamic_attr('item_guid', item, link),
243                                 enclosure = enc,
244                                 pubdate = pubdate,
245                                 author_name = author_name,
246                                 author_email = author_email,
247                                 author_link = author_link,
248                                 categories = self.__get_dynamic_attr('item_categories', item),
249                                 item_copyright = self.__get_dynamic_attr('item_copyright', item),
250                                 **self.item_extra_kwargs(item)
251                         )
252         
253         def __get_dynamic_attr(self, attname, obj, default=None):
254                 try:
255                         attr = getattr(self, attname)
256                 except AttributeError:
257                         return default
258                 if callable(attr):
259                         # Check func_code.co_argcount rather than try/excepting the
260                         # function and catching the TypeError, because something inside
261                         # the function may raise the TypeError. This technique is more
262                         # accurate.
263                         if hasattr(attr, 'func_code'):
264                                 argcount = attr.func_code.co_argcount
265                         else:
266                                 argcount = attr.__call__.func_code.co_argcount
267                         if argcount == 2: # one argument is 'self'
268                                 return attr(obj)
269                         else:
270                                 return attr()
271                 return attr
272         
273         def feed_extra_kwargs(self, obj):
274                 """
275                 Returns an extra keyword arguments dictionary that is used when
276                 initializing the feed generator.
277                 """
278                 return {}
279         
280         def item_extra_kwargs(self, item):
281                 """
282                 Returns an extra keyword arguments dictionary that is used with
283                 the `add_item` call of the feed generator.
284                 """
285                 return {}
286         
287         def item_title(self, item):
288                 return escape(force_unicode(item))
289         
290         def item_description(self, item):
291                 return force_unicode(item)
292         
293         class Meta:
294                 abstract=True
295
296
297 class Blog(Entity, Titled):
298         @property
299         def entry_tags(self):
300                 """ Returns a QuerySet of Tags that are used on any entries in this blog. """
301                 return Tag.objects.filter(blogentries__blog=self).distinct()
302         
303         @property
304         def entry_dates(self):
305                 dates = {'year': self.entries.dates('date', 'year', order='DESC'), 'month': self.entries.dates('date', 'month', order='DESC'), 'day': self.entries.dates('date', 'day', order='DESC')}
306                 return dates
307
308
309 register_value_model(Blog)
310
311
312 class BlogEntry(Entity, Titled):
313         blog = models.ForeignKey(Blog, related_name='entries', blank=True, null=True)
314         author = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='blogentries')
315         date = models.DateTimeField(default=None)
316         content = models.TextField()
317         excerpt = models.TextField(blank=True, null=True)
318         tags = models.ManyToManyField(Tag, related_name='blogentries', blank=True, null=True)
319         
320         def save(self, *args, **kwargs):
321                 if self.date is None:
322                         self.date = datetime.now()
323                 super(BlogEntry, self).save(*args, **kwargs)
324         
325         class Meta:
326                 ordering = ['-date']
327                 verbose_name_plural = "blog entries"
328                 get_latest_by = "date"
329
330
331 register_value_model(BlogEntry)
332
333
334 class BlogView(FeedView):
335         ENTRY_PERMALINK_STYLE_CHOICES = (
336                 ('D', 'Year, month, and day'),
337                 ('M', 'Year and month'),
338                 ('Y', 'Year'),
339                 ('B', 'Custom base'),
340                 ('N', 'No base')
341         )
342         
343         blog = models.ForeignKey(Blog, related_name='blogviews')
344         
345         index_page = models.ForeignKey(Page, related_name='blog_index_related')
346         entry_page = models.ForeignKey(Page, related_name='blog_entry_related')
347         # TODO: entry_archive is misleading. Rename to ymd_page or timespan_page.
348         entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True)
349         tag_page = models.ForeignKey(Page, related_name='blog_tag_related')
350         tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True)
351         entries_per_page = models.IntegerField(blank=True, validators=[validate_pagination_count], null=True)
352         
353         entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
354         entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
355         tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
356         
357         item_context_var = 'entries'
358         object_attr = 'blog'
359         
360         def __unicode__(self):
361                 return u'BlogView for %s' % self.blog.title
362         
363         def get_reverse_params(self, obj):
364                 if isinstance(obj, BlogEntry):
365                         if obj.blog == self.blog:
366                                 kwargs = {'slug': obj.slug}
367                                 if self.entry_permalink_style in 'DMY':
368                                         kwargs.update({'year': str(obj.date.year).zfill(4)})
369                                         if self.entry_permalink_style in 'DM':
370                                                 kwargs.update({'month': str(obj.date.month).zfill(2)})
371                                                 if self.entry_permalink_style == 'D':
372                                                         kwargs.update({'day': str(obj.date.day).zfill(2)})
373                                 return self.entry_view, [], kwargs
374                 elif isinstance(obj, Tag) or (isinstance(obj, models.query.QuerySet) and obj.model == Tag and obj):
375                         if isinstance(obj, Tag):
376                                 obj = [obj]
377                         slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()]
378                         if slugs:
379                                 return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)}
380                 elif isinstance(obj, (date, datetime)):
381                         kwargs = {
382                                 'year': str(obj.year).zfill(4),
383                                 'month': str(obj.month).zfill(2),
384                                 'day': str(obj.day).zfill(2)
385                         }
386                         return 'entries_by_day', [], kwargs
387                 raise ViewCanNotProvideSubpath
388         
389         @property
390         def urlpatterns(self):
391                 urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') +\
392                         self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)$' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'entries_by_tag')
393                 
394                 if self.tag_archive_page:
395                         urlpatterns += patterns('',
396                                 url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
397                         )
398                 
399                 if self.entry_archive_page:
400                         if self.entry_permalink_style in 'DMY':
401                                 urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')
402                                 if self.entry_permalink_style in 'DM':
403                                         urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_month')
404                                         if self.entry_permalink_style == 'D':
405                                                 urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')
406                 
407                 if self.entry_permalink_style == 'D':
408                         urlpatterns += patterns('',
409                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
410                         )
411                 elif self.entry_permalink_style == 'M':
412                         urlpatterns += patterns('',
413                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
414                         )
415                 elif self.entry_permalink_style == 'Y':
416                         urlpatterns += patterns('',
417                                 url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)$', self.entry_view)
418                         )
419                 elif self.entry_permalink_style == 'B':
420                         urlpatterns += patterns('',
421                                 url((r'^%s/(?P<slug>[-\w]+)$' % self.entry_permalink_base), self.entry_view)
422                         )
423                 else:
424                         urlpatterns = patterns('',
425                                 url(r'^(?P<slug>[-\w]+)$', self.entry_view)
426                         )
427                 return urlpatterns
428         
429         def get_context(self):
430                 return {'blog': self.blog}
431         
432         def get_entry_queryset(self):
433                 return self.blog.entries.all()
434         
435         def get_tag_queryset(self):
436                 return self.blog.entry_tags
437         
438         def get_all_entries(self, request, extra_context=None):
439                 return self.get_entry_queryset(), extra_context
440         
441         def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
442                 if not self.entry_archive_page:
443                         raise Http404
444                 entries = self.get_entry_queryset()
445                 if year:
446                         entries = entries.filter(date__year=year)
447                 if month:
448                         entries = entries.filter(date__month=month)
449                 if day:
450                         entries = entries.filter(date__day=day)
451                 
452                 context = extra_context or {}
453                 context.update({'year': year, 'month': month, 'day': day})
454                 return entries, context
455         
456         def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
457                 tag_slugs = tag_slugs.replace('+', '/').split('/')
458                 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
459                 
460                 if not tags:
461                         raise Http404
462                 
463                 # Raise a 404 on an incorrect slug.
464                 found_slugs = [tag.slug for tag in tags]
465                 for slug in tag_slugs:
466                         if slug and slug not in found_slugs:
467                                 raise Http404
468
469                 entries = self.get_entry_queryset()
470                 for tag in tags:
471                         entries = entries.filter(tags=tag)
472                 
473                 context = extra_context or {}
474                 context.update({'tags': tags})
475                 
476                 return entries, context
477         
478         def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
479                 entries = self.get_entry_queryset()
480                 if year:
481                         entries = entries.filter(date__year=year)
482                 if month:
483                         entries = entries.filter(date__month=month)
484                 if day:
485                         entries = entries.filter(date__day=day)
486                 try:
487                         entry = entries.get(slug=slug)
488                 except:
489                         raise Http404
490                 context = self.get_context()
491                 context.update(extra_context or {})
492                 context.update({'entry': entry})
493                 return self.entry_page.render_to_response(request, extra_context=context)
494         
495         def tag_archive_view(self, request, extra_context=None):
496                 if not self.tag_archive_page:
497                         raise Http404
498                 context = self.get_context()
499                 context.update(extra_context or {})
500                 context.update({
501                         'tags': self.get_tag_queryset()
502                 })
503                 return self.tag_archive_page.render_to_response(request, extra_context=context)
504         
505         def feed_view(self, get_items_attr, reverse_name):
506                 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
507                 
508                 def inner(request, extra_context=None, *args, **kwargs):
509                         obj = self.get_object(request, *args, **kwargs)
510                         feed = self.get_feed(obj, request, reverse_name)
511                         items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
512                         self.populate_feed(feed, items, request)
513                         
514                         if 'tags' in extra_context:
515                                 tags = extra_context['tags']
516                                 feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure())
517                         else:
518                                 tags = obj.entry_tags
519                         
520                         feed.feed['categories'] = [tag.name for tag in tags]
521                         
522                         response = HttpResponse(mimetype=feed.mime_type)
523                         feed.write(response, 'utf-8')
524                         return response
525                 
526                 return inner
527         
528         def process_page_items(self, request, items):
529                 if self.entries_per_page:
530                         page_num = request.GET.get('page', 1)
531                         paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num)
532                         item_context = {
533                                 'paginator': paginator,
534                                 'paginated_page': paginated_page,
535                                 self.item_context_var: items
536                         }
537                 else:
538                         item_context = {
539                                 self.item_context_var: items
540                         }
541                 return items, item_context
542         
543         def title(self, obj):
544                 return obj.title
545         
546         def item_title(self, item):
547                 return item.title
548         
549         def item_description(self, item):
550                 return item.content
551         
552         def item_author_name(self, item):
553                 return item.author.get_full_name()
554         
555         def item_pubdate(self, item):
556                 return item.date
557         
558         def item_categories(self, item):
559                 return [tag.name for tag in item.tags.all()]
560
561
562 class Newsletter(Entity, Titled):
563         pass
564
565
566 register_value_model(Newsletter)
567
568
569 class NewsletterArticle(Entity, Titled):
570         newsletter = models.ForeignKey(Newsletter, related_name='articles')
571         authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
572         date = models.DateTimeField(default=None)
573         lede = TemplateField(null=True, blank=True, verbose_name='Summary')
574         full_text = TemplateField(db_index=True)
575         tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True)
576         
577         def save(self, *args, **kwargs):
578                 if self.date is None:
579                         self.date = datetime.now()
580                 super(NewsletterArticle, self).save(*args, **kwargs)
581         
582         class Meta:
583                 get_latest_by = 'date'
584                 ordering = ['-date']
585                 unique_together = (('newsletter', 'slug'),)
586
587
588 register_value_model(NewsletterArticle)
589
590
591 class NewsletterIssue(Entity, Titled):
592         newsletter = models.ForeignKey(Newsletter, related_name='issues')
593         numbering = models.CharField(max_length=50, help_text='For example, 04.02 for volume 4, issue 2.')
594         articles = models.ManyToManyField(NewsletterArticle, related_name='issues')
595         
596         class Meta:
597                 ordering = ['-numbering']
598                 unique_together = (('newsletter', 'numbering'),)
599
600
601 register_value_model(NewsletterIssue)
602
603
604 class NewsletterView(FeedView):
605         ARTICLE_PERMALINK_STYLE_CHOICES = (
606                 ('D', 'Year, month, and day'),
607                 ('M', 'Year and month'),
608                 ('Y', 'Year'),
609                 ('S', 'Slug only')
610         )
611         
612         newsletter = models.ForeignKey(Newsletter, related_name='newsletterviews')
613         
614         index_page = models.ForeignKey(Page, related_name='newsletter_index_related')
615         article_page = models.ForeignKey(Page, related_name='newsletter_article_related')
616         article_archive_page = models.ForeignKey(Page, related_name='newsletter_article_archive_related', null=True, blank=True)
617         issue_page = models.ForeignKey(Page, related_name='newsletter_issue_related')
618         issue_archive_page = models.ForeignKey(Page, related_name='newsletter_issue_archive_related', null=True, blank=True)
619         
620         article_permalink_style = models.CharField(max_length=1, choices=ARTICLE_PERMALINK_STYLE_CHOICES)
621         article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
622         issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
623         
624         item_context_var = 'articles'
625         object_attr = 'newsletter'
626         
627         def __unicode__(self):
628                 return "NewsletterView for %s" % self.newsletter.__unicode__()
629         
630         def get_reverse_params(self, obj):
631                 if isinstance(obj, NewsletterArticle):
632                         if obj.newsletter == self.newsletter:
633                                 kwargs = {'slug': obj.slug}
634                                 if self.article_permalink_style in 'DMY':
635                                         kwargs.update({'year': str(obj.date.year).zfill(4)})
636                                         if self.article_permalink_style in 'DM':
637                                                 kwargs.update({'month': str(obj.date.month).zfill(2)})
638                                                 if self.article_permalink_style == 'D':
639                                                         kwargs.update({'day': str(obj.date.day).zfill(2)})
640                                 return self.article_view, [], kwargs
641                 elif isinstance(obj, NewsletterIssue):
642                         if obj.newsletter == self.newsletter:
643                                 return 'issue', [], {'numbering': obj.numbering}
644                 elif isinstance(obj, (date, datetime)):
645                         kwargs = {
646                                 'year': str(obj.year).zfill(4),
647                                 'month': str(obj.month).zfill(2),
648                                 'day': str(obj.day).zfill(2)
649                         }
650                         return 'articles_by_day', [], kwargs
651                 raise ViewCanNotProvideSubpath
652         
653         @property
654         def urlpatterns(self):
655                 urlpatterns = self.feed_patterns(r'^', 'get_all_articles', 'index_page', 'index') + patterns('',
656                         url(r'^%s/(?P<numbering>.+)$' % self.issue_permalink_base, self.page_view('get_articles_by_issue', 'issue_page'), name='issue')
657                 )
658                 if self.issue_archive_page:
659                         urlpatterns += patterns('',
660                                 url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
661                         )
662                 if self.article_archive_page:
663                         urlpatterns += patterns('',
664                                 url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
665                         )
666                         if self.article_permalink_style in 'DMY':
667                                 urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
668                                 if self.article_permalink_style in 'DM':
669                                         urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_month')
670                                         if self.article_permalink_style == 'D':
671                                                 urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_day')
672                 
673                 if self.article_permalink_style == 'Y':
674                         urlpatterns += patterns('',
675                                 url(r'^%s/(?P<year>\d{4})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
676                         )
677                 elif self.article_permalink_style == 'M':
678                         urlpatterns += patterns('',
679                                 url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
680                         )
681                 elif self.article_permalink_style == 'D':
682                         urlpatterns += patterns('',
683                                 url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
684                         )
685                 else:   
686                         urlpatterns += patterns('',
687                                 url(r'^%s/(?P<slug>[-\w]+)$' % self.article_permalink_base, self.article_view)
688                         )
689                 
690                 return urlpatterns
691         
692         def get_context(self):
693                 return {'newsletter': self.newsletter}
694         
695         def get_article_queryset(self):
696                 return self.newsletter.articles.all()
697         
698         def get_issue_queryset(self):
699                 return self.newsletter.issues.all()
700         
701         def get_all_articles(self, request, extra_context=None):
702                 return self.get_article_queryset(), extra_context
703         
704         def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
705                 articles = self.get_article_queryset().filter(date__year=year)
706                 if month:
707                         articles = articles.filter(date__month=month)
708                 if day:
709                         articles = articles.filter(date__day=day)
710                 return articles, extra_context
711         
712         def get_articles_by_issue(self, request, numbering, extra_context=None):
713                 try:
714                         issue = self.get_issue_queryset().get(numbering=numbering)
715                 except NewsletterIssue.DoesNotExist:
716                         raise Http404
717                 context = extra_context or {}
718                 context.update({'issue': issue})
719                 return self.get_article_queryset().filter(issues=issue), context
720         
721         def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
722                 articles = self.get_article_queryset()
723                 if year:
724                         articles = articles.filter(date__year=year)
725                 if month:
726                         articles = articles.filter(date__month=month)
727                 if day:
728                         articles = articles.filter(date__day=day)
729                 try:
730                         article = articles.get(slug=slug)
731                 except NewsletterArticle.DoesNotExist:
732                         raise Http404
733                 context = self.get_context()
734                 context.update(extra_context or {})
735                 context.update({'article': article})
736                 return self.article_page.render_to_response(request, extra_context=context)
737         
738         def issue_archive_view(self, request, extra_context):
739                 if not self.issue_archive_page:
740                         raise Http404
741                 context = self.get_context()
742                 context.update(extra_context or {})
743                 context.update({
744                         'issues': self.get_issue_queryset()
745                 })
746                 return self.issue_archive_page.render_to_response(request, extra_context=context)
747         
748         def title(self, obj):
749                 return obj.title
750         
751         def item_title(self, item):
752                 return item.title
753         
754         def item_description(self, item):
755                 return item.full_text
756         
757         def item_author_name(self, item):
758                 authors = list(item.authors.all())
759                 if len(authors) > 1:
760                         return "%s and %s" % (", ".join([author.get_full_name() for author in authors[:-1]]), authors[-1].get_full_name())
761                 elif authors:
762                         return authors[0].get_full_name()
763                 else:
764                         return ''
765         
766         def item_pubdate(self, item):
767                 return item.date
768         
769         def item_categories(self, item):
770                 return [tag.name for tag in item.tags.all()]