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,17 +1,24 @@
1
+ import json
1
2
  import logging
2
- from django.db.models.fields.related import ManyToOneRel, ManyToManyRel
3
- from django.core.cache import cache
3
+ import datetime
4
+ from decimal import Decimal
5
+
6
+ from django.db.models import QuerySet
7
+ from django.http.request import QueryDict
8
+ from django.db.models.fields import Field
9
+ from django.template.loader import render_to_string
4
10
  from django.utils.translation import gettext_lazy as _
5
11
  from django.utils.safestring import mark_safe
6
- from django.utils.translation import get_language
12
+ from django.apps import apps
13
+ from django.core import paginator
14
+ from accrete.utils.models import get_related_model, get_related_field
15
+ from accrete.utils import page_from_querystring, filter_from_querystring
7
16
 
8
17
  _logger = logging.getLogger(__name__)
9
18
 
10
19
 
11
20
  class Filter:
12
21
 
13
- query_relation_depth = 4
14
-
15
22
  LABEL_EXACT = _('Equals')
16
23
  LABEL_EXACT_NOT = _('Equals Not')
17
24
  LABEL_ICONTAINS = _('Contains')
@@ -22,302 +29,324 @@ class Filter:
22
29
  LABEL_FALSE = _('False')
23
30
  LABEL_SET = _('Is Set')
24
31
  LABEL_NOT_SET = _('Is Not Set')
32
+ LABEL_AND = _('And')
33
+ LABEL_OR = _('Or')
34
+ LABEL_XOR = _('Not Or')
35
+
36
+ # EXCLUDE = ['tenant']
37
+
38
+ DATE_FORMAT = '%Y-%m-%d'
39
+
40
+ TYPES_INTEGER = [
41
+ 'AutoField', 'BigAutoField', 'IntegerField', 'PositiveSmallIntegerField'
42
+ ]
43
+ TYPES_NUMBER = ['DecimalField', 'FloatField']
44
+ TYPES_CHAR = ['CharField', 'TextField']
45
+ TYPES_BOOL = ['BooleanField']
46
+ TYPES_DATETIME = ['DateTimeField']
47
+ TYPES_DATE = ['DateField']
48
+ TYPES_TIME = ['TimeField']
25
49
 
26
50
  def __init__(
27
- self, model, query_relation_depth: int = 4,
28
- default_exclude: list[str] = None, default_filter_term: str = None
51
+ self, model, query: QueryDict, default_lookup: str = None
29
52
  ):
30
53
  self.model = model
31
- self.query_relation_depth = query_relation_depth
32
- if default_exclude is None:
33
- default_exclude = ['tenant', 'user']
34
- self.default_exclude = default_exclude
35
- self.default_filter_term = default_filter_term or ''
36
- self.fields = []
37
- self.field_paths = []
54
+ self.query = query
55
+ self.model_name = f'{self.model._meta.app_label}.{self.model._meta.model_name}'
56
+ seen_models = query.get('models', '').split(',')
57
+ self.seen_models = [
58
+ model for model in seen_models
59
+ if model and model != self.model_name
60
+ ] + [self.model_name]
61
+ self.default_lookup = default_lookup
62
+ self.exclude = []
63
+ if hasattr(self.model, 'exclude_from_filter'):
64
+ self.exclude.extend(self.model.exclude_from_filter())
38
65
 
39
- @staticmethod
40
- def cast_decimal_places_to_step(decimal_places):
41
- if not decimal_places or decimal_places < 1:
42
- return '1'
43
- zero_count = decimal_places - 1
44
- return f'0.{"0" * zero_count}1'
66
+ def get_page(self, select_related: list = None, prefetch_related: list = None) -> paginator.Page:
67
+ return page_from_querystring(
68
+ self.model,
69
+ self.query,
70
+ select_related=select_related,
71
+ prefetch_related=prefetch_related
72
+ )
45
73
 
46
- def get_fields(self, model_path: list, field_path: str):
47
- fields = self.get_local_fields(model_path[-1], field_path)
48
- if len(model_path) <= self.query_relation_depth:
49
- fields.extend(self.get_relation_fields(model_path, field_path))
50
- return sorted(fields, key=lambda x: x['label'].lower())
74
+ def get_queryset(self) -> QuerySet:
75
+ return filter_from_querystring(self.model, self.query)
51
76
 
52
- def get_relation_fields(self, model_path, field_path):
53
- filter_exclude = getattr(model_path[-1], 'filter_exclude', [])
54
- filter_exclude.extend(self.default_exclude)
77
+ def query_params(self):
55
78
  fields = filter(
56
- lambda x: x.is_relation and x.name not in filter_exclude,
57
- model_path[-1]._meta.get_fields()
79
+ lambda x: x.name not in self.exclude,
80
+ self.model._meta.get_fields()
58
81
  )
59
- res = []
82
+ params = []
83
+ path = self.query.get('path', '')
60
84
  for field in fields:
61
- if field.related_model in model_path:
62
- continue
63
- rel_path = f'{field_path}{"__" if field_path else ""}{field.name}'
64
- model_path_copy = model_path.copy()
65
- model_path_copy.append(field.related_model)
66
- try:
67
- label = field.verbose_name
68
- except AttributeError:
69
- label = field.related_model._meta.verbose_name
70
- res.append({
71
- 'name': f'{rel_path}',
72
- 'label': str(label),
73
- 'type': field.get_internal_type(),
74
- 'null': field.null,
75
- 'choices': [],
76
- 'fields': self.get_fields(model_path_copy, rel_path)
77
- })
78
- return res
79
-
80
- def get_local_fields(self, model, path):
81
- filter_exclude = getattr(model, 'filter_exclude', [])
82
- filter_exclude.extend(self.default_exclude)
83
- fields = filter(
84
- lambda x: not x.is_relation and x.name not in filter_exclude,
85
- model._meta.get_fields()
85
+ field_params = self._get_field_params(field)
86
+ if not field.is_relation or field_params['model_name'] not in self.seen_models:
87
+ params.append(self._get_field_params(field))
88
+ has_previous = len(self.seen_models) > 1
89
+ ctx = dict(
90
+ params=sorted(params, key=lambda x: x['label'].lower()),
91
+ model_name=self.model_name,
92
+ seen_models=','.join(self.seen_models),
93
+ verbose_model_name=self.model._meta.verbose_name,
94
+ has_previous=has_previous,
86
95
  )
87
- res = []
88
- for field in fields:
89
- field_path = f'{path}{"__" if path else ""}{field.name}'
90
- self.field_paths.append(field_path)
91
- step = (hasattr(field, 'decimal_places')
92
- and self.cast_decimal_places_to_step(field.decimal_places)
93
- or 1)
94
- res.append({
95
- 'name': field_path,
96
- 'label': str(field.verbose_name),
97
- 'type': field.get_internal_type(),
98
- 'choices': field.choices or [],
99
- 'null': field.null,
100
- 'step': step
101
- })
102
- if not hasattr(model, 'get_annotations'):
103
- return res
104
- for annotation in model.get_annotations():
105
- field_path = f'{path}{"__" if path else ""}{annotation["name"]}'
106
- self.field_paths.append(field_path)
107
- res.append({
108
- 'name': field_path,
109
- 'label': str(annotation['annotation'].verbose_name),
110
- 'type': annotation['annotation'].field.__name__,
111
- 'choices': [],
112
- 'null': False,
113
- 'step': getattr(annotation['annotation'], 'step', '1')
114
- })
115
- return res
116
-
117
- def to_html(self):
118
- key = f'filter-{self.model.__module__}.{self.model.__name__}-{get_language()}'
119
- html = cache.get(key)
120
- if html:
121
- return html
122
- if not self.fields:
123
- self.fields = self.get_fields([self.model], '')
124
- html = ''
125
- for f in self.fields:
126
- html += self.field_params(f)
127
- html = {
128
- 'params': mark_safe(html.strip().replace('\n', '')),
129
- 'field_paths': mark_safe(
130
- self.field_path_selection().strip().replace('\n', '')
96
+ if has_previous:
97
+ previous_model_name = self.seen_models[-2]
98
+ previous_path = '__'.join(path.split('__')[:-1])
99
+ ctx.update(
100
+ previous_model_name=previous_model_name,
101
+ previous_seen_models=','.join(
102
+ self.seen_models[:-1] if has_previous else []
103
+ ),
104
+ previous_verbose_model_name=apps.get_model(
105
+ *previous_model_name.split('.')
106
+ )._meta.verbose_name,
107
+ previous_path=previous_path
131
108
  )
132
- }
133
- cache.set(key, html, 60 * 15)
134
- return {'params': html['params'], 'field_paths': html['field_paths']}
109
+ return render_to_string('ui/filter/query_params.html', ctx)
135
110
 
136
- def field_params(self, field):
137
- params = ''
138
- params += self.params(field)
139
- for f in field.get('fields', []):
140
- params += self.field_params(f)
141
- return f"""
142
- <div class="query-param" tabindex="-1" data-param="{field['name']}" data-param-label="{field['label']}">
143
- <p class="px-1 arrow">{field['label']}</p>
144
- <div class="query-params is-hidden" data-param="{field['name']}">
145
- {params}
146
- </div>
147
- </div>
148
- """
149
-
150
- def field_map(self):
151
- return {
152
- 'CharField': self.char_param,
153
- 'TextField': self.char_param,
154
- 'DecimalField': self.float_param,
155
- 'FloatField': self.float_param,
156
- 'BooleanField': self.bool_param,
157
- 'IntegerField': self.int_param,
158
- 'AutoField': self.int_param,
159
- 'BigAutoField': self.int_param,
160
- 'PositiveSmallIntegerField': self.int_param,
161
- 'DateTimeField': self.date_time_param,
162
- 'DateField': self.date_param,
163
- 'ForeignKey': self.foreign_key_param,
164
- 'FileField': self.file_param,
165
- 'ImageField': self.file_param
111
+ def query_input(self, lookup: str = None):
112
+ lookup = lookup or self.default_lookup
113
+ if not lookup:
114
+ ctx = {
115
+ 'verbose_lookup': str(_('Select an attribute')),
116
+ 'query_dict': self.query,
117
+ 'model_name': self.model_name
118
+ }
119
+ return render_to_string('ui/filter/query_input.html', ctx)
120
+ prefix = lookup.startswith('~') and '~' or ''
121
+ lookup = lookup.removeprefix('~')
122
+ lookup_parts = lookup.split('__')
123
+ lookup_operator = lookup_parts[-1]
124
+ rel_path = '__'.join(lookup_parts[:-2])
125
+ model, names = get_related_model(self.model, rel_path)
126
+ field = model._meta.get_field(lookup_parts[-2])
127
+ input_params = self._get_input_params(field, lookup_operator)
128
+ ctx = {
129
+ 'verbose_lookup': ' > '.join(self._get_query_tag_lhs(prefix + lookup)),
130
+ 'field': field,
131
+ 'lookup': prefix + lookup,
132
+ 'input': input_params,
133
+ 'lookup_operator': lookup_operator,
134
+ 'query_dict': self.query,
135
+ 'model_name': self.model_name
166
136
  }
137
+ return render_to_string('ui/filter/query_input.html', ctx)
167
138
 
168
- def parse_choices(self, choices):
169
- return ''.join([
170
- f'<option value="{choice[0]}">{choice[1]}</option>'
171
- for choice in choices
172
- ])
173
-
174
- def params(self, field):
175
- return self.field_map().get(field['type'], self.no_param)(field['name'], field)
176
-
177
- def param(
178
- self, key: str, value: dict, param: str, data_type: str,
179
- options: str, invert: bool = True
180
- ):
181
-
182
- def get_label(inverted=False):
183
- if param == 'exact':
184
- return self.LABEL_EXACT_NOT if inverted else self.LABEL_EXACT
185
- if param == 'icontains':
186
- return self.LABEL_ICONTAINS_NOT if inverted else self.LABEL_ICONTAINS
187
- if param == 'gte':
188
- return self.LABEL_GTE
189
- if param == 'lte':
190
- return self.LABEL_LTE
191
-
192
- def param_div(inverted=False):
193
- return f"""
194
- <div id="filter-id-{'~' if inverted else ''}{key}__{param}"
195
- class="query-param" tabindex="-1" data-type="{data_type}"
196
- data-step="{value.get('step', 1)}"
197
- data-param="{param}" data-param-label="{value.get("label")}"
198
- >
199
- <p class="px-1 arrowless">{get_label(inverted)}</p>
200
- <div class="param-options is-hidden">
201
- {options if options else ''}
202
- </div>
203
- </div>
204
- """
205
-
206
- html = param_div()
207
- if invert:
208
- html += param_div(inverted=True)
209
- return html
210
-
211
- def char_param(self, key, value):
212
- if value.get('choices'):
213
- return self.char_choice_param(key, value)
214
- options = self.parse_choices(value.get('choices', ''))
215
- html = self.param(key, value, 'icontains', 'text', options)
216
- html += self.param(key, value, 'exact', 'text', options)
217
- if value.get('null'):
218
- html += self.null_param(key, value)
219
- return html
220
-
221
- def char_choice_param(self, key, value):
222
- options = self.parse_choices(value.get('choices', []))
223
- html = self.param(key, value, 'exact', 'selection', options)
224
- if value.get('null'):
225
- html += self.null_param(key, value)
226
- return html
227
-
228
- def float_param(self, key, value):
229
- options = self.parse_choices(value.get('choices', ''))
230
- html = self.param(key, value, 'exact', 'number', options)
231
- html += self.param(key, value, 'gte', 'number', options, False)
232
- html += self.param(key, value, 'lte', 'number', options, False)
233
- if value.get('null'):
234
- html += self.null_param(key, value)
235
- return html
139
+ def query_tags(self, query: list | dict = None, operator: str = None):
140
+ if not query:
141
+ query = json.loads(self.query.get('q', '[]'))
142
+ if isinstance(query, dict):
143
+ query = [query]
144
+ operator = operator or '&'
145
+ html = '<div class="query-group-container">'
146
+ html += render_to_string(
147
+ 'ui/filter/query_operator.html', {'operator': operator}
148
+ )
149
+ html += (
150
+ '<div class="query-group" '
151
+ 'x-sort="applyQuery();" x-sort:group="query" '
152
+ 'x-sort:config="{preventOnFilter: false}">'
153
+ )
154
+ for idx, item in enumerate(query):
155
+ if isinstance(item, str):
156
+ continue
157
+ operator = '&'
158
+ if idx > 0 and isinstance(query[idx - 1], str):
159
+ operator = query[idx - 1]
160
+ if isinstance(item, list):
161
+ html += self.query_tags(item, operator)
162
+ if isinstance(item, dict):
163
+ item_ctx = self._get_query_tag_context(item, operator)
164
+ html += render_to_string('ui/filter/query_tags.html', item_ctx)
165
+ html += '</div></div>'
166
+ return mark_safe(html)
236
167
 
237
- def bool_param(self, key, value):
238
- options = self.parse_choices([('true', _('True')), ('false', _('False'))])
239
- html = self.param(key, value, 'exact', 'selection', options)
240
- if value.get('null'):
241
- html += self.null_param(key, value)
242
- return html
168
+ def _get_input_params(self, field: Field, lookup_operator: str):
169
+ field_type = field.get_internal_type()
170
+ params = {
171
+ 'field_type': field_type,
172
+ 'data_type': self._internal_type_to_data_type(field_type, lookup_operator)
173
+ }
174
+ if lookup_operator == 'isnull':
175
+ params.update(
176
+ input_type='select',
177
+ choices=[('false', str(_('True'))), ('true', str(_('False')))],
178
+ data_type='bool'
179
+ )
180
+ return params
181
+ if field_type in self.TYPES_BOOL:
182
+ params.update(
183
+ input_type='select',
184
+ choices=[('true', str(_('True'))), ('false', str(_('False')))],
185
+ data_type='bool'
186
+ )
187
+ return params
188
+ elif field_type in self.TYPES_INTEGER:
189
+ if field.choices:
190
+ params.update(input_type='select', choices=field.choices)
191
+ else:
192
+ params.update(input_type='number', step=1)
193
+ elif field_type in self.TYPES_NUMBER:
194
+ step = (
195
+ hasattr(field, 'decimal_places')
196
+ and self._cast_decimal_places_to_step(field.decimal_places)
197
+ or 1
198
+ )
199
+ params.update(input_type='number', step=step)
200
+ elif field_type in self.TYPES_CHAR:
201
+ if field.choices:
202
+ params.update(input_type='select', choices=field.choices)
203
+ else:
204
+ params.update(input_type='text')
205
+ elif field_type in self.TYPES_DATETIME:
206
+ params.update(input_type='datetime-local', format=self.DATE_FORMAT)
207
+ elif field_type in self.TYPES_DATE:
208
+ params.update(input_type='date', format=self.DATE_FORMAT)
209
+ elif field_type in self.TYPES_TIME:
210
+ params.update(input_type='time')
211
+ return params
243
212
 
244
- def int_param(self, key, value):
245
- options = self.parse_choices(value.get('choices', ''))
246
- html = self.param(key, value, 'exact', 'number', options)
247
- html += self.param(key, value, 'gte', 'number', options, False)
248
- html += self.param(key, value, 'lte', 'number', options, False)
249
- if value.get('null'):
250
- html += self.null_param(key, value)
251
- return html
213
+ def _internal_type_to_data_type(self, internal_type, lookup: str):
214
+ if lookup == 'isnull' or internal_type in self.TYPES_BOOL:
215
+ return 'bool'
216
+ if internal_type in self.TYPES_INTEGER + self.TYPES_NUMBER:
217
+ return 'number'
218
+ return 'text'
252
219
 
253
- def date_time_param(self, key, value):
254
- options = self.parse_choices(value.get('choices', ''))
255
- html = self.param(key, value, 'exact', 'datetime-local', options)
256
- html += self.param(key, value, 'gte', 'datetime-local', options, False)
257
- html += self.param(key, value, 'lte', 'datetime-local', options, False)
258
- if value.get('null'):
259
- html += self.null_param(key, value)
260
- return html
220
+ def _get_query_tag_context(self, data: dict, operator: str):
221
+ ctx = {'tags': []}
222
+ first_operator = operator
223
+ operator = '&'
224
+ for key, value in data.items():
261
225
 
262
- def date_param(self, key, value):
263
- options = self.parse_choices(value.get('choices', ''))
264
- html = self.param(key, value, 'exact', 'date', options)
265
- html += self.param(key, value, 'gte', 'date', options, False)
266
- html += self.param(key, value, 'lte', 'date', options, False)
267
- if value.get('null'):
268
- html += self.null_param(key, value)
269
- return html
226
+ field_path = key.split('__')
227
+ field = get_related_field(
228
+ self.model, '__'.join(field_path[:-1]).removeprefix('~')
229
+ )
230
+ lookup = field_path[-1]
231
+ internal_type = field.get_internal_type()
232
+ data_type = self._internal_type_to_data_type(internal_type, lookup)
233
+ ctx['tags'].append({
234
+ 'lhs': self._get_query_tag_lhs(key),
235
+ 'rhs': self._get_query_tag_rhs(value, internal_type, lookup),
236
+ 'lookup': key,
237
+ 'value': value,
238
+ 'operator': operator,
239
+ 'data_type': data_type,
240
+ 'model_name': self.model_name
241
+ })
242
+ if ctx['tags']:
243
+ ctx['tags'][0]['operator'] = first_operator
244
+ return ctx
270
245
 
271
- def foreign_key_param(self, key, value):
272
- if value.get('null'):
273
- return self.null_param(key, value)
274
- return ''
246
+ def _get_query_tag_lhs(self, lookup: str):
247
+ prefix = lookup.startswith('~') and '~' or ''
248
+ lookup = lookup.removeprefix('~')
249
+ parts = lookup.split('__')
250
+ assert len(parts) >= 2
251
+ related_parts = parts[:-2]
252
+ rel_model, names = get_related_model(
253
+ self.model, '__'.join(related_parts)
254
+ )
255
+ try:
256
+ names.extend([
257
+ str(rel_model._meta.get_field(parts[-2]).verbose_name),
258
+ str(self._get_lookup_label(f'{prefix}{parts[-1]}'))
259
+ ])
260
+ except AttributeError:
261
+ names.extend([
262
+ str(rel_model._meta.get_field(parts[-2]).field.verbose_name),
263
+ str(self._get_lookup_label(f'{prefix}{parts[-1]}'))
264
+ ])
265
+ return names[1:]
275
266
 
276
- def file_param(self, key, value):
277
- return self.null_param(key, value)
267
+ def _get_query_tag_rhs(self, value, internal_type, lookup):
268
+ if lookup == 'isnull':
269
+ return not value
270
+ if internal_type in self.TYPES_DATE:
271
+ return datetime.date.fromisoformat(value)
272
+ if internal_type in self.TYPES_DATETIME:
273
+ return datetime.datetime.fromisoformat(value)
274
+ if internal_type in self.TYPES_NUMBER:
275
+ return Decimal(value)
276
+ return value
278
277
 
279
- def null_param(self, key, value):
280
- options = self.parse_choices([
281
- ('false', _('True')),
282
- ('true', _('False'))
283
- ])
284
- return f"""
285
- <div id="filter-id-{key}__isnull"
286
- class="query-param" tabindex="-1" data-type="selection"
287
- data-param-invert="false"
288
- data-param="isnull" data-param-label="{value.get("label")}"
289
- >
290
- <p class="px-1 arrowless">{self.LABEL_SET}</p>
291
- <div class="param-options is-hidden">
292
- {options}
293
- </div>
294
- </div>
295
- """
278
+ def _get_lookup_label(self, lookup: str):
279
+ labels = {
280
+ 'exact': self.LABEL_EXACT,
281
+ '~exact': self.LABEL_EXACT_NOT,
282
+ 'icontains': self.LABEL_ICONTAINS,
283
+ '~icontains': self.LABEL_ICONTAINS_NOT,
284
+ 'gte': self.LABEL_GTE,
285
+ 'lte': self.LABEL_LTE,
286
+ 'isnull': self.LABEL_SET,
287
+ '~isnull': self.LABEL_NOT_SET
288
+ }
289
+ return str(labels[lookup])
296
290
 
297
- def no_param(self, key, value):
298
- return ''
291
+ def _get_field_params(self, field):
292
+ path = self.query.get('path', '')
293
+ label = ''
294
+ name = field.name
295
+ if path:
296
+ field_path = f'{path}__{name}'
297
+ else:
298
+ field_path = name
299
+ field_type = field.get_internal_type()
300
+ if field.concrete or not field.is_relation:
301
+ label = str(field.verbose_name)
302
+ if field.is_relation:
303
+ choices = None
304
+ step = None
305
+ model = ''
306
+ if field_type in ['ForeignKey', 'OneToOneField']:
307
+ model = (
308
+ f'{field.related_model._meta.app_label}.'
309
+ f'{field.related_model._meta.model_name}'
310
+ )
311
+ if not field.concrete:
312
+ label = str(
313
+ field.related_model._meta.verbose_name_plural
314
+ )
315
+ elif field_type == 'ManyToManyField':
316
+ model = (
317
+ f'{field.remote_field.model._meta.app_label}.'
318
+ f'{field.remote_field.model._meta.model_name}'
319
+ )
320
+ if not field.concrete:
321
+ label = (
322
+ f'{field.remote_field.model._meta.verbose_name} / '
323
+ f'{field.remote_field.verbose_name}'
324
+ )
325
+ else:
326
+ step = (
327
+ hasattr(field, 'decimal_places')
328
+ and self._cast_decimal_places_to_step(field.decimal_places)
329
+ or 1
330
+ )
331
+ choices = field.choices or None
332
+ model = self.model_name
333
+ return {
334
+ 'field': field,
335
+ 'name': name,
336
+ 'field_path': field_path,
337
+ 'label': label,
338
+ 'type': field_type,
339
+ 'param_type': '',
340
+ 'choices': choices,
341
+ 'null': field.null,
342
+ 'step': step,
343
+ 'is_relation': field.is_relation,
344
+ 'model_name': model,
345
+ }
299
346
 
300
- def field_path_selection(self):
301
- html = ''
302
- filter_exclude = getattr(self.model, 'filter_exclude', [])
303
- filter_exclude.extend(self.default_exclude)
304
- fields = [(x.verbose_name, x.name) for x in filter(
305
- lambda x:
306
- x.name not in filter_exclude
307
- and not isinstance(x, (ManyToOneRel, ManyToManyRel)),
308
- self.model._meta.get_fields()
309
- )]
310
- if hasattr(self.model, 'get_annotations'):
311
- fields.extend([
312
- (x['annotation'].verbose_name, x['name'])
313
- for x in self.model.get_annotations()
314
- ])
315
- sorted_fields = sorted(fields, key=lambda x: x[0].lower())
316
- for field in sorted_fields:
317
- html += f"""
318
- <label class="checkbox is-unselectable my-1" style="width: 100%">
319
- <input type="checkbox" name="{field[1]}" data-label="{field[0].lower()}">
320
- <span>{field[0]}</span>
321
- </label>
322
- """
323
- return html
347
+ @staticmethod
348
+ def _cast_decimal_places_to_step(decimal_places):
349
+ if not decimal_places or decimal_places < 1:
350
+ return '1'
351
+ zero_count = decimal_places - 1
352
+ return f'0.{"0" * zero_count}1'