accrete 0.0.10__py3-none-any.whl → 0.0.23__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 (114) hide show
  1. accrete/contrib/sequence/forms.py +2 -2
  2. accrete/contrib/sequence/queries.py +3 -4
  3. accrete/contrib/ui/__init__.py +11 -5
  4. accrete/contrib/ui/components.py +49 -0
  5. accrete/contrib/ui/context.py +284 -0
  6. accrete/contrib/ui/filter.py +22 -17
  7. accrete/contrib/ui/querystring.py +93 -0
  8. accrete/contrib/ui/static/bulma/LICENSE +21 -0
  9. accrete/contrib/ui/static/bulma/README.md +139 -0
  10. accrete/contrib/ui/static/bulma/bulma.sass +10 -0
  11. accrete/contrib/ui/static/bulma/css/bulma-rtl.css +11851 -0
  12. accrete/contrib/ui/static/bulma/css/bulma-rtl.css.map +1 -0
  13. accrete/contrib/ui/static/bulma/css/bulma-rtl.min.css +1 -0
  14. accrete/contrib/ui/static/bulma/css/bulma.css +11851 -0
  15. accrete/contrib/ui/static/bulma/css/bulma.css.map +1 -0
  16. accrete/contrib/ui/static/bulma/css/bulma.min.css +1 -0
  17. accrete/contrib/ui/static/bulma/package.json +56 -0
  18. accrete/contrib/ui/static/bulma/sass/base/_all.sass +6 -0
  19. accrete/contrib/ui/static/bulma/sass/base/animations.sass +5 -0
  20. accrete/contrib/ui/static/bulma/sass/base/generic.sass +145 -0
  21. accrete/contrib/ui/static/bulma/sass/base/helpers.sass +1 -0
  22. accrete/contrib/ui/static/bulma/sass/base/minireset.sass +79 -0
  23. accrete/contrib/ui/static/bulma/sass/components/_all.sass +15 -0
  24. accrete/contrib/ui/static/bulma/sass/components/breadcrumb.sass +77 -0
  25. accrete/contrib/ui/static/bulma/sass/components/card.sass +103 -0
  26. accrete/contrib/ui/static/bulma/sass/components/dropdown.sass +83 -0
  27. accrete/contrib/ui/static/bulma/sass/components/level.sass +79 -0
  28. accrete/contrib/ui/static/bulma/sass/components/media.sass +59 -0
  29. accrete/contrib/ui/static/bulma/sass/components/menu.sass +59 -0
  30. accrete/contrib/ui/static/bulma/sass/components/message.sass +101 -0
  31. accrete/contrib/ui/static/bulma/sass/components/modal.sass +117 -0
  32. accrete/contrib/ui/static/bulma/sass/components/navbar.sass +446 -0
  33. accrete/contrib/ui/static/bulma/sass/components/pagination.sass +167 -0
  34. accrete/contrib/ui/static/bulma/sass/components/panel.sass +121 -0
  35. accrete/contrib/ui/static/bulma/sass/components/tabs.sass +176 -0
  36. accrete/contrib/ui/static/bulma/sass/elements/_all.sass +16 -0
  37. accrete/contrib/ui/static/bulma/sass/elements/box.sass +26 -0
  38. accrete/contrib/ui/static/bulma/sass/elements/button.sass +357 -0
  39. accrete/contrib/ui/static/bulma/sass/elements/container.sass +29 -0
  40. accrete/contrib/ui/static/bulma/sass/elements/content.sass +162 -0
  41. accrete/contrib/ui/static/bulma/sass/elements/form.sass +1 -0
  42. accrete/contrib/ui/static/bulma/sass/elements/icon.sass +46 -0
  43. accrete/contrib/ui/static/bulma/sass/elements/image.sass +73 -0
  44. accrete/contrib/ui/static/bulma/sass/elements/notification.sass +52 -0
  45. accrete/contrib/ui/static/bulma/sass/elements/other.sass +31 -0
  46. accrete/contrib/ui/static/bulma/sass/elements/progress.sass +73 -0
  47. accrete/contrib/ui/static/bulma/sass/elements/table.sass +134 -0
  48. accrete/contrib/ui/static/bulma/sass/elements/tag.sass +140 -0
  49. accrete/contrib/ui/static/bulma/sass/elements/title.sass +70 -0
  50. accrete/contrib/ui/static/bulma/sass/form/_all.sass +9 -0
  51. accrete/contrib/ui/static/bulma/sass/form/checkbox-radio.sass +22 -0
  52. accrete/contrib/ui/static/bulma/sass/form/file.sass +184 -0
  53. accrete/contrib/ui/static/bulma/sass/form/input-textarea.sass +66 -0
  54. accrete/contrib/ui/static/bulma/sass/form/select.sass +88 -0
  55. accrete/contrib/ui/static/bulma/sass/form/shared.sass +60 -0
  56. accrete/contrib/ui/static/bulma/sass/form/tools.sass +215 -0
  57. accrete/contrib/ui/static/bulma/sass/grid/_all.sass +5 -0
  58. accrete/contrib/ui/static/bulma/sass/grid/columns.sass +513 -0
  59. accrete/contrib/ui/static/bulma/sass/grid/tiles.sass +36 -0
  60. accrete/contrib/ui/static/bulma/sass/helpers/_all.sass +12 -0
  61. accrete/contrib/ui/static/bulma/sass/helpers/color.sass +39 -0
  62. accrete/contrib/ui/static/bulma/sass/helpers/flexbox.sass +35 -0
  63. accrete/contrib/ui/static/bulma/sass/helpers/float.sass +10 -0
  64. accrete/contrib/ui/static/bulma/sass/helpers/other.sass +14 -0
  65. accrete/contrib/ui/static/bulma/sass/helpers/overflow.sass +2 -0
  66. accrete/contrib/ui/static/bulma/sass/helpers/position.sass +7 -0
  67. accrete/contrib/ui/static/bulma/sass/helpers/spacing.sass +31 -0
  68. accrete/contrib/ui/static/bulma/sass/helpers/typography.sass +103 -0
  69. accrete/contrib/ui/static/bulma/sass/helpers/visibility.sass +122 -0
  70. accrete/contrib/ui/static/bulma/sass/layout/_all.sass +6 -0
  71. accrete/contrib/ui/static/bulma/sass/layout/footer.sass +11 -0
  72. accrete/contrib/ui/static/bulma/sass/layout/hero.sass +153 -0
  73. accrete/contrib/ui/static/bulma/sass/layout/section.sass +17 -0
  74. accrete/contrib/ui/static/bulma/sass/utilities/_all.sass +9 -0
  75. accrete/contrib/ui/static/bulma/sass/utilities/animations.sass +1 -0
  76. accrete/contrib/ui/static/bulma/sass/utilities/controls.sass +49 -0
  77. accrete/contrib/ui/static/bulma/sass/utilities/derived-variables.sass +114 -0
  78. accrete/contrib/ui/static/bulma/sass/utilities/extends.sass +25 -0
  79. accrete/contrib/ui/static/bulma/sass/utilities/functions.sass +135 -0
  80. accrete/contrib/ui/static/bulma/sass/utilities/initial-variables.sass +79 -0
  81. accrete/contrib/ui/static/bulma/sass/utilities/mixins.sass +303 -0
  82. accrete/contrib/ui/static/css/accrete.css +10426 -79
  83. accrete/contrib/ui/static/css/accrete.css.bak +156 -0
  84. accrete/contrib/ui/static/css/accrete.css.map +1 -0
  85. accrete/contrib/ui/static/css/accrete.scss +232 -0
  86. accrete/contrib/ui/static/css/icons.css +9 -0
  87. accrete/contrib/ui/static/js/filter.js +129 -57
  88. accrete/contrib/ui/templates/ui/detail.html +1 -3
  89. accrete/contrib/ui/templates/ui/layout.html +37 -20
  90. accrete/contrib/ui/templates/ui/list.html +3 -3
  91. accrete/contrib/ui/templates/ui/partials/filter.html +10 -5
  92. accrete/contrib/ui/templates/ui/partials/header.html +5 -16
  93. accrete/contrib/ui/templates/ui/partials/pagination_detail.html +7 -11
  94. accrete/contrib/ui/templates/ui/partials/pagination_list.html +8 -10
  95. accrete/contrib/ui/templates/ui/partials/table_field.html +15 -0
  96. accrete/contrib/ui/templates/ui/partials/table_field_float.html +1 -0
  97. accrete/contrib/ui/templates/ui/partials/table_field_monetary.html +4 -0
  98. accrete/contrib/ui/templates/ui/partials/table_field_string.html +1 -0
  99. accrete/contrib/ui/templates/ui/table.html +20 -5
  100. accrete/contrib/ui/templatetags/accrete_ui.py +5 -3
  101. accrete/contrib/user/forms.py +28 -0
  102. accrete/contrib/user/models.py +16 -0
  103. accrete/contrib/user/templates/user/accrete_navbar_end.html +9 -0
  104. accrete/contrib/user/templates/user/user_detail.html +43 -0
  105. accrete/contrib/user/templates/user/user_form.html +56 -0
  106. accrete/contrib/user/urls.py +3 -1
  107. accrete/contrib/user/views.py +35 -3
  108. accrete/queries.py +5 -1
  109. {accrete-0.0.10.dist-info → accrete-0.0.23.dist-info}/METADATA +1 -1
  110. accrete-0.0.23.dist-info/RECORD +194 -0
  111. accrete/contrib/ui/helper.py +0 -417
  112. accrete-0.0.10.dist-info/RECORD +0 -107
  113. {accrete-0.0.10.dist-info → accrete-0.0.23.dist-info}/WHEEL +0 -0
  114. {accrete-0.0.10.dist-info → accrete-0.0.23.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,8 @@
1
- from tenant.forms import ModelForm
1
+ from accrete.forms import TenantModelForm
2
2
  from .models import Sequence
3
3
 
4
4
 
5
- class SequenceCreateForm(ModelForm):
5
+ class SequenceCreateForm(TenantModelForm):
6
6
  class Meta:
7
7
  model = Sequence
8
8
  fields = [
@@ -1,13 +1,12 @@
1
- import logging
2
1
  from django.db import transaction
3
2
  from django.db.models import F
4
3
 
4
+ from accrete.tenant import get_tenant
5
5
  from .models import Sequence
6
6
 
7
- _logger = logging.getLogger(__name__)
8
7
 
9
-
10
- def get_nextval(tenant, name, create_if_none=True):
8
+ def get_nextval(name, create_if_none=True):
9
+ tenant = get_tenant()
11
10
  with transaction.atomic():
12
11
  seq = Sequence.objects.filter(
13
12
  tenant=tenant.id, name=name
@@ -1,11 +1,17 @@
1
1
  from .filter import Filter
2
- from .helper import (
3
- ClientAction,
2
+ from .context import (
3
+ ListContext,
4
4
  DetailContext,
5
5
  FormContext,
6
- ListContext,
6
+ )
7
+ from .components import (
8
+ ClientAction,
7
9
  BreadCrumb,
8
10
  TableField,
9
- build_querystring,
10
- queryset_from_querystring
11
+ TableFieldAlignment,
12
+ TableFieldType
13
+ )
14
+ from .querystring import (
15
+ parse_querystring,
16
+ build_querystring
11
17
  )
@@ -0,0 +1,49 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+
4
+
5
+ class TableFieldAlignment(Enum):
6
+
7
+ LEFT = 'left'
8
+ CENTER = 'center'
9
+ RIGHT = 'right'
10
+
11
+
12
+ class TableFieldType(Enum):
13
+
14
+ NONE = ''
15
+ STRING = '_string'
16
+ MONETARY = '_monetary'
17
+ FLOAT = '_float'
18
+
19
+
20
+ @dataclass
21
+ class TableField:
22
+
23
+ label: str
24
+ name: str
25
+ alignment: type[TableFieldAlignment] = TableFieldAlignment.LEFT
26
+ header_alignment: type[TableFieldAlignment] = None
27
+ field_type: type[TableFieldType] = TableFieldType.NONE
28
+ prefix: str = ''
29
+ suffix: str = ''
30
+ truncate_after: int = 0
31
+
32
+
33
+ @dataclass
34
+ class BreadCrumb:
35
+
36
+ name: str
37
+ url: str
38
+
39
+
40
+ @dataclass
41
+ class ClientAction:
42
+
43
+ name: str
44
+ url: str = ''
45
+ query_params: str = ''
46
+ attrs: list[str] = field(default_factory=list)
47
+ submit: bool = False
48
+ form_id: str = 'form'
49
+ class_list: list = field(default_factory=list)
@@ -0,0 +1,284 @@
1
+ import logging
2
+ from dataclasses import dataclass, field
3
+
4
+ from django.utils.translation import gettext_lazy as _
5
+ from django.db.models import Model, QuerySet, Q
6
+ from django.core.paginator import Paginator
7
+
8
+ from .querystring import parse_querystring, build_querystring
9
+ from .components import ClientAction, BreadCrumb, TableField
10
+ from .filter import Filter
11
+
12
+ _logger = logging.getLogger(__name__)
13
+
14
+ DEFAULT_PAGINATE_BY = 40
15
+
16
+
17
+ @dataclass
18
+ class ListContext:
19
+
20
+ model: Model
21
+ get_params: dict
22
+ title: str = None
23
+ context: dict = field(default_factory=dict)
24
+ queryset: QuerySet = None
25
+ select_related: list[str] = field(default_factory=list)
26
+ prefetch_related: list[str] = field(default_factory=list)
27
+ annotate: dict = field(default_factory=dict)
28
+ paginate_by: int = DEFAULT_PAGINATE_BY
29
+ order_by: list[str] = field(default_factory=list)
30
+ filter_relation_depth: int = 4
31
+ default_filter_term: str = ''
32
+ actions: list[ClientAction] = field(default_factory=list)
33
+ breadcrumbs: list[BreadCrumb] = field(default_factory=list)
34
+ obj_label: str = None
35
+ fields: list[TableField] = field(default_factory=list)
36
+ unselect_button: bool = False
37
+
38
+ def get_queryset(self):
39
+ if self.queryset:
40
+ return self.queryset
41
+
42
+ order = self.order_by or self.model._meta.ordering
43
+
44
+ pks = self.model.objects.filter(
45
+ parse_querystring(self.model, self.get_params.get('q', '[]'))
46
+ ).distinct().values_list('pk', flat=True)
47
+
48
+ queryset = self.model.objects.select_related(
49
+ *self.select_related
50
+ ).prefetch_related(
51
+ *self.prefetch_related
52
+ ).filter(
53
+ Q(pk__in=pks)
54
+ ).annotate(
55
+ **self.get_annotations()
56
+ ).order_by(
57
+ *order
58
+ ).distinct()
59
+
60
+ return queryset
61
+
62
+ def get_annotations(self):
63
+ annotations = {
64
+ annotation['name']: annotation['func']
65
+ for annotation in getattr(self.model, 'annotations', [])
66
+ }
67
+ if self.annotate:
68
+ annotations.update(self.annotate)
69
+ return annotations
70
+
71
+ def get_page_number(self, paginator):
72
+ page_number = self.get_params.get('page', '1')
73
+
74
+ try:
75
+ page_number = int(page_number)
76
+ except ValueError:
77
+ page_number = 1
78
+
79
+ if page_number < 1:
80
+ page_number = 1
81
+ elif page_number > paginator.num_pages:
82
+ page_number = paginator.num_pages
83
+ return page_number
84
+
85
+ def get_paginate_by(self):
86
+ paginate_by = self.get_params.get('paginate_by', self.paginate_by)
87
+ try:
88
+ paginate_by = int(paginate_by)
89
+ except ValueError:
90
+ paginate_by = self.paginate_by
91
+ return paginate_by
92
+
93
+ def dict(self):
94
+ queryset = self.get_queryset()
95
+ paginate_by = self.get_paginate_by()
96
+ paginator = Paginator(queryset, paginate_by)
97
+ page = paginator.page(self.get_page_number(paginator))
98
+ context = {
99
+ 'queryset': queryset,
100
+ 'paginate_by': paginate_by,
101
+ 'order_by': self.get_params.get('order_by', self.model._meta.ordering),
102
+ 'paginator': paginator,
103
+ 'page': page,
104
+ 'list_pagination': True,
105
+ 'title': self.title or self.model._meta.verbose_name_plural,
106
+ 'filter_terms': Filter(self.model, self.filter_relation_depth).get_query_terms(),
107
+ 'default_filter_term': self.default_filter_term,
108
+ 'breadcrumbs': self.breadcrumbs,
109
+ 'querystring': build_querystring(self.get_params),
110
+ 'actions': self.actions,
111
+ 'obj_label': self.obj_label or _('Name'),
112
+ 'fields': self.fields
113
+ }
114
+ context.update(self.context)
115
+ return context
116
+
117
+
118
+ @dataclass
119
+ class DetailContext:
120
+
121
+ obj: Model
122
+ get_params: dict
123
+ order_by: str = None
124
+ paginate_by: int = DEFAULT_PAGINATE_BY
125
+ title: str = None
126
+ queryset: type[QuerySet] = None
127
+ select_related: list[str] = field(default_factory=list)
128
+ prefetch_related: list[str] = field(default_factory=list)
129
+ annotate: dict = field(default_factory=dict)
130
+ actions: list[ClientAction] = field(default_factory=list)
131
+ breadcrumbs: list[BreadCrumb] = field(default_factory=list)
132
+ context: dict = field(default_factory=dict)
133
+
134
+ def get_queryset(self):
135
+ if self.queryset:
136
+ return self.queryset
137
+
138
+ order = self.order_by or self.obj._meta.model._meta.ordering
139
+
140
+ pks = self.obj._meta.model.objects.filter(
141
+ parse_querystring(self.obj._meta.model, self.get_params.get('q', '[]'))
142
+ ).distinct().values_list('pk', flat=True)
143
+
144
+ queryset = self.obj._meta.model.objects.select_related(
145
+ *self.select_related
146
+ ).prefetch_related(
147
+ *self.prefetch_related
148
+ ).filter(
149
+ Q(pk__in=pks)
150
+ ).annotate(
151
+ **self.get_annotations()
152
+ ).order_by(
153
+ *order
154
+ ).distinct()
155
+
156
+ return queryset
157
+
158
+ def get_annotations(self):
159
+ annotations = {
160
+ annotation['name']: annotation['func']
161
+ for annotation in getattr(self.obj._meta.model, 'annotations', [])
162
+ }
163
+ if self.annotate:
164
+ annotations.update(self.annotate)
165
+ return annotations
166
+
167
+ def get_paginate_by(self):
168
+ paginate_by = self.get_params.get('paginate_by', self.paginate_by)
169
+ try:
170
+ paginate_by = int(paginate_by)
171
+ except ValueError:
172
+ paginate_by = self.paginate_by
173
+ return paginate_by
174
+
175
+ def get_pagination_context(self):
176
+ if not hasattr(self.obj, 'get_absolute_url'):
177
+ _logger.warning(
178
+ 'Detail pagination disabled for models without the '
179
+ 'get_absolute_url attribute. Set paginate_by to 0 to '
180
+ 'deactivate pagination.'
181
+ )
182
+ return {}
183
+ queryset = self.get_queryset()
184
+ idx = (*queryset,).index(self.obj)
185
+ previous_object_url = (
186
+ queryset[idx - 1] if idx - 1 >= 0 else queryset.last()
187
+ ).get_absolute_url()
188
+ next_object_url = (
189
+ queryset[idx + 1] if idx + 1 <= queryset.count() - 1 else queryset.first()
190
+ ).get_absolute_url()
191
+ ctx = {
192
+ 'previous_object_url': previous_object_url,
193
+ 'next_object_url': next_object_url,
194
+ 'current_object_idx': idx + 1,
195
+ 'total_objects': queryset.count(),
196
+ 'detail_pagination': True
197
+ }
198
+ return ctx
199
+
200
+ def dict(self):
201
+ paginate_by = self.get_paginate_by()
202
+ ctx = {
203
+ 'object': self.obj,
204
+ 'title': self.title or self.obj,
205
+ 'order_by': self.get_params.get('order_by', self.obj._meta.model._meta.ordering),
206
+ 'paginate_by': paginate_by,
207
+ 'detail_pagination': False,
208
+ 'view_type': 'detail',
209
+ 'breadcrumbs': self.breadcrumbs,
210
+ 'querystring': build_querystring(self.get_params, ['page']),
211
+ 'actions': self.actions
212
+ }
213
+ if self.paginate_by > 0:
214
+ ctx.update(self.get_pagination_context())
215
+ ctx.update(self.context)
216
+ return ctx
217
+
218
+
219
+ @dataclass
220
+ class FormContext:
221
+
222
+ model: Model | type[Model]
223
+ get_params: dict
224
+ title: str = None
225
+ context: dict = field(default_factory=dict)
226
+ form_id: str = 'form'
227
+ add_default_actions: bool = True
228
+ discard_url: str = None
229
+ actions: list[ClientAction] = field(default_factory=list)
230
+
231
+ def get_default_form_actions(self):
232
+ actions = [
233
+ ClientAction(
234
+ name=_('Save'),
235
+ submit=True,
236
+ class_list=['is-success'],
237
+ form_id=self.form_id
238
+ )
239
+ ]
240
+ try:
241
+ url = self.discard_url or self.model.get_absolute_url()
242
+ except TypeError:
243
+ raise TypeError(
244
+ 'Supply the discard_url parameter if FormContext is called '
245
+ 'with a model class instead of an instance.'
246
+ )
247
+ except AttributeError as e:
248
+ _logger.error(
249
+ 'Supply the discard_url parameter if FormContext is '
250
+ 'called with a model instance that has the get_absolute_url '
251
+ 'method not defined.'
252
+ )
253
+ raise e
254
+
255
+ actions.append(
256
+ ClientAction(
257
+ name=_('Discard'),
258
+ url=url,
259
+ )
260
+ )
261
+ return actions
262
+
263
+ def get_title(self):
264
+ if self.title:
265
+ return self.title
266
+ try:
267
+ int(self.model.pk)
268
+ return f'Edit {self.model}'
269
+ except TypeError:
270
+ return f'Add {self.model._meta.verbose_name}'
271
+
272
+ def dict(self):
273
+ ctx = {
274
+ 'title': self.get_title(),
275
+ 'view_type': 'form',
276
+ 'form_id': self.form_id,
277
+ 'querystring': build_querystring(self.get_params, ['page']),
278
+ 'actions': []
279
+ }
280
+ if self.add_default_actions:
281
+ ctx.update({'actions': self.get_default_form_actions()})
282
+ ctx['actions'].extend(self.actions)
283
+ ctx.update(self.context)
284
+ return ctx
@@ -78,6 +78,12 @@ class Filter:
78
78
  'label': str(self.LABEL_EXACT),
79
79
  'data_type': 'text',
80
80
  'param': 'exact'
81
+ },
82
+ {
83
+ 'label': str(self.LABEL_EXACT_NOT),
84
+ 'data_type': 'text',
85
+ 'invert': True,
86
+ 'param': 'exact'
81
87
  }
82
88
  ]
83
89
  }
@@ -238,10 +244,11 @@ class Filter:
238
244
  }
239
245
  ]
240
246
 
241
- def cast_decimal_places_to_step(self, descimal_places):
242
- if not descimal_places or descimal_places < 1:
247
+ @staticmethod
248
+ def cast_decimal_places_to_step(decimal_places):
249
+ if not decimal_places or decimal_places < 1:
243
250
  return '1'
244
- zero_count = descimal_places - 1
251
+ zero_count = decimal_places - 1
245
252
  return f'0.{"0" * zero_count}1'
246
253
 
247
254
  def get_query_term(self, field):
@@ -263,24 +270,22 @@ class Filter:
263
270
  elif internal_type in self.query_date_fields:
264
271
  return self.get_date_query_term(label, param)
265
272
 
266
- def get_searchable_method_term(self, model, method):
267
- method_name = method['name']
268
- param = '_c_' + method_name
269
- method_label = str(method.get('label', method_name.capitalize()))
270
- func = getattr(model, method['name'])
271
- return_type = func.__annotations__.get('return')
273
+ def get_annotation_term(self, model, annotation):
274
+ name = '_a_' + annotation['name']
275
+ label = annotation.get('label', name)
276
+ return_type = annotation['type']
272
277
  if return_type == str:
273
- return self.get_char_query_term(method_label, param)
278
+ return self.get_char_query_term(label, name)
274
279
  elif return_type == int:
275
- return self.get_int_query_term(method_label, param)
280
+ return self.get_int_query_term(label, name)
276
281
  elif return_type in [float, Decimal]:
277
- return self.get_float_query_term(method_label, param, 0)
282
+ return self.get_float_query_term(label, name, annotation.get('step', 0))
278
283
  elif return_type == bool:
279
- return self.get_boolean_query_term(method_label, param)
284
+ return self.get_boolean_query_term(label, name)
280
285
  elif return_type == datetime.datetime:
281
- return self.get_datetime_query_term(method_label, param)
286
+ return self.get_datetime_query_term(label, name)
282
287
  elif return_type == datetime.date:
283
- return self.get_date_query_term(method_label, param)
288
+ return self.get_date_query_term(label, name)
284
289
 
285
290
  def get_relation_query_terms(self, model, path):
286
291
  terms = []
@@ -322,8 +327,8 @@ class Filter:
322
327
  term['params'].extend(self.get_null_params())
323
328
  if term is not None:
324
329
  terms.append(term)
325
- for searchable_method in searchable_methods:
326
- terms.append(self.get_searchable_method_term(model, searchable_method))
330
+ for annotation in getattr(model, 'annotations', []):
331
+ terms.append(self.get_annotation_term(model, annotation))
327
332
  terms = sorted(terms, key=lambda x: x['label'])
328
333
  return terms
329
334
 
@@ -0,0 +1,93 @@
1
+ import json
2
+ import logging
3
+ import operator
4
+ from django.db.models import Model, Q
5
+
6
+ _logger = logging.getLogger(__name__)
7
+
8
+
9
+ def parse_querystring(model: type[Model], query_string: str) -> Q:
10
+
11
+ def get_expression(term: str, value) -> Q:
12
+ invert = False
13
+ if term.startswith('~'):
14
+ invert = True
15
+ term = term[1:]
16
+
17
+ parts = term.split('_a_')
18
+ if len(parts) == 1:
19
+ expression = Q(**{term: value})
20
+ return ~expression if invert else expression
21
+
22
+ rel_path = parts[0].rstrip('__')
23
+ term = parts[1]
24
+ rel_model = get_related_model(rel_path) if rel_path else model
25
+ objects = rel_model.objects.annotate(**{
26
+ annotation['name']: annotation['func']
27
+ for annotation in rel_model.annotations
28
+ }).filter(Q(**{term: value}))
29
+ expression = Q(**{
30
+ f'{rel_path}{"__" if rel_path else ""}id__in': objects.values_list('id', flat=True)
31
+ })
32
+
33
+ return ~expression if invert else expression
34
+
35
+ def get_related_model(rel_path: str):
36
+ related_model = model
37
+ for part in rel_path.split('__'):
38
+ try:
39
+ related_model = related_model._meta.fields_map[part].related_model
40
+ except (AttributeError, KeyError):
41
+ try:
42
+ related_model = getattr(related_model, part).field.related_model
43
+ except AttributeError:
44
+ break
45
+ return related_model
46
+
47
+ def parse_query_block(sub_item) -> Q:
48
+ op = ops['&']
49
+ parsed_query = Q()
50
+ for item in sub_item:
51
+ if isinstance(item, list):
52
+ parsed_query = op(parsed_query, parse_query_block(item))
53
+ elif isinstance(item, dict):
54
+ dict_query = Q()
55
+ for term, value in item.items():
56
+ dict_query = ops['&'](dict_query, get_expression(term, value))
57
+ parsed_query = op(parsed_query, dict_query)
58
+ elif isinstance(item, str):
59
+ try:
60
+ op = ops[item]
61
+ except KeyError as e:
62
+ _logger.exception(e)
63
+ raise ValueError(
64
+ f'Invalid operator in querystring: {item}.'
65
+ f'Operator must be one of &, |, ^'
66
+ )
67
+ else:
68
+ raise ValueError(
69
+ f'Unsupported item in querystring: {item}'
70
+ )
71
+ return parsed_query
72
+
73
+ query_data = json.loads(query_string)
74
+ if isinstance(query_data, dict):
75
+ query_data = [query_data]
76
+
77
+ ops = {'&': operator.and_, '|': operator.or_, '^': operator.xor}
78
+ query = parse_query_block(query_data)
79
+ return query
80
+
81
+
82
+ def build_querystring(get_params: dict, extra_params: list[str] = None) -> str:
83
+ querystring = f'?q={get_params.get("q", "[]")}'
84
+ if paginate_by := get_params.get('paginate_by', False):
85
+ querystring += f'&paginate_by={paginate_by}'
86
+ if order_by := get_params.get('order_by', False):
87
+ querystring += f'&order_by={order_by}'
88
+ if crumbs := get_params.get('crumbs', False):
89
+ querystring += f'&crumbs={crumbs}'
90
+ for param in extra_params or []:
91
+ if value := get_params.get(param, False):
92
+ querystring += f'&{param}={value}'
93
+ return querystring
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Jeremy Thomas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.