flask-construct-ui 0.11.5__tar.gz → 0.11.7__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.5 → flask_construct_ui-0.11.7}/PKG-INFO +1 -1
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/__init__.py +1 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/app_hooks.py +14 -1
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/auth.py +2 -2
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/base.py +62 -5
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/components/calendar.py +24 -29
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/crud.py +3 -1
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/data_sqlalchemy.py +21 -8
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/l10n/en/main.flt +20 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/l10n/sv/main.flt +20 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/navigation.py +31 -4
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/rcrud.py +33 -7
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/resume.py +14 -8
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/css/whitelabel.css +20 -0
- flask_construct_ui-0.11.7/flask_construct_ui/static/js/construct-core.min.js +1 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/form.html +4 -4
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/live.html +1 -1
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/_macros.html +8 -10
- flask_construct_ui-0.11.7/flask_construct_ui/templates/style/_footer.html +15 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/style/base_full.html +14 -4
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui.egg-info/PKG-INFO +1 -1
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui.egg-info/SOURCES.txt +1 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/pyproject.toml +1 -1
- flask_construct_ui-0.11.5/flask_construct_ui/static/js/construct-core.min.js +0 -1
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/LICENSE +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/README.md +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/capabilities.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/cli.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/components/__init__.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/components/chart.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/components/content.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/components/core.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/components/grid.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/components/kanban.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/components/layout.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/components/live.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/contrast.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/crud_forms.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/csrf.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/data.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/forms.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/grid.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/headless.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/icons.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/language.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/layout.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/menu.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/navbar.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/orgmode.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/pagination.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/py.typed +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/sanitize.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/schema.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/css/grid.css +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/css/prosemirror-bundle.min.css +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/css/unpoly.min.css +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/designs/adminlte.css +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/designs/sandstone.css +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/bootstrap-icons/LICENSE.txt +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/bootstrap-icons/bootstrap-icons.min.css +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/material-symbols/LICENSE.txt +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/material-symbols/material-symbols-outlined.woff2 +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/material-symbols/material-symbols.css +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/js/construct-grid.min.js +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/js/kanban-dnd.js +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/js/prosemirror-bundle.min.js +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/js/unpoly.min.js +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/_form_field.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/button.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/calendar.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/calendarmini.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/card.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/chart.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/grid.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/grid_layout.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/grid_notice.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/horizontal_layout.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/kanban.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/main.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/markup.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/rich_text.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/table.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/tabs.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/text.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/vertical_layout.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/_many_related_options.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/_related_options.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/create.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/edit.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/listindex.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/related_crud_tab_form.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/related_crud_tab_list.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/tab_view.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/view.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/errors/forbidden.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/navbar/notifications.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/navbar/search_results.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/structure/base.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/structure/public.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/style/_flashes.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/style/_menu.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/style/base_partial.html +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/testing.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/urls.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/validation.py +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui.egg-info/dependency_links.txt +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui.egg-info/requires.txt +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui.egg-info/top_level.txt +0 -0
- {flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/setup.cfg +0 -0
|
@@ -10,6 +10,7 @@ from .base import edit_action as edit_action
|
|
|
10
10
|
from .base import datasource as datasource
|
|
11
11
|
from .base import livesource as livesource
|
|
12
12
|
from .base import gridsource as gridsource
|
|
13
|
+
from .navigation import fragment as fragment
|
|
13
14
|
from .base import SSE_DONE as SSE_DONE
|
|
14
15
|
|
|
15
16
|
# Component-based pages and the component library. Exposed at the package root
|
|
@@ -239,7 +239,7 @@ def install_template_helpers(construct, app):
|
|
|
239
239
|
|
|
240
240
|
|
|
241
241
|
def register_features_blueprint(construct):
|
|
242
|
-
"""Register ``construct_features``: the search, notifications and
|
|
242
|
+
"""Register ``construct_features``: the search, notifications, menu and footer fragments."""
|
|
243
243
|
# The features blueprint is registered unconditionally because the menu
|
|
244
244
|
# fragment route cannot be conditional: providers are normally added
|
|
245
245
|
# after init_app (`fcu = FlaskConstructUI(app)` first, decorators
|
|
@@ -256,10 +256,12 @@ def register_features_blueprint(construct):
|
|
|
256
256
|
search_url = "/search"
|
|
257
257
|
notifications_url = "/notifications"
|
|
258
258
|
menu_url = "/menu"
|
|
259
|
+
footer_url = "/footer"
|
|
259
260
|
if construct.enforce_trailing_slash:
|
|
260
261
|
search_url += "/"
|
|
261
262
|
notifications_url += "/"
|
|
262
263
|
menu_url += "/"
|
|
264
|
+
footer_url += "/"
|
|
263
265
|
|
|
264
266
|
route_kwargs = {}
|
|
265
267
|
if construct.strict_slashes is not None:
|
|
@@ -301,5 +303,16 @@ def register_features_blueprint(construct):
|
|
|
301
303
|
construct=construct,
|
|
302
304
|
)
|
|
303
305
|
|
|
306
|
+
@features_bp.route(footer_url, **route_kwargs)
|
|
307
|
+
def footer_fragment():
|
|
308
|
+
# Just the #fcu-footer-content element that up-poll reloads. Always
|
|
309
|
+
# rendered, even empty: a poll response lacking its target is an
|
|
310
|
+
# Unpoly error, and an empty element lets the text come back later.
|
|
311
|
+
return render_template(
|
|
312
|
+
"style/_footer.html",
|
|
313
|
+
construct=construct,
|
|
314
|
+
footer_fragment=True,
|
|
315
|
+
)
|
|
316
|
+
|
|
304
317
|
construct.app.register_blueprint(features_bp)
|
|
305
318
|
construct._blueprint_names.add("construct_features")
|
|
@@ -177,8 +177,8 @@ class AuthProvider:
|
|
|
177
177
|
"""Where to send the reader once they are logged in again, or None.
|
|
178
178
|
|
|
179
179
|
Delegates to :func:`flask_construct_ui.resume.return_target`: a GET is
|
|
180
|
-
its own target, anything else
|
|
181
|
-
from. Override to apply a policy of your own — every redirect the
|
|
180
|
+
its own target, anything else — and a GET to a fragment endpoint —
|
|
181
|
+
falls back to the page it was made from. Override to apply a policy of your own — every redirect the
|
|
182
182
|
library builds for an interrupted request goes through here.
|
|
183
183
|
"""
|
|
184
184
|
return return_target()
|
|
@@ -36,6 +36,7 @@ from .components import (
|
|
|
36
36
|
)
|
|
37
37
|
from .language import Translator, set_language_from_browser
|
|
38
38
|
from .menu import MENU_STAMP_HEADER as MENU_STAMP_HEADER, MenuMixin
|
|
39
|
+
from .navigation import fragment
|
|
39
40
|
from .urls import apply_trailing_slash as apply_trailing_slash
|
|
40
41
|
|
|
41
42
|
logger = logging.getLogger("flask_construct_ui")
|
|
@@ -426,7 +427,7 @@ class ComponentPageBase(PageBase):
|
|
|
426
427
|
return controller.handle_json(row_id)
|
|
427
428
|
|
|
428
429
|
view.__name__ = f"grid_{'row' if with_row else 'rows'}_{sname}"
|
|
429
|
-
return view
|
|
430
|
+
return fragment(view)
|
|
430
431
|
|
|
431
432
|
def make_form_view(sname):
|
|
432
433
|
def view():
|
|
@@ -503,7 +504,7 @@ class ComponentPageBase(PageBase):
|
|
|
503
504
|
return "", 204
|
|
504
505
|
|
|
505
506
|
poll_view.__name__ = f"live_poll_{sname}"
|
|
506
|
-
return poll_view
|
|
507
|
+
return fragment(poll_view)
|
|
507
508
|
|
|
508
509
|
def make_sse_view(m, sname):
|
|
509
510
|
def sse_view():
|
|
@@ -534,7 +535,7 @@ class ComponentPageBase(PageBase):
|
|
|
534
535
|
)
|
|
535
536
|
|
|
536
537
|
sse_view.__name__ = f"live_sse_{sname}"
|
|
537
|
-
return sse_view
|
|
538
|
+
return fragment(sse_view)
|
|
538
539
|
|
|
539
540
|
poll_url = f"/live/{source_name}/poll"
|
|
540
541
|
sse_url = f"/live/{source_name}/sse"
|
|
@@ -677,6 +678,7 @@ class ComponentPageBase(PageBase):
|
|
|
677
678
|
self._set_active_tabs(root_component)
|
|
678
679
|
return self.render_component(root_component)
|
|
679
680
|
|
|
681
|
+
@fragment
|
|
680
682
|
@expose("/_part/<string:component_id>/<int:part_index>")
|
|
681
683
|
def get_part(self, component_id: str, part_index: int):
|
|
682
684
|
|
|
@@ -816,7 +818,8 @@ class FlaskConstructUI(MenuMixin):
|
|
|
816
818
|
url=None,
|
|
817
819
|
static_url_path=None,
|
|
818
820
|
title=None,
|
|
819
|
-
footer=
|
|
821
|
+
footer="footer-built-with",
|
|
822
|
+
footer_poll=None,
|
|
820
823
|
index_view_class=None,
|
|
821
824
|
theme=None,
|
|
822
825
|
design=None,
|
|
@@ -892,7 +895,11 @@ class FlaskConstructUI(MenuMixin):
|
|
|
892
895
|
else:
|
|
893
896
|
self.url = "/"
|
|
894
897
|
self.title = title
|
|
895
|
-
|
|
898
|
+
# See footer_content. Any falsy value (None, "", False) drops the
|
|
899
|
+
# footer; the default is an l10n key, so it translates like the rest.
|
|
900
|
+
self.footer = footer
|
|
901
|
+
# Seconds between Unpoly reloads of the footer content; see footer_poll.
|
|
902
|
+
self._footer_poll = footer_poll
|
|
896
903
|
self.templatefolder = path.join(path.dirname(__file__), "templates")
|
|
897
904
|
|
|
898
905
|
if index_view_class is not None:
|
|
@@ -1166,6 +1173,56 @@ class FlaskConstructUI(MenuMixin):
|
|
|
1166
1173
|
def t(self, msg_id, args=None):
|
|
1167
1174
|
return self.app.l10n.t(msg_id, args)
|
|
1168
1175
|
|
|
1176
|
+
@property
|
|
1177
|
+
def footer_content(self):
|
|
1178
|
+
"""The footer to render on full pages, or None for no footer at all.
|
|
1179
|
+
|
|
1180
|
+
``footer`` (the constructor argument, also settable as ``fcu.footer``)
|
|
1181
|
+
is one of
|
|
1182
|
+
|
|
1183
|
+
* a **string** -- an l10n key or plain text, passed through ``t()``
|
|
1184
|
+
and escaped. The default, ``"footer-built-with"``, reads "Built with
|
|
1185
|
+
Construct UI";
|
|
1186
|
+
* a ``markupsafe.Markup`` -- trusted HTML, emitted as-is (links,
|
|
1187
|
+
icons, several lines);
|
|
1188
|
+
* a **callable** taking no arguments and returning either of the above
|
|
1189
|
+
(or None). It runs per request, inside the request context, so it can
|
|
1190
|
+
use ``url_for``, ``current_user`` or the date;
|
|
1191
|
+
* anything **falsy** (``None``, ``""``, ``False``) -- no footer: the
|
|
1192
|
+
``<footer>`` element itself is omitted, not left empty.
|
|
1193
|
+
|
|
1194
|
+
For markup beyond one fragment, override ``{% block footer %}`` in the
|
|
1195
|
+
app's ``structure/base.html``; an empty override also removes it.
|
|
1196
|
+
"""
|
|
1197
|
+
footer = self.footer
|
|
1198
|
+
if callable(footer):
|
|
1199
|
+
footer = footer()
|
|
1200
|
+
if not footer:
|
|
1201
|
+
return None
|
|
1202
|
+
if isinstance(footer, Markup):
|
|
1203
|
+
return footer
|
|
1204
|
+
return self.t(str(footer))
|
|
1205
|
+
|
|
1206
|
+
@property
|
|
1207
|
+
def footer_url(self):
|
|
1208
|
+
"""URL of the footer fragment endpoint (for Unpoly polling)."""
|
|
1209
|
+
return apply_trailing_slash(self.url.rstrip("/") + "/_construct/footer", self)
|
|
1210
|
+
|
|
1211
|
+
@property
|
|
1212
|
+
def footer_poll(self):
|
|
1213
|
+
"""Interval in ms for polling the footer content, or None.
|
|
1214
|
+
|
|
1215
|
+
``footer_poll=<seconds>`` on the constructor keeps a live footer fresh:
|
|
1216
|
+
the footer lives outside ``#main-content``, so in-app navigation never
|
|
1217
|
+
re-renders it, and a callable ``footer`` would otherwise only update on
|
|
1218
|
+
a full page load. Off when no footer is configured -- there is nothing
|
|
1219
|
+
to refresh -- and it only covers ``footer_content``: an app that
|
|
1220
|
+
overrides ``{% block footer %}`` drops the polled element with it.
|
|
1221
|
+
"""
|
|
1222
|
+
if self._footer_poll is None or not self.footer:
|
|
1223
|
+
return None
|
|
1224
|
+
return int(self._footer_poll * 1000)
|
|
1225
|
+
|
|
1169
1226
|
@property
|
|
1170
1227
|
def notices(self):
|
|
1171
1228
|
if self._notifications is not None:
|
{flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/components/calendar.py
RENAMED
|
@@ -3,20 +3,26 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import calendar
|
|
6
|
-
import locale
|
|
7
6
|
from dataclasses import dataclass, field
|
|
8
7
|
from datetime import date, datetime
|
|
9
8
|
from typing import Any, Dict, List, Optional
|
|
10
9
|
|
|
11
|
-
from flask import g
|
|
12
10
|
from markupsafe import Markup
|
|
13
11
|
|
|
14
|
-
from flask_construct_ui.language import LOCALE_MAPPING
|
|
15
|
-
|
|
16
12
|
from ..sanitize import sanitize_html
|
|
17
13
|
from .core import Component, _t, jinja_env
|
|
18
14
|
|
|
19
15
|
|
|
16
|
+
_DAY_KEYS = ("mon", "tue", "wed", "thu", "fri", "sat", "sun")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _name(key, default):
|
|
20
|
+
"""A translated day/month name, or ``default`` (the C-locale English name)
|
|
21
|
+
when the active language's catalogue has no such key."""
|
|
22
|
+
value = _t(key, default=default)
|
|
23
|
+
return default if value == key else value
|
|
24
|
+
|
|
25
|
+
|
|
20
26
|
@dataclass
|
|
21
27
|
class CalendarComponent(Component):
|
|
22
28
|
datasource: Optional[str] = None
|
|
@@ -28,10 +34,13 @@ class CalendarComponent(Component):
|
|
|
28
34
|
return "calendar"
|
|
29
35
|
|
|
30
36
|
def render(self) -> str:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
# A local Calendar, not calendar.setfirstweekday(): that one (like
|
|
38
|
+
# locale.setlocale) is process-global, so concurrent requests on a
|
|
39
|
+
# threaded server would see each other's settings.
|
|
40
|
+
first = (
|
|
41
|
+
calendar.SUNDAY if self.start_day.lower() == "sunday" else calendar.MONDAY
|
|
42
|
+
)
|
|
43
|
+
cal = calendar.Calendar(first)
|
|
35
44
|
|
|
36
45
|
if self.data:
|
|
37
46
|
first_date = self.data[0].get("date")
|
|
@@ -43,27 +52,13 @@ class CalendarComponent(Component):
|
|
|
43
52
|
else:
|
|
44
53
|
today = date.today()
|
|
45
54
|
year, month = today.year, today.month
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
locale.setlocale(locale.LC_TIME, target_locale)
|
|
54
|
-
# Re-fetch names with the new locale
|
|
55
|
-
day_names = calendar.weekheader(3).split()
|
|
56
|
-
month_name = calendar.month_name[month]
|
|
57
|
-
except locale.Error:
|
|
58
|
-
# If locale is not installed on the system, it will fail.
|
|
59
|
-
# We just fall back to the default names.
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
|
-
finally:
|
|
63
|
-
# Always restore the original locale
|
|
64
|
-
locale.setlocale(locale.LC_TIME, original_locale)
|
|
65
|
-
|
|
66
|
-
month_calendar = calendar.monthcalendar(year, month)
|
|
55
|
+
|
|
56
|
+
day_names = [
|
|
57
|
+
_name(f"calendar-day-{_DAY_KEYS[d]}", calendar.day_abbr[d])
|
|
58
|
+
for d in cal.iterweekdays()
|
|
59
|
+
]
|
|
60
|
+
month_name = _name(f"calendar-month-{month}", calendar.month_name[month])
|
|
61
|
+
month_calendar = cal.monthdayscalendar(year, month)
|
|
67
62
|
|
|
68
63
|
# Cell content may come from a @datasource carrying user data, and the
|
|
69
64
|
# API allows HTML snippets — sanitize before the template marks it safe.
|
|
@@ -19,7 +19,7 @@ from .crud_forms import (
|
|
|
19
19
|
parse_form_data,
|
|
20
20
|
)
|
|
21
21
|
from .forms import FormData, validate_form_data
|
|
22
|
-
from .navigation import resolve_next
|
|
22
|
+
from .navigation import fragment, resolve_next
|
|
23
23
|
from .pagination import RELATED_SEARCH_LIMIT, get_pagination
|
|
24
24
|
from .base import PageBase, expose
|
|
25
25
|
from .rcrud import RelatedCrudTab as RelatedCrudTab
|
|
@@ -341,6 +341,7 @@ class CrudPageBase(PageBase):
|
|
|
341
341
|
**kwargs,
|
|
342
342
|
)
|
|
343
343
|
|
|
344
|
+
@fragment
|
|
344
345
|
@expose("/search/<field_name>")
|
|
345
346
|
def search_related(self, field_name):
|
|
346
347
|
ds = self.dataclass()
|
|
@@ -359,6 +360,7 @@ class CrudPageBase(PageBase):
|
|
|
359
360
|
field_name=field_name,
|
|
360
361
|
)
|
|
361
362
|
|
|
363
|
+
@fragment
|
|
362
364
|
@expose("/search-many/<field_name>")
|
|
363
365
|
def search_many_related(self, field_name):
|
|
364
366
|
ds = self.dataclass()
|
{flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/data_sqlalchemy.py
RENAMED
|
@@ -52,11 +52,16 @@ class DataSQLAlchemy(DataBase):
|
|
|
52
52
|
val_str = str(member.value).lower()
|
|
53
53
|
wrapper_str = str(ftype.ValueWrapper(member.name, member.value)).lower()
|
|
54
54
|
if op == "like":
|
|
55
|
-
|
|
55
|
+
# Only the SQL wildcards are operators; everything else the
|
|
56
|
+
# user typed is literal (an unescaped "(" was a 500).
|
|
57
|
+
pattern = "".join(
|
|
58
|
+
".*" if ch == "%" else "." if ch == "_" else re.escape(ch)
|
|
59
|
+
for ch in value
|
|
60
|
+
)
|
|
56
61
|
if (
|
|
57
|
-
re.
|
|
58
|
-
or re.
|
|
59
|
-
or re.
|
|
62
|
+
re.fullmatch(pattern, name, re.IGNORECASE)
|
|
63
|
+
or re.fullmatch(pattern, val_str, re.IGNORECASE)
|
|
64
|
+
or re.fullmatch(pattern, wrapper_str, re.IGNORECASE)
|
|
60
65
|
):
|
|
61
66
|
matched.append(member)
|
|
62
67
|
else:
|
|
@@ -330,18 +335,26 @@ class DataSQLAlchemy(DataBase):
|
|
|
330
335
|
|
|
331
336
|
sub_rec = list_class_ds.model(**sub_kwargs)
|
|
332
337
|
getattr(rec, ftype.list_field).append(sub_rec)
|
|
338
|
+
# Sub-row ids come from the form, so resolve them against
|
|
339
|
+
# this record's own collection — never a bare read(), which
|
|
340
|
+
# would reach any other parent's rows. Unknown ids are ignored.
|
|
341
|
+
owned = {}
|
|
342
|
+
if "delete" in actions or "update" in actions:
|
|
343
|
+
list_pk = ftype.list_class()._primary_key
|
|
344
|
+
owned = {
|
|
345
|
+
str(getattr(item, list_pk)): item
|
|
346
|
+
for item in getattr(rec, ftype.list_field)
|
|
347
|
+
}
|
|
333
348
|
if "delete" in actions:
|
|
334
|
-
list_class_ds = ftype.list_class()
|
|
335
349
|
for item_id_to_delete in actions["delete"]:
|
|
336
|
-
item_to_delete =
|
|
350
|
+
item_to_delete = owned.get(str(item_id_to_delete))
|
|
337
351
|
if item_to_delete:
|
|
338
352
|
self.session.delete(item_to_delete)
|
|
339
353
|
|
|
340
354
|
if "update" in actions:
|
|
341
|
-
list_class_ds = ftype.list_class()
|
|
342
355
|
for item_to_update_data in actions["update"]:
|
|
343
356
|
item_id_to_update = item_to_update_data.pop("id")
|
|
344
|
-
item_to_update =
|
|
357
|
+
item_to_update = owned.get(str(item_id_to_update))
|
|
345
358
|
if item_to_update:
|
|
346
359
|
for sub_key, sub_value in item_to_update_data.items():
|
|
347
360
|
setattr(item_to_update, sub_key, sub_value)
|
|
@@ -60,6 +60,7 @@ nav-skip-to-content = Skip to content
|
|
|
60
60
|
nav-main = Main navigation
|
|
61
61
|
nav-sidebar = Sidebar
|
|
62
62
|
nav-user-tools = User tools
|
|
63
|
+
footer-built-with = Built with Construct UI
|
|
63
64
|
nav-search = Search
|
|
64
65
|
nav-search-no-results = No results for "{$query}".
|
|
65
66
|
nav-notifications = Notifications
|
|
@@ -72,6 +73,25 @@ live-resume = Resume updates
|
|
|
72
73
|
live-reload = Reload for the latest
|
|
73
74
|
|
|
74
75
|
calendar-has-events = Has events
|
|
76
|
+
calendar-day-mon = Mon
|
|
77
|
+
calendar-day-tue = Tue
|
|
78
|
+
calendar-day-wed = Wed
|
|
79
|
+
calendar-day-thu = Thu
|
|
80
|
+
calendar-day-fri = Fri
|
|
81
|
+
calendar-day-sat = Sat
|
|
82
|
+
calendar-day-sun = Sun
|
|
83
|
+
calendar-month-1 = January
|
|
84
|
+
calendar-month-2 = February
|
|
85
|
+
calendar-month-3 = March
|
|
86
|
+
calendar-month-4 = April
|
|
87
|
+
calendar-month-5 = May
|
|
88
|
+
calendar-month-6 = June
|
|
89
|
+
calendar-month-7 = July
|
|
90
|
+
calendar-month-8 = August
|
|
91
|
+
calendar-month-9 = September
|
|
92
|
+
calendar-month-10 = October
|
|
93
|
+
calendar-month-11 = November
|
|
94
|
+
calendar-month-12 = December
|
|
75
95
|
|
|
76
96
|
chart-label = Chart
|
|
77
97
|
chart-unsupported-type = Unsupported chart type
|
|
@@ -61,6 +61,7 @@ nav-skip-to-content = Hoppa till innehåll
|
|
|
61
61
|
nav-main = Huvudnavigering
|
|
62
62
|
nav-sidebar = Sidopanel
|
|
63
63
|
nav-user-tools = Användarverktyg
|
|
64
|
+
footer-built-with = Byggd med Construct UI
|
|
64
65
|
nav-search = Sök
|
|
65
66
|
nav-search-no-results = Inga resultat för "{$query}".
|
|
66
67
|
nav-notifications = Notifieringar
|
|
@@ -73,6 +74,25 @@ live-resume = Återuppta uppdateringar
|
|
|
73
74
|
live-reload = Ladda om för senaste
|
|
74
75
|
|
|
75
76
|
calendar-has-events = Har händelser
|
|
77
|
+
calendar-day-mon = mån
|
|
78
|
+
calendar-day-tue = tis
|
|
79
|
+
calendar-day-wed = ons
|
|
80
|
+
calendar-day-thu = tor
|
|
81
|
+
calendar-day-fri = fre
|
|
82
|
+
calendar-day-sat = lör
|
|
83
|
+
calendar-day-sun = sön
|
|
84
|
+
calendar-month-1 = januari
|
|
85
|
+
calendar-month-2 = februari
|
|
86
|
+
calendar-month-3 = mars
|
|
87
|
+
calendar-month-4 = april
|
|
88
|
+
calendar-month-5 = maj
|
|
89
|
+
calendar-month-6 = juni
|
|
90
|
+
calendar-month-7 = juli
|
|
91
|
+
calendar-month-8 = augusti
|
|
92
|
+
calendar-month-9 = september
|
|
93
|
+
calendar-month-10 = oktober
|
|
94
|
+
calendar-month-11 = november
|
|
95
|
+
calendar-month-12 = december
|
|
76
96
|
|
|
77
97
|
chart-label = Diagram
|
|
78
98
|
chart-unsupported-type = Diagramtypen stöds inte
|
|
@@ -38,8 +38,12 @@ RECORD_TARGET = ":record"
|
|
|
38
38
|
#: OIDC authorize route out of ``next`` — a route can be perfectly legitimate
|
|
39
39
|
#: to *request* and still be nonsense to *land on*.
|
|
40
40
|
#:
|
|
41
|
+
#: Per-page fragment routes (a CRUD page's related-search dropdowns, a
|
|
42
|
+
#: component page's parts and live polls) have one endpoint per page, so they
|
|
43
|
+
#: cannot be listed here; their view functions carry :func:`fragment` instead.
|
|
41
44
|
#: Apps list their own fragment routes (anything driven by ``up-poll`` or
|
|
42
|
-
#: reloaded through ``up-source``) in ``CONSTRUCT_FRAGMENT_ENDPOINTS
|
|
45
|
+
#: reloaded through ``up-source``) in ``CONSTRUCT_FRAGMENT_ENDPOINTS``, or mark
|
|
46
|
+
#: the view with ``@fragment``.
|
|
43
47
|
FRAGMENT_ENDPOINTS = frozenset(
|
|
44
48
|
{
|
|
45
49
|
"construct_features.menu_fragment",
|
|
@@ -49,6 +53,22 @@ FRAGMENT_ENDPOINTS = frozenset(
|
|
|
49
53
|
)
|
|
50
54
|
|
|
51
55
|
|
|
56
|
+
#: Attribute :func:`fragment` sets on a view function.
|
|
57
|
+
FRAGMENT_ATTR = "_construct_fragment"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def fragment(view):
|
|
61
|
+
"""Mark ``view`` as answering with a bare fragment, never a whole page.
|
|
62
|
+
|
|
63
|
+
The per-view spelling of :data:`FRAGMENT_ENDPOINTS`, for routes whose
|
|
64
|
+
endpoint name is not known up front — every ``CrudPageBase`` gets its own
|
|
65
|
+
``<endpoint>.search_related``, and RCRUD forwards it again under the
|
|
66
|
+
parent's blueprint. Order relative to ``@expose`` does not matter.
|
|
67
|
+
"""
|
|
68
|
+
setattr(view, FRAGMENT_ATTR, True)
|
|
69
|
+
return view
|
|
70
|
+
|
|
71
|
+
|
|
52
72
|
def fragment_endpoints():
|
|
53
73
|
"""Every endpoint that must not be landed on: built-ins plus the app's."""
|
|
54
74
|
extra = current_app.config.get("CONSTRUCT_FRAGMENT_ENDPOINTS") or ()
|
|
@@ -69,9 +89,8 @@ def is_fragment_target(url=None):
|
|
|
69
89
|
there".
|
|
70
90
|
"""
|
|
71
91
|
try:
|
|
72
|
-
endpoints = fragment_endpoints()
|
|
73
92
|
if url is None:
|
|
74
|
-
return request.endpoint
|
|
93
|
+
return _is_fragment_endpoint(request.endpoint)
|
|
75
94
|
path = urlparse(url).path
|
|
76
95
|
adapter = current_app.url_map.bind(request.host)
|
|
77
96
|
try:
|
|
@@ -81,11 +100,19 @@ def is_fragment_target(url=None):
|
|
|
81
100
|
# to the same view, so follow it rather than reporting the URL as
|
|
82
101
|
# harmless because it was spelled without the slash.
|
|
83
102
|
endpoint, _ = adapter.match(urlparse(redirect.new_url).path, method="GET")
|
|
84
|
-
return endpoint
|
|
103
|
+
return _is_fragment_endpoint(endpoint)
|
|
85
104
|
except Exception:
|
|
86
105
|
return False
|
|
87
106
|
|
|
88
107
|
|
|
108
|
+
def _is_fragment_endpoint(endpoint):
|
|
109
|
+
"""True when ``endpoint`` is listed as a fragment or its view is marked."""
|
|
110
|
+
if endpoint in fragment_endpoints():
|
|
111
|
+
return True
|
|
112
|
+
view = current_app.view_functions.get(endpoint)
|
|
113
|
+
return bool(getattr(view, FRAGMENT_ATTR, False))
|
|
114
|
+
|
|
115
|
+
|
|
89
116
|
def is_safe_next(url):
|
|
90
117
|
"""True when ``url`` is a safe, app-relative redirect target."""
|
|
91
118
|
if not url or not url.startswith("/"):
|
|
@@ -22,7 +22,7 @@ from .crud_forms import (
|
|
|
22
22
|
)
|
|
23
23
|
from .data import DataBase
|
|
24
24
|
from .forms import FormData, validate_form_data
|
|
25
|
-
from .navigation import resolve_next
|
|
25
|
+
from .navigation import FRAGMENT_ATTR, resolve_next
|
|
26
26
|
from .pagination import get_pagination
|
|
27
27
|
|
|
28
28
|
|
|
@@ -246,6 +246,25 @@ class RelatedCrudTab:
|
|
|
246
246
|
rct=rct,
|
|
247
247
|
)
|
|
248
248
|
|
|
249
|
+
def _read_child(id, child_id):
|
|
250
|
+
"""The child record, or ``None`` if it is missing *or belongs to
|
|
251
|
+
another parent*. ``child_id`` comes from the URL, so without this a
|
|
252
|
+
user allowed on parent A could open, edit (and so re-parent) or
|
|
253
|
+
delete parent B's children. A mismatch reads as not-found so the
|
|
254
|
+
response doesn't reveal that the id exists elsewhere."""
|
|
255
|
+
ds = child_dataclass()
|
|
256
|
+
rec = ds.read(child_id)
|
|
257
|
+
if rec is None:
|
|
258
|
+
return ds, None
|
|
259
|
+
owner = (
|
|
260
|
+
rec.get(filter_field)
|
|
261
|
+
if isinstance(rec, dict)
|
|
262
|
+
else getattr(rec, filter_field, None)
|
|
263
|
+
)
|
|
264
|
+
if owner is None or str(owner) != str(id):
|
|
265
|
+
return ds, None
|
|
266
|
+
return ds, rec
|
|
267
|
+
|
|
249
268
|
def list_view(id):
|
|
250
269
|
ds = child_dataclass()
|
|
251
270
|
page, per_page, limit = get_pagination(
|
|
@@ -418,8 +437,7 @@ class RelatedCrudTab:
|
|
|
418
437
|
)
|
|
419
438
|
|
|
420
439
|
def edit_view(id, child_id):
|
|
421
|
-
ds =
|
|
422
|
-
rec = ds.read(child_id)
|
|
440
|
+
ds, rec = _read_child(id, child_id)
|
|
423
441
|
if rec is None:
|
|
424
442
|
flash("crud-error-not-found", "danger")
|
|
425
443
|
return redirect(url_for(f"{parent_view.endpoint}.{list_ep}", id=id))
|
|
@@ -525,8 +543,7 @@ class RelatedCrudTab:
|
|
|
525
543
|
)
|
|
526
544
|
|
|
527
545
|
def view_view(id, child_id):
|
|
528
|
-
ds =
|
|
529
|
-
rec = ds.read(child_id)
|
|
546
|
+
ds, rec = _read_child(id, child_id)
|
|
530
547
|
if rec is None:
|
|
531
548
|
flash("crud-error-not-found", "danger")
|
|
532
549
|
return redirect(url_for(f"{parent_view.endpoint}.{list_ep}", id=id))
|
|
@@ -571,11 +588,10 @@ class RelatedCrudTab:
|
|
|
571
588
|
)
|
|
572
589
|
|
|
573
590
|
def delete_view(id, child_id):
|
|
574
|
-
ds = child_dataclass()
|
|
575
591
|
back = redirect(
|
|
576
592
|
resolve_next(url_for(f"{parent_view.endpoint}.{list_ep}", id=id))
|
|
577
593
|
)
|
|
578
|
-
row =
|
|
594
|
+
ds, row = _read_child(id, child_id)
|
|
579
595
|
if row is None:
|
|
580
596
|
flash("crud-error-not-found", "danger")
|
|
581
597
|
return back
|
|
@@ -632,6 +648,12 @@ class RelatedCrudTab:
|
|
|
632
648
|
for p in prefix_params:
|
|
633
649
|
kwargs.pop(p, None)
|
|
634
650
|
if child_id_kwarg in kwargs:
|
|
651
|
+
# Same parent check as the built-in member routes.
|
|
652
|
+
if _read_child(id, kwargs[child_id_kwarg])[1] is None:
|
|
653
|
+
flash("crud-error-not-found", "danger")
|
|
654
|
+
return redirect(
|
|
655
|
+
url_for(f"{parent_view.endpoint}.{list_ep}", id=id)
|
|
656
|
+
)
|
|
635
657
|
kwargs["id"] = kwargs.pop(child_id_kwarg)
|
|
636
658
|
child._rcrud_context = {
|
|
637
659
|
"parent_endpoint": parent_view.endpoint,
|
|
@@ -643,6 +665,10 @@ class RelatedCrudTab:
|
|
|
643
665
|
return getattr(child, method_name)(**kwargs)
|
|
644
666
|
|
|
645
667
|
exposed_view.__name__ = ep_name
|
|
668
|
+
# A forwarded fragment (the child's related-search dropdown)
|
|
669
|
+
# is still a fragment under the parent's blueprint.
|
|
670
|
+
if getattr(getattr(child_cls, method_name), FRAGMENT_ATTR, False):
|
|
671
|
+
setattr(exposed_view, FRAGMENT_ATTR, True)
|
|
646
672
|
return exposed_view
|
|
647
673
|
|
|
648
674
|
for url, name, methods in child_cls._curls:
|
|
@@ -107,17 +107,23 @@ def return_target() -> str | None:
|
|
|
107
107
|
``next`` parameter uses (:func:`~flask_construct_ui.navigation.is_safe_next`).
|
|
108
108
|
|
|
109
109
|
Either way the target must be somewhere a reader can actually *be*. A
|
|
110
|
-
fragment endpoint (the sidebar nav,
|
|
111
|
-
guarded route like any other, so a logged-out
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
:func:`~flask_construct_ui.navigation.is_fragment_target`.
|
|
110
|
+
fragment endpoint (the sidebar nav, a related-search dropdown) is a
|
|
111
|
+
guarded route like any other, so a logged-out GET to one would otherwise
|
|
112
|
+
name itself here and the login round trip would end with the bare
|
|
113
|
+
fragment filling the window — see
|
|
114
|
+
:func:`~flask_construct_ui.navigation.is_fragment_target`. A fragment is
|
|
115
|
+
requested *from* a page, though, so it takes the referer branch too: the
|
|
116
|
+
reader comes back to the form that held the dropdown, not to the fragment
|
|
117
|
+
and not to the site root.
|
|
115
118
|
"""
|
|
116
|
-
if request.method in SAFE_METHODS:
|
|
117
|
-
if is_fragment_target():
|
|
118
|
-
return None
|
|
119
|
+
if request.method in SAFE_METHODS and not is_fragment_target():
|
|
119
120
|
target = request.full_path.rstrip("?") or "/"
|
|
120
121
|
return target if is_safe_next(target) else None
|
|
122
|
+
return _referer_target()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _referer_target() -> str | None:
|
|
126
|
+
"""The same-host page this request was made from, if it is a landing page."""
|
|
121
127
|
parsed = urlparse(request.referrer or "")
|
|
122
128
|
if not parsed.path or (parsed.netloc and parsed.netloc != request.host):
|
|
123
129
|
return None
|
{flask_construct_ui-0.11.5 → flask_construct_ui-0.11.7}/flask_construct_ui/static/css/whitelabel.css
RENAMED
|
@@ -400,6 +400,18 @@ a:hover {
|
|
|
400
400
|
text-decoration: none;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
/* App-owned slot between the brand and the actions (`{% block navbar_start %}`).
|
|
404
|
+
It takes the free width, so its content starts next to the brand; an app
|
|
405
|
+
wanting it centred adds `justify-content: center` on this class. */
|
|
406
|
+
.fcu-navbar__start {
|
|
407
|
+
flex: 1 1 auto;
|
|
408
|
+
min-width: 0;
|
|
409
|
+
display: flex;
|
|
410
|
+
align-items: center;
|
|
411
|
+
gap: var(--fcu-gap);
|
|
412
|
+
margin: 0 var(--fcu-gap);
|
|
413
|
+
}
|
|
414
|
+
|
|
403
415
|
/* Right-hand navbar action group: search, notifications, user menu — all on
|
|
404
416
|
one row, vertically centered. */
|
|
405
417
|
.fcu-navbar__actions {
|
|
@@ -1597,6 +1609,14 @@ fieldset.fcu-form-section__body {
|
|
|
1597
1609
|
flex-wrap: wrap;
|
|
1598
1610
|
}
|
|
1599
1611
|
|
|
1612
|
+
.fcu-navbar__start {
|
|
1613
|
+
/* Its own full-width row between the brand and the actions, still in DOM
|
|
1614
|
+
order, rather than squeezing between them. */
|
|
1615
|
+
flex-basis: 100%;
|
|
1616
|
+
flex-wrap: wrap;
|
|
1617
|
+
margin: 0;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1600
1620
|
.fcu-navbar__brand {
|
|
1601
1621
|
/* A long app title is one unbreakable flex item otherwise, and it sets the
|
|
1602
1622
|
floor for the whole page width. */
|