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