Merge branch 'patch-1' of git://github.com/lapilofu/philo into develop
[philo.git] / philo / contrib / penfield / models.py
1 # encoding: utf-8
2 from datetime import date, datetime
3
4 from django.conf import settings
5 from django.conf.urls.defaults import url, patterns, include
6 from django.db import models
7 from django.http import Http404, HttpResponse
8 from taggit.managers import TaggableManager
9 from taggit.models import Tag, TaggedItem
10
11 from philo.contrib.winer.models import FeedView
12 from philo.exceptions import ViewCanNotProvideSubpath
13 from philo.models import Entity, Page, register_value_model
14 from philo.models.fields import TemplateField
15 from philo.utils import paginate
16
17
18 class Blog(Entity):
19         """Represents a blog which can be posted to."""
20         #: The name of the :class:`Blog`, currently called 'title' for historical reasons.
21         title = models.CharField(max_length=255)
22         
23         #: A slug used to identify the :class:`Blog`.
24         slug = models.SlugField(max_length=255)
25         
26         def __unicode__(self):
27                 return self.title
28         
29         @property
30         def entry_tags(self):
31                 """Returns a :class:`QuerySet` of :class:`.Tag`\ s that are used on any entries in this blog."""
32                 entry_pks = list(self.entries.values_list('pk', flat=True))
33                 kwargs = {
34                         '%s__object_id__in' % TaggedItem.tag_relname(): entry_pks
35                 }
36                 return TaggedItem.tags_for(BlogEntry).filter(**kwargs)
37         
38         @property
39         def entry_dates(self):
40                 """Returns a dictionary of date :class:`QuerySet`\ s for years, months, and days for which there are entries."""
41                 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')}
42                 return dates
43
44
45 register_value_model(Blog)
46
47
48 class BlogEntry(Entity):
49         """Represents an entry in a :class:`Blog`."""
50         #: The title of the :class:`BlogEntry`.
51         title = models.CharField(max_length=255)
52         
53         #: A slug which identifies the :class:`BlogEntry`.
54         slug = models.SlugField(max_length=255)
55         
56         #: The :class:`Blog` which this entry has been posted to. Can be left blank to represent a "draft" status.
57         blog = models.ForeignKey(Blog, related_name='entries', blank=True, null=True)
58         
59         #: A :class:`ForeignKey` to the author. The model is either :setting:`PHILO_PERSON_MODULE` or :class:`auth.User`.
60         author = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='blogentries')
61         
62         #: The date and time which the :class:`BlogEntry` is considered posted at.
63         date = models.DateTimeField(default=None)
64         
65         #: The content of the :class:`BlogEntry`.
66         content = TemplateField()
67         
68         #: An optional brief excerpt from the :class:`BlogEntry`.
69         excerpt = TemplateField(blank=True, null=True)
70         
71         #: A ``django-taggit`` :class:`TaggableManager`.
72         tags = TaggableManager()
73         
74         def save(self, *args, **kwargs):
75                 if self.date is None:
76                         self.date = datetime.now()
77                 super(BlogEntry, self).save(*args, **kwargs)
78         
79         def __unicode__(self):
80                 return self.title
81         
82         class Meta:
83                 ordering = ['-date']
84                 verbose_name_plural = "blog entries"
85                 get_latest_by = "date"
86
87
88 register_value_model(BlogEntry)
89
90
91 class BlogView(FeedView):
92         """
93         A subclass of :class:`.FeedView` which handles patterns and feeds for a :class:`Blog` and its related :class:`entries <BlogEntry>`.
94         
95         """
96         ENTRY_PERMALINK_STYLE_CHOICES = (
97                 ('D', 'Year, month, and day'),
98                 ('M', 'Year and month'),
99                 ('Y', 'Year'),
100                 ('B', 'Custom base'),
101                 ('N', 'No base')
102         )
103         
104         #: The :class:`Blog` whose entries should be managed by this :class:`BlogView`
105         blog = models.ForeignKey(Blog, related_name='blogviews')
106         
107         #: The main page of the :class:`Blog` will be rendered with this :class:`.Page`.
108         index_page = models.ForeignKey(Page, related_name='blog_index_related')
109         #: The detail view of a :class:`BlogEntry` will be rendered with this :class:`Page`.
110         entry_page = models.ForeignKey(Page, related_name='blog_entry_related')
111         # TODO: entry_archive is misleading. Rename to ymd_page or timespan_page.
112         #: Views of :class:`BlogEntry` archives will be rendered with this :class:`Page` (optional).
113         entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True)
114         #: Views of :class:`BlogEntry` archives according to their :class:`.Tag`\ s will be rendered with this :class:`Page`.
115         tag_page = models.ForeignKey(Page, related_name='blog_tag_related')
116         #: The archive of all available tags will be rendered with this :class:`Page` (optional).
117         tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True)
118         #: This number will be passed directly into pagination for :class:`BlogEntry` list pages. Pagination will be disabled if this is left blank.
119         entries_per_page = models.IntegerField(blank=True, null=True)
120         
121         #: Depending on the needs of the site, different permalink styles may be appropriate. Example subpaths are provided for a :class:`BlogEntry` posted on May 2nd, 2011 with a slug of "hello". The choices are:
122         #: 
123         #:      * Year, month, and day - ``2011/05/02/hello``
124         #:      * Year and month - ``2011/05/hello``
125         #:      * Year - ``2011/hello``
126         #:      * Custom base - :attr:`entry_permalink_base`\ ``/hello``
127         #:      * No base - ``hello``
128         entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
129         #: If the :attr:`entry_permalink_style` is set to "Custom base" then the value of this field will be used as the base subpath for year/month/day entry archive pages and entry detail pages. Default: "entries"
130         entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
131         #: This will be used as the base for the views of :attr:`tag_page` and :attr:`tag_archive_page`. Default: "tags"
132         tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
133         
134         item_context_var = 'entries'
135         
136         def __unicode__(self):
137                 return u'BlogView for %s' % self.blog.title
138         
139         def get_reverse_params(self, obj):
140                 if isinstance(obj, BlogEntry):
141                         if obj.blog_id == self.blog_id:
142                                 kwargs = {'slug': obj.slug}
143                                 if self.entry_permalink_style in 'DMY':
144                                         kwargs.update({'year': str(obj.date.year).zfill(4)})
145                                         if self.entry_permalink_style in 'DM':
146                                                 kwargs.update({'month': str(obj.date.month).zfill(2)})
147                                                 if self.entry_permalink_style == 'D':
148                                                         kwargs.update({'day': str(obj.date.day).zfill(2)})
149                                 return self.entry_view, [], kwargs
150                 elif isinstance(obj, Tag) or (isinstance(obj, models.query.QuerySet) and obj.model == Tag and obj):
151                         if isinstance(obj, Tag):
152                                 obj = [obj]
153                         slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset(self.blog)]
154                         if slugs:
155                                 return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)}
156                 elif isinstance(obj, (date, datetime)):
157                         kwargs = {
158                                 'year': str(obj.year).zfill(4),
159                                 'month': str(obj.month).zfill(2),
160                                 'day': str(obj.day).zfill(2)
161                         }
162                         return 'entries_by_day', [], kwargs
163                 raise ViewCanNotProvideSubpath
164         
165         @property
166         def urlpatterns(self):
167                 urlpatterns = self.feed_patterns(r'^', 'get_entries', 'index_page', 'index') +\
168                         self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_entries', 'tag_page', 'entries_by_tag')
169                 
170                 if self.tag_archive_page_id:
171                         urlpatterns += patterns('',
172                                 url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
173                         )
174                 
175                 if self.entry_archive_page_id:
176                         if self.entry_permalink_style in 'DMY':
177                                 urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries', 'entry_archive_page', 'entries_by_year')
178                                 if self.entry_permalink_style in 'DM':
179                                         urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries', 'entry_archive_page', 'entries_by_month')
180                                         if self.entry_permalink_style == 'D':
181                                                 urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries', 'entry_archive_page', 'entries_by_day')
182                 
183                 if self.entry_permalink_style == 'D':
184                         urlpatterns += patterns('',
185                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
186                         )
187                 elif self.entry_permalink_style == 'M':
188                         urlpatterns += patterns('',
189                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
190                         )
191                 elif self.entry_permalink_style == 'Y':
192                         urlpatterns += patterns('',
193                                 url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)$', self.entry_view)
194                         )
195                 elif self.entry_permalink_style == 'B':
196                         urlpatterns += patterns('',
197                                 url((r'^%s/(?P<slug>[-\w]+)$' % self.entry_permalink_base), self.entry_view)
198                         )
199                 else:
200                         urlpatterns += patterns('',
201                                 url(r'^(?P<slug>[-\w]+)$', self.entry_view)
202                         )
203                 return urlpatterns
204         
205         def get_entry_queryset(self, obj):
206                 """Returns the default :class:`QuerySet` of :class:`BlogEntry` instances for the :class:`BlogView` - all entries that are considered posted in the past. This allows for scheduled posting of entries."""
207                 return obj.entries.filter(date__lte=datetime.now())
208         
209         def get_tag_queryset(self, obj):
210                 """Returns the default :class:`QuerySet` of :class:`.Tag`\ s for the :class:`BlogView`'s :meth:`get_entries_by_tag` and :meth:`tag_archive_view`."""
211                 return obj.entry_tags
212         
213         def get_object(self, request, year=None, month=None, day=None, tag_slugs=None):
214                 """Returns a dictionary representing the parameters for a feed which will be exposed."""
215                 if tag_slugs is None:
216                         tags = None
217                 else:
218                         tag_slugs = tag_slugs.replace('+', '/').split('/')
219                         tags = self.get_tag_queryset(self.blog).filter(slug__in=tag_slugs)
220                         if not tags:
221                                 raise Http404
222                         
223                         # Raise a 404 on an incorrect slug.
224                         found_slugs = set([tag.slug for tag in tags])
225                         for slug in tag_slugs:
226                                 if slug and slug not in found_slugs:
227                                         raise Http404
228                 
229                 try:
230                         if year and month and day:
231                                 context_date = date(int(year), int(month), int(day))
232                         elif year and month:
233                                 context_date = date(int(year), int(month), 1)
234                         elif year:
235                                 context_date = date(int(year), 1, 1)
236                         else:
237                                 context_date = None
238                 except TypeError, ValueError:
239                         context_date = None
240                 
241                 return {
242                         'blog': self.blog,
243                         'tags': tags,
244                         'year': year,
245                         'month': month,
246                         'day': day,
247                         'date': context_date
248                 }
249         
250         def get_entries(self, obj, request, year=None, month=None, day=None, tag_slugs=None, extra_context=None):
251                 """Returns the :class:`BlogEntry` objects which will be exposed for the given object, as returned from :meth:`get_object`."""
252                 entries = self.get_entry_queryset(obj['blog'])
253                 
254                 if obj['tags'] is not None:
255                         tags = obj['tags']
256                         for tag in tags:
257                                 entries = entries.filter(tags=tag)
258                 
259                 if obj['date'] is not None:
260                         if year:
261                                 entries = entries.filter(date__year=year)
262                         if month:
263                                 entries = entries.filter(date__month=month)
264                         if day:
265                                 entries = entries.filter(date__day=day)
266                 
267                 context = extra_context or {}
268                 context.update(obj)
269                 
270                 return entries, context
271         
272         def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
273                 """Renders :attr:`entry_page` with the entry specified by the given parameters."""
274                 entries = self.get_entry_queryset(self.blog)
275                 if year:
276                         entries = entries.filter(date__year=year)
277                 if month:
278                         entries = entries.filter(date__month=month)
279                 if day:
280                         entries = entries.filter(date__day=day)
281                 try:
282                         entry = entries.get(slug=slug)
283                 except:
284                         raise Http404
285                 context = self.get_context()
286                 context.update(extra_context or {})
287                 context.update({'entry': entry})
288                 return self.entry_page.render_to_response(request, extra_context=context)
289         
290         def tag_archive_view(self, request, extra_context=None):
291                 """Renders :attr:`tag_archive_page` with the result of :meth:`get_tag_queryset` added to the context."""
292                 if not self.tag_archive_page:
293                         raise Http404
294                 context = self.get_context()
295                 context.update(extra_context or {})
296                 context.update({
297                         'tags': self.get_tag_queryset(self.blog)
298                 })
299                 return self.tag_archive_page.render_to_response(request, extra_context=context)
300         
301         def process_page_items(self, request, items):
302                 """Overrides :meth:`.FeedView.process_page_items` to add pagination."""
303                 if self.entries_per_page:
304                         page_num = request.GET.get('page', 1)
305                         paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num)
306                         item_context = {
307                                 'paginator': paginator,
308                                 'paginated_page': paginated_page,
309                                 self.item_context_var: items
310                         }
311                 else:
312                         item_context = {
313                                 self.item_context_var: items
314                         }
315                 return items, item_context
316         
317         def title(self, obj):
318                 title = obj['blog'].title
319                 if obj['tags']:
320                         title += u" â€“ %s" % u", ".join((tag.name for tag in obj['tags']))
321                 date = obj['date']
322                 if date:
323                         if obj['day']:
324                                 datestr = date.strftime("%F %j, %Y")
325                         elif obj['month']:
326                                 datestr = date.strftime("%F, %Y")
327                         elif obj['year']:
328                                 datestr = date.strftime("%Y")
329                         title += u" â€“ %s" % datestr
330                 return title
331         
332         def categories(self, obj):
333                 tags = obj['tags']
334                 if tags:
335                         return (tag.name for tag in tags)
336                 return None
337         
338         def item_title(self, item):
339                 return item.title
340         
341         def item_description(self, item):
342                 return item.content
343         
344         def item_author_name(self, item):
345                 return item.author.get_full_name()
346         
347         def item_pubdate(self, item):
348                 return item.date
349         
350         def item_categories(self, item):
351                 return [tag.name for tag in item.tags.all()]
352
353
354 class Newsletter(Entity):
355         """Represents a newsletter which will contain :class:`articles <NewsletterArticle>` organized into :class:`issues <NewsletterIssue>`."""
356         #: The name of the :class:`Newsletter`, currently callse 'title' for historical reasons.
357         title = models.CharField(max_length=255)
358         #: A slug used to identify the :class:`Newsletter`.
359         slug = models.SlugField(max_length=255)
360         
361         def __unicode__(self):
362                 return self.title
363
364
365 register_value_model(Newsletter)
366
367
368 class NewsletterArticle(Entity):
369         """Represents an article in a :class:`Newsletter`"""
370         #: The title of the :class:`NewsletterArticle`.
371         title = models.CharField(max_length=255)
372         #: A slug which identifies the :class:`NewsletterArticle`.
373         slug = models.SlugField(max_length=255)
374         #: A :class:`ForeignKey` to :class:`Newsletter` representing the newsletter which this article was written for.
375         newsletter = models.ForeignKey(Newsletter, related_name='articles')
376         #: A :class:`ManyToManyField` to the author(s) of the :class:`NewsletterArticle`. The model is either :setting:`PHILO_PERSON_MODULE` or :class:`auth.User`.
377         authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
378         #: The date and time which the :class:`NewsletterArticle` is considered published at.
379         date = models.DateTimeField(default=None)
380         #: A :class:`.TemplateField` containing an optional short summary of the article, meant to grab a reader's attention and draw them in.
381         lede = TemplateField(null=True, blank=True, verbose_name='Summary')
382         #: A :class:`.TemplateField` containing the full text of the article.
383         full_text = TemplateField(db_index=True)
384         #: A ``django-taggit`` :class:`TaggableManager`.
385         tags = TaggableManager()
386         
387         def save(self, *args, **kwargs):
388                 if self.date is None:
389                         self.date = datetime.now()
390                 super(NewsletterArticle, self).save(*args, **kwargs)
391         
392         def __unicode__(self):
393                 return self.title
394         
395         class Meta:
396                 get_latest_by = 'date'
397                 ordering = ['-date']
398                 unique_together = (('newsletter', 'slug'),)
399
400
401 register_value_model(NewsletterArticle)
402
403
404 class NewsletterIssue(Entity):
405         """Represents an issue of the newsletter."""
406         #: The title of the :class:`NewsletterIssue`.
407         title = models.CharField(max_length=255)
408         #: A slug which identifies the :class:`NewsletterIssue`.
409         slug = models.SlugField(max_length=255)
410         #: A :class:`ForeignKey` to the :class:`Newsletter` which this issue belongs to.
411         newsletter = models.ForeignKey(Newsletter, related_name='issues')
412         #: The numbering of the issue - for example, 04.02 for volume 4, issue 2. This is an instance of :class:`CharField` to allow any arbitrary numbering system.
413         numbering = models.CharField(max_length=50, help_text='For example, 04.02 for volume 4, issue 2.')
414         #: A :class:`ManyToManyField` to articles belonging to this issue.
415         articles = models.ManyToManyField(NewsletterArticle, related_name='issues')
416         
417         def __unicode__(self):
418                 return self.title
419         
420         class Meta:
421                 ordering = ['-numbering']
422                 unique_together = (('newsletter', 'numbering'),)
423
424
425 register_value_model(NewsletterIssue)
426
427
428 class NewsletterView(FeedView):
429         """A subclass of :class:`.FeedView` which handles patterns and feeds for a :class:`Newsletter` and its related :class:`articles <NewsletterArticle>`."""
430         ARTICLE_PERMALINK_STYLE_CHOICES = (
431                 ('D', 'Year, month, and day'),
432                 ('M', 'Year and month'),
433                 ('Y', 'Year'),
434                 ('S', 'Slug only')
435         )
436         
437         #: A :class:`ForeignKey` to the :class:`Newsletter` managed by this :class:`NewsletterView`.
438         newsletter = models.ForeignKey(Newsletter, related_name='newsletterviews')
439         
440         #: A :class:`ForeignKey` to the :class:`Page` used to render the main page of this :class:`NewsletterView`.
441         index_page = models.ForeignKey(Page, related_name='newsletter_index_related')
442         #: A :class:`ForeignKey` to the :class:`Page` used to render the detail view of a :class:`NewsletterArticle` for this :class:`NewsletterView`.
443         article_page = models.ForeignKey(Page, related_name='newsletter_article_related')
444         #: A :class:`ForeignKey` to the :class:`Page` used to render the :class:`NewsletterArticle` archive pages for this :class:`NewsletterView`.
445         article_archive_page = models.ForeignKey(Page, related_name='newsletter_article_archive_related', null=True, blank=True)
446         #: A :class:`ForeignKey` to the :class:`Page` used to render the detail view of a :class:`NewsletterIssue` for this :class:`NewsletterView`.
447         issue_page = models.ForeignKey(Page, related_name='newsletter_issue_related')
448         #: A :class:`ForeignKey` to the :class:`Page` used to render the :class:`NewsletterIssue` archive pages for this :class:`NewsletterView`.
449         issue_archive_page = models.ForeignKey(Page, related_name='newsletter_issue_archive_related', null=True, blank=True)
450         
451         #: Depending on the needs of the site, different permalink styles may be appropriate. Example subpaths are provided for a :class:`NewsletterArticle` posted on May 2nd, 2011 with a slug of "hello". The choices are:
452         #: 
453         #:      * Year, month, and day - :attr:`article_permalink_base`\ ``/2011/05/02/hello``
454         #:      * Year and month - :attr:`article_permalink_base`\ ``/2011/05/hello``
455         #:      * Year - :attr:`article_permalink_base`\ ``/2011/hello``
456         #:      * Slug only - :attr:`article_permalink_base`\ ``/hello``
457         article_permalink_style = models.CharField(max_length=1, choices=ARTICLE_PERMALINK_STYLE_CHOICES)
458         #: This will be used as the base subpath for year/month/day article archive pages and article detail pages. Default: "articles"
459         article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
460         #: This will be used as the base subpath for issue detail pages and the issue archive page.
461         issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
462         
463         item_context_var = 'articles'
464         object_attr = 'newsletter'
465         
466         def __unicode__(self):
467                 return "NewsletterView for %s" % self.newsletter.__unicode__()
468         
469         def get_reverse_params(self, obj):
470                 if isinstance(obj, NewsletterArticle):
471                         if obj.newsletter_id == self.newsletter_id:
472                                 kwargs = {'slug': obj.slug}
473                                 if self.article_permalink_style in 'DMY':
474                                         kwargs.update({'year': str(obj.date.year).zfill(4)})
475                                         if self.article_permalink_style in 'DM':
476                                                 kwargs.update({'month': str(obj.date.month).zfill(2)})
477                                                 if self.article_permalink_style == 'D':
478                                                         kwargs.update({'day': str(obj.date.day).zfill(2)})
479                                 return self.article_view, [], kwargs
480                 elif isinstance(obj, NewsletterIssue):
481                         if obj.newsletter_id == self.newsletter_id:
482                                 return 'issue', [], {'numbering': obj.numbering}
483                 elif isinstance(obj, (date, datetime)):
484                         kwargs = {
485                                 'year': str(obj.year).zfill(4),
486                                 'month': str(obj.month).zfill(2),
487                                 'day': str(obj.day).zfill(2)
488                         }
489                         return 'articles_by_day', [], kwargs
490                 raise ViewCanNotProvideSubpath
491         
492         @property
493         def urlpatterns(self):
494                 urlpatterns = self.feed_patterns(r'^', 'get_all_articles', 'index_page', 'index') + patterns('',
495                         url(r'^%s/(?P<numbering>.+)$' % self.issue_permalink_base, self.page_view('get_articles_by_issue', 'issue_page'), name='issue')
496                 )
497                 if self.issue_archive_page_id:
498                         urlpatterns += patterns('',
499                                 url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
500                         )
501                 if self.article_archive_page_id:
502                         urlpatterns += self.feed_patterns(r'^%s' % self.article_permalink_base, 'get_all_articles', 'article_archive_page', 'articles')
503                         if self.article_permalink_style in 'DMY':
504                                 urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
505                                 if self.article_permalink_style in 'DM':
506                                         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')
507                                         if self.article_permalink_style == 'D':
508                                                 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')
509                 
510                 if self.article_permalink_style == 'Y':
511                         urlpatterns += patterns('',
512                                 url(r'^%s/(?P<year>\d{4})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
513                         )
514                 elif self.article_permalink_style == 'M':
515                         urlpatterns += patterns('',
516                                 url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
517                         )
518                 elif self.article_permalink_style == 'D':
519                         urlpatterns += patterns('',
520                                 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)
521                         )
522                 else:   
523                         urlpatterns += patterns('',
524                                 url(r'^%s/(?P<slug>[-\w]+)$' % self.article_permalink_base, self.article_view)
525                         )
526                 
527                 return urlpatterns
528         
529         def get_context(self):
530                 return {'newsletter': self.newsletter}
531         
532         def get_article_queryset(self, obj):
533                 """Returns the default :class:`QuerySet` of :class:`NewsletterArticle` instances for the :class:`NewsletterView` - all articles that are considered posted in the past. This allows for scheduled posting of articles."""
534                 return obj.articles.filter(date__lte=datetime.now())
535         
536         def get_issue_queryset(self, obj):
537                 """Returns the default :class:`QuerySet` of :class:`NewsletterIssue` instances for the :class:`NewsletterView`."""
538                 return obj.issues.all()
539         
540         def get_all_articles(self, obj, request, extra_context=None):
541                 """Used to generate :meth:`~.FeedView.feed_patterns` for all entries."""
542                 return self.get_article_queryset(obj), extra_context
543         
544         def get_articles_by_ymd(self, obj, request, year, month=None, day=None, extra_context=None):
545                 """Used to generate :meth:`~.FeedView.feed_patterns` for a specific year, month, and day."""
546                 articles = self.get_article_queryset(obj).filter(date__year=year)
547                 if month:
548                         articles = articles.filter(date__month=month)
549                 if day:
550                         articles = articles.filter(date__day=day)
551                 return articles, extra_context
552         
553         def get_articles_by_issue(self, obj, request, numbering, extra_context=None):
554                 """Used to generate :meth:`~.FeedView.feed_patterns` for articles from a certain issue."""
555                 try:
556                         issue = self.get_issue_queryset(obj).get(numbering=numbering)
557                 except NewsletterIssue.DoesNotExist:
558                         raise Http404
559                 context = extra_context or {}
560                 context.update({'issue': issue})
561                 return self.get_article_queryset(obj).filter(issues=issue), context
562         
563         def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
564                 """Renders :attr:`article_page` with the article specified by the given parameters."""
565                 articles = self.get_article_queryset(self.newsletter)
566                 if year:
567                         articles = articles.filter(date__year=year)
568                 if month:
569                         articles = articles.filter(date__month=month)
570                 if day:
571                         articles = articles.filter(date__day=day)
572                 try:
573                         article = articles.get(slug=slug)
574                 except NewsletterArticle.DoesNotExist:
575                         raise Http404
576                 context = self.get_context()
577                 context.update(extra_context or {})
578                 context.update({'article': article})
579                 return self.article_page.render_to_response(request, extra_context=context)
580         
581         def issue_archive_view(self, request, extra_context):
582                 """Renders :attr:`issue_archive_page` with the result of :meth:`get_issue_queryset` added to the context."""
583                 if not self.issue_archive_page:
584                         raise Http404
585                 context = self.get_context()
586                 context.update(extra_context or {})
587                 context.update({
588                         'issues': self.get_issue_queryset(self.newsletter)
589                 })
590                 return self.issue_archive_page.render_to_response(request, extra_context=context)
591         
592         def title(self, obj):
593                 return obj.title
594         
595         def item_title(self, item):
596                 return item.title
597         
598         def item_description(self, item):
599                 return item.full_text
600         
601         def item_author_name(self, item):
602                 authors = list(item.authors.all())
603                 if len(authors) > 1:
604                         return "%s and %s" % (", ".join([author.get_full_name() for author in authors[:-1]]), authors[-1].get_full_name())
605                 elif authors:
606                         return authors[0].get_full_name()
607                 else:
608                         return ''
609         
610         def item_pubdate(self, item):
611                 return item.date
612         
613         def item_categories(self, item):
614                 return [tag.name for tag in item.tags.all()]