1
0
Fork 0
forked from github/pelican
pelican-theme/pelican/writers.py
Vladimír Vondruš 343e24a26f 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.
2017-10-29 21:35:23 +01:00

235 lines
9.3 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals, with_statement
import logging
import os
from feedgenerator import Atom1Feed, Rss201rev2Feed, get_tag_uri
from jinja2 import Markup
import six
from pelican import signals
from pelican.paginator import Paginator
from pelican.utils import (get_relative_path, is_selected_for_writing,
path_to_url, sanitised_join, set_date_tzinfo)
if not six.PY3:
from codecs import open
logger = logging.getLogger(__name__)
class Writer(object):
def __init__(self, output_path, settings=None):
self.output_path = output_path
self.reminder = dict()
self.settings = settings or {}
self._written_files = set()
self._overridden_files = set()
def _create_new_feed(self, feed_type, feed_title, context):
feed_class = Rss201rev2Feed if feed_type == 'rss' else Atom1Feed
if feed_title:
feed_title = context['SITENAME'] + ' - ' + feed_title
else:
feed_title = context['SITENAME']
feed = feed_class(
title=Markup(feed_title).striptags(),
link=(self.site_url + '/'),
feed_url=self.feed_url,
description=context.get('SITESUBTITLE', ''))
return feed
def _add_item_to_the_feed(self, feed, item):
title = Markup(item.title).striptags()
link = '%s/%s' % (self.site_url, item.url)
is_rss = isinstance(feed, Rss201rev2Feed)
if not is_rss or self.settings.get('RSS_FEED_SUMMARY_ONLY'):
description = item.summary
else:
description = item.get_content(self.site_url)
feed.add_item(
title=title,
link=link,
unique_id=get_tag_uri(link, item.date),
description=description,
content=item.get_content(self.site_url),
categories=item.tags if hasattr(item, 'tags') else None,
author_name=getattr(item, 'author', ''),
pubdate=set_date_tzinfo(
item.date, self.settings.get('TIMEZONE', None)),
updateddate=set_date_tzinfo(
item.modified, self.settings.get('TIMEZONE', None)
) if hasattr(item, 'modified') else None)
def _open_w(self, filename, encoding, override=False):
"""Open a file to write some content to it.
Exit if we have already written to that file, unless one (and no more
than one) of the writes has the override parameter set to True.
"""
if filename in self._overridden_files:
if override:
raise RuntimeError('File %s is set to be overridden twice'
% filename)
else:
logger.info('Skipping %s', filename)
filename = os.devnull
elif filename in self._written_files:
if override:
logger.info('Overwriting %s', filename)
else:
raise RuntimeError('File %s is to be overwritten' % filename)
if override:
self._overridden_files.add(filename)
self._written_files.add(filename)
return open(filename, 'w', encoding=encoding)
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
return the feed 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
name should be skipped to keep that one)
:param feed_title: the title of the feed.o
"""
if not is_selected_for_writing(self.settings, path):
return
self.site_url = context.get(
'SITEURL', path_to_url(get_relative_path(path)))
self.feed_domain = context.get('FEED_DOMAIN')
self.feed_url = '{}/{}'.format(self.feed_domain, url if url else path)
feed = self._create_new_feed(feed_type, feed_title, context)
max_items = len(elements)
if self.settings['FEED_MAX_ITEMS']:
max_items = min(self.settings['FEED_MAX_ITEMS'], max_items)
for i in range(max_items):
self._add_item_to_the_feed(feed, elements[i])
signals.feed_generated.send(context, feed=feed)
if path:
complete_path = sanitised_join(self.output_path, path)
try:
os.makedirs(os.path.dirname(complete_path))
except Exception:
pass
encoding = 'utf-8' if six.PY3 else None
with self._open_w(complete_path, encoding, override_output) as fp:
feed.write(fp, 'utf-8')
logger.info('Writing %s', complete_path)
signals.feed_written.send(
complete_path, context=context, feed=feed)
return feed
def write_file(self, name, template, context, relative_urls=False,
paginated=None, override_output=False, **kwargs):
"""Render the template and write the file.
:param name: name of the file to output
:param template: template to use to generate the content
:param context: dict to pass to the templates.
:param relative_urls: use relative urls or absolutes ones
:param paginated: dict of article list to paginate - must have the
same length (same list in different orders)
:param override_output: boolean telling if we can override previous
output with the same name (and if next files written with the same
name should be skipped to keep that one)
:param **kwargs: additional variables to pass to the templates
"""
if name is False or \
name == "" or \
not is_selected_for_writing(self.settings,
os.path.join(self.output_path, name)):
return
elif not name:
# other stuff, just return for now
return
def _write_file(template, localcontext, output_path, name, override):
"""Render the template write the file."""
# set localsiteurl for context so that Contents can adjust links
if localcontext['localsiteurl']:
context['localsiteurl'] = localcontext['localsiteurl']
output = template.render(localcontext)
path = sanitised_join(output_path, name)
try:
os.makedirs(os.path.dirname(path))
except Exception:
pass
with self._open_w(path, 'utf-8', override=override) as f:
f.write(output)
logger.info('Writing %s', path)
# Send a signal to say we're writing a file with some specific
# local context.
signals.content_written.send(path, context=localcontext)
def _get_localcontext(context, name, kwargs, relative_urls):
localcontext = context.copy()
localcontext['localsiteurl'] = localcontext.get(
'localsiteurl', None)
if relative_urls:
relative_url = path_to_url(get_relative_path(name))
localcontext['SITEURL'] = relative_url
localcontext['localsiteurl'] = relative_url
localcontext['output_file'] = name
localcontext.update(kwargs)
return localcontext
# pagination
if paginated:
# pagination needed, init paginators
paginators = {key: Paginator(name, val, self.settings)
for key, val in paginated.items()}
# generated pages, and write
for page_num in range(list(paginators.values())[0].num_pages):
paginated_kwargs = kwargs.copy()
for key in paginators.keys():
paginator = paginators[key]
previous_page = paginator.page(page_num) \
if page_num > 0 else None
page = paginator.page(page_num + 1)
next_page = paginator.page(page_num + 2) \
if page_num + 1 < paginator.num_pages else None
paginated_kwargs.update(
{'%s_paginator' % key: paginator,
'%s_page' % key: page,
'%s_previous_page' % key: previous_page,
'%s_next_page' % key: next_page})
localcontext = _get_localcontext(
context, page.save_as, paginated_kwargs, relative_urls)
_write_file(template, localcontext, self.output_path,
page.save_as, override_output)
else:
# no pagination
localcontext = _get_localcontext(
context, name, kwargs, relative_urls)
_write_file(template, localcontext, self.output_path, name,
override_output)