Removed penfield's reliance on the Titled model and marked it as deprecated.
[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.contrib.sites.models import Site, RequestSite
6 from django.contrib.syndication.views import add_domain
7 from django.db import models
8 from django.http import Http404, HttpResponse
9 from django.template import RequestContext, Template as DjangoTemplate
10 from django.utils import feedgenerator, tzinfo
11 from django.utils.datastructures import SortedDict
12 from django.utils.encoding import smart_unicode, force_unicode
13 from django.utils.html import escape
14
15 from philo.contrib.penfield.exceptions import HttpNotAcceptable
16 from philo.contrib.penfield.middleware import http_not_acceptable
17 from philo.contrib.penfield.validators import validate_pagination_count
18 from philo.exceptions import ViewCanNotProvideSubpath
19 from philo.models import Tag, Entity, MultiView, Page, register_value_model, TemplateField, Template
20 from philo.utils import paginate
21
22 try:
23         import mimeparse
24 except:
25         mimeparse = None
26
27
28 ATOM = feedgenerator.Atom1Feed.mime_type
29 RSS = feedgenerator.Rss201rev2Feed.mime_type
30 FEEDS = SortedDict([
31         (ATOM, feedgenerator.Atom1Feed),
32         (RSS, feedgenerator.Rss201rev2Feed),
33 ])
34 FEED_CHOICES = (
35         (ATOM, "Atom"),
36         (RSS, "RSS"),
37 )
38
39
40 class FeedView(MultiView):
41         """
42         The FeedView expects to handle a number of different feeds for the
43         same object - i.e. patterns for a blog to handle all entries or
44         just entries for a certain year/month/day.
45         
46         This class would subclass django.contrib.syndication.views.Feed, but
47         that would make it callable, which causes problems.
48         """
49         feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM)
50         feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
51         feeds_enabled = models.BooleanField(default=True)
52         feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.")
53         
54         item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
55         item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
56         
57         item_context_var = 'items'
58         object_attr = 'object'
59         
60         description = ""
61         
62         def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
63                 """
64                 Given the name to be used to reverse this view and the names of
65                 the attributes for the function that fetches the objects, returns
66                 patterns suitable for inclusion in urlpatterns.
67                 """
68                 urlpatterns = patterns('')
69                 if self.feeds_enabled:
70                         feed_reverse_name = "%s_feed" % reverse_name
71                         feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name))
72                         feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
73                         urlpatterns += patterns('',
74                                 url(feed_pattern, feed_view, name=feed_reverse_name),
75                         )
76                 urlpatterns += patterns('',
77                         url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
78                 )
79                 return urlpatterns
80         
81         def get_object(self, request, **kwargs):
82                 return getattr(self, self.object_attr)
83         
84         def feed_view(self, get_items_attr, reverse_name):
85                 """
86                 Returns a view function that renders a list of items as a feed.
87                 """
88                 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
89                 
90                 def inner(request, extra_context=None, *args, **kwargs):
91                         obj = self.get_object(request, *args, **kwargs)
92                         feed = self.get_feed(obj, request, reverse_name)
93                         items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
94                         self.populate_feed(feed, items, request)
95                         
96                         response = HttpResponse(mimetype=feed.mime_type)
97                         feed.write(response, 'utf-8')
98                         return response
99                 
100                 return inner
101         
102         def page_view(self, get_items_attr, page_attr):
103                 """
104                 Returns a view function that renders a list of items as a page.
105                 """
106                 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
107                 page = isinstance(page_attr, Page) and page_attr or getattr(self, page_attr)
108                 
109                 def inner(request, extra_context=None, *args, **kwargs):
110                         items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
111                         items, item_context = self.process_page_items(request, items)
112                         
113                         context = self.get_context()
114                         context.update(extra_context or {})
115                         context.update(item_context or {})
116                         
117                         return page.render_to_response(request, extra_context=context)
118                 return inner
119         
120         def process_page_items(self, request, items):
121                 """
122                 Hook for handling any extra processing of items based on a
123                 request, such as pagination or searching. This method is
124                 expected to return a list of items and a dictionary to be
125                 added to the page context.
126                 """
127                 item_context = {
128                         self.item_context_var: items
129                 }
130                 return items, item_context
131         
132         def get_feed_type(self, request):
133                 feed_type = self.feed_type
134                 if feed_type not in FEEDS:
135                         feed_type = FEEDS.keys()[0]
136                 accept = request.META.get('HTTP_ACCEPT')
137                 if accept and feed_type not in accept and "*/*" not in accept and "%s/*" % feed_type.split("/")[0] not in accept:
138                         # Wups! They aren't accepting the chosen format. Is there another format we can use?
139                         if mimeparse:
140                                 feed_type = mimeparse.best_match(FEEDS.keys(), accept)
141                         else:
142                                 for feed_type in FEEDS.keys():
143                                         if feed_type in accept or "%s/*" % feed_type.split("/")[0] in accept:
144                                                 break
145                                 else:
146                                         feed_type = None
147                         if not feed_type:
148                                 raise HttpNotAcceptable
149                 return FEEDS[feed_type]
150         
151         def get_feed(self, obj, request, reverse_name):
152                 """
153                 Returns an unpopulated feedgenerator.DefaultFeed object for this object.
154                 """
155                 try:
156                         current_site = Site.objects.get_current()
157                 except Site.DoesNotExist:
158                         current_site = RequestSite(request)
159                 
160                 feed_type = self.get_feed_type(request)
161                 node = request.node
162                 link = node.get_absolute_url(with_domain=True, request=request, secure=request.is_secure())
163                 
164                 feed = feed_type(
165                         title = self.__get_dynamic_attr('title', obj),
166                         subtitle = self.__get_dynamic_attr('subtitle', obj),
167                         link = link,
168                         description = self.__get_dynamic_attr('description', obj),
169                         language = settings.LANGUAGE_CODE.decode(),
170                         feed_url = add_domain(
171                                 current_site.domain,
172                                 self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node.subpath, with_domain=True, request=request, secure=request.is_secure()),
173                                 request.is_secure()
174                         ),
175                         author_name = self.__get_dynamic_attr('author_name', obj),
176                         author_link = self.__get_dynamic_attr('author_link', obj),
177                         author_email = self.__get_dynamic_attr('author_email', obj),
178                         categories = self.__get_dynamic_attr('categories', obj),
179                         feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
180                         feed_guid = self.__get_dynamic_attr('feed_guid', obj),
181                         ttl = self.__get_dynamic_attr('ttl', obj),
182                         **self.feed_extra_kwargs(obj)
183                 )
184                 return feed
185         
186         def populate_feed(self, feed, items, request):
187                 if self.item_title_template:
188                         title_template = DjangoTemplate(self.item_title_template.code)
189                 else:
190                         title_template = None
191                 if self.item_description_template:
192                         description_template = DjangoTemplate(self.item_description_template.code)
193                 else:
194                         description_template = None
195                 
196                 node = request.node
197                 try:
198                         current_site = Site.objects.get_current()
199                 except Site.DoesNotExist:
200                         current_site = RequestSite(request)
201                 
202                 if self.feed_length is not None:
203                         items = items[:self.feed_length]
204                 
205                 for item in items:
206                         if title_template is not None:
207                                 title = title_template.render(RequestContext(request, {'obj': item}))
208                         else:
209                                 title = self.__get_dynamic_attr('item_title', item)
210                         if description_template is not None:
211                                 description = description_template.render(RequestContext(request, {'obj': item}))
212                         else:
213                                 description = self.__get_dynamic_attr('item_description', item)
214                         
215                         link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
216                         
217                         enc = None
218                         enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
219                         if enc_url:
220                                 enc = feedgenerator.Enclosure(
221                                         url = smart_unicode(add_domain(
222                                                         current_site.domain,
223                                                         enc_url,
224                                                         request.is_secure()
225                                         )),
226                                         length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
227                                         mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
228                                 )
229                         author_name = self.__get_dynamic_attr('item_author_name', item)
230                         if author_name is not None:
231                                 author_email = self.__get_dynamic_attr('item_author_email', item)
232                                 author_link = self.__get_dynamic_attr('item_author_link', item)
233                         else:
234                                 author_email = author_link = None
235                         
236                         pubdate = self.__get_dynamic_attr('item_pubdate', item)
237                         if pubdate and not pubdate.tzinfo:
238                                 ltz = tzinfo.LocalTimezone(pubdate)
239                                 pubdate = pubdate.replace(tzinfo=ltz)
240                         
241                         feed.add_item(
242                                 title = title,
243                                 link = link,
244                                 description = description,
245                                 unique_id = self.__get_dynamic_attr('item_guid', item, link),
246                                 enclosure = enc,
247                                 pubdate = pubdate,
248                                 author_name = author_name,
249                                 author_email = author_email,
250                                 author_link = author_link,
251                                 categories = self.__get_dynamic_attr('item_categories', item),
252                                 item_copyright = self.__get_dynamic_attr('item_copyright', item),
253                                 **self.item_extra_kwargs(item)
254                         )
255         
256         def __get_dynamic_attr(self, attname, obj, default=None):
257                 try:
258                         attr = getattr(self, attname)
259                 except AttributeError:
260                         return default
261                 if callable(attr):
262                         # Check func_code.co_argcount rather than try/excepting the
263                         # function and catching the TypeError, because something inside
264                         # the function may raise the TypeError. This technique is more
265                         # accurate.
266                         if hasattr(attr, 'func_code'):
267                                 argcount = attr.func_code.co_argcount
268                         else:
269                                 argcount = attr.__call__.func_code.co_argcount
270                         if argcount == 2: # one argument is 'self'
271                                 return attr(obj)
272                         else:
273                                 return attr()
274                 return attr
275         
276         def feed_extra_kwargs(self, obj):
277                 """
278                 Returns an extra keyword arguments dictionary that is used when
279                 initializing the feed generator.
280                 """
281                 return {}
282         
283         def item_extra_kwargs(self, item):
284                 """
285                 Returns an extra keyword arguments dictionary that is used with
286                 the `add_item` call of the feed generator.
287                 """
288                 return {}
289         
290         def item_title(self, item):
291                 return escape(force_unicode(item))
292         
293         def item_description(self, item):
294                 return force_unicode(item)
295         
296         class Meta:
297                 abstract=True
298
299
300 class Blog(Entity):
301         title = models.CharField(max_length=255)
302         slug = models.SlugField(max_length=255)
303         
304         def __unicode__(self):
305                 return self.title
306         
307         @property
308         def entry_tags(self):
309                 """ Returns a QuerySet of Tags that are used on any entries in this blog. """
310                 return Tag.objects.filter(blogentries__blog=self).distinct()
311         
312         @property
313         def entry_dates(self):
314                 dates = {'year': self.entries.dates('date', 'year', order='DESC'), 'month': self.entries.dates('date', 'month', order='DESC'), 'day': self.entries.dates('date', 'day', order='DESC')}
315                 return dates
316
317
318 register_value_model(Blog)
319
320
321 class BlogEntry(Entity):
322         title = models.CharField(max_length=255)
323         slug = models.SlugField(max_length=255)
324         blog = models.ForeignKey(Blog, related_name='entries', blank=True, null=True)
325         author = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='blogentries')
326         date = models.DateTimeField(default=None)
327         content = models.TextField()
328         excerpt = models.TextField(blank=True, null=True)
329         tags = models.ManyToManyField(Tag, related_name='blogentries', blank=True, null=True)
330         
331         def save(self, *args, **kwargs):
332                 if self.date is None:
333                         self.date = datetime.now()
334                 super(BlogEntry, self).save(*args, **kwargs)
335         
336         def __unicode__(self):
337                 return self.title
338         
339         class Meta:
340                 ordering = ['-date']
341                 verbose_name_plural = "blog entries"
342                 get_latest_by = "date"
343
344
345 register_value_model(BlogEntry)
346
347
348 class BlogView(FeedView):
349         ENTRY_PERMALINK_STYLE_CHOICES = (
350                 ('D', 'Year, month, and day'),
351                 ('M', 'Year and month'),
352                 ('Y', 'Year'),
353                 ('B', 'Custom base'),
354                 ('N', 'No base')
355         )
356         
357         blog = models.ForeignKey(Blog, related_name='blogviews')
358         
359         index_page = models.ForeignKey(Page, related_name='blog_index_related')
360         entry_page = models.ForeignKey(Page, related_name='blog_entry_related')
361         # TODO: entry_archive is misleading. Rename to ymd_page or timespan_page.
362         entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True)
363         tag_page = models.ForeignKey(Page, related_name='blog_tag_related')
364         tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True)
365         entries_per_page = models.IntegerField(blank=True, validators=[validate_pagination_count], null=True)
366         
367         entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
368         entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
369         tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
370         
371         item_context_var = 'entries'
372         object_attr = 'blog'
373         
374         def __unicode__(self):
375                 return u'BlogView for %s' % self.blog.title
376         
377         def get_reverse_params(self, obj):
378                 if isinstance(obj, BlogEntry):
379                         if obj.blog == self.blog:
380                                 kwargs = {'slug': obj.slug}
381                                 if self.entry_permalink_style in 'DMY':
382                                         kwargs.update({'year': str(obj.date.year).zfill(4)})
383                                         if self.entry_permalink_style in 'DM':
384                                                 kwargs.update({'month': str(obj.date.month).zfill(2)})
385                                                 if self.entry_permalink_style == 'D':
386                                                         kwargs.update({'day': str(obj.date.day).zfill(2)})
387                                 return self.entry_view, [], kwargs
388                 elif isinstance(obj, Tag) or (isinstance(obj, models.query.QuerySet) and obj.model == Tag and obj):
389                         if isinstance(obj, Tag):
390                                 obj = [obj]
391                         slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()]
392                         if slugs:
393                                 return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)}
394                 elif isinstance(obj, (date, datetime)):
395                         kwargs = {
396                                 'year': str(obj.year).zfill(4),
397                                 'month': str(obj.month).zfill(2),
398                                 'day': str(obj.day).zfill(2)
399                         }
400                         return 'entries_by_day', [], kwargs
401                 raise ViewCanNotProvideSubpath
402         
403         @property
404         def urlpatterns(self):
405                 urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') +\
406                         self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)$' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'entries_by_tag')
407                 
408                 if self.tag_archive_page:
409                         urlpatterns += patterns('',
410                                 url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
411                         )
412                 
413                 if self.entry_archive_page:
414                         if self.entry_permalink_style in 'DMY':
415                                 urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')
416                                 if self.entry_permalink_style in 'DM':
417                                         urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_month')
418                                         if self.entry_permalink_style == 'D':
419                                                 urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')
420                 
421                 if self.entry_permalink_style == 'D':
422                         urlpatterns += patterns('',
423                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
424                         )
425                 elif self.entry_permalink_style == 'M':
426                         urlpatterns += patterns('',
427                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
428                         )
429                 elif self.entry_permalink_style == 'Y':
430                         urlpatterns += patterns('',
431                                 url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)$', self.entry_view)
432                         )
433                 elif self.entry_permalink_style == 'B':
434                         urlpatterns += patterns('',
435                                 url((r'^%s/(?P<slug>[-\w]+)$' % self.entry_permalink_base), self.entry_view)
436                         )
437                 else:
438                         urlpatterns = patterns('',
439                                 url(r'^(?P<slug>[-\w]+)$', self.entry_view)
440                         )
441                 return urlpatterns
442         
443         def get_context(self):
444                 return {'blog': self.blog}
445         
446         def get_entry_queryset(self):
447                 return self.blog.entries.all()
448         
449         def get_tag_queryset(self):
450                 return self.blog.entry_tags
451         
452         def get_all_entries(self, request, extra_context=None):
453                 return self.get_entry_queryset(), extra_context
454         
455         def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
456                 if not self.entry_archive_page:
457                         raise Http404
458                 entries = self.get_entry_queryset()
459                 if year:
460                         entries = entries.filter(date__year=year)
461                 if month:
462                         entries = entries.filter(date__month=month)
463                 if day:
464                         entries = entries.filter(date__day=day)
465                 
466                 context = extra_context or {}
467                 context.update({'year': year, 'month': month, 'day': day})
468                 return entries, context
469         
470         def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
471                 tag_slugs = tag_slugs.replace('+', '/').split('/')
472                 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
473                 
474                 if not tags:
475                         raise Http404
476                 
477                 # Raise a 404 on an incorrect slug.
478                 found_slugs = [tag.slug for tag in tags]
479                 for slug in tag_slugs:
480                         if slug and slug not in found_slugs:
481                                 raise Http404
482
483                 entries = self.get_entry_queryset()
484                 for tag in tags:
485                         entries = entries.filter(tags=tag)
486                 
487                 context = extra_context or {}
488                 context.update({'tags': tags})
489                 
490                 return entries, context
491         
492         def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
493                 entries = self.get_entry_queryset()
494                 if year:
495                         entries = entries.filter(date__year=year)
496                 if month:
497                         entries = entries.filter(date__month=month)
498                 if day:
499                         entries = entries.filter(date__day=day)
500                 try:
501                         entry = entries.get(slug=slug)
502                 except:
503                         raise Http404
504                 context = self.get_context()
505                 context.update(extra_context or {})
506                 context.update({'entry': entry})
507                 return self.entry_page.render_to_response(request, extra_context=context)
508         
509         def tag_archive_view(self, request, extra_context=None):
510                 if not self.tag_archive_page:
511                         raise Http404
512                 context = self.get_context()
513                 context.update(extra_context or {})
514                 context.update({
515                         'tags': self.get_tag_queryset()
516                 })
517                 return self.tag_archive_page.render_to_response(request, extra_context=context)
518         
519         def feed_view(self, get_items_attr, reverse_name):
520                 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
521                 
522                 def inner(request, extra_context=None, *args, **kwargs):
523                         obj = self.get_object(request, *args, **kwargs)
524                         feed = self.get_feed(obj, request, reverse_name)
525                         items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
526                         self.populate_feed(feed, items, request)
527                         
528                         if 'tags' in extra_context:
529                                 tags = extra_context['tags']
530                                 feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure())
531                         else:
532                                 tags = obj.entry_tags
533                         
534                         feed.feed['categories'] = [tag.name for tag in tags]
535                         
536                         response = HttpResponse(mimetype=feed.mime_type)
537                         feed.write(response, 'utf-8')
538                         return response
539                 
540                 return inner
541         
542         def process_page_items(self, request, items):
543                 if self.entries_per_page:
544                         page_num = request.GET.get('page', 1)
545                         paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num)
546                         item_context = {
547                                 'paginator': paginator,
548                                 'paginated_page': paginated_page,
549                                 self.item_context_var: items
550                         }
551                 else:
552                         item_context = {
553                                 self.item_context_var: items
554                         }
555                 return items, item_context
556         
557         def title(self, obj):
558                 return obj.title
559         
560         def item_title(self, item):
561                 return item.title
562         
563         def item_description(self, item):
564                 return item.content
565         
566         def item_author_name(self, item):
567                 return item.author.get_full_name()
568         
569         def item_pubdate(self, item):
570                 return item.date
571         
572         def item_categories(self, item):
573                 return [tag.name for tag in item.tags.all()]
574
575
576 class Newsletter(Entity):
577         title = models.CharField(max_length=255)
578         slug = models.SlugField(max_length=255)
579         
580         def __unicode__(self):
581                 return self.title
582
583
584 register_value_model(Newsletter)
585
586
587 class NewsletterArticle(Entity):
588         title = models.CharField(max_length=255)
589         slug = models.SlugField(max_length=255)
590         newsletter = models.ForeignKey(Newsletter, related_name='articles')
591         authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
592         date = models.DateTimeField(default=None)
593         lede = TemplateField(null=True, blank=True, verbose_name='Summary')
594         full_text = TemplateField(db_index=True)
595         tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True)
596         
597         def save(self, *args, **kwargs):
598                 if self.date is None:
599                         self.date = datetime.now()
600                 super(NewsletterArticle, self).save(*args, **kwargs)
601         
602         def __unicode__(self):
603                 return self.title
604         
605         class Meta:
606                 get_latest_by = 'date'
607                 ordering = ['-date']
608                 unique_together = (('newsletter', 'slug'),)
609
610
611 register_value_model(NewsletterArticle)
612
613
614 class NewsletterIssue(Entity):
615         title = models.CharField(max_length=255)
616         slug = models.SlugField(max_length=255)
617         newsletter = models.ForeignKey(Newsletter, related_name='issues')
618         numbering = models.CharField(max_length=50, help_text='For example, 04.02 for volume 4, issue 2.')
619         articles = models.ManyToManyField(NewsletterArticle, related_name='issues')
620         
621         def __unicode__(self):
622                 return self.title
623         
624         class Meta:
625                 ordering = ['-numbering']
626                 unique_together = (('newsletter', 'numbering'),)
627
628
629 register_value_model(NewsletterIssue)
630
631
632 class NewsletterView(FeedView):
633         ARTICLE_PERMALINK_STYLE_CHOICES = (
634                 ('D', 'Year, month, and day'),
635                 ('M', 'Year and month'),
636                 ('Y', 'Year'),
637                 ('S', 'Slug only')
638         )
639         
640         newsletter = models.ForeignKey(Newsletter, related_name='newsletterviews')
641         
642         index_page = models.ForeignKey(Page, related_name='newsletter_index_related')
643         article_page = models.ForeignKey(Page, related_name='newsletter_article_related')
644         article_archive_page = models.ForeignKey(Page, related_name='newsletter_article_archive_related', null=True, blank=True)
645         issue_page = models.ForeignKey(Page, related_name='newsletter_issue_related')
646         issue_archive_page = models.ForeignKey(Page, related_name='newsletter_issue_archive_related', null=True, blank=True)
647         
648         article_permalink_style = models.CharField(max_length=1, choices=ARTICLE_PERMALINK_STYLE_CHOICES)
649         article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
650         issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
651         
652         item_context_var = 'articles'
653         object_attr = 'newsletter'
654         
655         def __unicode__(self):
656                 return "NewsletterView for %s" % self.newsletter.__unicode__()
657         
658         def get_reverse_params(self, obj):
659                 if isinstance(obj, NewsletterArticle):
660                         if obj.newsletter == self.newsletter:
661                                 kwargs = {'slug': obj.slug}
662                                 if self.article_permalink_style in 'DMY':
663                                         kwargs.update({'year': str(obj.date.year).zfill(4)})
664                                         if self.article_permalink_style in 'DM':
665                                                 kwargs.update({'month': str(obj.date.month).zfill(2)})
666                                                 if self.article_permalink_style == 'D':
667                                                         kwargs.update({'day': str(obj.date.day).zfill(2)})
668                                 return self.article_view, [], kwargs
669                 elif isinstance(obj, NewsletterIssue):
670                         if obj.newsletter == self.newsletter:
671                                 return 'issue', [], {'numbering': obj.numbering}
672                 elif isinstance(obj, (date, datetime)):
673                         kwargs = {
674                                 'year': str(obj.year).zfill(4),
675                                 'month': str(obj.month).zfill(2),
676                                 'day': str(obj.day).zfill(2)
677                         }
678                         return 'articles_by_day', [], kwargs
679                 raise ViewCanNotProvideSubpath
680         
681         @property
682         def urlpatterns(self):
683                 urlpatterns = self.feed_patterns(r'^', 'get_all_articles', 'index_page', 'index') + patterns('',
684                         url(r'^%s/(?P<numbering>.+)$' % self.issue_permalink_base, self.page_view('get_articles_by_issue', 'issue_page'), name='issue')
685                 )
686                 if self.issue_archive_page:
687                         urlpatterns += patterns('',
688                                 url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
689                         )
690                 if self.article_archive_page:
691                         urlpatterns += patterns('',
692                                 url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
693                         )
694                         if self.article_permalink_style in 'DMY':
695                                 urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
696                                 if self.article_permalink_style in 'DM':
697                                         urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_month')
698                                         if self.article_permalink_style == 'D':
699                                                 urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_day')
700                 
701                 if self.article_permalink_style == 'Y':
702                         urlpatterns += patterns('',
703                                 url(r'^%s/(?P<year>\d{4})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
704                         )
705                 elif self.article_permalink_style == 'M':
706                         urlpatterns += patterns('',
707                                 url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
708                         )
709                 elif self.article_permalink_style == 'D':
710                         urlpatterns += patterns('',
711                                 url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
712                         )
713                 else:   
714                         urlpatterns += patterns('',
715                                 url(r'^%s/(?P<slug>[-\w]+)$' % self.article_permalink_base, self.article_view)
716                         )
717                 
718                 return urlpatterns
719         
720         def get_context(self):
721                 return {'newsletter': self.newsletter}
722         
723         def get_article_queryset(self):
724                 return self.newsletter.articles.all()
725         
726         def get_issue_queryset(self):
727                 return self.newsletter.issues.all()
728         
729         def get_all_articles(self, request, extra_context=None):
730                 return self.get_article_queryset(), extra_context
731         
732         def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
733                 articles = self.get_article_queryset().filter(date__year=year)
734                 if month:
735                         articles = articles.filter(date__month=month)
736                 if day:
737                         articles = articles.filter(date__day=day)
738                 return articles, extra_context
739         
740         def get_articles_by_issue(self, request, numbering, extra_context=None):
741                 try:
742                         issue = self.get_issue_queryset().get(numbering=numbering)
743                 except NewsletterIssue.DoesNotExist:
744                         raise Http404
745                 context = extra_context or {}
746                 context.update({'issue': issue})
747                 return self.get_article_queryset().filter(issues=issue), context
748         
749         def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
750                 articles = self.get_article_queryset()
751                 if year:
752                         articles = articles.filter(date__year=year)
753                 if month:
754                         articles = articles.filter(date__month=month)
755                 if day:
756                         articles = articles.filter(date__day=day)
757                 try:
758                         article = articles.get(slug=slug)
759                 except NewsletterArticle.DoesNotExist:
760                         raise Http404
761                 context = self.get_context()
762                 context.update(extra_context or {})
763                 context.update({'article': article})
764                 return self.article_page.render_to_response(request, extra_context=context)
765         
766         def issue_archive_view(self, request, extra_context):
767                 if not self.issue_archive_page:
768                         raise Http404
769                 context = self.get_context()
770                 context.update(extra_context or {})
771                 context.update({
772                         'issues': self.get_issue_queryset()
773                 })
774                 return self.issue_archive_page.render_to_response(request, extra_context=context)
775         
776         def title(self, obj):
777                 return obj.title
778         
779         def item_title(self, item):
780                 return item.title
781         
782         def item_description(self, item):
783                 return item.full_text
784         
785         def item_author_name(self, item):
786                 authors = list(item.authors.all())
787                 if len(authors) > 1:
788                         return "%s and %s" % (", ".join([author.get_full_name() for author in authors[:-1]]), authors[-1].get_full_name())
789                 elif authors:
790                         return authors[0].get_full_name()
791                 else:
792                         return ''
793         
794         def item_pubdate(self, item):
795                 return item.date
796         
797         def item_categories(self, item):
798                 return [tag.name for tag in item.tags.all()]