accrete 0.0.99__py3-none-any.whl → 0.0.109__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 (254) hide show
  1. accrete/contrib/ui/__init__.py +6 -27
  2. accrete/contrib/ui/context.py +26 -294
  3. accrete/contrib/ui/filter.py +309 -280
  4. accrete/contrib/ui/middleware.py +44 -0
  5. accrete/contrib/ui/models.py +3 -0
  6. accrete/contrib/ui/static/bulma/README.md +4 -2
  7. accrete/contrib/ui/static/bulma/bulma.css +21551 -0
  8. accrete/contrib/ui/static/bulma/bulma.css.map +1 -0
  9. accrete/contrib/ui/static/bulma/bulma.scss +1 -1
  10. accrete/contrib/ui/static/bulma/css/bulma.css +1988 -2874
  11. accrete/contrib/ui/static/bulma/css/bulma.css.map +1 -1
  12. accrete/contrib/ui/static/bulma/css/bulma.min.css +2 -2
  13. accrete/contrib/ui/static/bulma/css/versions/bulma-no-dark-mode.css +19648 -0
  14. accrete/contrib/ui/static/bulma/css/versions/bulma-no-dark-mode.css.map +1 -0
  15. accrete/contrib/ui/static/bulma/css/versions/bulma-no-dark-mode.min.css +2 -2
  16. accrete/contrib/ui/static/bulma/css/versions/bulma-no-helpers-prefixed.css +11136 -0
  17. accrete/contrib/ui/static/bulma/css/versions/bulma-no-helpers-prefixed.css.map +1 -0
  18. accrete/contrib/ui/static/bulma/css/versions/bulma-no-helpers-prefixed.min.css +2 -2
  19. accrete/contrib/ui/static/bulma/css/versions/bulma-no-helpers.css +11136 -0
  20. accrete/contrib/ui/static/bulma/css/versions/bulma-no-helpers.css.map +1 -0
  21. accrete/contrib/ui/static/bulma/css/versions/bulma-no-helpers.min.css +2 -2
  22. accrete/contrib/ui/static/bulma/css/versions/bulma-prefixed.min.css +21550 -2
  23. accrete/contrib/ui/static/bulma/css/versions/bulma-prefixed.min.css.map +1 -1
  24. accrete/contrib/ui/static/bulma/css/versions/bulma-prefixed.min.min.css +3 -0
  25. accrete/contrib/ui/static/bulma/package.json +12 -11
  26. accrete/contrib/ui/static/bulma/sass/base/animations.css +15 -0
  27. accrete/contrib/ui/static/bulma/sass/base/animations.css.map +1 -0
  28. accrete/contrib/ui/static/bulma/sass/base/generic.css +196 -0
  29. accrete/contrib/ui/static/bulma/sass/base/generic.css.map +1 -0
  30. accrete/contrib/ui/static/bulma/sass/base/minireset.css +82 -0
  31. accrete/contrib/ui/static/bulma/sass/base/minireset.css.map +1 -0
  32. accrete/contrib/ui/static/bulma/sass/base/skeleton.css +113 -0
  33. accrete/contrib/ui/static/bulma/sass/base/skeleton.css.map +1 -0
  34. accrete/contrib/ui/static/bulma/sass/base/skeleton.scss +0 -12
  35. accrete/contrib/ui/static/bulma/sass/components/breadcrumb.css +108 -0
  36. accrete/contrib/ui/static/bulma/sass/components/breadcrumb.css.map +1 -0
  37. accrete/contrib/ui/static/bulma/sass/components/card.css +130 -0
  38. accrete/contrib/ui/static/bulma/sass/components/card.css.map +1 -0
  39. accrete/contrib/ui/static/bulma/sass/components/dropdown.css +119 -0
  40. accrete/contrib/ui/static/bulma/sass/components/dropdown.css.map +1 -0
  41. accrete/contrib/ui/static/bulma/sass/components/menu.css +119 -0
  42. accrete/contrib/ui/static/bulma/sass/components/menu.css.map +1 -0
  43. accrete/contrib/ui/static/bulma/sass/components/message.css +191 -0
  44. accrete/contrib/ui/static/bulma/sass/components/message.css.map +1 -0
  45. accrete/contrib/ui/static/bulma/sass/components/modal.css +194 -0
  46. accrete/contrib/ui/static/bulma/sass/components/modal.css.map +1 -0
  47. accrete/contrib/ui/static/bulma/sass/components/navbar.css +768 -0
  48. accrete/contrib/ui/static/bulma/sass/components/navbar.css.map +1 -0
  49. accrete/contrib/ui/static/bulma/sass/components/navbar.scss +41 -30
  50. accrete/contrib/ui/static/bulma/sass/components/pagination.css +302 -0
  51. accrete/contrib/ui/static/bulma/sass/components/pagination.css.map +1 -0
  52. accrete/contrib/ui/static/bulma/sass/components/panel.css +224 -0
  53. accrete/contrib/ui/static/bulma/sass/components/panel.css.map +1 -0
  54. accrete/contrib/ui/static/bulma/sass/components/panel.scss +2 -2
  55. accrete/contrib/ui/static/bulma/sass/components/tabs.css +192 -0
  56. accrete/contrib/ui/static/bulma/sass/components/tabs.css.map +1 -0
  57. accrete/contrib/ui/static/bulma/sass/elements/block.css +17 -0
  58. accrete/contrib/ui/static/bulma/sass/elements/block.css.map +1 -0
  59. accrete/contrib/ui/static/bulma/sass/elements/box.css +43 -0
  60. accrete/contrib/ui/static/bulma/sass/elements/box.css.map +1 -0
  61. accrete/contrib/ui/static/bulma/sass/elements/button.css +685 -0
  62. accrete/contrib/ui/static/bulma/sass/elements/button.css.map +1 -0
  63. accrete/contrib/ui/static/bulma/sass/elements/button.scss +9 -2
  64. accrete/contrib/ui/static/bulma/sass/elements/content.css +208 -0
  65. accrete/contrib/ui/static/bulma/sass/elements/content.css.map +1 -0
  66. accrete/contrib/ui/static/bulma/sass/elements/content.scss +8 -2
  67. accrete/contrib/ui/static/bulma/sass/elements/delete.css +60 -0
  68. accrete/contrib/ui/static/bulma/sass/elements/delete.css.map +1 -0
  69. accrete/contrib/ui/static/bulma/sass/elements/icon.css +51 -0
  70. accrete/contrib/ui/static/bulma/sass/elements/icon.css.map +1 -0
  71. accrete/contrib/ui/static/bulma/sass/elements/image.css +253 -0
  72. accrete/contrib/ui/static/bulma/sass/elements/image.css.map +1 -0
  73. accrete/contrib/ui/static/bulma/sass/elements/loader.css +14 -0
  74. accrete/contrib/ui/static/bulma/sass/elements/loader.css.map +1 -0
  75. accrete/contrib/ui/static/bulma/sass/elements/notification.css +213 -0
  76. accrete/contrib/ui/static/bulma/sass/elements/notification.css.map +1 -0
  77. accrete/contrib/ui/static/bulma/sass/elements/progress.css +119 -0
  78. accrete/contrib/ui/static/bulma/sass/elements/progress.css.map +1 -0
  79. accrete/contrib/ui/static/bulma/sass/elements/table.css +294 -0
  80. accrete/contrib/ui/static/bulma/sass/elements/table.css.map +1 -0
  81. accrete/contrib/ui/static/bulma/sass/elements/tag.css +252 -0
  82. accrete/contrib/ui/static/bulma/sass/elements/tag.css.map +1 -0
  83. accrete/contrib/ui/static/bulma/sass/elements/title.css +131 -0
  84. accrete/contrib/ui/static/bulma/sass/elements/title.css.map +1 -0
  85. accrete/contrib/ui/static/bulma/sass/form/checkbox-radio.css +72 -0
  86. accrete/contrib/ui/static/bulma/sass/form/checkbox-radio.css.map +1 -0
  87. accrete/contrib/ui/static/bulma/sass/form/checkbox-radio.scss +7 -3
  88. accrete/contrib/ui/static/bulma/sass/form/file.css +374 -0
  89. accrete/contrib/ui/static/bulma/sass/form/file.css.map +1 -0
  90. accrete/contrib/ui/static/bulma/sass/form/input-textarea.css +284 -0
  91. accrete/contrib/ui/static/bulma/sass/form/input-textarea.css.map +1 -0
  92. accrete/contrib/ui/static/bulma/sass/form/input-textarea.scss +0 -10
  93. accrete/contrib/ui/static/bulma/sass/form/select.css +347 -0
  94. accrete/contrib/ui/static/bulma/sass/form/select.css.map +1 -0
  95. accrete/contrib/ui/static/bulma/sass/form/select.scss +1 -0
  96. accrete/contrib/ui/static/bulma/sass/form/shared.css +48 -0
  97. accrete/contrib/ui/static/bulma/sass/form/shared.css.map +1 -0
  98. accrete/contrib/ui/static/bulma/sass/form/shared.scss +5 -1
  99. accrete/contrib/ui/static/bulma/sass/form/tools.css +356 -0
  100. accrete/contrib/ui/static/bulma/sass/form/tools.css.map +1 -0
  101. accrete/contrib/ui/static/bulma/sass/form/tools.scss +23 -12
  102. accrete/contrib/ui/static/bulma/sass/grid/columns-v2.css +1635 -0
  103. accrete/contrib/ui/static/bulma/sass/grid/columns-v2.css.map +1 -0
  104. accrete/contrib/ui/static/bulma/sass/grid/columns.css +1652 -0
  105. accrete/contrib/ui/static/bulma/sass/grid/columns.css.map +1 -0
  106. accrete/contrib/ui/static/bulma/sass/grid/columns.scss +109 -25
  107. accrete/contrib/ui/static/bulma/sass/grid/grid.css +3011 -0
  108. accrete/contrib/ui/static/bulma/sass/grid/grid.css.map +1 -0
  109. accrete/contrib/ui/static/bulma/sass/grid/grid.scss +3 -3
  110. accrete/contrib/ui/static/bulma/sass/helpers/aspect-ratio.css +61 -0
  111. accrete/contrib/ui/static/bulma/sass/helpers/aspect-ratio.css.map +1 -0
  112. accrete/contrib/ui/static/bulma/sass/helpers/border.css +17 -0
  113. accrete/contrib/ui/static/bulma/sass/helpers/border.css.map +1 -0
  114. accrete/contrib/ui/static/bulma/sass/helpers/color.css +5582 -0
  115. accrete/contrib/ui/static/bulma/sass/helpers/color.css.map +1 -0
  116. accrete/contrib/ui/static/bulma/sass/helpers/color.scss +166 -186
  117. accrete/contrib/ui/static/bulma/sass/helpers/flexbox.css +217 -0
  118. accrete/contrib/ui/static/bulma/sass/helpers/flexbox.css.map +1 -0
  119. accrete/contrib/ui/static/bulma/sass/helpers/float.css +37 -0
  120. accrete/contrib/ui/static/bulma/sass/helpers/float.css.map +1 -0
  121. accrete/contrib/ui/static/bulma/sass/helpers/gap.css +209 -0
  122. accrete/contrib/ui/static/bulma/sass/helpers/gap.css.map +1 -0
  123. accrete/contrib/ui/static/bulma/sass/helpers/other.css +34 -0
  124. accrete/contrib/ui/static/bulma/sass/helpers/other.css.map +1 -0
  125. accrete/contrib/ui/static/bulma/sass/helpers/overflow.css +65 -0
  126. accrete/contrib/ui/static/bulma/sass/helpers/overflow.css.map +1 -0
  127. accrete/contrib/ui/static/bulma/sass/helpers/position.css +45 -0
  128. accrete/contrib/ui/static/bulma/sass/helpers/position.css.map +1 -0
  129. accrete/contrib/ui/static/bulma/sass/helpers/spacing.css +489 -0
  130. accrete/contrib/ui/static/bulma/sass/helpers/spacing.css.map +1 -0
  131. accrete/contrib/ui/static/bulma/sass/helpers/typography.css +423 -0
  132. accrete/contrib/ui/static/bulma/sass/helpers/typography.css.map +1 -0
  133. accrete/contrib/ui/static/bulma/sass/helpers/visibility.css +485 -0
  134. accrete/contrib/ui/static/bulma/sass/helpers/visibility.css.map +1 -0
  135. accrete/contrib/ui/static/bulma/sass/layout/container.scss +16 -8
  136. accrete/contrib/ui/static/bulma/sass/layout/footer.css +9 -0
  137. accrete/contrib/ui/static/bulma/sass/layout/footer.css.map +1 -0
  138. accrete/contrib/ui/static/bulma/sass/layout/hero.css +534 -0
  139. accrete/contrib/ui/static/bulma/sass/layout/hero.css.map +1 -0
  140. accrete/contrib/ui/static/bulma/sass/layout/level.css +102 -0
  141. accrete/contrib/ui/static/bulma/sass/layout/level.css.map +1 -0
  142. accrete/contrib/ui/static/bulma/sass/layout/media.css +90 -0
  143. accrete/contrib/ui/static/bulma/sass/layout/media.css.map +1 -0
  144. accrete/contrib/ui/static/bulma/sass/layout/section.css +23 -0
  145. accrete/contrib/ui/static/bulma/sass/layout/section.css.map +1 -0
  146. accrete/contrib/ui/static/bulma/sass/layout/section.scss +4 -0
  147. accrete/contrib/ui/static/bulma/sass/themes/dark.css +3 -0
  148. accrete/contrib/ui/static/bulma/sass/themes/dark.css.map +1 -0
  149. accrete/contrib/ui/static/bulma/sass/themes/light.css +3 -0
  150. accrete/contrib/ui/static/bulma/sass/themes/light.css.map +1 -0
  151. accrete/contrib/ui/static/bulma/sass/themes/light.scss +1 -0
  152. accrete/contrib/ui/static/bulma/sass/themes/setup.css +3 -0
  153. accrete/contrib/ui/static/bulma/sass/themes/setup.css.map +1 -0
  154. accrete/contrib/ui/static/bulma/sass/utilities/controls.css +13 -0
  155. accrete/contrib/ui/static/bulma/sass/utilities/controls.css.map +1 -0
  156. accrete/contrib/ui/static/bulma/sass/utilities/css-variables.css +3 -0
  157. accrete/contrib/ui/static/bulma/sass/utilities/css-variables.css.map +1 -0
  158. accrete/contrib/ui/static/bulma/sass/utilities/css-variables.scss +3 -2
  159. accrete/contrib/ui/static/bulma/sass/utilities/derived-variables.css +3 -0
  160. accrete/contrib/ui/static/bulma/sass/utilities/derived-variables.css.map +1 -0
  161. accrete/contrib/ui/static/bulma/sass/utilities/extends.css +13 -0
  162. accrete/contrib/ui/static/bulma/sass/utilities/extends.css.map +1 -0
  163. accrete/contrib/ui/static/bulma/sass/utilities/functions.scss +2 -2
  164. accrete/contrib/ui/static/bulma/sass/utilities/initial-variables.css +3 -0
  165. accrete/contrib/ui/static/bulma/sass/utilities/initial-variables.css.map +1 -0
  166. accrete/contrib/ui/static/bulma/sass/utilities/initial-variables.scss +4 -4
  167. accrete/contrib/ui/static/bulma/sass/utilities/mixins.css +3 -0
  168. accrete/contrib/ui/static/bulma/sass/utilities/mixins.css.map +1 -0
  169. accrete/contrib/ui/static/bulma/sass/utilities/mixins.scss +1 -1
  170. accrete/contrib/ui/static/bulma/versions/bulma-no-dark-mode.css +19648 -0
  171. accrete/contrib/ui/static/bulma/versions/bulma-no-dark-mode.css.map +1 -0
  172. accrete/contrib/ui/static/bulma/versions/bulma-no-dark-mode.scss +2 -1
  173. accrete/contrib/ui/static/bulma/versions/bulma-no-helpers-prefixed.css +11136 -0
  174. accrete/contrib/ui/static/bulma/versions/bulma-no-helpers-prefixed.css.map +1 -0
  175. accrete/contrib/ui/static/bulma/versions/bulma-no-helpers-prefixed.scss +1 -1
  176. accrete/contrib/ui/static/bulma/versions/bulma-no-helpers.css +11136 -0
  177. accrete/contrib/ui/static/bulma/versions/bulma-no-helpers.css.map +1 -0
  178. accrete/contrib/ui/static/bulma/versions/bulma-no-helpers.scss +1 -1
  179. accrete/contrib/ui/static/bulma/versions/bulma-prefixed.css +21551 -0
  180. accrete/contrib/ui/static/bulma/versions/bulma-prefixed.css.map +1 -0
  181. accrete/contrib/ui/static/bulma/versions/bulma-prefixed.scss +1 -1
  182. accrete/contrib/ui/static/css/accrete.css +20757 -19996
  183. accrete/contrib/ui/static/css/accrete.css.map +1 -1
  184. accrete/contrib/ui/static/css/accrete.scss +185 -461
  185. accrete/contrib/ui/static/js/filter.js +98 -679
  186. accrete/contrib/ui/static/js/htmx.min.js +1 -1
  187. accrete/contrib/ui/templates/django/forms/widgets/date.html +5 -0
  188. accrete/contrib/ui/templates/django/forms/widgets/input.html +1 -1
  189. accrete/contrib/ui/templates/django/forms/widgets/select.html +7 -5
  190. accrete/contrib/ui/templates/ui/content_right.html +7 -0
  191. accrete/contrib/ui/templates/ui/filter/filter.html +27 -0
  192. accrete/contrib/ui/templates/ui/filter/query_input.html +31 -0
  193. accrete/contrib/ui/templates/ui/filter/query_operator.html +11 -0
  194. accrete/contrib/ui/templates/ui/filter/query_params.html +106 -0
  195. accrete/contrib/ui/templates/ui/filter/query_tags.html +26 -0
  196. accrete/contrib/ui/templates/ui/layout.html +175 -232
  197. accrete/contrib/ui/templates/ui/list.html +39 -28
  198. accrete/contrib/ui/templates/ui/list_update.html +3 -0
  199. accrete/contrib/ui/templates/ui/message.html +13 -0
  200. accrete/contrib/ui/templates/ui/{partials/modal.html → modal.html} +9 -5
  201. accrete/contrib/ui/templates/ui/oob.html +3 -0
  202. accrete/contrib/ui/templates/ui/table.html +67 -71
  203. accrete/contrib/ui/templates/ui/table_row_update.html +14 -0
  204. accrete/contrib/ui/templates/ui/widgets/model_search_select.html +42 -0
  205. accrete/contrib/ui/templates/ui/widgets/model_search_select_multi.html +78 -0
  206. accrete/contrib/ui/templates/ui/widgets/model_search_select_options.html +3 -0
  207. accrete/contrib/ui/templatetags/{accrete_ui.py → ui.py} +69 -57
  208. accrete/contrib/ui/urls.py +3 -1
  209. accrete/contrib/ui/views.py +33 -3
  210. accrete/contrib/ui/widgets/__init__.py +1 -0
  211. accrete/contrib/ui/widgets/search_select.py +106 -0
  212. accrete/contrib/user/forms.py +1 -1
  213. accrete/contrib/user/templates/user/change_email.html +22 -27
  214. accrete/contrib/user/templates/user/change_password.html +24 -36
  215. accrete/contrib/user/templates/user/login.html +72 -23
  216. accrete/contrib/user/templates/user/user_form.html +50 -52
  217. accrete/contrib/user/urls.py +2 -3
  218. accrete/contrib/user/views.py +39 -46
  219. accrete/forms.py +0 -2
  220. accrete/managers.py +4 -4
  221. accrete/middleware.py +43 -68
  222. accrete/models.py +7 -1
  223. accrete/storage.py +4 -1
  224. accrete/tenant.py +11 -5
  225. accrete/utils/__init__.py +2 -0
  226. accrete/utils/models.py +9 -1
  227. accrete/utils/views.py +36 -20
  228. accrete/views.py +9 -5
  229. {accrete-0.0.99.dist-info → accrete-0.0.109.dist-info}/METADATA +2 -2
  230. accrete-0.0.109.dist-info/RECORD +369 -0
  231. {accrete-0.0.99.dist-info → accrete-0.0.109.dist-info}/WHEEL +1 -1
  232. accrete/contrib/ui/elements.py +0 -93
  233. accrete/contrib/ui/static/bulma/css/versions/bulma-no-dark-mode.min.css.map +0 -1
  234. accrete/contrib/ui/static/bulma/css/versions/bulma-no-helpers-prefixed.min.css.map +0 -1
  235. accrete/contrib/ui/static/bulma/css/versions/bulma-no-helpers.min.css.map +0 -1
  236. accrete/contrib/ui/static/bulma/sass/grid/columns-v2.scss +0 -957
  237. accrete/contrib/ui/static/js/ui.js +0 -30
  238. accrete/contrib/ui/templates/ui/dashboard.html +0 -7
  239. accrete/contrib/ui/templates/ui/detail.html +0 -19
  240. accrete/contrib/ui/templates/ui/form.html +0 -21
  241. accrete/contrib/ui/templates/ui/partials/filter.html +0 -143
  242. accrete/contrib/ui/templates/ui/partials/form_errors.html +0 -34
  243. accrete/contrib/ui/templates/ui/partials/header.html +0 -123
  244. accrete/contrib/ui/templates/ui/partials/modal_form.html +0 -45
  245. accrete/contrib/ui/templates/ui/partials/onchange_form.html +0 -1
  246. accrete/contrib/ui/templates/ui/partials/pagination_detail.html +0 -23
  247. accrete/contrib/ui/templates/ui/partials/pagination_list.html +0 -28
  248. accrete/contrib/ui/templates/ui/partials/table_field.html +0 -16
  249. accrete/contrib/ui/templates/ui/partials/table_field_float.html +0 -1
  250. accrete/contrib/ui/templates/ui/partials/table_field_monetary.html +0 -4
  251. accrete/contrib/ui/templates/ui/partials/table_field_string.html +0 -1
  252. accrete/contrib/user/templates/user/user_detail.html +0 -43
  253. accrete-0.0.99.dist-info/RECORD +0 -231
  254. {accrete-0.0.99.dist-info → accrete-0.0.109.dist-info}/licenses/LICENSE +0 -0
@@ -1,81 +1,77 @@
1
1
  {% extends 'ui/layout.html' %}
2
- {% load static %}
3
2
  {% load i18n %}
4
- {% load accrete_ui %}
3
+ {% load ui %}
4
+ {% load partials %}
5
5
 
6
- {% block tenant_switch_url %}{% endblock %}
7
-
8
- {% block content %}
9
- <table id="content-table" class="table is-fullwidth is-hoverable has-box" hx-indicator=".htmx-indicator">
10
- <thead style="position: sticky; top: 0; z-index: 10">
11
- {% block table_header_row %}
12
- <tr>
13
- {% block table_header %}
14
- <th>{{ object_label }}</th>
15
- {% for field in fields %}
16
- <th data-name="{{ field.name }}" style="text-align: {% firstof field.header_alignment.value field.alignment.value %}">{{ field.label }}</th>
17
- {% endfor %}
18
- {% if unselect_button %}
19
- <th></th>
20
- {% endif %}
21
- {% endblock %}
22
- </tr>
23
- {% endblock %}
6
+ {% block content_left %}
7
+ <table id="content-table" class="table can-compact is-fullwidth is-hoverable is-narrow my-0" hx-indicator=".htmx-indicator" x-data="">
8
+ <thead style="position: sticky; top: 0; z-index: 10; background-color: var(--bulma-scheme-main)">
9
+ <tr>
10
+ <th>{{ object_label }}</th>
11
+ {% for field in fields %}
12
+ <th style="text-align: {{ page.object_list|table_alignment:field }}">{{ page.object_list|verbose_field_name:field }}</th>
13
+ {% endfor %}
14
+ </tr>
24
15
  </thead>
25
- <tbody id="content-table-body" style="z-index: 9">
26
- {% for obj in list_page %}
27
- {% block table_data_row %}
28
- <tr>
29
- {% block table_data %}
30
- <td>
31
- <a class="is-underlined" href="{{ obj.get_absolute_url }}{{ object_param_str }}">{{ obj }}</a>
32
- </td>
16
+ <tbody id="content-table-body" style="z-index: 9" x-ref="tbody">
17
+ {% for object in page.object_list %}
18
+ {% partialdef tr inline=True %}
19
+ <tr id="tr-{{ object.pk }}"
20
+ {% if detail_enabled %}
21
+ style="cursor: pointer;"
22
+ hx-get="{{ object.get_absolute_url }}{% querystring %}" hx-target="#content-right-header" hx-select="#content-right-header" hx-select-oob="#content-right" hx-swap="outerHTML"
23
+ x-data="{selected: false}" @unselect-tr.window="selected = false"
24
+ x-on:click="$dispatch('unselect-tr'); showContentRight = true; selected = true; $nextTick(() => { $el.scrollIntoView( {block: 'nearest'} ) });" x-bind:class="selected ? 'is-success' : ''"
25
+ {% endif %}
26
+ >
27
+ {% partialdef td inline=True %}
28
+ <td>{{ object }}</td>
33
29
  {% for field in fields %}
34
- {% if field.template %}
35
- {% include field.template %}
36
- {% else %}
37
- {% with val=obj|field_attr:field is_relation=field.is_relation %}
38
- <td style="text-align: {{ field.alignment.value }}" {% if val|length > field.truncate_after %}title="{{ val }}"{% endif %}>
39
- <span>
40
- <span class="responsive-heading has-text-weight-light" style="margin-right: .2rem">{{ field.label }}:</span>
41
- {% if is_relation and val.get_absolute_url %}<a href="{{ obj|related_obj_url:field.name }}" class="is-underlined">{% endif %}
42
- {{ field.prefix }}
43
- {% block td_content %}
44
- {% if field.truncate_after > 0 %}
45
- {{ val|default_if_none:'---'|truncatechars:field.truncate_after }}
46
- {% else %}
47
- {{ val|default_if_none:'---' }}
48
- {% endif %}
49
- {% endblock %}
50
- {{ field.suffix }}
51
- {% if is_relation %}</a>{% endif %}
52
- </span>
53
- </td>
54
- {% endwith %}
55
- {% endif %}
30
+ <td style="text-align: {{ object|table_alignment:field }}">
31
+ <span class="">
32
+ <span class="responsive-heading has-text-weight-light" style="margin-right: .2rem">{{ object|verbose_field_name:field }}:</span>
33
+ {{ object|table_display:field|default_if_none:'' }}
34
+ </span>
35
+ </td>
56
36
  {% endfor %}
57
- {% if unselect_button %}
58
- <td><button class="button is-small has-icon p-0 is-pulled-right" onclick="unselectSingleObject({{ obj.pk }})"><i class="icon-delete-filter"></i></button></td>
59
- {% endif %}
60
- {% endblock %}
37
+ {% endpartialdef %}
61
38
  </tr>
62
- {% endblock %}
39
+ {% endpartialdef %}
63
40
  {% endfor %}
64
41
  </tbody>
42
+ <tfoot style="position: sticky; bottom: 0; z-index: 10; background-color: var(--bulma-scheme-main);" class="has-text-weight-bold">
43
+ <tr id="tf-row">
44
+ {% partialdef tf inline=True %}
45
+ <td class="p-0"></td>
46
+ {% for field in fields %}
47
+ {% if field in footer %}
48
+ <td style="text-align: {{ queryset|table_alignment:field }}">
49
+ <span class="">
50
+ <span class="responsive-heading has-text-weight-light" style="margin-right: .2rem">{{ queryset|verbose_field_name:field }}:</span>
51
+ {{ footer|get_item:field }}
52
+ </span>
53
+ </td>
54
+ {% else %}
55
+ <td class="p-0"></td>
56
+ {% endif %}
57
+ {% endfor %}
58
+ {% endpartialdef %}
59
+ </tr>
60
+ </tfoot>
65
61
  </table>
66
- {% if endless_scroll %}
67
- <div id="endless-scroller"
68
- {% if list_page.has_next %}
69
- hx-get="{{ pagination_param_str }}&page={{ list_page.next_page_number }}"
70
- hx-trigger="intersect once"
71
- hx-select="tbody > tr"
72
- hx-target="#content-table-body"
73
- hx-swap="beforeend"
74
- hx-select-oob="#list-pagination-end-index,#list-pagination-next-button,#list-pagination-prev-button,#endless-scroller"
75
- hx-indicator="#endless-scroll-indicator"
76
- {% endif %}
77
- >
78
- </div>
79
- <progress id="endless-scroll-indicator" class="htmx-indicator progress is-small is-success" max="100">15%</progress>
80
- {% endif %}
81
- {% endblock %}
62
+
63
+ <div id="endless-scroller"
64
+ {% if page.has_next %}
65
+ hx-get="{% querystring page=page.next_page_number %}"
66
+ hx-trigger="intersect once"
67
+ hx-select="tbody > tr"
68
+ hx-target="#content-table-body"
69
+ hx-swap="beforeend"
70
+ hx-select-oob="#pagination-next-button,#pagination-end-index,#endless-scroller"
71
+ hx-indicator="#endless-scroll-indicator"
72
+ hx-replace-url="true"
73
+ {% endif %}
74
+ >
75
+ </div>
76
+ <progress id="endless-scroll-indicator" class="htmx-indicator progress is-small is-success" max="100">15%</progress>
77
+ {% endblock %}
@@ -0,0 +1,14 @@
1
+ <table>
2
+ <tbody>
3
+ <tr hx-swap-oob="innerHTML:#tr-{{ object.pk }}">
4
+ {% include 'ui/table.html#td' with object=object fields=fields queryset=queryset %}
5
+ </tr>
6
+ </tbody>
7
+ {% if footer %}
8
+ <tfoot>
9
+ <tr hx-swap-oob="innerHTML:#tf-row">
10
+ {% include 'ui/table.html#tf' with object=object fields=fields queryset=queryset %}
11
+ </tr>
12
+ </tfoot>
13
+ {% endif %}
14
+ </table>
@@ -0,0 +1,42 @@
1
+ {% load static %}
2
+ {% load i18n %}
3
+
4
+
5
+ <div class="field select" x-data="{open: false, shouldClose(event) {return event.relatedTarget == $refs.dropdownContent || $refs.dropdownContent.contains(event.relatedTarget);} }"
6
+ @click.outside="open = false" hx-disinherit="*"
7
+ >
8
+ <input id="id_{{ widget.name }}_value" hidden="hidden" type="{{ widget.type }}" name="{{ widget.name }}" x-ref="inputValue" aria-label="inputValue"
9
+ value="{{ widget.value }}"
10
+ >
11
+ <input id="id_{{ widget.name }}_display" class="input" type="text" readonly x-ref="inputDisplay" value="{{ widget.value_display }}" aria-label="inputDisplay"
12
+ style="padding-right: 30px"
13
+ x-on:focus="open = !open;"
14
+ x-on:keydown="if ($event.keyCode == 27) {document.activeElement.blur()} else if ($event.keyCode != 9) {$refs.searchInput.focus();}"
15
+ x-on:focusout="open = shouldClose($event)"
16
+ >
17
+ <div style="position: relative; z-index: 90;">
18
+ <div x-show="open" x-cloak="" class="box pt-1 mb-2" x-ref="dropdownContent" tabindex="-1" x-transition style="position: absolute; width: 100%">
19
+ <input id="id_{{ widget.name }}_search" type="search" autocomplete="off" class="input" x-ref="searchInput" aria-label="search"
20
+ name="{{ widget.search_parameter }}"
21
+ placeholder="{% translate 'Type to search' %}"
22
+ x-on:keydown="if ($event.keyCode == 27) {document.activeElement.blur()}"
23
+ x-on:focusout="open = shouldClose($event)"
24
+ hx-get="{{ widget.search_url }}"
25
+ hx-trigger="input changed delay:500ms, search"
26
+ hx-target="#{{ widget.name }}_dropdown_items"
27
+ >
28
+ <div x-on:click="$refs.inputValue.setAttribute('value', $event.target.value); $refs.inputDisplay.value = $event.target.innerText; htmx.trigger($refs.inputValue, 'submit'); open = false; {% if widget.hx_trigger_on_change %}htmx.trigger('#id_{{ widget.name }}_value', 'change');{% endif %}"
29
+ x-on:keydown="if ($event.keyCode == 27) {document.activeElement.blur()} else if ($event.keyCode != 9 && $event.keyCode != 13) {$refs.searchInput.focus();}"
30
+ >
31
+ {% if not widget.required %}
32
+ <div class="hoverable" style="max-height: 200px; width: 100%; overflow-y: auto;">
33
+ <button type="button" class="dropdown-item px-1" value="" style="word-wrap: break-word; width: 100%; white-space: normal">---------</button>
34
+ </div>
35
+ {% endif %}
36
+ <div id="{{ widget.name }}_dropdown_items" class="hoverable" style="max-height: 200px; width: 100%; overflow-y: auto;">
37
+ {% include 'ui/widgets/model_search_select_options.html' %}
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
@@ -0,0 +1,78 @@
1
+ {% load i18n %}
2
+ {% load ui %}
3
+
4
+ <div class="" x-data="{open: false, shouldClose(event) {return event.relatedTarget == $refs.dropdownContent || $refs.dropdownContent.contains(event.relatedTarget);} }"
5
+ @click.outside="open = false" style="width: 100%;" hx-disinherit="*"
6
+ >
7
+ <select multiple id="{{ widget.attrs.id }}" hidden="hidden" name="{{ widget.name }}" aria-label="{{ widget.name }}" x-ref="inputValue">
8
+ {% for obj in selected %}
9
+ <option selected value="{{ obj.pk }}"></option>
10
+ {% endfor %}
11
+ </select>
12
+ <div id="{{ widget.attrs.id }}_display" class="input select py-1 pl-1" tabindex="0" x-ref="inputDisplay" aria-label="inputDisplay"
13
+ style="padding-right: 30px; border-radius: 0; border-top: 0; border-right:0; border-left: 0; min-height: var(--bulma-control-height); height: fit-content; width: 100%"
14
+ x-on:focus="open = true;"
15
+ x-on:keydown="if ($event.keyCode == 27) {document.activeElement.blur()} else if ($event.keyCode != 9) {$refs.searchInput.focus();}"
16
+ x-on:focusout="open = shouldClose($event)"
17
+ >
18
+ <div class="tags" x-ref="inputDisplayTags"
19
+ @click="
20
+ if ($event.target.classList.contains('delete')) {
21
+ let val = $event.target.getAttribute('data-value');
22
+ $refs.inputValue.querySelector(`option[value=${CSS.escape(val)}]`).remove();
23
+ $event.target.parentElement.remove();
24
+ $dispatch('change');
25
+ }
26
+ "
27
+ >
28
+ {% for obj in selected %}
29
+ <span class="tag has-text-weight-normal is-medium">
30
+ {{ obj }}
31
+ <button class="delete" type="button" data-value="{{ obj.pk }}"></button>
32
+ </span>
33
+ {% endfor %}
34
+ </div>
35
+ </div>
36
+ <div style="position: relative; top: 10px; z-index: 9">
37
+ <div x-show="open" class="box pt-1 mb-2" x-ref="dropdownContent" x-cloak="" tabindex="-1" x-transition style="position: absolute; width: 100%;">
38
+ <input id="id_{{ widget.uuid }}_search" type="search" autocomplete="off" class="input" x-ref="searchInput" aria-label="search"
39
+ name="{{ widget.search_parameter }}"
40
+ placeholder="{% translate 'Type to search' %}"
41
+ x-on:keydown="if ($event.keyCode == 27) {document.activeElement.blur()}"
42
+ x-on:focusout="open = shouldClose($event)"
43
+ hx-get="{{ widget.search_url }}"
44
+ hx-trigger="input changed delay:300ms, search"
45
+ hx-target="#id_{{ widget.uuid }}_dropdown_items"
46
+ hx-swap="innerHTML"
47
+ @change.stop=""
48
+ >
49
+ <div id="id_{{ widget.uuid }}_dropdown_items" class="hoverable" style="max-height: 200px; width: 100%; overflow-y: auto;"
50
+ x-on:click="
51
+ if (! $refs.inputValue.querySelectorAll(`option[value=${CSS.escape($event.target.value)}]`).length) {
52
+ let option = document.createElement('option');
53
+ let tag = document.createElement('span');
54
+ let delButton = document.createElement('button');
55
+ option.setAttribute('selected', '');
56
+ option.value = $event.target.value;
57
+ tag.classList.add('tag', 'has-text-weight-normal', 'is-medium');
58
+ tag.innerText = $event.target.innerText;
59
+ delButton.classList.add('delete');
60
+ delButton.type = 'button';
61
+ delButton.setAttribute('data-value', $event.target.value);
62
+ tag.appendChild(delButton);
63
+ $refs.inputValue.appendChild(option);
64
+ $refs.inputDisplayTags.appendChild(tag);
65
+ $dispatch('change');
66
+ };
67
+ {% if widget.hx_trigger_on_change %}htmx.trigger('#{{ widget.attrs.id }}_value', 'change');{% endif %}
68
+ "
69
+ x-on:keydown="
70
+ if ($event.keyCode == 27) {document.activeElement.blur()}
71
+ else if ($event.keyCode != 9 && $event.keyCode != 13) {$refs.searchInput.focus();}
72
+ "
73
+ >
74
+ {% include 'ui/widgets/model_search_select_options.html' %}
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
@@ -0,0 +1,3 @@
1
+ {% for option in options %}
2
+ <button type="button" class="dropdown-item px-1" value="{{ option.pk }}" style="word-wrap: break-word; width: 100%; white-space: normal">{{ option }}</button>
3
+ {% endfor %}
@@ -1,15 +1,14 @@
1
1
  import logging
2
+ import re
2
3
  from datetime import datetime, date, timedelta
4
+
3
5
  from django.utils.translation import gettext_lazy as _
4
6
  from django import template
5
- from django.db.models import Manager
6
- from django.forms import Form, ModelForm
7
+ from django.db.models import Manager, DecimalField, IntegerField, FloatField, Model
7
8
  from django.conf import settings
8
9
  from django.template.loader import render_to_string
9
10
  from django.utils.safestring import mark_safe
10
11
 
11
- from accrete.contrib.ui import TableField, TableFieldType
12
-
13
12
  _logger = logging.getLogger(__name__)
14
13
  register = template.Library()
15
14
 
@@ -27,34 +26,19 @@ def combine_templates(template_name, request=None):
27
26
  return mark_safe(html)
28
27
 
29
28
 
30
- @register.filter(name='field_attr')
31
- def get_attr_from_field(obj: object, field: TableField):
32
-
33
- def return_save(attr):
34
- if isinstance(attr, Manager):
35
- return attr.related_model
36
- if attr is None:
37
- return None
38
- if isinstance(attr, str):
39
- return attr
40
- return attr
41
- f_name = field.name
42
- if field.field_type == TableFieldType.CHOICE_DISPLAY:
43
- f_name = f'get_{f_name}_display'
44
-
45
- try:
46
- attribute = getattr(obj, f_name, False)
47
- except (AttributeError,):
48
- _logger.warning(f'Object {obj} has no attribute {f_name}')
49
- attribute = None
50
- return return_save(attribute)
29
+ @register.filter(name='related_obj_url')
30
+ def related_obj_url(param: object, value: str):
31
+ attr = getattr(param, value, False)
32
+ if attr and hasattr(attr, 'get_absolute_url'):
33
+ return attr.get_absolute_url()
34
+ return '#'
51
35
 
52
36
 
53
37
  @register.filter(name='get_attr')
54
38
  def get_attr_from_string(param: object, value: str):
55
39
 
56
40
  def return_save(attr):
57
- if isinstance(attr, Manager):
41
+ if isinstance(attr, Manager) and hasattr(attr, 'related_model'):
58
42
  return attr.related_model
59
43
  if attr is None:
60
44
  return None
@@ -70,43 +54,67 @@ def get_attr_from_string(param: object, value: str):
70
54
  return return_save(attribute)
71
55
 
72
56
 
73
- @register.filter(name='related_obj_url')
74
- def related_obj_url(param: object, value: str):
75
- attr = getattr(param, value, False)
76
- if attr and hasattr(attr, 'get_absolute_url'):
77
- return attr.get_absolute_url()
78
- return '#'
57
+ @register.filter(name='has_attr')
58
+ def has_attr(param: object, value:str):
59
+ return hasattr(param, value)
79
60
 
80
61
 
81
- @register.filter(name='message_class')
82
- def message_class(param):
83
- if param.level == 25:
84
- return 'is-success'
85
- if param.level == 30:
86
- return 'is-warning'
87
- if param.level == 40:
88
- return 'is-danger'
62
+ @register.filter(name='get_item')
63
+ def get_item(param, value):
64
+ return param[value]
89
65
 
90
66
 
91
- @register.filter(name='non_field_error_code')
92
- def non_field_error_code(form: [Form|ModelForm], code: str) -> bool:
93
- if not form:
94
- return False
95
- for error in form.non_field_errors().as_data():
96
- if error.code and error.code == code:
97
- return True
98
- return False
67
+ @register.filter(name='get_model')
68
+ def get_model(instance):
69
+ return instance._meta.model
99
70
 
100
71
 
101
- @register.filter(name='extract_non_field_error')
102
- def extract_non_field_error(form: [Form|ModelForm], code: str) -> str:
103
- if not form:
72
+ @register.filter(name='verbose_field_name')
73
+ def verbose_field_name(param: object, value: str):
74
+ if not param:
104
75
  return ''
105
- errors = []
106
- for error in form.non_field_errors().as_data():
107
- if error.code and error.code == code:
108
- errors.append(str(error.message))
109
- return mark_safe('<br>'.join(errors))
76
+ if isinstance(param, Model):
77
+ name = param._meta.get_field(value).verbose_name
78
+ else:
79
+ name = getattr(param.model, value).field.verbose_name
80
+ return name
81
+
82
+
83
+ @register.filter(name='table_display')
84
+ def table_display(param: object, value: str):
85
+ field = getattr(param, value)
86
+ if isinstance(field, Manager):
87
+ return mark_safe(f"""
88
+ <span>
89
+ <div class="tags">
90
+ {''.join(['<span class="tag">{}</span>'.format(o) for o in field.all()])}
91
+ </div>
92
+ </span>
93
+ """)
94
+ return get_attr_from_string(param, value)
95
+
96
+
97
+ @register.filter(name='table_alignment')
98
+ def table_alignment(param, value):
99
+ if isinstance(param, str):
100
+ return 'left'
101
+ if isinstance(param, Model):
102
+ field = param._meta.get_field(value)
103
+ else:
104
+ field = getattr(param.model, value).field
105
+ if isinstance(field, (DecimalField, IntegerField, FloatField)):
106
+ return 'right'
107
+ return 'left'
108
+
109
+
110
+ @register.filter(name='message_class')
111
+ def message_class(message):
112
+ if message.level == 25:
113
+ return 'is-success'
114
+ if message.level == 30:
115
+ return 'is-warning'
116
+ if message.level == 40:
117
+ return 'is-danger'
110
118
 
111
119
 
112
120
  @register.filter(name='timedelta_cast')
@@ -120,7 +128,7 @@ def timedelta_cast(td: timedelta, code: str) -> str | None:
120
128
 
121
129
 
122
130
  @register.filter(name='weekday')
123
- def datetime_to_weekday(dt: datetime|date, default=None):
131
+ def datetime_to_weekday(dt: datetime|date, default=None) -> str:
124
132
  if dt is None:
125
133
  return default
126
134
  mapping = {
@@ -133,3 +141,7 @@ def datetime_to_weekday(dt: datetime|date, default=None):
133
141
  7: _('Sun')
134
142
  }
135
143
  return mapping[dt.isoweekday()]
144
+
145
+ @register.filter(name='xrefsave')
146
+ def x_ref_save(param: str):
147
+ return re.sub(r'[^A-Za-z]+','', param)
@@ -5,5 +5,7 @@ from . import views
5
5
  app_name = 'ui'
6
6
 
7
7
  urlpatterns = [
8
- path('', views.home, name='home')
8
+ path('filter/<str:model>/params/', views.params, name='filter_params'),
9
+ path('filter/<str:model>/input/', views.set_filter_input, name='filter_input'),
10
+ path('filter/<str:model>/query/add/', views.filter_add_query, name='filter_add_query')
9
11
  ]
@@ -1,5 +1,35 @@
1
- from django.shortcuts import render
1
+ import json
2
2
 
3
+ from django.contrib.auth.decorators import login_required
4
+ from django.apps import apps
5
+ from django.http import HttpResponse
6
+ from .filter import Filter
3
7
 
4
- def home(request):
5
- return render(request, 'ui/layout.html', {})
8
+
9
+ @login_required
10
+ def params(request, model: str):
11
+ app_label, model_name = model.split('.')
12
+ Model = apps.get_model(app_label, model_name)
13
+ return HttpResponse(Filter(Model, request.GET).query_params())
14
+
15
+
16
+ @login_required
17
+ def set_filter_input(request, model: str):
18
+ app_label, model_name = model.split('.')
19
+ Model = apps.get_model(app_label, model_name)
20
+ lookup = request.GET.get('lookup')
21
+ return HttpResponse(Filter(Model, request.GET).query_input(lookup))
22
+
23
+
24
+ @login_required
25
+ def filter_add_query(request, model: str):
26
+ app_label, model_name = model.split('.')
27
+ Model = apps.get_model(app_label, model_name)
28
+ query = json.loads(request.GET.get('q', '[]'))
29
+ lookup = request.GET.get('filter_lookup')
30
+ value = request.GET.get('filter_input')
31
+ query.append({lookup: value})
32
+ query = json.dumps(query)
33
+ query_dict = request.GET.copy()
34
+ query_dict.update(q=query)
35
+ return HttpResponse(Filter(Model, query_dict).query_tags())
@@ -0,0 +1 @@
1
+ from .search_select import ModelSearchSelect, ModelSearchSelectMulti
@@ -0,0 +1,106 @@
1
+ from uuid import uuid4
2
+ from django.forms import widgets
3
+ from django.shortcuts import resolve_url
4
+
5
+
6
+ class ModelSearchSelect(widgets.NumberInput):
7
+ template_name = 'ui/widgets/model_search_select.html'
8
+ option_template_name = 'ui/widgets/model_search_select_options.html'
9
+ input_type = 'number'
10
+
11
+ def __init__(
12
+ self,
13
+ search_url: str,
14
+ search_parameter: str = 'search',
15
+ limit: int | None = 5,
16
+ hx_trigger_on_change: bool = False,
17
+ choices=()
18
+ ):
19
+ super().__init__()
20
+ self.search_url = search_url
21
+ self.search_parameter = search_parameter
22
+ self.limit = limit
23
+ self.hx_trigger_on_change = hx_trigger_on_change
24
+ self.choices = choices
25
+
26
+ def get_context(self, name, value, attrs):
27
+ qs = self.choices.queryset
28
+ if self.limit:
29
+ qs = qs[:self.limit]
30
+ uuid = str(uuid4())
31
+
32
+ context = {
33
+ "widget": {
34
+ "name": name,
35
+ "is_hidden": self.is_hidden,
36
+ "required": self.is_required,
37
+ "value": self.format_value(value or ''),
38
+ 'value_display': self.value_display(value),
39
+ "attrs": self.build_attrs(self.attrs, attrs),
40
+ "template_name": self.template_name,
41
+ 'search_url': self.search_url,
42
+ 'search_parameter': self.search_parameter,
43
+ 'hx_trigger_on_change': self.hx_trigger_on_change,
44
+ 'uuid': uuid
45
+ },
46
+ 'options': qs
47
+ }
48
+ return context
49
+
50
+ def format_value(self, value):
51
+ res = super().format_value(value)
52
+ return res or ''
53
+
54
+ def value_display(self, value):
55
+ if value is None:
56
+ return ''
57
+ try:
58
+ value = int(value)
59
+ except ValueError:
60
+ return ''
61
+ return str(self.choices.queryset.get(pk=value))
62
+
63
+
64
+ class ModelSearchSelectMulti(widgets.SelectMultiple):
65
+ template_name = 'ui/widgets/model_search_select_multi.html'
66
+ option_template_name = 'ui/widgets/model_search_select_options.html'
67
+ input_type = 'number'
68
+
69
+ def __init__(
70
+ self,
71
+ search_url: str,
72
+ search_parameter: str = 'search',
73
+ limit: int | None = 5,
74
+ hx_trigger_on_change: bool = False,
75
+ choices=()
76
+ ):
77
+ super().__init__()
78
+ self.search_url = search_url
79
+ self.search_parameter = search_parameter
80
+ self.limit = limit
81
+ self.hx_trigger_on_change = hx_trigger_on_change
82
+ self.choices = choices
83
+
84
+ def get_context(self, name, value, attrs):
85
+ qs = self.choices.queryset
86
+ if self.limit:
87
+ qs = qs[:self.limit]
88
+ uuid = uuid4()
89
+
90
+ context = {
91
+ "widget": {
92
+ "name": name,
93
+ "is_hidden": self.is_hidden,
94
+ "required": self.is_required,
95
+ "value": self.format_value(value),
96
+ "attrs": self.build_attrs(self.attrs, attrs),
97
+ "template_name": self.template_name,
98
+ 'search_url': resolve_url(self.search_url),
99
+ 'search_parameter': self.search_parameter,
100
+ 'hx_trigger_on_change': self.hx_trigger_on_change,
101
+ 'uuid': uuid
102
+ },
103
+ 'options': qs,
104
+ 'selected': self.choices.queryset.filter(pk__in=value) if value else qs.none()
105
+ }
106
+ return context
@@ -18,7 +18,7 @@ class UserForm(ModelForm):
18
18
  fields = [
19
19
  'username',
20
20
  'first_name',
21
- 'last_name',
21
+ 'last_name'
22
22
  ]
23
23
 
24
24
  def save(self, commit=True):