Commit aac2a2d2 authored by Unai Zalakain's avatar Unai Zalakain Committed by Tim Graham
Browse files

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.
parent 71ebcb85
Loading
Loading
Loading
Loading
+13 −9
Original line number Diff line number Diff line
@@ -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,
+37 −15
Original line number Diff line number Diff line
@@ -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']:
+3 −0
Original line number Diff line number Diff line
@@ -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
+42 −6
Original line number Diff line number Diff line
@@ -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.

+9 −2
Original line number Diff line number Diff line
@@ -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()

Loading