Compare commits
7 Commits
0f19fcb174
...
b3dfab2611
Author | SHA1 | Date |
---|---|---|
Brian S. Stephan | b3dfab2611 | |
Brian S. Stephan | 715bc38d78 | |
Brian S. Stephan | e9af2de21e | |
Brian S. Stephan | 83bc8b2c21 | |
Brian S. Stephan | 4a2f650a33 | |
Brian S. Stephan | fd0fb390ff | |
Brian S. Stephan | be8a8dd35a |
|
@ -40,8 +40,9 @@ def create_app(instance_path=None, test_config=None):
|
|||
logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status)
|
||||
return response
|
||||
|
||||
from . import error_pages, pages
|
||||
from . import error_pages, pages, static
|
||||
app.register_blueprint(pages.bp)
|
||||
app.register_blueprint(static.bp)
|
||||
app.register_error_handler(400, error_pages.bad_request)
|
||||
app.register_error_handler(404, error_pages.page_not_found)
|
||||
app.register_error_handler(500, error_pages.internal_server_error)
|
||||
|
|
|
@ -50,6 +50,12 @@ class Config(object):
|
|||
MEDIA_DIR = 'media'
|
||||
|
||||
# customizations
|
||||
PAGE_STYLES = {
|
||||
'dark': '/static/css/dark.css',
|
||||
'light': '/static/css/light.css',
|
||||
'plain': '/static/css/plain.css',
|
||||
}
|
||||
|
||||
DEFAULT_PAGE_STYLE = 'light'
|
||||
TITLE_SUFFIX = 'example.com'
|
||||
CONTACT_EMAIL = 'admin@example.com'
|
||||
|
|
|
@ -31,19 +31,15 @@ def render(template_name_or_list, **context):
|
|||
|
||||
* Determine the proper site theme to use in the template and provide it.
|
||||
"""
|
||||
PAGE_STYLES = {
|
||||
'dark': 'css/dark.css',
|
||||
'light': 'css/light.css',
|
||||
'plain': 'css/plain.css',
|
||||
}
|
||||
|
||||
page_styles = app.config['PAGE_STYLES']
|
||||
selected_style = request.args.get('style', None)
|
||||
if selected_style:
|
||||
user_style = selected_style
|
||||
else:
|
||||
user_style = request.cookies.get('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))
|
||||
if selected_style:
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
"""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)
|
|
@ -28,7 +28,8 @@ body {
|
|||
|
||||
a {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
|
||||
div.header {
|
||||
|
@ -39,10 +40,6 @@ div.header {
|
|||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
div.header a {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
div.content {
|
||||
font-size: 11pt;
|
||||
padding: 0 1rem;
|
||||
|
@ -170,6 +167,5 @@ figcaption {
|
|||
}
|
||||
|
||||
.footnote-ref:link, .footnote-ref:visited, .footnote-ref:hover, .footnote-ref:active {
|
||||
border-bottom: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
|
|
@ -15,17 +15,14 @@ h1, h2, h3, h4, h5, h6 {
|
|||
|
||||
p a, ul a, ol a {
|
||||
color: #DDD;
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #999;
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
p a:hover, ul a:hover, ol a:hover, footer a:hover {
|
||||
color: #B31D15;
|
||||
border-bottom: 1px solid #B31D15;
|
||||
}
|
||||
|
||||
div.site-wrap {
|
||||
|
@ -34,10 +31,7 @@ div.site-wrap {
|
|||
|
||||
div.header, div.header a {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
div.header a:hover, div.header a:active {
|
||||
border-bottom: 1px solid #555;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
|
|
|
@ -15,17 +15,14 @@ h1, h2, h3, h4, h5, h6 {
|
|||
|
||||
p a, ul a, ol a {
|
||||
color: #222;
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #999;
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
p a:hover, ul a:hover, ol a:hover, footer a:hover {
|
||||
color: #811610;
|
||||
border-bottom: 1px solid #811610;
|
||||
}
|
||||
|
||||
div.site-wrap {
|
||||
|
@ -34,10 +31,7 @@ div.site-wrap {
|
|||
|
||||
div.header, div.header a {
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
div.header a:hover, div.header a:active {
|
||||
border-bottom: 1px solid #AAA;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<meta property="og:url" content="{{ base_url }}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename=user_style) }}">
|
||||
<link rel="stylesheet" href="{{ user_style }}">
|
||||
<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 %}>
|
||||
|
@ -20,9 +20,9 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
<div class="styles">
|
||||
<a href="?style=dark">[dark]</a>
|
||||
<a href="?style=light">[light]</a>
|
||||
<a href="?style=plain">[plain]</a>
|
||||
{% for style in page_styles %}
|
||||
<a href="?style={{ style }}">[{{ style }}]</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -21,6 +21,6 @@ safety # check requirements file for issues
|
|||
|
||||
# maintenance utilities and tox
|
||||
pip-tools # pip-compile
|
||||
tox # CI stuff
|
||||
tox<4 # CI stuff, pinned for now to avoid packaging conflict w/safety
|
||||
tox-wheel # build wheels in tox
|
||||
versioneer # automatic version numbering
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
#
|
||||
# This file is autogenerated by pip-compile with python 3.10
|
||||
# To update, run:
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in
|
||||
#
|
||||
attrs==22.1.0
|
||||
attrs==22.2.0
|
||||
# via pytest
|
||||
bandit==1.7.4
|
||||
# via -r requirements/requirements-dev.in
|
||||
build==0.8.0
|
||||
build==0.9.0
|
||||
# via pip-tools
|
||||
certifi==2022.9.14
|
||||
certifi==2022.12.7
|
||||
# via requests
|
||||
charset-normalizer==2.1.1
|
||||
# via requests
|
||||
|
@ -19,15 +19,17 @@ click==8.1.3
|
|||
# flask
|
||||
# pip-tools
|
||||
# safety
|
||||
coverage[toml]==6.4.4
|
||||
coverage[toml]==7.0.1
|
||||
# via pytest-cov
|
||||
distlib==0.3.6
|
||||
# via virtualenv
|
||||
dlint==0.13.0
|
||||
# via -r requirements/requirements-dev.in
|
||||
dparse==0.6.0
|
||||
dparse==0.6.2
|
||||
# via safety
|
||||
filelock==3.8.0
|
||||
exceptiongroup==1.1.0
|
||||
# via pytest
|
||||
filelock==3.9.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
|
@ -42,31 +44,31 @@ flake8==5.0.4
|
|||
# flake8-mutable
|
||||
flake8-blind-except==0.2.1
|
||||
# via -r requirements/requirements-dev.in
|
||||
flake8-builtins==1.5.3
|
||||
flake8-builtins==2.1.0
|
||||
# via -r requirements/requirements-dev.in
|
||||
flake8-docstrings==1.6.0
|
||||
# via -r requirements/requirements-dev.in
|
||||
flake8-executable==2.1.1
|
||||
flake8-executable==2.1.2
|
||||
# via -r requirements/requirements-dev.in
|
||||
flake8-fixme==1.1.1
|
||||
# via -r requirements/requirements-dev.in
|
||||
flake8-isort==4.2.0
|
||||
flake8-isort==6.0.0
|
||||
# via -r requirements/requirements-dev.in
|
||||
flake8-logging-format==0.7.5
|
||||
flake8-logging-format==0.9.0
|
||||
# via -r requirements/requirements-dev.in
|
||||
flake8-mutable==1.2.0
|
||||
# via -r requirements/requirements-dev.in
|
||||
flask==2.2.2
|
||||
# via -r requirements/requirements.in
|
||||
gitdb==4.0.9
|
||||
gitdb==4.0.10
|
||||
# via gitpython
|
||||
gitpython==3.1.27
|
||||
gitpython==3.1.30
|
||||
# via bandit
|
||||
idna==3.4
|
||||
# via requests
|
||||
iniconfig==1.1.1
|
||||
# via pytest
|
||||
isort==5.10.1
|
||||
isort==5.11.4
|
||||
# via flake8-isort
|
||||
itsdangerous==2.1.2
|
||||
# via flask
|
||||
|
@ -87,22 +89,20 @@ packaging==21.3
|
|||
# pytest
|
||||
# safety
|
||||
# tox
|
||||
pbr==5.10.0
|
||||
pbr==5.11.0
|
||||
# via stevedore
|
||||
pep517==0.13.0
|
||||
# via build
|
||||
pip-tools==6.8.0
|
||||
pip-tools==6.12.1
|
||||
# via -r requirements/requirements-dev.in
|
||||
platformdirs==2.5.2
|
||||
platformdirs==2.6.2
|
||||
# via virtualenv
|
||||
pluggy==1.0.0
|
||||
# via
|
||||
# pytest
|
||||
# tox
|
||||
py==1.11.0
|
||||
# via
|
||||
# pytest
|
||||
# tox
|
||||
# via tox
|
||||
pycodestyle==2.9.1
|
||||
# via flake8
|
||||
pydocstyle==6.1.1
|
||||
|
@ -115,11 +115,11 @@ pyparsing==3.0.9
|
|||
# via
|
||||
# packaging
|
||||
# pydot
|
||||
pytest==7.1.3
|
||||
pytest==7.2.0
|
||||
# via
|
||||
# -r requirements/requirements-dev.in
|
||||
# pytest-cov
|
||||
pytest-cov==3.0.0
|
||||
pytest-cov==4.0.0
|
||||
# via -r requirements/requirements-dev.in
|
||||
pyyaml==6.0
|
||||
# via bandit
|
||||
|
@ -127,9 +127,9 @@ requests==2.28.1
|
|||
# via safety
|
||||
ruamel-yaml==0.17.21
|
||||
# via safety
|
||||
ruamel-yaml-clib==0.2.6
|
||||
ruamel-yaml-clib==0.2.7
|
||||
# via ruamel-yaml
|
||||
safety==2.1.1
|
||||
safety==2.3.5
|
||||
# via -r requirements/requirements-dev.in
|
||||
six==1.16.0
|
||||
# via tox
|
||||
|
@ -137,7 +137,7 @@ smmap==5.0.0
|
|||
# via gitdb
|
||||
snowballstemmer==2.2.0
|
||||
# via pydocstyle
|
||||
stevedore==4.0.0
|
||||
stevedore==4.1.1
|
||||
# via bandit
|
||||
toml==0.10.2
|
||||
# via dparse
|
||||
|
@ -148,21 +148,21 @@ tomli==2.0.1
|
|||
# pep517
|
||||
# pytest
|
||||
# tox
|
||||
tox==3.26.0
|
||||
tox==3.28.0
|
||||
# via
|
||||
# -r requirements/requirements-dev.in
|
||||
# tox-wheel
|
||||
tox-wheel==0.7.0
|
||||
tox-wheel==1.0.0
|
||||
# via -r requirements/requirements-dev.in
|
||||
urllib3==1.26.12
|
||||
urllib3==1.26.13
|
||||
# via requests
|
||||
versioneer==0.26
|
||||
versioneer==0.28
|
||||
# via -r requirements/requirements-dev.in
|
||||
virtualenv==20.16.5
|
||||
virtualenv==20.17.1
|
||||
# via tox
|
||||
werkzeug==2.2.2
|
||||
# via flask
|
||||
wheel==0.37.1
|
||||
wheel==0.38.4
|
||||
# via
|
||||
# pip-tools
|
||||
# tox-wheel
|
||||
|
|
|
@ -190,15 +190,15 @@ def test_setting_selected_style_includes_cookie(client):
|
|||
response = client.get('/?style=light')
|
||||
style_cookie = next((cookie for cookie in client.cookie_jar if cookie.name == 'user-style'), None)
|
||||
assert response.status_code == 200
|
||||
assert b'light.css' in response.data
|
||||
assert b'dark.css' not in response.data
|
||||
assert b'/static/css/light.css' in response.data
|
||||
assert b'/static/css/dark.css' not in response.data
|
||||
assert style_cookie.value == 'light'
|
||||
|
||||
response = client.get('/?style=dark')
|
||||
style_cookie = next((cookie for cookie in client.cookie_jar if cookie.name == 'user-style'), None)
|
||||
assert response.status_code == 200
|
||||
assert b'dark.css' in response.data
|
||||
assert b'light.css' not in response.data
|
||||
assert b'/static/css/dark.css' in response.data
|
||||
assert b'/static/css/light.css' not in response.data
|
||||
assert style_cookie.value == 'dark'
|
||||
|
||||
|
||||
|
@ -218,3 +218,23 @@ def test_extra_footer_per_page(client):
|
|||
assert b'<div class="extra-footer">' not in response.data
|
||||
response = client.get('/index-but-with-footer')
|
||||
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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
* {
|
||||
color: red;
|
||||
}
|
|
@ -1,10 +1,15 @@
|
|||
"""Unit test helper methods."""
|
||||
import os
|
||||
|
||||
import pytest
|
||||
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,
|
||||
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):
|
||||
"""Test that the index page has navs to the root (itself)."""
|
||||
|
@ -49,22 +54,74 @@ def test_render_with_user_dark_theme(app):
|
|||
"""Test that a request with the dark theme selected renders the dark theme."""
|
||||
cookie = dump_cookie("user-style", 'dark')
|
||||
with app.test_request_context(headers={'COOKIE': cookie}):
|
||||
assert b'dark.css' in render('base.html').data
|
||||
assert b'light.css' not in render('base.html').data
|
||||
assert b'/static/css/dark.css' in render('base.html').data
|
||||
assert b'/static/css/light.css' not in render('base.html').data
|
||||
|
||||
|
||||
def test_render_with_user_light_theme(app):
|
||||
"""Test that a request with the light theme selected renders the light theme."""
|
||||
with app.test_request_context():
|
||||
assert b'light.css' in render('base.html').data
|
||||
assert b'dark.css' not in render('base.html').data
|
||||
assert b'/static/css/light.css' in render('base.html').data
|
||||
assert b'/static/css/dark.css' not in render('base.html').data
|
||||
|
||||
|
||||
def test_render_with_no_user_theme(app):
|
||||
"""Test that a request with no theme set renders the light theme."""
|
||||
with app.test_request_context():
|
||||
assert b'light.css' in render('base.html').data
|
||||
assert b'dark.css' not in render('base.html').data
|
||||
assert b'/static/css/light.css' in render('base.html').data
|
||||
assert b'/static/css/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):
|
||||
|
|
3
tox.ini
3
tox.ini
|
@ -56,9 +56,10 @@ commands =
|
|||
# run security checks
|
||||
#
|
||||
# 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 =
|
||||
bandit {envsitepackagesdir}/incorporealcms/ -r
|
||||
safety check -r requirements/requirements-dev.txt
|
||||
safety check -r requirements/requirements-dev.txt -i 51457
|
||||
|
||||
[testenv:lint]
|
||||
# run style checks
|
||||
|
|
Loading…
Reference in New Issue