Compare commits

...

7 Commits

Author SHA1 Message Date
Brian S. Stephan b3dfab2611
simplify and better standardize the link underline 2022-12-31 11:33:36 -06:00
Brian S. Stephan 715bc38d78
serve per-instance static files at custom-static/ 2022-12-31 10:51:36 -06:00
Brian S. Stephan e9af2de21e
don't assume all styles are in the static directory
this is to make room for a second, instance-configured spot for them
2022-12-31 10:16:35 -06:00
Brian S. Stephan 83bc8b2c21
requirements bump, only affected dev tools 2022-12-31 10:13:20 -06:00
Brian S. Stephan 4a2f650a33
don't hardcode styles to present, use config
now that we can override the styles in practice, we also need to only
present what is possible in the HTML
2022-12-31 09:53:22 -06:00
Brian S. Stephan fd0fb390ff
allow for overriding PAGE_STYLES
moving this allows for per-instance customizations later, but that won't
be practical until serving styles from the instance dir is also allowed.
but, this sets the ground work and does allow for removing some styles
(e.g. if someone wanted to only allow 'plain').

also I still need to add the ability to present the themes list dynamically
2022-12-31 09:40:13 -06:00
Brian S. Stephan be8a8dd35a
test full path for stylesheets
I'm going to be screwing around with this code in some future commits so
it's better to be explicit
2022-12-31 09:02:57 -06:00
14 changed files with 158 additions and 76 deletions

View File

@ -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)

View File

@ -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'

View File

@ -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:

14
incorporealcms/static.py Normal file
View File

@ -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)

View File

@ -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;
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 %}

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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):

View File

@ -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