16 Commits

Author SHA1 Message Date
5994b73b2e give tables a lighter border 2020-06-14 10:56:57 -05:00
dadc902c49 put a bit of a background behind blockquote
closes #2
2020-06-14 10:55:24 -05:00
5c8251d01a explicitly set the footer margin-top 2020-06-14 10:01:07 -05:00
29498504cc get the actual pinned requirements in setup.py 2020-05-28 17:00:58 -05:00
ce06de78a8 tests misleadingly had a leading /, need to append it ourselves 2020-05-28 16:52:43 -05:00
beea0c80bf CSS: slightly tweak/specify the text size/height 2020-05-28 12:18:28 -05:00
ab977f7e81 header CSS tweaks 2020-05-28 12:18:04 -05:00
05f879ab80 display untitled-page paths as /path rather than path.md 2020-05-28 12:17:27 -05:00
059108c37b rewrite generate_parent_navs
* works on a path now, not a file location
* as such is sliiiiiightly easier to understand
* now also puts the current page in the nav
* fixed failing test where this caused an error (rather than 404) on
  non-existent paths
2020-05-28 12:09:59 -05:00
0993147dea give tables a bottom margin
otherwise they look bad, for instance, at the very end of the page, too
close to the "Last modified" text.
2020-05-28 08:20:24 -05:00
9e97cb097e requirements bump; tests pass 2020-05-28 08:13:55 -05:00
da2476bbda enable table support in the markdown parser 2020-04-05 10:25:46 -05:00
576ffc359c show some navigation on every page
closes #1
2020-03-15 20:33:23 -05:00
582cc9a2d1 rename: page_file_resolver -> resolve_page_file 2020-03-15 18:52:49 -05:00
5ce44ba31c move display_page path resolution logging to DEBUG 2020-03-15 18:43:41 -05:00
ed0dab14f3 tweak request logging, log response info 2020-03-15 18:43:15 -05:00
12 changed files with 187 additions and 48 deletions

View File

@@ -32,7 +32,12 @@ def create_app(instance_path=None, test_config=None):
@app.before_request @app.before_request
def log_request(): def log_request():
logger.info("REQUEST: [ %s ]", request.path) logger.info("REQUEST: %s %s", request.method, request.path)
@app.after_request
def log_response(response):
logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status)
return response
@app.route(f'/{app.config["MEDIA_DIR"]}/<path:filename>') @app.route(f'/{app.config["MEDIA_DIR"]}/<path:filename>')
def media_files(filename): def media_files(filename):

View File

@@ -10,18 +10,19 @@ from tzlocal import get_localzone
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
bp = Blueprint('pages', __name__, url_prefix='/') bp = Blueprint('pages', __name__, url_prefix='/')
md = markdown.Markdown(extensions=['meta']) md = markdown.Markdown(extensions=['meta', 'tables'])
@bp.route('/', defaults={'path': 'index'}) @bp.route('/', defaults={'path': 'index'})
@bp.route('/<path:path>') @bp.route('/<path:path>')
def display_page(path): def display_page(path):
"""Get the file contents of the requested path and render the file.""" """Get the file contents of the requested path and render the file."""
resolved_path = page_file_resolver(path) resolved_path = resolve_page_file(path)
logger.info("received request for path '%s', resolved to '%s'", path, resolved_path) logger.debug("received request for path '%s', resolved to '%s'", path, resolved_path)
try: try:
with app.open_instance_resource(resolved_path, 'r') as entry_file: with app.open_instance_resource(resolved_path, 'r') as entry_file:
logger.debug("file '%s' found", resolved_path) logger.debug("file '%s' found", resolved_path)
parent_navs = generate_parent_navs(path)
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone()) mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone())
entry = entry_file.read() entry = entry_file.read()
except FileNotFoundError: except FileNotFoundError:
@@ -31,10 +32,11 @@ def display_page(path):
content = Markup(md.convert(entry)) content = Markup(md.convert(entry))
logger.debug("file metadata: %s", md.Meta) logger.debug("file metadata: %s", md.Meta)
title = " ".join(md.Meta.get('title')) if md.Meta.get('title') else "" title = " ".join(md.Meta.get('title')) if md.Meta.get('title') else ""
return render_template('base.html', title=title, content=content, mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z')) return render_template('base.html', title=title, content=content, navs=parent_navs,
mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'))
def page_file_resolver(path): def resolve_page_file(path):
"""Manipulate the request path to find appropriate page file. """Manipulate the request path to find appropriate page file.
* convert dir requests to index files * convert dir requests to index files
@@ -46,3 +48,23 @@ def page_file_resolver(path):
path = f'{path}index' path = f'{path}index'
path = f'pages/{path}.md' path = f'pages/{path}.md'
return path return path
def generate_parent_navs(path):
"""Create a series of paths/links to navigate up from the given path."""
# derive additional path/location stuff based on path
resolved_path = resolve_page_file(path)
parent_dir = os.path.dirname(resolved_path)
parent_path = '/'.join(path[:-1].split('/')[:-1]) + '/'
logger.debug("path: '%s'; parent path: '%s'; resolved path: '%s'; parent dir: '%s'",
path, parent_path, resolved_path, parent_dir)
if path in ('index', '/'):
return [(app.config['TITLE_SUFFIX'], '/')]
else:
with app.open_instance_resource(resolved_path, 'r') as entry_file:
entry = entry_file.read()
_ = Markup(md.convert(entry))
page_name = " ".join(md.Meta.get('title')) if md.Meta.get('title') else f'/{path}'
return generate_parent_navs(parent_path) + [(page_name, f'/{path}')]

View File

@@ -1,13 +1,42 @@
html { html {
font-family: sans-serif; font-family: sans-serif;
padding: 0 1em; padding: 0;
padding-bottom: 16px;
color: #222; color: #222;
} }
body {
margin: 0;
}
h1,h2,h3,h4,h5,h6 { h1,h2,h3,h4,h5,h6 {
color: #811610; color: #811610;
} }
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
h3 {
font-size: 1.25em;
}
h4 {
font-size: 1.17em;
}
h5 {
font-size: 1em;
}
h6 {
font-size: .83em;
}
a:link { a:link {
color: #222; color: #222;
font-weight: bold; font-weight: bold;
@@ -36,8 +65,49 @@ a:active {
border-bottom: 1px dotted #222; border-bottom: 1px dotted #222;
} }
section.nav {
color: #666;
background: #eee;
font-size: 0.75em;
border-bottom: 1px solid #ccc;
padding: 0.25em 0.5em;
}
section.nav a {
color: #666;
border-bottom: none;
}
section.content {
font-size: 11pt;
padding: 0 1em;
line-height: 1.5em;
}
footer { footer {
display: block; display: block;
font-size: 75%; font-size: 75%;
color: #999; color: #999;
padding: 0 1em;
margin-top: 15px;
}
table {
border-collapse: collapse;
}
table, th, td {
padding: 5px;
border: 1px solid #ccc;
margin-bottom: 15px;
}
th {
background: #eee;
}
blockquote {
background-color: rgba(120, 120, 120, 0.1);
padding: 1px 10px;
} }

View File

@@ -2,6 +2,12 @@
<title>{{ title }}{% if title %} - {% endif %}{{ config.TITLE_SUFFIX }}</title> <title>{{ title }}{% if title %} - {% endif %}{{ config.TITLE_SUFFIX }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}"> <link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
<section class="nav">
{% for nav in navs %}
<a href="{{ nav.1 }}">{{ nav.0 }}</a>
{% if not loop.last %} &raquo; {% endif %}
{% endfor %}
</section>
<section class="content"> <section class="content">
{{ content }} {{ content }}
</section> </section>

View File

@@ -5,34 +5,33 @@
# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in # pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in
# #
attrs==19.3.0 # via pytest attrs==19.3.0 # via pytest
click==7.0 # via flask, pip-tools click==7.1.2 # via flask, pip-tools
coverage==5.0.3 # via pytest-cov coverage==5.1 # via pytest-cov
entrypoints==0.3 # via flake8 flake8==3.8.2 # via -r requirements/requirements-dev.in
flake8==3.7.9 # via -r requirements/requirements-dev.in flask==1.1.2 # via -r requirements/requirements.in
flask==1.1.1 # via -r requirements/requirements.in importlib-metadata==1.6.0 # via flake8, markdown, pluggy, pytest
importlib-metadata==1.5.0 # via pluggy, pytest
itsdangerous==1.1.0 # via flask itsdangerous==1.1.0 # via flask
jinja2==2.11.1 # via flask jinja2==2.11.2 # via flask
markdown==3.2.1 # via -r requirements/requirements.in markdown==3.2.2 # via -r requirements/requirements.in
markupsafe==1.1.1 # via jinja2 markupsafe==1.1.1 # via jinja2
mccabe==0.6.1 # via flake8 mccabe==0.6.1 # via flake8
more-itertools==8.2.0 # via pytest more-itertools==8.3.0 # via pytest
packaging==20.3 # via pytest packaging==20.4 # via pytest
pip-tools==4.5.1 # via -r requirements/requirements-dev.in pip-tools==5.2.0 # via -r requirements/requirements-dev.in
pluggy==0.13.1 # via pytest pluggy==0.13.1 # via pytest
py==1.8.1 # via pytest py==1.8.1 # via pytest
pycodestyle==2.5.0 # via flake8 pycodestyle==2.6.0 # via flake8
pyflakes==2.1.1 # via flake8 pyflakes==2.2.0 # via flake8
pyparsing==2.4.6 # via packaging pyparsing==2.4.7 # via packaging
pytest-cov==2.8.1 # via -r requirements/requirements-dev.in pytest-cov==2.9.0 # via -r requirements/requirements-dev.in
pytest==5.3.5 # via -r requirements/requirements-dev.in, pytest-cov pytest==5.4.2 # via -r requirements/requirements-dev.in, pytest-cov
pytz==2019.3 # via tzlocal pytz==2020.1 # via tzlocal
six==1.14.0 # via packaging, pip-tools six==1.15.0 # via packaging, pip-tools
tzlocal==2.0.0 # via -r requirements/requirements.in tzlocal==2.1 # via -r requirements/requirements.in
versioneer==0.18 # via -r requirements/requirements-dev.in versioneer==0.18 # via -r requirements/requirements-dev.in
wcwidth==0.1.8 # via pytest wcwidth==0.1.9 # via pytest
werkzeug==1.0.0 # via flask werkzeug==1.0.1 # via flask
zipp==3.1.0 # via importlib-metadata zipp==3.1.0 # via importlib-metadata
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# setuptools # pip

View File

@@ -4,15 +4,14 @@
# #
# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in # pip-compile --output-file=requirements/requirements.txt requirements/requirements.in
# #
click==7.0 # via flask click==7.1.2 # via flask
flask==1.1.1 # via -r requirements/requirements.in flask==1.1.2 # via -r requirements/requirements.in
importlib-metadata==1.6.0 # via markdown
itsdangerous==1.1.0 # via flask itsdangerous==1.1.0 # via flask
jinja2==2.11.1 # via flask jinja2==2.11.2 # via flask
markdown==3.2.1 # via -r requirements/requirements.in markdown==3.2.2 # via -r requirements/requirements.in
markupsafe==1.1.1 # via jinja2 markupsafe==1.1.1 # via jinja2
pytz==2019.3 # via tzlocal pytz==2020.1 # via tzlocal
tzlocal==2.0.0 # via -r requirements/requirements.in tzlocal==2.1 # via -r requirements/requirements.in
werkzeug==1.0.0 # via flask werkzeug==1.0.1 # via flask
zipp==3.1.0 # via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
# setuptools

View File

@@ -8,8 +8,8 @@ HERE = os.path.dirname(os.path.abspath(__file__))
def extract_requires(): def extract_requires():
"""Get pinned requirements from requirements.in.""" """Get pinned requirements from requirements.txt."""
with open(os.path.join(HERE, 'requirements/requirements.in'), 'r') as reqs: with open(os.path.join(HERE, 'requirements/requirements.txt'), 'r') as reqs:
return [line.split(' ')[0] for line in reqs if not line[0] in ('-', '#')] return [line.split(' ')[0] for line in reqs if not line[0] in ('-', '#')]

View File

@@ -0,0 +1,3 @@
Title: SUB!
# subdir-with-title

View File

@@ -0,0 +1 @@
test page

View File

@@ -0,0 +1 @@
# another page

View File

@@ -0,0 +1,3 @@
Title: Page
# hello

View File

@@ -1,19 +1,19 @@
"""Test page views and helper methods.""" """Test page views and helper methods."""
import re import re
from incorporealcms.pages import page_file_resolver from incorporealcms.pages import generate_parent_navs, resolve_page_file
def test_page_file_resolver_dir_to_index(): def test_resolve_page_file_dir_to_index():
assert page_file_resolver('foo/') == 'pages/foo/index.md' assert resolve_page_file('foo/') == 'pages/foo/index.md'
def test_page_file_resolver_subdir_to_index(): def test_resolve_page_file_subdir_to_index():
assert page_file_resolver('foo/bar/') == 'pages/foo/bar/index.md' assert resolve_page_file('foo/bar/') == 'pages/foo/bar/index.md'
def test_page_file_resolver_other_requests_fine(): def test_resolve_page_file_other_requests_fine():
assert page_file_resolver('foo/baz') == 'pages/foo/baz.md' assert resolve_page_file('foo/baz') == 'pages/foo/baz.md'
def test_page_that_exists(client): def test_page_that_exists(client):
@@ -44,3 +44,33 @@ def test_page_has_modified_timestamp(client):
response = client.get('/') response = client.get('/')
assert response.status_code == 200 assert response.status_code == 200
assert re.search(r'Last modified: ....-..-.. ..:..:.. ...', response.data.decode()) is not None assert re.search(r'Last modified: ....-..-.. ..:..:.. ...', response.data.decode()) is not None
def test_generate_page_navs_index(app):
with app.app_context():
assert generate_parent_navs('/') == [('incorporeal.org', '/')]
def test_generate_page_navs_alternate_index(app):
with app.app_context():
assert generate_parent_navs('index') == [('incorporeal.org', '/')]
def test_generate_page_navs_subdir_index(app):
with app.app_context():
assert generate_parent_navs('subdir/') == [('incorporeal.org', '/'), ('/subdir/', '/subdir/')]
def test_generate_page_navs_subdir_real_page(app):
with app.app_context():
assert generate_parent_navs('subdir/page') == [('incorporeal.org', '/'), ('/subdir/', '/subdir/'),
('Page', '/subdir/page')]
def test_generate_page_navs_subdir_with_title_parsing_real_page(app):
with app.app_context():
assert generate_parent_navs('subdir-with-title/page') == [
('incorporeal.org', '/'),
('SUB!', '/subdir-with-title/'),
('/subdir-with-title/page', '/subdir-with-title/page')
]