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
20 ATOM = feedgenerator.Atom1Feed.mime_type
21 RSS = feedgenerator.Rss201rev2Feed.mime_type
23 (ATOM, feedgenerator.Atom1Feed),
24 (RSS, feedgenerator.Rss201rev2Feed),
32 class FeedView(MultiView):
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.
38 This class would subclass django.contrib.syndication.views.Feed, but
39 that would make it callable, which causes problems.
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)
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")
48 item_context_var = 'items'
49 object_attr = 'object'
53 def feed_patterns(self, get_items_attr, page_attr, reverse_name):
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.
59 urlpatterns = patterns('',
60 url(r'^$', self.page_view(get_items_attr, page_attr), name=reverse_name)
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),
69 def get_object(self, request, **kwargs):
70 return getattr(self, self.object_attr)
72 def feed_view(self, get_items_attr, reverse_name):
74 Returns a view function that renders a list of items as a feed.
76 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
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)
83 response = HttpResponse(mimetype=feed.mime_type)
84 feed.write(response, 'utf-8')
89 def page_view(self, get_items_attr, page_attr):
91 Returns a view function that renders a list of items as a page.
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)
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)
100 context = self.get_context()
101 context.update(extra_context or {})
102 context.update(item_context or {})
104 return page.render_to_response(request, extra_context=context)
107 def process_page_items(self, request, items):
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.
115 self.item_context_var: items
117 return items, item_context
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?
125 feed_type = mimeparse.best_match(FEEDS.keys(), accept)
127 for feed_type in FEEDS.keys():
128 if feed_type in accept or "%s/*" % feed_type.split("/")[0] in accept:
134 return HttpResponse(status=406)
135 return FEEDS[feed_type]
137 def get_feed(self, obj, request, reverse_name):
139 Returns an unpopulated feedgenerator.DefaultFeed object for this object.
141 feed_type = self.get_feed_type(request)
143 link = node.get_absolute_url(with_domain=True, request=request, secure=request.is_secure())
146 title = self.__get_dynamic_attr('title', obj),
147 subtitle = self.__get_dynamic_attr('subtitle', obj),
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)
163 def populate_feed(self, feed, items, request):
164 if self.item_title_template:
165 title_template = Template(self.item_title_template.code)
167 title_template = None
168 if self.item_description_template:
169 description_template = Template(self.item_description_template.code)
171 description_template = None
176 if title_template is not None:
177 title = title_template.render(RequestContext(request, {'obj': item}))
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}))
183 description = self.__get_dynamic_attr('item_description', item)
185 link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
188 enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
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))
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)
200 author_email = author_link = None
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)
210 description = description,
211 unique_id = self.__get_dynamic_attr('item_guid', item, link),
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)
222 def __get_dynamic_attr(self, attname, obj, default=None):
224 attr = getattr(self, attname)
225 except AttributeError:
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
232 if hasattr(attr, 'func_code'):
233 argcount = attr.func_code.co_argcount
235 argcount = attr.__call__.func_code.co_argcount
236 if argcount == 2: # one argument is 'self'
242 def feed_extra_kwargs(self, obj):
244 Returns an extra keyword arguments dictionary that is used when
245 initializing the feed generator.
249 def item_extra_kwargs(self, item):
251 Returns an extra keyword arguments dictionary that is used with
252 the `add_item` call of the feed generator.
256 def item_title(self, item):
257 return escape(force_unicode(item))
259 def item_description(self, item):
260 return force_unicode(item)
266 class Blog(Entity, Titled):
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()
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')}
278 register_value_model(Blog)
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)
289 def save(self, *args, **kwargs):
290 if self.date is None:
291 self.date = datetime.now()
292 super(BlogEntry, self).save(*args, **kwargs)
296 verbose_name_plural = "blog entries"
297 get_latest_by = "date"
300 register_value_model(BlogEntry)
303 class BlogView(FeedView):
304 ENTRY_PERMALINK_STYLE_CHOICES = (
305 ('D', 'Year, month, and day'),
306 ('M', 'Year and month'),
308 ('B', 'Custom base'),
312 blog = models.ForeignKey(Blog, related_name='blogviews')
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)
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')
325 item_context_var = 'entries'
328 def __unicode__(self):
329 return u'BlogView for %s' % self.blog.title
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):
345 slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()]
347 return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)}
348 elif isinstance(obj, (date, datetime)):
350 'year': str(obj.year).zfill(4),
351 'month': str(obj.month).zfill(2),
352 'day': str(obj.day).zfill(2)
354 return 'entries_by_day', [], kwargs
355 raise ViewCanNotProvideSubpath
358 def urlpatterns(self):
359 urlpatterns = patterns('',
360 url(r'^', include(self.feed_patterns('get_all_entries', 'index_page', 'index'))),
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'),
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')
369 if self.tag_archive_page:
370 urlpatterns += patterns('',
371 url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
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')))
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'))),
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')))
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)
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)
396 elif self.entry_permalink_style == 'Y':
397 urlpatterns += patterns('',
398 url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)$', self.entry_view)
400 elif self.entry_permalink_style == 'B':
401 urlpatterns += patterns('',
402 url((r'^%s/(?P<slug>[-\w]+)$' % self.entry_permalink_base), self.entry_view)
405 urlpatterns = patterns('',
406 url(r'^(?P<slug>[-\w]+)$', self.entry_view)
410 def get_context(self):
411 return {'blog': self.blog}
413 def get_entry_queryset(self):
414 return self.blog.entries.all()
416 def get_tag_queryset(self):
417 return self.blog.entry_tags
419 def get_all_entries(self, request, extra_context=None):
420 return self.get_entry_queryset(), extra_context
422 def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
423 if not self.entry_archive_page:
425 entries = self.get_entry_queryset()
427 entries = entries.filter(date__year=year)
429 entries = entries.filter(date__month=month)
431 entries = entries.filter(date__day=day)
433 context = extra_context or {}
434 context.update({'year': year, 'month': month, 'day': day})
435 return entries, context
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)
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:
450 entries = self.get_entry_queryset()
452 entries = entries.filter(tags=tag)
454 context = extra_context or {}
455 context.update({'tags': tags})
457 return entries, context
459 def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
460 entries = self.get_entry_queryset()
462 entries = entries.filter(date__year=year)
464 entries = entries.filter(date__month=month)
466 entries = entries.filter(date__day=day)
468 entry = entries.get(slug=slug)
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)
476 def tag_archive_view(self, request, extra_context=None):
477 if not self.tag_archive_page:
479 context = self.get_context()
480 context.update(extra_context or {})
482 'tags': self.get_tag_queryset()
484 return self.tag_archive_page.render_to_response(request, extra_context=context)
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)
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)
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())
498 tags = obj.entry_tags
500 feed.feed['categories'] = [tag.name for tag in tags]
502 response = HttpResponse(mimetype=feed.mime_type)
503 feed.write(response, 'utf-8')
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)
513 'paginator': paginator,
514 'paginated_page': paginated_page,
515 self.item_context_var: objects
519 self.item_context_var: items
521 return items, item_context
523 def title(self, obj):
526 def item_title(self, item):
529 def item_description(self, item):
532 def item_author_name(self, item):
533 return item.author.get_full_name()
535 def item_pubdate(self, item):
538 def item_categories(self, item):
539 return [tag.name for tag in item.tags.all()]
542 class Newsletter(Entity, Titled):
546 register_value_model(Newsletter)
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)
557 def save(self, *args, **kwargs):
558 if self.date is None:
559 self.date = datetime.now()
560 super(NewsletterArticle, self).save(*args, **kwargs)
563 get_latest_by = 'date'
565 unique_together = (('newsletter', 'slug'),)
568 register_value_model(NewsletterArticle)
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')
577 ordering = ['-numbering']
578 unique_together = (('newsletter', 'numbering'),)
581 register_value_model(NewsletterIssue)
584 class NewsletterView(FeedView):
585 ARTICLE_PERMALINK_STYLE_CHOICES = (
586 ('D', 'Year, month, and day'),
587 ('M', 'Year and month'),
592 newsletter = models.ForeignKey(Newsletter, related_name='newsletterviews')
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)
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')
604 item_context_var = 'articles'
605 object_attr = 'newsletter'
607 def __unicode__(self):
608 return "NewsletterView for %s" % self.newsletter.__unicode__()
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)):
626 'year': str(obj.year).zfill(4),
627 'month': str(obj.month).zfill(2),
628 'day': str(obj.day).zfill(2)
630 return 'articles_by_day', [], kwargs
631 raise ViewCanNotProvideSubpath
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')))
639 if self.issue_archive_page:
640 urlpatterns += patterns('',
641 url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
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')))
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')))
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')))
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')))
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)
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)
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)
673 urlpatterns += patterns('',
674 url(r'^%s/(?P<slug>[-\w]+)$' % self.article_permalink_base, self.article_view)
679 def get_context(self):
680 return {'newsletter': self.newsletter}
682 def get_article_queryset(self):
683 return self.newsletter.articles.all()
685 def get_issue_queryset(self):
686 return self.newsletter.issues.all()
688 def get_all_articles(self, request, extra_context=None):
689 return self.get_article_queryset(), extra_context
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)
694 articles = articles.filter(date__month=month)
696 articles = articles.filter(date__day=day)
697 return articles, extra_context
699 def get_articles_by_issue(self, request, numbering, extra_context=None):
701 issue = self.get_issue_queryset().get(numbering=numbering)
702 except NewsletterIssue.DoesNotExist:
704 context = extra_context or {}
705 context.update({'issue': issue})
706 return self.get_article_queryset().filter(issues=issue), context
708 def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
709 articles = self.get_article_queryset()
711 articles = articles.filter(date__year=year)
713 articles = articles.filter(date__month=month)
715 articles = articles.filter(date__day=day)
717 article = articles.get(slug=slug)
718 except NewsletterArticle.DoesNotExist:
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)
725 def issue_archive_view(self, request, extra_context):
726 if not self.issue_archive_page:
728 context = self.get_context()
729 context.update(extra_context or {})
731 'issues': self.get_issue_queryset()
733 return self.issue_archive_page.render_to_response(request, extra_context=context)
735 def title(self, obj):
738 def item_title(self, item):
741 def item_description(self, item):
742 return item.full_text
744 def item_author_name(self, item):
745 authors = list(item.authors.all())
747 return "%s and %s" % (", ".join([author.get_full_name() for author in authors[:-1]]), authors[-1].get_full_name())
749 return authors[0].get_full_name()
753 def item_pubdate(self, item):
756 def item_categories(self, item):
757 return [tag.name for tag in item.tags.all()]