Coverage for src/lektor_ng/admin/webui.py: 97%

35 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-09-01 11:35 +0000

1from __future__ import annotations 

2 

3from typing import TYPE_CHECKING 

4from wsgiref.util import shift_path_info 

5 

6from flask import Flask, request 

7 

8from lektor_ng.admin.context import LektorApp, LektorInfo 

9from lektor_ng.admin.modules import api, dash, livereload, serve 

10from lektor_ng.environment import Environment 

11 

12if TYPE_CHECKING: 

13 from _typeshed import StrPath 

14 from _typeshed.wsgi import WSGIApplication 

15 

16 

17def _common_configuration(app: Flask, debug: bool = False) -> None: 

18 app.debug = debug 

19 app.config["PROPAGATE_EXCEPTIONS"] = True 

20 

21 

22def make_app( 

23 env: Environment, 

24 debug: bool = False, 

25 output_path: StrPath | None = None, 

26 ui_lang: str = "en", 

27 verbosity: int = 0, 

28 extra_flags: dict[str, str] | None = None, 

29 *, 

30 admin_path: str = "/admin", 

31 static_folder: StrPath | None = "static", # testing 

32) -> LektorApp: 

33 if output_path is None: 

34 output_path = env.project.get_output_path() 

35 

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

37 

38 # The top-level app has a route that matches anything 

39 # ("/<path:path>" in the serve blueprint). That means that if 

40 # there is another route whose doesn't match based on request 

41 # method, the serve view will take over and try to serve it. To 

42 # prevent this from happening for the paths under /admin, we 

43 # structure them as a separate flask app. 

44 admin_app = LektorApp(lektor_info) 

45 _common_configuration(admin_app, debug=debug) 

46 admin_app.config["lektor.ui_lang"] = ui_lang 

47 admin_app.register_blueprint(dash.bp, url_prefix="/") 

48 admin_app.register_blueprint(api.bp, url_prefix="/api") 

49 

50 # Serve static files from top-level app 

51 app = LektorApp(lektor_info, static_url_path=f"{admin_path}/static", static_folder=static_folder) 

52 _common_configuration(app, debug=debug) 

53 app.register_blueprint(livereload.bp, url_prefix="/__reload__") 

54 app.register_blueprint(serve.bp) 

55 

56 # Pass requests for /admin/... to the admin app 

57 @app.route(f"{admin_path}/", defaults={"page": ""}) 

58 @app.route(f"{admin_path}/<path:page>", methods=["GET", "POST", "PUT"]) 

59 def admin_view(page: str) -> WSGIApplication: 

60 environ = request.environ 

61 # Save top-level SCRIPT_NAME (used by dash) 

62 environ["lektor.site_root"] = request.root_path 

63 while environ.get("PATH_INFO", "") != f"/{page}": 

64 assert environ["PATH_INFO"] 

65 shift_path_info(request.environ) 

66 return admin_app.wsgi_app 

67 

68 # Add rule to construct URL to /admin/edit 

69 app.add_url_rule(f"{admin_path}/edit", "url.edit", build_only=True) 

70 

71 return app 

72 

73 

74WebAdmin = WebUI = make_app