Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ The `VIEW` permissions are enforced server-side via route decorators. The `UI` p
|-----|---------|-------------|
| `WIKI_HOME` | `'home'` | Default page for `/` |
| `WIKI_URL_PREFIX` | `'/help'` | URL prefix for the wiki blueprint |
| `WIKI_URL_PREFIX_DEFAULTS` | `{}` | Value each variable part of the prefix takes when left out |
| `WIKI_CONTENT_DIR` | `'./data'` | Directory for Markdown files |
| `WIKI_UPLOAD_FOLDER` | `'./data/files'` | Directory for uploaded images |
| `WIKI_ALLOWED_EXTENSIONS` | `{'png','jpg','jpeg','gif','svg'}` | Allowed upload types |
Expand Down Expand Up @@ -141,6 +142,19 @@ name does not collide with an argument of the wiki views -- `url` and
Building such a URL from outside the wiki, from a footer for instance, means
passing the value: `url_for('wiki.index', org_code='unifr')`.

One value may be left out of the URL altogether. Name it in
`WIKI_URL_PREFIX_DEFAULTS` and the wiki serves its whole navigation under the
prefix stripped of it as well -- any other variable part keeps its place:

```python
app.config["WIKI_URL_PREFIX"] = "/<org_code>/help"
app.config["WIKI_URL_PREFIX_DEFAULTS"] = {"org_code": "global"}
```

`/help/setup` is then served, `url_for('wiki.index', org_code='global')` builds
`/help/`, and `/global/help/setup` redirects to `/help/setup`. Any other value
keeps its part of the prefix, `/unifr/help/setup`.

The uploaded files hang from the static part of the prefix, `/help/files/` in
the example above, whatever the prefix a reader came through. A WSGI mount
point carries no variable, and neither does the URL of an image written in a
Expand Down
47 changes: 41 additions & 6 deletions flask_wiki/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,58 @@ def init_app(self, app):
self.init_config(app)
prefix = app.config["WIKI_URL_PREFIX"]
self.prefix_variables = VARIABLE_PART.findall(prefix)
# the uploaded files hang from the static part of the prefix: a WSGI
# mount point carries no variable, and neither does the URL of an image
# written in a page
files_prefix = VARIABLE_PART.sub("", prefix)
# the prefix stripped of its variable parts: the uploaded files hang from
# it, as a WSGI mount point carries no variable, and neither does the URL
# of an image written in a page
static_prefix = VARIABLE_PART.sub("", prefix)
app.register_blueprint(blueprint, url_prefix=prefix)
self.init_prefix_defaults(app)
app.add_url_rule(
f"{files_prefix}/files/<filename>",
f"{static_prefix}/files/<filename>",
"uploaded_files",
build_only=True,
)

app.wsgi_app = SharedDataMiddleware(
app.wsgi_app,
{f"{files_prefix}/files": app.config["WIKI_UPLOAD_FOLDER"]},
{f"{static_prefix}/files": app.config["WIKI_UPLOAD_FOLDER"]},
)
app.extensions["flask-wiki"] = self

def init_prefix_defaults(self, app):
"""Serve the default value of a prefix variable without it in the URL.

``WIKI_URL_PREFIX_DEFAULTS`` gives the value each variable of the prefix
takes when a reader leaves it out. Registering every rule a second time
under the prefix stripped of these variables, holding their values as
Werkzeug defaults, is all it takes for the short URL to be served, to be
the one the wiki builds for those values, and to receive a redirect from
the spelled-out one. A variable the key does not name keeps its part of the
prefix, as a rule still needs a value for it, and a value naming no
variable of the prefix is dropped, as no rule would take it.

:param app: Flask application instance
"""
prefix = app.config["WIKI_URL_PREFIX"]
defaults = {
name: value
for name, value in app.config["WIKI_URL_PREFIX_DEFAULTS"].items()
if name in self.prefix_variables
}
if not defaults:
return
stripped = VARIABLE_PART.sub(lambda part: "" if part[1] in defaults else part[0], prefix)
for rule in list(app.url_map.iter_rules()):
if rule.endpoint.startswith(f"{blueprint.name}."):
app.add_url_rule(
rule.rule.replace(prefix, stripped, 1),
endpoint=rule.endpoint,
view_func=app.view_functions[rule.endpoint],
defaults=defaults,
# Flask puts HEAD and OPTIONS back, as it did for the copied rule
methods=rule.methods - {"HEAD", "OPTIONS"},
)

def init_config(self, app):
"""Initialize configuration."""
for k in dir(config):
Expand Down
2 changes: 2 additions & 0 deletions flask_wiki/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
# None means every language of WIKI_LANGUAGES, in order; [] disables the cascade
WIKI_FALLBACK_LANGUAGES = None
WIKI_URL_PREFIX = "/help"
# The value each variable part of the prefix takes when a reader leaves it out
WIKI_URL_PREFIX_DEFAULTS = {}
WIKI_CONTENT_DIR = "./data"
WIKI_UPLOAD_FOLDER = os.path.join(WIKI_CONTENT_DIR, "files")
WIKI_INDEX_DIR = "./index"
Expand Down
100 changes: 100 additions & 0 deletions tests/test_url_prefix.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""Tests for a WIKI_URL_PREFIX carrying a variable part."""

from pathlib import Path
from urllib.parse import urlsplit

import pytest
from flask import url_for
Expand Down Expand Up @@ -84,3 +85,102 @@ def test_each_host_keeps_its_own_prefix(prefixed_client):
for html in (first, second):
assert "first.example.org" not in html
assert "second.example.org" not in html


@pytest.fixture(scope="module")
def default_value_app(tmp_path_factory):
"""Create an application serving one value of the prefix without naming it."""
return make_app(
tmp_path_factory,
WIKI_URL_PREFIX="/<organisation>/help",
WIKI_URL_PREFIX_DEFAULTS={"organisation": "global"},
SERVER_NAME=None,
)


@pytest.fixture(scope="module")
def default_value_client(default_value_app):
"""Create a test client for the application with a default prefix value."""
return default_value_app.test_client()


def test_the_default_value_is_served_without_the_variable_part(default_value_client):
"""Test that the wiki answers under the prefix stripped of its variable part."""
assert default_value_client.get("/help/home/").status_code == 200


def test_spelling_out_the_default_value_redirects_to_the_short_url(default_value_client):
"""Test that the URL naming the default value redirects to the one omitting it."""
response = default_value_client.get("/global/help/home/")

assert response.status_code == 308
assert urlsplit(response.location).path == "/help/home/"


def test_urls_omit_the_default_value(default_value_client):
"""Test that the wiki leaves the default value out of every URL it builds."""
html = default_value_client.get("/help/home/").data.decode()

assert 'href="/help/"' in html
assert 'action="/help/search"' in html
assert 'href="/help/sample/"' in html
assert "/global/help/" not in html


def test_another_value_keeps_its_variable_part(default_value_client):
"""Test that a value other than the default one stays in the URLs."""
html = default_value_client.get("/hepvs/help/home/").data.decode()

assert 'href="/hepvs/help/"' in html
assert 'href="/hepvs/help/sample/"' in html


def test_urls_built_from_outside_honour_the_default_value(default_value_app):
"""Test that naming the default value from outside the wiki builds the short URL."""
with default_value_app.test_request_context():
assert url_for("wiki.index", organisation="global") == "/help/"
assert url_for("wiki.index", organisation="hepvs") == "/hepvs/help/"


@pytest.fixture(scope="module")
def partial_default_client(tmp_path_factory):
"""Create a test client for a prefix whose second variable has no default."""
app = make_app(
tmp_path_factory,
WIKI_URL_PREFIX="/<organisation>/<section>/help",
WIKI_URL_PREFIX_DEFAULTS={"organisation": "global"},
SERVER_NAME=None,
)
return app.test_client()


def test_a_variable_without_a_default_keeps_its_part_of_the_prefix(partial_default_client):
"""Test that only the variables named in the defaults leave the URL."""
assert partial_default_client.get("/staff/help/home/").status_code == 200
assert partial_default_client.get("/help/home/").status_code == 404

html = partial_default_client.get("/staff/help/home/").data.decode()

assert 'href="/staff/help/"' in html
assert 'href="/staff/help/sample/"' in html


def test_spelling_out_the_default_redirects_beside_a_kept_variable(partial_default_client):
"""Test that the URL naming the default value still redirects to the short one."""
response = partial_default_client.get("/global/staff/help/home/")

assert response.status_code == 308
assert urlsplit(response.location).path == "/staff/help/home/"


def test_a_default_naming_no_variable_of_the_prefix_is_ignored(tmp_path_factory):
"""Test that a default the prefix carries no variable for serves nothing new."""
client = make_app(
tmp_path_factory,
WIKI_URL_PREFIX="/<organisation>/help",
WIKI_URL_PREFIX_DEFAULTS={"typo": "global"},
SERVER_NAME=None,
).test_client()

assert client.get("/hepvs/help/home/").status_code == 200
assert client.get("/help/home/").status_code == 404