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