flask-construct-ui 0.10.0__tar.gz → 0.10.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/PKG-INFO +3 -3
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/README.md +2 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/auth.py +91 -6
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/base.py +34 -3
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/components.py +37 -1
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/crud.py +106 -457
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/data.py +8 -1
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/data_sqlalchemy.py +9 -6
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/orgmode.py +25 -0
- flask_construct_ui-0.10.2/flask_construct_ui/static/js/construct-core.min.js +1 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/themes/whitelabel/css/whitelabel.css +14 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/themes/whitelabel/designs/adminlte.css +15 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/themes/whitelabel/designs/sandstone.css +24 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/_form_field.html +4 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/crud/listindex.html +2 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/crud/related_crud_tab_list.html +99 -4
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/style/_flashes.html +1 -1
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/style/base_full.html +6 -1
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui.egg-info/PKG-INFO +3 -3
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui.egg-info/SOURCES.txt +0 -3
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui.egg-info/requires.txt +0 -3
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/pyproject.toml +2 -2
- flask_construct_ui-0.10.0/flask_construct_ui/static/js/construct-core.min.js +0 -1
- flask_construct_ui-0.10.0/flask_construct_ui/templates/themes/whitelabel/crud/_related_child_form.html +0 -98
- flask_construct_ui-0.10.0/flask_construct_ui/templates/themes/whitelabel/crud/_related_child_list.html +0 -136
- flask_construct_ui-0.10.0/flask_construct_ui/templates/themes/whitelabel/crud/related_crud_tab_child_view.html +0 -33
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/LICENSE +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/__init__.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/csrf.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/forms.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/headless.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/icons.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/l10n/en/main.flt +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/l10n/sv/main.flt +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/language.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/navbar.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/navigation.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/pagination.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/py.typed +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/sanitize.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/schema.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/css/prosemirror-bundle.min.css +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/css/unpoly.min.css +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/js/kanban-dnd.js +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/js/prosemirror-bundle.min.js +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/js/unpoly.min.js +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/LICENSE.txt +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/bootstrap-icons.min.css +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/themes/whitelabel/fonts/material-symbols/LICENSE.txt +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/themes/whitelabel/fonts/material-symbols/material-symbols-outlined.woff2 +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/static/themes/whitelabel/fonts/material-symbols/material-symbols.css +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/structure/base.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/structure/flashes.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/structure/public.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/button.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/calendar.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/calendarmini.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/card.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/chart.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/form.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/grid_layout.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/horizontal_layout.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/kanban.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/live.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/main.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/markup.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/rich_text.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/table.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/tabs.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/text.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/components/vertical_layout.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/crud/_macros.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/crud/_many_related_options.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/crud/_related_options.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/crud/create.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/crud/edit.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/crud/related_crud_tab_form.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/crud/tab_view.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/errors/forbidden.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/navbar/notifications.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/navbar/search_results.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/templates/themes/whitelabel/style/base_partial.html +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui/validation.py +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui.egg-info/dependency_links.txt +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/flask_construct_ui.egg-info/top_level.txt +0 -0
- {flask_construct_ui-0.10.0 → flask_construct_ui-0.10.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flask-construct-ui
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.2
|
|
4
4
|
Summary: A UI toolkit for Flask with CRUD and component features.
|
|
5
5
|
Author-email: Olof Tjerngren <olof@tjerngren.net>
|
|
6
6
|
License-Expression: LGPL-3.0-or-later
|
|
@@ -30,8 +30,6 @@ Requires-Dist: pygal
|
|
|
30
30
|
Requires-Dist: markdown-it-py
|
|
31
31
|
Requires-Dist: SQLAlchemy
|
|
32
32
|
Requires-Dist: orgparse
|
|
33
|
-
Provides-Extra: security
|
|
34
|
-
Requires-Dist: flask-security-too; extra == "security"
|
|
35
33
|
Provides-Extra: oidc
|
|
36
34
|
Requires-Dist: flask-oidc; extra == "oidc"
|
|
37
35
|
Dynamic: license-file
|
|
@@ -42,6 +40,8 @@ A UI toolkit for Flask with CRUD and component features.
|
|
|
42
40
|
Very influenced by flask-admin and flask-restx, used for quick admin UIs.
|
|
43
41
|
Recommended user authentication is oidc if required.
|
|
44
42
|
|
|
43
|
+
> Optional flask-security-too package removed until security issue fixed.
|
|
44
|
+
|
|
45
45
|
## Documentation
|
|
46
46
|
|
|
47
47
|
The [user manual](https://gitlab.com/Resample2545/flask-construct-ui/-/blob/release/manual/index.org?ref_type=heads) best read on the release branch to not get fooled.
|
|
@@ -4,6 +4,8 @@ A UI toolkit for Flask with CRUD and component features.
|
|
|
4
4
|
Very influenced by flask-admin and flask-restx, used for quick admin UIs.
|
|
5
5
|
Recommended user authentication is oidc if required.
|
|
6
6
|
|
|
7
|
+
> Optional flask-security-too package removed until security issue fixed.
|
|
8
|
+
|
|
7
9
|
## Documentation
|
|
8
10
|
|
|
9
11
|
The [user manual](https://gitlab.com/Resample2545/flask-construct-ui/-/blob/release/manual/index.org?ref_type=heads) best read on the release branch to not get fooled.
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
3
5
|
from flask import (
|
|
4
6
|
abort,
|
|
5
7
|
current_app,
|
|
6
8
|
g,
|
|
9
|
+
make_response,
|
|
7
10
|
redirect,
|
|
8
11
|
render_template,
|
|
9
12
|
request,
|
|
10
13
|
url_for,
|
|
11
14
|
)
|
|
15
|
+
from markupsafe import escape
|
|
12
16
|
|
|
13
17
|
# --- Authentication & authorization ---------------------------------------
|
|
14
18
|
# Construct routes are guarded fail-closed: once an ``AuthProvider`` is passed
|
|
@@ -19,10 +23,69 @@ from flask import (
|
|
|
19
23
|
# ``before_request`` in ``base.py`` (see ``auth_protect``), which is why this
|
|
20
24
|
# covers the dynamically-registered RCRUD nested routes too.
|
|
21
25
|
#
|
|
22
|
-
# The boundary is server-side, so it
|
|
23
|
-
# off
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
+
# The boundary is server-side, so it decides the same thing with JavaScript on
|
|
27
|
+
# or off; only the *shape* of the answer differs. Without JS an unauthenticated
|
|
28
|
+
# request gets a plain 302 the browser follows. Under Unpoly a fragment request
|
|
29
|
+
# cannot be answered that way (see ``on_unauthenticated``), so it gets a 401
|
|
30
|
+
# carrying the login URL in ``X-Construct-Login``. An authenticated request
|
|
31
|
+
# lacking the required group gets a server-rendered themed 403 page either way.
|
|
32
|
+
|
|
33
|
+
#: Response header carrying an opaque fingerprint of the auth state a response
|
|
34
|
+
#: was rendered for. The full-page layout stamps the same value on
|
|
35
|
+
#: ``<html data-fcu-auth>``, so the browser can notice that a fragment was
|
|
36
|
+
#: rendered for a *different* session than the surrounding shell.
|
|
37
|
+
AUTH_STAMP_HEADER = "X-Construct-Auth"
|
|
38
|
+
|
|
39
|
+
#: Response header asking the browser to leave the fragment pipeline and load
|
|
40
|
+
#: the given login URL as a full page navigation.
|
|
41
|
+
LOGIN_HEADER = "X-Construct-Login"
|
|
42
|
+
|
|
43
|
+
#: Stamp value for a request with no logged-in user.
|
|
44
|
+
ANONYMOUS_STAMP = "anon"
|
|
45
|
+
|
|
46
|
+
_STAMP_CACHE_KEY = "_construct_auth_stamp"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_unpoly_request() -> bool:
|
|
50
|
+
"""True when the current request was issued by Unpoly (i.e. JS is on)."""
|
|
51
|
+
return bool(request.headers.get("X-Up-Version"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def auth_stamp(provider) -> str:
|
|
55
|
+
"""Opaque fingerprint of the identity this request is being served for.
|
|
56
|
+
|
|
57
|
+
Returns :data:`ANONYMOUS_STAMP` when nobody is logged in, otherwise a hash
|
|
58
|
+
of the user name and their groups. It is hashed so that no user name or
|
|
59
|
+
group ever rides along in a response header — only *changes* are
|
|
60
|
+
observable, which is all the client needs to tell a stale shell from a
|
|
61
|
+
current one. Group membership is folded in so that a re-login as someone
|
|
62
|
+
else, or a permission change, also invalidates the rendered menu.
|
|
63
|
+
"""
|
|
64
|
+
cached = g.get(_STAMP_CACHE_KEY, None)
|
|
65
|
+
if cached is not None:
|
|
66
|
+
return cached
|
|
67
|
+
if provider is None or not provider.is_authenticated():
|
|
68
|
+
stamp = ANONYMOUS_STAMP
|
|
69
|
+
else:
|
|
70
|
+
groups = ",".join(sorted(provider.current_groups()))
|
|
71
|
+
ident = f"{provider.current_user_name() or ''}|{groups}"
|
|
72
|
+
stamp = hashlib.sha256(ident.encode("utf-8")).hexdigest()[:16]
|
|
73
|
+
setattr(g, _STAMP_CACHE_KEY, stamp)
|
|
74
|
+
return stamp
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _t(msg_id: str, fallback: str) -> str:
|
|
78
|
+
"""Translate ``msg_id``, falling back when l10n is absent or has no key.
|
|
79
|
+
|
|
80
|
+
The guard responds before ``set_language_from_browser`` has run, so this
|
|
81
|
+
resolves against English (see ``Translator.t``); it is only ever used for
|
|
82
|
+
text a JS-mode client replaces with a page navigation anyway.
|
|
83
|
+
"""
|
|
84
|
+
translator = getattr(current_app, "l10n", None)
|
|
85
|
+
if translator is None:
|
|
86
|
+
return fallback
|
|
87
|
+
text = translator.t(msg_id)
|
|
88
|
+
return fallback if text == msg_id else text
|
|
26
89
|
|
|
27
90
|
|
|
28
91
|
def public(f):
|
|
@@ -122,11 +185,33 @@ class AuthProvider:
|
|
|
122
185
|
return f"{target}{sep}next={nxt}"
|
|
123
186
|
|
|
124
187
|
def on_unauthenticated(self):
|
|
125
|
-
"""
|
|
188
|
+
"""Send the user to the login page (401 when none is configured).
|
|
189
|
+
|
|
190
|
+
Plain requests get a 302 the browser follows. An Unpoly *fragment*
|
|
191
|
+
request cannot be answered that way: its XHR follows the redirect
|
|
192
|
+
itself, and a hosted IdP bounces the login endpoint to another origin,
|
|
193
|
+
where the redirected XHR dies on CORS — the user sees a network error
|
|
194
|
+
while the shell keeps showing them as logged in. Even a same-origin
|
|
195
|
+
login form would only be swapped into ``#main-content``, leaving the
|
|
196
|
+
navbar and sidebar (rendered once, by the full-page layout) advertising
|
|
197
|
+
a session that no longer exists.
|
|
198
|
+
|
|
199
|
+
So JS-mode requests get a 401 carrying the login URL in
|
|
200
|
+
:data:`LOGIN_HEADER`, which ``construct-core.js`` turns into a full
|
|
201
|
+
page navigation. The small body is a no-JS-style fallback for the case
|
|
202
|
+
where the response is rendered anyway.
|
|
203
|
+
"""
|
|
126
204
|
url = self.login_url()
|
|
127
205
|
if url is None:
|
|
128
206
|
abort(401)
|
|
129
|
-
|
|
207
|
+
if not is_unpoly_request():
|
|
208
|
+
return redirect(url)
|
|
209
|
+
label = _t("auth-login-required", "You must log in to access this page.")
|
|
210
|
+
body = f'<p class="fcu-alert"><a href="{escape(url)}">{escape(label)}</a></p>'
|
|
211
|
+
response = make_response(body, 401)
|
|
212
|
+
response.headers[LOGIN_HEADER] = url
|
|
213
|
+
response.headers[AUTH_STAMP_HEADER] = ANONYMOUS_STAMP
|
|
214
|
+
return response
|
|
130
215
|
|
|
131
216
|
def on_forbidden(self):
|
|
132
217
|
"""Default: render the themed access-denied page with HTTP 403."""
|
|
@@ -53,6 +53,7 @@ from .pagination import (
|
|
|
53
53
|
DEFAULT_PAGE_SIZE,
|
|
54
54
|
MAX_PAGE_SIZE,
|
|
55
55
|
)
|
|
56
|
+
from .auth import AUTH_STAMP_HEADER, auth_stamp
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
def list_action(name, icon):
|
|
@@ -773,6 +774,21 @@ class FlaskConstructUI:
|
|
|
773
774
|
response.headers["Vary"] = (existing + ", X-Up-Target").lstrip(", ")
|
|
774
775
|
return response
|
|
775
776
|
|
|
777
|
+
@app.after_request
|
|
778
|
+
def add_auth_stamp(response):
|
|
779
|
+
# Tag every HTML response with the auth state it was rendered for.
|
|
780
|
+
# A fragment render only replaces #main-content, so the navbar and
|
|
781
|
+
# sidebar cannot correct themselves when the session ends (or
|
|
782
|
+
# switches user) while the page sits open — a logged-out request to
|
|
783
|
+
# a @public page still answers 200 with fresh content under a shell
|
|
784
|
+
# that names the old user. The client compares this header against
|
|
785
|
+
# the stamp the full-page layout put on <html data-fcu-auth> and
|
|
786
|
+
# reloads the whole document when they disagree.
|
|
787
|
+
if self._auth is None or "text/html" not in response.content_type:
|
|
788
|
+
return response
|
|
789
|
+
response.headers.setdefault(AUTH_STAMP_HEADER, auth_stamp(self._auth))
|
|
790
|
+
return response
|
|
791
|
+
|
|
776
792
|
@app.before_request
|
|
777
793
|
def csrf_protect():
|
|
778
794
|
# Only guard state-changing requests against construct-owned
|
|
@@ -1060,8 +1076,8 @@ class FlaskConstructUI:
|
|
|
1060
1076
|
collapsible submenu group with that title instead of being placed at
|
|
1061
1077
|
the top level. Pages sharing the same ``submenu`` title are collected
|
|
1062
1078
|
into the same group, in the order they are added. ``submenu_icon`` sets
|
|
1063
|
-
the icon for the group the first time it is created
|
|
1064
|
-
|
|
1079
|
+
the icon for the group the first time it is created; if omitted, the
|
|
1080
|
+
group falls back to the ``folder`` icon.
|
|
1065
1081
|
"""
|
|
1066
1082
|
self._pages.append(page)
|
|
1067
1083
|
self._blueprint_names.add(page.endpoint)
|
|
@@ -1083,7 +1099,10 @@ class FlaskConstructUI:
|
|
|
1083
1099
|
+ page.menu["link"]
|
|
1084
1100
|
)
|
|
1085
1101
|
if submenu is not None:
|
|
1086
|
-
|
|
1102
|
+
if submenu_icon is None:
|
|
1103
|
+
self._add_to_submenu(submenu, menu)
|
|
1104
|
+
else:
|
|
1105
|
+
self._add_to_submenu(submenu, menu, submenu_icon)
|
|
1087
1106
|
else:
|
|
1088
1107
|
self._menu.append(menu)
|
|
1089
1108
|
|
|
@@ -1151,6 +1170,18 @@ class FlaskConstructUI:
|
|
|
1151
1170
|
def auth(self):
|
|
1152
1171
|
return self._auth
|
|
1153
1172
|
|
|
1173
|
+
@property
|
|
1174
|
+
def auth_stamp(self):
|
|
1175
|
+
"""Fingerprint of the current auth state, or None when auth is off.
|
|
1176
|
+
|
|
1177
|
+
Rendered into the full-page layout as ``<html data-fcu-auth>`` so the
|
|
1178
|
+
browser can compare it against the ``X-Construct-Auth`` header of later
|
|
1179
|
+
fragment responses. Must be called within a request context.
|
|
1180
|
+
"""
|
|
1181
|
+
if self._auth is None:
|
|
1182
|
+
return None
|
|
1183
|
+
return auth_stamp(self._auth)
|
|
1184
|
+
|
|
1154
1185
|
@property
|
|
1155
1186
|
def navbar_features_available(self):
|
|
1156
1187
|
"""Whether navbar search/notifications should be rendered.
|
|
@@ -13,6 +13,7 @@ from markupsafe import Markup
|
|
|
13
13
|
from textwrap import dedent
|
|
14
14
|
import jinja2
|
|
15
15
|
import pygal
|
|
16
|
+
from pygal.style import Style
|
|
16
17
|
from markdown_it import MarkdownIt
|
|
17
18
|
from flask import current_app, g, url_for
|
|
18
19
|
from flask_construct_ui.language import LOCALE_MAPPING
|
|
@@ -45,6 +46,7 @@ class Field:
|
|
|
45
46
|
options: Optional[Union[List[str], str, Dict[str, str]]] = None
|
|
46
47
|
value: Any = None
|
|
47
48
|
secret: bool = False
|
|
49
|
+
autofocus: bool = False
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
class FieldRendererRegistry:
|
|
@@ -392,6 +394,40 @@ class FormComponent(Component):
|
|
|
392
394
|
return result
|
|
393
395
|
|
|
394
396
|
|
|
397
|
+
# Theme-aware pygal style. Every value is a CSS custom property, so the numbers
|
|
398
|
+
# are resolved by the browser against the inline SVG's host page -- which means
|
|
399
|
+
# charts pick up the active design's font and colours and follow light/dark
|
|
400
|
+
# mode automatically, with no server-side scheme detection. Restyle charts by
|
|
401
|
+
# overriding these `--fcu-chart-*` tokens in a design stylesheet (see
|
|
402
|
+
# whitelabel.css); nothing here needs to change. `background` is left on the
|
|
403
|
+
# surface token (not transparent) so hover tooltips keep a readable box.
|
|
404
|
+
CHART_STYLE = Style(
|
|
405
|
+
background="var(--fcu-surface)",
|
|
406
|
+
plot_background="transparent",
|
|
407
|
+
foreground="var(--fcu-text-muted)",
|
|
408
|
+
foreground_strong="var(--fcu-text)",
|
|
409
|
+
foreground_subtle="var(--fcu-border)",
|
|
410
|
+
font_family="var(--fcu-chart-font-family)",
|
|
411
|
+
title_font_size=15,
|
|
412
|
+
legend_font_size=13,
|
|
413
|
+
label_font_size=12,
|
|
414
|
+
major_label_font_size=12,
|
|
415
|
+
value_font_size=12,
|
|
416
|
+
tooltip_font_size=13,
|
|
417
|
+
opacity=".85",
|
|
418
|
+
opacity_hover="1",
|
|
419
|
+
transition="150ms ease-in-out",
|
|
420
|
+
colors=(
|
|
421
|
+
"var(--fcu-chart-color-1)",
|
|
422
|
+
"var(--fcu-chart-color-2)",
|
|
423
|
+
"var(--fcu-chart-color-3)",
|
|
424
|
+
"var(--fcu-chart-color-4)",
|
|
425
|
+
"var(--fcu-chart-color-5)",
|
|
426
|
+
"var(--fcu-chart-color-6)",
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
395
431
|
@dataclass
|
|
396
432
|
class ChartComponent(Component):
|
|
397
433
|
data: List[Tuple[str, List[Any]]] = field(default_factory=list)
|
|
@@ -418,7 +454,7 @@ class ChartComponent(Component):
|
|
|
418
454
|
):
|
|
419
455
|
is_timeseries = True
|
|
420
456
|
|
|
421
|
-
config_kwargs = {}
|
|
457
|
+
config_kwargs = {"style": CHART_STYLE}
|
|
422
458
|
if self.width:
|
|
423
459
|
config_kwargs["width"] = self.width
|
|
424
460
|
if self.height:
|