cherrypy-foundation 1.0.0__py3-none-any.whl

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 (136) hide show
  1. cherrypy_foundation/__init__.py +0 -0
  2. cherrypy_foundation/components/ColorModes.jinja +70 -0
  3. cherrypy_foundation/components/Datatable.css +47 -0
  4. cherrypy_foundation/components/Datatable.jinja +63 -0
  5. cherrypy_foundation/components/Datatable.js +358 -0
  6. cherrypy_foundation/components/Field.css +10 -0
  7. cherrypy_foundation/components/Field.jinja +66 -0
  8. cherrypy_foundation/components/Field.js +56 -0
  9. cherrypy_foundation/components/Fields.jinja +4 -0
  10. cherrypy_foundation/components/Flash.jinja +13 -0
  11. cherrypy_foundation/components/Icon.jinja +3 -0
  12. cherrypy_foundation/components/LocaleSelection.jinja +13 -0
  13. cherrypy_foundation/components/LocaleSelection.js +26 -0
  14. cherrypy_foundation/components/SideBySideMultiSelect.css +25 -0
  15. cherrypy_foundation/components/SideBySideMultiSelect.jinja +9 -0
  16. cherrypy_foundation/components/SideBySideMultiSelect.js +9 -0
  17. cherrypy_foundation/components/Typeahead.css +55 -0
  18. cherrypy_foundation/components/Typeahead.jinja +106 -0
  19. cherrypy_foundation/components/Typeahead.js +8 -0
  20. cherrypy_foundation/components/__init__.py +51 -0
  21. cherrypy_foundation/components/tests/__init__.py +0 -0
  22. cherrypy_foundation/components/tests/test_static.py +90 -0
  23. cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.css +2106 -0
  24. cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.min.css +5 -0
  25. cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  26. cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  27. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css +9262 -0
  28. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css.map +95 -0
  29. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css +6 -0
  30. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css.map +7 -0
  31. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js +4846 -0
  32. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js.map +1 -0
  33. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js +7 -0
  34. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js.map +7 -0
  35. cherrypy_foundation/components/vendor/bootstrap5/js/color-modes.js +80 -0
  36. cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.css +849 -0
  37. cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.min.css +1 -0
  38. cherrypy_foundation/components/vendor/datatables/images/sort_asc.png +0 -0
  39. cherrypy_foundation/components/vendor/datatables/images/sort_asc_disabled.png +0 -0
  40. cherrypy_foundation/components/vendor/datatables/images/sort_both.png +0 -0
  41. cherrypy_foundation/components/vendor/datatables/images/sort_desc.png +0 -0
  42. cherrypy_foundation/components/vendor/datatables/images/sort_desc_disabled.png +0 -0
  43. cherrypy_foundation/components/vendor/datatables/js/dataTables.js +14073 -0
  44. cherrypy_foundation/components/vendor/datatables/js/dataTables.min.js +4 -0
  45. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.css +556 -0
  46. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.min.css +1 -0
  47. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.js +1700 -0
  48. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.min.js +8 -0
  49. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.js +2944 -0
  50. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.min.js +4 -0
  51. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.css +13 -0
  52. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.min.css +1 -0
  53. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.js +1202 -0
  54. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.min.js +4 -0
  55. cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.js +11577 -0
  56. cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.min.js +13 -0
  57. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.css +194 -0
  58. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.min.css +1 -0
  59. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.js +1861 -0
  60. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.min.js +4 -0
  61. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.js +75023 -0
  62. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.min.js +3 -0
  63. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/vfs_fonts.js +6 -0
  64. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.css +53 -0
  65. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.min.css +1 -0
  66. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.js +485 -0
  67. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.min.js +4 -0
  68. cherrypy_foundation/components/vendor/jquery/jquery.min.js +2 -0
  69. cherrypy_foundation/components/vendor/multi/LICENSE +7 -0
  70. cherrypy_foundation/components/vendor/multi/README.md +109 -0
  71. cherrypy_foundation/components/vendor/multi/multi.css +95 -0
  72. cherrypy_foundation/components/vendor/multi/multi.js +328 -0
  73. cherrypy_foundation/components/vendor/popper/popper.js +1825 -0
  74. cherrypy_foundation/components/vendor/popper/popper.min.js +6 -0
  75. cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css +1 -0
  76. cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js +10 -0
  77. cherrypy_foundation/error_page.py +94 -0
  78. cherrypy_foundation/flash.py +50 -0
  79. cherrypy_foundation/form.py +119 -0
  80. cherrypy_foundation/logging.py +103 -0
  81. cherrypy_foundation/passwd.py +65 -0
  82. cherrypy_foundation/plugins/__init__.py +0 -0
  83. cherrypy_foundation/plugins/db.py +286 -0
  84. cherrypy_foundation/plugins/ldap.py +257 -0
  85. cherrypy_foundation/plugins/restapi.py +74 -0
  86. cherrypy_foundation/plugins/scheduler.py +287 -0
  87. cherrypy_foundation/plugins/smtp.py +223 -0
  88. cherrypy_foundation/plugins/tests/__init__.py +0 -0
  89. cherrypy_foundation/plugins/tests/test_db.py +118 -0
  90. cherrypy_foundation/plugins/tests/test_ldap.py +451 -0
  91. cherrypy_foundation/plugins/tests/test_scheduler.py +100 -0
  92. cherrypy_foundation/plugins/tests/test_scheduler_db.py +107 -0
  93. cherrypy_foundation/plugins/tests/test_smtp.py +140 -0
  94. cherrypy_foundation/sessions.py +93 -0
  95. cherrypy_foundation/tests/__init__.py +72 -0
  96. cherrypy_foundation/tests/templates/test_flash.html +9 -0
  97. cherrypy_foundation/tests/templates/test_form.html +16 -0
  98. cherrypy_foundation/tests/templates/test_url.html +15 -0
  99. cherrypy_foundation/tests/test_error_page.py +78 -0
  100. cherrypy_foundation/tests/test_flash.py +61 -0
  101. cherrypy_foundation/tests/test_form.py +148 -0
  102. cherrypy_foundation/tests/test_logging.py +78 -0
  103. cherrypy_foundation/tests/test_passwd.py +51 -0
  104. cherrypy_foundation/tests/test_sessions.py +89 -0
  105. cherrypy_foundation/tests/test_url.py +161 -0
  106. cherrypy_foundation/tools/__init__.py +0 -0
  107. cherrypy_foundation/tools/auth.py +263 -0
  108. cherrypy_foundation/tools/auth_mfa.py +249 -0
  109. cherrypy_foundation/tools/i18n.py +529 -0
  110. cherrypy_foundation/tools/jinja2.py +158 -0
  111. cherrypy_foundation/tools/ratelimit.py +265 -0
  112. cherrypy_foundation/tools/secure_headers.py +119 -0
  113. cherrypy_foundation/tools/sessions_timeout.py +167 -0
  114. cherrypy_foundation/tools/tests/__init__.py +0 -0
  115. cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
  116. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  117. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po +15 -0
  118. cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo +0 -0
  119. cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po +15 -0
  120. cherrypy_foundation/tools/tests/locales/messages.pot +2 -0
  121. cherrypy_foundation/tools/tests/templates/test_jinja2.html +11 -0
  122. cherrypy_foundation/tools/tests/templates/test_jinjax.html +9 -0
  123. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +22 -0
  124. cherrypy_foundation/tools/tests/test_auth.py +110 -0
  125. cherrypy_foundation/tools/tests/test_auth_mfa.py +369 -0
  126. cherrypy_foundation/tools/tests/test_i18n.py +247 -0
  127. cherrypy_foundation/tools/tests/test_jinja2.py +153 -0
  128. cherrypy_foundation/tools/tests/test_ratelimit.py +109 -0
  129. cherrypy_foundation/tools/tests/test_secure_headers.py +200 -0
  130. cherrypy_foundation/url.py +66 -0
  131. cherrypy_foundation/widgets.py +48 -0
  132. cherrypy_foundation-1.0.0.dist-info/METADATA +71 -0
  133. cherrypy_foundation-1.0.0.dist-info/RECORD +136 -0
  134. cherrypy_foundation-1.0.0.dist-info/WHEEL +5 -0
  135. cherrypy_foundation-1.0.0.dist-info/licenses/LICENSE.md +674 -0
  136. cherrypy_foundation-1.0.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,70 @@
1
+ {# def header="Toggle theme", light_label="Light", dark_label="Dark", auto_label="Auto" #}
2
+ {#css vendor/bootstrap5/css/bootstrap.min.css #}
3
+ {#js vendor/popper/popper.min.js, vendor/bootstrap5/js/bootstrap.min.js, vendor/bootstrap5/js/color-modes.js #}
4
+ <svg class="d-none" xmlns="http://www.w3.org/2000/svg">
5
+ <symbol id="check2" viewbox="0 0 16 16">
6
+ <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z">
7
+ </path>
8
+ </symbol>
9
+ <symbol id="circle-half" viewbox="0 0 16 16">
10
+ <path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"></path>
11
+ </symbol>
12
+ <symbol id="moon-stars-fill" viewbox="0 0 16 16">
13
+ <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z">
14
+ </path>
15
+ <path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z">
16
+ </path>
17
+ </symbol>
18
+ <symbol id="sun-fill" viewbox="0 0 16 16">
19
+ <path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z">
20
+ </path>
21
+ </symbol>
22
+ </svg>
23
+ <li>
24
+ <span id="bd-theme" class="dropdown-item disabled">
25
+ <span id="bd-theme-text">{{ header }}</span>
26
+ <svg aria-hidden="true"
27
+ class="theme-icon-active visually-hidden"
28
+ width="16"
29
+ height="16">
30
+ <use href="#circle-half">
31
+ </use>
32
+ </svg>
33
+ </span>
34
+ </li>
35
+ <li>
36
+ <button aria-pressed="false"
37
+ class="dropdown-item"
38
+ data-bs-theme-value="light"
39
+ type="button">
40
+ <svg aria-hidden="true" class="me-2" width="16" height="16">
41
+ <use href="#sun-fill">
42
+ </use>
43
+ </svg>
44
+ {{ light_label }}
45
+ </button>
46
+ </li>
47
+ <li>
48
+ <button aria-pressed="false"
49
+ class="dropdown-item"
50
+ data-bs-theme-value="dark"
51
+ type="button">
52
+ <svg aria-hidden="true" class="me-2" width="16" height="16">
53
+ <use href="#moon-stars-fill">
54
+ </use>
55
+ </svg>
56
+ {{ dark_label }}
57
+ </button>
58
+ </li>
59
+ <li>
60
+ <button aria-pressed="true"
61
+ class="dropdown-item"
62
+ data-bs-theme-value="auto"
63
+ type="button">
64
+ <svg aria-hidden="true" class="me-2 " width="16" height="16">
65
+ <use href="#circle-half">
66
+ </use>
67
+ </svg>
68
+ {{ auto_label }}
69
+ </button>
70
+ </li>
@@ -0,0 +1,47 @@
1
+ /* Remove default margin from layout */
2
+ div.dt-container div.dt-layout-row {
3
+ margin: 0;
4
+ }
5
+
6
+ /* Remove default seperator */
7
+ div.dt-container.dt-empty-footer .dt-scroll-body {
8
+ border-bottom: 0;
9
+ }
10
+
11
+ /* Show hide Action button */
12
+ .dt-container tr .btn-hover {
13
+ opacity: 0;
14
+ }
15
+
16
+ .dt-container tr:hover .btn-hover {
17
+ opacity: 1;
18
+ }
19
+
20
+ /*
21
+ * Hide responsive column "none" during loading.
22
+ */
23
+ .dt-container th.none {
24
+ display: none;
25
+ }
26
+
27
+ /* Fix background color of button collection in DarkMode */
28
+ div.dt-button-collection {
29
+ background-color: var(--bs-body-bg);
30
+ color: var(--bs-body-color);
31
+ border-color: var(--bs-border-color-translucent);
32
+ }
33
+
34
+ /* Css for responsive details render tableHidden */
35
+ table.dtr-details .dtr-title-cell {
36
+ width: 15%;
37
+ white-space: nowrap;
38
+ vertical-align: top;
39
+ padding: 8px 10px;
40
+ }
41
+
42
+ table.dtr-details .dtr-data-cell {
43
+ width: 85%;
44
+ white-space: normal;
45
+ word-break: break-word;
46
+ padding: 8px 10px;
47
+ }
@@ -0,0 +1,63 @@
1
+ {# def data, columns=[], order=[], empty_message=None, info_message=None, searching=True, buttons=[], paging=True, page_length=25, length_change=True, buttons_cfg={}, layout={}, server_side=False, state_save=True, auto_width=False, responsive=False, rowgroup=False, fixed_header=False, scroll_y=False #}
2
+ {#css vendor/bootstrap5/css/bootstrap.min.css, vendor/datatables/css/dataTables.dataTables.css, vendor/datatables-extensions/Buttons/css/buttons.dataTables.min.css, vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.css, vendor/datatables-extensions/Responsive/css/responsive.dataTables.min.css, vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.min.css #}
3
+ {#js vendor/jquery/jquery.min.js, vendor/popper/popper.min.js, vendor/bootstrap5/js/bootstrap.min.js, vendor/datatables-extensions/JSZip/jszip.min.js, vendor/datatables-extensions/pdfmake/build/pdfmake.min.js, vendor/datatables-extensions/pdfmake/build/vfs_fonts.js, vendor/datatables/js/dataTables.min.js, vendor/datatables-extensions/Responsive/js/dataTables.responsive.min.js, vendor/datatables-extensions/Buttons/js/dataTables.buttons.min.js, vendor/datatables-extensions/Buttons/js/buttons.html5.min.js, vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.min.js, vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.js #}
4
+ {% set classes = {
5
+ "sPaging": "d-flex justify-content-center",
6
+ "sPageButton": "btn btn-outline-primary ms-1 me-1",
7
+ "sPageButtonActive": "active"
8
+ } %}
9
+ {% set language = {
10
+ "url": url_for('language', _('en')),
11
+ "info": info_message or (_(' from _START_ to _END_ of _TOTAL_ total') if paging else _('Showing total of _TOTAL_')),
12
+ "emptyTable": empty_message or _('List is empty'),
13
+ } %}
14
+ {# Use jinja recursive loop to merge buttons config. #}
15
+ {% set buttons_cfg_default = {
16
+ "dom": {
17
+ "button": {
18
+ "className": "btn btn-sm ms-1 mb-2 mb-sm-0",
19
+ "active": "active"
20
+ },
21
+ "collection": {
22
+ "tag": "div",
23
+ "button": {
24
+ "tag": "a",
25
+ "className": "btn btn-sm btn-link mb-2 mb-sm-0 w-100 text-start",
26
+ "active": "active",
27
+ "disabled": "disabled"
28
+ }
29
+ }
30
+ }
31
+ } %}
32
+ {% set ns = namespace(buttons_cfg = dict(buttons_cfg_default), stack_value=[]) %}
33
+ {% set ns.stack_value = [ns.buttons_cfg] %}
34
+ {% for key, value in buttons_cfg.items() recursive %}
35
+ {% if value is mapping %}
36
+ {% do ns.stack_value.append( ns.stack_value[-1].get(key, {}) ) %}
37
+ {{ loop(value.items() ) }}
38
+ {% set value = ns.stack_value.pop() %}
39
+ {% endif %}
40
+ {% do ns.stack_value[-1].__setitem__(key, value) %}
41
+ {% endfor %}
42
+ {% do ns.buttons_cfg.__setitem__('buttons', buttons) %}
43
+ <table {{ attrs.render(class="table table-hover",
44
+ width="100%",
45
+ data_ajax=data,
46
+ data_auto_width=auto_width | tojson,
47
+ data_buttons=ns.buttons_cfg | tojson,
48
+ data_classes=classes | tojson,
49
+ data_columns=columns | tojson,
50
+ data_layout=layout | tojson,
51
+ data_fixed_header=fixed_header | tojson,
52
+ data_language=language | tojson,
53
+ data_length_change=length_change | tojson,
54
+ data_order=order | tojson | e,
55
+ data_page_length=page_length | tojson,
56
+ data_paging=paging | tojson,
57
+ data_responsive=responsive | tojson,
58
+ data_row_group=rowgroup | tojson,
59
+ data_searching=searching | tojson,
60
+ data_server_side=server_side | tojson,
61
+ data_state_save=state_save | tojson,
62
+ data_scroll_y=scroll_y) }}>
63
+ </table>
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Cherrypy-foundation
3
+ * Copyright (C) 2026 IKUS Software
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU General Public License
16
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ /* Throw a JavaScript error. */
20
+ $.fn.dataTable.ext.errMode = 'throw';
21
+
22
+ $.fn.dataTable.rowGroupRender = $.fn.dataTable.rowGroupRender || {};
23
+
24
+ /**
25
+ * Buttons to filter content of datatable.
26
+ *
27
+ * Options:
28
+ * - search: Define the search criteria when filter is active
29
+ * - search_off: Define the search criteria when filter is not active (optional)
30
+ * - regex: True to enable regex lookup (optional)
31
+ * - multi: True to enablemultiple selection for the same column.
32
+ */
33
+ $.fn.dataTable.ext.buttons.filter = {
34
+ init: function(dt, node, config) {
35
+ if (config.search_off && config.multi) {
36
+ console.error('search_off and multi are not supported together');
37
+ }
38
+ const that = this;
39
+ dt.on('search.dt', function() {
40
+ let activate;
41
+ const curSearch = dt.column(config.column).search();
42
+ if (config.multi) {
43
+ const terms = curSearch.replace(/^\(/, '').replace(/\)$/, '').split('|');
44
+ activate = terms.includes(config.search);
45
+ } else {
46
+ activate = dt.column(config.column).search() === config.search;
47
+ }
48
+ that.active(activate);
49
+ });
50
+ },
51
+ action: function(e, dt, node, config) {
52
+ const curSearch = dt.column(config.column).search();
53
+ let terms = curSearch.replace(/^\(/, '').replace(/\)$/, '').split('|').filter(item => item !== '');
54
+ if (node.hasClass('active')) {
55
+ if (config.search_off) {
56
+ // Disable - replace by our search_off pattern
57
+ terms = [config.search_off];
58
+ } else {
59
+ // Disable - remove from term.
60
+ terms = terms.filter(item => item != config.search)
61
+ }
62
+ } else if (config.multi) {
63
+ // Enable - add new terms
64
+ terms.push(config.search)
65
+ } else {
66
+ // Enable - replace all terms
67
+ terms = [config.search];
68
+ }
69
+ let search;
70
+ if (terms.length == 0) {
71
+ search = '';
72
+ } else if (terms.length == 1) {
73
+ search = terms[0];
74
+ } else {
75
+ search = '(' + terms.join('|') + ')';
76
+ }
77
+ dt.column(config.column).search(search, true);
78
+ dt.draw(true);
79
+ }
80
+ };
81
+ $.fn.dataTable.ext.buttons.btnfilter = {
82
+ extend: 'filter',
83
+ className: 'cdt-btn-filter'
84
+ };
85
+ $.fn.dataTable.ext.buttons.collectionfilter = {
86
+ align: 'button-right',
87
+ autoClose: true,
88
+ background: false,
89
+ extend: 'collection',
90
+ className: 'cdt-btn-collectionfilter',
91
+ init: function(dt, node, config) {
92
+ const that = this;
93
+ dt.on('search.dt', function() {
94
+ const activate = dt.column(config.column).search() !== '';
95
+ that.active(activate);
96
+ });
97
+ },
98
+ };
99
+ /**
100
+ * Button to reset the filters of datatable.
101
+ * Default settings are restored using init() API.
102
+ */
103
+ $.fn.dataTable.ext.buttons.reset = {
104
+ text: 'Reset',
105
+ action: function(e, dt, node, config) {
106
+ dt.search('');
107
+ if (dt.init().aoSearchCols) {
108
+ const searchCols = dt.init().aoSearchCols;
109
+ for (let i = 0; i < searchCols.length; i++) {
110
+ const search = searchCols[i].search || "";
111
+ dt.column(i).search(search);
112
+ }
113
+ } else {
114
+ dt.columns().search('');
115
+ }
116
+ dt.draw(true);
117
+ }
118
+ };
119
+ /**
120
+ * Default render
121
+ */
122
+ $.fn.dataTable.render.button = function ({
123
+ label = 'changeme',
124
+ className = 'btn btn-sm btn-primary btn-hover text-nowrap',
125
+ ...attrs
126
+ } = {}) {
127
+ const { escapeHtml } = DataTable.util;
128
+
129
+ const attr = (name, value) => {
130
+ if (value == null || value === false) return '';
131
+ if (value === true) return ` ${name}`;
132
+ return ` ${name}="${escapeHtml(String(value))}"`;
133
+ };
134
+
135
+ return {
136
+ display: function (data, type, row, meta) {
137
+ if (!data) return '';
138
+
139
+ const href = encodeURI(String(data));
140
+
141
+ // If caller supplies `class`, prefer it over className
142
+ const { class: clsFromAttrs, ...rest } = attrs;
143
+ const classValue = clsFromAttrs ?? className;
144
+
145
+ const known = attr('class', classValue);
146
+
147
+ const extra = Object.entries(rest)
148
+ .map(([k, v]) => attr(k, v))
149
+ .join('');
150
+
151
+ return `<a${known}${extra} href="${escapeHtml(href)}">${escapeHtml(label)}</a>`;
152
+ },
153
+ };
154
+ };
155
+ $.fn.dataTable.render.choices = function(choices) {
156
+ let lookup = null;
157
+ if (Array.isArray(choices)) {
158
+ // Convert array of tuples to a lookup object
159
+ lookup = Object.fromEntries(choices);
160
+ } else if (typeof choices === "object" && choices !== null) {
161
+ // Already a dictionary
162
+ lookup = choices;
163
+ }
164
+ return {
165
+ display: function(data, type, row, meta) {
166
+ return (lookup && data in lookup) ? lookup[data] : data;
167
+ },
168
+ };
169
+ }
170
+
171
+ // Build a child-row table showing only the hidden columns
172
+ $.fn.dataTable.Responsive.renderer.tableHidden = function (options) {
173
+ options = $.extend(
174
+ {
175
+ tableClass: '',
176
+ empty: '—' // placeholder when a hidden cell is empty
177
+ },
178
+ options
179
+ );
180
+
181
+ return function (api, rowIdx, columns) {
182
+ const data = $.map(columns, function (col) {
183
+ if (!col.hidden) {
184
+ return '';
185
+ }
186
+
187
+ const klass = col.className
188
+ ? 'class="' + col.className + '"'
189
+ : '';
190
+
191
+ const title =
192
+ '' !== col.title
193
+ ? col.title + ':'
194
+ : '';
195
+
196
+ // Treat null/undefined/empty-string as empty
197
+ const cell =
198
+ col.data !== null &&
199
+ col.data !== undefined &&
200
+ col.data !== ''
201
+ ? col.data
202
+ : options.empty;
203
+
204
+ return (
205
+ '<tr ' +
206
+ klass +
207
+ ' data-dt-row="' +
208
+ col.rowIndex +
209
+ '" data-dt-column="' +
210
+ col.columnIndex +
211
+ '">' +
212
+ '<th class="dtr-title-cell">' +
213
+ title +
214
+ '</th> ' +
215
+ '<td class="dtr-data-cell">' +
216
+ cell +
217
+ '</td>' +
218
+ '</tr>'
219
+ );
220
+ }).join('');
221
+
222
+ // If there are no hidden columns, return false so no child row is shown
223
+ return data
224
+ ? $(
225
+ '<table class="' +
226
+ options.tableClass +
227
+ ' dtr-details" width="100%"/>'
228
+ ).append(data)
229
+ : false;
230
+ };
231
+ }
232
+
233
+ // Resolve one spec (render/startRender/endRender) into a callable
234
+ function resolveRenderSpec(source, key) {
235
+ if (!source || !key || source[key] == null) return;
236
+
237
+ const value = source[key];
238
+ let fn;
239
+
240
+ if (typeof value === 'function') {
241
+ // Case A: already a function — use as-is
242
+ fn = value;
243
+ } else {
244
+ // Case B: value is a string naming a render factory, e.g. 'number', 'text', 'ellipsis', 'myPlugin'
245
+ const factoryName = value;
246
+ let renderNS;
247
+ if(key == 'render') {
248
+ renderNS = $.fn.dataTable?.render;
249
+ } else if(key == 'startRender' || key == 'endRender') {
250
+ renderNS = $.fn.dataTable?.rowGroupRender;
251
+ } else if(key == 'renderer') {
252
+ renderNS = $.fn.dataTable?.Responsive?.renderer;
253
+ }
254
+
255
+ const factory = renderNS?.[factoryName];
256
+ if (typeof factory !== 'function') {
257
+ console.warn(`DataTables render factory '${factoryName}' not found`);
258
+ return;
259
+ }
260
+
261
+ // Support kwargs | args | arg
262
+ if (source[`${key}_kwargs`]) {
263
+ fn = factory({...source[`${key}_kwargs`]});
264
+ } else if (source[`${key}_args`]) {
265
+ fn = factory(...source[`${key}_args`]);
266
+ } else if (Object.hasOwn(source, `${key}_arg`)) {
267
+ fn = factory(source[`${key}_arg`]);
268
+ } else {
269
+ fn = factory();
270
+ }
271
+ }
272
+
273
+ return fn;
274
+ }
275
+
276
+ jQuery(function() {
277
+ $('table[data-ajax]').each(function(_idx) {
278
+ /* Load column properties */
279
+ let columns = $(this).attr('data-columns');
280
+ $(this).removeAttr('data-columns');
281
+ columns = JSON.parse(columns);
282
+ $.each(columns, function(_index, item) {
283
+ item.render = resolveRenderSpec(item, 'render');
284
+ });
285
+
286
+ /* Process rowGroup render */
287
+ let rowGroup = $(this).attr('data-row-group');
288
+ $(this).removeAttr('data-row-group');
289
+ rowGroup = JSON.parse(rowGroup);
290
+ if(rowGroup && typeof rowGroup === 'object') {
291
+ rowGroup.startRender = resolveRenderSpec(rowGroup, 'startRender');
292
+ rowGroup.endRender = resolveRenderSpec(rowGroup, 'endRender');
293
+ }
294
+
295
+ /* Process responsive details render */
296
+ let responsive = $(this).attr('data-responsive');
297
+ $(this).removeAttr('data-responsive');
298
+ responsive = JSON.parse(responsive);
299
+ if(responsive && typeof responsive === 'object') {
300
+ responsive.renderer = resolveRenderSpec(responsive, 'renderer');
301
+ }
302
+
303
+ let searchCols = columns.map(function(item, _index) {
304
+ if (item.search !== undefined) {
305
+ return {
306
+ "search": item.search,
307
+ "regex": item.regex || false
308
+ };
309
+ }
310
+ return null;
311
+ });
312
+ let dt = $(this).DataTable({
313
+ columns: columns,
314
+ rowGroup: rowGroup,
315
+ responsive: responsive,
316
+ searchCols: searchCols,
317
+ drawCallback: function(_settings) {
318
+ // This callback show or hide the pagination when required
319
+ if (_settings.aanFeatures.p) {
320
+ if (_settings._iDisplayLength > _settings.fnRecordsDisplay()) {
321
+ $(_settings.aanFeatures.p[0]).parent().hide();
322
+ } else {
323
+ $(_settings.aanFeatures.p[0]).parent().show();
324
+ }
325
+ }
326
+ // This callback is responsible to add and remove 'sorting-x-x' class
327
+ // to allow CSS customization of the table based on the sorted column
328
+ this.removeClass(function(_index, className) {
329
+ return className.split(/\s+/).filter(function(c) {
330
+ return c.startsWith('sorted-');
331
+ }).join(' ');
332
+ });
333
+ // Add sorting class when sorting without filter
334
+ if (this.api().order() && this.api().order()[0] && this.api().order()[0][0] >= 0 && this.api().search() === '') {
335
+ const colIdx = this.api().order()[0][0];
336
+ const direction = this.api().order()[0][1]
337
+ this.addClass('sorted-' + colIdx + '-' + direction);
338
+ const colName = _settings.aoColumns[colIdx].name;
339
+ if (colName) {
340
+ this.addClass('sorted-' + colName + '-' + direction);
341
+ }
342
+ }
343
+ },
344
+ initComplete: function() {
345
+ // Remove no-footer class to fix CSS display with bootstrap5
346
+ $(this).removeClass("no-footer");
347
+ // If searching is enabled, focus on search field.
348
+ $("div.dataTables_filter input").focus();
349
+ // Trigger responsive recalculation on window resize
350
+ $(window).on('resize', function() {
351
+ dt.columns.adjust().responsive.recalc();
352
+ });
353
+ },
354
+ processing: true,
355
+ deferRender: true,
356
+ });
357
+ });
358
+ });
@@ -0,0 +1,10 @@
1
+ /* Adjust style of form check box */
2
+ ul.form-list-widget {
3
+ list-style: none;
4
+ }
5
+
6
+ /* Change color of read-only fields */
7
+ input[type="text"][readonly], select[readonly] {
8
+ background-color: var(--bs-tertiary-bg);
9
+ cursor: not-allowed;
10
+ }
@@ -0,0 +1,66 @@
1
+ {# def field, floating=False #}
2
+ {#css vendor/bootstrap5/css/bootstrap.min.css #}
3
+ {#js vendor/jquery/jquery.min.js, vendor/popper/popper.min.js, vendor/bootstrap5/js/bootstrap.min.js #}
4
+ {# djlint:off #}
5
+ {% set bootstrap_class_table = {
6
+ "CheckboxInput": ("form-check-input", "form-check-label", False, True),
7
+ "EmailInput": ("form-control", "form-label", True, False),
8
+ "NumberInput": ("form-control", "form-label", True, False),
9
+ "PasswordInput": ("form-control", "form-label", True, False),
10
+ "Select": ("form-select", "form-label", True, False),
11
+ "SubmitInput": ("btn", "d-none", False, False),
12
+ "TextArea": ("form-control", "form-label", True, False),
13
+ "TextInput": ("form-control", "form-label", True, False),
14
+ "RadioInput": ("form-check", "form-label", False, False),
15
+ "ListWidget": ("form-list-widget", "form-label", False, False)
16
+ } %}
17
+ {# djlint:on #}
18
+ {# Add proper class if field is invalid. #}
19
+ {% if field.errors %}
20
+ {% do attrs.add_class('is-invalid') %}
21
+ {% endif %}
22
+ {# Also include render_kw if any #}
23
+ {% if field.render_kw %}
24
+ {% do attrs.set(**field.render_kw) %}
25
+ {% endif %}
26
+ {% if field.widget.input_type is defined and field.widget.input_type == 'hidden' %}
27
+ {{ field.widget(field, **attrs.as_dict) }}
28
+ {% else %}
29
+ {# For each widget list supported options. #}
30
+ {# Assign propert bootstrap class based on wtform widget #}
31
+ {% set input_class, label_class, floating_supported, label_always_last = bootstrap_class_table.get(field.widget.__class__.__name__) or ('', 'form-label', False, False) %}
32
+ {% set label_last = (floating and floating_supported) or label_always_last %}
33
+ {% do attrs.add_class(input_class) %}
34
+ {# Used for input-group #}
35
+ {% set prepend = attrs.get('prepend', None) %}
36
+ {% set append = attrs.get('append', None) %}
37
+ {% set input_group = prepend or append %}
38
+ {# Build container & label specific attributes #}
39
+ {% set container_attrs = attrs.__class__({'class': 'mb-2 form-field'}) %}
40
+ {% set label_attrs = attrs.__class__({'class': label_class}) %}
41
+ {% for key, value in attrs.as_dict.items() %}
42
+ {% if key.startswith('container-') or key.startswith('container_') %}
43
+ {% do container_attrs.set(**{key[10:]: value|string}) %}
44
+ {% endif %}
45
+ {% if key.startswith('label-') or key.startswith('label_') %}
46
+ {% do label_attrs.set(**{key[6:]: value|string}) %}
47
+ {% endif %}
48
+ {% endfor %}
49
+ <div {{ container_attrs.as_dict | xmlattr }}>
50
+ {% if floating and floating_supported %}<div class="form-floating">{% endif %}
51
+ {% if not label_last %}{{ field.label(**label_attrs.as_dict) }}{% endif %}
52
+ {% if input_group %}
53
+ <div class="input-group">
54
+ {% if prepend %}<span class="input-group-text">{{ prepend }}</span>{% endif %}
55
+ {% endif %}
56
+ {{ field.widget(field, **attrs.as_dict) }}
57
+ {% if input_group %}
58
+ {% if append %}<span class="input-group-text">{{ append }}</span>{% endif %}
59
+ </div>
60
+ {% endif %}
61
+ {% if label_last %}{{ field.label(**label_attrs.as_dict) }}{% endif %}
62
+ {% for error in field.errors %}<div class="invalid-feedback">{{ error }}</div>{% endfor %}
63
+ {% if field.description %}<div class="form-text small test-secondary">{{ field.description }}</div>{% endif %}
64
+ {% if floating and floating_supported %}</div>{% endif %}
65
+ </div>
66
+ {% endif %}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Control showif
3
+ */
4
+ jQuery(function() {
5
+ function escape(v) {
6
+ return v.replace(/(:|\.|\[|\]|,|=)/g, "\\$1");
7
+ }
8
+ $('[data-showif-field]').each(function() {
9
+ const elem = $(this);
10
+ const field = $(this).data('showif-field');
11
+ const operator = $(this).data('showif-operator');
12
+ const value = $(this).data('showif-value');
13
+ // Lookup field
14
+ if (!field) {
15
+ return;
16
+ }
17
+ const fieldElem = $("[name='" + escape(field) + "']");
18
+ if (fieldElem.length > 0) {
19
+ function updateShowIf() {
20
+ const curValue = fieldElem.val();
21
+ let visible = false;
22
+ if (operator == 'eq') {
23
+ visible = curValue == value;
24
+ } else if (operator == 'ne') {
25
+ visible = curValue != value;
26
+ } else if (operator == 'in' && Array.isArray(value)) {
27
+ visible = $.inArray(curValue, value) >= 0;
28
+ }
29
+ // To handle the initial state, manually add the collapse class before creating the collapsable class.
30
+ const parent = elem.closest('.form-field');
31
+ if (!parent.hasClass('collapse')) {
32
+ parent.addClass('collapse');
33
+ if (visible) {
34
+ parent.addClass('show');
35
+ }
36
+ }
37
+ // Update widget visibility accordingly.
38
+ let collapsible = bootstrap.Collapse.getOrCreateInstance(parent, {
39
+ toggle: false
40
+ });
41
+ if (visible) {
42
+ collapsible.show();
43
+ elem.removeAttr('disabled');
44
+ } else {
45
+ collapsible.hide();
46
+ elem.attr('disabled', '1');
47
+ }
48
+ }
49
+ // Attach event to field.
50
+ fieldElem.change(function() {
51
+ updateShowIf();
52
+ })
53
+ updateShowIf();
54
+ }
55
+ });
56
+ });
@@ -0,0 +1,4 @@
1
+ {# def form, floating=False #}
2
+ <div class="row">
3
+ {% for id, field in form._fields.items() %}<Field field={{ field }} floating={{ floating }} />{% endfor %}
4
+ </div>