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 HttpResponse
7 from django.template import RequestContext, Template as DjangoTemplate
8 from django.utils import feedgenerator, tzinfo
9 from django.utils.encoding import smart_unicode, force_unicode
10 from django.utils.html import escape
12 from philo.contrib.syndication.exceptions import HttpNotAcceptable
13 from philo.contrib.syndication.feeds import registry, DEFAULT_FEED
14 from philo.contrib.syndication.middleware import http_not_acceptable
15 from philo.models import Page, Template, MultiView
23 class FeedView(MultiView):
25 :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>`_.
28 #: The type of feed which should be served by the :class:`FeedView`.
29 feed_type = models.CharField(max_length=50, choices=registry.choices, default=registry.get_slug(DEFAULT_FEED))
30 #: The suffix which will be appended to a page URL for a feed of its items. Default: "feed"
31 feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
32 #: A :class:`BooleanField` - whether or not feeds are enabled.
33 feeds_enabled = models.BooleanField(default=True)
34 #: 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.
35 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.")
37 #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the title of each item in the feed if provided.
38 item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
39 #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the description of each item in the feed if provided.
40 item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
42 #: The name of the context variable to be populated with the items managed by the :class:`FeedView`.
43 item_context_var = 'items'
44 #: The attribute on a subclass of :class:`FeedView` which will contain the main object of a feed (such as a :class:`Blog`.)
45 object_attr = 'object'
47 #: A description of the feeds served by the :class:`FeedView`. This is a required part of the :class:`django.contrib.syndication.view.Feed` API.
50 def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
52 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.
54 :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.
55 :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`.
56 :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``.
57 :param reverse_name: The string which is considered the "name" of the view function returned by :meth:`page_view` for the given parameters.
58 :returns: Patterns suitable for use in urlpatterns.
63 def urlpatterns(self):
64 urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index')
65 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')
69 urlpatterns = patterns('')
70 if self.feeds_enabled:
71 feed_reverse_name = "%s_feed" % reverse_name
72 feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name))
73 feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
74 urlpatterns += patterns('',
75 url(feed_pattern, feed_view, name=feed_reverse_name),
77 urlpatterns += patterns('',
78 url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
82 def get_object(self, request, **kwargs):
83 """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."""
84 return getattr(self, self.object_attr)
86 def feed_view(self, get_items_attr, reverse_name):
88 Returns a view function that renders a list of items as a feed.
90 :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.
91 :param reverse_name: The name which can be used reverse this feed using the :class:`FeedView` as the urlconf.
93 :returns: A view function that renders a list of items as a feed.
96 get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
98 def inner(request, extra_context=None, *args, **kwargs):
99 obj = self.get_object(request, *args, **kwargs)
100 feed = self.get_feed(obj, request, reverse_name)
101 items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
102 self.populate_feed(feed, items, request)
104 response = HttpResponse(mimetype=feed.mime_type)
105 feed.write(response, 'utf-8')
110 def page_view(self, get_items_attr, page_attr):
112 :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.
113 :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``.
115 :returns: A view function that renders a list of items as an :class:`HttpResponse`.
118 get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
119 page = page_attr if isinstance(page_attr, Page) else getattr(self, page_attr)
121 def inner(request, extra_context=None, *args, **kwargs):
122 items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
123 items, item_context = self.process_page_items(request, items)
125 context = self.get_context()
126 context.update(extra_context or {})
127 context.update(item_context or {})
129 return page.render_to_response(request, extra_context=context)
132 def process_page_items(self, request, items):
134 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.
138 self.item_context_var: items
140 return items, item_context
142 def get_feed_type(self, request):
144 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`.
147 feed_type = registry.get(self.feed_type, DEFAULT_FEED)
148 mt = feed_type.mime_type
149 accept = request.META.get('HTTP_ACCEPT')
150 if accept and mt not in accept and "*/*" not in accept and "%s/*" % mt.split("/")[0] not in accept:
151 # Wups! They aren't accepting the chosen format. Is there another format we can use?
153 accepted_mts = dict([(obj.mime_type, obj) for obj in registry.values()])
155 mt = mimeparse.best_match(accepted_mts.keys(), accept)
157 feed_type = accepted_mts[mt]
159 for mt in accepted_mts:
160 if mt in accept or "%s/*" % mt.split("/")[0] in accept:
161 feed_type = accepted_mts[mt]
164 raise HttpNotAcceptable
167 def get_feed(self, obj, request, reverse_name):
169 Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object.
173 current_site = Site.objects.get_current()
174 except Site.DoesNotExist:
175 current_site = RequestSite(request)
177 feed_type = self.get_feed_type(request)
179 link = node.get_absolute_url(with_domain=True, request=request, secure=request.is_secure())
182 title = self.__get_dynamic_attr('title', obj),
183 subtitle = self.__get_dynamic_attr('subtitle', obj),
185 description = self.__get_dynamic_attr('description', obj),
186 language = settings.LANGUAGE_CODE.decode(),
187 feed_url = add_domain(
189 self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node._subpath, with_domain=True, request=request, secure=request.is_secure()),
192 author_name = self.__get_dynamic_attr('author_name', obj),
193 author_link = self.__get_dynamic_attr('author_link', obj),
194 author_email = self.__get_dynamic_attr('author_email', obj),
195 categories = self.__get_dynamic_attr('categories', obj),
196 feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
197 feed_guid = self.__get_dynamic_attr('feed_guid', obj),
198 ttl = self.__get_dynamic_attr('ttl', obj),
199 **self.feed_extra_kwargs(obj)
203 def populate_feed(self, feed, items, request):
204 """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``."""
205 if self.item_title_template:
206 title_template = DjangoTemplate(self.item_title_template.code)
208 title_template = None
209 if self.item_description_template:
210 description_template = DjangoTemplate(self.item_description_template.code)
212 description_template = None
216 current_site = Site.objects.get_current()
217 except Site.DoesNotExist:
218 current_site = RequestSite(request)
220 if self.feed_length is not None:
221 items = items[:self.feed_length]
224 if title_template is not None:
225 title = title_template.render(RequestContext(request, {'obj': item}))
227 title = self.__get_dynamic_attr('item_title', item)
228 if description_template is not None:
229 description = description_template.render(RequestContext(request, {'obj': item}))
231 description = self.__get_dynamic_attr('item_description', item)
233 link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
236 enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
238 enc = feedgenerator.Enclosure(
239 url = smart_unicode(add_domain(
244 length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
245 mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
247 author_name = self.__get_dynamic_attr('item_author_name', item)
248 if author_name is not None:
249 author_email = self.__get_dynamic_attr('item_author_email', item)
250 author_link = self.__get_dynamic_attr('item_author_link', item)
252 author_email = author_link = None
254 pubdate = self.__get_dynamic_attr('item_pubdate', item)
255 if pubdate and not pubdate.tzinfo:
256 ltz = tzinfo.LocalTimezone(pubdate)
257 pubdate = pubdate.replace(tzinfo=ltz)
262 description = description,
263 unique_id = self.__get_dynamic_attr('item_guid', item, link),
266 author_name = author_name,
267 author_email = author_email,
268 author_link = author_link,
269 categories = self.__get_dynamic_attr('item_categories', item),
270 item_copyright = self.__get_dynamic_attr('item_copyright', item),
271 **self.item_extra_kwargs(item)
274 def __get_dynamic_attr(self, attname, obj, default=None):
276 attr = getattr(self, attname)
277 except AttributeError:
280 # Check func_code.co_argcount rather than try/excepting the
281 # function and catching the TypeError, because something inside
282 # the function may raise the TypeError. This technique is more
284 if hasattr(attr, 'func_code'):
285 argcount = attr.func_code.co_argcount
287 argcount = attr.__call__.func_code.co_argcount
288 if argcount == 2: # one argument is 'self'
294 def feed_extra_kwargs(self, obj):
295 """Returns an extra keyword arguments dictionary that is used when initializing the feed generator."""
298 def item_extra_kwargs(self, item):
299 """Returns an extra keyword arguments dictionary that is used with the `add_item` call of the feed generator."""
302 def item_title(self, item):
303 return escape(force_unicode(item))
305 def item_description(self, item):
306 return force_unicode(item)