flask-construct-ui 0.11.6__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.
Files changed (111) hide show
  1. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/PKG-INFO +1 -1
  2. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/components/calendar.py +24 -29
  3. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/data_sqlalchemy.py +21 -8
  4. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/l10n/en/main.flt +19 -0
  5. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/l10n/sv/main.flt +19 -0
  6. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/rcrud.py +28 -6
  7. flask_construct_ui-0.11.7/flask_construct_ui/static/js/construct-core.min.js +1 -0
  8. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/live.html +1 -1
  9. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui.egg-info/PKG-INFO +1 -1
  10. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/pyproject.toml +1 -1
  11. flask_construct_ui-0.11.6/flask_construct_ui/static/js/construct-core.min.js +0 -1
  12. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/LICENSE +0 -0
  13. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/README.md +0 -0
  14. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/__init__.py +0 -0
  15. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/app_hooks.py +0 -0
  16. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/auth.py +0 -0
  17. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/base.py +0 -0
  18. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/capabilities.py +0 -0
  19. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/cli.py +0 -0
  20. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/components/__init__.py +0 -0
  21. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/components/chart.py +0 -0
  22. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/components/content.py +0 -0
  23. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/components/core.py +0 -0
  24. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/components/grid.py +0 -0
  25. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/components/kanban.py +0 -0
  26. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/components/layout.py +0 -0
  27. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/components/live.py +0 -0
  28. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/contrast.py +0 -0
  29. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/crud.py +0 -0
  30. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/crud_forms.py +0 -0
  31. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/csrf.py +0 -0
  32. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/data.py +0 -0
  33. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/forms.py +0 -0
  34. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/grid.py +0 -0
  35. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/headless.py +0 -0
  36. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/icons.py +0 -0
  37. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/language.py +0 -0
  38. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/layout.py +0 -0
  39. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/menu.py +0 -0
  40. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/navbar.py +0 -0
  41. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/navigation.py +0 -0
  42. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/orgmode.py +0 -0
  43. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/pagination.py +0 -0
  44. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/py.typed +0 -0
  45. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/resume.py +0 -0
  46. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/sanitize.py +0 -0
  47. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/schema.py +0 -0
  48. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/css/grid.css +0 -0
  49. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/css/prosemirror-bundle.min.css +0 -0
  50. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/css/unpoly.min.css +0 -0
  51. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/css/whitelabel.css +0 -0
  52. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/designs/adminlte.css +0 -0
  53. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/designs/sandstone.css +0 -0
  54. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/bootstrap-icons/LICENSE.txt +0 -0
  55. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/bootstrap-icons/bootstrap-icons.min.css +0 -0
  56. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  57. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  58. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/material-symbols/LICENSE.txt +0 -0
  59. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/material-symbols/material-symbols-outlined.woff2 +0 -0
  60. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/fonts/material-symbols/material-symbols.css +0 -0
  61. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/js/construct-grid.min.js +0 -0
  62. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/js/kanban-dnd.js +0 -0
  63. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/js/prosemirror-bundle.min.js +0 -0
  64. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/static/js/unpoly.min.js +0 -0
  65. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/_form_field.html +0 -0
  66. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/button.html +0 -0
  67. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/calendar.html +0 -0
  68. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/calendarmini.html +0 -0
  69. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/card.html +0 -0
  70. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/chart.html +0 -0
  71. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/form.html +0 -0
  72. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/grid.html +0 -0
  73. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/grid_layout.html +0 -0
  74. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/grid_notice.html +0 -0
  75. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/horizontal_layout.html +0 -0
  76. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/kanban.html +0 -0
  77. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/main.html +0 -0
  78. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/markup.html +0 -0
  79. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/rich_text.html +0 -0
  80. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/table.html +0 -0
  81. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/tabs.html +0 -0
  82. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/text.html +0 -0
  83. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/components/vertical_layout.html +0 -0
  84. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/_macros.html +0 -0
  85. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/_many_related_options.html +0 -0
  86. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/_related_options.html +0 -0
  87. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/create.html +0 -0
  88. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/edit.html +0 -0
  89. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/listindex.html +0 -0
  90. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/related_crud_tab_form.html +0 -0
  91. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/related_crud_tab_list.html +0 -0
  92. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/tab_view.html +0 -0
  93. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/crud/view.html +0 -0
  94. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/errors/forbidden.html +0 -0
  95. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/navbar/notifications.html +0 -0
  96. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/navbar/search_results.html +0 -0
  97. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/structure/base.html +0 -0
  98. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/structure/public.html +0 -0
  99. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/style/_flashes.html +0 -0
  100. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/style/_footer.html +0 -0
  101. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/style/_menu.html +0 -0
  102. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/style/base_full.html +0 -0
  103. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/templates/style/base_partial.html +0 -0
  104. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/testing.py +0 -0
  105. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/urls.py +0 -0
  106. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui/validation.py +0 -0
  107. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui.egg-info/SOURCES.txt +0 -0
  108. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui.egg-info/dependency_links.txt +0 -0
  109. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui.egg-info/requires.txt +0 -0
  110. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/flask_construct_ui.egg-info/top_level.txt +0 -0
  111. {flask_construct_ui-0.11.6 → flask_construct_ui-0.11.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask-construct-ui
3
- Version: 0.11.6
3
+ Version: 0.11.7
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
@@ -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
- if self.start_day.lower() == "sunday":
32
- calendar.setfirstweekday(calendar.SUNDAY)
33
- else:
34
- calendar.setfirstweekday(calendar.MONDAY)
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
- lang = getattr(g, "language", None)
47
- target_locale = LOCALE_MAPPING.get(lang)
48
- day_names = calendar.weekheader(3).split()
49
- month_name = calendar.month_name[month]
50
- if target_locale:
51
- original_locale = locale.getlocale(locale.LC_TIME)
52
- try:
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.
@@ -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
- pattern = value.replace("%", ".*").replace("_", ".")
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.match(f"^{pattern}$", name, re.IGNORECASE)
58
- or re.match(f"^{pattern}$", val_str, re.IGNORECASE)
59
- or re.match(f"^{pattern}$", wrapper_str, re.IGNORECASE)
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 = list_class_ds.read(item_id_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 = list_class_ds.read(item_id_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)
@@ -73,6 +73,25 @@ live-resume = Resume updates
73
73
  live-reload = Reload for the latest
74
74
 
75
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
76
95
 
77
96
  chart-label = Chart
78
97
  chart-unsupported-type = Unsupported chart type
@@ -74,6 +74,25 @@ live-resume = Återuppta uppdateringar
74
74
  live-reload = Ladda om för senaste
75
75
 
76
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
77
96
 
78
97
  chart-label = Diagram
79
98
  chart-unsupported-type = Diagramtypen stöds inte
@@ -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 = child_dataclass()
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 = child_dataclass()
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 = ds.read(child_id)
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,
@@ -0,0 +1 @@
1
+ (()=>{var _={};function F(s,c){return(s==="manyrelated-search"?"manyrelated-results-":"search-results-")+c}function M(s,c){["manyrelated-search-","search-input-"].forEach(function(l){let f=document.getElementById(l+s);f&&f.getAttribute("role")==="combobox"&&(f.setAttribute("aria-expanded",c?"true":"false"),c||f.removeAttribute("aria-activedescendant"))})}function j(s){s&&(s.hasAttribute("tabindex")||(s.setAttribute("tabindex","-1"),s.addEventListener("blur",function c(){s.removeAttribute("tabindex"),s.removeEventListener("blur",c)})),s.focus({preventScroll:!0}))}function J(s,c,l){let f=document.getElementById((c?"manyrelated-status-":"search-status-")+s);if(!f)return;let p=document.getElementById((c?"manyrelated-results-":"search-results-")+s),y=p&&p.querySelector('[aria-disabled="true"]');f.textContent=l>0?String(l):y?y.textContent.trim():""}function P(s){if(!s)return;let c=null;try{c=window.localStorage.getItem("fcu-sidebar-open")}catch{return}c!==null&&(s.open=c==="1")}window.fcuRestoreSidebar=P;if(typeof up<"u"){let s=function(e){return e.getClientRects().length>0},c=function(e){let r=document.getElementById(e)||document.querySelector('[name="'+CSS.escape(e)+'"]');if(!r)return null;if(s(r)&&!r.disabled)return r;let n=r.closest(".fcu-form-group");return n&&Array.from(n.querySelectorAll('input, select, textarea, [contenteditable="true"], button')).find(i=>!i.disabled&&i.type!=="hidden"&&s(i))||null},f=function(e){let r=e.querySelector(".fcu-menu-text");return r?r.textContent.trim():""},p=function(){try{let e=window.sessionStorage.getItem(l),r=e?JSON.parse(e):null;return r&&typeof r=="object"?r:{}}catch{return{}}},y=function(e,r){if(e)try{let n=p();n[e]=r,window.sessionStorage.setItem(l,JSON.stringify(n))}catch{}},t=function(e){try{window.localStorage.setItem(S,e?"1":"0")}catch{}},o=function(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")};up.status&&up.status.config&&up.status.config.currentClasses&&(up.status.config.currentClasses.includes("active")||up.status.config.currentClasses.push("active")),up.feedback&&up.feedback.config&&up.feedback.config.currentClasses&&(up.feedback.config.currentClasses.includes("active")||up.feedback.config.currentClasses.push("active")),up.on("up:fragment:loaded",function(e){let r=e.response;if(!r)return;let n=r.header("X-Construct-Login"),i=r.header("X-Construct-Auth"),u=document.documentElement.getAttribute("data-fcu-auth");!n&&!(i&&u&&i!==u)||(e.preventDefault(),!(e.request&&e.request.background)&&(window.location.href=n||r.url||window.location.href))}),up.on("up:fragment:loaded",function(e){let r=e.response;if(!r||r.header("X-Construct-Login"))return;let n=r.header("X-Construct-Menu");if(!n)return;let i=document.documentElement;if(n===i.getAttribute("data-fcu-menu"))return;let u=document.querySelector("#fcu-main-menu[up-source]");u&&(i.setAttribute("data-fcu-menu",n),up.reload(u,{cache:!1,background:!0}).catch(function(){i.setAttribute("data-fcu-menu","")}))}),up.on("up:fragment:inserted",function(e,r){let n=r||e&&e.target;if(!n||n.id!=="main-content"||n.closest('[up-focus="keep"]'))return;let i=n.querySelector("#form-errors");j(i||n)}),up.on("up:fragment:loaded",function(e){let r=e.response;!r||!e.renderOptions||r.text.indexOf('id="form-errors"')!==-1&&(e.renderOptions.focus="#form-errors",e.renderOptions.failFocus="#form-errors")}),document.addEventListener("click",function(e){let r=e.target.closest&&e.target.closest('#form-errors a[href^="#"]');if(!r)return;let n=c(decodeURIComponent(r.getAttribute("href").slice(1)));n&&(e.preventDefault(),n.focus(),n.scrollIntoView({block:"center"}))},!0),up.on("up:location:changed",function(){setTimeout(()=>{document.querySelectorAll(".fcu-submenu-link.active, .fcu-submenu-link.up-current").forEach(function(e){let r=e.closest("details");r&&(r.open=!0)})},0)});let l="fcu-menu-submenus";up.compiler("#fcu-main-menu",function(e){let r=p();e.querySelectorAll("details.fcu-submenu-details").forEach(function(n){let i=r[f(n)];i!==void 0&&(!i&&n.querySelector(".fcu-submenu-link.active, .fcu-submenu-link.up-current")||(n.open=i))}),e.addEventListener("toggle",function(n){let i=n.target;!i.matches||!i.matches("details.fcu-submenu-details")||y(f(i),i.open)},!0)});let S="fcu-sidebar-open";up.compiler(".fcu-sidebar-disclosure",function(e){P(e),!e.dataset.fcuSidebarBound&&(e.dataset.fcuSidebarBound="true",e.addEventListener("toggle",function(){t(e.open)}))}),up.compiler(".fcu-menu-link, .fcu-submenu-link",function(e){let r=e.getAttribute("href");if(r&&r!=="#"){let n=r.replace(/\/+$/,"");n&&n!=="/"&&e.setAttribute("up-alias",`${n} ${n}/ ${n}/*`)}}),up.compiler("[data-live-sse-url], [data-live-poll-url]",function(e){let r=e.getAttribute("data-live-sse-url"),n=e.getAttribute("data-live-poll-url"),i=parseInt(e.getAttribute("data-live-interval")||"10000",10),u=e.getAttribute("data-live-content-id")||"live-content-"+e.getAttribute("data-live-source"),a=document.getElementById(u),E=e.getAttribute("data-live-format")||"default",b=e.getAttribute("data-live-limit")?parseInt(e.getAttribute("data-live-limit"),10):200;if(!a)return;let w=null,h=null,I=!1,m=!1,x=!1;setTimeout(()=>{m||(I=!0)},2*i);function q(){x||(a.scrollTop=a.scrollHeight)}function L(d){if(m||d&&!I)return;e.classList.toggle("live-stale",d);let g=e.querySelector(".live-stale-indicator");g&&(g.style.display=d?"":"none")}function $(d){if(m)return;let g=location.href;if(d)try{let v=new URL(d,location.href);v.origin===location.origin&&(g=v.href)}catch{}typeof up<"u"&&up.visit?(up.cache&&up.cache.expire&&up.cache.expire(),up.visit(g,{cache:!1})):window.location.href=g}function U(){let d=i*.15*(Math.random()*2-1);return Math.max(1e3,i+d)}function T(){m||x||document.hidden||fetch(n).then(d=>m?null:d.status===205?(C(),$(),null):d.text()).then(d=>{if(m||!d)return;let v=new DOMParser().parseFromString(d,"text/html").querySelector(".live-content");v&&(a.innerHTML=v.innerHTML),L(!1)}).catch(()=>{m||L(!0)})}function H(){m||(w=setTimeout(()=>{m||(T(),H())},U()))}function R(){m||document.hidden||T()}function B(){m||w||!n||(document.addEventListener("visibilitychange",R),H())}function C(){w&&(clearTimeout(w),w=null),document.removeEventListener("visibilitychange",R)}r?(h=new EventSource(r),h.addEventListener("update",function(d){if(!(m||x)){if(E==="raw"){a.innerHTML=d.data,q(),L(!1),e.dispatchEvent(new CustomEvent("fcu:live:update",{detail:d.data}));return}try{let g=JSON.parse(d.data);e.dispatchEvent(new CustomEvent("fcu:live:update",{detail:g}));let v=Object.entries(g).map(([O,k])=>O==="line"?`<div style="padding:2px 0;border-bottom:1px solid rgba(0,0,0,0.07)">${o(k)}</div>`:Array.isArray(k)?k.map(X=>`<div style="padding:2px 0;border-bottom:1px solid rgba(0,0,0,0.07)">${o(X)}</div>`).join(""):`<div><strong>${o(O)}</strong>: ${o(k)}</div>`).join("");if(E==="append"){if(a.insertAdjacentHTML("beforeend",v),b>0)for(;a.children.length>b;)a.removeChild(a.firstChild)}else a.innerHTML=v;q(),L(!1)}catch{if(E==="append"){let v=document.createElement("div");if(v.textContent=d.data,a.appendChild(v),b>0)for(;a.children.length>b;)a.removeChild(a.firstChild)}else a.textContent=d.data;q(),e.dispatchEvent(new CustomEvent("fcu:live:update",{detail:d.data}))}}}),h.addEventListener("done",function(d){if(m)return;h.close(),h=null,C();let g={};try{g=JSON.parse(d.data)||{}}catch{}e.dispatchEvent(new CustomEvent("fcu:live:done",{detail:g.redirect})),g.auto||$(g.redirect)}),h.onerror=function(){m||(h.close(),h=null,L(!0),B())}):B();let A=e.querySelector("[data-live-pause]");function D(){x=!x,A.setAttribute("aria-pressed",x?"true":"false");let d=A.querySelector("[data-live-pause-label]");d&&(d.textContent=x?A.dataset.resumeLabel||"Resume updates":A.dataset.pauseLabel||"Pause updates"),x?C():h||(B(),T())}return A&&A.addEventListener("click",D),function(){m=!0,A&&A.removeEventListener("click",D),h&&(h.close(),h=null),C()}})}if(typeof up<"u"){let l=function(t){let o=document.getElementById("manyrelated-results-"+t);o&&(o.innerHTML="");let e=document.getElementById("manyrelated-search-"+t);e&&(e.value="",e.focus())},f=function(t,o,e,r){let n=document.createElement("span");n.style.cssText=`display: inline-flex; align-items: center; gap: 4px; background-color: var(--fcu-surface-alt); border: 1px ${e?"dashed":"solid"} var(--fcu-border); font-size: var(--fcu-font-size-sm); font-weight: 500; padding: 2px 8px; border-radius: var(--fcu-border-radius);`,n.appendChild(document.createTextNode(o));let i=document.createElement("button");i.type="button",i.style.cssText="margin-left: 4px; border: none; background: transparent; padding: 0; cursor: pointer; color: var(--fcu-text-muted); display: inline-flex; align-items: center;";let u=document.getElementById("manyrelated-search-"+t),a=u&&u.parentNode.querySelector("span button");return a?i.innerHTML=a.innerHTML:i.textContent="\xD7",i.addEventListener("click",function(E){E.stopPropagation(),r(n)}),n.appendChild(i),u&&u.parentNode.insertBefore(n,u),n},p=function(t){return t.querySelectorAll("a.fcu-dropdown-item, a.list-group-item")},y=function(t,o,e){if(p(o).forEach(function(r){r.classList.remove("active"),r.setAttribute("aria-selected","false")}),!e){t.removeAttribute("aria-activedescendant");return}e.classList.add("active"),e.setAttribute("aria-selected","true"),e.id&&t.setAttribute("aria-activedescendant",e.id),e.scrollIntoView({block:"nearest"})},S=function(t,o){t.addEventListener("keydown",function(e){let r=t.id.replace(o+"-",""),n=document.getElementById(F(o,r));if(!n)return;if(e.key==="Escape"){e.preventDefault(),n.innerHTML="",y(t,n,null),M(r,!1),o==="search-input"&&t.blur();return}let i=p(n);if(i.length===0)return;let u=n.querySelector("a.fcu-dropdown-item.active, a.list-group-item.active"),a=u?Array.from(i).indexOf(u):-1;if(e.key==="ArrowDown")e.preventDefault(),y(t,n,i[(a+1)%i.length]);else if(e.key==="ArrowUp"){e.preventDefault();let E=a<0?i.length-1:(a-1+i.length)%i.length;y(t,n,i[E])}else e.key==="Enter"&&(e.preventDefault(),u&&u.click())})};up.compiler(".js-hide",function(t){t.querySelectorAll("input, textarea, select").forEach(o=>o.disabled=!0)}),up.compiler(".secret-toggle-btn",function(t){let o=e=>{let r=t.closest(".fcu-form-group");if(!r)return;let n=r.querySelector(".secret-input-container"),i=r.querySelector(".secret-change-checkbox");if(!n||!i)return;let u=n.querySelectorAll("input, textarea, select"),a=b=>{t.title=b;let w=t.querySelector(".fcu-sr-only");w&&(w.textContent=b)},E=(b,w)=>{let h=t.querySelector(".bi");if(h){Array.from(h.classList).filter(m=>m.indexOf("bi-")===0).forEach(m=>h.classList.remove(m)),h.classList.add(b);return}let I=t.querySelector(".material-symbols-outlined");I&&(I.textContent=w)};n.style.display==="none"?(n.style.display="block",u.forEach(b=>b.disabled=!1),i.checked=!0,t.classList.add("active"),t.setAttribute("aria-expanded","true"),E("bi-x-lg","close"),a(t.dataset.cancelLabel||t.title),u.length>0&&u[0].focus()):(n.style.display="none",u.forEach(b=>b.disabled=!0),i.checked=!1,t.classList.remove("active"),t.setAttribute("aria-expanded","false"),E("bi-pencil-fill","edit"),a(t.dataset.changeLabel||t.title))};return t.addEventListener("click",o),()=>t.removeEventListener("click",o)}),up.compiler("[data-fcu-dismiss]",function(t){let o=()=>{let e=t.closest(".fcu-alert");if(!e)return;let r=e.parentElement,n=e.nextElementSibling||e.previousElementSibling;if(up.destroy(e),n&&n.querySelector("[data-fcu-dismiss]"))n.querySelector("[data-fcu-dismiss]").focus();else{let i=document.getElementById("main-content");i?j(i):r&&r.parentElement&&r.parentElement.focus?.()}};return t.addEventListener("click",o),()=>t.removeEventListener("click",o)}),up.compiler("[data-auto-dismiss]",function(t){let o=(e,r)=>{let n=r||e.target;n&&!n.contains(t)&&document.contains(t)&&(up.destroy(t,{transition:"fade-out"}),up.off("up:fragment:inserted",o))};return up.on("up:fragment:inserted",o),()=>up.off("up:fragment:inserted",o)});let s={},c={};window.addItem=function(t){if(s[t]===void 0){let r=document.querySelectorAll(`[name^="${t}-new-"]`).length;s[t]=r}let o=document.getElementById(`${t}-new-item-template`),e=document.getElementById(`${t}-new-items-container`);if(o&&e){let r=o.content.cloneNode(!0),n=document.createElement("div");n.appendChild(r);let i=n.innerHTML.replace(/__INDEX__/g,s[t]);e.insertAdjacentHTML("beforeend",i),s[t]++}},window.selectItem=function(t,o,e){let r=document.getElementById(t);r&&(r.value=o);let n=document.getElementById(`selected-item-display-${t}`);n&&(n.value=e);let i=document.getElementById(`search-results-${t}`);i&&(i.innerHTML="");let u=document.getElementById(`search-input-${t}`);u&&(u.value="")},window.addManyRelatedItem=function(t,o,e){let r=document.getElementById(t+"-hidden-inputs");if(!r)return;if(r.querySelector(`input[value="${o}"]`)){l(t);return}let n=document.createElement("input");n.type="hidden",n.name=t,n.value=o,r.appendChild(n);let i=f(t,e,!1,function(u){n.remove(),u.remove()});i.id=t+"-badge-"+o,l(t)},window.addManyRelatedQuickItem=function(t,o){if(Array.from(document.querySelectorAll(`input[name="${t}-quicknew"]`)).some(function(a){return!a.disabled&&a.value===o})){l(t);return}let r=document.getElementById(t+"-hidden-inputs"),n=r&&r.dataset.pendingLabel||"",i=document.createElement("input");i.type="hidden",i.name=t+"-quicknew",i.value=o;let u=f(t,o,!0,function(a){a.remove()});if(u.insertBefore(i,u.firstChild),n){let a=document.createElement("span");a.style.cssText="color: var(--fcu-text-muted); font-weight: normal;",a.textContent=n,u.insertBefore(a,u.lastChild)}l(t)},window.addManyRelatedNewPanel=function(t){let o=`${t}-newrel-`;if(c[t]===void 0){let n=-1;document.querySelectorAll(`[name^="${o}"]`).forEach(function(i){let u=parseInt(i.name.slice(o.length).split("-")[0],10);!isNaN(u)&&u>n&&(n=u)}),c[t]=n+1}let e=document.getElementById(`${t}-newrel-item-template`),r=document.getElementById(`${t}-newrel-items-container`);if(e&&r){let n=e.content.cloneNode(!0),i=document.createElement("div");i.appendChild(n);let u=i.innerHTML.replace(/__INDEX__/g,c[t]);r.insertAdjacentHTML("beforeend",u),c[t]++}},window.removeManyRelatedItem=function(t,o){let e=document.getElementById(t+"-hidden-inputs");if(e){let n=e.querySelector(`input[value="${o}"]`);n&&n.remove()}let r=document.getElementById(t+"-badge-"+o);r&&r.remove()},up.compiler("[data-js-enable]",function(t){t.disabled=!1}),up.compiler(".js-hide select",function(t){t.disabled=!0}),up.compiler('input[id^="search-input-"]',function(t){S(t,"search-input")}),up.compiler('input[id^="manyrelated-search-"]',function(t){S(t,"manyrelated-search")}),up.compiler('[role="listbox"][id$="-results"], #search-results, [id^="search-results-"], [id^="manyrelated-results-"]',function(t){let o=t.id,e=o.indexOf("manyrelated-results-")===0,r=o.replace(e?"manyrelated-results-":"search-results-","");if(!document.getElementById((e?"manyrelated-search-":"search-input-")+r))return;let i=p(t).length;M(r,i>0),J(r,e,i)})}document.addEventListener("input",function(s){let c=s.target;if(c.matches('input[id^="manyrelated-search-"]')){let l=c.id.replace("manyrelated-search-",""),f=c.getAttribute("up-href"),p=c.value,y=parseInt(c.getAttribute("up-delay"))||0,S=parseInt(c.getAttribute("up-min-length"))||0;if(clearTimeout(window.manyRelatedTimer),f&&p.length>=S)window.manyRelatedTimer=setTimeout(function(){typeof up<"u"&&up.render({url:f,target:"#manyrelated-results-"+l,params:{q:p}})},y);else{let t=document.getElementById("manyrelated-results-"+l);t&&(t.innerHTML="")}return}if(c.matches('input[id^="search-input-"]')){let l=c.id.replace("search-input-",""),f=c.getAttribute("up-href"),p=c.getAttribute("up-target"),y=c.value,S=parseInt(c.getAttribute("up-delay"))||0,t=parseInt(c.getAttribute("up-min-length"))||0;if(clearTimeout(window.searchTimer),f&&p)if(y.length>=t)window.searchTimer=setTimeout(function(){typeof up<"u"&&up.render({url:f,target:p,params:{q:y}})},S);else{let o=document.querySelector(p);o&&(o.innerHTML="")}}});document.addEventListener("focusin",function(s){let c=s.target,l=c.closest&&c.closest(".fcu-dropdown-group");if(l){let f=l.querySelector('input[id^="manyrelated-search-"]');f&&clearTimeout(_[f.id.replace("manyrelated-search-","")])}c.matches('input[id^="manyrelated-search-"]')&&c.dispatchEvent(new Event("input",{bubbles:!0})),c.matches('input[id^="search-input-"]')&&c.dispatchEvent(new Event("input",{bubbles:!0}))});document.addEventListener("keydown",function(s){let c=s.target;if(s.key==="ArrowDown"&&c.matches('input[id^="selected-item-display-"]')){let l=document.getElementById("search-input-"+c.id.replace("selected-item-display-",""));l&&(s.preventDefault(),l.focus())}});document.addEventListener("focusout",function(s){let c=s.target;if(c.matches('input[id^="manyrelated-search-"]')){let l=c.id.replace("manyrelated-search-",""),f=c.closest(".fcu-dropdown-group");_[l]=setTimeout(function(){if(f&&f.contains(document.activeElement))return;let p=document.getElementById("manyrelated-results-"+l);p&&(p.innerHTML=""),M(l,!1)},200)}});})();
@@ -48,7 +48,7 @@
48
48
  {% endif %}
49
49
  {% endfor %}
50
50
  {% else %}
51
- {{ c.placeholder | safe }}
51
+ {{ c.placeholder }}
52
52
  {% endif %}
53
53
  </div>
54
54
  {# No <meta http-equiv="refresh"> fallback: an auto-reloading page with no
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask-construct-ui
3
- Version: 0.11.6
3
+ Version: 0.11.7
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flask-construct-ui"
7
- version = "0.11.6"
7
+ version = "0.11.7"
8
8
  description = "A UI toolkit for Flask with CRUD and component features."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1 +0,0 @@
1
- (()=>{var _={};function F(s,o){return(s==="manyrelated-search"?"manyrelated-results-":"search-results-")+o}function M(s,o){["manyrelated-search-","search-input-"].forEach(function(l){let d=document.getElementById(l+s);d&&d.getAttribute("role")==="combobox"&&(d.setAttribute("aria-expanded",o?"true":"false"),o||d.removeAttribute("aria-activedescendant"))})}function j(s){s&&(s.hasAttribute("tabindex")||(s.setAttribute("tabindex","-1"),s.addEventListener("blur",function o(){s.removeAttribute("tabindex"),s.removeEventListener("blur",o)})),s.focus({preventScroll:!0}))}function J(s,o,l){let d=document.getElementById((o?"manyrelated-status-":"search-status-")+s);if(!d)return;let p=document.getElementById((o?"manyrelated-results-":"search-results-")+s),y=p&&p.querySelector('[aria-disabled="true"]');d.textContent=l>0?String(l):y?y.textContent.trim():""}function P(s){if(!s)return;let o=null;try{o=window.localStorage.getItem("fcu-sidebar-open")}catch{return}o!==null&&(s.open=o==="1")}window.fcuRestoreSidebar=P;if(typeof up<"u"){let s=function(e){return e.getClientRects().length>0},o=function(e){let r=document.getElementById(e)||document.querySelector('[name="'+CSS.escape(e)+'"]');if(!r)return null;if(s(r)&&!r.disabled)return r;let n=r.closest(".fcu-form-group");return n&&Array.from(n.querySelectorAll('input, select, textarea, [contenteditable="true"], button')).find(i=>!i.disabled&&i.type!=="hidden"&&s(i))||null},d=function(e){let r=e.querySelector(".fcu-menu-text");return r?r.textContent.trim():""},p=function(){try{let e=window.sessionStorage.getItem(l),r=e?JSON.parse(e):null;return r&&typeof r=="object"?r:{}}catch{return{}}},y=function(e,r){if(e)try{let n=p();n[e]=r,window.sessionStorage.setItem(l,JSON.stringify(n))}catch{}},t=function(e){try{window.localStorage.setItem(S,e?"1":"0")}catch{}},c=function(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")};up.status&&up.status.config&&up.status.config.currentClasses&&(up.status.config.currentClasses.includes("active")||up.status.config.currentClasses.push("active")),up.feedback&&up.feedback.config&&up.feedback.config.currentClasses&&(up.feedback.config.currentClasses.includes("active")||up.feedback.config.currentClasses.push("active")),up.on("up:fragment:loaded",function(e){let r=e.response;if(!r)return;let n=r.header("X-Construct-Login"),i=r.header("X-Construct-Auth"),u=document.documentElement.getAttribute("data-fcu-auth");!n&&!(i&&u&&i!==u)||(e.preventDefault(),!(e.request&&e.request.background)&&(window.location.href=n||r.url||window.location.href))}),up.on("up:fragment:loaded",function(e){let r=e.response;if(!r||r.header("X-Construct-Login"))return;let n=r.header("X-Construct-Menu");if(!n)return;let i=document.documentElement;if(n===i.getAttribute("data-fcu-menu"))return;let u=document.querySelector("#fcu-main-menu[up-source]");u&&(i.setAttribute("data-fcu-menu",n),up.reload(u,{cache:!1,background:!0}).catch(function(){i.setAttribute("data-fcu-menu","")}))}),up.on("up:fragment:inserted",function(e,r){let n=r||e&&e.target;if(!n||n.id!=="main-content"||n.closest('[up-focus="keep"]'))return;let i=n.querySelector("#form-errors");j(i||n)}),up.on("up:fragment:loaded",function(e){let r=e.response;!r||!e.renderOptions||r.text.indexOf('id="form-errors"')!==-1&&(e.renderOptions.focus="#form-errors",e.renderOptions.failFocus="#form-errors")}),document.addEventListener("click",function(e){let r=e.target.closest&&e.target.closest('#form-errors a[href^="#"]');if(!r)return;let n=o(decodeURIComponent(r.getAttribute("href").slice(1)));n&&(e.preventDefault(),n.focus(),n.scrollIntoView({block:"center"}))},!0),up.on("up:location:changed",function(){setTimeout(()=>{document.querySelectorAll(".fcu-submenu-link.active, .fcu-submenu-link.up-current").forEach(function(e){let r=e.closest("details");r&&(r.open=!0)})},0)});let l="fcu-menu-submenus";up.compiler("#fcu-main-menu",function(e){let r=p();e.querySelectorAll("details.fcu-submenu-details").forEach(function(n){let i=r[d(n)];i!==void 0&&(!i&&n.querySelector(".fcu-submenu-link.active, .fcu-submenu-link.up-current")||(n.open=i))}),e.addEventListener("toggle",function(n){let i=n.target;!i.matches||!i.matches("details.fcu-submenu-details")||y(d(i),i.open)},!0)});let S="fcu-sidebar-open";up.compiler(".fcu-sidebar-disclosure",function(e){P(e),!e.dataset.fcuSidebarBound&&(e.dataset.fcuSidebarBound="true",e.addEventListener("toggle",function(){t(e.open)}))}),up.compiler(".fcu-menu-link, .fcu-submenu-link",function(e){let r=e.getAttribute("href");if(r&&r!=="#"){let n=r.replace(/\/+$/,"");n&&n!=="/"&&e.setAttribute("up-alias",`${n} ${n}/ ${n}/*`)}}),up.compiler("[data-live-sse-url], [data-live-poll-url]",function(e){let r=e.getAttribute("data-live-sse-url"),n=e.getAttribute("data-live-poll-url"),i=parseInt(e.getAttribute("data-live-interval")||"10000",10),u=e.getAttribute("data-live-content-id")||"live-content-"+e.getAttribute("data-live-source"),a=document.getElementById(u),v=e.getAttribute("data-live-format")||"default",b=e.getAttribute("data-live-limit")?parseInt(e.getAttribute("data-live-limit"),10):200;if(!a)return;let E=null,h=null,I=!1,m=!1,x=!1;setTimeout(()=>{m||(I=!0)},2*i);function q(){x||(a.scrollTop=a.scrollHeight)}function L(f){if(m||f&&!I)return;e.classList.toggle("live-stale",f);let g=e.querySelector(".live-stale-indicator");g&&(g.style.display=f?"":"none")}function $(f){if(m)return;let g=f||location.href;typeof up<"u"&&up.visit?(up.cache&&up.cache.expire&&up.cache.expire(),up.visit(g,{cache:!1})):window.location.href=g}function U(){let f=i*.15*(Math.random()*2-1);return Math.max(1e3,i+f)}function T(){m||x||document.hidden||fetch(n).then(f=>m?null:f.status===205?(C(),$(),null):f.text()).then(f=>{if(m||!f)return;let w=new DOMParser().parseFromString(f,"text/html").querySelector(".live-content");w&&(a.innerHTML=w.innerHTML),L(!1)}).catch(()=>{m||L(!0)})}function H(){m||(E=setTimeout(()=>{m||(T(),H())},U()))}function R(){m||document.hidden||T()}function B(){m||E||!n||(document.addEventListener("visibilitychange",R),H())}function C(){E&&(clearTimeout(E),E=null),document.removeEventListener("visibilitychange",R)}r?(h=new EventSource(r),h.addEventListener("update",function(f){if(!(m||x)){if(v==="raw"){a.innerHTML=f.data,q(),L(!1),e.dispatchEvent(new CustomEvent("fcu:live:update",{detail:f.data}));return}try{let g=JSON.parse(f.data);e.dispatchEvent(new CustomEvent("fcu:live:update",{detail:g}));let w=Object.entries(g).map(([O,k])=>O==="line"?`<div style="padding:2px 0;border-bottom:1px solid rgba(0,0,0,0.07)">${c(k)}</div>`:Array.isArray(k)?k.map(X=>`<div style="padding:2px 0;border-bottom:1px solid rgba(0,0,0,0.07)">${c(X)}</div>`).join(""):`<div><strong>${c(O)}</strong>: ${c(k)}</div>`).join("");if(v==="append"){if(a.insertAdjacentHTML("beforeend",w),b>0)for(;a.children.length>b;)a.removeChild(a.firstChild)}else a.innerHTML=w;q(),L(!1)}catch{if(v==="append"){let w=document.createElement("div");if(w.textContent=f.data,a.appendChild(w),b>0)for(;a.children.length>b;)a.removeChild(a.firstChild)}else a.textContent=f.data;q(),e.dispatchEvent(new CustomEvent("fcu:live:update",{detail:f.data}))}}}),h.addEventListener("done",function(f){if(m)return;h.close(),h=null,C();let g={};try{g=JSON.parse(f.data)||{}}catch{}e.dispatchEvent(new CustomEvent("fcu:live:done",{detail:g.redirect})),g.auto||$(g.redirect)}),h.onerror=function(){m||(h.close(),h=null,L(!0),B())}):B();let A=e.querySelector("[data-live-pause]");function D(){x=!x,A.setAttribute("aria-pressed",x?"true":"false");let f=A.querySelector("[data-live-pause-label]");f&&(f.textContent=x?A.dataset.resumeLabel||"Resume updates":A.dataset.pauseLabel||"Pause updates"),x?C():h||(B(),T())}return A&&A.addEventListener("click",D),function(){m=!0,A&&A.removeEventListener("click",D),h&&(h.close(),h=null),C()}})}if(typeof up<"u"){let l=function(t){let c=document.getElementById("manyrelated-results-"+t);c&&(c.innerHTML="");let e=document.getElementById("manyrelated-search-"+t);e&&(e.value="",e.focus())},d=function(t,c,e,r){let n=document.createElement("span");n.style.cssText=`display: inline-flex; align-items: center; gap: 4px; background-color: var(--fcu-surface-alt); border: 1px ${e?"dashed":"solid"} var(--fcu-border); font-size: var(--fcu-font-size-sm); font-weight: 500; padding: 2px 8px; border-radius: var(--fcu-border-radius);`,n.appendChild(document.createTextNode(c));let i=document.createElement("button");i.type="button",i.style.cssText="margin-left: 4px; border: none; background: transparent; padding: 0; cursor: pointer; color: var(--fcu-text-muted); display: inline-flex; align-items: center;";let u=document.getElementById("manyrelated-search-"+t),a=u&&u.parentNode.querySelector("span button");return a?i.innerHTML=a.innerHTML:i.textContent="\xD7",i.addEventListener("click",function(v){v.stopPropagation(),r(n)}),n.appendChild(i),u&&u.parentNode.insertBefore(n,u),n},p=function(t){return t.querySelectorAll("a.fcu-dropdown-item, a.list-group-item")},y=function(t,c,e){if(p(c).forEach(function(r){r.classList.remove("active"),r.setAttribute("aria-selected","false")}),!e){t.removeAttribute("aria-activedescendant");return}e.classList.add("active"),e.setAttribute("aria-selected","true"),e.id&&t.setAttribute("aria-activedescendant",e.id),e.scrollIntoView({block:"nearest"})},S=function(t,c){t.addEventListener("keydown",function(e){let r=t.id.replace(c+"-",""),n=document.getElementById(F(c,r));if(!n)return;if(e.key==="Escape"){e.preventDefault(),n.innerHTML="",y(t,n,null),M(r,!1),c==="search-input"&&t.blur();return}let i=p(n);if(i.length===0)return;let u=n.querySelector("a.fcu-dropdown-item.active, a.list-group-item.active"),a=u?Array.from(i).indexOf(u):-1;if(e.key==="ArrowDown")e.preventDefault(),y(t,n,i[(a+1)%i.length]);else if(e.key==="ArrowUp"){e.preventDefault();let v=a<0?i.length-1:(a-1+i.length)%i.length;y(t,n,i[v])}else e.key==="Enter"&&(e.preventDefault(),u&&u.click())})};up.compiler(".js-hide",function(t){t.querySelectorAll("input, textarea, select").forEach(c=>c.disabled=!0)}),up.compiler(".secret-toggle-btn",function(t){let c=e=>{let r=t.closest(".fcu-form-group");if(!r)return;let n=r.querySelector(".secret-input-container"),i=r.querySelector(".secret-change-checkbox");if(!n||!i)return;let u=n.querySelectorAll("input, textarea, select"),a=b=>{t.title=b;let E=t.querySelector(".fcu-sr-only");E&&(E.textContent=b)},v=(b,E)=>{let h=t.querySelector(".bi");if(h){Array.from(h.classList).filter(m=>m.indexOf("bi-")===0).forEach(m=>h.classList.remove(m)),h.classList.add(b);return}let I=t.querySelector(".material-symbols-outlined");I&&(I.textContent=E)};n.style.display==="none"?(n.style.display="block",u.forEach(b=>b.disabled=!1),i.checked=!0,t.classList.add("active"),t.setAttribute("aria-expanded","true"),v("bi-x-lg","close"),a(t.dataset.cancelLabel||t.title),u.length>0&&u[0].focus()):(n.style.display="none",u.forEach(b=>b.disabled=!0),i.checked=!1,t.classList.remove("active"),t.setAttribute("aria-expanded","false"),v("bi-pencil-fill","edit"),a(t.dataset.changeLabel||t.title))};return t.addEventListener("click",c),()=>t.removeEventListener("click",c)}),up.compiler("[data-fcu-dismiss]",function(t){let c=()=>{let e=t.closest(".fcu-alert");if(!e)return;let r=e.parentElement,n=e.nextElementSibling||e.previousElementSibling;if(up.destroy(e),n&&n.querySelector("[data-fcu-dismiss]"))n.querySelector("[data-fcu-dismiss]").focus();else{let i=document.getElementById("main-content");i?j(i):r&&r.parentElement&&r.parentElement.focus?.()}};return t.addEventListener("click",c),()=>t.removeEventListener("click",c)}),up.compiler("[data-auto-dismiss]",function(t){let c=(e,r)=>{let n=r||e.target;n&&!n.contains(t)&&document.contains(t)&&(up.destroy(t,{transition:"fade-out"}),up.off("up:fragment:inserted",c))};return up.on("up:fragment:inserted",c),()=>up.off("up:fragment:inserted",c)});let s={},o={};window.addItem=function(t){if(s[t]===void 0){let r=document.querySelectorAll(`[name^="${t}-new-"]`).length;s[t]=r}let c=document.getElementById(`${t}-new-item-template`),e=document.getElementById(`${t}-new-items-container`);if(c&&e){let r=c.content.cloneNode(!0),n=document.createElement("div");n.appendChild(r);let i=n.innerHTML.replace(/__INDEX__/g,s[t]);e.insertAdjacentHTML("beforeend",i),s[t]++}},window.selectItem=function(t,c,e){let r=document.getElementById(t);r&&(r.value=c);let n=document.getElementById(`selected-item-display-${t}`);n&&(n.value=e);let i=document.getElementById(`search-results-${t}`);i&&(i.innerHTML="");let u=document.getElementById(`search-input-${t}`);u&&(u.value="")},window.addManyRelatedItem=function(t,c,e){let r=document.getElementById(t+"-hidden-inputs");if(!r)return;if(r.querySelector(`input[value="${c}"]`)){l(t);return}let n=document.createElement("input");n.type="hidden",n.name=t,n.value=c,r.appendChild(n);let i=d(t,e,!1,function(u){n.remove(),u.remove()});i.id=t+"-badge-"+c,l(t)},window.addManyRelatedQuickItem=function(t,c){if(Array.from(document.querySelectorAll(`input[name="${t}-quicknew"]`)).some(function(a){return!a.disabled&&a.value===c})){l(t);return}let r=document.getElementById(t+"-hidden-inputs"),n=r&&r.dataset.pendingLabel||"",i=document.createElement("input");i.type="hidden",i.name=t+"-quicknew",i.value=c;let u=d(t,c,!0,function(a){a.remove()});if(u.insertBefore(i,u.firstChild),n){let a=document.createElement("span");a.style.cssText="color: var(--fcu-text-muted); font-weight: normal;",a.textContent=n,u.insertBefore(a,u.lastChild)}l(t)},window.addManyRelatedNewPanel=function(t){let c=`${t}-newrel-`;if(o[t]===void 0){let n=-1;document.querySelectorAll(`[name^="${c}"]`).forEach(function(i){let u=parseInt(i.name.slice(c.length).split("-")[0],10);!isNaN(u)&&u>n&&(n=u)}),o[t]=n+1}let e=document.getElementById(`${t}-newrel-item-template`),r=document.getElementById(`${t}-newrel-items-container`);if(e&&r){let n=e.content.cloneNode(!0),i=document.createElement("div");i.appendChild(n);let u=i.innerHTML.replace(/__INDEX__/g,o[t]);r.insertAdjacentHTML("beforeend",u),o[t]++}},window.removeManyRelatedItem=function(t,c){let e=document.getElementById(t+"-hidden-inputs");if(e){let n=e.querySelector(`input[value="${c}"]`);n&&n.remove()}let r=document.getElementById(t+"-badge-"+c);r&&r.remove()},up.compiler("[data-js-enable]",function(t){t.disabled=!1}),up.compiler(".js-hide select",function(t){t.disabled=!0}),up.compiler('input[id^="search-input-"]',function(t){S(t,"search-input")}),up.compiler('input[id^="manyrelated-search-"]',function(t){S(t,"manyrelated-search")}),up.compiler('[role="listbox"][id$="-results"], #search-results, [id^="search-results-"], [id^="manyrelated-results-"]',function(t){let c=t.id,e=c.indexOf("manyrelated-results-")===0,r=c.replace(e?"manyrelated-results-":"search-results-","");if(!document.getElementById((e?"manyrelated-search-":"search-input-")+r))return;let i=p(t).length;M(r,i>0),J(r,e,i)})}document.addEventListener("input",function(s){let o=s.target;if(o.matches('input[id^="manyrelated-search-"]')){let l=o.id.replace("manyrelated-search-",""),d=o.getAttribute("up-href"),p=o.value,y=parseInt(o.getAttribute("up-delay"))||0,S=parseInt(o.getAttribute("up-min-length"))||0;if(clearTimeout(window.manyRelatedTimer),d&&p.length>=S)window.manyRelatedTimer=setTimeout(function(){typeof up<"u"&&up.render({url:d,target:"#manyrelated-results-"+l,params:{q:p}})},y);else{let t=document.getElementById("manyrelated-results-"+l);t&&(t.innerHTML="")}return}if(o.matches('input[id^="search-input-"]')){let l=o.id.replace("search-input-",""),d=o.getAttribute("up-href"),p=o.getAttribute("up-target"),y=o.value,S=parseInt(o.getAttribute("up-delay"))||0,t=parseInt(o.getAttribute("up-min-length"))||0;if(clearTimeout(window.searchTimer),d&&p)if(y.length>=t)window.searchTimer=setTimeout(function(){typeof up<"u"&&up.render({url:d,target:p,params:{q:y}})},S);else{let c=document.querySelector(p);c&&(c.innerHTML="")}}});document.addEventListener("focusin",function(s){let o=s.target,l=o.closest&&o.closest(".fcu-dropdown-group");if(l){let d=l.querySelector('input[id^="manyrelated-search-"]');d&&clearTimeout(_[d.id.replace("manyrelated-search-","")])}o.matches('input[id^="manyrelated-search-"]')&&o.dispatchEvent(new Event("input",{bubbles:!0})),o.matches('input[id^="search-input-"]')&&o.dispatchEvent(new Event("input",{bubbles:!0}))});document.addEventListener("keydown",function(s){let o=s.target;if(s.key==="ArrowDown"&&o.matches('input[id^="selected-item-display-"]')){let l=document.getElementById("search-input-"+o.id.replace("selected-item-display-",""));l&&(s.preventDefault(),l.focus())}});document.addEventListener("focusout",function(s){let o=s.target;if(o.matches('input[id^="manyrelated-search-"]')){let l=o.id.replace("manyrelated-search-",""),d=o.closest(".fcu-dropdown-group");_[l]=setTimeout(function(){if(d&&d.contains(document.activeElement))return;let p=document.getElementById("manyrelated-results-"+l);p&&(p.innerHTML=""),M(l,!1)},200)}});})();