Moved FeedMultiViewMixin to models.py and renamed to FeedView. Improved feed type...
[philo.git] / contrib / penfield / models.py
index 4191463..c018247 100644 (file)
@@ -2,14 +2,265 @@ from django.conf import settings
 from django.conf.urls.defaults import url, patterns, include
 from django.db import models
 from django.http import Http404
 from django.conf.urls.defaults import url, patterns, include
 from django.db import models
 from django.http import Http404
-from django.template import loader, Context
-from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
+from django.template import RequestContext, Template as DjangoTemplate
+from django.utils import feedgenerator, tzinfo
+from django.utils.datastructures import SortedDict
+from django.utils.encoding import smart_unicode, force_unicode
+from django.utils.html import escape
 from datetime import date, datetime
 from datetime import date, datetime
-from philo.contrib.penfield.utils import FeedMultiViewMixin
 from philo.contrib.penfield.validators import validate_pagination_count
 from philo.exceptions import ViewCanNotProvideSubpath
 from philo.contrib.penfield.validators import validate_pagination_count
 from philo.exceptions import ViewCanNotProvideSubpath
-from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField
+from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField, Template
 from philo.utils import paginate
 from philo.utils import paginate
+try:
+       import mimeparse
+except:
+       mimeparse = None
+
+ATOM = feedgenerator.Atom1Feed.mime_type
+RSS = feedgenerator.Rss201rev2Feed.mime_type
+FEEDS = SortedDict([
+       (ATOM, feedgenerator.Atom1Feed),
+       (RSS, feedgenerator.Rss201rev2Feed),
+])
+FEED_CHOICES = (
+       (ATOM, "Atom"),
+       (RSS, "RSS"),
+)
+
+
+class FeedView(MultiView):
+       """
+       The FeedView expects to handle a number of different feeds for the
+       same object - i.e. patterns for a blog to handle all entries or
+       just entries for a certain year/month/day.
+       
+       This class would subclass django.contrib.syndication.views.Feed, but
+       that would make it callable, which causes problems.
+       """
+       feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM)
+       feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
+       feeds_enabled = models.BooleanField(default=True)
+       
+       item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
+       item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
+       
+       item_context_var = 'items'
+       object_attr = 'object'
+       
+       description = ""
+       
+       def feed_patterns(self, get_items_attr, page_attr, reverse_name):
+               """
+               Given the name to be used to reverse this view and the names of
+               the attributes for the function that fetches the objects, returns
+               patterns suitable for inclusion in urlpatterns.
+               """
+               urlpatterns = patterns('',
+                       url(r'^$', self.page_view(get_items_attr, page_attr), name=reverse_name)
+               )
+               if self.feeds_enabled:
+                       feed_reverse_name = "%s_feed" % reverse_name
+                       urlpatterns += patterns('',
+                               url(r'^%s$' % self.feed_suffix, self.feed_view(get_items_attr, feed_reverse_name), name=feed_reverse_name),
+                       )
+               return urlpatterns
+       
+       def get_object(self, request, **kwargs):
+               return getattr(self, self.object_attr)
+       
+       def feed_view(self, get_items_attr, reverse_name):
+               """
+               Returns a view function that renders a list of items as a feed.
+               """
+               get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
+               
+               def inner(request, extra_context=None, *args, **kwargs):
+                       obj = self.get_object(request, *args, **kwargs)
+                       feed = self.get_feed(obj, request, reverse_name)
+                       items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
+                       
+                       response = HttpResponse(mimetype=feed.mime_type)
+                       feed.write(response, 'utf-8')
+                       return response
+               
+               return inner
+       
+       def page_view(self, get_items_attr, page_attr):
+               """
+               Returns a view function that renders a list of items as a page.
+               """
+               get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
+               page = isinstance(page_attr, Page) and page_attr or getattr(self, page_attr)
+               
+               def inner(request, extra_context=None, *args, **kwargs):
+                       items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
+                       items, item_context = self.process_page_items(request, items)
+                       
+                       context = self.get_context()
+                       context.update(extra_context or {})
+                       context.update(item_context or {})
+                       
+                       return page.render_to_response(request, extra_context=context)
+               return inner
+       
+       def process_page_items(self, request, items):
+               """
+               Hook for handling any extra processing of items based on a
+               request, such as pagination or searching. This method is
+               expected to return a list of items and a dictionary to be
+               added to the page context.
+               """
+               item_context = {
+                       self.item_context_var: items
+               }
+               return items, item_context
+       
+       def get_feed_type(self, request):
+               feed_type = self.feed_type
+               accept = request.META.get('HTTP_ACCEPT')
+               if accept and feed_type not in accept and "*/*" not in accept and "%s/*" % feed_type.split("/")[0] not in accept:
+                       # Wups! They aren't accepting the chosen format. Is there another format we can use?
+                       if mimeparse:
+                               feed_type = mimeparse.best_match(FEEDS.keys(), accept)
+                       else:
+                               for feed_type in FEEDS.keys():
+                                       if feed_type in accept or "%s/*" % feed_type.split("/")[0] in accept:
+                                               break
+                               else:
+                                       feed_type = None
+                       if not feed_type:
+                               # See RFC 2616
+                               return HttpResponse(status=406)
+               return FEEDS[feed_type]
+       
+       def get_feed(self, obj, request, reverse_name):
+               """
+               Returns an unpopulated feedgenerator.DefaultFeed object for this object.
+               """
+               feed_type = self.get_feed_type(request)
+               node = request.node
+               link = node.get_absolute_url(with_domain=True, request=request, secure=request.is_secure())
+               
+               feed = feed_type(
+                       title = self.__get_dynamic_attr('title', obj),
+                       subtitle = self.__get_dynamic_attr('subtitle', obj),
+                       link = link,
+                       description = self.__get_dynamic_attr('description', obj),
+                       language = settings.LANGUAGE_CODE.decode(),
+                       feed_url = node.construct_url(self.reverse(reverse_name), with_domain=True, request=request, secure=request.is_secure()),
+                       author_name = self.__get_dynamic_attr('author_name', obj),
+                       author_link = self.__get_dynamic_attr('author_link', obj),
+                       author_email = self.__get_dynamic_attr('author_email', obj),
+                       categories = self.__get_dynamic_attr('categories', obj),
+                       feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
+                       feed_guid = self.__get_dynamic_attr('feed_guid', obj),
+                       ttl = self.__get_dynamic_attr('ttl', obj),
+                       **self.feed_extra_kwargs(obj)
+               )
+               return feed
+       
+       def populate_feed(self, feed, items, request):
+               if self.item_title_template:
+                       title_template = Template(self.item_title_template.code)
+               else:
+                       title_template = None
+               if self.item_description_template:
+                       description_template = Template(self.item_description_template.code)
+               else:
+                       description_template = None
+               
+               node = request.node
+               
+               for item in items:
+                       if title_template is not None:
+                               title = title_template.render(RequestContext(request, {'obj': item}))
+                       else:
+                               title = self.__get_dynamic_attr('item_title', item)
+                       if description_template is not None:
+                               description = description_template.render(RequestContext(request, {'obj': item}))
+                       else:
+                               description = self.__get_dynamic_attr('item_description', item)
+                       
+                       link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
+                       
+                       enc = None
+                       enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
+                       if enc_url:
+                               enc = feedgenerator.Enclosure(
+                                       url = smart_unicode(enc_url),
+                                       length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
+                                       mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
+                               )
+                       author_name = self.__get_dynamic_attr('item_author_name', item)
+                       if author_name is not None:
+                               author_email = self.__get_dynamic_attr('item_author_email', item)
+                               author_link = self.__get_dynamic_attr('item_author_link', item)
+                       else:
+                               author_email = author_link = None
+                       
+                       pubdate = self.__get_dynamic_attr('item_pubdate', item)
+                       if pubdate and not pubdate.tzinfo:
+                               ltz = tzinfo.LocalTimezone(pubdate)
+                               pubdate = pubdate.replace(tzinfo=ltz)
+                       
+                       feed.add_item(
+                               title = title,
+                               link = link,
+                               description = description,
+                               unique_id = self.__get_dynamic_attr('item_guid', item, link),
+                               enclosure = enc,
+                               pubdate = pubdate,
+                               author_name = author_name,
+                               author_email = author_email,
+                               author_link = author_link,
+                               categories = self.__get_dynamic_attr('item_categories', item),
+                               item_copyright = self.__get_dynamic_attr('item_copyright', item),
+                               **self.item_extra_kwargs(item)
+                       )
+       
+       def __get_dynamic_attr(self, attname, obj, default=None):
+               try:
+                       attr = getattr(self, attname)
+               except AttributeError:
+                       return default
+               if callable(attr):
+                       # Check func_code.co_argcount rather than try/excepting the
+                       # function and catching the TypeError, because something inside
+                       # the function may raise the TypeError. This technique is more
+                       # accurate.
+                       if hasattr(attr, 'func_code'):
+                               argcount = attr.func_code.co_argcount
+                       else:
+                               argcount = attr.__call__.func_code.co_argcount
+                       if argcount == 2: # one argument is 'self'
+                               return attr(obj)
+                       else:
+                               return attr()
+               return attr
+       
+       def feed_extra_kwargs(self, obj):
+               """
+               Returns an extra keyword arguments dictionary that is used when
+               initializing the feed generator.
+               """
+               return {}
+       
+       def item_extra_kwargs(self, item):
+               """
+               Returns an extra keyword arguments dictionary that is used with
+               the `add_item` call of the feed generator.
+               """
+               return {}
+       
+       def item_title(self, item):
+               return escape(force_unicode(item))
+       
+       def item_description(self, item):
+               return force_unicode(item)
+       
+       class Meta:
+               abstract=True
 
 
 class Blog(Entity, Titled):
 
 
 class Blog(Entity, Titled):
@@ -49,7 +300,7 @@ class BlogEntry(Entity, Titled):
 register_value_model(BlogEntry)
 
 
 register_value_model(BlogEntry)
 
 
-class BlogView(MultiView, FeedMultiViewMixin):
+class BlogView(FeedView):
        ENTRY_PERMALINK_STYLE_CHOICES = (
                ('D', 'Year, month, and day'),
                ('M', 'Year and month'),
        ENTRY_PERMALINK_STYLE_CHOICES = (
                ('D', 'Year, month, and day'),
                ('M', 'Year and month'),
@@ -70,17 +321,13 @@ class BlogView(MultiView, FeedMultiViewMixin):
        entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
        entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
        tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
        entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
        entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
        tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
-       feed_suffix = models.CharField(max_length=255, blank=False, default=FeedMultiViewMixin.feed_suffix)
-       feeds_enabled = models.BooleanField()
-       list_var = 'entries'
+       
+       item_context_var = 'entries'
+       object_attr = 'blog'
        
        def __unicode__(self):
                return u'BlogView for %s' % self.blog.title
        
        
        def __unicode__(self):
                return u'BlogView for %s' % self.blog.title
        
-       @property
-       def per_page(self):
-               return self.entries_per_page
-       
        def get_reverse_params(self, obj):
                if isinstance(obj, BlogEntry):
                        if obj.blog == self.blog:
        def get_reverse_params(self, obj):
                if isinstance(obj, BlogEntry):
                        if obj.blog == self.blog:
@@ -92,9 +339,12 @@ class BlogView(MultiView, FeedMultiViewMixin):
                                                if self.entry_permalink_style == 'D':
                                                        kwargs.update({'day': str(obj.date.day).zfill(2)})
                                return self.entry_view, [], kwargs
                                                if self.entry_permalink_style == 'D':
                                                        kwargs.update({'day': str(obj.date.day).zfill(2)})
                                return self.entry_view, [], kwargs
-               elif isinstance(obj, Tag):
-                       if obj in self.blog.entry_tags:
-                               return 'entries_by_tag', [], {'tag_slugs': obj.slug}
+               elif isinstance(obj, Tag) or (isinstance(obj, models.QuerySet) and obj.model == Tag and obj):
+                       if isinstance(obj, Tag):
+                               obj = [obj]
+                       slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()]
+                       if slugs:
+                               return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)}
                elif isinstance(obj, (date, datetime)):
                        kwargs = {
                                'year': str(obj.year).zfill(4),
                elif isinstance(obj, (date, datetime)):
                        kwargs = {
                                'year': str(obj.year).zfill(4),
@@ -104,69 +354,75 @@ class BlogView(MultiView, FeedMultiViewMixin):
                        return 'entries_by_day', [], kwargs
                raise ViewCanNotProvideSubpath
        
                        return 'entries_by_day', [], kwargs
                raise ViewCanNotProvideSubpath
        
-       def get_context(self):
-               return {'blog': self.blog}
-       
        @property
        def urlpatterns(self):
                urlpatterns = patterns('',
        @property
        def urlpatterns(self):
                urlpatterns = patterns('',
-                       url(r'^', include(self.feed_patterns(self.get_all_entries, self.index_page, 'index'))),
+                       url(r'^', include(self.feed_patterns('get_all_entries', 'index_page', 'index'))),
                )
                if self.feeds_enabled:
                        urlpatterns += patterns('',
                )
                if self.feeds_enabled:
                        urlpatterns += patterns('',
-                               url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)/%s/' % (self.tag_permalink_base, self.feed_suffix), self.feed_view(self.get_entries_by_tag, 'entries_by_tag_feed'), name='entries_by_tag_feed'),
+                               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'),
                        )
                urlpatterns += patterns('',
                        )
                urlpatterns += patterns('',
-                       url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)/' % self.tag_permalink_base, self.page_view(self.get_entries_by_tag, self.tag_page), name='entries_by_tag')
+                       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')
                )
                if self.tag_archive_page:
                        urlpatterns += patterns('',
                )
                if self.tag_archive_page:
                        urlpatterns += patterns('',
-                               url((r'^(?:%s)/?$' % self.tag_permalink_base), self.tag_archive_view)
+                               url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
                        )
                
                if self.entry_archive_page:
                        if self.entry_permalink_style in 'DMY':
                                urlpatterns += patterns('',
                        )
                
                if self.entry_archive_page:
                        if self.entry_permalink_style in 'DMY':
                                urlpatterns += patterns('',
-                                       url(r'^(?P<year>\d{4})/', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_year')))
+                                       url(r'^(?P<year>\d{4})', include(self.feed_patterns('get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')))
                                )
                                if self.entry_permalink_style in 'DM':
                                        urlpatterns += patterns('',
                                )
                                if self.entry_permalink_style in 'DM':
                                        urlpatterns += patterns('',
-                                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/?$', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_month'))),
+                                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})$', include(self.feed_patterns('get_entries_by_ymd', 'entry_archive_page', 'entries_by_month'))),
                                        )
                                        if self.entry_permalink_style == 'D':
                                                urlpatterns += patterns('',
                                        )
                                        if self.entry_permalink_style == 'D':
                                                urlpatterns += patterns('',
-                                                       url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/?$', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_day')))
+                                                       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')))
                                                )
                
                if self.entry_permalink_style == 'D':
                        urlpatterns += patterns('',
                                                )
                
                if self.entry_permalink_style == 'D':
                        urlpatterns += patterns('',
-                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)/?$', self.entry_view)
+                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
                        )
                elif self.entry_permalink_style == 'M':
                        urlpatterns += patterns('',
                        )
                elif self.entry_permalink_style == 'M':
                        urlpatterns += patterns('',
-                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)/?$', self.entry_view)
+                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)$', self.entry_view)
                        )
                elif self.entry_permalink_style == 'Y':
                        urlpatterns += patterns('',
                        )
                elif self.entry_permalink_style == 'Y':
                        urlpatterns += patterns('',
-                               url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)/?$', self.entry_view)
+                               url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)$', self.entry_view)
                        )
                elif self.entry_permalink_style == 'B':
                        urlpatterns += patterns('',
                        )
                elif self.entry_permalink_style == 'B':
                        urlpatterns += patterns('',
-                               url((r'^(?:%s)/(?P<slug>[-\w]+)/?$' % self.entry_permalink_base), self.entry_view)
+                               url((r'^%s/(?P<slug>[-\w]+)$' % self.entry_permalink_base), self.entry_view)
                        )
                else:
                        urlpatterns = patterns('',
                        )
                else:
                        urlpatterns = patterns('',
-                               url(r'^(?P<slug>[-\w]+)/?$', self.entry_view)
+                               url(r'^(?P<slug>[-\w]+)$', self.entry_view)
                        )
                return urlpatterns
        
                        )
                return urlpatterns
        
+       def get_context(self):
+               return {'blog': self.blog}
+       
+       def get_entry_queryset(self):
+               return self.blog.entries.all()
+       
+       def get_tag_queryset(self):
+               return self.blog.entry_tags
+       
        def get_all_entries(self, request, extra_context=None):
        def get_all_entries(self, request, extra_context=None):
-               return self.blog.entries.all(), extra_context
+               return self.get_entry_queryset(), extra_context
        
        def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
                if not self.entry_archive_page:
                        raise Http404
        
        def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
                if not self.entry_archive_page:
                        raise Http404
-               entries = self.blog.entries.all()
+               entries = self.get_entry_queryset()
                if year:
                        entries = entries.filter(date__year=year)
                if month:
                if year:
                        entries = entries.filter(date__year=year)
                if month:
@@ -180,7 +436,7 @@ class BlogView(MultiView, FeedMultiViewMixin):
        
        def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
                tag_slugs = tag_slugs.replace('+', '/').split('/')
        
        def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
                tag_slugs = tag_slugs.replace('+', '/').split('/')
-               tags = self.blog.entry_tags.filter(slug__in=tag_slugs)
+               tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
                
                if not tags:
                        raise Http404
                
                if not tags:
                        raise Http404
@@ -191,43 +447,17 @@ class BlogView(MultiView, FeedMultiViewMixin):
                        if slug and slug not in found_slugs:
                                raise Http404
 
                        if slug and slug not in found_slugs:
                                raise Http404
 
-               entries = self.blog.entries.all()
+               entries = self.get_entry_queryset()
                for tag in tags:
                        entries = entries.filter(tags=tag)
                
                for tag in tags:
                        entries = entries.filter(tags=tag)
                
-               context = self.get_context()
-               context.update(extra_context or {})
+               context = extra_context or {}
                context.update({'tags': tags})
                
                return entries, context
        
                context.update({'tags': tags})
                
                return entries, context
        
-       def add_item(self, feed, obj, kwargs=None):
-               title = loader.get_template("penfield/feeds/blog_entry/title.html")
-               description = loader.get_template("penfield/feeds/blog_entry/description.html")
-               defaults = {
-                       'title': title.render(Context({'entry': obj})),
-                       'description': description.render(Context({'entry': obj})),
-                       'author_name': obj.author.get_full_name(),
-                       'pubdate': obj.date
-               }
-               defaults.update(kwargs or {})
-               super(BlogView, self).add_item(feed, obj, defaults)
-       
-       def get_feed(self, feed_type, extra_context, kwargs=None):
-               tags = (extra_context or {}).get('tags', None)
-               title = self.blog.title
-               
-               if tags is not None:
-                       title += " - %s" % ', '.join([tag.name for tag in tags])
-               
-               defaults = {
-                       'title': title
-               }
-               defaults.update(kwargs or {})
-               return super(BlogView, self).get_feed(feed_type, extra_context, defaults)
-       
        def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
        def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
-               entries = self.blog.entries.all()
+               entries = self.get_entry_queryset()
                if year:
                        entries = entries.filter(date__year=year)
                if month:
                if year:
                        entries = entries.filter(date__year=year)
                if month:
@@ -246,10 +476,67 @@ class BlogView(MultiView, FeedMultiViewMixin):
        def tag_archive_view(self, request, extra_context=None):
                if not self.tag_archive_page:
                        raise Http404
        def tag_archive_view(self, request, extra_context=None):
                if not self.tag_archive_page:
                        raise Http404
-               context = {}
+               context = self.get_context()
                context.update(extra_context or {})
                context.update(extra_context or {})
-               context.update({'blog': self.blog})
+               context.update({
+                       'tags': self.get_tag_queryset()
+               })
                return self.tag_archive_page.render_to_response(request, extra_context=context)
                return self.tag_archive_page.render_to_response(request, extra_context=context)
+       
+       def feed_view(self, get_items_attr, reverse_name):
+               get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
+               
+               def inner(request, extra_context=None, *args, **kwargs):
+                       obj = self.get_object(request, *args, **kwargs)
+                       feed = self.get_feed(obj, request, reverse_name)
+                       items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
+                       
+                       if 'tags' in extra_context:
+                               tags = extra_context['tags']
+                               feed.feed['link'] = request.node.construct_url(self.reverse(tags), with_domain=True, request=request, secure=request.is_secure())
+                       else:
+                               tags = obj.entry_tags
+                       
+                       feed.feed['categories'] = [tag.name for tag in tags]
+                       
+                       response = HttpResponse(mimetype=feed.mime_type)
+                       feed.write(response, 'utf-8')
+                       return response
+               
+               return inner
+       
+       def process_page_items(self, request, items):
+               if self.entries_per_page:
+                       page_num = request.GET.get('page', 1)
+                       paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num)
+                       item_context = {
+                               'paginator': paginator,
+                               'paginated_page': paginated_page,
+                               self.item_context_var: objects
+                       }
+               else:
+                       item_context = {
+                               self.item_context_var: items
+                       }
+               return items, item_context
+       
+       def title(self, obj):
+               return obj.title
+       
+       def item_title(self, item):
+               return item.title
+       
+       def item_description(self, item):
+               return item.content
+       
+       def item_author_name(self, item):
+               return item.author.get_full_name()
+       
+       def item_pubdate(self, item):
+               return item.date
+       
+       def item_categories(self, item):
+               return [tag.name for tag in item.tags.all()]
 
 
 class Newsletter(Entity, Titled):
 
 
 class Newsletter(Entity, Titled):
@@ -294,7 +581,7 @@ class NewsletterIssue(Entity, Titled):
 register_value_model(NewsletterIssue)
 
 
 register_value_model(NewsletterIssue)
 
 
-class NewsletterView(MultiView, FeedMultiViewMixin):
+class NewsletterView(FeedView):
        ARTICLE_PERMALINK_STYLE_CHOICES = (
                ('D', 'Year, month, and day'),
                ('M', 'Year and month'),
        ARTICLE_PERMALINK_STYLE_CHOICES = (
                ('D', 'Year, month, and day'),
                ('M', 'Year and month'),
@@ -314,12 +601,11 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
        article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
        issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
        
        article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
        issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
        
-       feed_suffix = models.CharField(max_length=255, blank=False, default=FeedMultiViewMixin.feed_suffix)
-       feeds_enabled = models.BooleanField()
-       list_var = 'articles'
+       item_context_var = 'articles'
+       object_attr = 'newsletter'
        
        def __unicode__(self):
        
        def __unicode__(self):
-               return self.newsletter.__unicode__()
+               return "NewsletterView for %s" % self.newsletter.__unicode__()
        
        def get_reverse_params(self, obj):
                if isinstance(obj, NewsletterArticle):
        
        def get_reverse_params(self, obj):
                if isinstance(obj, NewsletterArticle):
@@ -347,45 +633,45 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
        @property
        def urlpatterns(self):
                urlpatterns = patterns('',
        @property
        def urlpatterns(self):
                urlpatterns = patterns('',
-                       url(r'^', include(self.feed_patterns(self.get_all_articles, self.index_page, 'index'))),
-                       url(r'^(?:%s)/(?P<numbering>.+)/' % self.issue_permalink_base, include(self.feed_patterns(self.get_articles_by_issue, self.issue_page, 'issue')))
+                       url(r'^', include(self.feed_patterns('get_all_articles', 'index_page', 'index'))),
+                       url(r'^%s/(?P<numbering>.+)' % self.issue_permalink_base, include(self.feed_patterns('get_articles_by_issue', 'issue_page', 'issue')))
                )
                if self.issue_archive_page:
                        urlpatterns += patterns('',
                )
                if self.issue_archive_page:
                        urlpatterns += patterns('',
-                               url(r'^(?:%s)/$' % self.issue_permalink_base, self.issue_archive_view)
+                               url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
                        )
                if self.article_archive_page:
                        urlpatterns += patterns('',
                        )
                if self.article_archive_page:
                        urlpatterns += patterns('',
-                               url(r'^(?:%s)/' % self.article_permalink_base, include(self.feed_patterns(self.get_all_articles, self.article_archive_page, 'articles')))
+                               url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
                        )
                        if self.article_permalink_style in 'DMY':
                                urlpatterns += patterns('',
                        )
                        if self.article_permalink_style in 'DMY':
                                urlpatterns += patterns('',
-                                       url(r'^(?:%s)/(?P<year>\d{4})/' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_year')))
+                                       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')))
                                )
                                if self.article_permalink_style in 'DM':
                                        urlpatterns += patterns('',
                                )
                                if self.article_permalink_style in 'DM':
                                        urlpatterns += patterns('',
-                                               url(r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_month')))
+                                               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')))
                                        )
                                        if self.article_permalink_style == 'D':
                                                urlpatterns += patterns('',
                                        )
                                        if self.article_permalink_style == 'D':
                                                urlpatterns += patterns('',
-                                                       url(r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_day')))
+                                                       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')))
                                                )
                
                if self.article_permalink_style == 'Y':
                        urlpatterns += patterns('',
                                                )
                
                if self.article_permalink_style == 'Y':
                        urlpatterns += patterns('',
-                               url(r'^(?:%s)/(?P<year>\d{4})/(?P<slug>[\w-]+)/$' % self.article_permalink_base, self.article_view)
+                               url(r'^%s/(?P<year>\d{4})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
                        )
                elif self.article_permalink_style == 'M':
                        urlpatterns += patterns('',
                        )
                elif self.article_permalink_style == 'M':
                        urlpatterns += patterns('',
-                               url(r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w-]+)/$' % self.article_permalink_base, self.article_view)
+                               url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w-]+)$' % self.article_permalink_base, self.article_view)
                        )
                elif self.article_permalink_style == 'D':
                        urlpatterns += patterns('',
                        )
                elif self.article_permalink_style == 'D':
                        urlpatterns += patterns('',
-                               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)
+                               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)
                        )
                else:   
                        urlpatterns += patterns('',
                        )
                else:   
                        urlpatterns += patterns('',
-                               url(r'^(?:%s)/(?P<slug>[-\w]+)/?$' % self.article_permalink_base, self.article_view)
+                               url(r'^%s/(?P<slug>[-\w]+)$' % self.article_permalink_base, self.article_view)
                        )
                
                return urlpatterns
                        )
                
                return urlpatterns
@@ -393,28 +679,34 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
        def get_context(self):
                return {'newsletter': self.newsletter}
        
        def get_context(self):
                return {'newsletter': self.newsletter}
        
+       def get_article_queryset(self):
+               return self.newsletter.articles.all()
+       
+       def get_issue_queryset(self):
+               return self.newsletter.issues.all()
+       
        def get_all_articles(self, request, extra_context=None):
        def get_all_articles(self, request, extra_context=None):
-               return self.newsletter.articles.all(), extra_context
+               return self.get_article_queryset(), extra_context
        
        def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
        
        def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
-               articles = self.newsletter.articles.filter(dat__year=year)
+               articles = self.get_article_queryset().filter(date__year=year)
                if month:
                        articles = articles.filter(date__month=month)
                if day:
                        articles = articles.filter(date__day=day)
                if month:
                        articles = articles.filter(date__month=month)
                if day:
                        articles = articles.filter(date__day=day)
-               return articles
+               return articles, extra_context
        
        def get_articles_by_issue(self, request, numbering, extra_context=None):
                try:
        
        def get_articles_by_issue(self, request, numbering, extra_context=None):
                try:
-                       issue = self.newsletter.issues.get(numbering=numbering)
+                       issue = self.get_issue_queryset().get(numbering=numbering)
                except NewsletterIssue.DoesNotExist:
                        raise Http404
                context = extra_context or {}
                context.update({'issue': issue})
                except NewsletterIssue.DoesNotExist:
                        raise Http404
                context = extra_context or {}
                context.update({'issue': issue})
-               return issue.articles.all(), context
+               return self.get_article_queryset().filter(issues=issue), context
        
        def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
        
        def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
-               articles = self.newsletter.articles.all()
+               articles = self.get_article_queryset()
                if year:
                        articles = articles.filter(date__year=year)
                if month:
                if year:
                        articles = articles.filter(date__year=year)
                if month:
@@ -430,32 +722,36 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
                context.update({'article': article})
                return self.article_page.render_to_response(request, extra_context=context)
        
                context.update({'article': article})
                return self.article_page.render_to_response(request, extra_context=context)
        
-       def issue_archive_view(self, request, extra_context=None):
+       def issue_archive_view(self, request, extra_context):
                if not self.issue_archive_page:
                        raise Http404
                if not self.issue_archive_page:
                        raise Http404
-               context = {}
+               context = self.get_context()
                context.update(extra_context or {})
                context.update(extra_context or {})
-               context.update({'newsletter': self.newsletter})
+               context.update({
+                       'issues': self.get_issue_queryset()
+               })
                return self.issue_archive_page.render_to_response(request, extra_context=context)
        
                return self.issue_archive_page.render_to_response(request, extra_context=context)
        
-       def add_item(self, feed, obj, kwargs=None):
-               title = loader.get_template("penfield/feeds/newsletter_article/title.html")
-               description = loader.get_template("penfield/feeds/newsletter_article/description.html")
-               defaults = {
-                       'title': title.render(Context({'article': obj})),
-                       'author_name': ', '.join([author.get_full_name() for author in obj.authors.all()]),
-                       'pubdate': obj.date,
-                       'description': description.render(Context({'article': obj})),
-                       'categories': [tag.name for tag in obj.tags.all()]
-               }
-               defaults.update(kwargs or {})
-               super(NewsletterView, self).add_item(feed, obj, defaults)
+       def title(self, obj):
+               return obj.title
        
        
-       def get_feed(self, feed_type, extra_context, kwargs=None):
-               title = self.newsletter.title
-               
-               defaults = {
-                       'title': title
-               }
-               defaults.update(kwargs or {})
-               return super(NewsletterView, self).get_feed(feed_type, extra_context, defaults)
+       def item_title(self, item):
+               return item.title
+       
+       def item_description(self, item):
+               return item.full_text
+       
+       def item_author_name(self, item):
+               authors = list(item.authors.all())
+               if len(authors) > 1:
+                       return "%s and %s" % (", ".join([author.get_full_name() for author in authors[:-1]]), authors[-1].get_full_name())
+               elif authors:
+                       return authors[0].get_full_name()
+               else:
+                       return ''
+       
+       def item_pubdate(self, item):
+               return item.date
+       
+       def item_categories(self, item):
+               return [tag.name for tag in item.tags.all()]
\ No newline at end of file