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