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.winer.exceptions import HttpNotAcceptable
13 from philo.contrib.winer.feeds import registry, DEFAULT_FEED
14 from philo.contrib.winer.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. In addition to ``base`` and ``base`` + :attr:`feed_suffix`, patterns will be provided for each registered feed type as ``base`` + ``slug``.
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 suffixes = [(self.feed_suffix, None)] + [(slug, slug) for slug in registry]
72 for suffix, feed_type in suffixes:
73 feed_view = http_not_acceptable(self.feed_view(get_items_attr, reverse_name, feed_type))
74 feed_pattern = r'%s%s%s$' % (base, "/" if base and base[-1] != "^" else "", suffix)
75 urlpatterns += patterns('',
76 url(feed_pattern, feed_view, name="%s_%s" % (reverse_name, suffix)),
78 urlpatterns += patterns('',
79 url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
83 def get_object(self, request, **kwargs):
84 """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."""
85 return getattr(self, self.object_attr)
87 def feed_view(self, get_items_attr, reverse_name, feed_type=None):
89 Returns a view function that renders a list of items as a feed.
91 :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.
92 :param reverse_name: The name which can be used reverse the page for this feed using the :class:`FeedView` as the urlconf.
93 :param feed_type: The slug used to render the feed class which will be used by the returned view function.
95 :returns: A view function that renders a list of items as a feed.
98 get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
100 def inner(request, extra_context=None, *args, **kwargs):
101 obj = self.get_object(request, *args, **kwargs)
102 feed = self.get_feed(obj, request, reverse_name, feed_type, *args, **kwargs)
103 items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
104 self.populate_feed(feed, items, request)
106 response = HttpResponse(mimetype=feed.mime_type)
107 feed.write(response, 'utf-8')
112 def page_view(self, get_items_attr, page_attr):
114 :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.
115 :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``.
117 :returns: A view function that renders a list of items as an :class:`HttpResponse`.
120 get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
121 page = page_attr if isinstance(page_attr, Page) else getattr(self, page_attr)
123 def inner(request, extra_context=None, *args, **kwargs):
124 items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
125 items, item_context = self.process_page_items(request, items)
127 context = self.get_context()
128 context.update(extra_context or {})
129 context.update(item_context or {})
131 return page.render_to_response(request, extra_context=context)
134 def process_page_items(self, request, items):
136 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.
140 self.item_context_var: items
142 return items, item_context
144 def get_feed_type(self, request, feed_type=None):
146 If ``feed_type`` is not ``None``, returns the corresponding class from the registry or reises :exc:`.HttpNotAcceptable`. Otherwise, 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`, raises :exc:`.HttpNotAcceptable`.
149 if feed_type is not None:
150 feed_type = registry[feed_type]
153 feed_type = registry.get(self.feed_type, DEFAULT_FEED)
155 mt = feed_type.mime_type
156 accept = request.META.get('HTTP_ACCEPT')
157 if accept and mt not in accept and "*/*" not in accept and "%s/*" % mt.split("/")[0] not in accept:
158 # Wups! They aren't accepting the chosen format.
161 # Is there another format we can use?
162 accepted_mts = dict([(obj.mime_type, obj) for obj in registry.values()])
164 mt = mimeparse.best_match(accepted_mts.keys(), accept)
166 feed_type = accepted_mts[mt]
168 for mt in accepted_mts:
169 if mt in accept or "%s/*" % mt.split("/")[0] in accept:
170 feed_type = accepted_mts[mt]
173 raise HttpNotAcceptable
176 def get_feed(self, obj, request, reverse_name, feed_type=None, *args, **kwargs):
178 Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object.
180 :param obj: The object for which the feed should be generated.
181 :param request: The current request.
182 :param reverse_name: The name which can be used to reverse the URL of the page corresponding to this feed.
183 :param feed_type: The slug used to register the feed class that will be instantiated and returned.
185 :returns: An instance of the feed class registered as ``feed_type``, falling back to :attr:`feed_type` if ``feed_type`` is ``None``.
189 current_site = Site.objects.get_current()
190 except Site.DoesNotExist:
191 current_site = RequestSite(request)
193 feed_type = self.get_feed_type(request, feed_type)
195 link = node.construct_url(self.reverse(reverse_name, args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure())
198 title = self.__get_dynamic_attr('title', obj),
199 subtitle = self.__get_dynamic_attr('subtitle', obj),
201 description = self.__get_dynamic_attr('description', obj),
202 language = settings.LANGUAGE_CODE.decode(),
203 feed_url = add_domain(
205 self.__get_dynamic_attr('feed_url', obj) or node.construct_url(self.reverse("%s_%s" % (reverse_name, registry.get_slug(feed_type)), args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure()),
208 author_name = self.__get_dynamic_attr('author_name', obj),
209 author_link = self.__get_dynamic_attr('author_link', obj),
210 author_email = self.__get_dynamic_attr('author_email', obj),
211 categories = self.__get_dynamic_attr('categories', obj),
212 feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
213 feed_guid = self.__get_dynamic_attr('feed_guid', obj),
214 ttl = self.__get_dynamic_attr('ttl', obj),
215 **self.feed_extra_kwargs(obj)
219 def populate_feed(self, feed, items, request):
220 """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``."""
221 if self.item_title_template:
222 title_template = DjangoTemplate(self.item_title_template.code)
224 title_template = None
225 if self.item_description_template:
226 description_template = DjangoTemplate(self.item_description_template.code)
228 description_template = None
232 current_site = Site.objects.get_current()
233 except Site.DoesNotExist:
234 current_site = RequestSite(request)
236 if self.feed_length is not None:
237 items = items[:self.feed_length]
240 if title_template is not None:
241 title = title_template.render(RequestContext(request, {'obj': item}))
243 title = self.__get_dynamic_attr('item_title', item)
244 if description_template is not None:
245 description = description_template.render(RequestContext(request, {'obj': item}))
247 description = self.__get_dynamic_attr('item_description', item)
249 link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
252 enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
254 enc = feedgenerator.Enclosure(
255 url = smart_unicode(add_domain(
260 length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
261 mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
263 author_name = self.__get_dynamic_attr('item_author_name', item)
264 if author_name is not None:
265 author_email = self.__get_dynamic_attr('item_author_email', item)
266 author_link = self.__get_dynamic_attr('item_author_link', item)
268 author_email = author_link = None
270 pubdate = self.__get_dynamic_attr('item_pubdate', item)
271 if pubdate and not pubdate.tzinfo:
272 ltz = tzinfo.LocalTimezone(pubdate)
273 pubdate = pubdate.replace(tzinfo=ltz)
278 description = description,
279 unique_id = self.__get_dynamic_attr('item_guid', item, link),
282 author_name = author_name,
283 author_email = author_email,
284 author_link = author_link,
285 categories = self.__get_dynamic_attr('item_categories', item),
286 item_copyright = self.__get_dynamic_attr('item_copyright', item),
287 **self.item_extra_kwargs(item)
290 def __get_dynamic_attr(self, attname, obj, default=None):
292 attr = getattr(self, attname)
293 except AttributeError:
296 # Check func_code.co_argcount rather than try/excepting the
297 # function and catching the TypeError, because something inside
298 # the function may raise the TypeError. This technique is more
300 if hasattr(attr, 'func_code'):
301 argcount = attr.func_code.co_argcount
303 argcount = attr.__call__.func_code.co_argcount
304 if argcount == 2: # one argument is 'self'
310 def feed_extra_kwargs(self, obj):
311 """Returns an extra keyword arguments dictionary that is used when initializing the feed generator."""
314 def item_extra_kwargs(self, item):
315 """Returns an extra keyword arguments dictionary that is used with the `add_item` call of the feed generator."""
318 def item_title(self, item):
319 return escape(force_unicode(item))
321 def item_description(self, item):
322 return force_unicode(item)