119 Commits

Author SHA1 Message Date
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
d89fd151ca use just the page part of the path in breadcrumbs
rather than showing the full path (e.g. /foo/bar/baz) in breadcrumbs
when the page doesn't have a Title, show just the leaf (baz)

Closes #4
2021-02-27 00:30:32 -06:00
ce1ed60dd2 allow for configuration to override the favicon
Closes #5
2021-02-27 00:10:03 -06:00
f46bff6ec6 tweak language around the email 2021-02-23 13:16:58 -06:00
70a8d4f06a add configurable contact email for error pages 2021-02-23 13:11:52 -06:00
085571e58f requiremnets bump 2021-02-22 21:20:49 -06:00
0bfea79a8f log the OSError before returning 500 2021-02-21 19:37:59 -06:00
39d78af524 add error handler pages for 400, 404, 500 2021-02-21 19:35:52 -06:00
e570ee26b5 clean up unused imports 2021-02-21 18:41:09 -06:00
411ecebbc6 fold some nav/style menu styles into header
unnecessarily duplicated now that I have div.header, and also useful for
when I start the error handling pages
2021-02-21 10:14:42 -06:00
c1801b0086 move render() into shared spot
going to be used by error page handling code, once it exists
2021-02-20 23:36:03 -06:00
f08c1117d8 move site suffix into title always
this tweaks the behavior of the title to always append ' - suffix' to
any title (from the meta tag, or generated via request path), unless the
page explicitly specifies an empty Title meta tag
2021-02-20 23:21:29 -06:00
dd7687884a remove resolve_page_file, been refactored away 2021-02-20 22:50:56 -06:00
15c88d920b use request path as an alternative to Title metadata 2021-02-20 22:43:42 -06:00
1cef3b8196 rewrite generate_parent_navs to work on resource paths
the old code was kind of impossible to understand by reading it, so this
is hopefully considerably clearer
2021-02-20 21:47:39 -06:00
faf4a7f166 minor style cleanup 2021-02-20 19:22:23 -06:00
4dcc1c91c2 add method to from resource path to request path 2021-02-20 19:19:36 -06:00
1c40f45ffd clarify name of request_path_to_instance_resource_path 2021-02-20 17:53:32 -06:00
6026c51490 add some functional tests for our sanity checks 2021-02-20 17:47:36 -06:00
2e0e87fe95 begin rewriting path to resource resolver
this code was getting too messy and scattered, and I realized that Flask
wasn't doing as much as I thought it was here, so now we need more
safety and sanity checks
2021-02-20 17:42:58 -06:00
b6aa125b8d add sane_lists to markdown extensions
this fixes stuff like

* foo
* bar

1. hax
2021-02-13 11:07:00 -06:00
15142054da tweak the appearance of footnotes 2021-02-12 19:37:25 -06:00
dc81ef35de float image left/right for inlining in an article 2021-02-12 12:51:43 -06:00
c292f33334 CSS for framing an image inline the article 2021-02-12 12:51:12 -06:00
1c052b8409 pin bandit in requirements-dev since 1.7.0 is weird in tox 2021-02-12 09:28:12 -06:00
7cf8a427ce add an .img-25 for 25% wide images 2021-02-12 09:26:10 -06:00
e8a749d9ba Revert "tweak the base text line height, again"
This reverts commit 1878d5951b.

the more I look at this, the more I like the old text spacing
2021-02-12 09:21:21 -06:00
ae72fe87b5 class to center an image as a block element
this is effectively a replacement for div.splash means of getting a
centered header image, and can be used anywhere
2021-02-12 09:19:35 -06:00
bb0e71e9e4 give *all* images max-width of the inner column
this was done for the giant splash logo but I should really just
restrain this everywhere
2021-02-12 09:18:54 -06:00
3bfdacdb6d add attr_list to markdown extensions
this will lead to me putting less HTML in the .md files, which is a good
thing
2021-02-12 09:15:41 -06:00
e6d2015de5 use smarty markdown extension for dashes, ellipses 2021-02-11 19:05:01 -06:00
56eb767e33 don't let sub/superscripts affect line height 2021-02-11 18:53:57 -06:00
07031fe667 enable footnotes extra for markdown 2021-02-11 18:36:48 -06:00
48c6e8495a provide some styling of footnotes 2021-02-11 18:20:42 -06:00
4f45943775 initialize markdown on a per-page basis
the footnote extra expects to only parse one document over the Markup's
lifetime, and writes the footnotes to the bottom of every page that is
rendered (again assuming only one) with links back to the reference

having one parser for the entire app, naturally, introduced
ever-increasing footnote links and every footnote on the site showing up
on every page. this was not intended

in some light testing, doing this per-request has a nominal effect on
performance
2021-02-11 18:17:26 -06:00
b26ea6a661 add html tag in order to specify lang="en" 2021-02-11 09:36:24 -06:00
1878d5951b tweak the base text line height, again 2021-02-11 09:35:21 -06:00
829165ad8c style link underline same color as the hover 2021-02-11 09:35:21 -06:00
7d982b96c9 tweak text colors; less normal, more bold 2021-02-11 09:35:21 -06:00
5e41cde52e use a flexbox for the header sections
this is better than a float because I have always kind of hated how
floating divs work, and this also orders and displays the navs better in
elinks
2021-02-11 00:23:19 -06:00
ad33cf2e83 replace section tags with div tags
syntactically incorrect usage, as picked up by a W3C validator
2021-02-11 00:08:19 -06:00
87ad48d8d2 add mdx-linkify to markdown extensions 2021-01-22 09:51:53 -06:00
8a6f4d6b45 test multi-line metadata entries 2021-01-17 23:58:57 -06:00
c25fefa9e3 add opengraph metadata to pages, via Markdown meta 2021-01-17 23:02:14 -06:00
b0795999fe make splash images look better on small devices 2020-12-14 16:26:08 -06:00
aaced9d0e1 add polycephaly-style figure support
this is really pushing my patience for CSS, but I've always thought this
looked nice, so I'm going to try to retain it
2020-12-14 16:25:35 -06:00
4042932240 tone down the line-height a bit
I think the color changes and using viewport magic has helped
readability a bit
2020-12-08 18:56:49 -06:00
49ab2befb6 disable browser resize magic, do viewport magic instead 2020-12-08 18:47:02 -06:00
fbf6a81e0b use my old "square" logo as favicon
also provide a backgroundless version as a possible splash page image
2020-12-08 18:46:04 -06:00
dabf9f7544 more corrections of the link colors 2020-12-08 18:27:55 -06:00
dcf173ab61 add a test to ensure style selection works 2020-12-08 17:12:35 -06:00
d2c1c2e3ce why did I make user styles a config setting???
this moves it into the code, where it's sensible, and leaves the default
to the config
2020-12-08 16:43:20 -06:00
3fcf916317 requirements bump 2020-12-08 16:33:19 -06:00
67e1890629 increase the line height for readability(?) 2020-12-07 21:55:56 -06:00
e1cb541ea5 highlight links in light theme as in dark theme 2020-12-07 21:55:32 -06:00
93e9c8dc24 tweaks to the dark theme 2020-12-07 21:54:43 -06:00
7cf11986c5 user-selectable light and dark themes
cookies, template rendering with different CSS files via default or
request param or cookie, etc.
2020-10-30 00:19:19 -05:00
5ca483a904 configurable markdown extensions
meta is always loaded, because the code expects it
2020-10-29 23:51:58 -05:00
fe7d61e1f7 actually style the white bg beyond the viewport scroll 2020-10-25 18:05:48 -05:00
1398cfe3db put some sidebars on the site for readability 2020-10-25 17:48:19 -05:00
f63de031f6 tox updates: run py38, combine coverage, dist-as-dir 2020-10-20 16:07:49 -05:00
46bce5a0a5 recompile all requirements, add flake8-mutable 2020-10-20 16:05:17 -05:00
0af0f4e8aa tox.ini updates, use requirements-dev.txt, fix pathing 2020-06-23 13:33:15 -05:00
08896a18c1 reorganize requirements-dev.in, add dlint and flake8-fixme, bandit 2020-06-23 13:30:49 -05:00
ea7c9a1e07 let TODOs through linting, but warn about them 2020-06-22 19:09:39 -05:00
63da59efd5 enable flake8-logging-format violations 2020-06-22 18:50:13 -05:00
c7d4a1c930 add any suppressed flake8-fixme messages in the fail-open run 2020-06-22 18:49:34 -05:00
421d0e6f8e properly create the symlink to dist/ across multiple runs of tox 2020-06-22 18:48:22 -05:00
5c1fc93ff9 combine tox deps in order to unconfuse flake8-isort
with pytest not being included in the lint environment, flake8-isort
didn't know how to treat it vs. incorporealcms imports, leading to false
positives only inside tox. this makes it so that certain packages
(defined in base deps) can be imported in any/all envs, because they
show up in analyzed/imported/etc code rather than being merely tools
2020-06-22 18:48:18 -05:00
7b5f7ff00b add dlint and flake8-fixme 2020-06-20 10:48:46 -05:00
9db5189c65 add flake8-isort, with a caveat 2020-06-19 20:23:23 -05:00
ab2d754e43 reorganize tox.ini a bit and use pytest-cov rather than coverage directly 2020-06-19 20:01:06 -05:00
0f7495bf2b add the ability to redirect a file-looking request to a dir
if the client has requested /foo, and foo is actually a directory,
this redirects the client to /foo/
2020-06-19 19:58:12 -05:00
cf8f0325a2 fix /most/ isort problems, but conftest.py is being weird 2020-06-19 19:54:01 -05:00
718b217868 add flake8 and many plugins to requirements-dev, for vim's sake 2020-06-19 19:40:01 -05:00
ebaccbd0ad organize tests a bit better between unit and functional tests 2020-06-18 23:36:51 -05:00
63f13398e0 versioneer.py doesn't need to be included in the package 2020-06-18 23:29:37 -05:00
605a82680d add bandit and flake8 plugins to tox, remove redundant deps 2020-06-18 17:39:34 -05:00
14f6125f4e use new-style tox.ini, add flake8-docstrings, add docstrings 2020-06-17 20:18:43 -05:00
21f65813fb properly run pytest + cov in the tox env 2020-06-17 16:34:50 -05:00
f77aebb097 replace CI tools with tox invocation 2020-06-16 23:00:49 -05:00
5994b73b2e give tables a lighter border 2020-06-14 10:56:57 -05:00
dadc902c49 put a bit of a background behind blockquote
closes #2
2020-06-14 10:55:24 -05:00
5c8251d01a explicitly set the footer margin-top 2020-06-14 10:01:07 -05:00
29498504cc get the actual pinned requirements in setup.py 2020-05-28 17:00:58 -05:00
ce06de78a8 tests misleadingly had a leading /, need to append it ourselves 2020-05-28 16:52:43 -05:00
beea0c80bf CSS: slightly tweak/specify the text size/height 2020-05-28 12:18:28 -05:00
ab977f7e81 header CSS tweaks 2020-05-28 12:18:04 -05:00
05f879ab80 display untitled-page paths as /path rather than path.md 2020-05-28 12:17:27 -05:00
059108c37b rewrite generate_parent_navs
* works on a path now, not a file location
* as such is sliiiiiightly easier to understand
* now also puts the current page in the nav
* fixed failing test where this caused an error (rather than 404) on
  non-existent paths
2020-05-28 12:09:59 -05:00
0993147dea give tables a bottom margin
otherwise they look bad, for instance, at the very end of the page, too
close to the "Last modified" text.
2020-05-28 08:20:24 -05:00
9e97cb097e requirements bump; tests pass 2020-05-28 08:13:55 -05:00
da2476bbda enable table support in the markdown parser 2020-04-05 10:25:46 -05:00
576ffc359c show some navigation on every page
closes #1
2020-03-15 20:33:23 -05:00
582cc9a2d1 rename: page_file_resolver -> resolve_page_file 2020-03-15 18:52:49 -05:00
5ce44ba31c move display_page path resolution logging to DEBUG 2020-03-15 18:43:41 -05:00
ed0dab14f3 tweak request logging, log response info 2020-03-15 18:43:15 -05:00
2634c144a7 support a media/ dir under the instance dir
this is for miscellaneous files that should be served directly rather
than being a page route
2020-03-08 11:48:49 -05:00
16373d3e55 custom styling to links, lighten up text a bit 2020-03-08 11:32:34 -05:00
53 changed files with 1954 additions and 762 deletions

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ __pycache__/
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
dist
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/

47
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,47 @@
# 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:" or "Bug:" so I can do a high level of
prioritization.
## Providing Code
Some guidelines:
* 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.
### Contributing
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.
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 GPLv2 or later.

947
LICENSE
View File

@@ -1,626 +1,339 @@
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Version 3, 29 June 2007
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
Copyright © 2007 Free Software Foundation, Inc. <http s ://fsf.org/> 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
Everyone is permitted to copy and distribute verbatim copies of this license of this license document, but changing it is not allowed.
document, but changing it is not allowed.
Preamble
Preamble
The licenses for most software are designed to take away your
The GNU General Public License is a free, copyleft license for software and freedom to share and change it. By contrast, the GNU General Public
other kinds of works. License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
The licenses for most software and other practical works are designed to take General Public License applies to most of the Free Software
away your freedom to share and change the works. By contrast, the GNU General Foundation's software and to any other program whose authors commit to
Public License is intended to guarantee your freedom to share and change all using it. (Some other Free Software Foundation software is covered by
versions of a program--to make sure it remains free software for all its users. the GNU Lesser General Public License instead.) You can apply it to
We, the Free Software Foundation, use the GNU General Public License for most your programs, too.
of our software; it applies also to any other work released this way by its
authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
When we speak of free software, we are referring to freedom, not price. Our have the freedom to distribute copies of free software (and charge for
General Public Licenses are designed to make sure that you have the freedom this service if you wish), that you receive source code or can get it
to distribute copies of free software (and charge for them if you wish), that if you want it, that you can change the software or use pieces of it
you receive source code or can get it if you want it, that you can change in new free programs; and that you know you can do these things.
the software or use pieces of it in new free programs, and that you know you
can do these things. To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
To protect your rights, we need to prevent others from denying you these rights These restrictions translate to certain responsibilities for you if you
or asking you to surrender the rights. Therefore, you have certain responsibilities distribute copies of the software, or if you modify it.
if you distribute copies of the software, or if you modify it: responsibilities
to respect the freedom of others. For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
For example, if you distribute copies of such a program, whether gratis or you have. You must make sure that they, too, receive or can get the
for a fee, you must pass on to the recipients the same freedoms that you received. source code. And you must show them these terms so they know their
You must make sure that they, too, receive or can get the source code. And rights.
you must show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and
Developers that use the GNU GPL protect your rights with two steps: (1) assert (2) offer you this license which gives you legal permission to copy,
copyright on the software, and (2) offer you this License giving you legal distribute and/or modify the software.
permission to copy, distribute and/or modify it.
Also, for each author's protection and ours, we want to make certain
For the developers' and authors' protection, the GPL clearly explains that that everyone understands that there is no warranty for this free
there is no warranty for this free software. For both users' and authors' software. If the software is modified by someone else and passed on, we
sake, the GPL requires that modified versions be marked as changed, so that want its recipients to know that what they have is not the original, so
their problems will not be attributed erroneously to authors of previous versions. that any problems introduced by others will not reflect on the original
authors' reputations.
Some devices are designed to deny users access to install or run modified
versions of the software inside them, although the manufacturer can do so. Finally, any free program is threatened constantly by software
This is fundamentally incompatible with the aim of protecting users' freedom patents. We wish to avoid the danger that redistributors of a free
to change the software. The systematic pattern of such abuse occurs in the program will individually obtain patent licenses, in effect making the
area of products for individuals to use, which is precisely where it is most program proprietary. To prevent this, we have made it clear that any
unacceptable. Therefore, we have designed this version of the GPL to prohibit patent must be licensed for everyone's free use or not licensed at all.
the practice for those products. If such problems arise substantially in other
domains, we stand ready to extend this provision to those domains in future The precise terms and conditions for copying, distribution and
versions of the GPL, as needed to protect the freedom of users. modification follow.
Finally, every program is threatened constantly by software patents. States GNU GENERAL PUBLIC LICENSE
should not allow patents to restrict development and use of software on general-purpose TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
computers, but in those that do, we wish to avoid the special danger that
patents applied to a free program could make it effectively proprietary. To 0. This License applies to any program or other work which contains
prevent this, the GPL assures that patents cannot be used to render the program a notice placed by the copyright holder saying it may be distributed
non-free. under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
The precise terms and conditions for copying, distribution and modification means either the Program or any derivative work under copyright law:
follow. that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
TERMS AND CONDITIONS language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
0. Definitions.
Activities other than copying, distribution and modification are not
"This License" refers to version 3 of the GNU General Public License. covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
"Copyright" also means copyright-like laws that apply to other kinds of works, is covered only if its contents constitute a work based on the
such as semiconductor masks. Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
"The Program" refers to any copyrightable work licensed under this License.
Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals 1. You may copy and distribute verbatim copies of the Program's
or organizations. source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
To "modify" a work means to copy from or adapt all or part of the work in copyright notice and disclaimer of warranty; keep intact all the
a fashion requiring copyright permission, other than the making of an exact notices that refer to this License and to the absence of any warranty;
copy. The resulting work is called a "modified version" of the earlier work and give any other recipients of the Program a copy of this License
or a work "based on" the earlier work. along with the Program.
A "covered work" means either the unmodified Program or a work based on the You may charge a fee for the physical act of transferring a copy, and
Program. you may at your option offer warranty protection in exchange for a fee.
To "propagate" a work means to do anything with it that, without permission, 2. You may modify your copy or copies of the Program or any portion
would make you directly or secondarily liable for infringement under applicable of it, thus forming a work based on the Program, and copy and
copyright law, except executing it on a computer or modifying a private copy. distribute such modifications or work under the terms of Section 1
Propagation includes copying, distribution (with or without modification), above, provided that you also meet all of these conditions:
making available to the public, and in some countries other activities as
well. a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
To "convey" a work means any kind of propagation that enables other parties
to make or receive copies. Mere interaction with a user through a computer b) You must cause any work that you distribute or publish, that in
network, with no transfer of a copy, is not conveying. whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
An interactive user interface displays "Appropriate Legal Notices" to the parties under the terms of this License.
extent that it includes a convenient and prominently visible feature that
(1) displays an appropriate copyright notice, and (2) tells the user that c) If the modified program normally reads commands interactively
there is no warranty for the work (except to the extent that warranties are when run, you must cause it, when started running for such
provided), that licensees may convey the work under this License, and how interactive use in the most ordinary way, to print or display an
to view a copy of this License. If the interface presents a list of user commands announcement including an appropriate copyright notice and a
or options, such as a menu, a prominent item in the list meets this criterion. notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
1. Source Code. these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
The "source code" for a work means the preferred form of the work for making does not normally print such an announcement, your work based on
modifications to it. "Object code" means any non-source form of a work. the Program is not required to print an announcement.)
A "Standard Interface" means an interface that either is an official standard These requirements apply to the modified work as a whole. If
defined by a recognized standards body, or, in the case of interfaces specified identifiable sections of that work are not derived from the Program,
for a particular programming language, one that is widely used among developers and can be reasonably considered independent and separate works in
working in that language. themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
The "System Libraries" of an executable work include anything, other than distribute the same sections as part of a whole which is a work based
the work as a whole, that (a) is included in the normal form of packaging on the Program, the distribution of the whole must be on the terms of
a Major Component, but which is not part of that Major Component, and (b) this License, whose permissions for other licensees extend to the
serves only to enable use of the work with that Major Component, or to implement entire whole, and thus to each and every part regardless of who wrote it.
a Standard Interface for which an implementation is available to the public
in source code form. A "Major Component", in this context, means a major essential Thus, it is not the intent of this section to claim rights or contest
component (kernel, window system, and so on) of the specific operating system your rights to work written entirely by you; rather, the intent is to
(if any) on which the executable work runs, or a compiler used to produce exercise the right to control the distribution of derivative or
the work, or an object code interpreter used to run it. collective works based on the Program.
The "Corresponding Source" for a work in object code form means all the source In addition, mere aggregation of another work not based on the Program
code needed to generate, install, and (for an executable work) run the object with the Program (or with a work based on the Program) on a volume of
code and to modify the work, including scripts to control those activities. a storage or distribution medium does not bring the other work under
However, it does not include the work's System Libraries, or general-purpose the scope of this License.
tools or generally available free programs which are used unmodified in performing
those activities but which are not part of the work. For example, Corresponding 3. You may copy and distribute the Program (or a work based on it,
Source includes interface definition files associated with source files for under Section 2) in object code or executable form under the terms of
the work, and the source code for shared libraries and dynamically linked Sections 1 and 2 above provided that you also do one of the following:
subprograms that the work is specifically designed to require, such as by
intimate data communication or control flow between those subprograms and a) Accompany it with the complete corresponding machine-readable
other parts of the work. source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
The Corresponding Source need not include anything that users can regenerate
automatically from other parts of the Corresponding Source. b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
The Corresponding Source for a work in source code form is that same work. cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
2. Basic Permissions. distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
All rights granted under this License are granted for the term of copyright
on the Program, and are irrevocable provided the stated conditions are met. c) Accompany it with the information you received as to the offer
This License explicitly affirms your unlimited permission to run the unmodified to distribute corresponding source code. (This alternative is
Program. The output from running a covered work is covered by this License allowed only for noncommercial distribution and only if you
only if the output, given its content, constitutes a covered work. This License received the program in object code or executable form with such
acknowledges your rights of fair use or other equivalent, as provided by copyright an offer, in accord with Subsection b above.)
law.
The source code for a work means the preferred form of the work for
You may make, run and propagate covered works that you do not convey, without making modifications to it. For an executable work, complete source
conditions so long as your license otherwise remains in force. You may convey code means all the source code for all modules it contains, plus any
covered works to others for the sole purpose of having them make modifications associated interface definition files, plus the scripts used to
exclusively for you, or provide you with facilities for running those works, control compilation and installation of the executable. However, as a
provided that you comply with the terms of this License in conveying all material special exception, the source code distributed need not include
for which you do not control copyright. Those thus making or running the covered anything that is normally distributed (in either source or binary
works for you must do so exclusively on your behalf, under your direction form) with the major components (compiler, kernel, and so on) of the
and control, on terms that prohibit them from making any copies of your copyrighted operating system on which the executable runs, unless that component
material outside their relationship with you. itself accompanies the executable.
Conveying under any other circumstances is permitted solely under the conditions If distribution of executable or object code is made by offering
stated below. Sublicensing is not allowed; section 10 makes it unnecessary. access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
3. Protecting Users' Legal Rights From Anti-Circumvention Law. distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
No covered work shall be deemed part of an effective technological measure
under any applicable law fulfilling obligations under article 11 of the WIPO 4. You may not copy, modify, sublicense, or distribute the Program
copyright treaty adopted on 20 December 1996, or similar laws prohibiting except as expressly provided under this License. Any attempt
or restricting circumvention of such measures. otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
When you convey a covered work, you waive any legal power to forbid circumvention However, parties who have received copies, or rights, from you under
of technological measures to the extent such circumvention is effected by this License will not have their licenses terminated so long as such
exercising rights under this License with respect to the covered work, and parties remain in full compliance.
you disclaim any intention to limit operation or modification of the work
as a means of enforcing, against the work's users, your or third parties' 5. You are not required to accept this License, since you have not
legal rights to forbid circumvention of technological measures. signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
4. Conveying Verbatim Copies. prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
You may convey verbatim copies of the Program's source code as you receive Program), you indicate your acceptance of this License to do so, and
it, in any medium, provided that you conspicuously and appropriately publish all its terms and conditions for copying, distributing or modifying
on each copy an appropriate copyright notice; keep intact all notices stating the Program or works based on it.
that this License and any non-permissive terms added in accord with section
7 apply to the code; keep intact all notices of the absence of any warranty; 6. Each time you redistribute the Program (or any work based on the
and give all recipients a copy of this License along with the Program. Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
You may charge any price or no price for each copy that you convey, and you these terms and conditions. You may not impose any further
may offer support or warranty protection for a fee. restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
5. Conveying Modified Source Versions. this License.
You may convey a work based on the Program, or the modifications to produce 7. If, as a consequence of a court judgment or allegation of patent
it from the Program, in the form of source code under the terms of section infringement or for any other reason (not limited to patent issues),
4, provided that you also meet all of these conditions: conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
a) The work must carry prominent notices stating that you modified it, and excuse you from the conditions of this License. If you cannot
giving a relevant date. distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
b) The work must carry prominent notices stating that it is released under may not distribute the Program at all. For example, if a patent
this License and any conditions added under section 7. This requirement modifies license would not permit royalty-free redistribution of the Program by
the requirement in section 4 to "keep intact all notices". all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
c) You must license the entire work, as a whole, under this License to anyone refrain entirely from distribution of the Program.
who comes into possession of a copy. This License will therefore apply, along
with any applicable section 7 additional terms, to the whole of the work, If any portion of this section is held invalid or unenforceable under
and all its parts, regardless of how they are packaged. This License gives any particular circumstance, the balance of the section is intended to
no permission to license the work in any other way, but it does not invalidate apply and the section as a whole is intended to apply in other
such permission if you have separately received it. circumstances.
d) If the work has interactive user interfaces, each must display Appropriate It is not the purpose of this section to induce you to infringe any
Legal Notices; however, if the Program has interactive interfaces that do patents or other property right claims or to contest validity of any
not display Appropriate Legal Notices, your work need not make them do so. such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
A compilation of a covered work with other separate and independent works, implemented by public license practices. Many people have made
which are not by their nature extensions of the covered work, and which are generous contributions to the wide range of software distributed
not combined with it such as to form a larger program, in or on a volume of through that system in reliance on consistent application of that
a storage or distribution medium, is called an "aggregate" if the compilation system; it is up to the author/donor to decide if he or she is willing
and its resulting copyright are not used to limit the access or legal rights to distribute software through any other system and a licensee cannot
of the compilation's users beyond what the individual works permit. Inclusion impose that choice.
of a covered work in an aggregate does not cause this License to apply to
the other parts of the aggregate. This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
6. Conveying Non-Source Forms.
8. If the distribution and/or use of the Program is restricted in
You may convey a covered work in object code form under the terms of sections certain countries either by patents or by copyrighted interfaces, the
4 and 5, provided that you also convey the machine-readable Corresponding original copyright holder who places the Program under this License
Source under the terms of this License, in one of these ways: may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
a) Convey the object code in, or embodied in, a physical product (including countries not thus excluded. In such case, this License incorporates
a physical distribution medium), accompanied by the Corresponding Source fixed the limitation as if written in the body of this License.
on a durable physical medium customarily used for software interchange.
9. The Free Software Foundation may publish revised and/or new versions
b) Convey the object code in, or embodied in, a physical product (including of the General Public License from time to time. Such new versions will
a physical distribution medium), accompanied by a written offer, valid for be similar in spirit to the present version, but may differ in detail to
at least three years and valid for as long as you offer spare parts or customer address new problems or concerns.
support for that product model, to give anyone who possesses the object code
either (1) a copy of the Corresponding Source for all the software in the Each version is given a distinguishing version number. If the Program
product that is covered by this License, on a durable physical medium customarily specifies a version number of this License which applies to it and "any
used for software interchange, for a price no more than your reasonable cost later version", you have the option of following the terms and conditions
of physically performing this conveying of source, or (2) access to copy the either of that version or of any later version published by the Free
Corresponding Source from a network server at no charge. Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
c) Convey individual copies of the object code with a copy of the written Foundation.
offer to provide the Corresponding Source. This alternative is allowed only
occasionally and noncommercially, and only if you received the object code 10. If you wish to incorporate parts of the Program into other free
with such an offer, in accord with subsection 6b. programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
d) Convey the object code by offering access from a designated place (gratis Software Foundation, write to the Free Software Foundation; we sometimes
or for a charge), and offer equivalent access to the Corresponding Source make exceptions for this. Our decision will be guided by the two goals
in the same way through the same place at no further charge. You need not of preserving the free status of all derivatives of our free software and
require recipients to copy the Corresponding Source along with the object of promoting the sharing and reuse of software generally.
code. If the place to copy the object code is a network server, the Corresponding
Source may be on a different server (operated by you or a third party) that NO WARRANTY
supports equivalent copying facilities, provided you maintain clear directions
next to the object code saying where to find the Corresponding Source. Regardless 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
of what server hosts the Corresponding Source, you remain obligated to ensure FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
that it is available for as long as needed to satisfy these requirements. OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
e) Convey the object code using peer-to-peer transmission, provided you inform OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
other peers where the object code and Corresponding Source of the work are MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
being offered to the general public at no charge under subsection 6d. TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
A separable portion of the object code, whose source code is excluded from REPAIR OR CORRECTION.
the Corresponding Source as a System Library, need not be included in conveying
the object code work. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
A "User Product" is either (1) a "consumer product", which means any tangible REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
personal property which is normally used for personal, family, or household INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
purposes, or (2) anything designed or sold for incorporation into a dwelling. OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
In determining whether a product is a consumer product, doubtful cases shall TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
be resolved in favor of coverage. For a particular product received by a particular YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
user, "normally used" refers to a typical or common use of that class of product, PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
regardless of the status of the particular user or of the way in which the POSSIBILITY OF SUCH DAMAGES.
particular user actually uses, or expects or is expected to use, the product.
A product is a consumer product regardless of whether the product has substantial END OF TERMS AND CONDITIONS
commercial, industrial or non-consumer uses, unless such uses represent the
only significant mode of use of the product. How to Apply These Terms to Your New Programs
"Installation Information" for a User Product means any methods, procedures, If you develop a new program, and you want it to be of the greatest
authorization keys, or other information required to install and execute modified possible use to the public, the best way to achieve this is to make it
versions of a covered work in that User Product from a modified version of free software which everyone can redistribute and change under these terms.
its Corresponding Source. The information must suffice to ensure that the
continued functioning of the modified object code is in no case prevented To do so, attach the following notices to the program. It is safest
or interfered with solely because modification has been made. to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
If you convey an object code work under this section in, or with, or specifically the "copyright" line and a pointer to where the full notice is found.
for use in, a User Product, and the conveying occurs as part of a transaction
in which the right of possession and use of the User Product is transferred <one line to give the program's name and a brief idea of what it does.>
to the recipient in perpetuity or for a fixed term (regardless of how the Copyright (C) <year> <name of author>
transaction is characterized), the Corresponding Source conveyed under this
section must be accompanied by the Installation Information. But this requirement This program is free software; you can redistribute it and/or modify
does not apply if neither you nor any third party retains the ability to install it under the terms of the GNU General Public License as published by
modified object code on the User Product (for example, the work has been installed the Free Software Foundation; either version 2 of the License, or
in ROM). (at your option) any later version.
The requirement to provide Installation Information does not include a requirement This program is distributed in the hope that it will be useful,
to continue to provide support service, warranty, or updates for a work that but WITHOUT ANY WARRANTY; without even the implied warranty of
has been modified or installed by the recipient, or for the User Product in MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
which it has been modified or installed. Access to a network may be denied GNU General Public License for more details.
when the modification itself materially and adversely affects the operation
of the network or violates the rules and protocols for communication across You should have received a copy of the GNU General Public License along
the network. with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Corresponding Source conveyed, and Installation Information provided, in accord
with this section must be in a format that is publicly documented (and with
an implementation available to the public in source code form), and must require
no special password or key for unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this License
by making exceptions from one or more of its conditions. Additional permissions
that are applicable to the entire Program shall be treated as though they
were included in this License, to the extent that they are valid under applicable
law. If additional permissions apply only to part of the Program, that part
may be used separately under those permissions, but the entire Program remains
governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any
additional permissions from that copy, or from any part of it. (Additional
permissions may be written to require their own removal in certain cases when
you modify the work.) You may place additional permissions on material, added
by you to a covered work, for which you have or can give appropriate copyright
permission.
Notwithstanding any other provision of this License, for material you add
to a covered work, you may (if authorized by the copyright holders of that
material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of
sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author
attributions in that material or in the Appropriate Legal Notices displayed
by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring
that modified versions of such material be marked in reasonable ways as different
from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors
of the material; or
e) Declining to grant rights under trademark law for use of some trade names,
trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by
anyone who conveys the material (or modified versions of it) with contractual
assumptions of liability to the recipient, for any liability that these contractual
assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered "further restrictions"
within the meaning of section 10. If the Program as you received it, or any
part of it, contains a notice stating that it is governed by this License
along with a term that is a further restriction, you may remove that term.
If a license document contains a further restriction but permits relicensing
or conveying under this License, you may add to a covered work material governed
by the terms of that license document, provided that the further restriction
does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place,
in the relevant source files, a statement of the additional terms that apply
to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form
of a separately written license, or stated as exceptions; the above requirements
apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided
under this License. Any attempt otherwise to propagate or modify it is void,
and will automatically terminate your rights under this License (including
any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from
a particular copyright holder is reinstated (a) provisionally, unless and
until the copyright holder explicitly and finally terminates your license,
and (b) permanently, if the copyright holder fails to notify you of the violation
by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently
if the copyright holder notifies you of the violation by some reasonable means,
this is the first time you have received notice of violation of this License
(for any work) from that copyright holder, and you cure the violation prior
to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses
of parties who have received copies or rights from you under this License.
If your rights have been terminated and not permanently reinstated, you do
not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy
of the Program. Ancillary propagation of a covered work occurring solely as
a consequence of using peer-to-peer transmission to receive a copy likewise
does not require acceptance. However, nothing other than this License grants
you permission to propagate or modify any covered work. These actions infringe
copyright if you do not accept this License. Therefore, by modifying or propagating
a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives
a license from the original licensors, to run, modify and propagate that work,
subject to this License. You are not responsible for enforcing compliance
by third parties with this License.
An "entity transaction" is a transaction transferring control of an organization,
or substantially all assets of one, or subdividing an organization, or merging
organizations. If propagation of a covered work results from an entity transaction,
each party to that transaction who receives a copy of the work also receives
whatever licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the Corresponding
Source of the work from the predecessor in interest, if the predecessor has
it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights
granted or affirmed under this License. For example, you may not impose a
license fee, royalty, or other charge for exercise of rights granted under
this License, and you may not initiate litigation (including a cross-claim
or counterclaim in a lawsuit) alleging that any patent claim is infringed
by making, using, selling, offering for sale, or importing the Program or
any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this License
of the Program or a work on which the Program is based. The work thus licensed
is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned or controlled
by the contributor, whether already acquired or hereafter acquired, that would
be infringed by some manner, permitted by this License, of making, using,
or selling its contributor version, but do not include claims that would be
infringed only as a consequence of further modification of the contributor
version. For purposes of this definition, "control" includes the right to
grant patent sublicenses in a manner consistent with the requirements of this
License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent
license under the contributor's essential patent claims, to make, use, sell,
offer for sale, import and otherwise run, modify and propagate the contents
of its contributor version.
In the following three paragraphs, a "patent license" is any express agreement
or commitment, however denominated, not to enforce a patent (such as an express
permission to practice a patent or covenant not to sue for patent infringement).
To "grant" such a patent license to a party means to make such an agreement
or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the
Corresponding Source of the work is not available for anyone to copy, free
of charge and under the terms of this License, through a publicly available
network server or other readily accessible means, then you must either (1)
cause the Corresponding Source to be so available, or (2) arrange to deprive
yourself of the benefit of the patent license for this particular work, or
(3) arrange, in a manner consistent with the requirements of this License,
to extend the patent license to downstream recipients. "Knowingly relying"
means you have actual knowledge that, but for the patent license, your conveying
the covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that country
that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement,
you convey, or propagate by procuring conveyance of, a covered work, and grant
a patent license to some of the parties receiving the covered work authorizing
them to use, propagate, modify or convey a specific copy of the covered work,
then the patent license you grant is automatically extended to all recipients
of the covered work and works based on it.
A patent license is "discriminatory" if it does not include within the scope
of its coverage, prohibits the exercise of, or is conditioned on the non-exercise
of one or more of the rights that are specifically granted under this License.
You may not convey a covered work if you are a party to an arrangement with
a third party that is in the business of distributing software, under which
you make payment to the third party based on the extent of your activity of
conveying the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by you
(or copies made from those copies), or (b) primarily for and in connection
with specific products or compilations that contain the covered work, unless
you entered into that arrangement, or that patent license was granted, prior
to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied
license or other defenses to infringement that may otherwise be available
to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise)
that contradict the conditions of this License, they do not excuse you from
the conditions of this License. If you cannot convey a covered work so as
to satisfy simultaneously your obligations under this License and any other
pertinent obligations, then as a consequence you may not convey it at all.
For example, if you agree to terms that obligate you to collect a royalty
for further conveying from those to whom you convey the Program, the only
way you could satisfy both those terms and this License would be to refrain
entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to
link or combine any covered work with a work licensed under version 3 of the
GNU Affero General Public License into a single combined work, and to convey
the resulting work. The terms of this License will continue to apply to the
part which is the covered work, but the special requirements of the GNU Affero
General Public License, section 13, concerning interaction through a network
will apply to the combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the
GNU General Public License from time to time. Such new versions will be similar
in spirit to the present version, but may differ in detail to address new
problems or concerns.
Each version is given a distinguishing version number. If the Program specifies
that a certain numbered version of the GNU General Public License "or any
later version" applies to it, you have the option of following the terms and
conditions either of that numbered version or of any later version published
by the Free Software Foundation. If the Program does not specify a version
number of the GNU General Public License, you may choose any version ever
published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of
the GNU General Public License can be used, that proxy's public statement
of acceptance of a version permanently authorizes you to choose that version
for the Program.
Later license versions may give you additional or different permissions. However,
no additional obligations are imposed on any author or copyright holder as
a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM
PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM
AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO
USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot
be given local legal effect according to their terms, reviewing courts shall
apply local law that most closely approximates an absolute waiver of all civil
liability in connection with the Program, unless a warranty or assumption
of liability accompanies a copy of the Program in return for a fee. END OF
TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible
use to the public, the best way to achieve this is to make it free software
which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach
them to the start of each source file to most effectively state the exclusion
of warranty; and each file should have at least the "copyright" line and a
pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <http s ://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like If the program is interactive, make it output a short notice like this
this when it starts in an interactive mode: when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author> Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
This is free software, and you are welcome to redistribute it under certain
conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands might parts of the General Public License. Of course, the commands you use may
be different; for a GUI interface, you would use an "about box". be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or your
if any, to sign a "copyright disclaimer" for the program, if necessary. For school, if any, to sign a "copyright disclaimer" for the program, if
more information on this, and how to apply and follow the GNU GPL, see <http necessary. Here is a sample; alter the names:
s ://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program Yoyodyne, Inc., hereby disclaims all copyright interest in the program
into proprietary programs. If your program is a subroutine library, you may `Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General Public library. If this is what you want to do, use the GNU Lesser General
License instead of this License. But first, please read <http s ://www.gnu.org/ Public License instead of this License.
licenses /why-not-lgpl.html>.

View File

@@ -1,5 +1,4 @@
graft incorporealcms/static graft incorporealcms/static
graft incorporealcms/templates graft incorporealcms/templates
include versioneer.py
global-exclude *.pyc global-exclude *.pyc
global-exclude *.swp global-exclude *.swp

View File

@@ -1,3 +1,44 @@
# incorporeal-cms # 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.
## 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 General Public License as published by the Free Software Foundation; either version
2 of the License (included in this repository), or (at your option) any later version.

View File

@@ -1,4 +1,4 @@
"""create_app application factory function and similar things.""" """An application for running my Markdown-based sites."""
import logging import logging
import os import os
from logging.config import dictConfig from logging.config import dictConfig
@@ -12,6 +12,7 @@ del get_versions
def create_app(instance_path=None, test_config=None): def create_app(instance_path=None, test_config=None):
"""Create the Flask app, with allowances for customizing path and test settings."""
app = Flask(__name__, instance_relative_config=True, instance_path=instance_path) app = Flask(__name__, instance_relative_config=True, instance_path=instance_path)
# if it doesn't already exist, create the instance folder # if it doesn't already exist, create the instance folder
@@ -32,9 +33,17 @@ def create_app(instance_path=None, test_config=None):
@app.before_request @app.before_request
def log_request(): def log_request():
logger.info("REQUEST: [ %s ]", request.path) logger.info("REQUEST: %s %s", request.method, request.path)
from . import pages @app.after_request
def log_response(response):
logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status)
return response
from . import error_pages, pages
app.register_blueprint(pages.bp) app.register_blueprint(pages.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)
return app return app

View File

@@ -32,4 +32,27 @@ class Config(object):
}, },
} }
MARKDOWN_EXTENSIONS = ['extra', 'mdx_linkify', 'sane_lists', 'smarty', 'tables']
MARKDOWN_EXTENSION_CONFIGS = {
'extra': {
'attr_list': {},
'footnotes': {
'UNIQUE_IDS': True,
},
},
'smarty': {
'smart_dashes': True,
'smart_quotes': False,
'smart_angled_quotes': False,
'smart_ellipses': True,
},
}
MEDIA_DIR = 'media'
# customizations
DEFAULT_PAGE_STYLE = 'light'
TITLE_SUFFIX = 'incorporeal.org' TITLE_SUFFIX = 'incorporeal.org'
CONTACT_EMAIL = 'bss@incorporeal.org'
# specify FAVICON in your instance config.py to override the suou icon

View File

@@ -0,0 +1,17 @@
"""Error page views for 400, 404, etc."""
from incorporealcms.lib import render
def bad_request(error):
"""Display 400 error messaging."""
return render('400.html'), 400
def internal_server_error(error):
"""Display 500 error messaging."""
return render('500.html'), 500
def page_not_found(error):
"""Display 404 error messaging."""
return render('404.html'), 404

51
incorporealcms/lib.py Normal file
View File

@@ -0,0 +1,51 @@
"""Miscellaneous helper functions and whatnot."""
import logging
import markdown
from flask import current_app as app
from flask import make_response, render_template, request
logger = logging.getLogger(__name__)
def get_meta_str(md, key):
"""Provide the page's (parsed in Markup obj md) metadata for the specified key, or '' if unset."""
return " ".join(md.Meta.get(key)) if md.Meta.get(key) else ""
def init_md():
"""Initialize the Markdown parser.
This used to done at the app level in __init__, but extensions like footnotes apparently
assume the parser to only live for the length of parsing one document, and create double
footnote ref links if the one parser sees the same document multiple times.
"""
# initialize markdown parser from config, but include
# extensions our app depends on, like the meta extension
return markdown.Markdown(extensions=app.config['MARKDOWN_EXTENSIONS'] + ['meta'],
extension_configs=app.config['MARKDOWN_EXTENSION_CONFIGS'])
def render(template_name_or_list, **context):
"""Wrap Flask's render_template.
* 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',
}
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']))
resp = make_response(render_template(template_name_or_list, **context))
if selected_style:
resp.set_cookie('user-style', selected_style)
return resp

View File

@@ -2,47 +2,188 @@
import datetime import datetime
import logging import logging
import os import os
import re
import markdown from flask import Blueprint, Markup, abort
from flask import Blueprint, Markup, abort, current_app as app, render_template from flask import current_app as app
from flask import redirect, request, send_from_directory
from tzlocal import get_localzone from tzlocal import get_localzone
from incorporealcms.lib import get_meta_str, init_md, render
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
bp = Blueprint('pages', __name__, url_prefix='/') bp = Blueprint('pages', __name__, url_prefix='/')
md = markdown.Markdown(extensions=['meta'])
@bp.route('/', defaults={'path': 'index'}) @bp.route('/', defaults={'path': 'index'})
@bp.route('/<path:path>') @bp.route('/<path:path>')
def display_page(path): def display_page(path):
"""Get the file contents of the requested path and render the file.""" """Get the file contents of the requested path and render the file."""
resolved_path = page_file_resolver(path) try:
logger.info("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)
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: try:
with app.open_instance_resource(resolved_path, 'r') as entry_file: with app.open_instance_resource(resolved_path, 'r') as entry_file:
logger.debug("file '%s' found", resolved_path)
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone()) mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone())
entry = entry_file.read() entry = entry_file.read()
except FileNotFoundError: except OSError:
logger.warning("requested path '%s' (resolved path '%s') not found!", path, resolved_path) logger.exception("resolved path '%s' could not be opened!", resolved_path)
abort(404) abort(500)
else: else:
md = init_md()
content = Markup(md.convert(entry)) content = Markup(md.convert(entry))
logger.debug("file metadata: %s", md.Meta) logger.debug("file metadata: %s", md.Meta)
title = " ".join(md.Meta.get('title')) if md.Meta.get('title') else ""
return render_template('base.html', title=title, content=content, mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z')) parent_navs = generate_parent_navs(resolved_path)
page_name = (get_meta_str(md, 'title') if md.Meta.get('title') else
f'/{instance_resource_path_to_request_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)
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 page_file_resolver(path): def request_path_to_instance_resource_path(path):
"""Manipulate the request path to find appropriate page file. """Turn a request URL path to the full page path.
* convert dir requests to index files flask.Flask.open_instance_resource will open a file like /etc/hosts if you tell it to,
which sucks, so we do a lot of work here to make sure we have a valid request to
Worth noting, Flask already does stuff like convert '/foo/../../../bar' to something inside the pages dir.
'/bar', so we don't need to take care around file access here.
""" """
if path.endswith('/'): # check if the path is allowed
path = f'{path}index' base_dir = os.path.realpath(f'{app.instance_path}/pages/')
path = f'pages/{path}.md' verbatim_path = os.path.abspath(os.path.join(base_dir, path))
return 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
# 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
# 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'
# 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(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 resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'markdown'
def instance_resource_path_to_request_path(path):
"""Reverse a (presumed to exist) disk path to the canonical path that would show up in a Flask route.
This does not include the leading /, so aside from the root index case, this should be
bidirectional.
"""
return re.sub(r'^pages/', '', re.sub(r'.md$', '', re.sub(r'index.md$', '', path)))
def generate_parent_navs(path):
"""Create a series of paths/links to navigate up from the given resource path."""
if path == 'pages/index.md':
# bail and return the title suffix (generally the domain name) as a terminal case
return [(app.config['TITLE_SUFFIX'], '/')]
else:
if path.endswith('index.md'):
# index case: one dirname for foo/bar/index.md -> foo/bar, one for foo/bar -> foo
parent_resource_dir = os.path.dirname(os.path.dirname(path))
else:
# usual case: foo/buh.md -> foo
parent_resource_dir = os.path.dirname(path)
# generate the request path (i.e. what the link will be) for this path, and
# also the resource path of this parent (which is always a dir, so always index.md)
request_path = f'/{instance_resource_path_to_request_path(path)}'
parent_resource_path = os.path.join(parent_resource_dir, 'index.md')
logger.debug("resource path: '%s'; request path: '%s'; parent resource path: '%s'", path,
request_path, parent_resource_path)
# for issues regarding parser reuse (see lib.init_md) we reinitialize the parser here
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):
"""Given a request path, e.g. "/foo/bar/baz/", turn it into breadcrumby text "baz"."""
undired = path.rstrip('/')
leaf = undired[undired.rfind('/'):]
return leaf.strip('/')

View File

@@ -0,0 +1,226 @@
html {
font-family: sans-serif;
padding: 0;
}
body {
margin: 0;
text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
.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;
}
a:visited {
font-weight: bold;
text-decoration: none;
}
a:hover {
font-weight: bold;
text-decoration: none;
}
a:active {
font-weight: bold;
text-decoration: none;
}
div.header {
display: flex;
justify-content: space-between;
font-size: 0.75em;
padding: 0.25em 0.5em;
}
div.header a {
border-bottom: none;
}
div.content {
font-size: 11pt;
padding: 0 1em;
line-height: 1.5em;
}
sup, sub {
vertical-align: baseline;
position: relative;
top: -0.4em;
}
sub {
top: 0.4em;
}
footer {
clear: both;
display: block;
font-size: 75%;
color: #999;
padding: 0 1em;
padding-bottom: 16px;
margin-top: 15px;
}
table {
border-collapse: collapse;
}
table, th, td {
padding: 5px;
border: 1px solid #ccc;
margin-bottom: 15px;
}
blockquote {
background-color: rgba(120, 120, 120, 0.3);
padding: 1px 10px;
}
.splash {
margin-top: 1em;
margin-bottom: 1em;
text-align: center;
}
img {
max-width: 100%;
}
.img-25 {
max-width: 25% !important;
}
.img-50 {
max-width: 50% !important;
}
.img-center {
display: block;
margin-left: auto;
margin-right: auto;
}
.img-left {
float: left;
margin-right: 1em;
}
.img-right {
float: right;
margin-left: 1em;
}
.img-frame {
padding: 5px;
}
/* For screens with width smaller than 400px */
.figure-left .figure-right {
max-width: 95%;
float: none;
margin-left: 10px;
margin-right: 10px;
}
/* For larger screens */
@media only screen and (min-width: 400px) {
.figure-left {
float: left;
margin-top: 0;
margin-left: 0;
}
.figure-right {
float: right;
margin-top: 0;
margin-right: 0;
}
}
figure {
max-width: 400px;
padding: 5px;
margin: 10px;
margin-top: 0;
margin-bottom: 5px;
}
figure img {
max-width: 100%;
height: auto;
}
figcaption {
font-family: "Times New Roman", serif;
color: #777777;
text-align: center;
font-style: italic;
line-height: 1.3em;
margin-top: 5px;
}
.thumbnail-image {
width: 150px;
height: auto;
margin: 5px;
display: inline;
}
.footnote {
font-size: 0.8em;
}
.footnote p {
margin: 0;
}
.footnote-ref:link, .footnote-ref:visited, .footnote-ref:hover, .footnote-ref:active {
border-bottom: none;
font-weight: normal;
}

View File

@@ -0,0 +1,72 @@
@import '/static/css/base.css';
html {
color: #CCC;
}
body {
background: black;
}
strong {
color: #EEE;
}
.site-wrap {
background: #111;
border: 1px solid #222;
border-top: none;
border-bottom: none;
}
h1, h2, h3, h4, h5, h6 {
color: #B31D15;
}
a:link, a:visited {
color: #EEE;
border-bottom: 1px dotted #EEE;
}
a:hover, a:active {
color: #B31D15;
border-bottom: 1px dotted #B31D15;
}
div.header {
background: #222;
border-bottom: 1px solid #222;
color: #BBB;
}
div.header a {
color: #BBB;
}
table, th, td {
border: 1px solid #333;
}
th {
background: #333;
}
blockquote {
background-color: rgba(120, 120, 120, 0.3);
border: 1px solid #222;
}
.img-frame {
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid #333;
}
figure {
background: #222;
border: 1px solid #333;
}
figcaption {
color: #BBB;
}

View File

@@ -0,0 +1,72 @@
@import '/static/css/base.css';
html {
color: #222;
}
body {
background: #999;
}
strong {
color: #111;
}
.site-wrap {
background: white;
border: 1px solid #ddd;
border-top: none;
border-bottom: none;
}
h1, h2, h3, h4, h5, h6 {
color: #811610;
}
a:link, a:visited {
color: #111;
border-bottom: 1px dotted #111;
}
a:hover, a:active {
color: #811610;
border-bottom: 1px dotted #811610;
}
div.header {
background: #EEE;
border-bottom: 1px solid #CCC;
color: #666;
}
div.header a {
color: #666;
}
table, th, td {
border: 1px solid #ccc;
}
th {
background: #eee;
}
blockquote {
background-color: rgba(120, 120, 120, 0.1);
border: 1px solid #CCC;
}
.img-frame {
background-color: rgba(0, 0, 0, 0.1);
border: 1px solid #BBB;
}
figure {
background: #EFEFEF;
border: 1px solid #CCCCCC;
}
figcaption {
color: #777777;
}

View File

@@ -0,0 +1,8 @@
.img-25 {
max-width: 25% !important;
}
.img-50 {
max-width: 50% !important;
}

View File

@@ -1,14 +0,0 @@
html {
font-family: sans-serif;
padding: 0 1em;
}
h1,h2,h3,h4,h5,h6 {
color: #811610;
}
footer {
display: block;
font-size: 75%;
color: #999;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block header %}
<div class="header">
<a href="/">{{ config.TITLE_SUFFIX }}</a>
</div>
{% endblock %}
{% block body %}
<div class="content">
<h1>BAD REQUEST</h1>
<p>You're doing something you're not supposed to. Stop it?</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block header %}
<div class="header">
<a href="/">{{ config.TITLE_SUFFIX }}</a>
</div>
{% endblock %}
{% block body %}
<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 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
you're looking for.</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block header %}
<div class="header">
<a href="/">{{ config.TITLE_SUFFIX }}</a>
</div>
{% endblock %}
{% block body %}
<div class="content">
<h1>INTERNAL SERVER ERROR</h1>
<p>Something bad happened! Please email me at {{ config.CONTACT_EMAIL }} and tell me what happened.</p>
</div>
{% endblock %}

View File

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

View File

@@ -1,10 +1,38 @@
<!doctype html> <!doctype html>
<title>{{ title }}{% if title %} - {% endif %}{{ config.TITLE_SUFFIX }}</title> <html lang="en">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <title>{{ title }}</title>
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}"> {% if title %}<meta property="og:title" content="{{ title }}">{% endif %}
<section class="content"> {% if description %}<meta property="og:description" content="{{ description }}">{% endif %}
{{ content }} {% if image %}<meta property="og:image" content="{{ image }}">{% endif %}
</section> <meta property="og:url" content="{{ base_url }}">
<footer> <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="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 %}>
{% block header %}
<div class="header">
<div class="nav">
{% for nav in navs %}
<a href="{{ nav.1 }}">{{ nav.0 }}</a>
{% if not loop.last %} &raquo; {% endif %}
{% endfor %}
</div>
<div class="styles">
<a href="?style=dark">[dark]</a>
<a href="?style=light">[light]</a>
<a href="?style=plain">[plain]</a>
</div>
</div>
{% endblock %}
{% block body %}
<div class="content">
{{ content }}
</div>
<footer>
<i>Last modified: {{ mtime }}</i> <i>Last modified: {{ mtime }}</i>
</footer> </footer>
{% endblock %}
</div>
</html>

View File

@@ -1,7 +1,24 @@
-r requirements.in -r requirements.in
flake8 # python code quality stuff # testing runner, test reporting, packages used during testing (e.g. requests-mock), etc.
pytest
pytest-cov
# linting and other static code analysis
bandit==1.6.2 # pinned because 1.7.0 wasn't running right in tox
dlint
flake8
flake8-blind-except
flake8-builtins
flake8-docstrings
flake8-executable
flake8-fixme
flake8-isort
flake8-logging-format
flake8-mutable
# maintenance utilities and tox
pip-tools # pip-compile pip-tools # pip-compile
pytest # unit tests tox # CI stuff
pytest-cov # coverage in unit tests tox-wheel # build wheels in tox
versioneer # automatic version numbering versioneer # automatic version numbering

View File

@@ -4,35 +4,152 @@
# #
# 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==19.3.0 # via pytest appdirs==1.4.4
click==7.0 # via flask, pip-tools # via virtualenv
coverage==5.0.3 # via pytest-cov attrs==20.3.0
entrypoints==0.3 # via flake8 # via pytest
flake8==3.7.9 # via -r requirements/requirements-dev.in bandit==1.6.2
flask==1.1.1 # via -r requirements/requirements.in # via -r requirements/requirements-dev.in
importlib-metadata==1.5.0 # via pluggy, pytest bleach==3.3.0
itsdangerous==1.1.0 # via flask # via mdx-linkify
jinja2==2.11.1 # via flask click==7.1.2
markdown==3.2.1 # via -r requirements/requirements.in # via
markupsafe==1.1.1 # via jinja2 # flask
mccabe==0.6.1 # via flake8 # pip-tools
more-itertools==8.2.0 # via pytest coverage==5.5
packaging==20.3 # via pytest # via pytest-cov
pip-tools==4.5.1 # via -r requirements/requirements-dev.in distlib==0.3.1
pluggy==0.13.1 # via pytest # via virtualenv
py==1.8.1 # via pytest dlint==0.11.0
pycodestyle==2.5.0 # via flake8 # via -r requirements/requirements-dev.in
pyflakes==2.1.1 # via flake8 filelock==3.0.12
pyparsing==2.4.6 # via packaging # via
pytest-cov==2.8.1 # via -r requirements/requirements-dev.in # tox
pytest==5.3.5 # via -r requirements/requirements-dev.in, pytest-cov # virtualenv
pytz==2019.3 # via tzlocal flake8-blind-except==0.2.0
six==1.14.0 # via packaging, pip-tools # via -r requirements/requirements-dev.in
tzlocal==2.0.0 # via -r requirements/requirements.in flake8-builtins==1.5.3
versioneer==0.18 # via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
wcwidth==0.1.8 # via pytest flake8-docstrings==1.6.0
werkzeug==1.0.0 # via flask # via -r requirements/requirements-dev.in
zipp==3.1.0 # via importlib-metadata 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.9.1
# via
# -r requirements/requirements-dev.in
# dlint
# flake8-builtins
# flake8-docstrings
# flake8-executable
# flake8-isort
# flake8-mutable
flask==1.1.2
# via -r requirements/requirements.in
gitdb==4.0.7
# via gitpython
gitpython==3.1.14
# via bandit
iniconfig==1.1.1
# via pytest
isort==5.8.0
# via flake8-isort
itsdangerous==1.1.0
# via flask
jinja2==2.11.3
# via flask
markdown==3.3.4
# via
# -r requirements/requirements.in
# mdx-linkify
markupsafe==1.1.1
# via jinja2
mccabe==0.6.1
# via flake8
mdx-linkify==2.1
# via -r requirements/requirements.in
packaging==20.9
# via
# bleach
# pytest
# tox
pbr==5.5.1
# via stevedore
pep517==0.10.0
# via pip-tools
pip-tools==6.1.0
# via -r requirements/requirements-dev.in
pluggy==0.13.1
# via
# pytest
# tox
py==1.10.0
# via
# pytest
# tox
pycodestyle==2.7.0
# via flake8
pydocstyle==6.0.0
# via flake8-docstrings
pyflakes==2.3.1
# via flake8
pyparsing==2.4.7
# via packaging
pytest-cov==2.11.1
# via -r requirements/requirements-dev.in
pytest==6.2.3
# via
# -r requirements/requirements-dev.in
# pytest-cov
pytz==2021.1
# via tzlocal
pyyaml==5.4.1
# via bandit
six==1.15.0
# via
# bandit
# bleach
# tox
# virtualenv
smmap==4.0.0
# via gitdb
snowballstemmer==2.1.0
# via pydocstyle
stevedore==3.3.0
# via bandit
testfixtures==6.17.1
# via flake8-isort
toml==0.10.2
# via
# pep517
# pytest
# tox
tox-wheel==0.6.0
# via -r requirements/requirements-dev.in
tox==3.23.0
# via
# -r requirements/requirements-dev.in
# tox-wheel
tzlocal==2.1
# via -r requirements/requirements.in
versioneer==0.19
# via -r requirements/requirements-dev.in
virtualenv==20.4.3
# via tox
webencodings==0.5.1
# via bleach
werkzeug==1.0.1
# via flask
wheel==0.36.2
# via tox-wheel
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools # setuptools

View File

@@ -1,3 +1,4 @@
Flask # general purpose web service and web server stuff Flask # general purpose web service and web server stuff
Markdown # markdown rendering in templates Markdown # markdown rendering in templates
mdx-linkify # convert URLs in the text to clickable links
tzlocal # identifying system's local timezone tzlocal # identifying system's local timezone

View File

@@ -4,15 +4,35 @@
# #
# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in # pip-compile --output-file=requirements/requirements.txt requirements/requirements.in
# #
click==7.0 # via flask bleach==3.3.0
flask==1.1.1 # via -r requirements/requirements.in # via mdx-linkify
itsdangerous==1.1.0 # via flask click==7.1.2
jinja2==2.11.1 # via flask # via flask
markdown==3.2.1 # via -r requirements/requirements.in flask==1.1.2
markupsafe==1.1.1 # via jinja2 # via -r requirements/requirements.in
pytz==2019.3 # via tzlocal itsdangerous==1.1.0
tzlocal==2.0.0 # via -r requirements/requirements.in # via flask
werkzeug==1.0.0 # via flask jinja2==2.11.3
# via flask
# The following packages are considered to be unsafe in a requirements file: markdown==3.3.4
# setuptools # via
# -r requirements/requirements.in
# mdx-linkify
markupsafe==1.1.1
# via jinja2
mdx-linkify==2.1
# via -r requirements/requirements.in
packaging==20.9
# via bleach
pyparsing==2.4.7
# via packaging
pytz==2021.1
# via tzlocal
six==1.15.0
# via bleach
tzlocal==2.1
# via -r requirements/requirements.in
webencodings==0.5.1
# via bleach
werkzeug==1.0.1
# via flask

View File

@@ -1,23 +1,3 @@
[coverage:run]
branch = True
omit =
.venv/*
incorporealcms/_version.py
setup.py
tests/*
versioneer.py
source =
incorporealcms
[flake8]
exclude = .git,.venv,__pycache__,versioneer.py,incorporealcms/_version.py
max-line-length = 120
[tool:pytest]
addopts = --cov=. --cov-report=term --cov-report=term-missing
log_cli = 1
log_cli_level = DEBUG
[versioneer] [versioneer]
VCS = git VCS = git
style = pep440-post style = pep440-post

View File

@@ -1,5 +1,6 @@
"""Setuptools configuration.""" """Setuptools configuration."""
import os import os
from setuptools import find_packages, setup from setuptools import find_packages, setup
import versioneer import versioneer
@@ -8,8 +9,8 @@ HERE = os.path.dirname(os.path.abspath(__file__))
def extract_requires(): def extract_requires():
"""Get pinned requirements from requirements.in.""" """Get pinned requirements from requirements.txt."""
with open(os.path.join(HERE, 'requirements/requirements.in'), 'r') as reqs: with open(os.path.join(HERE, 'requirements/requirements.txt'), 'r') as reqs:
return [line.split(' ')[0] for line in reqs if not line[0] in ('-', '#')] return [line.split(' ')[0] for line in reqs if not line[0] in ('-', '#')]

View File

@@ -10,6 +10,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
@pytest.fixture @pytest.fixture
def app(): def app():
"""Create the Flask application, with test settings."""
app = create_app(instance_path=os.path.join(HERE, 'instance')) app = create_app(instance_path=os.path.join(HERE, 'instance'))
yield app yield app
@@ -17,4 +18,5 @@ def app():
@pytest.fixture @pytest.fixture
def client(app): def client(app):
"""Create a test client based on the test app."""
return app.test_client() return app.test_client()

212
tests/functional_tests.py Normal file
View File

@@ -0,0 +1,212 @@
"""Test page requests."""
import re
from unittest.mock import patch
def test_page_that_exists(client):
"""Test that the app can serve a basic file at the index."""
response = client.get('/')
assert response.status_code == 200
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
def test_files_outside_pages_do_not_get_served(client):
"""Test that page pathing doesn't break out of the instance/pages/ dir, and the error uses my error page."""
response = client.get('/../unreachable')
assert response.status_code == 400
assert b'You\'re doing something you\'re not supposed to. Stop it?' in response.data
def test_internal_server_error_serves_error_page(client):
"""Test that various exceptions serve up the 500 page."""
response = client.get('/actually-a-png')
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
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):
"""Test that we clean up requests as desired."""
response = client.get('/../../')
assert response.status_code == 400
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
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
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
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: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
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:description"' not in response.data
assert b'<meta property="og:image"' not in response.data
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
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):
"""Test that pages have modified timestamps in them."""
response = client.get('/')
assert response.status_code == 200
assert re.search(r'Last modified: ....-..-.. ..:..:.. ...', response.data.decode()) is not None
def test_that_page_request_redirects_to_directory(client):
"""Test that a request to /foo reirects to /foo/, if foo is a directory.
This might be useful in cases where a formerly page-only page has been
converted to a directory with subpages.
"""
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):
"""Test that a request to /foo/ serves the index page, if foo is a directory."""
response = client.get('/subdir/')
assert response.status_code == 200
assert b'another page' in response.data
def test_setting_selected_style_includes_cookie(client):
"""Test that a request with style=foo sets the cookie and renders appropriately."""
response = client.get('/')
style_cookie = next((cookie for cookie in client.cookie_jar if cookie.name == 'user-style'), None)
assert style_cookie is None
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 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 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

@@ -1,3 +1,5 @@
"""Configure the test application."""
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'formatters': { 'formatters': {

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

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

@@ -0,0 +1,3 @@
Title:
some words are here

View File

@@ -0,0 +1,3 @@
Test page
word --- word

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,6 @@
Title: title for the page
Description: description of this page
made even longer
Image: http://buh.com/test.img
hello

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,3 @@
Title: SUB!
# subdir-with-title

View File

@@ -0,0 +1 @@
test page

View File

@@ -0,0 +1 @@
# another page

View File

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

View File

@@ -0,0 +1,3 @@
Title: Page
# hello

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 @@
this file exists but the app should not serve it.

View File

@@ -13,6 +13,36 @@ def test_config():
assert create_app(instance_path=instance_path, test_config={"TESTING": True}).testing assert create_app(instance_path=instance_path, test_config={"TESTING": True}).testing
def test_markdown_meta_extension_always():
"""Test that the markdown meta extension is always loaded, even if not specified."""
app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'MARKDOWN_EXTENSIONS': []})
client = app.test_client()
response = client.get('/')
assert response.status_code == 200
assert b'<title>Index - incorporeal.org</title>' in response.data
def test_custom_markdown_extensions_work():
"""Test we can change extensions via config, and that they work.
This used to test smarty, but that's added by default now, so we test
that it can be removed by overriding the option.
"""
app = create_app(instance_path=os.path.join(HERE, 'instance'))
client = app.test_client()
response = client.get('/mdash-or-triple-dash')
assert response.status_code == 200
assert b'word &mdash; word' in response.data
app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'MARKDOWN_EXTENSIONS': []})
client = app.test_client()
response = client.get('/mdash-or-triple-dash')
assert response.status_code == 200
assert b'word --- word' in response.data
def test_title_override(): def test_title_override():
"""Test that a configuration with a specific title overrides the default.""" """Test that a configuration with a specific title overrides the default."""
instance_path = os.path.join(HERE, 'instance') instance_path = os.path.join(HERE, 'instance')
@@ -20,4 +50,21 @@ def test_title_override():
client = app.test_client() client = app.test_client()
response = client.get('/no-title') response = client.get('/no-title')
assert response.status_code == 200 assert response.status_code == 200
assert b'<title>suou.net</title>' in response.data assert b'<title>/no-title - suou.net</title>' in response.data
def test_media_file_access(client):
"""Test that media files are served, and properly."""
response = client.get('/media/favicon.png')
assert response.status_code == 200
assert response.headers['content-type'] == 'image/png'
def test_favicon_override():
"""Test that a configuration with a specific favicon overrides the default."""
instance_path = os.path.join(HERE, 'instance')
app = create_app(instance_path=instance_path, test_config={'FAVICON': '/media/foo.png'})
client = app.test_client()
response = client.get('/no-title')
assert response.status_code == 200
assert b'<link rel="icon" href="/media/foo.png">' in response.data

View File

@@ -1,46 +1,221 @@
"""Test page views and helper methods.""" """Unit test helper methods."""
import re import pytest
from werkzeug.http import dump_cookie
from incorporealcms.pages import page_file_resolver 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)
def test_page_file_resolver_dir_to_index(): def test_generate_page_navs_index(app):
assert page_file_resolver('foo/') == 'pages/foo/index.md' """Test that the index page has navs to the root (itself)."""
with app.app_context():
assert generate_parent_navs('pages/index.md') == [('incorporeal.org', '/')]
def test_page_file_resolver_subdir_to_index(): def test_generate_page_navs_subdir_index(app):
assert page_file_resolver('foo/bar/') == 'pages/foo/bar/index.md' """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/')]
def test_page_file_resolver_other_requests_fine(): def test_generate_page_navs_subdir_real_page(app):
assert page_file_resolver('foo/baz') == 'pages/foo/baz.md' """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/'),
('Page', '/subdir/page')]
def test_page_that_exists(client): def test_generate_page_navs_subdir_with_title_parsing_real_page(app):
response = client.get('/') """Test that title metadata is used in the nav text."""
assert response.status_code == 200 with app.app_context():
assert b'<h1>test index</h1>' in response.data assert generate_parent_navs('pages/subdir-with-title/page.md') == [
('incorporeal.org', '/'),
('SUB!', '/subdir-with-title/'),
('page', '/subdir-with-title/page')
]
def test_page_that_doesnt_exist(client): def test_generate_page_navs_subdir_with_no_index(app):
response = client.get('/ohuesthaoeusth') """Test that breadcrumbs still generate even if a subdir doesn't have an index.md."""
assert response.status_code == 404 with app.app_context():
assert generate_parent_navs('pages/no-index-dir/page.md') == [
('incorporeal.org', '/'),
('/no-index-dir/', '/no-index-dir/'),
('page', '/no-index-dir/page')
]
def test_page_with_title_metadata(client): def test_render_with_user_dark_theme(app):
response = client.get('/') """Test that a request with the dark theme selected renders the dark theme."""
assert response.status_code == 200 cookie = dump_cookie("user-style", 'dark')
assert b'<title>Index - incorporeal.org</title>' in response.data 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
def test_page_without_title_metadata(client): def test_render_with_user_light_theme(app):
response = client.get('/no-title') """Test that a request with the light theme selected renders the light theme."""
assert response.status_code == 200 with app.test_request_context():
assert b'<title>incorporeal.org</title>' in response.data assert b'light.css' in render('base.html').data
assert b'<h1>this page doesn\'t have a title!</h1>' in response.data assert b'dark.css' not in render('base.html').data
def test_page_has_modified_timestamp(client): def test_render_with_no_user_theme(app):
response = client.get('/') """Test that a request with no theme set renders the light theme."""
assert response.status_code == 200 with app.test_request_context():
assert re.search(r'Last modified: ....-..-.. ..:..:.. ...', response.data.decode()) is not None assert b'light.css' in render('base.html').data
assert b'dark.css' not in render('base.html').data
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', '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', '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', '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', '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', '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', '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', 'markdown'))
def test_request_path_to_instance_resource_path_permission_error_on_ref_above_pages(app):
"""Test that attempts to get above the base dir ("/../../foo") fail."""
with app.test_request_context():
with pytest.raises(PermissionError):
assert request_path_to_instance_resource_path('../unreachable')
def test_request_path_to_instance_resource_path_isadirectory_on_file_like_req_for_dir(app):
"""Test that a request for e.g. '/foo' when foo is a dir indicate to redirect."""
with app.test_request_context():
with pytest.raises(IsADirectoryError):
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():
with pytest.raises(FileNotFoundError):
assert request_path_to_instance_resource_path('nthanpthpnh')
def test_request_path_to_instance_resource_path_absolute_file_errors(app):
"""Test that a request for something not on disk errors."""
with app.test_request_context():
with pytest.raises(PermissionError):
assert request_path_to_instance_resource_path('/etc/hosts')
def test_instance_resource_path_to_request_path_on_index(app):
"""Test index.md -> /."""
with app.test_request_context():
assert instance_resource_path_to_request_path('index.md') == ''
def test_instance_resource_path_to_request_path_on_page(app):
"""Test no-title.md -> no-title."""
with app.test_request_context():
assert instance_resource_path_to_request_path('no-title.md') == 'no-title'
def test_instance_resource_path_to_request_path_on_subdir(app):
"""Test subdir/index.md -> subdir/."""
with app.test_request_context():
assert instance_resource_path_to_request_path('subdir/index.md') == 'subdir/'
def test_instance_resource_path_to_request_path_on_subdir_and_page(app):
"""Test subdir/page.md -> subdir/page."""
with app.test_request_context():
assert instance_resource_path_to_request_path('subdir/page.md') == 'subdir/page'
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_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_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():
"""Test various conversions from request path to leaf nodes for display in the breadcrumbs."""
assert request_path_to_breadcrumb_display('/foo') == 'foo'
assert request_path_to_breadcrumb_display('/foo/') == 'foo'
assert request_path_to_breadcrumb_display('/foo/bar') == 'bar'
assert request_path_to_breadcrumb_display('/foo/bar/') == 'bar'
assert request_path_to_breadcrumb_display('/') == ''

104
tox.ini Normal file
View File

@@ -0,0 +1,104 @@
# tox (https://tox.readthedocs.io/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = begin,py37,py38,coverage,security,lint,bundle
[testenv]
# build a wheel and test it
wheel = true
wheel_build_env = build
# whitelist commands we need
whitelist_externals = cp
# install everything via requirements-dev.txt, so that developer environment
# is the same as the tox environment (for ease of use/no weird gotchas in
# local dev results vs. tox results) and also to avoid ticky-tacky maintenance
# of "oh this particular env has weird results unless I install foo" --- just
# shotgun blast install everything everywhere
deps =
-rrequirements/requirements-dev.txt
[testenv:build]
# require setuptools when building
deps = setuptools
[testenv:begin]
# clean up potential previous coverage runs
skip_install = true
commands = coverage erase
[testenv:py37]
# run pytest with coverage
commands =
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
[testenv:py38]
# run pytest with coverage
commands =
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
[testenv:coverage]
# report on coverage runs from above
skip_install = true
commands =
coverage report --fail-under=95 --show-missing
[testenv:security]
# run security checks
#
# again it seems the most valuable here to run against the packaged code
commands =
bandit {envsitepackagesdir}/incorporealcms/ -r
[testenv:lint]
# run style checks
commands =
flake8
- flake8 --disable-noqa --ignore= --select=E,W,F,C,D,A,G,B,I,T,M,DUO
[testenv:bundle]
# take extra actions (build sdist, sphinx, whatever) to completely package the app
commands =
cp -r {distdir} .
python setup.py sdist
[coverage:paths]
source =
./
.tox/**/site-packages/
[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
[flake8]
enable-extensions = G,M
exclude =
.tox/
versioneer.py
_version.py
instance/
extend-ignore = T101
max-complexity = 10
max-line-length = 120
[isort]
line_length = 120
[pytest]
python_files =
*_tests.py
tests.py
test_*.py