Added/corrected docstrings for custom model fields and EntityProxyFields (now renamed...
[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, Titled, 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, Titled):
301         @property
302         def entry_tags(self):
303                 """ Returns a QuerySet of Tags that are used on any entries in this blog. """
304                 return Tag.objects.filter(blogentries__blog=self).distinct()
305         
306         @property
307         def entry_dates(self):
308                 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')}
309                 return dates
310
311
312 register_value_model(Blog)
313
314
315 class BlogEntry(Entity, Titled):
316         blog = models.ForeignKey(Blog, related_name='entries', blank=True, null=True)
317         author = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='blogentries')
318         date = models.DateTimeField(default=None)
319         content = models.TextField()
320         excerpt = models.TextField(blank=True, null=True)
321         tags = models.ManyToManyField(Tag, related_name='blogentries', blank=True, null=True)
322         
323         def save(self, *args, **kwargs):
324                 if self.date is None:
325                         self.date = datetime.now()
326                 super(BlogEntry, self).save(*args, **kwargs)
327         
328         class Meta:
329                 ordering = ['-date']
330                 verbose_name_plural = "blog entries"
331                 get_latest_by = "date"
332
333
334 register_value_model(BlogEntry)
335
336
337 class BlogView(FeedView):
338         ENTRY_PERMALINK_STYLE_CHOICES = (
339                 ('D', 'Year, month, and day'),
340                 ('M', 'Year and month'),
341                 ('Y', 'Year'),
342                 ('B', 'Custom base'),
343                 ('N', 'No base')
344         )
345         
346         blog = models.ForeignKey(Blog, related_name='blogviews')
347         
348         index_page = models.ForeignKey(Page, related_name='blog_index_related')
349         entry_page = models.ForeignKey(Page, related_name='blog_entry_related')
350         # TODO: entry_archive is misleading. Rename to ymd_page or timespan_page.
351         entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True)
352         tag_page = models.ForeignKey(Page, related_name='blog_tag_related')
353         tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True)
354         entries_per_page = models.IntegerField(blank=True, validators=[validate_pagination_count], null=True)
355         
356         entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
357         entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
358         tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
359         
360         item_context_var = 'entries'
361         object_attr = 'blog'
362         
363         def __unicode__(self):
364                 return u'BlogView for %s' % self.blog.title
365         
366         def get_reverse_params(self, obj):
367                 if isinstance(obj, BlogEntry):
368                         if obj.blog == self.blog:
369                                 kwargs = {'slug': obj.slug}
370                                 if self.entry_permalink_style in 'DMY':
371                                         kwargs.update({'year': str(obj.date.year).zfill(4)})
372                                         if self.entry_permalink_style in 'DM':
373                                                 kwargs.update({'month': str(obj.date.month).zfill(2)})
374                                                 if self.entry_permalink_style == 'D':
375                                                         kwargs.update({'day': str(obj.date.day).zfill(2)})
376                                 return self.entry_view, [], kwargs
377                 elif isinstance(obj, Tag) or (isinstance(obj, models.query.QuerySet) and obj.model == Tag and obj):
378                         if isinstance(obj, Tag):
379                                 obj = [obj]
380                         slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()]
381                         if slugs:
382                                 return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)}
383                 elif isinstance(obj, (date, datetime)):
384                         kwargs = {
385                                 'year': str(obj.year).zfill(4),
386                                 'month': str(obj.month).zfill(2),
387                                 'day': str(obj.day).zfill(2)
388                         }
389                         return 'entries_by_day', [], kwargs
390                 raise ViewCanNotProvideSubpath
391         
392         @property
393         def urlpatterns(self):
394                 urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') +\
395                         self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)$' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'entries_by_tag')
396                 
397                 if self.tag_archive_page:
398                         urlpatterns += patterns('',
399                                 url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
400                         )
401                 
402                 if self.entry_archive_page:
403                         if self.entry_permalink_style in 'DMY':
404                                 urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')
405                                 if self.entry_permalink_style in 'DM':
406                                         urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_month')
407                                         if self.entry_permalink_style == 'D':
408                                                 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')
409                 
410                 if self.entry_permalink_style == 'D':
411                         urlpatterns += patterns('',
412                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
413                         )
414                 elif self.entry_permalink_style == 'M':
415                         urlpatterns += patterns('',
416                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
417                         )
418                 elif self.entry_permalink_style == 'Y':
419                         urlpatterns += patterns('',
420                                 url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)$', self.entry_view)
421                         )
422                 elif self.entry_permalink_style == 'B':
423                         urlpatterns += patterns('',
424                                 url((r'^%s/(?P<slug>[-\w]+)$' % self.entry_permalink_base), self.entry_view)
425                         )
426                 else:
427                         urlpatterns = patterns('',
428                                 url(r'^(?P<slug>[-\w]+)$', self.entry_view)
429                         )
430                 return urlpatterns
431         
432         def get_context(self):
433                 return {'blog': self.blog}
434         
435         def get_entry_queryset(self):
436                 return self.blog.entries.all()
437         
438         def get_tag_queryset(self):
439                 return self.blog.entry_tags
440         
441         def get_all_entries(self, request, extra_context=None):
442                 return self.get_entry_queryset(), extra_context
443         
444         def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
445                 if not self.entry_archive_page:
446                         raise Http404
447                 entries = self.get_entry_queryset()
448                 if year:
449                         entries = entries.filter(date__year=year)
450                 if month:
451                         entries = entries.filter(date__month=month)
452                 if day:
453                         entries = entries.filter(date__day=day)
454                 
455                 context = extra_context or {}
456                 context.update({'year': year, 'month': month, 'day': day})
457                 return entries, context
458         
459         def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
460                 tag_slugs = tag_slugs.replace('+', '/').split('/')
461                 tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
462                 
463                 if not tags:
464                         raise Http404
465                 
466                 # Raise a 404 on an incorrect slug.
467                 found_slugs = [tag.slug for tag in tags]
468                 for slug in tag_slugs:
469                         if slug and slug not in found_slugs:
470                                 raise Http404
471
472                 entries = self.get_entry_queryset()
473                 for tag in tags:
474                         entries = entries.filter(tags=tag)
475                 
476                 context = extra_context or {}
477                 context.update({'tags': tags})
478                 
479                 return entries, context
480         
481         def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
482                 entries = self.get_entry_queryset()
483                 if year:
484                         entries = entries.filter(date__year=year)
485                 if month:
486                         entries = entries.filter(date__month=month)
487                 if day:
488                         entries = entries.filter(date__day=day)
489                 try:
490                         entry = entries.get(slug=slug)
491                 except:
492                         raise Http404
493                 context = self.get_context()
494                 context.update(extra_context or {})
495                 context.update({'entry': entry})
496                 return self.entry_page.render_to_response(request, extra_context=context)
497         
498         def tag_archive_view(self, request, extra_context=None):
499                 if not self.tag_archive_page:
500                         raise Http404
501                 context = self.get_context()
502                 context.update(extra_context or {})
503                 context.update({
504                         'tags': self.get_tag_queryset()
505                 })
506                 return self.tag_archive_page.render_to_response(request, extra_context=context)
507         
508         def feed_view(self, get_items_attr, reverse_name):
509                 get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
510                 
511                 def inner(request, extra_context=None, *args, **kwargs):
512                         obj = self.get_object(request, *args, **kwargs)
513                         feed = self.get_feed(obj, request, reverse_name)
514                         items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
515                         self.populate_feed(feed, items, request)
516                         
517                         if 'tags' in extra_context:
518                                 tags = extra_context['tags']
519                                 feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure())
520                         else:
521                                 tags = obj.entry_tags
522                         
523                         feed.feed['categories'] = [tag.name for tag in tags]
524                         
525                         response = HttpResponse(mimetype=feed.mime_type)
526                         feed.write(response, 'utf-8')
527                         return response
528                 
529                 return inner
530         
531         def process_page_items(self, request, items):
532                 if self.entries_per_page:
533                         page_num = request.GET.get('page', 1)
534                         paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num)
535                         item_context = {
536                                 'paginator': paginator,
537                                 'paginated_page': paginated_page,
538                                 self.item_context_var: items
539                         }
540                 else:
541                         item_context = {
542                                 self.item_context_var: items
543                         }
544                 return items, item_context
545         
546         def title(self, obj):
547                 return obj.title
548         
549         def item_title(self, item):
550                 return item.title
551         
552         def item_description(self, item):
553                 return item.content
554         
555         def item_author_name(self, item):
556                 return item.author.get_full_name()
557         
558         def item_pubdate(self, item):
559                 return item.date
560         
561         def item_categories(self, item):
562                 return [tag.name for tag in item.tags.all()]
563
564
565 class Newsletter(Entity, Titled):
566         pass
567
568
569 register_value_model(Newsletter)
570
571
572 class NewsletterArticle(Entity, Titled):
573         newsletter = models.ForeignKey(Newsletter, related_name='articles')
574         authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
575         date = models.DateTimeField(default=None)
576         lede = TemplateField(null=True, blank=True, verbose_name='Summary')
577         full_text = TemplateField(db_index=True)
578         tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True)
579         
580         def save(self, *args, **kwargs):
581                 if self.date is None:
582                         self.date = datetime.now()
583                 super(NewsletterArticle, self).save(*args, **kwargs)
584         
585         class Meta:
586                 get_latest_by = 'date'
587                 ordering = ['-date']
588                 unique_together = (('newsletter', 'slug'),)
589
590
591 register_value_model(NewsletterArticle)
592
593
594 class NewsletterIssue(Entity, Titled):
595         newsletter = models.ForeignKey(Newsletter, related_name='issues')
596         numbering = models.CharField(max_length=50, help_text='For example, 04.02 for volume 4, issue 2.')
597         articles = models.ManyToManyField(NewsletterArticle, related_name='issues')
598         
599         class Meta:
600                 ordering = ['-numbering']
601                 unique_together = (('newsletter', 'numbering'),)
602
603
604 register_value_model(NewsletterIssue)
605
606
607 class NewsletterView(FeedView):
608         ARTICLE_PERMALINK_STYLE_CHOICES = (
609                 ('D', 'Year, month, and day'),
610                 ('M', 'Year and month'),
611                 ('Y', 'Year'),
612                 ('S', 'Slug only')
613         )
614         
615         newsletter = models.ForeignKey(Newsletter, related_name='newsletterviews')
616         
617         index_page = models.ForeignKey(Page, related_name='newsletter_index_related')
618         article_page = models.ForeignKey(Page, related_name='newsletter_article_related')
619         article_archive_page = models.ForeignKey(Page, related_name='newsletter_article_archive_related', null=True, blank=True)
620         issue_page = models.ForeignKey(Page, related_name='newsletter_issue_related')
621         issue_archive_page = models.ForeignKey(Page, related_name='newsletter_issue_archive_related', null=True, blank=True)
622         
623         article_permalink_style = models.CharField(max_length=1, choices=ARTICLE_PERMALINK_STYLE_CHOICES)
624         article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
625         issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
626         
627         item_context_var = 'articles'
628         object_attr = 'newsletter'
629         
630         def __unicode__(self):
631                 return "NewsletterView for %s" % self.newsletter.__unicode__()
632         
633         def get_reverse_params(self, obj):
634                 if isinstance(obj, NewsletterArticle):
635                         if obj.newsletter == self.newsletter:
636                                 kwargs = {'slug': obj.slug}
637                                 if self.article_permalink_style in 'DMY':
638                                         kwargs.update({'year': str(obj.date.year).zfill(4)})
639                                         if self.article_permalink_style in 'DM':
640                                                 kwargs.update({'month': str(obj.date.month).zfill(2)})
641                                                 if self.article_permalink_style == 'D':
642                                                         kwargs.update({'day': str(obj.date.day).zfill(2)})
643                                 return self.article_view, [], kwargs
644                 elif isinstance(obj, NewsletterIssue):
645                         if obj.newsletter == self.newsletter:
646                                 return 'issue', [], {'numbering': obj.numbering}
647                 elif isinstance(obj, (date, datetime)):
648                         kwargs = {
649                                 'year': str(obj.year).zfill(4),
650                                 'month': str(obj.month).zfill(2),
651                                 'day': str(obj.day).zfill(2)
652                         }
653                         return 'articles_by_day', [], kwargs
654                 raise ViewCanNotProvideSubpath
655         
656         @property
657         def urlpatterns(self):
658                 urlpatterns = self.feed_patterns(r'^', 'get_all_articles', 'index_page', 'index') + patterns('',
659                         url(r'^%s/(?P<numbering>.+)$' % self.issue_permalink_base, self.page_view('get_articles_by_issue', 'issue_page'), name='issue')
660                 )
661                 if self.issue_archive_page:
662                         urlpatterns += patterns('',
663                                 url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
664                         )
665                 if self.article_archive_page:
666                         urlpatterns += patterns('',
667                                 url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
668                         )
669                         if self.article_permalink_style in 'DMY':
670                                 urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
671                                 if self.article_permalink_style in 'DM':
672                                         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')
673                                         if self.article_permalink_style == 'D':
674                                                 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')
675                 
676                 if self.article_permalink_style == 'Y':
677                         urlpatterns += patterns('',
678                                 url(r'^%s/(?P<year>\d{4})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
679                         )
680                 elif self.article_permalink_style == 'M':
681                         urlpatterns += patterns('',
682                                 url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
683                         )
684                 elif self.article_permalink_style == 'D':
685                         urlpatterns += patterns('',
686                                 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)
687                         )
688                 else:   
689                         urlpatterns += patterns('',
690                                 url(r'^%s/(?P<slug>[-\w]+)$' % self.article_permalink_base, self.article_view)
691                         )
692                 
693                 return urlpatterns
694         
695         def get_context(self):
696                 return {'newsletter': self.newsletter}
697         
698         def get_article_queryset(self):
699                 return self.newsletter.articles.all()
700         
701         def get_issue_queryset(self):
702                 return self.newsletter.issues.all()
703         
704         def get_all_articles(self, request, extra_context=None):
705                 return self.get_article_queryset(), extra_context
706         
707         def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
708                 articles = self.get_article_queryset().filter(date__year=year)
709                 if month:
710                         articles = articles.filter(date__month=month)
711                 if day:
712                         articles = articles.filter(date__day=day)
713                 return articles, extra_context
714         
715         def get_articles_by_issue(self, request, numbering, extra_context=None):
716                 try:
717                         issue = self.get_issue_queryset().get(numbering=numbering)
718                 except NewsletterIssue.DoesNotExist:
719                         raise Http404
720                 context = extra_context or {}
721                 context.update({'issue': issue})
722                 return self.get_article_queryset().filter(issues=issue), context
723         
724         def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
725                 articles = self.get_article_queryset()
726                 if year:
727                         articles = articles.filter(date__year=year)
728                 if month:
729                         articles = articles.filter(date__month=month)
730                 if day:
731                         articles = articles.filter(date__day=day)
732                 try:
733                         article = articles.get(slug=slug)
734                 except NewsletterArticle.DoesNotExist:
735                         raise Http404
736                 context = self.get_context()
737                 context.update(extra_context or {})
738                 context.update({'article': article})
739                 return self.article_page.render_to_response(request, extra_context=context)
740         
741         def issue_archive_view(self, request, extra_context):
742                 if not self.issue_archive_page:
743                         raise Http404
744                 context = self.get_context()
745                 context.update(extra_context or {})
746                 context.update({
747                         'issues': self.get_issue_queryset()
748                 })
749                 return self.issue_archive_page.render_to_response(request, extra_context=context)
750         
751         def title(self, obj):
752                 return obj.title
753         
754         def item_title(self, item):
755                 return item.title
756         
757         def item_description(self, item):
758                 return item.full_text
759         
760         def item_author_name(self, item):
761                 authors = list(item.authors.all())
762                 if len(authors) > 1:
763                         return "%s and %s" % (", ".join([author.get_full_name() for author in authors[:-1]]), authors[-1].get_full_name())
764                 elif authors:
765                         return authors[0].get_full_name()
766                 else:
767                         return ''
768         
769         def item_pubdate(self, item):
770                 return item.date
771         
772         def item_categories(self, item):
773                 return [tag.name for tag in item.tags.all()]