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