1 from datetime import date, datetime
3 from django.conf import settings
4 from django.conf.urls.defaults import url, patterns, include
5 from django.contrib.sites.models import Site, RequestSite
6 from django.contrib.syndication.views import add_domain
7 from django.db import models
8 from django.http import Http404, HttpResponse
9 from django.template import RequestContext, Template as DjangoTemplate
10 from django.utils import feedgenerator, tzinfo
11 from django.utils.datastructures import SortedDict
12 from django.utils.encoding import smart_unicode, force_unicode
13 from django.utils.html import escape
15 from philo.contrib.penfield.exceptions import HttpNotAcceptable
16 from philo.contrib.penfield.middleware import http_not_acceptable
17 from philo.contrib.penfield.validators import validate_pagination_count
18 from philo.exceptions import ViewCanNotProvideSubpath
19 from philo.models import Tag, Entity, MultiView, Page, register_value_model, TemplateField, Template
20 from philo.utils import paginate
28 ATOM = feedgenerator.Atom1Feed.mime_type
29 RSS = feedgenerator.Rss201rev2Feed.mime_type
31 (ATOM, feedgenerator.Atom1Feed),
32 (RSS, feedgenerator.Rss201rev2Feed),
40 class FeedView(MultiView):
42 The FeedView expects to handle a number of different feeds for the
43 same object - i.e. patterns for a blog to handle all entries or
44 just entries for a certain year/month/day.
46 This class would subclass django.contrib.syndication.views.Feed, but
47 that would make it callable, which causes problems.
49 feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM)
50 feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
51 feeds_enabled = models.BooleanField(default=True)
52 feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.")
54 item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
55 item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
57 item_context_var = 'items'
58 object_attr = 'object'
62 def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
64 Given the name to be used to reverse this view and the names of
65 the attributes for the function that fetches the objects, returns
66 patterns suitable for inclusion in urlpatterns.
68 urlpatterns = patterns('')
69 if self.feeds_enabled:
70 feed_reverse_name = "%s_feed" % reverse_name
71 feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name))
72 feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
73 urlpatterns += patterns('',
74 url(feed_pattern, feed_view, name=feed_reverse_name),
76 urlpatterns += patterns('',
77 url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
81 def get_object(self, request, **kwargs):
82 return getattr(self, self.object_attr)
84 def feed_view(self, get_items_attr, reverse_name):
86 Returns a view function that renders a list of items as a feed.
88 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
90 def inner(request, extra_context=None, *args, **kwargs):
91 obj = self.get_object(request, *args, **kwargs)
92 feed = self.get_feed(obj, request, reverse_name)
93 items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
94 self.populate_feed(feed, items, request)
96 response = HttpResponse(mimetype=feed.mime_type)
97 feed.write(response, 'utf-8')
102 def page_view(self, get_items_attr, page_attr):
104 Returns a view function that renders a list of items as a page.
106 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
107 page = isinstance(page_attr, Page) and page_attr or getattr(self, page_attr)
109 def inner(request, extra_context=None, *args, **kwargs):
110 items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
111 items, item_context = self.process_page_items(request, items)
113 context = self.get_context()
114 context.update(extra_context or {})
115 context.update(item_context or {})
117 return page.render_to_response(request, extra_context=context)
120 def process_page_items(self, request, items):
122 Hook for handling any extra processing of items based on a
123 request, such as pagination or searching. This method is
124 expected to return a list of items and a dictionary to be
125 added to the page context.
128 self.item_context_var: items
130 return items, item_context
132 def get_feed_type(self, request):
133 feed_type = self.feed_type
134 if feed_type not in FEEDS:
135 feed_type = FEEDS.keys()[0]
136 accept = request.META.get('HTTP_ACCEPT')
137 if accept and feed_type not in accept and "*/*" not in accept and "%s/*" % feed_type.split("/")[0] not in accept:
138 # Wups! They aren't accepting the chosen format. Is there another format we can use?
140 feed_type = mimeparse.best_match(FEEDS.keys(), accept)
142 for feed_type in FEEDS.keys():
143 if feed_type in accept or "%s/*" % feed_type.split("/")[0] in accept:
148 raise HttpNotAcceptable
149 return FEEDS[feed_type]
151 def get_feed(self, obj, request, reverse_name):
153 Returns an unpopulated feedgenerator.DefaultFeed object for this object.
156 current_site = Site.objects.get_current()
157 except Site.DoesNotExist:
158 current_site = RequestSite(request)
160 feed_type = self.get_feed_type(request)
162 link = node.get_absolute_url(with_domain=True, request=request, secure=request.is_secure())
165 title = self.__get_dynamic_attr('title', obj),
166 subtitle = self.__get_dynamic_attr('subtitle', obj),
168 description = self.__get_dynamic_attr('description', obj),
169 language = settings.LANGUAGE_CODE.decode(),
170 feed_url = add_domain(
172 self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node.subpath, with_domain=True, request=request, secure=request.is_secure()),
175 author_name = self.__get_dynamic_attr('author_name', obj),
176 author_link = self.__get_dynamic_attr('author_link', obj),
177 author_email = self.__get_dynamic_attr('author_email', obj),
178 categories = self.__get_dynamic_attr('categories', obj),
179 feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
180 feed_guid = self.__get_dynamic_attr('feed_guid', obj),
181 ttl = self.__get_dynamic_attr('ttl', obj),
182 **self.feed_extra_kwargs(obj)
186 def populate_feed(self, feed, items, request):
187 if self.item_title_template:
188 title_template = DjangoTemplate(self.item_title_template.code)
190 title_template = None
191 if self.item_description_template:
192 description_template = DjangoTemplate(self.item_description_template.code)
194 description_template = None
198 current_site = Site.objects.get_current()
199 except Site.DoesNotExist:
200 current_site = RequestSite(request)
202 if self.feed_length is not None:
203 items = items[:self.feed_length]
206 if title_template is not None:
207 title = title_template.render(RequestContext(request, {'obj': item}))
209 title = self.__get_dynamic_attr('item_title', item)
210 if description_template is not None:
211 description = description_template.render(RequestContext(request, {'obj': item}))
213 description = self.__get_dynamic_attr('item_description', item)
215 link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
218 enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
220 enc = feedgenerator.Enclosure(
221 url = smart_unicode(add_domain(
226 length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
227 mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
229 author_name = self.__get_dynamic_attr('item_author_name', item)
230 if author_name is not None:
231 author_email = self.__get_dynamic_attr('item_author_email', item)
232 author_link = self.__get_dynamic_attr('item_author_link', item)
234 author_email = author_link = None
236 pubdate = self.__get_dynamic_attr('item_pubdate', item)
237 if pubdate and not pubdate.tzinfo:
238 ltz = tzinfo.LocalTimezone(pubdate)
239 pubdate = pubdate.replace(tzinfo=ltz)
244 description = description,
245 unique_id = self.__get_dynamic_attr('item_guid', item, link),
248 author_name = author_name,
249 author_email = author_email,
250 author_link = author_link,
251 categories = self.__get_dynamic_attr('item_categories', item),
252 item_copyright = self.__get_dynamic_attr('item_copyright', item),
253 **self.item_extra_kwargs(item)
256 def __get_dynamic_attr(self, attname, obj, default=None):
258 attr = getattr(self, attname)
259 except AttributeError:
262 # Check func_code.co_argcount rather than try/excepting the
263 # function and catching the TypeError, because something inside
264 # the function may raise the TypeError. This technique is more
266 if hasattr(attr, 'func_code'):
267 argcount = attr.func_code.co_argcount
269 argcount = attr.__call__.func_code.co_argcount
270 if argcount == 2: # one argument is 'self'
276 def feed_extra_kwargs(self, obj):
278 Returns an extra keyword arguments dictionary that is used when
279 initializing the feed generator.
283 def item_extra_kwargs(self, item):
285 Returns an extra keyword arguments dictionary that is used with
286 the `add_item` call of the feed generator.
290 def item_title(self, item):
291 return escape(force_unicode(item))
293 def item_description(self, item):
294 return force_unicode(item)
301 title = models.CharField(max_length=255)
302 slug = models.SlugField(max_length=255)
304 def __unicode__(self):
308 def entry_tags(self):
309 """ Returns a QuerySet of Tags that are used on any entries in this blog. """
310 return Tag.objects.filter(blogentries__blog=self).distinct()
313 def entry_dates(self):
314 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')}
318 register_value_model(Blog)
321 class BlogEntry(Entity):
322 title = models.CharField(max_length=255)
323 slug = models.SlugField(max_length=255)
324 blog = models.ForeignKey(Blog, related_name='entries', blank=True, null=True)
325 author = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='blogentries')
326 date = models.DateTimeField(default=None)
327 content = models.TextField()
328 excerpt = models.TextField(blank=True, null=True)
329 tags = models.ManyToManyField(Tag, related_name='blogentries', blank=True, null=True)
331 def save(self, *args, **kwargs):
332 if self.date is None:
333 self.date = datetime.now()
334 super(BlogEntry, self).save(*args, **kwargs)
336 def __unicode__(self):
341 verbose_name_plural = "blog entries"
342 get_latest_by = "date"
345 register_value_model(BlogEntry)
348 class BlogView(FeedView):
349 ENTRY_PERMALINK_STYLE_CHOICES = (
350 ('D', 'Year, month, and day'),
351 ('M', 'Year and month'),
353 ('B', 'Custom base'),
357 blog = models.ForeignKey(Blog, related_name='blogviews')
359 index_page = models.ForeignKey(Page, related_name='blog_index_related')
360 entry_page = models.ForeignKey(Page, related_name='blog_entry_related')
361 # TODO: entry_archive is misleading. Rename to ymd_page or timespan_page.
362 entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True)
363 tag_page = models.ForeignKey(Page, related_name='blog_tag_related')
364 tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True)
365 entries_per_page = models.IntegerField(blank=True, validators=[validate_pagination_count], null=True)
367 entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
368 entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
369 tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
371 item_context_var = 'entries'
374 def __unicode__(self):
375 return u'BlogView for %s' % self.blog.title
377 def get_reverse_params(self, obj):
378 if isinstance(obj, BlogEntry):
379 if obj.blog == self.blog:
380 kwargs = {'slug': obj.slug}
381 if self.entry_permalink_style in 'DMY':
382 kwargs.update({'year': str(obj.date.year).zfill(4)})
383 if self.entry_permalink_style in 'DM':
384 kwargs.update({'month': str(obj.date.month).zfill(2)})
385 if self.entry_permalink_style == 'D':
386 kwargs.update({'day': str(obj.date.day).zfill(2)})
387 return self.entry_view, [], kwargs
388 elif isinstance(obj, Tag) or (isinstance(obj, models.query.QuerySet) and obj.model == Tag and obj):
389 if isinstance(obj, Tag):
391 slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()]
393 return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)}
394 elif isinstance(obj, (date, datetime)):
396 'year': str(obj.year).zfill(4),
397 'month': str(obj.month).zfill(2),
398 'day': str(obj.day).zfill(2)
400 return 'entries_by_day', [], kwargs
401 raise ViewCanNotProvideSubpath
404 def urlpatterns(self):
405 urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') +\
406 self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)$' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'entries_by_tag')
408 if self.tag_archive_page:
409 urlpatterns += patterns('',
410 url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
413 if self.entry_archive_page:
414 if self.entry_permalink_style in 'DMY':
415 urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')
416 if self.entry_permalink_style in 'DM':
417 urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_month')
418 if self.entry_permalink_style == 'D':
419 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')
421 if self.entry_permalink_style == 'D':
422 urlpatterns += patterns('',
423 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
425 elif self.entry_permalink_style == 'M':
426 urlpatterns += patterns('',
427 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
429 elif self.entry_permalink_style == 'Y':
430 urlpatterns += patterns('',
431 url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)$', self.entry_view)
433 elif self.entry_permalink_style == 'B':
434 urlpatterns += patterns('',
435 url((r'^%s/(?P<slug>[-\w]+)$' % self.entry_permalink_base), self.entry_view)
438 urlpatterns = patterns('',
439 url(r'^(?P<slug>[-\w]+)$', self.entry_view)
443 def get_context(self):
444 return {'blog': self.blog}
446 def get_entry_queryset(self):
447 return self.blog.entries.all()
449 def get_tag_queryset(self):
450 return self.blog.entry_tags
452 def get_all_entries(self, request, extra_context=None):
453 return self.get_entry_queryset(), extra_context
455 def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
456 if not self.entry_archive_page:
458 entries = self.get_entry_queryset()
460 entries = entries.filter(date__year=year)
462 entries = entries.filter(date__month=month)
464 entries = entries.filter(date__day=day)
466 context = extra_context or {}
467 context.update({'year': year, 'month': month, 'day': day})
468 return entries, context
470 def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
471 tag_slugs = tag_slugs.replace('+', '/').split('/')
472 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
477 # Raise a 404 on an incorrect slug.
478 found_slugs = [tag.slug for tag in tags]
479 for slug in tag_slugs:
480 if slug and slug not in found_slugs:
483 entries = self.get_entry_queryset()
485 entries = entries.filter(tags=tag)
487 context = extra_context or {}
488 context.update({'tags': tags})
490 return entries, context
492 def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
493 entries = self.get_entry_queryset()
495 entries = entries.filter(date__year=year)
497 entries = entries.filter(date__month=month)
499 entries = entries.filter(date__day=day)
501 entry = entries.get(slug=slug)
504 context = self.get_context()
505 context.update(extra_context or {})
506 context.update({'entry': entry})
507 return self.entry_page.render_to_response(request, extra_context=context)
509 def tag_archive_view(self, request, extra_context=None):
510 if not self.tag_archive_page:
512 context = self.get_context()
513 context.update(extra_context or {})
515 'tags': self.get_tag_queryset()
517 return self.tag_archive_page.render_to_response(request, extra_context=context)
519 def feed_view(self, get_items_attr, reverse_name):
520 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
522 def inner(request, extra_context=None, *args, **kwargs):
523 obj = self.get_object(request, *args, **kwargs)
524 feed = self.get_feed(obj, request, reverse_name)
525 items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
526 self.populate_feed(feed, items, request)
528 if 'tags' in extra_context:
529 tags = extra_context['tags']
530 feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure())
532 tags = obj.entry_tags
534 feed.feed['categories'] = [tag.name for tag in tags]
536 response = HttpResponse(mimetype=feed.mime_type)
537 feed.write(response, 'utf-8')
542 def process_page_items(self, request, items):
543 if self.entries_per_page:
544 page_num = request.GET.get('page', 1)
545 paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num)
547 'paginator': paginator,
548 'paginated_page': paginated_page,
549 self.item_context_var: items
553 self.item_context_var: items
555 return items, item_context
557 def title(self, obj):
560 def item_title(self, item):
563 def item_description(self, item):
566 def item_author_name(self, item):
567 return item.author.get_full_name()
569 def item_pubdate(self, item):
572 def item_categories(self, item):
573 return [tag.name for tag in item.tags.all()]
576 class Newsletter(Entity):
577 title = models.CharField(max_length=255)
578 slug = models.SlugField(max_length=255)
580 def __unicode__(self):
584 register_value_model(Newsletter)
587 class NewsletterArticle(Entity):
588 title = models.CharField(max_length=255)
589 slug = models.SlugField(max_length=255)
590 newsletter = models.ForeignKey(Newsletter, related_name='articles')
591 authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
592 date = models.DateTimeField(default=None)
593 lede = TemplateField(null=True, blank=True, verbose_name='Summary')
594 full_text = TemplateField(db_index=True)
595 tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True)
597 def save(self, *args, **kwargs):
598 if self.date is None:
599 self.date = datetime.now()
600 super(NewsletterArticle, self).save(*args, **kwargs)
602 def __unicode__(self):
606 get_latest_by = 'date'
608 unique_together = (('newsletter', 'slug'),)
611 register_value_model(NewsletterArticle)
614 class NewsletterIssue(Entity):
615 title = models.CharField(max_length=255)
616 slug = models.SlugField(max_length=255)
617 newsletter = models.ForeignKey(Newsletter, related_name='issues')
618 numbering = models.CharField(max_length=50, help_text='For example, 04.02 for volume 4, issue 2.')
619 articles = models.ManyToManyField(NewsletterArticle, related_name='issues')
621 def __unicode__(self):
625 ordering = ['-numbering']
626 unique_together = (('newsletter', 'numbering'),)
629 register_value_model(NewsletterIssue)
632 class NewsletterView(FeedView):
633 ARTICLE_PERMALINK_STYLE_CHOICES = (
634 ('D', 'Year, month, and day'),
635 ('M', 'Year and month'),
640 newsletter = models.ForeignKey(Newsletter, related_name='newsletterviews')
642 index_page = models.ForeignKey(Page, related_name='newsletter_index_related')
643 article_page = models.ForeignKey(Page, related_name='newsletter_article_related')
644 article_archive_page = models.ForeignKey(Page, related_name='newsletter_article_archive_related', null=True, blank=True)
645 issue_page = models.ForeignKey(Page, related_name='newsletter_issue_related')
646 issue_archive_page = models.ForeignKey(Page, related_name='newsletter_issue_archive_related', null=True, blank=True)
648 article_permalink_style = models.CharField(max_length=1, choices=ARTICLE_PERMALINK_STYLE_CHOICES)
649 article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
650 issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
652 item_context_var = 'articles'
653 object_attr = 'newsletter'
655 def __unicode__(self):
656 return "NewsletterView for %s" % self.newsletter.__unicode__()
658 def get_reverse_params(self, obj):
659 if isinstance(obj, NewsletterArticle):
660 if obj.newsletter == self.newsletter:
661 kwargs = {'slug': obj.slug}
662 if self.article_permalink_style in 'DMY':
663 kwargs.update({'year': str(obj.date.year).zfill(4)})
664 if self.article_permalink_style in 'DM':
665 kwargs.update({'month': str(obj.date.month).zfill(2)})
666 if self.article_permalink_style == 'D':
667 kwargs.update({'day': str(obj.date.day).zfill(2)})
668 return self.article_view, [], kwargs
669 elif isinstance(obj, NewsletterIssue):
670 if obj.newsletter == self.newsletter:
671 return 'issue', [], {'numbering': obj.numbering}
672 elif isinstance(obj, (date, datetime)):
674 'year': str(obj.year).zfill(4),
675 'month': str(obj.month).zfill(2),
676 'day': str(obj.day).zfill(2)
678 return 'articles_by_day', [], kwargs
679 raise ViewCanNotProvideSubpath
682 def urlpatterns(self):
683 urlpatterns = self.feed_patterns(r'^', 'get_all_articles', 'index_page', 'index') + patterns('',
684 url(r'^%s/(?P<numbering>.+)$' % self.issue_permalink_base, self.page_view('get_articles_by_issue', 'issue_page'), name='issue')
686 if self.issue_archive_page:
687 urlpatterns += patterns('',
688 url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
690 if self.article_archive_page:
691 urlpatterns += patterns('',
692 url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
694 if self.article_permalink_style in 'DMY':
695 urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
696 if self.article_permalink_style in 'DM':
697 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')
698 if self.article_permalink_style == 'D':
699 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')
701 if self.article_permalink_style == 'Y':
702 urlpatterns += patterns('',
703 url(r'^%s/(?P<year>\d{4})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
705 elif self.article_permalink_style == 'M':
706 urlpatterns += patterns('',
707 url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
709 elif self.article_permalink_style == 'D':
710 urlpatterns += patterns('',
711 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)
714 urlpatterns += patterns('',
715 url(r'^%s/(?P<slug>[-\w]+)$' % self.article_permalink_base, self.article_view)
720 def get_context(self):
721 return {'newsletter': self.newsletter}
723 def get_article_queryset(self):
724 return self.newsletter.articles.all()
726 def get_issue_queryset(self):
727 return self.newsletter.issues.all()
729 def get_all_articles(self, request, extra_context=None):
730 return self.get_article_queryset(), extra_context
732 def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
733 articles = self.get_article_queryset().filter(date__year=year)
735 articles = articles.filter(date__month=month)
737 articles = articles.filter(date__day=day)
738 return articles, extra_context
740 def get_articles_by_issue(self, request, numbering, extra_context=None):
742 issue = self.get_issue_queryset().get(numbering=numbering)
743 except NewsletterIssue.DoesNotExist:
745 context = extra_context or {}
746 context.update({'issue': issue})
747 return self.get_article_queryset().filter(issues=issue), context
749 def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
750 articles = self.get_article_queryset()
752 articles = articles.filter(date__year=year)
754 articles = articles.filter(date__month=month)
756 articles = articles.filter(date__day=day)
758 article = articles.get(slug=slug)
759 except NewsletterArticle.DoesNotExist:
761 context = self.get_context()
762 context.update(extra_context or {})
763 context.update({'article': article})
764 return self.article_page.render_to_response(request, extra_context=context)
766 def issue_archive_view(self, request, extra_context):
767 if not self.issue_archive_page:
769 context = self.get_context()
770 context.update(extra_context or {})
772 'issues': self.get_issue_queryset()
774 return self.issue_archive_page.render_to_response(request, extra_context=context)
776 def title(self, obj):
779 def item_title(self, item):
782 def item_description(self, item):
783 return item.full_text
785 def item_author_name(self, item):
786 authors = list(item.authors.all())
788 return "%s and %s" % (", ".join([author.get_full_name() for author in authors[:-1]]), authors[-1].get_full_name())
790 return authors[0].get_full_name()
794 def item_pubdate(self, item):
797 def item_categories(self, item):
798 return [tag.name for tag in item.tags.all()]