Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
55cfad90a9
|
|||
|
b3dfab2611
|
|||
|
715bc38d78
|
|||
|
e9af2de21e
|
|||
|
83bc8b2c21
|
|||
|
4a2f650a33
|
|||
|
fd0fb390ff
|
|||
|
be8a8dd35a
|
|||
|
0f19fcb174
|
|||
|
f1684a57a9
|
|||
|
83eb464be9
|
|||
|
0f03ad6f38
|
|||
|
21ea24ffa1
|
|||
|
724a2240b2
|
|||
|
aa6a27dd8b
|
|||
|
c80172cffd
|
|||
|
89ea2fb87e
|
|||
|
8ac5b25208
|
|||
|
54b953f5ed
|
|||
|
de0641b08f
|
|||
|
cc3e311738
|
@@ -1,7 +1,10 @@
|
||||
# How to Contribute
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
* 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
|
||||
@@ -27,22 +46,31 @@ prioritization.
|
||||
* 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
|
||||
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.
|
||||
* 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
|
||||
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.
|
||||
I do not request the copyright of contributions be assigned to me or to the project, and I require no
|
||||
provision that I be allowed to relicense your contributions. My personal oath is to maintain
|
||||
inbound=outbound in my open source projects, and the expectation is authors are responsible for their
|
||||
contributions.
|
||||
|
||||
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.
|
||||
I am following the *spirit* of the [Developer Certificate of Origin](https://developercertificate.org/),
|
||||
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
|
||||
methods in the event I choose to provide the project under a new license and need to contact you
|
||||
to approve the new license terms. Please note that the software is provided under the GNU AGPLv3 (or
|
||||
later).
|
||||
1. The contribution was created by you and you have the right to submit it under the open source license
|
||||
indicated in the LICENSE file; or
|
||||
2. The contribution is based upon previous work that is covered under an appropriate open source license
|
||||
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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -7,6 +7,7 @@ import re
|
||||
from flask import Blueprint, Markup, abort
|
||||
from flask import current_app as app
|
||||
from flask import redirect, request, send_from_directory
|
||||
from werkzeug.security import safe_join
|
||||
|
||||
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']
|
||||
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'
|
||||
|
||||
# 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'),
|
||||
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):
|
||||
@@ -98,15 +102,16 @@ def request_path_to_instance_resource_path(path):
|
||||
"""
|
||||
# check if the path is allowed
|
||||
base_dir = os.path.realpath(f'{app.instance_path}/pages/')
|
||||
verbatim_path = os.path.abspath(os.path.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)
|
||||
|
||||
safe_path = safe_join(base_dir, path)
|
||||
# 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)
|
||||
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
|
||||
if os.path.exists(resolved_path):
|
||||
# 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'
|
||||
|
||||
# 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)
|
||||
|
||||
# does the final file actually exist?
|
||||
|
||||
14
incorporealcms/static.py
Normal file
14
incorporealcms/static.py
Normal 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)
|
||||
@@ -12,11 +12,11 @@ body {
|
||||
}
|
||||
|
||||
.site-wrap-normal-width {
|
||||
max-width: 80pc;
|
||||
max-width: 65pc;
|
||||
}
|
||||
|
||||
.site-wrap-double-width {
|
||||
max-width: 160pc;
|
||||
max-width: 130pc;
|
||||
}
|
||||
|
||||
.site-wrap {
|
||||
@@ -28,33 +28,24 @@ body {
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
|
||||
div.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.75em;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
div.header a {
|
||||
border-bottom: none;
|
||||
font-size: 0.8em;
|
||||
padding: 1rem 1rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
div.content {
|
||||
font-size: 11pt;
|
||||
padding: 0 1em;
|
||||
padding: 0 1rem;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 70pc) {
|
||||
div.content, footer {
|
||||
margin-left: 5pc;
|
||||
margin-right: 5pc;
|
||||
}
|
||||
}
|
||||
|
||||
div.content p {
|
||||
margin: 1.25em 0;
|
||||
}
|
||||
@@ -78,6 +69,10 @@ footer {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.extra-footer {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
@@ -172,6 +167,5 @@ figcaption {
|
||||
}
|
||||
|
||||
.footnote-ref:link, .footnote-ref:visited, .footnote-ref:hover, .footnote-ref:active {
|
||||
border-bottom: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@@ -6,34 +6,32 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
background: #090909;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #EEE;
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
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;
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
color: #B31D15;
|
||||
}
|
||||
|
||||
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;
|
||||
div.header, div.header a {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
|
||||
@@ -6,34 +6,32 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
background: #F6F6F6;
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #111;
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
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;
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
color: #811610;
|
||||
}
|
||||
|
||||
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;
|
||||
div.header, div.header a {
|
||||
color: #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 %}
|
||||
@@ -31,7 +31,8 @@
|
||||
{{ content }}
|
||||
</div>
|
||||
<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>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
@@ -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,35 +1,39 @@
|
||||
#
|
||||
# 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==21.4.0
|
||||
attrs==22.2.0
|
||||
# via pytest
|
||||
bandit==1.7.4
|
||||
# via -r requirements/requirements-dev.in
|
||||
certifi==2021.10.8
|
||||
build==0.9.0
|
||||
# via pip-tools
|
||||
certifi==2022.12.7
|
||||
# via requests
|
||||
charset-normalizer==2.0.12
|
||||
charset-normalizer==2.1.1
|
||||
# via requests
|
||||
click==8.1.2
|
||||
click==8.1.3
|
||||
# via
|
||||
# flask
|
||||
# pip-tools
|
||||
# safety
|
||||
coverage[toml]==6.3.2
|
||||
coverage[toml]==7.0.1
|
||||
# via pytest-cov
|
||||
distlib==0.3.4
|
||||
distlib==0.3.6
|
||||
# via virtualenv
|
||||
dlint==0.12.0
|
||||
dlint==0.13.0
|
||||
# via -r requirements/requirements-dev.in
|
||||
dparse==0.5.1
|
||||
dparse==0.6.2
|
||||
# via safety
|
||||
filelock==3.6.0
|
||||
exceptiongroup==1.1.0
|
||||
# via pytest
|
||||
filelock==3.9.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
flake8==4.0.1
|
||||
flake8==5.0.4
|
||||
# via
|
||||
# -r requirements/requirements-dev.in
|
||||
# dlint
|
||||
@@ -40,126 +44,125 @@ flake8==4.0.1
|
||||
# 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.1.1
|
||||
flake8-isort==6.0.0
|
||||
# via -r requirements/requirements-dev.in
|
||||
flake8-logging-format==0.6.0
|
||||
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.1.1
|
||||
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.3
|
||||
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
|
||||
jinja2==3.1.1
|
||||
jinja2==3.1.2
|
||||
# via flask
|
||||
markdown==3.3.6
|
||||
markdown==3.4.1
|
||||
# via -r requirements/requirements.in
|
||||
markupsafe==2.1.1
|
||||
# via jinja2
|
||||
mccabe==0.6.1
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
mccabe==0.7.0
|
||||
# via flake8
|
||||
packaging==21.3
|
||||
# via
|
||||
# build
|
||||
# dparse
|
||||
# pytest
|
||||
# safety
|
||||
# tox
|
||||
pbr==5.8.1
|
||||
pbr==5.11.0
|
||||
# via stevedore
|
||||
pep517==0.12.0
|
||||
# via pip-tools
|
||||
pip-tools==6.6.0
|
||||
pep517==0.13.0
|
||||
# via build
|
||||
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
|
||||
pycodestyle==2.8.0
|
||||
# via tox
|
||||
pycodestyle==2.9.1
|
||||
# via flake8
|
||||
pydocstyle==6.1.1
|
||||
# via flake8-docstrings
|
||||
pydot==1.4.2
|
||||
# via -r requirements/requirements-dev.in
|
||||
pyflakes==2.4.0
|
||||
pyflakes==2.5.0
|
||||
# via flake8
|
||||
pyparsing==3.0.8
|
||||
pyparsing==3.0.9
|
||||
# via
|
||||
# packaging
|
||||
# pydot
|
||||
pytest==7.1.1
|
||||
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
|
||||
# dparse
|
||||
requests==2.27.1
|
||||
# via bandit
|
||||
requests==2.28.1
|
||||
# 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
|
||||
six==1.16.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
# via tox
|
||||
smmap==5.0.0
|
||||
# via gitdb
|
||||
snowballstemmer==2.2.0
|
||||
# via pydocstyle
|
||||
stevedore==3.5.0
|
||||
stevedore==4.1.1
|
||||
# via bandit
|
||||
testfixtures==6.18.5
|
||||
# via flake8-isort
|
||||
toml==0.10.2
|
||||
# via
|
||||
# dparse
|
||||
# tox
|
||||
# via dparse
|
||||
tomli==2.0.1
|
||||
# via
|
||||
# build
|
||||
# coverage
|
||||
# pep517
|
||||
# pytest
|
||||
tox==3.25.0
|
||||
# tox
|
||||
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.9
|
||||
urllib3==1.26.13
|
||||
# via requests
|
||||
versioneer==0.22
|
||||
versioneer==0.28
|
||||
# via -r requirements/requirements-dev.in
|
||||
virtualenv==20.14.1
|
||||
virtualenv==20.17.1
|
||||
# via tox
|
||||
werkzeug==2.1.1
|
||||
werkzeug==2.2.2
|
||||
# via flask
|
||||
wheel==0.37.1
|
||||
wheel==0.38.4
|
||||
# via
|
||||
# pip-tools
|
||||
# tox-wheel
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
#
|
||||
# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in
|
||||
#
|
||||
click==8.1.2
|
||||
click==8.1.3
|
||||
# via flask
|
||||
flask==2.1.1
|
||||
flask==2.2.2
|
||||
# via -r requirements/requirements.in
|
||||
itsdangerous==2.1.2
|
||||
# via flask
|
||||
jinja2==3.1.1
|
||||
jinja2==3.1.2
|
||||
# via flask
|
||||
markdown==3.3.6
|
||||
markdown==3.4.1
|
||||
# via -r requirements/requirements.in
|
||||
markupsafe==2.1.1
|
||||
# via jinja2
|
||||
werkzeug==2.1.1
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
werkzeug==2.2.2
|
||||
# via flask
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -210,3 +210,31 @@ def test_pages_can_supply_alternate_templates(client):
|
||||
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-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
|
||||
|
||||
3
tests/instance/custom-static/css/warm.css
Normal file
3
tests/instance/custom-static/css/warm.css
Normal file
@@ -0,0 +1,3 @@
|
||||
* {
|
||||
color: red;
|
||||
}
|
||||
6
tests/instance/pages/index-but-with-footer.md
Normal file
6
tests/instance/pages/index-but-with-footer.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Title: Index
|
||||
Footer: ooo <a href="a">a</a>
|
||||
|
||||
# test index
|
||||
|
||||
this is some test content
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user