accrete 0.0.103__py3-none-any.whl → 0.0.105__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 (251) hide show
  1. accrete/contrib/ui/__init__.py +6 -29
  2. accrete/contrib/ui/context.py +26 -294
  3. accrete/contrib/ui/filter.py +316 -324
  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 -19997
  183. accrete/contrib/ui/static/css/accrete.css.map +1 -1
  184. accrete/contrib/ui/static/css/accrete.scss +185 -462
  185. accrete/contrib/ui/static/js/filter.js +97 -679
  186. accrete/contrib/ui/static/js/htmx.min.js +1 -1
  187. accrete/contrib/ui/templates/django/forms/widgets/date.html +5 -9
  188. accrete/contrib/ui/templates/django/forms/widgets/select.html +7 -5
  189. accrete/contrib/ui/templates/ui/content_right.html +7 -0
  190. accrete/contrib/ui/templates/ui/filter/filter.html +27 -0
  191. accrete/contrib/ui/templates/ui/filter/query_input.html +31 -0
  192. accrete/contrib/ui/templates/ui/filter/query_operator.html +11 -0
  193. accrete/contrib/ui/templates/ui/filter/query_params.html +106 -0
  194. accrete/contrib/ui/templates/ui/filter/query_tags.html +26 -0
  195. accrete/contrib/ui/templates/ui/layout.html +162 -233
  196. accrete/contrib/ui/templates/ui/list.html +39 -28
  197. accrete/contrib/ui/templates/ui/list_update.html +3 -0
  198. accrete/contrib/ui/templates/ui/message.html +13 -0
  199. accrete/contrib/ui/templates/ui/{partials/modal.html → modal.html} +9 -5
  200. accrete/contrib/ui/templates/ui/oob.html +3 -0
  201. accrete/contrib/ui/templates/ui/table.html +67 -71
  202. accrete/contrib/ui/templates/ui/table_row_update.html +14 -0
  203. accrete/contrib/ui/templates/ui/widgets/model_search_select.html +2 -2
  204. accrete/contrib/ui/templates/ui/widgets/model_search_select_multi.html +25 -14
  205. accrete/contrib/ui/templates/ui/widgets/model_search_select_options.html +1 -1
  206. accrete/contrib/ui/templatetags/{accrete_ui.py → ui.py} +68 -56
  207. accrete/contrib/ui/urls.py +3 -1
  208. accrete/contrib/ui/views.py +33 -3
  209. accrete/contrib/ui/widgets/__init__.py +1 -0
  210. accrete/contrib/ui/{forms/widgets.py → widgets/search_select.py} +7 -2
  211. accrete/contrib/user/templates/user/login.html +71 -23
  212. accrete/forms.py +0 -2
  213. accrete/managers.py +4 -4
  214. accrete/middleware.py +43 -66
  215. accrete/models.py +7 -1
  216. accrete/storage.py +4 -1
  217. accrete/tenant.py +9 -4
  218. accrete/utils/__init__.py +2 -0
  219. accrete/utils/models.py +9 -1
  220. accrete/utils/views.py +36 -20
  221. accrete/views.py +9 -5
  222. {accrete-0.0.103.dist-info → accrete-0.0.105.dist-info}/METADATA +2 -2
  223. accrete-0.0.105.dist-info/RECORD +370 -0
  224. {accrete-0.0.103.dist-info → accrete-0.0.105.dist-info}/WHEEL +1 -1
  225. accrete/contrib/ui/components/__init__.py +0 -1
  226. accrete/contrib/ui/components/search_select.py +0 -18
  227. accrete/contrib/ui/elements.py +0 -95
  228. accrete/contrib/ui/forms/__init__.py +0 -0
  229. accrete/contrib/ui/static/bulma/css/versions/bulma-no-dark-mode.min.css.map +0 -1
  230. accrete/contrib/ui/static/bulma/css/versions/bulma-no-helpers-prefixed.min.css.map +0 -1
  231. accrete/contrib/ui/static/bulma/css/versions/bulma-no-helpers.min.css.map +0 -1
  232. accrete/contrib/ui/static/bulma/sass/grid/columns-v2.scss +0 -957
  233. accrete/contrib/ui/static/js/ui.js +0 -30
  234. accrete/contrib/ui/templates/ui/dashboard.html +0 -7
  235. accrete/contrib/ui/templates/ui/detail.html +0 -19
  236. accrete/contrib/ui/templates/ui/form.html +0 -21
  237. accrete/contrib/ui/templates/ui/partials/filter.html +0 -146
  238. accrete/contrib/ui/templates/ui/partials/form_errors.html +0 -34
  239. accrete/contrib/ui/templates/ui/partials/header.html +0 -123
  240. accrete/contrib/ui/templates/ui/partials/modal_form.html +0 -46
  241. accrete/contrib/ui/templates/ui/partials/onchange_form.html +0 -1
  242. accrete/contrib/ui/templates/ui/partials/pagination_detail.html +0 -23
  243. accrete/contrib/ui/templates/ui/partials/pagination_list.html +0 -28
  244. accrete/contrib/ui/templates/ui/partials/table_field.html +0 -16
  245. accrete/contrib/ui/templates/ui/partials/table_field_float.html +0 -1
  246. accrete/contrib/ui/templates/ui/partials/table_field_monetary.html +0 -4
  247. accrete/contrib/ui/templates/ui/partials/table_field_string.html +0 -1
  248. accrete/contrib/ui/templates/ui/widgets/model_search_select_multi_selected_options.html +0 -3
  249. accrete/contrib/ui/templates/ui/widgets/model_search_select_multi_tags.html +0 -6
  250. accrete-0.0.103.dist-info/RECORD +0 -241
  251. {accrete-0.0.103.dist-info → accrete-0.0.105.dist-info}/licenses/LICENSE +0 -0
@@ -1,19 +1,24 @@
1
+ import json
1
2
  import logging
2
- from collections import Counter
3
- from itertools import tee
4
- from django.db.models import fields as db_fields
5
- 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
6
10
  from django.utils.translation import gettext_lazy as _
7
11
  from django.utils.safestring import mark_safe
8
- 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
9
16
 
10
17
  _logger = logging.getLogger(__name__)
11
18
 
12
19
 
13
20
  class Filter:
14
21
 
15
- query_relation_depth = 4
16
-
17
22
  LABEL_EXACT = _('Equals')
18
23
  LABEL_EXACT_NOT = _('Equals Not')
19
24
  LABEL_ICONTAINS = _('Contains')
@@ -24,337 +29,324 @@ class Filter:
24
29
  LABEL_FALSE = _('False')
25
30
  LABEL_SET = _('Is Set')
26
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']
27
49
 
28
50
  def __init__(
29
- self, model, query_relation_depth: int = 4,
30
- default_exclude: list[str] = None, default_filter_term: str = None
51
+ self, model, query: QueryDict, default_lookup: str = None
31
52
  ):
32
53
  self.model = model
33
- self.query_relation_depth = query_relation_depth
34
- if default_exclude is None:
35
- default_exclude = ['tenant', 'user']
36
- self.default_exclude = default_exclude
37
- self.default_filter_term = default_filter_term or ''
38
- self.fields = []
39
- self.field_paths = []
40
-
41
- @staticmethod
42
- def cast_decimal_places_to_step(decimal_places):
43
- if not decimal_places or decimal_places < 1:
44
- return '1'
45
- zero_count = decimal_places - 1
46
- return f'0.{"0" * zero_count}1'
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())
65
+
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
+ )
47
73
 
48
- def get_fields(self, model_path: list, field_path: str):
49
- fields = self.get_local_fields(model_path[-1], field_path)
50
- if len(model_path) <= self.query_relation_depth:
51
- fields.extend(self.get_relation_fields(model_path, field_path))
52
- return sorted(fields, key=lambda x: x['label'].lower())
74
+ def get_queryset(self) -> QuerySet:
75
+ return filter_from_querystring(self.model, self.query)
53
76
 
54
- def get_relation_fields(self, model_path, field_path):
55
- filter_exclude = getattr(model_path[-1], 'filter_exclude', [])
56
- filter_exclude.extend(self.default_exclude)
77
+ def query_params(self):
57
78
  fields = filter(
58
- lambda x: x.is_relation and x.name not in filter_exclude,
59
- model_path[-1]._meta.get_fields()
79
+ lambda x: x.name not in self.exclude,
80
+ self.model._meta.get_fields()
60
81
  )
61
- fields, fields_counter = tee(fields)
62
- res = []
63
- occurrences = Counter([
64
- f.related_model for f in filter(
65
- lambda x: isinstance(
66
- x, db_fields.reverse_related.ManyToOneRel
67
- ), fields_counter
68
- )
69
- ])
70
- multi_related_models = set([
71
- key for key, val in occurrences.items() if val > 1
72
- ])
82
+ params = []
83
+ path = self.query.get('path', '')
73
84
  for field in fields:
74
- multi_ref = field.related_model in multi_related_models
75
- if field.related_model in model_path:
76
- continue
77
- rel_path = f'{field_path}{"__" if field_path else ""}{field.name}'
78
- model_path_copy = model_path.copy()
79
- model_path_copy.append(field.related_model)
80
- if isinstance(field, db_fields.reverse_related.ManyToManyRel):
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,
95
+ )
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
108
+ )
109
+ return render_to_string('ui/filter/query_params.html', ctx)
110
+
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
136
+ }
137
+ return render_to_string('ui/filter/query_input.html', ctx)
138
+
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):
81
156
  continue
82
- elif isinstance(field, db_fields.reverse_related.ManyToOneRel):
83
- label = field.related_model._meta.verbose_name_plural
84
- if multi_ref:
85
- label = f'{label}/{field.field.verbose_name}'
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)
167
+
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)
86
203
  else:
87
- label = field.verbose_name
88
- res.append({
89
- 'name': f'{rel_path}',
90
- 'label': str(label),
91
- 'type': field.get_internal_type(),
92
- 'null': field.null,
93
- 'choices': [],
94
- 'fields': self.get_fields(model_path_copy, rel_path)
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
212
+
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'
219
+
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():
225
+
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
95
241
  })
96
- return res
97
-
98
- def get_local_fields(self, model, path):
99
- filter_exclude = getattr(model, 'filter_exclude', [])
100
- filter_exclude.extend(self.default_exclude)
101
- fields = filter(
102
- lambda x: not x.is_relation and x.name not in filter_exclude,
103
- model._meta.get_fields()
242
+ if ctx['tags']:
243
+ ctx['tags'][0]['operator'] = first_operator
244
+ return ctx
245
+
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)
104
254
  )
105
- res = []
106
- for field in fields:
107
- field_path = f'{path}{"__" if path else ""}{field.name}'
108
- self.field_paths.append(field_path)
109
- step = (hasattr(field, 'decimal_places')
110
- and self.cast_decimal_places_to_step(field.decimal_places)
111
- or 1)
112
- res.append({
113
- 'name': field_path,
114
- 'label': str(field.verbose_name),
115
- 'type': field.get_internal_type(),
116
- 'choices': field.choices or [],
117
- 'null': field.null,
118
- 'step': step
119
- })
120
- if not hasattr(model, 'get_annotations'):
121
- return res
122
- for annotation in model.get_annotations():
123
- field_path = f'{path}{"__" if path else ""}{annotation["name"]}'
124
- self.field_paths.append(field_path)
125
- res.append({
126
- 'name': field_path,
127
- 'label': str(annotation['annotation'].verbose_name),
128
- 'type': annotation['annotation'].field.__name__,
129
- 'choices': [],
130
- 'null': False,
131
- 'step': getattr(annotation['annotation'], 'step', '1')
132
- })
133
- return res
134
-
135
- def to_html(self):
136
- key = f'filter-{self.model.__module__}.{self.model.__name__}-{get_language()}'
137
- html = cache.get(key)
138
- if html:
139
- return html
140
- if not self.fields:
141
- self.fields = self.get_fields([self.model], '')
142
- html = ''
143
- for f in self.fields:
144
- html += self.field_params(f)
145
- html = {
146
- 'params': mark_safe(html.strip().replace('\n', '')),
147
- 'field_paths': mark_safe(
148
- self.field_path_selection().strip().replace('\n', '')
149
- )
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:]
266
+
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
277
+
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
150
288
  }
151
- cache.set(key, html, 60 * 15)
152
- return {'params': html['params'], 'field_paths': html['field_paths']}
153
-
154
- def field_params(self, field):
155
- params = ''
156
- params += self.params(field)
157
- for f in field.get('fields', []):
158
- params += self.field_params(f)
159
- return f"""
160
- <div class="query-param" tabindex="-1" data-param="{field['name']}"
161
- data-param-label="{field['label']}"
162
- >
163
- <p class="px-1 arrow">{field['label']}</p>
164
- <div class="query-params is-hidden" data-param="{field['name']}">
165
- {params}
166
- </div>
167
- </div>
168
- """
169
-
170
- def field_map(self):
289
+ return str(labels[lookup])
290
+
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
171
333
  return {
172
- 'CharField': self.char_param,
173
- 'TextField': self.char_param,
174
- 'DecimalField': self.float_param,
175
- 'FloatField': self.float_param,
176
- 'BooleanField': self.bool_param,
177
- 'IntegerField': self.int_param,
178
- 'AutoField': self.int_param,
179
- 'BigAutoField': self.int_param,
180
- 'PositiveSmallIntegerField': self.int_param,
181
- 'DateTimeField': self.date_time_param,
182
- 'DateField': self.date_param,
183
- 'TimeField': self.time_param,
184
- 'ForeignKey': self.foreign_key_param,
185
- 'ManyToManyField': self.many_to_many_param,
186
- 'FileField': self.file_param,
187
- 'ImageField': self.file_param
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,
188
345
  }
189
346
 
190
- def parse_choices(self, choices):
191
- return ''.join([
192
- f'<option value="{choice[0]}">{choice[1]}</option>'
193
- for choice in choices
194
- ])
195
-
196
- def params(self, field):
197
- return self.field_map().get(field['type'], self.no_param)(field['name'], field)
198
-
199
- def param(
200
- self, key: str, value: dict, param: str, data_type: str,
201
- options: str, invert: bool = True
202
- ):
203
-
204
- def get_label(inverted=False):
205
- if param == 'exact':
206
- return self.LABEL_EXACT_NOT if inverted else self.LABEL_EXACT
207
- if param == 'icontains':
208
- return self.LABEL_ICONTAINS_NOT if inverted else self.LABEL_ICONTAINS
209
- if param == 'gte':
210
- return self.LABEL_GTE
211
- if param == 'lte':
212
- return self.LABEL_LTE
213
-
214
- def param_div(inverted=False):
215
- return f"""
216
- <div id="filter-id-{'~' if inverted else ''}{key}__{param}"
217
- class="query-param" tabindex="-1" data-type="{data_type}"
218
- data-step="{value.get('step', 1)}"
219
- data-param="{param}" data-param-label="{value.get("label")}"
220
- >
221
- <p class="px-1 arrowless">{get_label(inverted)}</p>
222
- <div class="param-options is-hidden">
223
- {options if options else ''}
224
- </div>
225
- </div>
226
- """
227
-
228
- html = param_div()
229
- if invert:
230
- html += param_div(inverted=True)
231
- return html
232
-
233
- def char_param(self, key, value):
234
- if value.get('choices'):
235
- return self.char_choice_param(key, value)
236
- options = self.parse_choices(value.get('choices', ''))
237
- html = self.param(key, value, 'icontains', 'text', options)
238
- html += self.param(key, value, 'exact', 'text', options)
239
- if value.get('null'):
240
- html += self.null_param(key, value)
241
- return html
242
-
243
- def char_choice_param(self, key, value):
244
- options = self.parse_choices(value.get('choices', []))
245
- html = self.param(key, value, 'exact', 'selection', options)
246
- if value.get('null'):
247
- html += self.null_param(key, value)
248
- return html
249
-
250
- def float_param(self, key, value):
251
- options = self.parse_choices(value.get('choices', ''))
252
- html = self.param(key, value, 'exact', 'number', options)
253
- html += self.param(key, value, 'gte', 'number', options, False)
254
- html += self.param(key, value, 'lte', 'number', options, False)
255
- if value.get('null'):
256
- html += self.null_param(key, value)
257
- return html
258
-
259
- def bool_param(self, key, value):
260
- options = self.parse_choices([('true', _('True')), ('false', _('False'))])
261
- html = self.param(key, value, 'exact', 'selection', options)
262
- if value.get('null'):
263
- html += self.null_param(key, value)
264
- return html
265
-
266
- def int_param(self, key, value):
267
- options = self.parse_choices(value.get('choices', ''))
268
- html = self.param(key, value, 'exact', 'number', options)
269
- html += self.param(key, value, 'gte', 'number', options, False)
270
- html += self.param(key, value, 'lte', 'number', options, False)
271
- if value.get('null'):
272
- html += self.null_param(key, value)
273
- return html
274
-
275
- def date_time_param(self, key, value):
276
- options = self.parse_choices(value.get('choices', ''))
277
- html = self.param(key, value, 'exact', 'datetime-local', options)
278
- html += self.param(key, value, 'gte', 'datetime-local', options, False)
279
- html += self.param(key, value, 'lte', 'datetime-local', options, False)
280
- if value.get('null'):
281
- html += self.null_param(key, value)
282
- return html
283
-
284
- def date_param(self, key, value):
285
- options = self.parse_choices(value.get('choices', ''))
286
- html = self.param(key, value, 'exact', 'date', options)
287
- html += self.param(key, value, 'gte', 'date', options, False)
288
- html += self.param(key, value, 'lte', 'date', options, False)
289
- if value.get('null'):
290
- html += self.null_param(key, value)
291
- return html
292
-
293
- def time_param(self, key, value):
294
- options = self.parse_choices(value.get('choices', ''))
295
- html = self.param(key, value, 'exact', 'time', options)
296
- html += self.param(key, value, 'gte', 'time', options, False)
297
- html += self.param(key, value, 'lte', 'time', options, False)
298
- if value.get('null'):
299
- html += self.null_param(key, value)
300
- return html
301
-
302
- def foreign_key_param(self, key, value):
303
- if value.get('null'):
304
- return self.null_param(key, value)
305
- return ''
306
-
307
- def many_to_many_param(self, key, value):
308
- return self.null_param(key, value)
309
-
310
- def file_param(self, key, value):
311
- return self.null_param(key, value)
312
-
313
- def null_param(self, key, value):
314
- options = self.parse_choices([
315
- ('false', _('True')),
316
- ('true', _('False'))
317
- ])
318
- return f"""
319
- <div id="filter-id-{key}__isnull"
320
- class="query-param" tabindex="-1" data-type="selection"
321
- data-param-invert="false"
322
- data-param="isnull" data-param-label="{value.get("label")}"
323
- >
324
- <p class="px-1 arrowless">{self.LABEL_SET}</p>
325
- <div class="param-options is-hidden">
326
- {options}
327
- </div>
328
- </div>
329
- """
330
-
331
- def no_param(self, key, value):
332
- return ''
333
-
334
- def field_path_selection(self):
335
- html = ''
336
- filter_exclude = getattr(self.model, 'filter_exclude', [])
337
- filter_exclude.extend(self.default_exclude)
338
- fields = [(x.verbose_name, x.name) for x in filter(
339
- lambda x:
340
- x.name not in filter_exclude
341
- and not isinstance(x, (
342
- db_fields.related.ManyToOneRel,
343
- db_fields.related.ManyToManyRel
344
- )),
345
- self.model._meta.get_fields()
346
- )]
347
- if hasattr(self.model, 'get_annotations'):
348
- fields.extend([
349
- (x['annotation'].verbose_name, x['name'])
350
- for x in self.model.get_annotations()
351
- ])
352
- sorted_fields = sorted(fields, key=lambda x: x[0].lower())
353
- for field in sorted_fields:
354
- html += f"""
355
- <label class="checkbox is-unselectable my-1" style="width: 100%">
356
- <input type="checkbox" name="{field[1]}" data-label="{field[0].lower()}">
357
- <span>{field[0]}</span>
358
- </label>
359
- """
360
- 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'