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'
|
||||
TITLE_SUFFIX = DOMAIN_NAME
|
||||
BASE_HOST = 'http://' + DOMAIN_NAME
|
||||
CONTACT_EMAIL = 'admin@example.com'
|
||||
CONTACT_EMAIL = 'admin@example.org'
|
||||
|
||||
# feed settings
|
||||
AUTHOR = {'name': 'Test Name', 'email': 'admin@example.com'}
|
||||
AUTHOR = {'name': 'Test Name', 'email': 'admin@example.org'}
|
||||
|
||||
FAVICON = '/static/img/favicon.png'
|
||||
|
||||
|
@ -14,60 +14,72 @@ import os
|
||||
import re
|
||||
|
||||
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__)
|
||||
|
||||
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>')
|
||||
def serve_feed(feed_type):
|
||||
"""Serve the Atom or RSS feed as requested."""
|
||||
logger.warning("wat")
|
||||
Args:
|
||||
feed_type: 'atom' or 'rss' feed
|
||||
instance_dir: the directory for the instance, containing both the feed dir and pages
|
||||
dest_dir: the directory to place the feed subdir and requested feed
|
||||
"""
|
||||
if feed_type not in ('atom', 'rss'):
|
||||
abort(404)
|
||||
raise ValueError(f"unsupported feed type {feed_type}")
|
||||
|
||||
fg = FeedGenerator()
|
||||
fg.id(f'https://{app.config["DOMAIN_NAME"]}/')
|
||||
fg.title(f'{app.config["TITLE_SUFFIX"]}')
|
||||
fg.author(app.config["AUTHOR"])
|
||||
fg.link(href=f'https://{app.config["DOMAIN_NAME"]}/feed/{feed_type}', rel='self')
|
||||
fg.link(href=f'https://{app.config["DOMAIN_NAME"]}', rel='alternate')
|
||||
fg.subtitle(f"Blog posts and other dated materials from {app.config['TITLE_SUFFIX']}")
|
||||
fg.id(f'https://{Config.DOMAIN_NAME}/')
|
||||
fg.title(f'{Config.TITLE_SUFFIX}')
|
||||
fg.author(Config.AUTHOR)
|
||||
fg.link(href=f'https://{Config.DOMAIN_NAME}/feed/{feed_type}', rel='self')
|
||||
fg.link(href=f'https://{Config.DOMAIN_NAME}', rel='alternate')
|
||||
fg.subtitle(f"Blog posts and other interesting materials from {Config.TITLE_SUFFIX}")
|
||||
|
||||
# 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)
|
||||
for filename in filenames if os.path.islink(os.path.join(dirpath, filename))]
|
||||
for feed_entry_path in sorted(feed_entry_paths):
|
||||
# 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:
|
||||
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):
|
||||
logger.exception("error loading/rendering markdown!")
|
||||
abort(500)
|
||||
raise
|
||||
|
||||
fe = fg.add_entry()
|
||||
fe.id(_generate_feed_id(feed_entry_path))
|
||||
fe.title(page_name if page_name else page_title)
|
||||
fe.author(app.config["AUTHOR"])
|
||||
fe.id(_generate_feed_id(feed_entry_path, instance_resource_path_to_request_path(resolved_path)))
|
||||
fe.title(page_title)
|
||||
fe.author(Config.AUTHOR)
|
||||
fe.link(href=link)
|
||||
fe.content(content, type='html')
|
||||
|
||||
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:
|
||||
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."""
|
||||
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, '')
|
||||
return f'tag:{app.config["DOMAIN_NAME"]},{date}:{cleaned}'
|
||||
cleaned = request_path.replace('#', '/')
|
||||
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():
|
||||
"""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, 'EXTRA_VALUE', None)
|
||||
|
||||
|
@ -3,42 +3,65 @@
|
||||
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
|
||||
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):
|
||||
"""Test that requesting a feed type that doesn't exist is a 404."""
|
||||
response = client.get('/feed/wat')
|
||||
assert response.status_code == 404
|
||||
def test_atom_type_generated():
|
||||
"""Test that an ATOM feed can be generated."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
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):
|
||||
"""Test that requesting an ATOM feed is found."""
|
||||
response = client.get('/feed/atom')
|
||||
assert response.status_code == 200
|
||||
assert 'application/atom+xml' in response.content_type
|
||||
print(response.text)
|
||||
def test_rss_type_generated():
|
||||
"""Test that an RSS feed can be generated."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
generate_feed('rss', src_dir, tmpdir)
|
||||
|
||||
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):
|
||||
"""Test that requesting an RSS feed is found."""
|
||||
response = client.get('/feed/rss')
|
||||
assert response.status_code == 200
|
||||
assert 'application/rss+xml' in response.content_type
|
||||
print(response.text)
|
||||
# forced-no-title.md
|
||||
assert '<title>example.org</title>' in data
|
||||
assert '<link>https://example.org/forced-no-title</link>' in data
|
||||
assert '<guid isPermaLink="false">tag:example.org,2023-12-01:/forced-no-title</guid>' in data
|
||||
assert '<description><p>some words are here</p></description>' in data
|
||||
assert '<author>admin@example.org (Test Name)</author>' in data
|
||||
|
||||
|
||||
def test_feed_generator_atom(app):
|
||||
"""Test the root feed generator."""
|
||||
with app.test_request_context():
|
||||
content = serve_feed('atom')
|
||||
assert b'<id>https://example.com/</id>' in content.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
|
||||
# more-metadata.md
|
||||
assert '<title>title for the page - example.org</title>' in data
|
||||
assert '<link>https://example.org/more-metadata</link>' in data
|
||||
assert '<guid isPermaLink="false">tag:example.org,2025-03-16:/more-metadata</guid>' in data
|
||||
assert '<description><p>hello</p></description>' in data
|
||||
assert '<author>admin@example.org (Test Name)</author>' in data
|
||||
|
Loading…
x
Reference in New Issue
Block a user