flask-construct-ui 0.11.6__tar.gz → 0.12.0__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.11.6 → flask_construct_ui-0.12.0}/PKG-INFO +1 -1
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/__init__.py +18 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/app_hooks.py +74 -4
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/auth.py +5 -5
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/base.py +147 -19
- flask_construct_ui-0.12.0/flask_construct_ui/capabilities.py +130 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/cli.py +5 -1
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/components/__init__.py +17 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/components/calendar.py +25 -30
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/components/chart.py +100 -10
- flask_construct_ui-0.12.0/flask_construct_ui/components/chart_native.py +362 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/components/content.py +52 -12
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/components/core.py +30 -1
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/components/grid.py +12 -7
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/components/layout.py +7 -7
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/components/live.py +26 -6
- flask_construct_ui-0.12.0/flask_construct_ui/components/period.py +185 -0
- flask_construct_ui-0.12.0/flask_construct_ui/components/pivot.py +379 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/contrast.py +30 -4
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/crud.py +17 -4
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/crud_forms.py +2 -2
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/csrf.py +1 -1
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/data.py +32 -7
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/data_sqlalchemy.py +179 -39
- flask_construct_ui-0.12.0/flask_construct_ui/dragdrop.py +109 -0
- flask_construct_ui-0.12.0/flask_construct_ui/errors.py +81 -0
- flask_construct_ui-0.12.0/flask_construct_ui/flashes.py +43 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/grid.py +32 -9
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/headless.py +4 -3
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/icons.py +153 -4
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/l10n/en/main.flt +47 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/l10n/sv/main.flt +47 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/language.py +10 -4
- flask_construct_ui-0.12.0/flask_construct_ui/live.py +255 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/menu.py +7 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/navigation.py +1 -1
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/orgmode.py +5 -4
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/rcrud.py +182 -26
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/schema.py +26 -3
- flask_construct_ui-0.12.0/flask_construct_ui/static/css/unpoly.min.css +7 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/css/whitelabel.css +360 -0
- flask_construct_ui-0.12.0/flask_construct_ui/static/fonts/material-symbols/names.txt +4264 -0
- flask_construct_ui-0.12.0/flask_construct_ui/static/js/construct-core.min.js +2 -0
- flask_construct_ui-0.12.0/flask_construct_ui/static/js/construct-grid.min.js +1 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/js/prosemirror-bundle.min.js +1 -1
- flask_construct_ui-0.12.0/flask_construct_ui/static/js/unpoly.min.js +1 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/chart.html +2 -2
- flask_construct_ui-0.12.0/flask_construct_ui/templates/components/chart_native.html +67 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/live.html +1 -1
- flask_construct_ui-0.12.0/flask_construct_ui/templates/components/live_refresh.html +23 -0
- flask_construct_ui-0.12.0/flask_construct_ui/templates/components/period_nav.html +61 -0
- flask_construct_ui-0.12.0/flask_construct_ui/templates/components/pivot_form.html +99 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/crud/listindex.html +6 -5
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/crud/related_crud_tab_list.html +3 -3
- flask_construct_ui-0.12.0/flask_construct_ui/templates/errors/error.html +21 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/errors/forbidden.html +1 -1
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/style/_flashes.html +12 -7
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/style/base_full.html +7 -2
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/style/base_partial.html +8 -4
- flask_construct_ui-0.12.0/flask_construct_ui/urls.py +118 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/validation.py +6 -4
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui.egg-info/PKG-INFO +1 -1
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui.egg-info/SOURCES.txt +13 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/pyproject.toml +45 -1
- flask_construct_ui-0.11.6/flask_construct_ui/capabilities.py +0 -39
- flask_construct_ui-0.11.6/flask_construct_ui/static/css/unpoly.min.css +0 -6
- flask_construct_ui-0.11.6/flask_construct_ui/static/js/construct-core.min.js +0 -1
- flask_construct_ui-0.11.6/flask_construct_ui/static/js/construct-grid.min.js +0 -1
- flask_construct_ui-0.11.6/flask_construct_ui/static/js/unpoly.min.js +0 -1
- flask_construct_ui-0.11.6/flask_construct_ui/urls.py +0 -19
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/LICENSE +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/README.md +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/components/kanban.py +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/forms.py +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/layout.py +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/navbar.py +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/pagination.py +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/py.typed +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/resume.py +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/sanitize.py +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/css/grid.css +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/css/prosemirror-bundle.min.css +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/designs/adminlte.css +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/designs/sandstone.css +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/fonts/bootstrap-icons/LICENSE.txt +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/fonts/bootstrap-icons/bootstrap-icons.min.css +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/fonts/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/fonts/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/fonts/material-symbols/LICENSE.txt +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/fonts/material-symbols/material-symbols-outlined.woff2 +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/fonts/material-symbols/material-symbols.css +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/static/js/kanban-dnd.js +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/_form_field.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/button.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/calendar.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/calendarmini.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/card.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/form.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/grid.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/grid_layout.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/grid_notice.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/horizontal_layout.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/kanban.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/main.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/markup.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/rich_text.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/table.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/tabs.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/text.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/components/vertical_layout.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/crud/_macros.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/crud/_many_related_options.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/crud/_related_options.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/crud/create.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/crud/edit.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/crud/related_crud_tab_form.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/crud/tab_view.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/crud/view.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/navbar/notifications.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/navbar/search_results.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/structure/base.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/structure/public.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/style/_footer.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/templates/style/_menu.html +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui/testing.py +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui.egg-info/dependency_links.txt +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui.egg-info/requires.txt +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/flask_construct_ui.egg-info/top_level.txt +0 -0
- {flask_construct_ui-0.11.6 → flask_construct_ui-0.12.0}/setup.cfg +0 -0
|
@@ -27,6 +27,7 @@ from .components import (
|
|
|
27
27
|
find_component as find_component,
|
|
28
28
|
find_component_by_id as find_component_by_id,
|
|
29
29
|
LiveComponent as LiveComponent,
|
|
30
|
+
LiveRefreshComponent as LiveRefreshComponent,
|
|
30
31
|
VerticalLayoutComponent as VerticalLayoutComponent,
|
|
31
32
|
HorizontalLayoutComponent as HorizontalLayoutComponent,
|
|
32
33
|
GridLayoutComponent as GridLayoutComponent,
|
|
@@ -45,6 +46,11 @@ from .components import (
|
|
|
45
46
|
KanbanColumn as KanbanColumn,
|
|
46
47
|
KanbanCard as KanbanCard,
|
|
47
48
|
KanbanCardAction as KanbanCardAction,
|
|
49
|
+
PivotFormComponent as PivotFormComponent,
|
|
50
|
+
PivotRow as PivotRow,
|
|
51
|
+
PivotColumn as PivotColumn,
|
|
52
|
+
PivotCell as PivotCell,
|
|
53
|
+
PeriodNavComponent as PeriodNavComponent,
|
|
48
54
|
)
|
|
49
55
|
|
|
50
56
|
# Authentication & authorization
|
|
@@ -98,6 +104,18 @@ from .headless import (
|
|
|
98
104
|
live_container as live_container,
|
|
99
105
|
)
|
|
100
106
|
|
|
107
|
+
# Live refresh on commit (see live.py)
|
|
108
|
+
from .live import (
|
|
109
|
+
ChangeFeed as ChangeFeed,
|
|
110
|
+
MemoryBroker as MemoryBroker,
|
|
111
|
+
RedisBroker as RedisBroker,
|
|
112
|
+
SQLAlchemyChanges as SQLAlchemyChanges,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Declarative drag-and-drop attributes (see dragdrop.py)
|
|
116
|
+
from .dragdrop import drag_attrs as drag_attrs, drop_attrs as drop_attrs
|
|
117
|
+
from .errors import render_error as render_error
|
|
118
|
+
|
|
101
119
|
# HTML sanitizer (allowlist-based; for trusted-HTML surfaces such as
|
|
102
120
|
# LiveComponent format="raw" and TextComponent format="html")
|
|
103
121
|
from .sanitize import sanitize_html as sanitize_html
|
|
@@ -6,7 +6,9 @@ decides the order they run in.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from flask import Blueprint, current_app, g, jsonify, render_template, request
|
|
10
12
|
import jinja2
|
|
11
13
|
from markupsafe import Markup
|
|
12
14
|
|
|
@@ -18,7 +20,9 @@ from .csrf import (
|
|
|
18
20
|
generate_csrf_token,
|
|
19
21
|
validate_csrf_token,
|
|
20
22
|
)
|
|
21
|
-
from .
|
|
23
|
+
from .flashes import render_flashes
|
|
24
|
+
from .headless import live_attributes, live_container, sse_event, sse_response
|
|
25
|
+
from .live import live_refresh
|
|
22
26
|
from .icons import render_icon
|
|
23
27
|
from .navigation import resolve_next
|
|
24
28
|
from .pagination import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
|
|
@@ -32,6 +36,10 @@ from .resume import (
|
|
|
32
36
|
resumed_values,
|
|
33
37
|
)
|
|
34
38
|
from .sanitize import sanitize_html
|
|
39
|
+
from .dragdrop import drag_attrs, drop_attrs
|
|
40
|
+
from .urls import current_url, hidden_inputs, query_string
|
|
41
|
+
|
|
42
|
+
_log = logging.getLogger(__name__)
|
|
35
43
|
|
|
36
44
|
|
|
37
45
|
def set_config_defaults(construct, app):
|
|
@@ -59,6 +67,10 @@ def set_config_defaults(construct, app):
|
|
|
59
67
|
app.config.setdefault("CONSTRUCT_RESUME_MAX_BYTES", RESUME_MAX_BYTES)
|
|
60
68
|
app.config.setdefault("CONSTRUCT_RESUME_TTL", RESUME_TTL)
|
|
61
69
|
app.config.setdefault("CONSTRUCT_RESUME_SKIP_FIELDS", RESUME_SKIP_FIELDS)
|
|
70
|
+
# Seconds a /_construct/live/changes stream waits for a change before
|
|
71
|
+
# ending; the browser reconnects at once. Bounds how long a worker is held
|
|
72
|
+
# by a client that has gone away without a write to notice it by.
|
|
73
|
+
app.config.setdefault("CONSTRUCT_LIVE_WAIT", 25)
|
|
62
74
|
|
|
63
75
|
|
|
64
76
|
def install_request_hooks(construct, app):
|
|
@@ -171,7 +183,7 @@ def install_template_helpers(construct, app):
|
|
|
171
183
|
|
|
172
184
|
# MarkdownIt passes raw HTML through, so the converted output must be
|
|
173
185
|
# sanitized before it is marked safe (stored-XSS boundary).
|
|
174
|
-
return Markup(
|
|
186
|
+
return Markup( # noqa: S704
|
|
175
187
|
sanitize_html(md.render(s))
|
|
176
188
|
) # nosemgrep: explicit-unescape-with-markup # nosec B704
|
|
177
189
|
|
|
@@ -181,7 +193,7 @@ def install_template_helpers(construct, app):
|
|
|
181
193
|
return ""
|
|
182
194
|
from .orgmode import org_to_html
|
|
183
195
|
|
|
184
|
-
return Markup(
|
|
196
|
+
return Markup( # noqa: S704
|
|
185
197
|
sanitize_html(org_to_html(s))
|
|
186
198
|
) # nosemgrep: explicit-unescape-with-markup # nosec B704
|
|
187
199
|
|
|
@@ -209,16 +221,28 @@ def install_template_helpers(construct, app):
|
|
|
209
221
|
app.jinja_env.globals.update(
|
|
210
222
|
live_container=live_container,
|
|
211
223
|
live_attributes=live_attributes,
|
|
224
|
+
live_refresh=live_refresh,
|
|
212
225
|
csrf_field=csrf_field,
|
|
213
226
|
csrf_token=generate_csrf_token,
|
|
214
227
|
t=translate,
|
|
215
228
|
icon=render_icon,
|
|
216
229
|
next_url=resolve_next,
|
|
230
|
+
# The flash region, placed where the page wants it; the layout then
|
|
231
|
+
# leaves its own out for this request (see flashes.py).
|
|
232
|
+
flashes=render_flashes,
|
|
217
233
|
# An app template can put its own form back after a re-login
|
|
218
234
|
# with no Python change; see resume.py.
|
|
219
235
|
resumed_value=resumed_value,
|
|
220
236
|
resumed_values=resumed_values,
|
|
221
237
|
has_resumable_form=has_resumable_form,
|
|
238
|
+
# Links and GET forms that change one query argument and carry the
|
|
239
|
+
# rest, repeated keys included; see urls.py.
|
|
240
|
+
current_url=current_url,
|
|
241
|
+
query_string=query_string,
|
|
242
|
+
hidden_inputs=hidden_inputs,
|
|
243
|
+
# Drag a thing onto a zone and POST both ends' fields; see dragdrop.py.
|
|
244
|
+
drag_attrs=drag_attrs,
|
|
245
|
+
drop_attrs=drop_attrs,
|
|
222
246
|
)
|
|
223
247
|
# Components render through their own standalone Jinja environment,
|
|
224
248
|
# so the CSRF helpers, translator and icon renderer must be registered
|
|
@@ -228,6 +252,12 @@ def install_template_helpers(construct, app):
|
|
|
228
252
|
csrf_token=generate_csrf_token,
|
|
229
253
|
t=translate,
|
|
230
254
|
icon=render_icon,
|
|
255
|
+
current_url=current_url,
|
|
256
|
+
query_string=query_string,
|
|
257
|
+
hidden_inputs=hidden_inputs,
|
|
258
|
+
drag_attrs=drag_attrs,
|
|
259
|
+
drop_attrs=drop_attrs,
|
|
260
|
+
live_refresh=live_refresh,
|
|
231
261
|
)
|
|
232
262
|
if app.jinja_env.loader:
|
|
233
263
|
jinja_env.loader = jinja2.ChoiceLoader(
|
|
@@ -257,11 +287,13 @@ def register_features_blueprint(construct):
|
|
|
257
287
|
notifications_url = "/notifications"
|
|
258
288
|
menu_url = "/menu"
|
|
259
289
|
footer_url = "/footer"
|
|
290
|
+
live_url = "/live/changes"
|
|
260
291
|
if construct.enforce_trailing_slash:
|
|
261
292
|
search_url += "/"
|
|
262
293
|
notifications_url += "/"
|
|
263
294
|
menu_url += "/"
|
|
264
295
|
footer_url += "/"
|
|
296
|
+
live_url += "/"
|
|
265
297
|
|
|
266
298
|
route_kwargs = {}
|
|
267
299
|
if construct.strict_slashes is not None:
|
|
@@ -314,5 +346,43 @@ def register_features_blueprint(construct):
|
|
|
314
346
|
footer_fragment=True,
|
|
315
347
|
)
|
|
316
348
|
|
|
349
|
+
@features_bp.route(live_url, **route_kwargs)
|
|
350
|
+
def live_changes():
|
|
351
|
+
# The change feed behind live_refresh(); see live.py. Unconditional
|
|
352
|
+
# for the same reason as the menu route — `construct.live` may be set
|
|
353
|
+
# after init_app — so it 404s while no feed is configured. `since` is
|
|
354
|
+
# the version the page was rendered from; the stream ends with a
|
|
355
|
+
# `change` event as soon as the version differs from it, or quietly
|
|
356
|
+
# after CONSTRUCT_LIVE_WAIT seconds (EventSource reconnects by
|
|
357
|
+
# itself). `?poll=1` answers at once, for browsers that can't stream.
|
|
358
|
+
feed = construct.live
|
|
359
|
+
if feed is None:
|
|
360
|
+
return "", 404
|
|
361
|
+
since = request.args.get("since", "")
|
|
362
|
+
no_store = {"Cache-Control": "no-store"}
|
|
363
|
+
if request.args.get("poll"):
|
|
364
|
+
version = feed.version()
|
|
365
|
+
if version is None:
|
|
366
|
+
return jsonify(error="unavailable"), 503, no_store
|
|
367
|
+
return jsonify(version=version), 200, no_store
|
|
368
|
+
# Fail the request, not the stream, when the broker is down: an
|
|
369
|
+
# EventSource only gives up (and the page falls back to polling and
|
|
370
|
+
# shows "not updating") on a non-200 answer.
|
|
371
|
+
if feed.version() is None:
|
|
372
|
+
return "", 503, no_store
|
|
373
|
+
wait = float(current_app.config.get("CONSTRUCT_LIVE_WAIT", 25))
|
|
374
|
+
|
|
375
|
+
def stream():
|
|
376
|
+
yield "retry: 2000\n\n"
|
|
377
|
+
try:
|
|
378
|
+
version = feed.wait(since, wait)
|
|
379
|
+
except Exception: # noqa: BLE001 - the client reconnects and sees the 503
|
|
380
|
+
_log.warning("live: change feed failed mid-stream", exc_info=True)
|
|
381
|
+
return
|
|
382
|
+
if version != since:
|
|
383
|
+
yield sse_event("change", {"version": version})
|
|
384
|
+
|
|
385
|
+
return sse_response(stream())
|
|
386
|
+
|
|
317
387
|
construct.app.register_blueprint(features_bp)
|
|
318
388
|
construct._blueprint_names.add("construct_features")
|
|
@@ -124,7 +124,7 @@ class AuthProvider:
|
|
|
124
124
|
|
|
125
125
|
#: Flask endpoint name (or literal path) to send unauthenticated users to.
|
|
126
126
|
#: Overridden by the ``CONSTRUCT_LOGIN_ENDPOINT`` config when set.
|
|
127
|
-
login_endpoint = None
|
|
127
|
+
login_endpoint: str | None = None
|
|
128
128
|
|
|
129
129
|
def is_authenticated(self) -> bool:
|
|
130
130
|
raise NotImplementedError
|
|
@@ -194,7 +194,7 @@ class AuthProvider:
|
|
|
194
194
|
nxt = self.return_target()
|
|
195
195
|
try:
|
|
196
196
|
return url_for(target, next=nxt) if nxt else url_for(target)
|
|
197
|
-
except Exception:
|
|
197
|
+
except Exception: # noqa: BLE001 - url_for failure means a literal path
|
|
198
198
|
# Not a known endpoint — treat it as a literal path.
|
|
199
199
|
if not nxt:
|
|
200
200
|
return target
|
|
@@ -296,7 +296,7 @@ class FlaskSecurityAuth(AuthProvider):
|
|
|
296
296
|
"icon": "logout",
|
|
297
297
|
}
|
|
298
298
|
]
|
|
299
|
-
except Exception:
|
|
299
|
+
except Exception: # noqa: BLE001 - no sign-out route, no link
|
|
300
300
|
return []
|
|
301
301
|
|
|
302
302
|
|
|
@@ -418,7 +418,7 @@ class FlaskOIDCAuth(AuthProvider):
|
|
|
418
418
|
code would be spent, and the abort that follows is worse than the
|
|
419
419
|
front page this replaces.
|
|
420
420
|
"""
|
|
421
|
-
if request.blueprint == self.login_endpoint.rsplit(".", 1)[0]:
|
|
421
|
+
if request.blueprint == (self.login_endpoint or "").rsplit(".", 1)[0]:
|
|
422
422
|
nxt = request.args.get("next")
|
|
423
423
|
if not nxt or not is_safe_next(nxt) or is_fragment_target(nxt):
|
|
424
424
|
return None
|
|
@@ -495,5 +495,5 @@ class FlaskOIDCAuth(AuthProvider):
|
|
|
495
495
|
"icon": "logout",
|
|
496
496
|
}
|
|
497
497
|
]
|
|
498
|
-
except Exception:
|
|
498
|
+
except Exception: # noqa: BLE001 - no sign-out route, no link
|
|
499
499
|
return []
|
|
@@ -24,6 +24,9 @@ from markupsafe import Markup
|
|
|
24
24
|
|
|
25
25
|
from . import app_hooks
|
|
26
26
|
from .auth import auth_stamp
|
|
27
|
+
from .capabilities import UNSET, guard_route, route_capability
|
|
28
|
+
from .errors import check_mapping as check_error_mapping
|
|
29
|
+
from .errors import make_handler as make_error_handler
|
|
27
30
|
from .components import (
|
|
28
31
|
CalendarComponent,
|
|
29
32
|
ChartComponent,
|
|
@@ -66,8 +69,25 @@ def edit_action(name, icon):
|
|
|
66
69
|
return decorator
|
|
67
70
|
|
|
68
71
|
|
|
69
|
-
def expose(url="/", methods=None):
|
|
72
|
+
def expose(url="/", methods=None, capability=UNSET):
|
|
73
|
+
"""Route the decorated page method at *url*.
|
|
74
|
+
|
|
75
|
+
``capability="<name>"`` asks the page's ``can_<name>`` hook before the
|
|
76
|
+
view runs and answers 403 when it refuses — the route-side half of a
|
|
77
|
+
hidden control. ``"edit"``/``"view"``/``"delete"`` are asked about the
|
|
78
|
+
record whose id is the route's ``<id>``; ``"add"`` and any name of your
|
|
79
|
+
own take no row. ``capability=False`` exempts a writing route from the
|
|
80
|
+
page's ``write_capability``.
|
|
81
|
+
"""
|
|
82
|
+
if capability is None:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"@expose(capability=None) is ambiguous; omit it, or pass False "
|
|
85
|
+
"to opt out of the page's write_capability."
|
|
86
|
+
)
|
|
87
|
+
|
|
70
88
|
def wrap(f):
|
|
89
|
+
if capability is not UNSET:
|
|
90
|
+
f._expose_capability = capability
|
|
71
91
|
if not hasattr(f, "_urls"):
|
|
72
92
|
f._urls = []
|
|
73
93
|
if methods is None:
|
|
@@ -220,12 +240,12 @@ class PageMeta(type):
|
|
|
220
240
|
|
|
221
241
|
|
|
222
242
|
class PageBase(metaclass=PageMeta):
|
|
223
|
-
url = None
|
|
224
|
-
name = None
|
|
243
|
+
url: str | None = None
|
|
244
|
+
name: str | None = None
|
|
225
245
|
# Semantic icon name resolved per icon mode (see icons.ICONS). Apps may
|
|
226
246
|
# override with their own semantic name or a raw icon string.
|
|
227
247
|
icon = "page"
|
|
228
|
-
endpoint = None
|
|
248
|
+
endpoint: str | None = None
|
|
229
249
|
# Page-wide auth defaults (only enforced when an AuthProvider is configured).
|
|
230
250
|
# Set ``login_required = False`` to make every route on the page public, or
|
|
231
251
|
# ``required_groups = [...]`` to gate every route — including RCRUD
|
|
@@ -233,6 +253,11 @@ class PageBase(metaclass=PageMeta):
|
|
|
233
253
|
# decorators override these.
|
|
234
254
|
login_required = True
|
|
235
255
|
required_groups = None
|
|
256
|
+
# Capability asked before every custom @expose route that accepts a
|
|
257
|
+
# writing method (POST/PUT/PATCH/DELETE) and declares none of its own —
|
|
258
|
+
# e.g. ``write_capability = "write"`` with a ``can_write(self)`` hook.
|
|
259
|
+
# Fail-closed for routes added later; ``capability=False`` opts one out.
|
|
260
|
+
write_capability: str | None = None
|
|
236
261
|
|
|
237
262
|
def __init__(self, static_folder=None, static_url_path=None, menu=None):
|
|
238
263
|
self.name = self.name or self.__class__.__name__.capitalize()
|
|
@@ -333,13 +358,32 @@ class PageBase(metaclass=PageMeta):
|
|
|
333
358
|
|
|
334
359
|
url = apply_trailing_slash(url, construct)
|
|
335
360
|
|
|
336
|
-
|
|
361
|
+
view = getattr(self, name)
|
|
362
|
+
capability = route_capability(type(self), name, methods)
|
|
363
|
+
if capability is not None:
|
|
364
|
+
view = guard_route(
|
|
365
|
+
self,
|
|
366
|
+
view,
|
|
367
|
+
capability,
|
|
368
|
+
url,
|
|
369
|
+
self._capability_row_reader(),
|
|
370
|
+
f"{type(self).__name__}.{name}",
|
|
371
|
+
)
|
|
372
|
+
self.blueprint.add_url_rule(url, name, view, **kwargs)
|
|
337
373
|
|
|
338
374
|
return self.blueprint
|
|
339
375
|
|
|
340
376
|
def _run_view(self, f, *args, **kwargs):
|
|
341
377
|
return f(self, *args, **kwargs)
|
|
342
378
|
|
|
379
|
+
def _capability_row_reader(self):
|
|
380
|
+
"""``read(id)`` for row capabilities on custom routes, or ``None``
|
|
381
|
+
when the page has no dataclass to read a record from."""
|
|
382
|
+
dataclass = getattr(self, "dataclass", None)
|
|
383
|
+
if dataclass is None:
|
|
384
|
+
return None
|
|
385
|
+
return lambda id: dataclass().read(id)
|
|
386
|
+
|
|
343
387
|
|
|
344
388
|
class ComponentPageBase(PageBase):
|
|
345
389
|
"""Base class for component-based pages."""
|
|
@@ -488,7 +532,7 @@ class ComponentPageBase(PageBase):
|
|
|
488
532
|
|
|
489
533
|
def _register_live_endpoints(self, blueprint, construct):
|
|
490
534
|
|
|
491
|
-
for source_name, (method,
|
|
535
|
+
for source_name, (method, _interval) in self.livesources.items():
|
|
492
536
|
|
|
493
537
|
def make_poll_view(m, sname, page_self):
|
|
494
538
|
def poll_view():
|
|
@@ -766,7 +810,7 @@ class ExtensionsPage(ComponentPageBase):
|
|
|
766
810
|
bg_color = "var(--fcu-secondary)"
|
|
767
811
|
text_color = "var(--fcu-on-secondary)"
|
|
768
812
|
|
|
769
|
-
status_badge = Markup( # nosemgrep: explicit-unescape-with-markup # nosec B704
|
|
813
|
+
status_badge = Markup( # nosemgrep: explicit-unescape-with-markup # nosec B704 # noqa: S704
|
|
770
814
|
f'<span style="background-color: {bg_color}; color: {text_color}; '
|
|
771
815
|
f"padding: 2px 6px; border-radius: var(--fcu-border-radius); font-size: var(--fcu-font-size-sm); "
|
|
772
816
|
f'font-weight: bold; display: inline-block;">{status}</span>'
|
|
@@ -809,7 +853,7 @@ class ExtensionsPage(ComponentPageBase):
|
|
|
809
853
|
|
|
810
854
|
class FlaskConstructUI(MenuMixin):
|
|
811
855
|
app = None
|
|
812
|
-
index_view_class = BaseIndexView
|
|
856
|
+
index_view_class: type[PageBase] | None = BaseIndexView
|
|
813
857
|
|
|
814
858
|
def __init__(
|
|
815
859
|
self,
|
|
@@ -833,8 +877,15 @@ class FlaskConstructUI(MenuMixin):
|
|
|
833
877
|
strict_slashes=None,
|
|
834
878
|
check_trailing_slashes=False,
|
|
835
879
|
enforce_trailing_slash=True,
|
|
880
|
+
app_l10n_dir=None,
|
|
881
|
+
live=None,
|
|
882
|
+
error_statuses=None,
|
|
836
883
|
):
|
|
837
884
|
self._pages = []
|
|
885
|
+
# Domain exception class -> status (or callable); see add_error_status.
|
|
886
|
+
self._error_statuses = {}
|
|
887
|
+
for exc_class, status in (error_statuses or {}).items():
|
|
888
|
+
self.add_error_status(exc_class, status)
|
|
838
889
|
self._init_menu(menu_poll)
|
|
839
890
|
self._blueprint_names = set()
|
|
840
891
|
self._pages_by_endpoint = {}
|
|
@@ -850,9 +901,18 @@ class FlaskConstructUI(MenuMixin):
|
|
|
850
901
|
# `app.l10n` (only then is it safe to rebuild its search path).
|
|
851
902
|
self._l10n_dirs = []
|
|
852
903
|
self._owns_l10n = False
|
|
904
|
+
# The application's own catalog directory. None means "find it": see
|
|
905
|
+
# app_l10n_paths. Not to be confused with the `l10n_dir` property,
|
|
906
|
+
# which is the library's catalog.
|
|
907
|
+
self._app_l10n_dir = app_l10n_dir
|
|
908
|
+
self._app_l10n_resolved = None
|
|
853
909
|
self._auth = auth
|
|
854
910
|
self._search = search
|
|
855
911
|
self._notifications = notifications
|
|
912
|
+
# The change feed behind live_refresh() (see live.py), or None. May
|
|
913
|
+
# also be assigned after construction: the route and the marker read
|
|
914
|
+
# it per request.
|
|
915
|
+
self.live = live
|
|
856
916
|
# The only structural theme is `whitelabel`; `theme=` is still accepted
|
|
857
917
|
# for backward compatibility but no longer selects templates or assets.
|
|
858
918
|
# Whenever no design is given we fill in `adminlte`, which reproduces
|
|
@@ -933,7 +993,7 @@ class FlaskConstructUI(MenuMixin):
|
|
|
933
993
|
app.cli.add_command(make_cli(self))
|
|
934
994
|
if not hasattr(app, "l10n"):
|
|
935
995
|
# Only add if not already added by the app.
|
|
936
|
-
app.l10n = Translator()
|
|
996
|
+
app.l10n = Translator() # type: ignore[attr-defined]
|
|
937
997
|
self._owns_l10n = True
|
|
938
998
|
self._apply_l10n_paths()
|
|
939
999
|
app.before_request(set_language_from_browser)
|
|
@@ -954,6 +1014,9 @@ class FlaskConstructUI(MenuMixin):
|
|
|
954
1014
|
|
|
955
1015
|
app_hooks.register_features_blueprint(self)
|
|
956
1016
|
|
|
1017
|
+
for exc_class, status in self._error_statuses.items():
|
|
1018
|
+
app.register_error_handler(exc_class, make_error_handler(status, self))
|
|
1019
|
+
|
|
957
1020
|
def load_extensions(self):
|
|
958
1021
|
"""Discover and load companion libraries via entry points in the
|
|
959
1022
|
group 'flask_construct_ui.extensions'.
|
|
@@ -964,7 +1027,7 @@ class FlaskConstructUI(MenuMixin):
|
|
|
964
1027
|
# Fallback for older python / importlib_metadata select API compatibility
|
|
965
1028
|
try:
|
|
966
1029
|
eps = entry_points().select(group="flask_construct_ui.extensions")
|
|
967
|
-
except Exception:
|
|
1030
|
+
except Exception: # noqa: BLE001 - no extensions rather than no app
|
|
968
1031
|
eps = []
|
|
969
1032
|
|
|
970
1033
|
for ep in eps:
|
|
@@ -1000,7 +1063,7 @@ class FlaskConstructUI(MenuMixin):
|
|
|
1000
1063
|
except Exception as e:
|
|
1001
1064
|
ext_info["status"] = "Error"
|
|
1002
1065
|
ext_info["error"] = str(e)
|
|
1003
|
-
logger.
|
|
1066
|
+
logger.exception("Failed to load extension %s: %s", ep.name, e)
|
|
1004
1067
|
self._extensions.append(ext_info)
|
|
1005
1068
|
|
|
1006
1069
|
if self._extensions:
|
|
@@ -1053,8 +1116,10 @@ class FlaskConstructUI(MenuMixin):
|
|
|
1053
1116
|
if not path.endswith("/"):
|
|
1054
1117
|
warnings.append((rule.rule, rule.endpoint))
|
|
1055
1118
|
app.logger.warning(
|
|
1056
|
-
|
|
1057
|
-
|
|
1119
|
+
"Construct UI: Route '%s' (endpoint '%s') does not end with "
|
|
1120
|
+
"a trailing slash '/'. This may cause 308 redirects.",
|
|
1121
|
+
rule.rule,
|
|
1122
|
+
rule.endpoint,
|
|
1058
1123
|
)
|
|
1059
1124
|
return warnings
|
|
1060
1125
|
|
|
@@ -1098,12 +1163,53 @@ class FlaskConstructUI(MenuMixin):
|
|
|
1098
1163
|
def _apply_l10n_paths(self):
|
|
1099
1164
|
"""(Re)build the translator's search path.
|
|
1100
1165
|
|
|
1101
|
-
Precedence, highest first: the application's own ``l10n
|
|
1102
|
-
directories registered by companion
|
|
1103
|
-
then the library defaults. Only meaningful when this construct owns
|
|
1166
|
+
Precedence, highest first: the application's own ``l10n/`` (see
|
|
1167
|
+
``app_l10n_paths``), then any directories registered by companion
|
|
1168
|
+
libraries (in registration order), then the library defaults. Only meaningful when this construct owns
|
|
1104
1169
|
``app.l10n`` (the host app didn't supply its own).
|
|
1105
1170
|
"""
|
|
1106
|
-
|
|
1171
|
+
# Resolved once: the cwd fallback means "the cwd at startup", and
|
|
1172
|
+
# add_l10n_dir rebuilds must not repeat the warning.
|
|
1173
|
+
if self._app_l10n_resolved is None:
|
|
1174
|
+
self._app_l10n_resolved = self.app_l10n_paths()
|
|
1175
|
+
self.app.l10n.set_languages(
|
|
1176
|
+
[*self._app_l10n_resolved, *self._l10n_dirs, self.l10n_dir]
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
def app_l10n_paths(self):
|
|
1180
|
+
"""The directories searched for the application's own catalogs.
|
|
1181
|
+
|
|
1182
|
+
An explicit ``app_l10n_dir=`` is resolved against ``app.root_path``
|
|
1183
|
+
(so an absolute path is used as-is). Otherwise ``l10n/`` next to the
|
|
1184
|
+
app package (``app.root_path``) is searched first, then ``l10n/`` in
|
|
1185
|
+
the working directory as a compatibility fallback. The fallback is
|
|
1186
|
+
what breaks when a server is started from another directory, so
|
|
1187
|
+
finding the catalog *only* there logs a warning.
|
|
1188
|
+
"""
|
|
1189
|
+
root = self.app.root_path
|
|
1190
|
+
if self._app_l10n_dir is not None:
|
|
1191
|
+
explicit = path.abspath(path.join(root, self._app_l10n_dir))
|
|
1192
|
+
if not path.isdir(explicit):
|
|
1193
|
+
logger.warning(
|
|
1194
|
+
"app_l10n_dir %r does not exist; application labels will "
|
|
1195
|
+
"not be translated",
|
|
1196
|
+
explicit,
|
|
1197
|
+
)
|
|
1198
|
+
return [explicit]
|
|
1199
|
+
beside_app = path.join(root, "l10n")
|
|
1200
|
+
in_cwd = path.abspath("l10n")
|
|
1201
|
+
if path.abspath(beside_app) == in_cwd:
|
|
1202
|
+
return [in_cwd]
|
|
1203
|
+
if path.isdir(in_cwd) and not path.isdir(beside_app):
|
|
1204
|
+
logger.warning(
|
|
1205
|
+
"Application l10n directory found only relative to the "
|
|
1206
|
+
"working directory (%s); it will be missed when the app is "
|
|
1207
|
+
"started from elsewhere. Move it to %s or pass "
|
|
1208
|
+
"FlaskConstructUI(app_l10n_dir=...).",
|
|
1209
|
+
in_cwd,
|
|
1210
|
+
beside_app,
|
|
1211
|
+
)
|
|
1212
|
+
return [beside_app, in_cwd]
|
|
1107
1213
|
|
|
1108
1214
|
def add_l10n_dir(self, path):
|
|
1109
1215
|
"""Register an extra l10n directory, e.g. one shipped by a companion
|
|
@@ -1121,6 +1227,28 @@ class FlaskConstructUI(MenuMixin):
|
|
|
1121
1227
|
if self.app is not None and self._owns_l10n:
|
|
1122
1228
|
self._apply_l10n_paths()
|
|
1123
1229
|
|
|
1230
|
+
def add_error_status(self, exc_class, status):
|
|
1231
|
+
"""Answer *exc_class* (and subclasses), raised from any route, with the
|
|
1232
|
+
themed error page instead of a 500.
|
|
1233
|
+
|
|
1234
|
+
*status* is an HTTP status (``404``) or a callable taking the exception
|
|
1235
|
+
and returning a status or a ``(status, message)`` pair; *message* is
|
|
1236
|
+
translated and shown in place of the status's default explanation::
|
|
1237
|
+
|
|
1238
|
+
fcu.add_error_status(NotFound, 404)
|
|
1239
|
+
fcu.add_error_status(CommandError, lambda e: (e.status, e.message))
|
|
1240
|
+
|
|
1241
|
+
The page renders in the current layout, so an Unpoly fragment request
|
|
1242
|
+
gets the partial. 403 is the same access-denied page a capability
|
|
1243
|
+
refusal shows. It is a plain Flask error handler on the app, so
|
|
1244
|
+
Flask's rules apply: the most specific registered class wins, and an
|
|
1245
|
+
``app.errorhandler`` of your own for the same class replaces it.
|
|
1246
|
+
"""
|
|
1247
|
+
check_error_mapping(exc_class, status)
|
|
1248
|
+
self._error_statuses[exc_class] = status
|
|
1249
|
+
if self.app is not None:
|
|
1250
|
+
self.app.register_error_handler(exc_class, make_error_handler(status, self))
|
|
1251
|
+
|
|
1124
1252
|
def add_stylesheet(self, sheet):
|
|
1125
1253
|
"""Register an application stylesheet to link in every full page render.
|
|
1126
1254
|
|
|
@@ -1360,7 +1488,7 @@ class FlaskConstructUI(MenuMixin):
|
|
|
1360
1488
|
"""
|
|
1361
1489
|
try:
|
|
1362
1490
|
name = (self.titleFromLink or "").strip()
|
|
1363
|
-
except Exception: # pragma: no cover - defensive
|
|
1491
|
+
except Exception: # pragma: no cover # noqa: BLE001 - defensive
|
|
1364
1492
|
return self.title
|
|
1365
1493
|
if not name or name == self.title:
|
|
1366
1494
|
return self.title
|
|
@@ -1378,7 +1506,7 @@ class FlaskConstructUI(MenuMixin):
|
|
|
1378
1506
|
"""
|
|
1379
1507
|
try:
|
|
1380
1508
|
name = (self.titleFromLink or "").strip()
|
|
1381
|
-
except Exception: # pragma: no cover - defensive
|
|
1509
|
+
except Exception: # pragma: no cover # noqa: BLE001 - defensive
|
|
1382
1510
|
name = ""
|
|
1383
1511
|
return self.t(name) if name else self.title
|
|
1384
1512
|
|