44 Commits

Author SHA1 Message Date
410f96ffb4 relicense under the AGPLv3+
after a lot of deliberation I think I'm starting to prefer GPLv3 over
GPLv2 for copyleft, and this is a case where my rationale benefits from
the additions of the Affero clause
2022-01-17 16:55:24 -06:00
e325831f70 some tweaks to the CONTRIBUTING section 2022-01-17 16:49:40 -06:00
56289ab75d remove warning for coverage feature not in use 2022-01-15 17:40:12 -06:00
d623c9c93d adding support for 3.10, dropping support for 3.7 2022-01-15 17:35:56 -06:00
9d87aab61b go back to lighter red in dark style
contrast is just too low for the "incorporeal red"
2022-01-03 12:40:30 -06:00
35ea94185b unify some of the colors in light vs. dark 2022-01-02 22:55:43 -06:00
69feb0c29c add a 75% max-width class, using it for GOTY? 2022-01-02 22:51:21 -06:00
788a9cbaba use a smaller font for the styled pages 2022-01-02 22:50:04 -06:00
be6d96273c eliminate warnings about how we register the pydot markdown extension 2021-11-03 14:16:18 -05:00
a700470067 document the customization options
more work towards #15
2021-11-01 23:36:10 -05:00
8a62167cea remove some self-specific stuff from settings
implements most, if not all, of #15
2021-11-01 23:27:00 -05:00
4ea824e86f provide some comments explaining the css files 2021-11-01 23:20:42 -05:00
28dbfd45b5 remove bss-specific image from the package
part of the work of #15
2021-11-01 23:20:16 -05:00
1de69dfc70 requirements bump 2021-10-08 07:30:46 -05:00
ccf8434f43 remove a bunch of unnecessary font size styling
in the end, a lot of this doesn't really matter, and right now, I
slightly prefer the site with a touch bigger font, so let's just let the
default do its thing
2021-10-08 07:16:31 -05:00
5aabb79199 call the pydot rendering support 'dot' in package extras 2021-10-08 07:09:21 -05:00
509072ab78 safety in tox: scan requirements for known bad packages 2021-06-24 11:46:56 -05:00
e61c55bed2 handle graphviz parsing errors more cleanly 2021-06-24 11:37:57 -05:00
41a53a2a13 add py39 environment to tox test envs 2021-06-24 11:23:36 -05:00
da055acda6 provide markdown extension to render graphviz
this is server side, and a more standard format, and thus I like it more
than mermaid, which I've been using at work. but, I really wanted a
server-side option (see my manifesto) for drawing relationship graphs,
for D&D stuff of all things.

this adds an optional 'graphviz' feature to package installation which
consequently depends on pydot
2021-06-24 09:46:26 -05:00
1583e3be99 more debugging and catch md misconfigured error 2021-06-24 09:43:00 -05:00
946a557177 correct the license declaration to match LICENSE and README 2021-06-24 08:34:26 -05:00
f0d4e7d3d9 have footer clear: both;
closes #14
2021-06-06 22:28:43 -05:00
954f7f4e80 allow markdown files to specify a redirect
closes #13
2021-06-06 22:24:35 -05:00
27bb139a2b add installation and usage information 2021-06-05 22:59:00 -05:00
c15862850f add a CONTRIBUTING file 2021-06-04 17:41:18 -05:00
afbfab338f properly apply site wrap classes to style wide version
fixes #12
2021-06-03 07:52:01 -05:00
cac6b40af5 relicense the project under GPLv2
prepping for an actual public release of a sort, this hopefully
clarifies the license and copyright

license from https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
2021-06-02 07:44:23 -05:00
65bc028524 implement base-wide.html as an extension of base.html
not quite sure why I didn't think of this initially... too much hacking
2021-05-19 22:30:20 -05:00
e8377adcf5 allow pages to specify different templates 2021-04-30 19:41:39 -05:00
f4beb15a3b make directory redirects absolute paths
I think this is always the right choice, since we're rewriting the full
input path
2021-04-17 15:06:39 -05:00
da447d2873 Merge branch 'master' of git.incorporeal.org:bss/incorporeal-cms 2021-04-17 14:57:20 -05:00
cde82ab918 don't route /media separately anymore 2021-04-17 11:16:34 -05:00
1ac13f3b9c add some 500 tests for test coverage 2021-04-17 11:08:01 -05:00
6705fa38eb requirements bumps 2021-04-17 10:58:06 -05:00
30b79e9dc1 add tests for subdir symlinks
this is automagically supported by the previous rewrite
2021-04-17 10:39:05 -05:00
60715a3a5c make request -> instance conversion support symlink dirs
I think this also clarifies the code, a bit
2021-04-17 10:31:05 -05:00
c90f0a3a42 treat symlinks as redirects
closes #7
2021-04-15 21:44:02 -05:00
71ead20f3f have file handler return render type rather than bool
for when we have further types to render
2021-04-15 20:36:30 -05:00
be88c3c1bc don't error on breadcrumbs if a dir doesn't have index.md
fixes #8
2021-04-14 21:35:14 -05:00
ced67bec8b allow for serving files directly inside pages/ 2021-04-14 20:45:50 -05:00
757b067e16 create a "plain" style with next to no CSS 2021-03-09 09:10:33 -06:00
06d948a709 have specific styles @import the base styles
this clarifies the value of what was formerly "style.css" a bit, and
also opens the door for potential styles that don't inherit the base
styling at all
2021-03-07 23:09:58 -06:00
f46bff6ec6 tweak language around the email 2021-02-23 13:16:58 -06:00
36 changed files with 1327 additions and 834 deletions

48
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,48 @@
# 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.
## Opening Issues
Issues should be posted to my Gitea instance at
<https://git.incorporeal.org/bss/incorporeal-cms/issues>. I'm not too picky about format, but I
recommend starting the title with "Improvement:", "Bug:", or similar, so I can do a high level of
prioritization.
## Guidelines for Patches, etc.
* 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
convincing.
* Follow the style precedent set in the code. Do **not** use Black, or otherwise reformat existing
code. I like it the way it is and don't need a militant tool making bad decisions about what is
readable.
* `tox` should run cleanly, of course.
* Almost any change should include unit tests, and also functional tests if they provide a feature
to the CMS functionality. For defects, include unit tests that fail on the unfixed codebase, so I
know exactly what's happening.
* Commits:
* 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
formatting a good commit message.
## 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.
### Copyright of Contributions
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).

1019
LICENSE

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,66 @@
# incorporeal-cms
Software that makes incorporeal.org go.
Software that makes simple Markdown content go.
## Installation and Usage
I recommend getting a release from <https://git.incorporeal.org/bss/incorporeal-cms/releases> and
installing the Python package in a virtualenv. Something like the following should suffice:
```
% cd ~/site
% virtualenv --python=python3.8 env-py3.8
% source env-py3.8/bin/activate
% pip install -U pip
% pip install ~/incorporeal_cms-1.3.0-py3-none-any.whl
% pip install -U gunicorn
% gunicorn -w 5 -t 60 -b 127.0.0.1:10000 --reload 'incorporealcms:create_app()'
```
This will get the CMS up and running, and listening on the specified port. The application is
further configured within `env-py3.8/var/incorporealcms-instance/config.py`, and content is served
out of `env-py3.8/var/incorporealcms-instance/pages/`.
## Serving a Site
Put content inside `env-py3.8/var/incorporealcms-instance/pages/` and go.
* Markdown files (ending in `.md`) are rendered via Python-Markdown if they are accessed without the
suffix (i.e., `post.md` should be referred to as `/post` to get it to render as Markdown.
* Directory paths (e.g. `/dir/`) can be rendered with a `/dir/index.md` file.
* Symlinks to files are treated as redirects to the destination content.
* Request paths with file suffixes are not rendered and served directly, so images, etc., can be
referenced naturally, and even the unrendered Markdown can be served as a text file via e.g.
`/post.md`.
Care is taken to not serve content above the `pages/` dir, even via symlink.
## Configuration
I've tried to keep the software agnostic to my personal domains, logos, etc. There are some settings
you are probably interested in tweaking, by specifying new values in
`incorporealcms-instance/config.py`:
* `TITLE_SUFFIX` is appended to the title of every page, separated from other title content by a
dash.
* `CONTACT_EMAIL` is referred to in error templates.
* `FAVICON` supplies the image used in browser tabs and that kind of thing.
If I missed anything, please let me know.
## Author and Licensing
Written by and copyright Brian S. Stephan (bss@incorporeal.org).
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@@ -3,7 +3,7 @@ import logging
import os
from logging.config import dictConfig
from flask import Flask, request, send_from_directory
from flask import Flask, request
from ._version import get_versions
@@ -40,11 +40,6 @@ def create_app(instance_path=None, test_config=None):
logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status)
return response
@app.route(f'/{app.config["MEDIA_DIR"]}/<path:filename>')
def media_files(filename):
return send_from_directory(os.path.join(app.instance_path, app.config['MEDIA_DIR']),
filename)
from . import error_pages, pages
app.register_blueprint(pages.bp)
app.register_error_handler(400, error_pages.bad_request)

View File

@@ -52,7 +52,7 @@ class Config(object):
# customizations
DEFAULT_PAGE_STYLE = 'light'
TITLE_SUFFIX = 'incorporeal.org'
CONTACT_EMAIL = 'bss@incorporeal.org'
TITLE_SUFFIX = 'example.com'
CONTACT_EMAIL = 'admin@example.com'
# specify FAVICON in your instance config.py to override the suou icon
# specify FAVICON in your instance config.py to override the provided icon

View File

@@ -34,6 +34,7 @@ def render(template_name_or_list, **context):
PAGE_STYLES = {
'dark': 'css/dark.css',
'light': 'css/light.css',
'plain': 'css/plain.css',
}
selected_style = request.args.get('style', None)

View File

@@ -0,0 +1 @@
"""Markdown extensions."""

View File

@@ -0,0 +1,51 @@
"""Serve dot diagrams inline."""
import base64
import logging
import re
import markdown
import pydot
logger = logging.getLogger(__name__)
class InlinePydot(markdown.Extension):
"""Wrap the markdown prepcoressor."""
def extendMarkdown(self, md):
"""Add InlinePydotPreprocessor to the Markdown instance."""
md.preprocessors.register(InlinePydotPreprocessor(md), 'dot_block', 100)
class InlinePydotPreprocessor(markdown.preprocessors.Preprocessor):
"""Identify dot codeblocks and run them through pydot."""
BLOCK_RE = re.compile(r'~~~pydot:(?P<filename>[^\s]+)\n(?P<content>.*?)~~~', re.DOTALL)
def run(self, lines):
"""Match and generate diagrams from dot code blocks."""
text = '\n'.join(lines)
for match in self.BLOCK_RE.finditer(text):
filename = match.group(1)
dot_string = match.group(2)
# use pydot to turn the text into pydot
graphs = pydot.graph_from_dot_data(dot_string)
if not graphs:
logger.debug("some kind of issue with parsed 'dot' %s", dot_string)
raise ValueError("error parsing dot text!")
# encode the image and provide as an inline image in markdown
encoded_image = base64.b64encode(graphs[0].create_png()).decode('ascii')
data_path = f'data:image/png;base64,{encoded_image}'
inline_image = f'![{filename}]({data_path})'
# replace the image in the output markdown
text = f'{text[:match.start()]}\n{inline_image}\n{text[match.end():]}'
return text.split('\n')
def makeExtension(*args, **kwargs):
"""Provide the extension to the markdown extension loader."""
return InlinePydot(*args, **kwargs)

View File

@@ -6,7 +6,7 @@ import re
from flask import Blueprint, Markup, abort
from flask import current_app as app
from flask import redirect, request
from flask import redirect, request, send_from_directory
from tzlocal import get_localzone
from incorporealcms.lib import get_meta_str, init_md, render
@@ -21,25 +21,53 @@ bp = Blueprint('pages', __name__, url_prefix='/')
def display_page(path):
"""Get the file contents of the requested path and render the file."""
try:
resolved_path = request_path_to_instance_resource_path(path)
logger.debug("received request for path '%s', resolved to '%s'", path, resolved_path)
resolved_path, render_type = request_path_to_instance_resource_path(path)
logger.debug("received request for path '%s', resolved to '%s', type '%s'",
path, resolved_path, render_type)
except PermissionError:
abort(400)
except IsADirectoryError:
return redirect(f'{path}/', code=301)
return redirect(f'/{path}/', code=301)
except FileNotFoundError:
abort(404)
if render_type == 'file':
return send_from_directory(app.instance_path, resolved_path)
elif render_type == 'symlink':
logger.debug("attempting to redirect path '%s' to reverse of resource '%s'", path, resolved_path)
redirect_path = f'/{instance_resource_path_to_request_path(resolved_path)}'
logger.debug("redirect path: '%s'", redirect_path)
return redirect(redirect_path, code=301)
elif render_type == 'markdown':
logger.debug("treating path '%s' as markdown '%s'", path, resolved_path)
return handle_markdown_file_path(resolved_path)
else:
logger.exception("unsupported render_type '%s'!?", render_type)
abort(500)
def handle_markdown_file_path(resolved_path):
"""Given a location on disk, attempt to open it and render the markdown within."""
try:
logger.debug("opening resolved path '%s'", resolved_path)
with app.open_instance_resource(resolved_path, 'r') as entry_file:
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone())
entry = entry_file.read()
logger.debug("resolved path '%s' read", resolved_path)
except OSError:
logger.exception("resolved path '%s' could not be opened!", resolved_path)
abort(500)
else:
try:
md = init_md()
content = Markup(md.convert(entry))
except ValueError:
logger.exception("error parsing/rendering markdown!")
abort(500)
except TypeError:
logger.exception("error loading/rendering markdown!")
abort(500)
logger.debug("file metadata: %s", md.Meta)
parent_navs = generate_parent_navs(resolved_path)
@@ -49,9 +77,17 @@ def display_page(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)
return render('base.html', 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'))
template = get_meta_str(md, 'template') if md.Meta.get('template') else 'base.html'
# check if this has a HTTP redirect
redirect_url = get_meta_str(md, 'redirect') if md.Meta.get('redirect') else None
if redirect_url:
logger.debug("redirecting via meta tag to '%s'", redirect_url)
return redirect(redirect_url, code=301)
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'))
def request_path_to_instance_resource_path(path):
@@ -63,34 +99,52 @@ def request_path_to_instance_resource_path(path):
"""
# check if the path is allowed
base_dir = os.path.realpath(f'{app.instance_path}/pages/')
resolved_path = os.path.realpath(os.path.join(base_dir, path))
logger.debug("base_dir: %s, constructed resolved_path: %s", base_dir, resolved_path)
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)
# bail if the requested real path isn't inside the base directory
if base_dir != os.path.commonpath((base_dir, resolved_path)):
logger.warning("client tried to request a path '%s' outside of the base_dir!", path)
raise PermissionError
# if this is a file-like requset but actually a directory, redirect the user
# 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
if os.path.isdir(resolved_path) and not path.endswith('/'):
logger.info("client requested a path '%s' that is actually a directory", path)
raise IsADirectoryError
# derive the proper markdown file depending on if this is a dir or file
if os.path.isdir(resolved_path):
absolute_resource = os.path.join(resolved_path, 'index.md')
else:
absolute_resource = f'{resolved_path}.md'
# if the requested path contains a symlink, redirect the user
if verbatim_path != resolved_path:
logger.info("client requested a path '%s' that is actually a symlink to file '%s'", path, resolved_path)
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'symlink'
logger.info("final path = '%s' for request '%s'", absolute_resource, path)
# derive the proper markdown or actual file depending on if this is a dir or file
if os.path.isdir(resolved_path):
resolved_path = os.path.join(resolved_path, 'index.md')
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'markdown'
logger.info("final DIRECT path = '%s' for request '%s'", resolved_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'))
resolved_path = os.path.realpath(verbatim_path)
# does the final file actually exist?
if not os.path.exists(absolute_resource):
logger.warning("requested final path '%s' does not exist!", absolute_resource)
if not os.path.exists(resolved_path):
logger.warning("requested final path '%s' does not exist!", resolved_path)
raise FileNotFoundError
# check for symlinks
if verbatim_path != resolved_path:
logger.info("client requested a path '%s' that is actually a symlink to file '%s'", path, resolved_path)
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'symlink'
logger.info("final path = '%s' for request '%s'", resolved_path, path)
# we checked that the file exists via absolute path, but now we need to give the path relative to instance dir
return absolute_resource.replace(f'{app.instance_path}{os.path.sep}', '')
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'markdown'
def instance_resource_path_to_request_path(path):
@@ -127,12 +181,15 @@ def generate_parent_navs(path):
md = init_md()
# read the resource
try:
with app.open_instance_resource(path, 'r') as entry_file:
entry = entry_file.read()
_ = Markup(md.convert(entry))
page_name = (" ".join(md.Meta.get('title')) if md.Meta.get('title')
else request_path_to_breadcrumb_display(request_path))
return generate_parent_navs(parent_resource_path) + [(page_name, request_path)]
except FileNotFoundError:
return generate_parent_navs(parent_resource_path) + [(request_path, request_path)]
def request_path_to_breadcrumb_display(path):

View File

@@ -11,38 +11,21 @@ body {
-webkit-text-size-adjust: 100%;
}
.site-wrap {
.site-wrap-normal-width {
max-width: 70pc;
}
.site-wrap-double-width {
max-width: 140pc;
}
.site-wrap {
min-height: 100vh;
margin: 0;
margin-left: auto;
margin-right: auto;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
h3 {
font-size: 1.25em;
}
h4 {
font-size: 1.17em;
}
h5 {
font-size: 1em;
}
h6 {
font-size: .83em;
}
a:link {
font-weight: bold;
text-decoration: none;
@@ -76,7 +59,6 @@ div.header a {
div.content {
font-size: 11pt;
padding: 0 1em;
line-height: 1.5em;
}
@@ -91,6 +73,7 @@ sub {
}
footer {
clear: both;
display: block;
font-size: 75%;
color: #999;
@@ -132,6 +115,10 @@ img {
max-width: 50% !important;
}
.img-75 {
max-width: 75% !important;
}
.img-center {
display: block;
margin-left: auto;

View File

@@ -1,9 +1,12 @@
/* common styling via the base.css, used in light and dark */
@import '/static/css/base.css';
html {
color: #CCC;
color: #DDD;
}
body {
background: black;
background: #090909;
}
strong {
@@ -11,7 +14,7 @@ strong {
}
.site-wrap {
background: #111;
background: black;
border: 1px solid #222;
border-top: none;
@@ -59,12 +62,3 @@ blockquote {
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid #333;
}
figure {
background: #222;
border: 1px solid #333;
}
figcaption {
color: #BBB;
}

View File

@@ -1,9 +1,12 @@
/* common styling via the base.css, used in light and dark */
@import '/static/css/base.css';
html {
color: #222;
}
body {
background: #999;
background: #F6F6F6;
}
strong {
@@ -13,7 +16,7 @@ strong {
.site-wrap {
background: white;
border: 1px solid #ddd;
border: 1px solid #DDD;
border-top: none;
border-bottom: none;
}
@@ -33,38 +36,29 @@ a:hover, a:active {
}
div.header {
background: #EEE;
border-bottom: 1px solid #CCC;
color: #666;
background: #DDD;
border-bottom: 1px solid #DDD;
color: #444;
}
div.header a {
color: #666;
color: #444;
}
table, th, td {
border: 1px solid #ccc;
border: 1px solid #CCC;
}
th {
background: #eee;
background: #CCC;
}
blockquote {
background-color: rgba(120, 120, 120, 0.1);
border: 1px solid #CCC;
border: 1px solid #DDD;
}
.img-frame {
background-color: rgba(0, 0, 0, 0.1);
border: 1px solid #BBB;
}
figure {
background: #EFEFEF;
border: 1px solid #CCCCCC;
}
figcaption {
color: #777777;
border: 1px solid #CCC;
}

View File

@@ -0,0 +1,12 @@
/* specify almost no styling, just fix some image rendering */
.img-25 {
max-width: 25% !important;
}
.img-50 {
max-width: 50% !important;
}
.img-75 {
max-width: 75% !important;
}

View File

@@ -10,7 +10,7 @@
<div class="content">
<h1>NOT FOUND</h1>
<p>Sorry, <b><tt>{{ request.path }}</tt></b> does not seem to exist, at least not anymore.</p>
<p>It's possible you followed a dead link on this site, in which case I would appreciate it if you could email me via:
<p>It's possible you followed a dead link on this site, in which case I would appreciate it if you could email me at
{{ config.CONTACT_EMAIL }} and I can take a look. I make an effort to symlink old content to its new location,
so old links and URLs should, generally speaking, work.</p>
<p>Otherwise, I suggest you go <a href="/">to the index</a> and navigate your way (hopefully) to what

View File

@@ -0,0 +1,2 @@
{% extends "base.html" %}
{% block site_class %}class="site-wrap site-wrap-double-width"{% endblock %}

View File

@@ -7,11 +7,10 @@
<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='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename=user_style) }}">
<link rel="icon" href="{% if config.FAVICON %}{{ config.FAVICON }}{% else %}{{ url_for('static', filename='img/favicon.png') }}{% endif %}">
<div class="site-wrap">
<div {% block site_class %}class="site-wrap site-wrap-normal-width"{% endblock %}>
{% block header %}
<div class="header">
<div class="nav">
@@ -23,6 +22,7 @@
<div class="styles">
<a href="?style=dark">[dark]</a>
<a href="?style=light">[light]</a>
<a href="?style=plain">[plain]</a>
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,7 @@
-r requirements.in
# testing runner, test reporting, packages used during testing (e.g. requests-mock), etc.
pydot
pytest
pytest-cov
@@ -16,6 +17,7 @@ flake8-fixme
flake8-isort
flake8-logging-format
flake8-mutable
safety # check requirements file for issues
# maintenance utilities and tox
pip-tools # pip-compile

View File

@@ -1,48 +1,37 @@
#
# This file is autogenerated by pip-compile
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in
#
appdirs==1.4.4
# via virtualenv
attrs==20.3.0
attrs==21.4.0
# via pytest
bandit==1.6.2
# via -r requirements/requirements-dev.in
bleach==3.3.0
bleach==4.1.0
# via mdx-linkify
click==7.1.2
certifi==2021.10.8
# via requests
charset-normalizer==2.0.10
# via requests
click==8.0.3
# via
# flask
# pip-tools
coverage==5.4
# safety
coverage[toml]==6.2
# via pytest-cov
distlib==0.3.1
distlib==0.3.4
# via virtualenv
dlint==0.11.0
dlint==0.12.0
# via -r requirements/requirements-dev.in
filelock==3.0.12
dparse==0.5.1
# via safety
filelock==3.4.2
# via
# tox
# virtualenv
flake8-blind-except==0.2.0
# via -r requirements/requirements-dev.in
flake8-builtins==1.5.3
# via -r requirements/requirements-dev.in
flake8-docstrings==1.5.0
# via -r requirements/requirements-dev.in
flake8-executable==2.1.1
# via -r requirements/requirements-dev.in
flake8-fixme==1.1.1
# via -r requirements/requirements-dev.in
flake8-isort==4.0.0
# via -r requirements/requirements-dev.in
flake8-logging-format==0.6.0
# via -r requirements/requirements-dev.in
flake8-mutable==1.2.0
# via -r requirements/requirements-dev.in
flake8==3.8.4
flake8==4.0.1
# via
# -r requirements/requirements-dev.in
# dlint
@@ -51,101 +40,146 @@ flake8==3.8.4
# flake8-executable
# flake8-isort
# flake8-mutable
flask==1.1.2
flake8-blind-except==0.2.0
# via -r requirements/requirements-dev.in
flake8-builtins==1.5.3
# via -r requirements/requirements-dev.in
flake8-docstrings==1.6.0
# via -r requirements/requirements-dev.in
flake8-executable==2.1.1
# via -r requirements/requirements-dev.in
flake8-fixme==1.1.1
# via -r requirements/requirements-dev.in
flake8-isort==4.1.1
# via -r requirements/requirements-dev.in
flake8-logging-format==0.6.0
# via -r requirements/requirements-dev.in
flake8-mutable==1.2.0
# via -r requirements/requirements-dev.in
flask==2.0.2
# via -r requirements/requirements.in
gitdb==4.0.5
gitdb==4.0.9
# via gitpython
gitpython==3.1.13
gitpython==3.1.26
# via bandit
idna==3.3
# via requests
iniconfig==1.1.1
# via pytest
isort==5.7.0
isort==5.10.1
# via flake8-isort
itsdangerous==1.1.0
itsdangerous==2.0.1
# via flask
jinja2==2.11.3
jinja2==3.0.3
# via flask
markdown==3.3.3
markdown==3.3.6
# via
# -r requirements/requirements.in
# mdx-linkify
markupsafe==1.1.1
markupsafe==2.0.1
# via jinja2
mccabe==0.6.1
# via flake8
mdx-linkify==2.1
# via -r requirements/requirements.in
packaging==20.9
packaging==21.3
# via
# bleach
# dparse
# pytest
# safety
# tox
pbr==5.5.1
pbr==5.8.0
# via stevedore
pip-tools==5.5.0
pep517==0.12.0
# via pip-tools
pip-tools==6.4.0
# via -r requirements/requirements-dev.in
pluggy==0.13.1
platformdirs==2.4.1
# via virtualenv
pluggy==1.0.0
# via
# pytest
# tox
py==1.10.0
py==1.11.0
# via
# pytest
# tox
pycodestyle==2.6.0
pycodestyle==2.8.0
# via flake8
pydocstyle==5.1.1
pydocstyle==6.1.1
# via flake8-docstrings
pyflakes==2.2.0
# via flake8
pyparsing==2.4.7
# via packaging
pytest-cov==2.11.1
pydot==1.4.2
# via -r requirements/requirements-dev.in
pytest==6.2.2
pyflakes==2.4.0
# via flake8
pyparsing==3.0.6
# via
# packaging
# pydot
pytest==6.2.5
# via
# -r requirements/requirements-dev.in
# pytest-cov
pytz==2021.1
pytest-cov==3.0.0
# via -r requirements/requirements-dev.in
pytz-deprecation-shim==0.1.0.post0
# via tzlocal
pyyaml==5.4.1
# via bandit
six==1.15.0
pyyaml==6.0
# via
# bandit
# dparse
requests==2.27.1
# via safety
safety==1.10.3
# via -r requirements/requirements-dev.in
six==1.16.0
# via
# bandit
# bleach
# tox
# virtualenv
smmap==3.0.5
smmap==5.0.0
# via gitdb
snowballstemmer==2.1.0
snowballstemmer==2.2.0
# via pydocstyle
stevedore==3.3.0
stevedore==3.5.0
# via bandit
testfixtures==6.17.1
testfixtures==6.18.3
# via flake8-isort
toml==0.10.2
# via
# dparse
# pytest
# tox
tox-wheel==0.6.0
# via -r requirements/requirements-dev.in
tox==3.22.0
tomli==2.0.0
# via
# coverage
# pep517
tox==3.24.5
# via
# -r requirements/requirements-dev.in
# tox-wheel
tzlocal==2.1
# via -r requirements/requirements.in
versioneer==0.19
tox-wheel==0.7.0
# via -r requirements/requirements-dev.in
virtualenv==20.4.2
tzdata==2021.5
# via pytz-deprecation-shim
tzlocal==4.1
# via -r requirements/requirements.in
urllib3==1.26.8
# via requests
versioneer==0.21
# via -r requirements/requirements-dev.in
virtualenv==20.13.0
# via tox
webencodings==0.5.1
# via bleach
werkzeug==1.0.1
werkzeug==2.0.2
# via flask
wheel==0.36.2
# via tox-wheel
wheel==0.37.1
# via
# pip-tools
# tox-wheel
# The following packages are considered to be unsafe in a requirements file:
# pip

View File

@@ -1,38 +1,40 @@
#
# This file is autogenerated by pip-compile
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in
#
bleach==3.3.0
bleach==4.1.0
# via mdx-linkify
click==7.1.2
click==8.0.3
# via flask
flask==1.1.2
flask==2.0.2
# via -r requirements/requirements.in
itsdangerous==1.1.0
itsdangerous==2.0.1
# via flask
jinja2==2.11.3
jinja2==3.0.3
# via flask
markdown==3.3.3
markdown==3.3.6
# via
# -r requirements/requirements.in
# mdx-linkify
markupsafe==1.1.1
markupsafe==2.0.1
# via jinja2
mdx-linkify==2.1
# via -r requirements/requirements.in
packaging==20.9
packaging==21.3
# via bleach
pyparsing==2.4.7
pyparsing==3.0.6
# via packaging
pytz==2021.1
pytz-deprecation-shim==0.1.0.post0
# via tzlocal
six==1.15.0
six==1.16.0
# via bleach
tzlocal==2.1
tzdata==2021.5
# via pytz-deprecation-shim
tzlocal==4.1
# via -r requirements/requirements.in
webencodings==0.5.1
# via bleach
werkzeug==1.0.1
werkzeug==2.0.2
# via flask

View File

@@ -18,13 +18,19 @@ setup(
name='incorporeal-cms',
description='Flask project for running https://suou.net (and eventually others).',
url='https://git.incorporeal.org/bss/incorporeal-cms',
license='GPL3',
author='Brian S. Stephan',
author_email='bss@incorporeal.org',
license='AGPLv3+',
classifiers=[
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
],
version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(),
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=extract_requires(),
extras_require={
'dot': ['pydot'],
},
)

View File

@@ -0,0 +1,39 @@
"""Test graphviz functionality."""
import os
from incorporealcms import create_app
HERE = os.path.dirname(os.path.abspath(__file__))
def app_with_pydot():
"""Create the test app, including the pydot extension."""
return create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'MARKDOWN_EXTENSIONS': ['incorporealcms.mdx.pydot']})
def test_functional_initialization():
"""Test initialization with the graphviz config."""
app = app_with_pydot()
assert app is not None
def test_graphviz_is_rendered():
"""Initialize the app with the graphviz extension and ensure it does something."""
app = app_with_pydot()
client = app.test_client()
response = client.get('/test-graphviz')
assert response.status_code == 200
assert b'~~~pydot' not in response.data
assert b'data:image/png;base64' in response.data
def test_invalid_graphviz_is_not_rendered():
"""Check that invalid graphviz doesn't blow things up."""
app = app_with_pydot()
client = app.test_client()
response = client.get('/test-invalid-graphviz')
assert response.status_code == 500
assert b'INTERNAL SERVER ERROR' in response.data

View File

@@ -1,5 +1,6 @@
"""Test page requests."""
import re
from unittest.mock import patch
def test_page_that_exists(client):
@@ -9,13 +10,20 @@ def test_page_that_exists(client):
assert b'<h1>test index</h1>' in response.data
def test_direct_file_that_exists(client):
"""Test that the app can serve a basic file at the index."""
response = client.get('/foo.txt')
assert response.status_code == 200
assert b'test file' in response.data
def test_page_that_doesnt_exist(client):
"""Test that the app returns 404 for nonsense requests and they use my error page."""
response = client.get('/ohuesthaoeusth')
assert response.status_code == 404
assert b'<b><tt>/ohuesthaoeusth</tt></b> does not seem to exist' in response.data
# test the contact email config
assert b'bss@incorporeal.org' in response.data
assert b'admin@example.com' in response.data
def test_files_outside_pages_do_not_get_served(client):
@@ -31,7 +39,25 @@ def test_internal_server_error_serves_error_page(client):
assert response.status_code == 500
assert b'INTERNAL SERVER ERROR' in response.data
# test the contact email config
assert b'bss@incorporeal.org' in response.data
assert b'admin@example.com' in response.data
def test_oserror_is_500(client, app):
"""Test that an OSError raises as a 500."""
with app.test_request_context():
with patch('flask.current_app.open_instance_resource', side_effect=OSError):
response = client.get('/')
assert response.status_code == 500
assert b'INTERNAL SERVER ERROR' in response.data
def test_unsupported_file_type_is_500(client, app):
"""Test a coding condition mishap raises as a 500."""
with app.test_request_context():
with patch('incorporealcms.pages.request_path_to_instance_resource_path', return_value=('foo', 'bar')):
response = client.get('/')
assert response.status_code == 500
assert b'INTERNAL SERVER ERROR' in response.data
def test_weird_paths_do_not_get_served(client):
@@ -44,28 +70,28 @@ def test_page_with_title_metadata(client):
"""Test that a page with title metadata has its title written."""
response = client.get('/')
assert response.status_code == 200
assert b'<title>Index - incorporeal.org</title>' in response.data
assert b'<title>Index - example.com</title>' in response.data
def test_page_without_title_metadata(client):
"""Test that a page without title metadata gets the default title."""
response = client.get('/no-title')
assert response.status_code == 200
assert b'<title>/no-title - incorporeal.org</title>' in response.data
assert b'<title>/no-title - example.com</title>' in response.data
def test_page_in_subdir_without_title_metadata(client):
"""Test that the title-less page display is as expected."""
response = client.get('/subdir//page-no-title')
assert response.status_code == 200
assert b'<title>/subdir/page-no-title - incorporeal.org</title>' in response.data
assert b'<title>/subdir/page-no-title - example.com</title>' in response.data
def test_page_with_card_metadata(client):
"""Test that a page with opengraph metadata."""
response = client.get('/more-metadata')
assert response.status_code == 200
assert b'<meta property="og:title" content="title for the page - incorporeal.org">' in response.data
assert b'<meta property="og:title" content="title for the page - example.com">' in response.data
assert b'<meta property="og:description" content="description of this page made even longer">' in response.data
assert b'<meta property="og:image" content="http://buh.com/test.img">' in response.data
@@ -74,7 +100,7 @@ def test_page_with_card_title_even_when_no_metadata(client):
"""Test that a page without metadata still has a card with the derived title."""
response = client.get('/no-title')
assert response.status_code == 200
assert b'<meta property="og:title" content="/no-title - incorporeal.org">' in response.data
assert b'<meta property="og:title" content="/no-title - example.com">' in response.data
assert b'<meta property="og:description"' not in response.data
assert b'<meta property="og:image"' not in response.data
@@ -83,7 +109,14 @@ def test_page_with_forced_empty_title_just_shows_suffix(client):
"""Test that if a page specifies a blank Title meta tag explicitly, only the suffix is used in the title."""
response = client.get('/forced-no-title')
assert response.status_code == 200
assert b'<title>incorporeal.org</title>' in response.data
assert b'<title>example.com</title>' in response.data
def test_page_with_redirect_meta_url_redirects(client):
"""Test that if a page specifies a URL to redirect to, that the site serves up a 301."""
response = client.get('/redirect')
assert response.status_code == 301
assert response.location == 'http://www.google.com/'
def test_page_has_modified_timestamp(client):
@@ -101,6 +134,44 @@ def test_that_page_request_redirects_to_directory(client):
"""
response = client.get('/subdir')
assert response.status_code == 301
assert response.location == 'http://localhost/subdir/'
def test_that_request_to_symlink_redirects_markdown(client):
"""Test that a request to /foo redirects to /what-foo-points-at."""
response = client.get('/symlink-to-no-title')
assert response.status_code == 301
assert response.location == 'http://localhost/no-title'
def test_that_request_to_symlink_redirects_file(client):
"""Test that a request to /foo.txt redirects to /what-foo-points-at.txt."""
response = client.get('/symlink-to-foo.txt')
assert response.status_code == 301
assert response.location == 'http://localhost/foo.txt'
def test_that_request_to_symlink_redirects_directory(client):
"""Test that a request to /foo/ redirects to /what-foo-points-at/."""
response = client.get('/symlink-to-subdir/')
assert response.status_code == 301
assert response.location == 'http://localhost/subdir'
# sadly, this location also redirects
response = client.get('/subdir')
assert response.status_code == 301
assert response.location == 'http://localhost/subdir/'
# but we do get there
response = client.get('/subdir/')
assert response.status_code == 200
def test_that_request_to_symlink_redirects_subdirectory(client):
"""Test that a request to /foo/bar redirects to /what-foo-points-at/bar."""
response = client.get('/symlink-to-subdir/page-no-title')
assert response.status_code == 301
assert response.location == 'http://localhost/subdir/page-no-title'
response = client.get('/subdir/page-no-title')
assert response.status_code == 200
def test_that_dir_request_does_not_redirect(client):
@@ -129,3 +200,13 @@ def test_setting_selected_style_includes_cookie(client):
assert b'dark.css' in response.data
assert b'light.css' not in response.data
assert style_cookie.value == 'dark'
def test_pages_can_supply_alternate_templates(client):
"""Test that pages can supply templates other than the default."""
response = client.get('/')
assert b'class="site-wrap site-wrap-normal-width"' in response.data
assert b'class="site-wrap site-wrap-double-width"' not in response.data
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

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,3 @@
Template: base-wide.html
testttttttttt

View File

@@ -0,0 +1 @@
test file

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1 @@
this is a test page

View File

@@ -0,0 +1 @@
Redirect: http://www.google.com/

View File

@@ -0,0 +1 @@
foo.txt

View File

@@ -0,0 +1 @@
no-title.md

View File

@@ -0,0 +1 @@
subdir

View File

@@ -0,0 +1,12 @@
# test
test
~~~pydot:attack-plan
digraph G {
rankdir=LR
Earth
Mars
Earth -> Mars
}
~~~
more test

View File

@@ -0,0 +1,11 @@
# test
test
~~~pydot:attack-plan
rankdir=LR
Earth
Mars
Earth -> Mars
}
~~~
more test

View File

@@ -20,7 +20,7 @@ def test_markdown_meta_extension_always():
client = app.test_client()
response = client.get('/')
assert response.status_code == 200
assert b'<title>Index - incorporeal.org</title>' in response.data
assert b'<title>Index - example.com</title>' in response.data
def test_custom_markdown_extensions_work():
@@ -68,3 +68,12 @@ def test_favicon_override():
response = client.get('/no-title')
assert response.status_code == 200
assert b'<link rel="icon" href="/media/foo.png">' in response.data
def test_misconfigured_markdown_extensions():
"""Test that a misconfigured markdown extensions leads to a 500 at render time."""
instance_path = os.path.join(HERE, 'instance')
app = create_app(instance_path=instance_path, test_config={'MARKDOWN_EXTENSIONS': 'WRONG'})
client = app.test_client()
response = client.get('/no-title')
assert response.status_code == 500

View File

@@ -9,19 +9,19 @@ from incorporealcms.pages import (generate_parent_navs, instance_resource_path_t
def test_generate_page_navs_index(app):
"""Test that the index page has navs to the root (itself)."""
with app.app_context():
assert generate_parent_navs('pages/index.md') == [('incorporeal.org', '/')]
assert generate_parent_navs('pages/index.md') == [('example.com', '/')]
def test_generate_page_navs_subdir_index(app):
"""Test that dir pages have navs to the root and themselves."""
with app.app_context():
assert generate_parent_navs('pages/subdir/index.md') == [('incorporeal.org', '/'), ('subdir', '/subdir/')]
assert generate_parent_navs('pages/subdir/index.md') == [('example.com', '/'), ('subdir', '/subdir/')]
def test_generate_page_navs_subdir_real_page(app):
"""Test that real pages have navs to the root, their parent, and themselves."""
with app.app_context():
assert generate_parent_navs('pages/subdir/page.md') == [('incorporeal.org', '/'), ('subdir', '/subdir/'),
assert generate_parent_navs('pages/subdir/page.md') == [('example.com', '/'), ('subdir', '/subdir/'),
('Page', '/subdir/page')]
@@ -29,12 +29,22 @@ def test_generate_page_navs_subdir_with_title_parsing_real_page(app):
"""Test that title metadata is used in the nav text."""
with app.app_context():
assert generate_parent_navs('pages/subdir-with-title/page.md') == [
('incorporeal.org', '/'),
('example.com', '/'),
('SUB!', '/subdir-with-title/'),
('page', '/subdir-with-title/page')
]
def test_generate_page_navs_subdir_with_no_index(app):
"""Test that breadcrumbs still generate even if a subdir doesn't have an index.md."""
with app.app_context():
assert generate_parent_navs('pages/no-index-dir/page.md') == [
('example.com', '/'),
('/no-index-dir/', '/no-index-dir/'),
('page', '/no-index-dir/page')
]
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')
@@ -60,44 +70,45 @@ def test_render_with_no_user_theme(app):
def test_request_path_to_instance_resource_path(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('index') == 'pages/index.md'
assert request_path_to_instance_resource_path('index') == ('pages/index.md', 'markdown')
def test_request_path_to_instance_resource_path_direct_file(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('no-title') == 'pages/no-title.md'
assert request_path_to_instance_resource_path('no-title') == ('pages/no-title.md', 'markdown')
def test_request_path_to_instance_resource_path_in_subdir(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/page') == 'pages/subdir/page.md'
assert request_path_to_instance_resource_path('subdir/page') == ('pages/subdir/page.md', 'markdown')
def test_request_path_to_instance_resource_path_subdir_index(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/') == 'pages/subdir/index.md'
assert request_path_to_instance_resource_path('subdir/') == ('pages/subdir/index.md', 'markdown')
def test_request_path_to_instance_resource_path_relatives_walked(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('subdir/more-subdir/../../more-metadata') ==
'pages/more-metadata.md')
('pages/more-metadata.md', 'markdown'))
def test_request_path_to_instance_resource_path_relatives_walked_indexes_work_too(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/more-subdir/../../') == 'pages/index.md'
assert request_path_to_instance_resource_path('subdir/more-subdir/../../') == ('pages/index.md', 'markdown')
def test_request_path_to_instance_resource_path_relatives_walked_into_subdirs_also_fine(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/more-subdir/../../subdir/page') == 'pages/subdir/page.md'
assert (request_path_to_instance_resource_path('subdir/more-subdir/../../subdir/page') ==
('pages/subdir/page.md', 'markdown'))
def test_request_path_to_instance_resource_path_permission_error_on_ref_above_pages(app):
@@ -114,6 +125,41 @@ def test_request_path_to_instance_resource_path_isadirectory_on_file_like_req_fo
assert request_path_to_instance_resource_path('subdir')
def test_request_path_to_instance_resource_path_actual_file(app):
"""Test that a request for e.g. '/foo.png' when foo.png is a real file works."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('bss-square-no-bg.png') ==
('pages/bss-square-no-bg.png', 'file'))
def test_request_path_to_instance_resource_path_markdown_symlink(app):
"""Test that a request for e.g. '/foo' when foo.md is a symlink to another .md file redirects."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('symlink-to-no-title') ==
('pages/no-title.md', 'symlink'))
def test_request_path_to_instance_resource_path_file_symlink(app):
"""Test that a request for e.g. '/foo' when foo.txt is a symlink to another .txt file redirects."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('symlink-to-foo.txt') ==
('pages/foo.txt', 'symlink'))
def test_request_path_to_instance_resource_path_dir_symlink(app):
"""Test that a request for e.g. '/foo' when /foo is a symlink to /bar redirects."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('symlink-to-subdir/') ==
('pages/subdir', 'symlink'))
def test_request_path_to_instance_resource_path_subdir_symlink(app):
"""Test that a request for e.g. '/foo/baz' when /foo is a symlink to /bar redirects."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('symlink-to-subdir/page-no-title') ==
('pages/subdir/page-no-title.md', 'symlink'))
def test_request_path_to_instance_resource_path_nonexistant_file_errors(app):
"""Test that a request for something not on disk errors."""
with app.test_request_context():
@@ -155,13 +201,15 @@ def test_instance_resource_path_to_request_path_on_subdir_and_page(app):
def test_request_resource_request_root(app):
"""Test that a request can resolve to a resource and back to a request."""
with app.test_request_context():
instance_resource_path_to_request_path(request_path_to_instance_resource_path('index')) == ''
instance_path, _ = request_path_to_instance_resource_path('index')
instance_resource_path_to_request_path(instance_path) == ''
def test_request_resource_request_page(app):
"""Test that a request can resolve to a resource and back to a request."""
with app.test_request_context():
instance_resource_path_to_request_path(request_path_to_instance_resource_path('no-title')) == 'no-title'
instance_path, _ = request_path_to_instance_resource_path('no-title')
instance_resource_path_to_request_path(instance_path) == 'no-title'
def test_request_path_to_breadcrumb_display_patterns():

18
tox.ini
View File

@@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
envlist = begin,py37,py38,coverage,security,lint,bundle
envlist = begin,py38,py39,py310,coverage,security,lint,bundle
[testenv]
# build a wheel and test it
@@ -31,12 +31,17 @@ deps = setuptools
skip_install = true
commands = coverage erase
[testenv:py37]
[testenv:py38]
# run pytest with coverage
commands =
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
[testenv:py38]
[testenv:py39]
# run pytest with coverage
commands =
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
[testenv:py310]
# run pytest with coverage
commands =
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
@@ -53,6 +58,7 @@ commands =
# again it seems the most valuable here to run against the packaged code
commands =
bandit {envsitepackagesdir}/incorporealcms/ -r
safety check -r requirements/requirements-dev.txt
[testenv:lint]
# run style checks
@@ -74,12 +80,6 @@ source =
[coverage:run]
branch = True
# redundant with pytest --cov above, but this tricks the coverage.xml report into
# using the full path, otherwise files with the same name in different paths
# get clobbered. maybe appends would fix this, IDK
include =
.tox/**/incorporealcms/
omit =
**/_version.py