Minor optimization: only call patterns at the end of winer.FeedView.feed_patterns.
[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 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(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                         items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
142                         items, item_context = self.process_page_items(request, items)
143                         
144                         context = self.get_context()
145                         context.update(extra_context or {})
146                         context.update(item_context or {})
147                         
148                         return page.render_to_response(request, extra_context=context)
149                 return inner
150         
151         def process_page_items(self, request, items):
152                 """
153                 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.
154                 
155                 """
156                 item_context = {
157                         self.item_context_var: items
158                 }
159                 return items, item_context
160         
161         def get_feed_type(self, request, feed_type=None):
162                 """
163                 If ``feed_type`` is not ``None``, returns the corresponding class from the registry or raises :exc:`.HttpNotAcceptable`.
164                 
165                 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`.
166                 
167                 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.
168                 
169                 """
170                 if feed_type is not None:
171                         feed_type = registry[feed_type]
172                         loose = False
173                 else:
174                         feed_type = registry.get(self.feed_type, DEFAULT_FEED)
175                         loose = True
176                 mt = feed_type.mime_type
177                 accept = request.META.get('HTTP_ACCEPT')
178                 if accept and mt not in accept and "*/*" not in accept and "%s/*" % mt.split("/")[0] not in accept:
179                         # Wups! They aren't accepting the chosen format.
180                         feed_type = None
181                         if loose:
182                                 # Is there another format we can use?
183                                 accepted_mts = dict([(obj.mime_type, obj) for obj in registry.values()])
184                                 if mimeparse:
185                                         mt = mimeparse.best_match(accepted_mts.keys(), accept)
186                                         if mt:
187                                                 feed_type = accepted_mts[mt]
188                                 else:
189                                         for mt in accepted_mts:
190                                                 if mt in accept or "%s/*" % mt.split("/")[0] in accept:
191                                                         feed_type = accepted_mts[mt]
192                                                         break
193                         if not feed_type:
194                                 raise HttpNotAcceptable
195                 return feed_type
196         
197         def get_feed(self, obj, request, reverse_name, feed_type=None, *args, **kwargs):
198                 """
199                 Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object.
200                 
201                 :param obj: The object for which the feed should be generated.
202                 :param request: The current request.
203                 :param reverse_name: The name which can be used to reverse the URL of the page corresponding to this feed.
204                 :param feed_type: The slug used to register the feed class that will be instantiated and returned.
205                 
206                 :returns: An instance of the feed class registered as ``feed_type``, falling back to :attr:`feed_type` if ``feed_type`` is ``None``.
207                 
208                 """
209                 try:
210                         current_site = Site.objects.get_current()
211                 except Site.DoesNotExist:
212                         current_site = RequestSite(request)
213                 
214                 feed_type = self.get_feed_type(request, feed_type)
215                 node = request.node
216                 link = node.construct_url(self.reverse(reverse_name, args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure())
217                 
218                 feed = feed_type(
219                         title = self.__get_dynamic_attr('title', obj),
220                         subtitle = self.__get_dynamic_attr('subtitle', obj),
221                         link = link,
222                         description = self.__get_dynamic_attr('description', obj),
223                         language = settings.LANGUAGE_CODE.decode(),
224                         feed_url = add_domain(
225                                 current_site.domain,
226                                 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()),
227                                 request.is_secure()
228                         ),
229                         author_name = self.__get_dynamic_attr('author_name', obj),
230                         author_link = self.__get_dynamic_attr('author_link', obj),
231                         author_email = self.__get_dynamic_attr('author_email', obj),
232                         categories = self.__get_dynamic_attr('categories', obj),
233                         feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
234                         feed_guid = self.__get_dynamic_attr('feed_guid', obj),
235                         ttl = self.__get_dynamic_attr('ttl', obj),
236                         **self.feed_extra_kwargs(obj)
237                 )
238                 return feed
239         
240         def populate_feed(self, feed, items, request):
241                 """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``."""
242                 if self.item_title_template:
243                         title_template = DjangoTemplate(self.item_title_template.code)
244                 else:
245                         title_template = None
246                 if self.item_description_template:
247                         description_template = DjangoTemplate(self.item_description_template.code)
248                 else:
249                         description_template = None
250                 
251                 node = request.node
252                 try:
253                         current_site = Site.objects.get_current()
254                 except Site.DoesNotExist:
255                         current_site = RequestSite(request)
256                 
257                 if self.feed_length is not None:
258                         items = items[:self.feed_length]
259                 
260                 for item in items:
261                         if title_template is not None:
262                                 title = title_template.render(RequestContext(request, {'obj': item}))
263                         else:
264                                 title = self.__get_dynamic_attr('item_title', item)
265                         if description_template is not None:
266                                 description = description_template.render(RequestContext(request, {'obj': item}))
267                         else:
268                                 description = self.__get_dynamic_attr('item_description', item)
269                         
270                         link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
271                         
272                         enc = None
273                         enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
274                         if enc_url:
275                                 enc = feedgenerator.Enclosure(
276                                         url = smart_unicode(add_domain(
277                                                         current_site.domain,
278                                                         enc_url,
279                                                         request.is_secure()
280                                         )),
281                                         length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
282                                         mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
283                                 )
284                         author_name = self.__get_dynamic_attr('item_author_name', item)
285                         if author_name is not None:
286                                 author_email = self.__get_dynamic_attr('item_author_email', item)
287                                 author_link = self.__get_dynamic_attr('item_author_link', item)
288                         else:
289                                 author_email = author_link = None
290                         
291                         pubdate = self.__get_dynamic_attr('item_pubdate', item)
292                         if pubdate and not pubdate.tzinfo:
293                                 ltz = tzinfo.LocalTimezone(pubdate)
294                                 pubdate = pubdate.replace(tzinfo=ltz)
295                         
296                         feed.add_item(
297                                 title = title,
298                                 link = link,
299                                 description = description,
300                                 unique_id = self.__get_dynamic_attr('item_guid', item, link),
301                                 enclosure = enc,
302                                 pubdate = pubdate,
303                                 author_name = author_name,
304                                 author_email = author_email,
305                                 author_link = author_link,
306                                 categories = self.__get_dynamic_attr('item_categories', item),
307                                 item_copyright = self.__get_dynamic_attr('item_copyright', item),
308                                 **self.item_extra_kwargs(item)
309                         )
310         
311         def __get_dynamic_attr(self, attname, obj, default=None):
312                 try:
313                         attr = getattr(self, attname)
314                 except AttributeError:
315                         return default
316                 if callable(attr):
317                         # Check func_code.co_argcount rather than try/excepting the
318                         # function and catching the TypeError, because something inside
319                         # the function may raise the TypeError. This technique is more
320                         # accurate.
321                         if hasattr(attr, 'func_code'):
322                                 argcount = attr.func_code.co_argcount
323                         else:
324                                 argcount = attr.__call__.func_code.co_argcount
325                         if argcount == 2: # one argument is 'self'
326                                 return attr(obj)
327                         else:
328                                 return attr()
329                 return attr
330         
331         def feed_extra_kwargs(self, obj):
332                 """Returns an extra keyword arguments dictionary that is used when initializing the feed generator."""
333                 return {}
334         
335         def item_extra_kwargs(self, item):
336                 """Returns an extra keyword arguments dictionary that is used with the `add_item` call of the feed generator."""
337                 return {}
338         
339         def item_title(self, item):
340                 return escape(force_unicode(item))
341         
342         def item_description(self, item):
343                 return force_unicode(item)
344         
345         class Meta:
346                 abstract=True