lektor_ng.admin.webui

src/lektor_ng/admin/webui.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from __future__ import annotations

from typing import TYPE_CHECKING
from wsgiref.util import shift_path_info

from flask import Flask, request

from lektor_ng.admin.context import LektorApp, LektorInfo
from lektor_ng.admin.modules import api, dash, livereload, serve
from lektor_ng.environment import Environment

if TYPE_CHECKING:
    from _typeshed import StrPath
    from _typeshed.wsgi import WSGIApplication


def _common_configuration(app: Flask, debug: bool = False) -> None:
    app.debug = debug
    app.config["PROPAGATE_EXCEPTIONS"] = True


def make_app(
    env: Environment,
    debug: bool = False,
    output_path: StrPath | None = None,
    ui_lang: str = "en",
    verbosity: int = 0,
    extra_flags: dict[str, str] | None = None,
    *,
    admin_path: str = "/admin",
    static_folder: StrPath | None = "static",  # testing
) -> LektorApp:
    if output_path is None:
        output_path = env.project.get_output_path()

    lektor_info = LektorInfo(env, output_path, verbosity, extra_flags)

    # The top-level app has a route that matches anything
    # ("/<path:path>" in the serve blueprint).  That means that if
    # there is another route whose doesn't match based on request
    # method, the serve view will take over and try to serve it.  To
    # prevent this from happening for the paths under /admin, we
    # structure them as a separate flask app.
    admin_app = LektorApp(lektor_info)
    _common_configuration(admin_app, debug=debug)
    admin_app.config["lektor.ui_lang"] = ui_lang
    admin_app.register_blueprint(dash.bp, url_prefix="/")
    admin_app.register_blueprint(api.bp, url_prefix="/api")

    # Serve static files from top-level app
    app = LektorApp(lektor_info, static_url_path=f"{admin_path}/static", static_folder=static_folder)
    _common_configuration(app, debug=debug)
    app.register_blueprint(livereload.bp, url_prefix="/__reload__")
    app.register_blueprint(serve.bp)

    # Pass requests for /admin/... to the admin app
    @app.route(f"{admin_path}/", defaults={"page": ""})
    @app.route(f"{admin_path}/<path:page>", methods=["GET", "POST", "PUT"])
    def admin_view(page: str) -> WSGIApplication:
        environ = request.environ
        # Save top-level SCRIPT_NAME (used by dash)
        environ["lektor.site_root"] = request.root_path
        while environ.get("PATH_INFO", "") != f"/{page}":
            assert environ["PATH_INFO"]
            shift_path_info(request.environ)
        return admin_app.wsgi_app

    # Add rule to construct URL to /admin/edit
    app.add_url_rule(f"{admin_path}/edit", "url.edit", build_only=True)

    return app


WebAdmin = WebUI = make_app