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