mirror of https://github.com/django/django.git
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.
This commit is contained in:
parent
71ebcb85b9
commit
aac2a2d2ae
|
@ -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,
|
||||
|
|
|
@ -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']:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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'),
|
||||
)
|
||||
|
|
|
@ -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()),
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue