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