Compare commits

..

No commits in common. "b3dfab2611d294f8c1d25b1d4acef62122ba75cf" and "0f19fcb174586c3d516f2203fb5253447f1c5494" have entirely different histories.

14 changed files with 76 additions and 158 deletions

View File

@ -40,9 +40,8 @@ def create_app(instance_path=None, test_config=None):
logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status) logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status)
return response return response
from . import error_pages, pages, static from . import error_pages, pages
app.register_blueprint(pages.bp) app.register_blueprint(pages.bp)
app.register_blueprint(static.bp)
app.register_error_handler(400, error_pages.bad_request) app.register_error_handler(400, error_pages.bad_request)
app.register_error_handler(404, error_pages.page_not_found) app.register_error_handler(404, error_pages.page_not_found)
app.register_error_handler(500, error_pages.internal_server_error) app.register_error_handler(500, error_pages.internal_server_error)

View File

@ -50,12 +50,6 @@ class Config(object):
MEDIA_DIR = 'media' MEDIA_DIR = 'media'
# customizations # customizations
PAGE_STYLES = {
'dark': '/static/css/dark.css',
'light': '/static/css/light.css',
'plain': '/static/css/plain.css',
}
DEFAULT_PAGE_STYLE = 'light' DEFAULT_PAGE_STYLE = 'light'
TITLE_SUFFIX = 'example.com' TITLE_SUFFIX = 'example.com'
CONTACT_EMAIL = 'admin@example.com' CONTACT_EMAIL = 'admin@example.com'

View File

@ -31,15 +31,19 @@ def render(template_name_or_list, **context):
* Determine the proper site theme to use in the template and provide it. * Determine the proper site theme to use in the template and provide it.
""" """
page_styles = app.config['PAGE_STYLES'] PAGE_STYLES = {
'dark': 'css/dark.css',
'light': 'css/light.css',
'plain': 'css/plain.css',
}
selected_style = request.args.get('style', None) selected_style = request.args.get('style', None)
if selected_style: if selected_style:
user_style = selected_style user_style = selected_style
else: else:
user_style = request.cookies.get('user-style') user_style = request.cookies.get('user-style')
logger.debug("user style cookie: %s", user_style) logger.debug("user style cookie: %s", user_style)
context['user_style'] = page_styles.get(user_style, page_styles.get(app.config['DEFAULT_PAGE_STYLE'])) context['user_style'] = PAGE_STYLES.get(user_style, PAGE_STYLES.get(app.config['DEFAULT_PAGE_STYLE']))
context['page_styles'] = page_styles
resp = make_response(render_template(template_name_or_list, **context)) resp = make_response(render_template(template_name_or_list, **context))
if selected_style: if selected_style:

View File

@ -1,14 +0,0 @@
"""Serve static files from the instance directory."""
import os
from flask import Blueprint
from flask import current_app as app
from flask import send_from_directory
bp = Blueprint('static', __name__, url_prefix='/custom-static')
@bp.route('/<path:name>')
def serve_instance_static_file(name):
"""Serve a static file from the instance directory, used for customization."""
return send_from_directory(os.path.join(app.instance_path, 'custom-static'), name)

View File

@ -28,8 +28,7 @@ body {
a { a {
font-weight: bold; font-weight: bold;
text-decoration-line: underline; text-decoration: none;
text-decoration-thickness: 1px;
} }
div.header { div.header {
@ -40,6 +39,10 @@ div.header {
padding-bottom: 0; padding-bottom: 0;
} }
div.header a {
border-bottom: none;
}
div.content { div.content {
font-size: 11pt; font-size: 11pt;
padding: 0 1rem; padding: 0 1rem;
@ -167,5 +170,6 @@ figcaption {
} }
.footnote-ref:link, .footnote-ref:visited, .footnote-ref:hover, .footnote-ref:active { .footnote-ref:link, .footnote-ref:visited, .footnote-ref:hover, .footnote-ref:active {
border-bottom: none;
font-weight: normal; font-weight: normal;
} }

View File

@ -15,14 +15,17 @@ h1, h2, h3, h4, h5, h6 {
p a, ul a, ol a { p a, ul a, ol a {
color: #DDD; color: #DDD;
border-bottom: 1px solid #DDD;
} }
footer a { footer a {
color: #999; color: #999;
border-bottom: 1px solid #999;
} }
p a:hover, ul a:hover, ol a:hover, footer a:hover { p a:hover, ul a:hover, ol a:hover, footer a:hover {
color: #B31D15; color: #B31D15;
border-bottom: 1px solid #B31D15;
} }
div.site-wrap { div.site-wrap {
@ -31,7 +34,10 @@ div.site-wrap {
div.header, div.header a { div.header, div.header a {
color: #555; color: #555;
text-decoration: none; }
div.header a:hover, div.header a:active {
border-bottom: 1px solid #555;
} }
table, th, td { table, th, td {

View File

@ -15,14 +15,17 @@ h1, h2, h3, h4, h5, h6 {
p a, ul a, ol a { p a, ul a, ol a {
color: #222; color: #222;
border-bottom: 1px solid #222;
} }
footer a { footer a {
color: #999; color: #999;
border-bottom: 1px solid #999;
} }
p a:hover, ul a:hover, ol a:hover, footer a:hover { p a:hover, ul a:hover, ol a:hover, footer a:hover {
color: #811610; color: #811610;
border-bottom: 1px solid #811610;
} }
div.site-wrap { div.site-wrap {
@ -31,7 +34,10 @@ div.site-wrap {
div.header, div.header a { div.header, div.header a {
color: #AAA; color: #AAA;
text-decoration: none; }
div.header a:hover, div.header a:active {
border-bottom: 1px solid #AAA;
} }
table, th, td { table, th, td {

View File

@ -7,7 +7,7 @@
<meta property="og:url" content="{{ base_url }}"> <meta property="og:url" content="{{ base_url }}">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ user_style }}"> <link rel="stylesheet" href="{{ url_for('static', filename=user_style) }}">
<link rel="icon" href="{% if config.FAVICON %}{{ config.FAVICON }}{% else %}{{ url_for('static', filename='img/favicon.png') }}{% endif %}"> <link rel="icon" href="{% if config.FAVICON %}{{ config.FAVICON }}{% else %}{{ url_for('static', filename='img/favicon.png') }}{% endif %}">
<div {% block site_class %}class="site-wrap site-wrap-normal-width"{% endblock %}> <div {% block site_class %}class="site-wrap site-wrap-normal-width"{% endblock %}>
@ -20,9 +20,9 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="styles"> <div class="styles">
{% for style in page_styles %} <a href="?style=dark">[dark]</a>
<a href="?style={{ style }}">[{{ style }}]</a> <a href="?style=light">[light]</a>
{% endfor %} <a href="?style=plain">[plain]</a>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -21,6 +21,6 @@ safety # check requirements file for issues
# maintenance utilities and tox # maintenance utilities and tox
pip-tools # pip-compile pip-tools # pip-compile
tox<4 # CI stuff, pinned for now to avoid packaging conflict w/safety tox # CI stuff
tox-wheel # build wheels in tox tox-wheel # build wheels in tox
versioneer # automatic version numbering versioneer # automatic version numbering

View File

@ -1,16 +1,16 @@
# #
# This file is autogenerated by pip-compile with Python 3.10 # This file is autogenerated by pip-compile with python 3.10
# by the following command: # To update, run:
# #
# 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==22.2.0 attrs==22.1.0
# via pytest # via pytest
bandit==1.7.4 bandit==1.7.4
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
build==0.9.0 build==0.8.0
# via pip-tools # via pip-tools
certifi==2022.12.7 certifi==2022.9.14
# via requests # via requests
charset-normalizer==2.1.1 charset-normalizer==2.1.1
# via requests # via requests
@ -19,17 +19,15 @@ click==8.1.3
# flask # flask
# pip-tools # pip-tools
# safety # safety
coverage[toml]==7.0.1 coverage[toml]==6.4.4
# via pytest-cov # via pytest-cov
distlib==0.3.6 distlib==0.3.6
# via virtualenv # via virtualenv
dlint==0.13.0 dlint==0.13.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
dparse==0.6.2 dparse==0.6.0
# via safety # via safety
exceptiongroup==1.1.0 filelock==3.8.0
# via pytest
filelock==3.9.0
# via # via
# tox # tox
# virtualenv # virtualenv
@ -44,31 +42,31 @@ flake8==5.0.4
# flake8-mutable # flake8-mutable
flake8-blind-except==0.2.1 flake8-blind-except==0.2.1
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-builtins==2.1.0 flake8-builtins==1.5.3
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-docstrings==1.6.0 flake8-docstrings==1.6.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-executable==2.1.2 flake8-executable==2.1.1
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-fixme==1.1.1 flake8-fixme==1.1.1
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-isort==6.0.0 flake8-isort==4.2.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-logging-format==0.9.0 flake8-logging-format==0.7.5
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-mutable==1.2.0 flake8-mutable==1.2.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flask==2.2.2 flask==2.2.2
# via -r requirements/requirements.in # via -r requirements/requirements.in
gitdb==4.0.10 gitdb==4.0.9
# via gitpython # via gitpython
gitpython==3.1.30 gitpython==3.1.27
# via bandit # via bandit
idna==3.4 idna==3.4
# via requests # via requests
iniconfig==1.1.1 iniconfig==1.1.1
# via pytest # via pytest
isort==5.11.4 isort==5.10.1
# via flake8-isort # via flake8-isort
itsdangerous==2.1.2 itsdangerous==2.1.2
# via flask # via flask
@ -89,20 +87,22 @@ packaging==21.3
# pytest # pytest
# safety # safety
# tox # tox
pbr==5.11.0 pbr==5.10.0
# via stevedore # via stevedore
pep517==0.13.0 pep517==0.13.0
# via build # via build
pip-tools==6.12.1 pip-tools==6.8.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
platformdirs==2.6.2 platformdirs==2.5.2
# via virtualenv # via virtualenv
pluggy==1.0.0 pluggy==1.0.0
# via # via
# pytest # pytest
# tox # tox
py==1.11.0 py==1.11.0
# via tox # via
# pytest
# tox
pycodestyle==2.9.1 pycodestyle==2.9.1
# via flake8 # via flake8
pydocstyle==6.1.1 pydocstyle==6.1.1
@ -115,11 +115,11 @@ pyparsing==3.0.9
# via # via
# packaging # packaging
# pydot # pydot
pytest==7.2.0 pytest==7.1.3
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# pytest-cov # pytest-cov
pytest-cov==4.0.0 pytest-cov==3.0.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
pyyaml==6.0 pyyaml==6.0
# via bandit # via bandit
@ -127,9 +127,9 @@ requests==2.28.1
# via safety # via safety
ruamel-yaml==0.17.21 ruamel-yaml==0.17.21
# via safety # via safety
ruamel-yaml-clib==0.2.7 ruamel-yaml-clib==0.2.6
# via ruamel-yaml # via ruamel-yaml
safety==2.3.5 safety==2.1.1
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
six==1.16.0 six==1.16.0
# via tox # via tox
@ -137,7 +137,7 @@ smmap==5.0.0
# via gitdb # via gitdb
snowballstemmer==2.2.0 snowballstemmer==2.2.0
# via pydocstyle # via pydocstyle
stevedore==4.1.1 stevedore==4.0.0
# via bandit # via bandit
toml==0.10.2 toml==0.10.2
# via dparse # via dparse
@ -148,21 +148,21 @@ tomli==2.0.1
# pep517 # pep517
# pytest # pytest
# tox # tox
tox==3.28.0 tox==3.26.0
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# tox-wheel # tox-wheel
tox-wheel==1.0.0 tox-wheel==0.7.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
urllib3==1.26.13 urllib3==1.26.12
# via requests # via requests
versioneer==0.28 versioneer==0.26
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
virtualenv==20.17.1 virtualenv==20.16.5
# via tox # via tox
werkzeug==2.2.2 werkzeug==2.2.2
# via flask # via flask
wheel==0.38.4 wheel==0.37.1
# via # via
# pip-tools # pip-tools
# tox-wheel # tox-wheel

View File

@ -190,15 +190,15 @@ def test_setting_selected_style_includes_cookie(client):
response = client.get('/?style=light') response = client.get('/?style=light')
style_cookie = next((cookie for cookie in client.cookie_jar if cookie.name == 'user-style'), None) style_cookie = next((cookie for cookie in client.cookie_jar if cookie.name == 'user-style'), None)
assert response.status_code == 200 assert response.status_code == 200
assert b'/static/css/light.css' in response.data assert b'light.css' in response.data
assert b'/static/css/dark.css' not in response.data assert b'dark.css' not in response.data
assert style_cookie.value == 'light' assert style_cookie.value == 'light'
response = client.get('/?style=dark') response = client.get('/?style=dark')
style_cookie = next((cookie for cookie in client.cookie_jar if cookie.name == 'user-style'), None) style_cookie = next((cookie for cookie in client.cookie_jar if cookie.name == 'user-style'), None)
assert response.status_code == 200 assert response.status_code == 200
assert b'/static/css/dark.css' in response.data assert b'dark.css' in response.data
assert b'/static/css/light.css' not in response.data assert b'light.css' not in response.data
assert style_cookie.value == 'dark' assert style_cookie.value == 'dark'
@ -218,23 +218,3 @@ def test_extra_footer_per_page(client):
assert b'<div class="extra-footer">' not in response.data assert b'<div class="extra-footer">' not in response.data
response = client.get('/index-but-with-footer') response = client.get('/index-but-with-footer')
assert b'<div class="extra-footer"><i>ooo <a href="a">a</a></i>' in response.data assert b'<div class="extra-footer"><i>ooo <a href="a">a</a></i>' in response.data
def test_serving_static_files(client):
"""Test the usage of send_from_directory to serve extra static files."""
response = client.get('/custom-static/css/warm.css')
assert response.status_code == 200
# can't serve directories, just files
response = client.get('/custom-static/')
assert response.status_code == 404
response = client.get('/custom-static/css/')
assert response.status_code == 404
response = client.get('/custom-static/css')
assert response.status_code == 404
# can't serve files that don't exist or bad paths
response = client.get('/custom-static/css/cold.css')
assert response.status_code == 404
response = client.get('/custom-static/css/../../unreachable.md')
assert response.status_code == 404

View File

@ -1,3 +0,0 @@
* {
color: red;
}

View File

@ -1,15 +1,10 @@
"""Unit test helper methods.""" """Unit test helper methods."""
import os
import pytest import pytest
from werkzeug.http import dump_cookie from werkzeug.http import dump_cookie
from incorporealcms import create_app
from incorporealcms.pages import (generate_parent_navs, instance_resource_path_to_request_path, render, from incorporealcms.pages import (generate_parent_navs, instance_resource_path_to_request_path, render,
request_path_to_breadcrumb_display, request_path_to_instance_resource_path) request_path_to_breadcrumb_display, request_path_to_instance_resource_path)
HERE = os.path.dirname(os.path.abspath(__file__))
def test_generate_page_navs_index(app): def test_generate_page_navs_index(app):
"""Test that the index page has navs to the root (itself).""" """Test that the index page has navs to the root (itself)."""
@ -54,74 +49,22 @@ def test_render_with_user_dark_theme(app):
"""Test that a request with the dark theme selected renders the dark theme.""" """Test that a request with the dark theme selected renders the dark theme."""
cookie = dump_cookie("user-style", 'dark') cookie = dump_cookie("user-style", 'dark')
with app.test_request_context(headers={'COOKIE': cookie}): with app.test_request_context(headers={'COOKIE': cookie}):
assert b'/static/css/dark.css' in render('base.html').data assert b'dark.css' in render('base.html').data
assert b'/static/css/light.css' not in render('base.html').data assert b'light.css' not in render('base.html').data
def test_render_with_user_light_theme(app): def test_render_with_user_light_theme(app):
"""Test that a request with the light theme selected renders the light theme.""" """Test that a request with the light theme selected renders the light theme."""
with app.test_request_context(): with app.test_request_context():
assert b'/static/css/light.css' in render('base.html').data assert b'light.css' in render('base.html').data
assert b'/static/css/dark.css' not in render('base.html').data assert b'dark.css' not in render('base.html').data
def test_render_with_no_user_theme(app): def test_render_with_no_user_theme(app):
"""Test that a request with no theme set renders the light theme.""" """Test that a request with no theme set renders the light theme."""
with app.test_request_context(): with app.test_request_context():
assert b'/static/css/light.css' in render('base.html').data assert b'light.css' in render('base.html').data
assert b'/static/css/dark.css' not in render('base.html').data assert b'dark.css' not in render('base.html').data
def test_render_with_theme_defaults_affects_html(app):
"""Test that the base themes are all that's presented in the HTML."""
# test we can remove stuff from the default
with app.test_request_context():
assert b'?style=light' in render('base.html').data
assert b'?style=dark' in render('base.html').data
assert b'?style=plain' in render('base.html').data
def test_render_with_theme_overrides_affects_html(app):
"""Test that the overridden themes are presented in the HTML."""
# test we can remove stuff from the default
restyled_app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'PAGE_STYLES': {'light': '/static/css/light.css'}})
with restyled_app.test_request_context():
assert b'?style=light' in render('base.html').data
assert b'?style=dark' not in render('base.html').data
assert b'?style=plain' not in render('base.html').data
# test that we can add new stuff too/instead
restyled_app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'PAGE_STYLES': {'cool': '/static/css/cool.css',
'warm': '/static/css/warm.css'},
'DEFAULT_PAGE_STYLE': 'warm'})
with restyled_app.test_request_context():
assert b'?style=cool' in render('base.html').data
assert b'?style=warm' in render('base.html').data
def test_render_with_theme_overrides(app):
"""Test that the loaded themes can be overridden from the default."""
cookie = dump_cookie("user-style", 'cool')
restyled_app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'PAGE_STYLES': {'cool': '/static/css/cool.css',
'warm': '/static/css/warm.css'}})
with restyled_app.test_request_context(headers={'COOKIE': cookie}):
assert b'/static/css/cool.css' in render('base.html').data
assert b'/static/css/warm.css' not in render('base.html').data
def test_render_with_theme_overrides_not_found_is_default(app):
"""Test that theme overrides work, and if a requested theme doesn't exist, the default is loaded."""
cookie = dump_cookie("user-style", 'nonexistent')
restyled_app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'PAGE_STYLES': {'cool': '/static/css/cool.css',
'warm': '/static/css/warm.css'},
'DEFAULT_PAGE_STYLE': 'warm'})
with restyled_app.test_request_context(headers={'COOKIE': cookie}):
assert b'/static/css/warm.css' in render('base.html').data
assert b'/static/css/nonexistent.css' not in render('base.html').data
def test_request_path_to_instance_resource_path(app): def test_request_path_to_instance_resource_path(app):

View File

@ -56,10 +56,9 @@ commands =
# run security checks # run security checks
# #
# again it seems the most valuable here to run against the packaged code # again it seems the most valuable here to run against the packaged code
# 51457 is nearly a red herring that I'm stuck with because tox is pinned, try removing occasionally
commands = commands =
bandit {envsitepackagesdir}/incorporealcms/ -r bandit {envsitepackagesdir}/incorporealcms/ -r
safety check -r requirements/requirements-dev.txt -i 51457 safety check -r requirements/requirements-dev.txt
[testenv:lint] [testenv:lint]
# run style checks # run style checks