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