summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorUnai Zalakain <unai@gisa-elkartea.org>2015-08-21 11:50:43 +0200
committerTim Graham <timograham@gmail.com>2015-09-18 18:31:58 -0400
commitaac2a2d2ae2486342058db0c72ed7ba2c7c8eb1e (patch)
tree9d36cf0b889eceb832aee1ffbeea0a826a557b22
parent71ebcb85b931f43865df5b322b2cf06d3da23f69 (diff)
Fixed #13110 -- Added support for multiple enclosures in Atom feeds.
The ``item_enclosures`` hook returns a list of ``Enclosure`` objects which is then used by the feed builder. If the feed is a RSS feed, an exception is raised as RSS feeds don't allow multiple enclosures per feed item. The ``item_enclosures`` hook defaults to an empty list or, if the ``item_enclosure_url`` hook is defined, to a list with a single ``Enclosure`` built from the ``item_enclosure_url``, ``item_enclosure_length``, and ``item_enclosure_mime_type`` hooks.
-rw-r--r--django/contrib/syndication/views.py22
-rw-r--r--django/utils/feedgenerator.py52
-rw-r--r--docs/internals/deprecation.txt3
-rw-r--r--docs/ref/contrib/syndication.txt48
-rw-r--r--docs/ref/utils.txt11
-rw-r--r--docs/releases/1.9.txt8
-rw-r--r--tests/syndication_tests/feeds.py50
-rw-r--r--tests/syndication_tests/tests.py57
-rw-r--r--tests/syndication_tests/urls.py4
9 files changed, 215 insertions, 40 deletions
diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py
index 7bcc951611..2e5e5f3ef9 100644
--- a/django/contrib/syndication/views.py
+++ b/django/contrib/syndication/views.py
@@ -64,6 +64,17 @@ class Feed(object):
'item_link() method in your Feed class.' % item.__class__.__name__
)
+ def item_enclosures(self, item):
+ enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
+ if enc_url:
+ enc = feedgenerator.Enclosure(
+ url=smart_text(enc_url),
+ length=smart_text(self.__get_dynamic_attr('item_enclosure_length', item)),
+ mime_type=smart_text(self.__get_dynamic_attr('item_enclosure_mime_type', item)),
+ )
+ return [enc]
+ return []
+
def __get_dynamic_attr(self, attname, obj, default=None):
try:
attr = getattr(self, attname)
@@ -171,14 +182,7 @@ class Feed(object):
self.__get_dynamic_attr('item_link', item),
request.is_secure(),
)
- enc = None
- enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
- if enc_url:
- enc = feedgenerator.Enclosure(
- url=smart_text(enc_url),
- length=smart_text(self.__get_dynamic_attr('item_enclosure_length', item)),
- mime_type=smart_text(self.__get_dynamic_attr('item_enclosure_mime_type', item))
- )
+ enclosures = self.__get_dynamic_attr('item_enclosures', item)
author_name = self.__get_dynamic_attr('item_author_name', item)
if author_name is not None:
author_email = self.__get_dynamic_attr('item_author_email', item)
@@ -203,7 +207,7 @@ class Feed(object):
unique_id=self.__get_dynamic_attr('item_guid', item, link),
unique_id_is_permalink=self.__get_dynamic_attr(
'item_guid_is_permalink', item),
- enclosure=enc,
+ enclosures=enclosures,
pubdate=pubdate,
updateddate=updateddate,
author_name=author_name,
diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py
index 9b1421160b..c67fedb277 100644
--- a/django/utils/feedgenerator.py
+++ b/django/utils/feedgenerator.py
@@ -118,11 +118,13 @@ class SyndicationFeed(object):
def add_item(self, title, link, description, author_email=None,
author_name=None, author_link=None, pubdate=None, comments=None,
unique_id=None, unique_id_is_permalink=None, enclosure=None,
- categories=(), item_copyright=None, ttl=None, updateddate=None, **kwargs):
+ categories=(), item_copyright=None, ttl=None, updateddate=None,
+ enclosures=None, **kwargs):
"""
Adds an item to the feed. All args are expected to be Python Unicode
objects except pubdate and updateddate, which are datetime.datetime
- objects, and enclosure, which is an instance of the Enclosure class.
+ objects, and enclosures, which is an iterable of instances of the
+ Enclosure class.
"""
to_unicode = lambda s: force_text(s, strings_only=True)
if categories:
@@ -130,6 +132,16 @@ class SyndicationFeed(object):
if ttl is not None:
# Force ints to unicode
ttl = force_text(ttl)
+ if enclosure is None:
+ enclosures = [] if enclosures is None else enclosures
+ else:
+ warnings.warn(
+ "The enclosure keyword argument is deprecated, "
+ "use enclosures instead.",
+ RemovedInDjango20Warning,
+ stacklevel=2,
+ )
+ enclosures = [enclosure]
item = {
'title': to_unicode(title),
'link': iri_to_uri(link),
@@ -142,7 +154,7 @@ class SyndicationFeed(object):
'comments': to_unicode(comments),
'unique_id': to_unicode(unique_id),
'unique_id_is_permalink': unique_id_is_permalink,
- 'enclosure': enclosure,
+ 'enclosures': enclosures,
'categories': categories or (),
'item_copyright': to_unicode(item_copyright),
'ttl': ttl,
@@ -317,10 +329,19 @@ class Rss201rev2Feed(RssFeed):
handler.addQuickElement("ttl", item['ttl'])
# Enclosure.
- if item['enclosure'] is not None:
- handler.addQuickElement("enclosure", '',
- {"url": item['enclosure'].url, "length": item['enclosure'].length,
- "type": item['enclosure'].mime_type})
+ if item['enclosures']:
+ enclosures = list(item['enclosures'])
+ if len(enclosures) > 1:
+ raise ValueError(
+ "RSS feed items may only have one enclosure, see "
+ "http://www.rssboard.org/rss-profile#element-channel-item-enclosure"
+ )
+ enclosure = enclosures[0]
+ handler.addQuickElement('enclosure', '', {
+ 'url': enclosure.url,
+ 'length': enclosure.length,
+ 'type': enclosure.mime_type,
+ })
# Categories.
for cat in item['categories']:
@@ -328,7 +349,7 @@ class Rss201rev2Feed(RssFeed):
class Atom1Feed(SyndicationFeed):
- # Spec: http://atompub.org/2005/07/11/draft-ietf-atompub-format-10.html
+ # Spec: https://tools.ietf.org/html/rfc4287
content_type = 'application/atom+xml; charset=utf-8'
ns = "http://www.w3.org/2005/Atom"
@@ -405,13 +426,14 @@ class Atom1Feed(SyndicationFeed):
if item['description'] is not None:
handler.addQuickElement("summary", item['description'], {"type": "html"})
- # Enclosure.
- if item['enclosure'] is not None:
- handler.addQuickElement("link", '',
- {"rel": "enclosure",
- "href": item['enclosure'].url,
- "length": item['enclosure'].length,
- "type": item['enclosure'].mime_type})
+ # Enclosures.
+ for enclosure in item.get('enclosures') or []:
+ handler.addQuickElement('link', '', {
+ 'rel': 'enclosure',
+ 'href': enclosure.url,
+ 'length': enclosure.length,
+ 'type': enclosure.mime_type,
+ })
# Categories.
for cat in item['categories']:
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index f37ec25179..fb3dc859c3 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -94,6 +94,9 @@ details on these changes.
* The ``callable_obj`` keyword argument to
``SimpleTestCase.assertRaisesMessage()`` will be removed.
+* The ``enclosure`` keyword argument to ``SyndicationFeed.add_item()`` will be
+ removed.
+
.. _deprecation-removed-in-1.10:
1.10
diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt
index 4d0e3da100..0a91a16e40 100644
--- a/docs/ref/contrib/syndication.txt
+++ b/docs/ref/contrib/syndication.txt
@@ -298,10 +298,16 @@ Enclosures
----------
To specify enclosures, such as those used in creating podcast feeds, use the
-``item_enclosure_url``, ``item_enclosure_length`` and
+``item_enclosures`` hook or, alternatively and if you only have a single
+enclosure per item, the ``item_enclosure_url``, ``item_enclosure_length``, and
``item_enclosure_mime_type`` hooks. See the ``ExampleFeed`` class below for
usage examples.
+.. versionchanged:: 1.9
+
+ Support for multiple enclosures per feed item was added through the
+ ``item_enclosures`` hook.
+
Language
--------
@@ -742,8 +748,28 @@ This example illustrates all possible attributes and methods for a
item_author_link = 'http://www.example.com/' # Hard-coded author URL.
+ # ITEM ENCLOSURES -- One of the following three is optional. The
+ # framework looks for them in this order. If one of them is defined,
+ # ``item_enclosure_url``, ``item_enclosure_length``, and
+ # ``item_enclosure_mime_type`` will have no effect.
+
+ def item_enclosures(self, item):
+ """
+ Takes an item, as returned by items(), and returns a list of
+ ``django.utils.feedgenerator.Enclosure`` objects.
+ """
+
+ def item_enclosure_url(self):
+ """
+ Returns the ``django.utils.feedgenerator.Enclosure`` list for every
+ item in the feed.
+ """
+
+ item_enclosures = [] # Hard-coded enclosure list
+
# ITEM ENCLOSURE URL -- One of these three is required if you're
- # publishing enclosures. The framework looks for them in this order.
+ # publishing enclosures and you're not using ``item_enclosures``. The
+ # framework looks for them in this order.
def item_enclosure_url(self, item):
"""
@@ -759,9 +785,10 @@ This example illustrates all possible attributes and methods for a
item_enclosure_url = "/foo/bar.mp3" # Hard-coded enclosure link.
# ITEM ENCLOSURE LENGTH -- One of these three is required if you're
- # publishing enclosures. The framework looks for them in this order.
- # In each case, the returned value should be either an integer, or a
- # string representation of the integer, in bytes.
+ # publishing enclosures and you're not using ``item_enclosures``. The
+ # framework looks for them in this order. In each case, the returned
+ # value should be either an integer, or a string representation of the
+ # integer, in bytes.
def item_enclosure_length(self, item):
"""
@@ -777,7 +804,8 @@ This example illustrates all possible attributes and methods for a
item_enclosure_length = 32000 # Hard-coded enclosure length.
# ITEM ENCLOSURE MIME TYPE -- One of these three is required if you're
- # publishing enclosures. The framework looks for them in this order.
+ # publishing enclosures and you're not using ``item_enclosures``. The
+ # framework looks for them in this order.
def item_enclosure_mime_type(self, item):
"""
@@ -941,6 +969,7 @@ They share this interface:
* ``comments``
* ``unique_id``
* ``enclosure``
+ * ``enclosures``
* ``categories``
* ``item_copyright``
* ``ttl``
@@ -954,8 +983,15 @@ They share this interface:
* ``updateddate`` should be a Python :class:`~datetime.datetime` object.
* ``enclosure`` should be an instance of
:class:`django.utils.feedgenerator.Enclosure`.
+ * ``enclosures`` should be a list of
+ :class:`django.utils.feedgenerator.Enclosure` instances.
* ``categories`` should be a sequence of Unicode objects.
+ .. deprecated:: 1.9
+
+ The ``enclosure`` keyword argument is deprecated in favor of the
+ ``enclosures`` keyword argument.
+
:meth:`.SyndicationFeed.write`
Outputs the feed in the given encoding to outfile, which is a file-like object.
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index c416c82db1..c1df458872 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -351,11 +351,18 @@ SyndicationFeed
All parameters should be Unicode objects, except ``categories``, which
should be a sequence of Unicode objects.
- .. method:: add_item(title, link, description, author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, unique_id=None, enclosure=None, categories=(), item_copyright=None, ttl=None, updateddate=None, **kwargs)
+ .. method:: add_item(title, link, description, author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, unique_id=None, enclosure=None, categories=(), item_copyright=None, ttl=None, updateddate=None, enclosures=None, **kwargs)
Adds an item to the feed. All args are expected to be Python ``unicode``
objects except ``pubdate`` and ``updateddate``, which are ``datetime.datetime``
- objects, and ``enclosure``, which is an instance of the ``Enclosure`` class.
+ objects, ``enclosure``, which is an ``Enclosure`` instance, and
+ ``enclosures``, which is a list of ``Enclosure`` instances.
+
+ .. deprecated:: 1.9
+
+ The ``enclosure`` keyword argument is deprecated in favor of the
+ new ``enclosures`` keyword argument which accepts a list of
+ ``Enclosure`` objects.
.. method:: num_items()
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index 30f812c4d9..411d8bc255 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -303,7 +303,9 @@ Minor features
:mod:`django.contrib.syndication`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-* ...
+* Support for multiple enclosures per feed item has been added. If multiple
+ enclosures are defined on a RSS feed, an exception is raised as RSS feeds,
+ unlike Atom feeds, do not support multiple enclosures per feed item.
Cache
^^^^^
@@ -1265,6 +1267,10 @@ Miscellaneous
:func:`~django.utils.safestring.mark_safe` when constructing the method's
return value instead.
+* The ``enclosure`` keyword argument to ``SyndicationFeed.add_item()`` is
+ deprecated. Use the new ``enclosures`` argument which accepts a list of
+ ``Enclosure`` objects instead of a single one.
+
.. removed-features-1.9:
Features removed in 1.9
diff --git a/tests/syndication_tests/feeds.py b/tests/syndication_tests/feeds.py
index 6408838b68..ce938b8356 100644
--- a/tests/syndication_tests/feeds.py
+++ b/tests/syndication_tests/feeds.py
@@ -88,8 +88,29 @@ class ArticlesFeed(TestRss2Feed):
return Article.objects.all()
-class TestEnclosureFeed(TestRss2Feed):
- pass
+class TestSingleEnclosureRSSFeed(TestRss2Feed):
+ """
+ A feed to test that RSS feeds work with a single enclosure.
+ """
+ def item_enclosure_url(self, item):
+ return 'http://example.com'
+
+ def item_enclosure_size(self, item):
+ return 0
+
+ def item_mime_type(self, item):
+ return 'image/png'
+
+
+class TestMultipleEnclosureRSSFeed(TestRss2Feed):
+ """
+ A feed to test that RSS feeds raise an exception with multiple enclosures.
+ """
+ def item_enclosures(self, item):
+ return [
+ feedgenerator.Enclosure('http://example.com/hello.png', 0, 'image/png'),
+ feedgenerator.Enclosure('http://example.com/goodbye.png', 0, 'image/png'),
+ ]
class TemplateFeed(TestRss2Feed):
@@ -165,3 +186,28 @@ class MyCustomAtom1Feed(feedgenerator.Atom1Feed):
class TestCustomFeed(TestAtomFeed):
feed_type = MyCustomAtom1Feed
+
+
+class TestSingleEnclosureAtomFeed(TestAtomFeed):
+ """
+ A feed to test that Atom feeds work with a single enclosure.
+ """
+ def item_enclosure_url(self, item):
+ return 'http://example.com'
+
+ def item_enclosure_size(self, item):
+ return 0
+
+ def item_mime_type(self, item):
+ return 'image/png'
+
+
+class TestMultipleEnclosureAtomFeed(TestAtomFeed):
+ """
+ A feed to test that Atom feeds work with multiple enclosures.
+ """
+ def item_enclosures(self, item):
+ return [
+ feedgenerator.Enclosure('http://example.com/hello.png', 0, 'image/png'),
+ feedgenerator.Enclosure('http://example.com/goodbye.png', 0, 'image/png'),
+ ]
diff --git a/tests/syndication_tests/tests.py b/tests/syndication_tests/tests.py
index 78c8708f9e..5a7cfbb057 100644
--- a/tests/syndication_tests/tests.py
+++ b/tests/syndication_tests/tests.py
@@ -9,7 +9,10 @@ from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase, override_settings
from django.test.utils import requires_tz_support
from django.utils import timezone
-from django.utils.feedgenerator import rfc2822_date, rfc3339_date
+from django.utils.deprecation import RemovedInDjango20Warning
+from django.utils.feedgenerator import (
+ Enclosure, SyndicationFeed, rfc2822_date, rfc3339_date,
+)
from .models import Article, Entry
@@ -63,10 +66,6 @@ class FeedTestCase(TestCase):
set(expected)
)
-######################################
-# Feed view
-######################################
-
@override_settings(ROOT_URLCONF='syndication_tests.urls')
class SyndicationFeedTest(FeedTestCase):
@@ -186,6 +185,22 @@ class SyndicationFeedTest(FeedTestCase):
item.getElementsByTagName('guid')[0].attributes.get(
'isPermaLink').value, "true")
+ def test_rss2_single_enclosure(self):
+ response = self.client.get('/syndication/rss2/single-enclosure/')
+ doc = minidom.parseString(response.content)
+ chan = doc.getElementsByTagName('rss')[0].getElementsByTagName('channel')[0]
+ items = chan.getElementsByTagName('item')
+ for item in items:
+ enclosures = item.getElementsByTagName('enclosure')
+ self.assertEqual(len(enclosures), 1)
+
+ def test_rss2_multiple_enclosures(self):
+ with self.assertRaisesMessage(ValueError, (
+ "RSS feed items may only have one enclosure, see "
+ "http://www.rssboard.org/rss-profile#element-channel-item-enclosure"
+ )):
+ self.client.get('/syndication/rss2/multiple-enclosure/')
+
def test_rss091_feed(self):
"""
Test the structure and content of feeds generated by RssUserland091Feed.
@@ -284,6 +299,24 @@ class SyndicationFeedTest(FeedTestCase):
self.assertNotEqual(published, updated)
+ def test_atom_single_enclosure(self):
+ response = self.client.get('/syndication/rss2/single-enclosure/')
+ feed = minidom.parseString(response.content).firstChild
+ items = feed.getElementsByTagName('entry')
+ for item in items:
+ links = item.getElementsByTagName('link')
+ links = [link for link in links if link.getAttribute('rel') == 'enclosure']
+ self.assertEqual(len(links), 1)
+
+ def test_atom_multiple_enclosures(self):
+ response = self.client.get('/syndication/rss2/single-enclosure/')
+ feed = minidom.parseString(response.content).firstChild
+ items = feed.getElementsByTagName('entry')
+ for item in items:
+ links = item.getElementsByTagName('link')
+ links = [link for link in links if link.getAttribute('rel') == 'enclosure']
+ self.assertEqual(len(links), 2)
+
def test_latest_post_date(self):
"""
Test that both the published and updated dates are
@@ -493,3 +526,17 @@ class SyndicationFeedTest(FeedTestCase):
views.add_domain('example.com', '//example.com/foo/?arg=value'),
'http://example.com/foo/?arg=value'
)
+
+
+class FeedgeneratorTestCase(TestCase):
+ def test_add_item_warns_when_enclosure_kwarg_is_used(self):
+ feed = SyndicationFeed(title='Example', link='http://example.com', description='Foo')
+ with self.assertRaisesMessage(RemovedInDjango20Warning, (
+ 'The enclosure keyword argument is deprecated, use enclosures instead.'
+ )):
+ feed.add_item(
+ title='Example Item',
+ link='https://example.com/item',
+ description='bar',
+ enclosure=Enclosure('http://example.com/favicon.ico', 0, 'image/png'),
+ )
diff --git a/tests/syndication_tests/urls.py b/tests/syndication_tests/urls.py
index d3caeae888..09f7e789cd 100644
--- a/tests/syndication_tests/urls.py
+++ b/tests/syndication_tests/urls.py
@@ -19,4 +19,8 @@ urlpatterns = [
url(r'^syndication/articles/$', feeds.ArticlesFeed()),
url(r'^syndication/template/$', feeds.TemplateFeed()),
url(r'^syndication/template_context/$', feeds.TemplateContextFeed()),
+ url(r'^syndication/rss2/single-enclosure/$', feeds.TestSingleEnclosureRSSFeed()),
+ url(r'^syndication/rss2/multiple-enclosure/$', feeds.TestMultipleEnclosureRSSFeed()),
+ url(r'^syndication/atom/single-enclosure/$', feeds.TestSingleEnclosureAtomFeed()),
+ url(r'^syndication/atom/multiple-enclosure/$', feeds.TestMultipleEnclosureAtomFeed()),
]