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