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