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 .. 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 .. 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' .. 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. 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 .. 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. 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' .. 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 .. 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' .. 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' .. 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 .. 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" The location to save the tag Atom feed, including the ``%s`` placeholder.
match in the tag name. [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 .. 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 .. 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``. 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 FeedBurner
@ -948,12 +997,24 @@ more information.
.. data:: TRANSLATION_FEED_ATOM = 'feeds/all-%s.atom.xml' .. 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 .. data:: TRANSLATION_FEED_RSS = None, i.e. no RSS
Where to put the RSS feed for translations. 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 .. [3] %s is the language

View file

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

View file

@ -155,6 +155,7 @@ class TestArticlesGenerator(unittest.TestCase):
writer = MagicMock() writer = MagicMock()
generator.generate_feeds(writer) generator.generate_feeds(writer)
writer.write_feed.assert_called_with([], settings, writer.write_feed.assert_called_with([], settings,
'feeds/all.atom.xml',
'feeds/all.atom.xml') 'feeds/all.atom.xml')
generator = ArticlesGenerator( generator = ArticlesGenerator(
@ -164,6 +165,20 @@ class TestArticlesGenerator(unittest.TestCase):
generator.generate_feeds(writer) generator.generate_feeds(writer)
self.assertFalse(writer.write_feed.called) 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): def test_generate_context(self):
articles_expected = [ articles_expected = [
['Article title', 'published', 'Default', 'article'], ['Article title', 'published', 'Default', 'article'],

View file

@ -89,8 +89,8 @@ class Writer(object):
self._written_files.add(filename) self._written_files.add(filename)
return open(filename, 'w', encoding=encoding) return open(filename, 'w', encoding=encoding)
def write_feed(self, elements, context, path=None, feed_type='atom', def write_feed(self, elements, context, path=None, url=None,
override_output=False, feed_title=None): feed_type='atom', override_output=False, feed_title=None):
"""Generate a feed with the list of articles provided """Generate a feed with the list of articles provided
Return the feed. If no path or output_path is specified, just 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 elements: the articles to put on the feed.
:param context: the context to get the feed metadata. :param context: the context to get the feed metadata.
:param path: the path to output. :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 feed_type: the feed type to use (atom or rss)
:param override_output: boolean telling if we can override previous :param override_output: boolean telling if we can override previous
output with the same name (and if next files written with the same 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))) 'SITEURL', path_to_url(get_relative_path(path)))
self.feed_domain = context.get('FEED_DOMAIN') 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) feed = self._create_new_feed(feed_type, feed_title, context)