21 Commits

Author SHA1 Message Date
55cfad90a9 use werkzeug safe_join to sanitize the requested path
no tests changed, so my implementation might have been good, but let's
use the provided check
2022-12-31 11:53:14 -06:00
b3dfab2611 simplify and better standardize the link underline 2022-12-31 11:33:36 -06:00
715bc38d78 serve per-instance static files at custom-static/ 2022-12-31 10:51:36 -06:00
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
83bc8b2c21 requirements bump, only affected dev tools 2022-12-31 10:13:20 -06:00
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
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
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
0f19fcb174 fix bad copy and paste job on link styles 2022-09-16 14:16:13 -05:00
f1684a57a9 requirements recompile 2022-09-16 13:49:57 -05:00
83eb464be9 style the potential for links in the footer 2022-09-16 13:40:23 -05:00
0f03ad6f38 allow pages to supply extra footer text 2022-09-16 13:35:40 -05:00
21ea24ffa1 header style tweaks, deemphasizing it a bit 2022-06-05 21:30:49 -05:00
724a2240b2 requirements bump for latest release 2022-05-25 07:24:03 -05:00
aa6a27dd8b make the header bigger, and align header and content padding better 2022-05-17 07:57:23 -05:00
c80172cffd go back to red headers and links as normal-colored text
the new way to do the links without adding links to images is probably
dumb and/or missing some stuff, but it works and does what I want, and I
think I like the old look of the colors better, so time to try it and
see if it sticks still
2022-05-17 07:57:23 -05:00
89ea2fb87e give the header nav links an underline on hover 2022-05-17 07:57:23 -05:00
8ac5b25208 get rid of the slight recoloring of bold text 2022-05-17 07:57:23 -05:00
54b953f5ed go back to the old, balanced width alignments 2022-05-17 07:57:23 -05:00
de0641b08f tweak the two-tone backgrounds and whitespace up the header 2022-05-17 07:57:23 -05:00
cc3e311738 clarify my DCO-like stance, and provide cloning info 2022-05-17 07:52:58 -05:00
18 changed files with 313 additions and 172 deletions

View File

@@ -1,7 +1,10 @@
# How to Contribute # How to Contribute
incorporeal-cms is a personal project seeking to implement a simpler, cleaner form of what would incorporeal-cms is a personal project seeking to implement a simpler, cleaner form of what would
commonly be called a "CMS". I appreciate any help in making incorporeal-cms better. commonly be called a "CMS". I appreciate any help in making it better.
incorporeal-cms is made available under the GNU Affero General Public License version 3, or any
later version.
## Opening Issues ## Opening Issues
@@ -10,8 +13,24 @@ Issues should be posted to my Gitea instance at
recommend starting the title with "Improvement:", "Bug:", or similar, so I can do a high level of recommend starting the title with "Improvement:", "Bug:", or similar, so I can do a high level of
prioritization. prioritization.
## Guidelines for Patches, etc. ## Contributions
I don't expect contributors to sign up for my personal Gitea in order to send contributions, but it
of course makes it easier. If you wish to go this route, please sign up at
<https://git.incorporeal.org/bss/incorporeal-cms> and fork the project. People planning on
contributing often are also welcome to request access to the project directly.
Otherwise, contact me via any means you know to reach me at, or <bss@incorporeal.org>, to discuss
your change and to tell me how to pull your changes.
### Guidelines for Patches, etc.
* Cloning
* Clone the project. I would advise using a pull-based workflow where I have access to the hosted
repository --- using my Gitea, cloning to a public GitHub, etc. --- rather than doing this over
email, but that works too if we must.
* Make your contributions in a new branch, generally off of `master`.
* Send me a pull request when you're ready, and we'll go through a code review.
* Code: * Code:
* Keep in mind that I strive for simplicity in the software. It serves files and renders * Keep in mind that I strive for simplicity in the software. It serves files and renders
Markdown, that's pretty much it. Features around that function are good; otherwise, I need Markdown, that's pretty much it. Features around that function are good; otherwise, I need
@@ -27,22 +46,31 @@ prioritization.
* Squash tiny commits if you'd like. I prefer commits that make one atomic conceptual change * Squash tiny commits if you'd like. I prefer commits that make one atomic conceptual change
that doesn't affect the rest of the code, assembling multiple of those commits into larger that doesn't affect the rest of the code, assembling multiple of those commits into larger
changes. changes.
* Follow something like [Chris Beams'](https://chris.beams.io/posts/git-commit/) post on * Follow something like [Chris Beams's post](https://chris.beams.io/posts/git-commit/) on
formatting a good commit message. formatting a good commit message.
* Please make sure your Author contact information is stable, in case I need to reach you.
* Consider cryptographically signing (`git commit -S`) your commits.
## Contributions ### Custody of Contributions
I don't expect contributors to sign up for my personal Gitea in order to send contributions, but it I do not request the copyright of contributions be assigned to me or to the project, and I require no
of course makes it easier. If you wish to go this route, please sign up at provision that I be allowed to relicense your contributions. My personal oath is to maintain
<https://git.incorporeal.org/bss/incorporeal-cms> and fork the project. People planning on inbound=outbound in my open source projects, and the expectation is authors are responsible for their
contributing often are also welcome to request access to the project directly. contributions.
Otherwise, contact me via any means you know to reach me at, or <bss@incorporeal.org>, to discuss I am following the *spirit* of the [Developer Certificate of Origin](https://developercertificate.org/),
your change and to tell me how to pull your changes. but in a simplified fashion:
### Copyright of Contributions By making a contribution to this project, you certify that:
Accepted changes remain the copyright of the original author, but please include appropriate contact 1. The contribution was created by you and you have the right to submit it under the open source license
methods in the event I choose to provide the project under a new license and need to contact you indicated in the LICENSE file; or
to approve the new license terms. Please note that the software is provided under the GNU AGPLv3 (or 2. The contribution is based upon previous work that is covered under an appropriate open source license
later). compatible with the license indicated in the LICENSE file, and you have the right to contribute that
work with or without modifications, under the terms of that same open source license; or
3. The contribution was provided directly to you by some other person who certified points 1, 2, or 3, and
you have not modified it.
In the event of point 3, your commit **must** include the Signed-off-by line(s) as a chain of custody,
via `git commit -s`. For points 1 and 2, your commit with accurate Author information doubles as direct
custody.

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) logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status)
return response return response
from . import error_pages, pages from . import error_pages, pages, static
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,6 +50,12 @@ 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,19 +31,15 @@ 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 = { page_styles = app.config['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

@@ -7,6 +7,7 @@ import re
from flask import Blueprint, Markup, abort from flask import Blueprint, Markup, abort
from flask import current_app as app from flask import current_app as app
from flask import redirect, request, send_from_directory from flask import redirect, request, send_from_directory
from werkzeug.security import safe_join
from incorporealcms.lib import get_meta_str, init_md, render from incorporealcms.lib import get_meta_str, init_md, render
@@ -76,6 +77,8 @@ def handle_markdown_file_path(resolved_path):
page_title = f'{page_name} - {app.config["TITLE_SUFFIX"]}' if page_name else app.config['TITLE_SUFFIX'] page_title = f'{page_name} - {app.config["TITLE_SUFFIX"]}' if page_name else app.config['TITLE_SUFFIX']
logger.debug("title (potentially derived): %s", page_title) logger.debug("title (potentially derived): %s", page_title)
extra_footer = get_meta_str(md, 'footer') if md.Meta.get('footer') else None
template = get_meta_str(md, 'template') if md.Meta.get('template') else 'base.html' template = get_meta_str(md, 'template') if md.Meta.get('template') else 'base.html'
# check if this has a HTTP redirect # check if this has a HTTP redirect
@@ -86,7 +89,8 @@ def handle_markdown_file_path(resolved_path):
return render(template, title=page_title, description=get_meta_str(md, 'description'), return render(template, title=page_title, description=get_meta_str(md, 'description'),
image=get_meta_str(md, 'image'), base_url=request.base_url, content=content, image=get_meta_str(md, 'image'), base_url=request.base_url, content=content,
navs=parent_navs, mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z')) navs=parent_navs, mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'),
extra_footer=extra_footer)
def request_path_to_instance_resource_path(path): def request_path_to_instance_resource_path(path):
@@ -98,15 +102,16 @@ def request_path_to_instance_resource_path(path):
""" """
# check if the path is allowed # check if the path is allowed
base_dir = os.path.realpath(f'{app.instance_path}/pages/') base_dir = os.path.realpath(f'{app.instance_path}/pages/')
verbatim_path = os.path.abspath(os.path.join(base_dir, path)) safe_path = safe_join(base_dir, path)
resolved_path = os.path.realpath(verbatim_path)
logger.debug("base_dir '%s', constructed resolved_path '%s' for path '%s'", base_dir, resolved_path, path)
# bail if the requested real path isn't inside the base directory # bail if the requested real path isn't inside the base directory
if base_dir != os.path.commonpath((base_dir, resolved_path)): if not safe_path:
logger.warning("client tried to request a path '%s' outside of the base_dir!", path) logger.warning("client tried to request a path '%s' outside of the base_dir!", path)
raise PermissionError raise PermissionError
verbatim_path = os.path.abspath(safe_path)
resolved_path = os.path.realpath(verbatim_path)
logger.debug("base_dir '%s', constructed resolved_path '%s' for path '%s'", base_dir, resolved_path, path)
# see if we have a real file or if we should infer markdown rendering # see if we have a real file or if we should infer markdown rendering
if os.path.exists(resolved_path): if os.path.exists(resolved_path):
# if this is a file-like request but actually a directory, redirect the user # if this is a file-like request but actually a directory, redirect the user
@@ -128,7 +133,7 @@ def request_path_to_instance_resource_path(path):
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'file' return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'file'
# if we're here, this isn't direct file access, so try markdown inference # if we're here, this isn't direct file access, so try markdown inference
verbatim_path = os.path.abspath(os.path.join(base_dir, f'{path}.md')) verbatim_path = f'{safe_path}.md'
resolved_path = os.path.realpath(verbatim_path) resolved_path = os.path.realpath(verbatim_path)
# does the final file actually exist? # does the final file actually exist?

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

@@ -12,11 +12,11 @@ body {
} }
.site-wrap-normal-width { .site-wrap-normal-width {
max-width: 80pc; max-width: 65pc;
} }
.site-wrap-double-width { .site-wrap-double-width {
max-width: 160pc; max-width: 130pc;
} }
.site-wrap { .site-wrap {
@@ -28,33 +28,24 @@ body {
a { a {
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration-line: underline;
text-decoration-thickness: 1px;
} }
div.header { div.header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-size: 0.75em; font-size: 0.8em;
padding: 0.5em 1em; padding: 1rem 1rem;
} padding-bottom: 0;
div.header a {
border-bottom: none;
} }
div.content { div.content {
font-size: 11pt; font-size: 11pt;
padding: 0 1em; padding: 0 1rem;
line-height: 1.6em; line-height: 1.6em;
} }
@media only screen and (min-width: 70pc) {
div.content, footer {
margin-left: 5pc;
margin-right: 5pc;
}
}
div.content p { div.content p {
margin: 1.25em 0; margin: 1.25em 0;
} }
@@ -78,6 +69,10 @@ footer {
margin-top: 30px; margin-top: 30px;
} }
.extra-footer {
margin-bottom: 5px;
}
table { table {
border-collapse: collapse; border-collapse: collapse;
} }
@@ -172,6 +167,5 @@ 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

@@ -6,34 +6,32 @@ html {
} }
body { body {
background: #090909; background: #111;
} }
strong { h1, h2, h3, h4, h5, h6 {
color: #EEE; color: #B31D15;
} }
.site-wrap { p a, ul a, ol a {
color: #DDD;
}
footer a {
color: #999;
}
p a:hover, ul a:hover, ol a:hover, footer a:hover {
color: #B31D15;
}
div.site-wrap {
background: black; background: black;
} }
a:link, a:visited { div.header, div.header a {
color: #B31D15; color: #555;
} text-decoration: none;
a:hover, a:active {
color: #B31D15;
border-bottom: 1px solid #B31D15;
}
div.header {
background: #222;
border-bottom: 1px solid #222;
color: #BBB;
}
div.header a {
color: #BBB;
} }
table, th, td { table, th, td {

View File

@@ -6,34 +6,32 @@ html {
} }
body { body {
background: #F6F6F6; background: #EEE;
} }
strong { h1, h2, h3, h4, h5, h6 {
color: #111; color: #811610;
} }
.site-wrap { p a, ul a, ol a {
color: #222;
}
footer a {
color: #999;
}
p a:hover, ul a:hover, ol a:hover, footer a:hover {
color: #811610;
}
div.site-wrap {
background: white; background: white;
} }
a:link, a:visited { div.header, div.header a {
color: #811610; color: #AAA;
} text-decoration: none;
a:hover, a:active {
color: #811610;
border-bottom: 1px solid #B31D15;
}
div.header {
background: #DDD;
border-bottom: 1px solid #DDD;
color: #444;
}
div.header a {
color: #444;
} }
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="{{ 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 %}"> <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">
<a href="?style=dark">[dark]</a> {% for style in page_styles %}
<a href="?style=light">[light]</a> <a href="?style={{ style }}">[{{ style }}]</a>
<a href="?style=plain">[plain]</a> {% endfor %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
@@ -31,7 +31,8 @@
{{ content }} {{ content }}
</div> </div>
<footer> <footer>
<i>Last modified: {{ mtime }}</i> {% if extra_footer %}<div class="extra-footer"><i>{{ extra_footer|safe }}</i></div>{% endif %}
<div class="footer"><i>Last modified: {{ mtime }}</i></div>
</footer> </footer>
{% endblock %} {% endblock %}
</div> </div>

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 # CI stuff tox<4 # CI stuff, pinned for now to avoid packaging conflict w/safety
tox-wheel # build wheels in tox tox-wheel # build wheels in tox
versioneer # automatic version numbering versioneer # automatic version numbering

View File

@@ -1,35 +1,39 @@
# #
# This file is autogenerated by pip-compile with python 3.10 # This file is autogenerated by pip-compile with Python 3.10
# To update, run: # by the following command:
# #
# 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==21.4.0 attrs==22.2.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
certifi==2021.10.8 build==0.9.0
# via pip-tools
certifi==2022.12.7
# via requests # via requests
charset-normalizer==2.0.12 charset-normalizer==2.1.1
# via requests # via requests
click==8.1.2 click==8.1.3
# via # via
# flask # flask
# pip-tools # pip-tools
# safety # safety
coverage[toml]==6.3.2 coverage[toml]==7.0.1
# via pytest-cov # via pytest-cov
distlib==0.3.4 distlib==0.3.6
# via virtualenv # via virtualenv
dlint==0.12.0 dlint==0.13.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
dparse==0.5.1 dparse==0.6.2
# via safety # via safety
filelock==3.6.0 exceptiongroup==1.1.0
# via pytest
filelock==3.9.0
# via # via
# tox # tox
# virtualenv # virtualenv
flake8==4.0.1 flake8==5.0.4
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# dlint # dlint
@@ -40,126 +44,125 @@ flake8==4.0.1
# 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==1.5.3 flake8-builtins==2.1.0
# 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.1 flake8-executable==2.1.2
# 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==4.1.1 flake8-isort==6.0.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-logging-format==0.6.0 flake8-logging-format==0.9.0
# 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.1.1 flask==2.2.2
# via -r requirements/requirements.in # via -r requirements/requirements.in
gitdb==4.0.9 gitdb==4.0.10
# via gitpython # via gitpython
gitpython==3.1.27 gitpython==3.1.30
# via bandit # via bandit
idna==3.3 idna==3.4
# via requests # via requests
iniconfig==1.1.1 iniconfig==1.1.1
# via pytest # via pytest
isort==5.10.1 isort==5.11.4
# via flake8-isort # via flake8-isort
itsdangerous==2.1.2 itsdangerous==2.1.2
# via flask # via flask
jinja2==3.1.1 jinja2==3.1.2
# via flask # via flask
markdown==3.3.6 markdown==3.4.1
# via -r requirements/requirements.in # via -r requirements/requirements.in
markupsafe==2.1.1 markupsafe==2.1.1
# via jinja2 # via
mccabe==0.6.1 # jinja2
# werkzeug
mccabe==0.7.0
# via flake8 # via flake8
packaging==21.3 packaging==21.3
# via # via
# build
# dparse # dparse
# pytest # pytest
# safety # safety
# tox # tox
pbr==5.8.1 pbr==5.11.0
# via stevedore # via stevedore
pep517==0.12.0 pep517==0.13.0
# via pip-tools # via build
pip-tools==6.6.0 pip-tools==6.12.1
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
platformdirs==2.5.2 platformdirs==2.6.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 # via tox
# pytest pycodestyle==2.9.1
# tox
pycodestyle==2.8.0
# via flake8 # via flake8
pydocstyle==6.1.1 pydocstyle==6.1.1
# via flake8-docstrings # via flake8-docstrings
pydot==1.4.2 pydot==1.4.2
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
pyflakes==2.4.0 pyflakes==2.5.0
# via flake8 # via flake8
pyparsing==3.0.8 pyparsing==3.0.9
# via # via
# packaging # packaging
# pydot # pydot
pytest==7.1.1 pytest==7.2.0
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# pytest-cov # pytest-cov
pytest-cov==3.0.0 pytest-cov==4.0.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
pyyaml==6.0 pyyaml==6.0
# via # via bandit
# bandit requests==2.28.1
# dparse
requests==2.27.1
# via safety # via safety
safety==1.10.3 ruamel-yaml==0.17.21
# via safety
ruamel-yaml-clib==0.2.7
# via ruamel-yaml
safety==2.3.5
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
six==1.16.0 six==1.16.0
# via # via tox
# tox
# virtualenv
smmap==5.0.0 smmap==5.0.0
# via gitdb # via gitdb
snowballstemmer==2.2.0 snowballstemmer==2.2.0
# via pydocstyle # via pydocstyle
stevedore==3.5.0 stevedore==4.1.1
# via bandit # via bandit
testfixtures==6.18.5
# via flake8-isort
toml==0.10.2 toml==0.10.2
# via # via dparse
# dparse
# tox
tomli==2.0.1 tomli==2.0.1
# via # via
# build
# coverage # coverage
# pep517 # pep517
# pytest # pytest
tox==3.25.0 # tox
tox==3.28.0
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# tox-wheel # tox-wheel
tox-wheel==0.7.0 tox-wheel==1.0.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
urllib3==1.26.9 urllib3==1.26.13
# via requests # via requests
versioneer==0.22 versioneer==0.28
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
virtualenv==20.14.1 virtualenv==20.17.1
# via tox # via tox
werkzeug==2.1.1 werkzeug==2.2.2
# via flask # via flask
wheel==0.37.1 wheel==0.38.4
# via # via
# pip-tools # pip-tools
# tox-wheel # tox-wheel

View File

@@ -4,17 +4,19 @@
# #
# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in # pip-compile --output-file=requirements/requirements.txt requirements/requirements.in
# #
click==8.1.2 click==8.1.3
# via flask # via flask
flask==2.1.1 flask==2.2.2
# via -r requirements/requirements.in # via -r requirements/requirements.in
itsdangerous==2.1.2 itsdangerous==2.1.2
# via flask # via flask
jinja2==3.1.1 jinja2==3.1.2
# via flask # via flask
markdown==3.3.6 markdown==3.4.1
# via -r requirements/requirements.in # via -r requirements/requirements.in
markupsafe==2.1.1 markupsafe==2.1.1
# via jinja2 # via
werkzeug==2.1.1 # jinja2
# werkzeug
werkzeug==2.2.2
# via flask # via flask

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'light.css' in response.data assert b'/static/css/light.css' in response.data
assert b'dark.css' not in response.data assert b'/static/css/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'dark.css' in response.data assert b'/static/css/dark.css' in response.data
assert b'light.css' not in response.data assert b'/static/css/light.css' not in response.data
assert style_cookie.value == 'dark' assert style_cookie.value == 'dark'
@@ -210,3 +210,31 @@ def test_pages_can_supply_alternate_templates(client):
response = client.get('/custom-template') response = client.get('/custom-template')
assert b'class="site-wrap site-wrap-normal-width"' not in response.data assert b'class="site-wrap site-wrap-normal-width"' not in response.data
assert b'class="site-wrap site-wrap-double-width"' in response.data assert b'class="site-wrap site-wrap-double-width"' in response.data
def test_extra_footer_per_page(client):
"""Test that we don't include the extra-footer if there isn't one (or do if there is)."""
response = client.get('/')
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

@@ -0,0 +1,6 @@
Title: Index
Footer: ooo <a href="a">a</a>
# test index
this is some test content

View File

@@ -1,10 +1,15 @@
"""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)."""
@@ -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.""" """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'dark.css' in render('base.html').data assert b'/static/css/dark.css' in render('base.html').data
assert b'light.css' not 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): 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'light.css' in render('base.html').data assert b'/static/css/light.css' in render('base.html').data
assert b'dark.css' not 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): 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'light.css' in render('base.html').data assert b'/static/css/light.css' in render('base.html').data
assert b'dark.css' not 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): def test_request_path_to_instance_resource_path(app):

View File

@@ -56,9 +56,10 @@ 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 safety check -r requirements/requirements-dev.txt -i 51457
[testenv:lint] [testenv:lint]
# run style checks # run style checks