static site generator part 8 - port the feed generator
I think this is everything? now just for more functional tests Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
This commit is contained in:
parent
4e96199920
commit
c8c39befb3
@ -62,10 +62,10 @@ class Config(object):
|
|||||||
DOMAIN_NAME = 'example.org'
|
DOMAIN_NAME = 'example.org'
|
||||||
TITLE_SUFFIX = DOMAIN_NAME
|
TITLE_SUFFIX = DOMAIN_NAME
|
||||||
BASE_HOST = 'http://' + DOMAIN_NAME
|
BASE_HOST = 'http://' + DOMAIN_NAME
|
||||||
CONTACT_EMAIL = 'admin@example.com'
|
CONTACT_EMAIL = 'admin@example.org'
|
||||||
|
|
||||||
# feed settings
|
# feed settings
|
||||||
AUTHOR = {'name': 'Test Name', 'email': 'admin@example.com'}
|
AUTHOR = {'name': 'Test Name', 'email': 'admin@example.org'}
|
||||||
|
|
||||||
FAVICON = '/static/img/favicon.png'
|
FAVICON = '/static/img/favicon.png'
|
||||||
|
|
||||||
|
@ -14,60 +14,72 @@ import os
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from feedgen.feed import FeedGenerator
|
from feedgen.feed import FeedGenerator
|
||||||
from flask import Blueprint, Response, abort
|
|
||||||
from flask import current_app as app
|
|
||||||
|
|
||||||
from incorporealcms.lib import instance_resource_path_to_request_path, parse_md
|
from incorporealcms.config import Config
|
||||||
|
from incorporealcms.markdown import instance_resource_path_to_request_path, parse_md
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
bp = Blueprint('feed', __name__, url_prefix='/feed')
|
|
||||||
|
|
||||||
|
def generate_feed(feed_type: str, instance_dir: str, dest_dir: str) -> None:
|
||||||
|
"""Generate the Atom or RSS feed as requested.
|
||||||
|
|
||||||
@bp.route('/<feed_type>')
|
Args:
|
||||||
def serve_feed(feed_type):
|
feed_type: 'atom' or 'rss' feed
|
||||||
"""Serve the Atom or RSS feed as requested."""
|
instance_dir: the directory for the instance, containing both the feed dir and pages
|
||||||
logger.warning("wat")
|
dest_dir: the directory to place the feed subdir and requested feed
|
||||||
|
"""
|
||||||
if feed_type not in ('atom', 'rss'):
|
if feed_type not in ('atom', 'rss'):
|
||||||
abort(404)
|
raise ValueError(f"unsupported feed type {feed_type}")
|
||||||
|
|
||||||
fg = FeedGenerator()
|
fg = FeedGenerator()
|
||||||
fg.id(f'https://{app.config["DOMAIN_NAME"]}/')
|
fg.id(f'https://{Config.DOMAIN_NAME}/')
|
||||||
fg.title(f'{app.config["TITLE_SUFFIX"]}')
|
fg.title(f'{Config.TITLE_SUFFIX}')
|
||||||
fg.author(app.config["AUTHOR"])
|
fg.author(Config.AUTHOR)
|
||||||
fg.link(href=f'https://{app.config["DOMAIN_NAME"]}/feed/{feed_type}', rel='self')
|
fg.link(href=f'https://{Config.DOMAIN_NAME}/feed/{feed_type}', rel='self')
|
||||||
fg.link(href=f'https://{app.config["DOMAIN_NAME"]}', rel='alternate')
|
fg.link(href=f'https://{Config.DOMAIN_NAME}', rel='alternate')
|
||||||
fg.subtitle(f"Blog posts and other dated materials from {app.config['TITLE_SUFFIX']}")
|
fg.subtitle(f"Blog posts and other interesting materials from {Config.TITLE_SUFFIX}")
|
||||||
|
|
||||||
# get recent feeds
|
# get recent feeds
|
||||||
feed_path = os.path.join(app.instance_path, 'feed')
|
feed_path = os.path.join(instance_dir, 'feed')
|
||||||
feed_entry_paths = [os.path.join(dirpath, filename) for dirpath, _, filenames in os.walk(feed_path)
|
feed_entry_paths = [os.path.join(dirpath, filename) for dirpath, _, filenames in os.walk(feed_path)
|
||||||
for filename in filenames if os.path.islink(os.path.join(dirpath, filename))]
|
for filename in filenames if os.path.islink(os.path.join(dirpath, filename))]
|
||||||
for feed_entry_path in sorted(feed_entry_paths):
|
for feed_entry_path in sorted(feed_entry_paths):
|
||||||
# get the actual file to parse it
|
# get the actual file to parse it
|
||||||
resolved_path = os.path.realpath(feed_entry_path).replace(f'{app.instance_path}/', '')
|
os.chdir(os.path.abspath(os.path.join(instance_dir, 'pages')))
|
||||||
|
resolved_path = os.path.relpath(os.path.realpath(feed_entry_path), os.path.join(instance_dir, 'pages'))
|
||||||
try:
|
try:
|
||||||
content, md, page_name, page_title, mtime = parse_md(resolved_path)
|
content, md, page_name, page_title, mtime = parse_md(resolved_path)
|
||||||
link = f'https://{app.config["DOMAIN_NAME"]}/{instance_resource_path_to_request_path(resolved_path)}'
|
link = f'https://{Config.DOMAIN_NAME}{instance_resource_path_to_request_path(resolved_path)}'
|
||||||
except (OSError, ValueError, TypeError):
|
except (OSError, ValueError, TypeError):
|
||||||
logger.exception("error loading/rendering markdown!")
|
logger.exception("error loading/rendering markdown!")
|
||||||
abort(500)
|
raise
|
||||||
|
|
||||||
fe = fg.add_entry()
|
fe = fg.add_entry()
|
||||||
fe.id(_generate_feed_id(feed_entry_path))
|
fe.id(_generate_feed_id(feed_entry_path, instance_resource_path_to_request_path(resolved_path)))
|
||||||
fe.title(page_name if page_name else page_title)
|
fe.title(page_title)
|
||||||
fe.author(app.config["AUTHOR"])
|
fe.author(Config.AUTHOR)
|
||||||
fe.link(href=link)
|
fe.link(href=link)
|
||||||
fe.content(content, type='html')
|
fe.content(content, type='html')
|
||||||
|
|
||||||
if feed_type == 'atom':
|
if feed_type == 'atom':
|
||||||
return Response(fg.atom_str(pretty=True), mimetype='application/atom+xml')
|
try:
|
||||||
|
os.mkdir(os.path.join(dest_dir, 'feed'))
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
with open(os.path.join(dest_dir, 'feed', 'atom'), 'wb') as feed_file:
|
||||||
|
feed_file.write(fg.atom_str(pretty=True))
|
||||||
else:
|
else:
|
||||||
return Response(fg.rss_str(pretty=True), mimetype='application/rss+xml')
|
try:
|
||||||
|
os.mkdir(os.path.join(dest_dir, 'feed'))
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
with open(os.path.join(dest_dir, 'feed', 'rss'), 'wb') as feed_file:
|
||||||
|
feed_file.write(fg.rss_str(pretty=True))
|
||||||
|
|
||||||
|
|
||||||
def _generate_feed_id(feed_entry_path):
|
def _generate_feed_id(feed_entry_path, request_path):
|
||||||
"""For a relative file path, generate the Atom/RSS feed ID for it."""
|
"""For a relative file path, generate the Atom/RSS feed ID for it."""
|
||||||
date = re.sub(r'.*(\d{4})(\d{2})(\d{2}).*', r'\1-\2-\3', feed_entry_path)
|
date = re.sub(r'.*(\d{4})(\d{2})(\d{2}).*', r'\1-\2-\3', feed_entry_path)
|
||||||
cleaned = feed_entry_path.replace('#', '/').replace('feed/', '', 1).replace(app.instance_path, '')
|
cleaned = request_path.replace('#', '/')
|
||||||
return f'tag:{app.config["DOMAIN_NAME"]},{date}:{cleaned}'
|
return f'tag:{Config.DOMAIN_NAME},{date}:{cleaned}'
|
||||||
|
1
tests/instance/feed/20250316-more-metadata.md
Symbolic link
1
tests/instance/feed/20250316-more-metadata.md
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../pages/more-metadata.md
|
@ -13,6 +13,12 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
|||||||
|
|
||||||
def test_config():
|
def test_config():
|
||||||
"""Test that the app initialization sets values not normally present in the config."""
|
"""Test that the app initialization sets values not normally present in the config."""
|
||||||
|
# this may have gotten here from other imports in other tests
|
||||||
|
try:
|
||||||
|
delattr(Config, 'INSTANCE_VALUE')
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
assert not getattr(Config, 'INSTANCE_VALUE', None)
|
assert not getattr(Config, 'INSTANCE_VALUE', None)
|
||||||
assert not getattr(Config, 'EXTRA_VALUE', None)
|
assert not getattr(Config, 'EXTRA_VALUE', None)
|
||||||
|
|
||||||
|
@ -3,42 +3,65 @@
|
|||||||
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
|
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""
|
||||||
from incorporealcms.feed import serve_feed
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from incorporealcms import init_instance
|
||||||
|
from incorporealcms.feed import generate_feed
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
init_instance(instance_path=os.path.join(HERE, 'instance'))
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_type_is_404(client):
|
def test_atom_type_generated():
|
||||||
"""Test that requesting a feed type that doesn't exist is a 404."""
|
"""Test that an ATOM feed can be generated."""
|
||||||
response = client.get('/feed/wat')
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
assert response.status_code == 404
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
|
generate_feed('atom', src_dir, tmpdir)
|
||||||
|
|
||||||
|
with open(os.path.join(tmpdir, 'feed', 'atom'), 'r') as feed_output:
|
||||||
|
data = feed_output.read()
|
||||||
|
assert '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<feed xmlns="http://www.w3.org/2005/Atom">' in data
|
||||||
|
assert '<id>https://example.org/</id>' in data
|
||||||
|
assert '<email>admin@example.org</email>' in data
|
||||||
|
assert '<name>Test Name</name>' in data
|
||||||
|
|
||||||
|
# forced-no-title.md
|
||||||
|
assert '<title>example.org</title>' in data
|
||||||
|
assert '<link href="https://example.org/forced-no-title"/>' in data
|
||||||
|
assert '<id>tag:example.org,2023-12-01:/forced-no-title</id>' in data
|
||||||
|
assert '<content type="html"><p>some words are here</p></content>' in data
|
||||||
|
|
||||||
|
# more-metadata.md
|
||||||
|
assert '<title>title for the page - example.org</title>' in data
|
||||||
|
assert '<link href="https://example.org/more-metadata"/>' in data
|
||||||
|
assert '<id>tag:example.org,2025-03-16:/more-metadata</id>' in data
|
||||||
|
assert '<content type="html"><p>hello</p></content>' in data
|
||||||
|
|
||||||
|
|
||||||
def test_atom_type_is_200(client):
|
def test_rss_type_generated():
|
||||||
"""Test that requesting an ATOM feed is found."""
|
"""Test that an RSS feed can be generated."""
|
||||||
response = client.get('/feed/atom')
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
assert response.status_code == 200
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
assert 'application/atom+xml' in response.content_type
|
generate_feed('rss', src_dir, tmpdir)
|
||||||
print(response.text)
|
|
||||||
|
|
||||||
|
with open(os.path.join(tmpdir, 'feed', 'rss'), 'r') as feed_output:
|
||||||
|
data = feed_output.read()
|
||||||
|
assert '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<rss xmlns:atom="http://www.w3.org/2005/Atom"' in data
|
||||||
|
assert 'xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">' in data
|
||||||
|
assert '<link>https://example.org</link>' in data
|
||||||
|
|
||||||
def test_rss_type_is_200(client):
|
# forced-no-title.md
|
||||||
"""Test that requesting an RSS feed is found."""
|
assert '<title>example.org</title>' in data
|
||||||
response = client.get('/feed/rss')
|
assert '<link>https://example.org/forced-no-title</link>' in data
|
||||||
assert response.status_code == 200
|
assert '<guid isPermaLink="false">tag:example.org,2023-12-01:/forced-no-title</guid>' in data
|
||||||
assert 'application/rss+xml' in response.content_type
|
assert '<description><p>some words are here</p></description>' in data
|
||||||
print(response.text)
|
assert '<author>admin@example.org (Test Name)</author>' in data
|
||||||
|
|
||||||
|
# more-metadata.md
|
||||||
def test_feed_generator_atom(app):
|
assert '<title>title for the page - example.org</title>' in data
|
||||||
"""Test the root feed generator."""
|
assert '<link>https://example.org/more-metadata</link>' in data
|
||||||
with app.test_request_context():
|
assert '<guid isPermaLink="false">tag:example.org,2025-03-16:/more-metadata</guid>' in data
|
||||||
content = serve_feed('atom')
|
assert '<description><p>hello</p></description>' in data
|
||||||
assert b'<id>https://example.com/</id>' in content.data
|
assert '<author>admin@example.org (Test Name)</author>' in data
|
||||||
assert b'<email>admin@example.com</email>' in content.data
|
|
||||||
assert b'<name>Test Name</name>' in content.data
|
|
||||||
|
|
||||||
|
|
||||||
def test_feed_generator_rss(app):
|
|
||||||
"""Test the root feed generator."""
|
|
||||||
with app.test_request_context():
|
|
||||||
content = serve_feed('rss')
|
|
||||||
assert b'<author>admin@example.com (Test Name)</author>' in content.data
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user