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