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