accrete 0.0.21__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.
- accrete/contrib/sequence/forms.py +2 -2
- accrete/contrib/sequence/queries.py +3 -4
- accrete/contrib/ui/__init__.py +10 -5
- accrete/contrib/ui/components.py +49 -0
- accrete/contrib/ui/context.py +284 -0
- accrete/contrib/ui/filter.py +16 -17
- accrete/contrib/ui/querystring.py +93 -0
- accrete/contrib/ui/static/css/accrete.css +225 -162
- accrete/contrib/ui/static/css/accrete.css.map +1 -1
- accrete/contrib/ui/static/css/accrete.scss +90 -21
- accrete/contrib/ui/static/js/filter.js +18 -11
- accrete/contrib/ui/templates/ui/detail.html +1 -3
- accrete/contrib/ui/templates/ui/layout.html +3 -3
- accrete/contrib/ui/templates/ui/list.html +3 -3
- accrete/contrib/ui/templates/ui/partials/filter.html +1 -0
- accrete/contrib/ui/templates/ui/partials/header.html +1 -1
- accrete/contrib/ui/templates/ui/partials/pagination_detail.html +2 -2
- accrete/contrib/ui/templates/ui/partials/pagination_list.html +5 -5
- accrete/contrib/ui/templates/ui/partials/table_field.html +15 -0
- accrete/contrib/ui/templates/ui/partials/table_field_float.html +1 -0
- accrete/contrib/ui/templates/ui/partials/table_field_monetary.html +4 -0
- accrete/contrib/ui/templates/ui/partials/table_field_string.html +1 -0
- accrete/contrib/ui/templates/ui/table.html +13 -4
- accrete/contrib/user/templates/user/user_detail.html +9 -9
- accrete/contrib/user/templates/user/user_form.html +2 -0
- accrete/contrib/user/views.py +2 -2
- accrete/queries.py +5 -1
- {accrete-0.0.21.dist-info → accrete-0.0.23.dist-info}/METADATA +1 -1
- {accrete-0.0.21.dist-info → accrete-0.0.23.dist-info}/RECORD +31 -25
- accrete/contrib/ui/helper.py +0 -432
- {accrete-0.0.21.dist-info → accrete-0.0.23.dist-info}/WHEEL +0 -0
- {accrete-0.0.21.dist-info → accrete-0.0.23.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
-
|
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
|
accrete/contrib/ui/__init__.py
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
from .filter import Filter
|
2
|
-
from .
|
3
|
-
|
2
|
+
from .context import (
|
3
|
+
ListContext,
|
4
4
|
DetailContext,
|
5
5
|
FormContext,
|
6
|
-
|
6
|
+
)
|
7
|
+
from .components import (
|
8
|
+
ClientAction,
|
7
9
|
BreadCrumb,
|
8
10
|
TableField,
|
9
11
|
TableFieldAlignment,
|
10
|
-
|
11
|
-
|
12
|
+
TableFieldType
|
13
|
+
)
|
14
|
+
from .querystring import (
|
15
|
+
parse_querystring,
|
16
|
+
build_querystring
|
12
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
|
accrete/contrib/ui/filter.py
CHANGED
@@ -244,10 +244,11 @@ class Filter:
|
|
244
244
|
}
|
245
245
|
]
|
246
246
|
|
247
|
-
|
248
|
-
|
247
|
+
@staticmethod
|
248
|
+
def cast_decimal_places_to_step(decimal_places):
|
249
|
+
if not decimal_places or decimal_places < 1:
|
249
250
|
return '1'
|
250
|
-
zero_count =
|
251
|
+
zero_count = decimal_places - 1
|
251
252
|
return f'0.{"0" * zero_count}1'
|
252
253
|
|
253
254
|
def get_query_term(self, field):
|
@@ -269,24 +270,22 @@ class Filter:
|
|
269
270
|
elif internal_type in self.query_date_fields:
|
270
271
|
return self.get_date_query_term(label, param)
|
271
272
|
|
272
|
-
def
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
func = getattr(model, method['name'])
|
277
|
-
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']
|
278
277
|
if return_type == str:
|
279
|
-
return self.get_char_query_term(
|
278
|
+
return self.get_char_query_term(label, name)
|
280
279
|
elif return_type == int:
|
281
|
-
return self.get_int_query_term(
|
280
|
+
return self.get_int_query_term(label, name)
|
282
281
|
elif return_type in [float, Decimal]:
|
283
|
-
return self.get_float_query_term(
|
282
|
+
return self.get_float_query_term(label, name, annotation.get('step', 0))
|
284
283
|
elif return_type == bool:
|
285
|
-
return self.get_boolean_query_term(
|
284
|
+
return self.get_boolean_query_term(label, name)
|
286
285
|
elif return_type == datetime.datetime:
|
287
|
-
return self.get_datetime_query_term(
|
286
|
+
return self.get_datetime_query_term(label, name)
|
288
287
|
elif return_type == datetime.date:
|
289
|
-
return self.get_date_query_term(
|
288
|
+
return self.get_date_query_term(label, name)
|
290
289
|
|
291
290
|
def get_relation_query_terms(self, model, path):
|
292
291
|
terms = []
|
@@ -328,8 +327,8 @@ class Filter:
|
|
328
327
|
term['params'].extend(self.get_null_params())
|
329
328
|
if term is not None:
|
330
329
|
terms.append(term)
|
331
|
-
for
|
332
|
-
terms.append(self.
|
330
|
+
for annotation in getattr(model, 'annotations', []):
|
331
|
+
terms.append(self.get_annotation_term(model, annotation))
|
333
332
|
terms = sorted(terms, key=lambda x: x['label'])
|
334
333
|
return terms
|
335
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
|