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