Make it possible to specify custom URLs for feeds.

With this patch, there are new FEED_*_URL configuration options that
allow to specify custom URLs for feeds, which is helpful in case the
feed filename and the actual URL differ a lot -- for example if a feed
is saved to

    blog/feeds/all.atom.xml

but the actual URL from the user PoV is

    http://blog.your.site/feeds/all.atom.xml

This setting currently affects only the generated feed XML. This change
is also fully backwards compatible, so if the FEED_*_URL setting is not
present, the value of FEED_* is used for both file location and URL.
This commit is contained in:
Vladimír Vondruš 2017-10-29 19:58:26 +01:00
commit 343e24a26f
4 changed files with 140 additions and 21 deletions

View file

@ -811,46 +811,95 @@ the ``TAG_FEED_ATOM`` and ``TAG_FEED_RSS`` settings:
.. data:: FEED_ATOM = None, i.e. no Atom feed
Relative URL to output the Atom feed.
The location to save the Atom feed.
.. data:: FEED_ATOM_URL = None
Relative URL of the Atom feed. If not set, ``FEED_ATOM`` is used both for
save location and URL.
.. data:: FEED_RSS = None, i.e. no RSS
Relative URL to output the RSS feed.
The location to save the RSS feed.
.. data:: FEED_RSS_URL = None
Relative URL of the RSS feed. If not set, ``FEED_RSS`` is used both for save
location and URL.
.. data:: FEED_ALL_ATOM = 'feeds/all.atom.xml'
Relative URL to output the all-posts Atom feed: this feed will contain all
The location to save the all-posts Atom feed: this feed will contain all
posts regardless of their language.
.. data:: FEED_ALL_ATOM_URL = None
Relative URL of the all-posts Atom feed. If not set, ``FEED_ALL_ATOM`` is
used both for save location and URL.
.. data:: FEED_ALL_RSS = None, i.e. no all-posts RSS
Relative URL to output the all-posts RSS feed: this feed will contain all
The location to save the the all-posts RSS feed: this feed will contain all
posts regardless of their language.
.. data:: FEED_ALL_RSS_URL = None
Relative URL of the all-posts RSS feed. If not set, ``FEED_ALL_RSS`` is used
both for save location and URL.
.. data:: CATEGORY_FEED_ATOM = 'feeds/%s.atom.xml'
Where to put the category Atom feeds. [2]_
The location to save the category Atom feeds. [2]_
.. data:: CATEGORY_FEED_ATOM_URL = None
Relative URL of the category Atom feeds, including the ``%s`` placeholder.
[2]_ If not set, ``CATEGORY_FEED_ATOM`` is used both for save location and
URL.
.. data:: CATEGORY_FEED_RSS = None, i.e. no RSS
Where to put the category RSS feeds.
The location to save the category RSS feeds, including the ``%s``
placeholder. [2]_
.. data:: CATEGORY_FEED_RSS_URL = None
Relative URL of the category RSS feeds, including the ``%s`` placeholder.
[2]_ If not set, ``CATEGORY_FEED_RSS`` is used both for save location and
URL.
.. data:: AUTHOR_FEED_ATOM = 'feeds/%s.atom.xml'
Where to put the author Atom feeds. [2]_
The location to save the author Atom feeds. [2]_
.. data:: AUTHOR_FEED_ATOM_URL = None
Relative URL of the author Atom feeds, including the ``%s`` placeholder.
[2]_ If not set, ``AUTHOR_FEED_ATOM`` is used both for save location and
URL.
.. data:: AUTHOR_FEED_RSS = 'feeds/%s.rss.xml'
Where to put the author RSS feeds. [2]_
The location to save the author RSS feeds. [2]_
.. data:: AUTHOR_FEED_RSS_URL = None
Relative URL of the author RSS feeds, including the ``%s`` placeholder. [2]_
If not set, ``AUTHOR_FEED_RSS`` is used both for save location and URL.
.. data:: TAG_FEED_ATOM = None, i.e. no tag feed
Relative URL to output the tag Atom feed. It should be defined using a "%s"
match in the tag name.
The location to save the tag Atom feed, including the ``%s`` placeholder.
[2]_
.. data:: TAG_FEED_ATOM_URL = None
Relative URL of the tag Atom feed, including the ``%s`` placeholder. [2]_
.. data:: TAG_FEED_RSS = None, i.e. no RSS tag feed
Relative URL to output the tag RSS feed
Relative URL to output the tag RSS feed, including the ``%s`` placeholder.
If not set, ``TAG_FEED_RSS`` is used both for save location and URL.
.. data:: FEED_MAX_ITEMS
@ -865,7 +914,7 @@ the ``TAG_FEED_ATOM`` and ``TAG_FEED_RSS`` settings:
If you don't want to generate some or any of these feeds, set the above variables to ``None``.
.. [2] %s is the name of the category.
.. [2] ``%s`` is replaced by name of the category / author / tag.
FeedBurner
@ -948,12 +997,24 @@ more information.
.. data:: TRANSLATION_FEED_ATOM = 'feeds/all-%s.atom.xml'
Where to put the Atom feed for translations. [3]_
The location to save the Atom feed for translations. [3]_
.. data:: TRANSLATION_FEED_ATOM_URL = None
Relative URL of the Atom feed for translations, including the ``%s``
placeholder. [3]_ If not set, ``TRANSLATION_FEED_ATOM`` is used both for
save location and URL.
.. data:: TRANSLATION_FEED_RSS = None, i.e. no RSS
Where to put the RSS feed for translations.
.. data:: TRANSLATION_FEED_RSS_URL = None
Relative URL of the RSS feed for translations, including the ``%s``
placeholder. [3]_ If not set, ``TRANSLATION_FEED_RSS`` is used both for save
location and URL.
.. [3] %s is the language

View file

@ -282,11 +282,16 @@ class ArticlesGenerator(CachingGenerator):
if self.settings.get('FEED_ATOM'):
writer.write_feed(self.articles, self.context,
self.settings['FEED_ATOM'])
self.settings['FEED_ATOM'],
self.settings.get('FEED_ATOM_URL',
self.settings['FEED_ATOM']))
if self.settings.get('FEED_RSS'):
writer.write_feed(self.articles, self.context,
self.settings['FEED_RSS'], feed_type='rss')
self.settings['FEED_RSS'],
self.settings.get('FEED_RSS_URL',
self.settings['FEED_RSS']),
feed_type='rss')
if (self.settings.get('FEED_ALL_ATOM') or
self.settings.get('FEED_ALL_RSS')):
@ -297,11 +302,17 @@ class ArticlesGenerator(CachingGenerator):
if self.settings.get('FEED_ALL_ATOM'):
writer.write_feed(all_articles, self.context,
self.settings['FEED_ALL_ATOM'])
self.settings['FEED_ALL_ATOM'],
self.settings.get(
'FEED_ALL_ATOM_URL',
self.settings['FEED_ALL_ATOM']))
if self.settings.get('FEED_ALL_RSS'):
writer.write_feed(all_articles, self.context,
self.settings['FEED_ALL_RSS'],
self.settings.get(
'FEED_ALL_RSS_URL',
self.settings['FEED_ALL_RSS']),
feed_type='rss')
for cat, arts in self.categories:
@ -309,11 +320,19 @@ class ArticlesGenerator(CachingGenerator):
if self.settings.get('CATEGORY_FEED_ATOM'):
writer.write_feed(arts, self.context,
self.settings['CATEGORY_FEED_ATOM']
% cat.slug,
self.settings.get(
'CATEGORY_FEED_ATOM_URL',
self.settings['CATEGORY_FEED_ATOM'])
% cat.slug, feed_title=cat.name)
if self.settings.get('CATEGORY_FEED_RSS'):
writer.write_feed(arts, self.context,
self.settings['CATEGORY_FEED_RSS']
% cat.slug,
self.settings.get(
'CATEGORY_FEED_RSS_URL',
self.settings['CATEGORY_FEED_RSS'])
% cat.slug, feed_title=cat.name,
feed_type='rss')
@ -322,11 +341,19 @@ class ArticlesGenerator(CachingGenerator):
if self.settings.get('AUTHOR_FEED_ATOM'):
writer.write_feed(arts, self.context,
self.settings['AUTHOR_FEED_ATOM']
% auth.slug,
self.settings.get(
'AUTHOR_FEED_ATOM_URL',
self.settings['AUTHOR_FEED_ATOM'])
% auth.slug, feed_title=auth.name)
if self.settings.get('AUTHOR_FEED_RSS'):
writer.write_feed(arts, self.context,
self.settings['AUTHOR_FEED_RSS']
% auth.slug,
self.settings.get(
'AUTHOR_FEED_RSS_URL',
self.settings['AUTHOR_FEED_RSS'])
% auth.slug, feed_title=auth.name,
feed_type='rss')
@ -337,12 +364,20 @@ class ArticlesGenerator(CachingGenerator):
if self.settings.get('TAG_FEED_ATOM'):
writer.write_feed(arts, self.context,
self.settings['TAG_FEED_ATOM']
% tag.slug,
self.settings.get(
'TAG_FEED_ATOM_URL',
self.settings['TAG_FEED_ATOM'])
% tag.slug, feed_title=tag.name)
if self.settings.get('TAG_FEED_RSS'):
writer.write_feed(arts, self.context,
self.settings['TAG_FEED_RSS'] % tag.slug,
feed_title=tag.name, feed_type='rss')
self.settings.get(
'TAG_FEED_RSS_URL',
self.settings['TAG_FEED_RSS'])
% tag.slug, feed_title=tag.name,
feed_type='rss')
if (self.settings.get('TRANSLATION_FEED_ATOM') or
self.settings.get('TRANSLATION_FEED_RSS')):
@ -355,11 +390,17 @@ class ArticlesGenerator(CachingGenerator):
if self.settings.get('TRANSLATION_FEED_ATOM'):
writer.write_feed(
items, self.context,
self.settings['TRANSLATION_FEED_ATOM'] % lang)
self.settings['TRANSLATION_FEED_ATOM'] % lang,
self.settings.get(
'TRANSLATION_FEED_ATOM_URL',
self.settings['TRANSLATION_FEED_ATOM']) % lang)
if self.settings.get('TRANSLATION_FEED_RSS'):
writer.write_feed(
items, self.context,
self.settings['TRANSLATION_FEED_RSS'] % lang,
self.settings.get(
'TRANSLATION_FEED_RSS_URL',
self.settings['TRANSLATION_FEED_RSS']) % lang,
feed_type='rss')
def generate_articles(self, write):

View file

@ -155,6 +155,7 @@ class TestArticlesGenerator(unittest.TestCase):
writer = MagicMock()
generator.generate_feeds(writer)
writer.write_feed.assert_called_with([], settings,
'feeds/all.atom.xml',
'feeds/all.atom.xml')
generator = ArticlesGenerator(
@ -164,6 +165,20 @@ class TestArticlesGenerator(unittest.TestCase):
generator.generate_feeds(writer)
self.assertFalse(writer.write_feed.called)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_generate_feeds_override_url(self):
settings = get_settings()
settings['CACHE_PATH'] = self.temp_cache
settings['FEED_ALL_ATOM_URL'] = 'feeds/atom/all/'
generator = ArticlesGenerator(
context=settings, settings=settings,
path=None, theme=settings['THEME'], output_path=None)
writer = MagicMock()
generator.generate_feeds(writer)
writer.write_feed.assert_called_with([], settings,
'feeds/all.atom.xml',
'feeds/atom/all/')
def test_generate_context(self):
articles_expected = [
['Article title', 'published', 'Default', 'article'],

View file

@ -89,8 +89,8 @@ class Writer(object):
self._written_files.add(filename)
return open(filename, 'w', encoding=encoding)
def write_feed(self, elements, context, path=None, feed_type='atom',
override_output=False, feed_title=None):
def write_feed(self, elements, context, path=None, url=None,
feed_type='atom', override_output=False, feed_title=None):
"""Generate a feed with the list of articles provided
Return the feed. If no path or output_path is specified, just
@ -99,6 +99,8 @@ class Writer(object):
:param elements: the articles to put on the feed.
:param context: the context to get the feed metadata.
:param path: the path to output.
:param url: the publicly visible feed URL; if None, path is used
instead
:param feed_type: the feed type to use (atom or rss)
:param override_output: boolean telling if we can override previous
output with the same name (and if next files written with the same
@ -112,7 +114,7 @@ class Writer(object):
'SITEURL', path_to_url(get_relative_path(path)))
self.feed_domain = context.get('FEED_DOMAIN')
self.feed_url = '{}/{}'.format(self.feed_domain, path)
self.feed_url = '{}/{}'.format(self.feed_domain, url if url else path)
feed = self._create_new_feed(feed_type, feed_title, context)