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` is an abstract model which 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 :attr:`feed_type` feed of its items. Default: "feed". Note that RSS and Atom feeds will always be available at ``<page_url>/rss`` and ``<page_url>/atom`` regardless of the value of this setting.
32 #: .. seealso:: :meth:`get_feed_type`, :meth:`feed_patterns`
33 feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
34 #: A :class:`BooleanField` - whether or not feeds are enabled.
35 feeds_enabled = models.BooleanField(default=True)
36 #: 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.
37 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.")
39 #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the title of each item in the feed if provided.
40 item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
41 #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the description of each item in the feed if provided.
42 item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
44 #: An attribute holding the name of the context variable to be populated with the items managed by the :class:`FeedView`. Default: "items"
45 item_context_var = 'items'
46 #: An attribute holding the name of the attribute on a subclass of :class:`FeedView` which will contain the main object of a feed (such as a :class:`~philo.contrib.penfield.models.Blog`.) Default: "object"
50 #: class BlogView(FeedView):
51 #: blog = models.ForeignKey(Blog)
53 #: object_attr = 'blog'
54 #: item_context_var = 'entries'
55 object_attr = 'object'
57 #: An attribute holding a description of the feeds served by the :class:`FeedView`. This is a required part of the :class:`django.contrib.syndication.view.Feed` API.
60 def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
62 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`` (which will serve the page at ``page_attr``) and ``base`` + :attr:`feed_suffix` (which will serve a :attr:`feed_type` feed), patterns will be provided for each registered feed type as ``base`` + ``slug``.
64 :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.
65 :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`.
66 :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``.
67 :param reverse_name: The string which is considered the "name" of the view function returned by :meth:`page_view` for the given parameters.
68 :returns: Patterns suitable for use in urlpatterns.
72 class BlogView(FeedView):
73 blog = models.ForeignKey(Blog)
74 entry_archive_page = models.ForeignKey(Page)
77 def urlpatterns(self):
78 urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index')
79 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')
82 def get_entries_by_ymd(request, year, month, day, extra_context=None):
83 entries = Blog.entries.all()
84 # filter entries based on the year, month, and day.
85 return entries, extra_context
87 .. seealso:: :meth:`get_feed_type`
91 if self.feeds_enabled:
92 suffixes = [(self.feed_suffix, None)] + [(slug, slug) for slug in registry]
93 for suffix, feed_type in suffixes:
94 feed_view = http_not_acceptable(self.feed_view(get_items_attr, reverse_name, feed_type))
95 feed_pattern = r'%s%s%s$' % (base, "/" if base and base[-1] != "^" else "", suffix)
96 feed_patterns += (url(feed_pattern, feed_view, name="%s_%s" % (reverse_name, suffix)),)
97 feed_patterns += (url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name),)
98 return patterns('', *feed_patterns)
100 def get_object(self, request, **kwargs):
101 """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."""
102 return getattr(self, self.object_attr)
104 def feed_view(self, get_items_attr, reverse_name, feed_type=None):
106 Returns a view function that renders a list of items as a feed.
108 :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 the object for the feed and view arguments.
109 :param reverse_name: The name which can be used reverse the page for this feed using the :class:`FeedView` as the urlconf.
110 :param feed_type: The slug used to render the feed class which will be used by the returned view function.
112 :returns: A view function that renders a list of items as a feed.
115 get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
117 def inner(request, extra_context=None, *args, **kwargs):
118 obj = self.get_object(request, *args, **kwargs)
119 feed = self.get_feed(obj, request, reverse_name, feed_type, *args, **kwargs)
120 items, xxx = get_items(obj, request, extra_context=extra_context, *args, **kwargs)
121 self.populate_feed(feed, items, request)
123 response = HttpResponse(mimetype=feed.mime_type)
124 feed.write(response, 'utf-8')
129 def page_view(self, get_items_attr, page_attr):
131 :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.
132 :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``.
134 :returns: A view function that renders a list of items as an :class:`HttpResponse`.
137 get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
139 def inner(request, extra_context=None, *args, **kwargs):
140 obj = self.get_object(request, *args, **kwargs)
141 items, extra_context = get_items(obj, request, extra_context=extra_context, *args, **kwargs)
142 items, item_context = self.process_page_items(request, items)
144 context = self.get_context()
145 context.update(extra_context or {})
146 context.update(item_context or {})
148 page = page_attr if isinstance(page_attr, Page) else getattr(self, page_attr)
149 return page.render_to_response(request, extra_context=context)
152 def process_page_items(self, request, items):
154 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.
158 self.item_context_var: items
160 return items, item_context
162 def get_feed_type(self, request, feed_type=None):
164 If ``feed_type`` is not ``None``, returns the corresponding class from the registry or raises :exc:`.HttpNotAcceptable`.
166 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`.
168 If `mimeparse <http://code.google.com/p/mimeparse/>`_ is installed, it will be used to select the best matching accepted format; otherwise, the first available format that is accepted will be selected.
171 if feed_type is not None:
172 feed_type = registry[feed_type]
175 feed_type = registry.get(self.feed_type, DEFAULT_FEED)
177 mt = feed_type.mime_type
178 accept = request.META.get('HTTP_ACCEPT')
179 if accept and mt not in accept and "*/*" not in accept and "%s/*" % mt.split("/")[0] not in accept:
180 # Wups! They aren't accepting the chosen format.
183 # Is there another format we can use?
184 accepted_mts = dict([(obj.mime_type, obj) for obj in registry.values()])
186 mt = mimeparse.best_match(accepted_mts.keys(), accept)
188 feed_type = accepted_mts[mt]
190 for mt in accepted_mts:
191 if mt in accept or "%s/*" % mt.split("/")[0] in accept:
192 feed_type = accepted_mts[mt]
195 raise HttpNotAcceptable
198 def get_feed(self, obj, request, reverse_name, feed_type=None, *args, **kwargs):
200 Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object.
202 :param obj: The object for which the feed should be generated.
203 :param request: The current request.
204 :param reverse_name: The name which can be used to reverse the URL of the page corresponding to this feed.
205 :param feed_type: The slug used to register the feed class that will be instantiated and returned.
207 :returns: An instance of the feed class registered as ``feed_type``, falling back to :attr:`feed_type` if ``feed_type`` is ``None``.
211 current_site = Site.objects.get_current()
212 except Site.DoesNotExist:
213 current_site = RequestSite(request)
215 feed_type = self.get_feed_type(request, feed_type)
217 link = node.construct_url(self.reverse(reverse_name, args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure())
220 title = self.__get_dynamic_attr('title', obj),
221 subtitle = self.__get_dynamic_attr('subtitle', obj),
223 description = self.__get_dynamic_attr('description', obj),
224 language = settings.LANGUAGE_CODE.decode(),
225 feed_url = add_domain(
227 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()),
230 author_name = self.__get_dynamic_attr('author_name', obj),
231 author_link = self.__get_dynamic_attr('author_link', obj),
232 author_email = self.__get_dynamic_attr('author_email', obj),
233 categories = self.__get_dynamic_attr('categories', obj),
234 feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
235 feed_guid = self.__get_dynamic_attr('feed_guid', obj),
236 ttl = self.__get_dynamic_attr('ttl', obj),
237 **self.feed_extra_kwargs(obj)
241 def populate_feed(self, feed, items, request):
242 """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``."""
243 if self.item_title_template:
244 title_template = DjangoTemplate(self.item_title_template.code)
246 title_template = None
247 if self.item_description_template:
248 description_template = DjangoTemplate(self.item_description_template.code)
250 description_template = None
254 current_site = Site.objects.get_current()
255 except Site.DoesNotExist:
256 current_site = RequestSite(request)
258 if self.feed_length is not None:
259 items = items[:self.feed_length]
262 if title_template is not None:
263 title = title_template.render(RequestContext(request, {'obj': item}))
265 title = self.__get_dynamic_attr('item_title', item)
266 if description_template is not None:
267 description = description_template.render(RequestContext(request, {'obj': item}))
269 description = self.__get_dynamic_attr('item_description', item)
271 link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
274 enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
276 enc = feedgenerator.Enclosure(
277 url = smart_unicode(add_domain(
282 length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
283 mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
285 author_name = self.__get_dynamic_attr('item_author_name', item)
286 if author_name is not None:
287 author_email = self.__get_dynamic_attr('item_author_email', item)
288 author_link = self.__get_dynamic_attr('item_author_link', item)
290 author_email = author_link = None
292 pubdate = self.__get_dynamic_attr('item_pubdate', item)
293 if pubdate and not pubdate.tzinfo:
294 ltz = tzinfo.LocalTimezone(pubdate)
295 pubdate = pubdate.replace(tzinfo=ltz)
300 description = description,
301 unique_id = self.__get_dynamic_attr('item_guid', item, link),
304 author_name = author_name,
305 author_email = author_email,
306 author_link = author_link,
307 categories = self.__get_dynamic_attr('item_categories', item),
308 item_copyright = self.__get_dynamic_attr('item_copyright', item),
309 **self.item_extra_kwargs(item)
312 def __get_dynamic_attr(self, attname, obj, default=None):
314 attr = getattr(self, attname)
315 except AttributeError:
318 # Check func_code.co_argcount rather than try/excepting the
319 # function and catching the TypeError, because something inside
320 # the function may raise the TypeError. This technique is more
322 if hasattr(attr, 'func_code'):
323 argcount = attr.func_code.co_argcount
325 argcount = attr.__call__.func_code.co_argcount
326 if argcount == 2: # one argument is 'self'
332 def feed_extra_kwargs(self, obj):
333 """Returns an extra keyword arguments dictionary that is used when initializing the feed generator."""
336 def item_extra_kwargs(self, item):
337 """Returns an extra keyword arguments dictionary that is used with the `add_item` call of the feed generator."""
340 def item_title(self, item):
341 return escape(force_unicode(item))
343 def item_description(self, item):
344 return force_unicode(item)