Tweaked the FeedView API slightly to pass the object from get_object into the get_ite...
[philo.git] / philo / contrib / winer / models.py
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
11
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
16
17 try:
18         import mimeparse
19 except:
20         mimeparse = None
21
22
23 class FeedView(MultiView):
24         """
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>`_.
26         
27         """
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.
31         #:
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.")
38         
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")
43         
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"
47         #:
48         #: Example::
49         #:
50         #:     class BlogView(FeedView):
51         #:         blog = models.ForeignKey(Blog)
52         #:         
53         #:         object_attr = 'blog'
54         #:         item_context_var = 'entries'
55         object_attr = 'object'
56         
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.
58         description = ""
59         
60         def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
61                 """
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``.
63                 
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.
69                 
70                 Example::
71                 
72                         class BlogView(FeedView):
73                             blog = models.ForeignKey(Blog)
74                             entry_archive_page = models.ForeignKey(Page)
75                             
76                             @property
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')
80                                 return urlpatterns
81                             
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
86                 
87                 .. seealso:: :meth:`get_feed_type`
88                 
89                 """
90                 feed_patterns = ()
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)
99         
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)
103         
104         def feed_view(self, get_items_attr, reverse_name, feed_type=None):
105                 """
106                 Returns a view function that renders a list of items as a feed.
107                 
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.
111                 
112                 :returns: A view function that renders a list of items as a feed.
113                 
114                 """
115                 get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
116                 
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)
122                         
123                         response = HttpResponse(mimetype=feed.mime_type)
124                         feed.write(response, 'utf-8')
125                         return response
126                 
127                 return inner
128         
129         def page_view(self, get_items_attr, page_attr):
130                 """
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``.
133                 
134                 :returns: A view function that renders a list of items as an :class:`HttpResponse`.
135                 
136                 """
137                 get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
138                 page = page_attr if isinstance(page_attr, Page) else getattr(self, page_attr)
139                 
140                 def inner(request, extra_context=None, *args, **kwargs):
141                         obj = self.get_object(request, *args, **kwargs)
142                         items, extra_context = get_items(obj, request, extra_context=extra_context, *args, **kwargs)
143                         items, item_context = self.process_page_items(request, items)
144                         
145                         context = self.get_context()
146                         context.update(extra_context or {})
147                         context.update(item_context or {})
148                         
149                         return page.render_to_response(request, extra_context=context)
150                 return inner
151         
152         def process_page_items(self, request, items):
153                 """
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.
155                 
156                 """
157                 item_context = {
158                         self.item_context_var: items
159                 }
160                 return items, item_context
161         
162         def get_feed_type(self, request, feed_type=None):
163                 """
164                 If ``feed_type`` is not ``None``, returns the corresponding class from the registry or raises :exc:`.HttpNotAcceptable`.
165                 
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`.
167                 
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.
169                 
170                 """
171                 if feed_type is not None:
172                         feed_type = registry[feed_type]
173                         loose = False
174                 else:
175                         feed_type = registry.get(self.feed_type, DEFAULT_FEED)
176                         loose = True
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.
181                         feed_type = None
182                         if loose:
183                                 # Is there another format we can use?
184                                 accepted_mts = dict([(obj.mime_type, obj) for obj in registry.values()])
185                                 if mimeparse:
186                                         mt = mimeparse.best_match(accepted_mts.keys(), accept)
187                                         if mt:
188                                                 feed_type = accepted_mts[mt]
189                                 else:
190                                         for mt in accepted_mts:
191                                                 if mt in accept or "%s/*" % mt.split("/")[0] in accept:
192                                                         feed_type = accepted_mts[mt]
193                                                         break
194                         if not feed_type:
195                                 raise HttpNotAcceptable
196                 return feed_type
197         
198         def get_feed(self, obj, request, reverse_name, feed_type=None, *args, **kwargs):
199                 """
200                 Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object.
201                 
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.
206                 
207                 :returns: An instance of the feed class registered as ``feed_type``, falling back to :attr:`feed_type` if ``feed_type`` is ``None``.
208                 
209                 """
210                 try:
211                         current_site = Site.objects.get_current()
212                 except Site.DoesNotExist:
213                         current_site = RequestSite(request)
214                 
215                 feed_type = self.get_feed_type(request, feed_type)
216                 node = request.node
217                 link = node.construct_url(self.reverse(reverse_name, args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure())
218                 
219                 feed = feed_type(
220                         title = self.__get_dynamic_attr('title', obj),
221                         subtitle = self.__get_dynamic_attr('subtitle', obj),
222                         link = link,
223                         description = self.__get_dynamic_attr('description', obj),
224                         language = settings.LANGUAGE_CODE.decode(),
225                         feed_url = add_domain(
226                                 current_site.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()),
228                                 request.is_secure()
229                         ),
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)
238                 )
239                 return feed
240         
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)
245                 else:
246                         title_template = None
247                 if self.item_description_template:
248                         description_template = DjangoTemplate(self.item_description_template.code)
249                 else:
250                         description_template = None
251                 
252                 node = request.node
253                 try:
254                         current_site = Site.objects.get_current()
255                 except Site.DoesNotExist:
256                         current_site = RequestSite(request)
257                 
258                 if self.feed_length is not None:
259                         items = items[:self.feed_length]
260                 
261                 for item in items:
262                         if title_template is not None:
263                                 title = title_template.render(RequestContext(request, {'obj': item}))
264                         else:
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}))
268                         else:
269                                 description = self.__get_dynamic_attr('item_description', item)
270                         
271                         link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
272                         
273                         enc = None
274                         enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
275                         if enc_url:
276                                 enc = feedgenerator.Enclosure(
277                                         url = smart_unicode(add_domain(
278                                                         current_site.domain,
279                                                         enc_url,
280                                                         request.is_secure()
281                                         )),
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))
284                                 )
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)
289                         else:
290                                 author_email = author_link = None
291                         
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)
296                         
297                         feed.add_item(
298                                 title = title,
299                                 link = link,
300                                 description = description,
301                                 unique_id = self.__get_dynamic_attr('item_guid', item, link),
302                                 enclosure = enc,
303                                 pubdate = pubdate,
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)
310                         )
311         
312         def __get_dynamic_attr(self, attname, obj, default=None):
313                 try:
314                         attr = getattr(self, attname)
315                 except AttributeError:
316                         return default
317                 if callable(attr):
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
321                         # accurate.
322                         if hasattr(attr, 'func_code'):
323                                 argcount = attr.func_code.co_argcount
324                         else:
325                                 argcount = attr.__call__.func_code.co_argcount
326                         if argcount == 2: # one argument is 'self'
327                                 return attr(obj)
328                         else:
329                                 return attr()
330                 return attr
331         
332         def feed_extra_kwargs(self, obj):
333                 """Returns an extra keyword arguments dictionary that is used when initializing the feed generator."""
334                 return {}
335         
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."""
338                 return {}
339         
340         def item_title(self, item):
341                 return escape(force_unicode(item))
342         
343         def item_description(self, item):
344                 return force_unicode(item)
345         
346         class Meta:
347                 abstract=True