Initial implementation of a separate syndication contrib app based on penfield's...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 2 Jun 2011 21:35:59 +0000 (17:35 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 2 Jun 2011 21:39:32 +0000 (17:39 -0400)
philo/contrib/julian/models.py
philo/contrib/penfield/models.py
philo/contrib/syndication/__init__.py [new file with mode: 0644]
philo/contrib/syndication/exceptions.py [moved from philo/contrib/penfield/exceptions.py with 100% similarity]
philo/contrib/syndication/feeds.py [new file with mode: 0644]
philo/contrib/syndication/middleware.py [moved from philo/contrib/penfield/middleware.py with 100% similarity]
philo/contrib/syndication/models.py [new file with mode: 0644]

index 62b938a..33c6e2a 100644 (file)
@@ -15,7 +15,8 @@ from django.http import HttpResponse, Http404
 from django.utils.encoding import force_unicode
 
 from philo.contrib.julian.feedgenerator import ICalendarFeed
-from philo.contrib.penfield.models import FeedView, FEEDS
+from philo.contrib.syndication.models import FeedView
+from philo.contrib.syndication.feeds import registry
 from philo.exceptions import ViewCanNotProvideSubpath
 from philo.models import Tag, Entity, Page
 from philo.models.fields import TemplateField
@@ -25,8 +26,7 @@ from philo.utils import ContentTypeRegistryLimiter
 __all__ = ('register_location_model', 'unregister_location_model', 'Location', 'TimedModel', 'Event', 'Calendar', 'CalendarView',)
 
 
-ICALENDAR = ICalendarFeed.mime_type
-FEEDS[ICALENDAR] = ICalendarFeed
+registry.register(ICalendarFeed, verbose_name="iCalendar")
 try:
        DEFAULT_SITE = Site.objects.get_current()
 except:
@@ -461,5 +461,4 @@ class CalendarView(FeedView):
                return u"%s for %s" % (self.__class__.__name__, self.calendar)
 
 field = CalendarView._meta.get_field('feed_type')
-field._choices += ((ICALENDAR, 'iCalendar'),)
-field.default = ICALENDAR
\ No newline at end of file
+field.default = registry.get_slug(ICalendarFeed, field.default)
\ No newline at end of file
index 2a145fb..e31de9f 100644 (file)
@@ -2,327 +2,15 @@ from datetime import date, datetime
 
 from django.conf import settings
 from django.conf.urls.defaults import url, patterns, include
-from django.contrib.sites.models import Site, RequestSite
-from django.contrib.syndication.views import add_domain
 from django.db import models
 from django.http import Http404, HttpResponse
-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 philo.contrib.penfield.exceptions import HttpNotAcceptable
-from philo.contrib.penfield.middleware import http_not_acceptable
+from philo.contrib.syndication.models import FeedView
 from philo.exceptions import ViewCanNotProvideSubpath
-from philo.models import Tag, Entity, MultiView, Page, register_value_model, Template
+from philo.models import Tag, Entity, Page, register_value_model
 from philo.models.fields import TemplateField
 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):
-       """
-       :class:`FeedView` handles a number of pages and related feeds for a single object such as a blog or newsletter. In addition to all other methods and attributes, :class:`FeedView` supports the same generic API as `django.contrib.syndication.views.Feed <http://docs.djangoproject.com/en/dev/ref/contrib/syndication/#django.contrib.syndication.django.contrib.syndication.views.Feed>`_.
-       
-       """
-       #: The type of feed which should be served by the :class:`FeedView`.
-       feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM)
-       #: The suffix which will be appended to a page URL for a feed of its items. Default: "feed"
-       feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
-       #: A :class:`BooleanField` - whether or not feeds are enabled.
-       feeds_enabled = models.BooleanField(default=True)
-       #: A :class:`PositiveIntegerField` - the maximum number of items to return for this feed. All items will be returned if this field is blank. Default: 15.
-       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.")
-       
-       #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the title of each item in the feed if provided.
-       item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
-       #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the description of each item in the feed if provided.
-       item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
-       
-       #: The name of the context variable to be populated with the items managed by the :class:`FeedView`.
-       item_context_var = 'items'
-       #: The attribute on a subclass of :class:`FeedView` which will contain the main object of a feed (such as a :class:`Blog`.)
-       object_attr = 'object'
-       
-       #: A description of the feeds served by the :class:`FeedView`. This is a required part of the :class:`django.contrib.syndication.view.Feed` API.
-       description = ""
-       
-       def feed_patterns(self, base, 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.
-               
-               :param base: The base of the returned patterns - that is, the subpath pattern which will reference the page for the items. The :attr:`feed_suffix` will be appended to this subpath.
-               :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` which will return an (``items``, ``extra_context``) tuple. This will be passed directly to :meth:`feed_view` and :meth:`page_view`.
-               :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be passed directly to :meth:`page_view` and will be rendered with the items from ``get_items_attr``.
-               :param reverse_name: The string which is considered the "name" of the view function returned by :meth:`page_view` for the given parameters.
-               :returns: Patterns suitable for use in urlpatterns.
-               
-               Example::
-               
-                       @property
-                       def urlpatterns(self):
-                               urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index')
-                               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')
-                               return urlpatterns
-               
-               """
-               urlpatterns = patterns('')
-               if self.feeds_enabled:
-                       feed_reverse_name = "%s_feed" % reverse_name
-                       feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name))
-                       feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
-                       urlpatterns += patterns('',
-                               url(feed_pattern, feed_view, name=feed_reverse_name),
-                       )
-               urlpatterns += patterns('',
-                       url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
-               )
-               return urlpatterns
-       
-       def get_object(self, request, **kwargs):
-               """By default, returns the object stored in the attribute named by :attr:`object_attr`. This can be overridden for subclasses that publish different data for different URL parameters. It is part of the :class:`django.contrib.syndication.views.Feed` API."""
-               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.
-               
-               :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments.
-               :param reverse_name: The name which can be used reverse this feed using the :class:`FeedView` as the urlconf.
-               
-               :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)
-                       self.populate_feed(feed, items, request)
-                       
-                       response = HttpResponse(mimetype=feed.mime_type)
-                       feed.write(response, 'utf-8')
-                       return response
-               
-               return inner
-       
-       def page_view(self, get_items_attr, page_attr):
-               """
-               :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments.
-               :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be rendered with the items from ``get_items_attr``.
-               
-               :returns: A view function that renders a list of items as an :class:`HttpResponse`.
-               
-               """
-               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 an :class:`HttpRequest`, 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):
-               """
-               Intelligently chooses a feed type for a given request. Tries to return :attr:`feed_type`, but if the Accept header does not include that mimetype, tries to return the best match from the feed types that are offered by the :class:`FeedView`. If none of the offered feed types are accepted by the :class:`HttpRequest`, then this method will raise :exc:`philo.contrib.penfield.exceptions.HttpNotAcceptable`.
-               
-               """
-               feed_type = self.feed_type
-               if feed_type not in FEEDS:
-                       feed_type = FEEDS.keys()[0]
-               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:
-                               raise HttpNotAcceptable
-               return FEEDS[feed_type]
-       
-       def get_feed(self, obj, request, reverse_name):
-               """
-               Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object.
-               
-               """
-               try:
-                       current_site = Site.objects.get_current()
-               except Site.DoesNotExist:
-                       current_site = RequestSite(request)
-               
-               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 = add_domain(
-                               current_site.domain,
-                               self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node._subpath, with_domain=True, request=request, secure=request.is_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):
-               """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``."""
-               if self.item_title_template:
-                       title_template = DjangoTemplate(self.item_title_template.code)
-               else:
-                       title_template = None
-               if self.item_description_template:
-                       description_template = DjangoTemplate(self.item_description_template.code)
-               else:
-                       description_template = None
-               
-               node = request.node
-               try:
-                       current_site = Site.objects.get_current()
-               except Site.DoesNotExist:
-                       current_site = RequestSite(request)
-               
-               if self.feed_length is not None:
-                       items = items[:self.feed_length]
-               
-               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(add_domain(
-                                                       current_site.domain,
-                                                       enc_url,
-                                                       request.is_secure()
-                                       )),
-                                       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):
        """Represents a blog which can be posted to."""
diff --git a/philo/contrib/syndication/__init__.py b/philo/contrib/syndication/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/philo/contrib/syndication/feeds.py b/philo/contrib/syndication/feeds.py
new file mode 100644 (file)
index 0000000..0554591
--- /dev/null
@@ -0,0 +1,13 @@
+from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
+
+from philo.utils.registry import Registry
+
+
+DEFAULT_FEED = Atom1Feed
+
+
+registry = Registry()
+
+
+registry.register(Atom1Feed, verbose_name='Atom')
+registry.register(Rss201rev2Feed, verbose_name='RSS')
\ No newline at end of file
diff --git a/philo/contrib/syndication/models.py b/philo/contrib/syndication/models.py
new file mode 100644 (file)
index 0000000..aea2077
--- /dev/null
@@ -0,0 +1,309 @@
+from django.conf import settings
+from django.conf.urls.defaults import url, patterns, include
+from django.contrib.sites.models import Site, RequestSite
+from django.contrib.syndication.views import add_domain
+from django.db import models
+from django.http import HttpResponse
+from django.template import RequestContext, Template as DjangoTemplate
+from django.utils import feedgenerator, tzinfo
+from django.utils.encoding import smart_unicode, force_unicode
+from django.utils.html import escape
+
+from philo.contrib.syndication.exceptions import HttpNotAcceptable
+from philo.contrib.syndication.feeds import registry, DEFAULT_FEED
+from philo.contrib.syndication.middleware import http_not_acceptable
+from philo.models import Page, Template, MultiView
+
+try:
+       import mimeparse
+except:
+       mimeparse = None
+
+
+class FeedView(MultiView):
+       """
+       :class:`FeedView` handles a number of pages and related feeds for a single object such as a blog or newsletter. In addition to all other methods and attributes, :class:`FeedView` supports the same generic API as `django.contrib.syndication.views.Feed <http://docs.djangoproject.com/en/dev/ref/contrib/syndication/#django.contrib.syndication.django.contrib.syndication.views.Feed>`_.
+       
+       """
+       #: The type of feed which should be served by the :class:`FeedView`.
+       feed_type = models.CharField(max_length=50, choices=registry.choices, default=registry.get_slug(DEFAULT_FEED))
+       #: The suffix which will be appended to a page URL for a feed of its items. Default: "feed"
+       feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
+       #: A :class:`BooleanField` - whether or not feeds are enabled.
+       feeds_enabled = models.BooleanField(default=True)
+       #: A :class:`PositiveIntegerField` - the maximum number of items to return for this feed. All items will be returned if this field is blank. Default: 15.
+       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.")
+       
+       #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the title of each item in the feed if provided.
+       item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
+       #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the description of each item in the feed if provided.
+       item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
+       
+       #: The name of the context variable to be populated with the items managed by the :class:`FeedView`.
+       item_context_var = 'items'
+       #: The attribute on a subclass of :class:`FeedView` which will contain the main object of a feed (such as a :class:`Blog`.)
+       object_attr = 'object'
+       
+       #: A description of the feeds served by the :class:`FeedView`. This is a required part of the :class:`django.contrib.syndication.view.Feed` API.
+       description = ""
+       
+       def feed_patterns(self, base, 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.
+               
+               :param base: The base of the returned patterns - that is, the subpath pattern which will reference the page for the items. The :attr:`feed_suffix` will be appended to this subpath.
+               :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` which will return an (``items``, ``extra_context``) tuple. This will be passed directly to :meth:`feed_view` and :meth:`page_view`.
+               :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be passed directly to :meth:`page_view` and will be rendered with the items from ``get_items_attr``.
+               :param reverse_name: The string which is considered the "name" of the view function returned by :meth:`page_view` for the given parameters.
+               :returns: Patterns suitable for use in urlpatterns.
+               
+               Example::
+               
+                       @property
+                       def urlpatterns(self):
+                               urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index')
+                               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')
+                               return urlpatterns
+               
+               """
+               urlpatterns = patterns('')
+               if self.feeds_enabled:
+                       feed_reverse_name = "%s_feed" % reverse_name
+                       feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name))
+                       feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
+                       urlpatterns += patterns('',
+                               url(feed_pattern, feed_view, name=feed_reverse_name),
+                       )
+               urlpatterns += patterns('',
+                       url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
+               )
+               return urlpatterns
+       
+       def get_object(self, request, **kwargs):
+               """By default, returns the object stored in the attribute named by :attr:`object_attr`. This can be overridden for subclasses that publish different data for different URL parameters. It is part of the :class:`django.contrib.syndication.views.Feed` API."""
+               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.
+               
+               :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments.
+               :param reverse_name: The name which can be used reverse this feed using the :class:`FeedView` as the urlconf.
+               
+               :returns: A view function that renders a list of items as a feed.
+               
+               """
+               get_items = get_items_attr if callable(get_items_attr) else 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)
+                       self.populate_feed(feed, items, request)
+                       
+                       response = HttpResponse(mimetype=feed.mime_type)
+                       feed.write(response, 'utf-8')
+                       return response
+               
+               return inner
+       
+       def page_view(self, get_items_attr, page_attr):
+               """
+               :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments.
+               :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be rendered with the items from ``get_items_attr``.
+               
+               :returns: A view function that renders a list of items as an :class:`HttpResponse`.
+               
+               """
+               get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
+               page = page_attr if isinstance(page_attr, Page) else 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 an :class:`HttpRequest`, 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):
+               """
+               Intelligently chooses a feed type for a given request. Tries to return :attr:`feed_type`, but if the Accept header does not include that mimetype, tries to return the best match from the feed types that are offered by the :class:`FeedView`. If none of the offered feed types are accepted by the :class:`HttpRequest`, then this method will raise :exc:`philo.contrib.penfield.exceptions.HttpNotAcceptable`.
+               
+               """
+               feed_type = registry.get(self.feed_type, DEFAULT_FEED)
+               mt = feed_type.mime_type
+               accept = request.META.get('HTTP_ACCEPT')
+               if accept and mt not in accept and "*/*" not in accept and "%s/*" % mt.split("/")[0] not in accept:
+                       # Wups! They aren't accepting the chosen format. Is there another format we can use?
+                       feed_type = None
+                       accepted_mts = dict([(obj.mime_type, obj) for obj in registry.values()])
+                       if mimeparse:
+                               mt = mimeparse.best_match(accepted_mts.keys(), accept)
+                               if mt:
+                                       feed_type = accepted_mts[mt]
+                       else:
+                               for mt in accepted_mts:
+                                       if mt in accept or "%s/*" % mt.split("/")[0] in accept:
+                                               feed_type = accepted_mts[mt]
+                                               break
+                       if not feed_type:
+                               raise HttpNotAcceptable
+               return feed_type
+       
+       def get_feed(self, obj, request, reverse_name):
+               """
+               Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object.
+               
+               """
+               try:
+                       current_site = Site.objects.get_current()
+               except Site.DoesNotExist:
+                       current_site = RequestSite(request)
+               
+               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 = add_domain(
+                               current_site.domain,
+                               self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node._subpath, with_domain=True, request=request, secure=request.is_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):
+               """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``."""
+               if self.item_title_template:
+                       title_template = DjangoTemplate(self.item_title_template.code)
+               else:
+                       title_template = None
+               if self.item_description_template:
+                       description_template = DjangoTemplate(self.item_description_template.code)
+               else:
+                       description_template = None
+               
+               node = request.node
+               try:
+                       current_site = Site.objects.get_current()
+               except Site.DoesNotExist:
+                       current_site = RequestSite(request)
+               
+               if self.feed_length is not None:
+                       items = items[:self.feed_length]
+               
+               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(add_domain(
+                                                       current_site.domain,
+                                                       enc_url,
+                                                       request.is_secure()
+                                       )),
+                                       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
\ No newline at end of file