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
accrete/contrib/ui/helper.py
DELETED
@@ -1,432 +0,0 @@
|
|
1
|
-
import datetime
|
2
|
-
import logging
|
3
|
-
import json
|
4
|
-
from enum import Enum
|
5
|
-
from dataclasses import dataclass, field
|
6
|
-
from django.db.models.functions import Lower
|
7
|
-
from django.db.models import Model, QuerySet, Q, CharField, Manager
|
8
|
-
from django.utils.translation import gettext_lazy as _
|
9
|
-
from django.core.exceptions import FieldDoesNotExist
|
10
|
-
from django.core.paginator import Paginator
|
11
|
-
from accrete.contrib.ui.filter import Filter
|
12
|
-
|
13
|
-
_logger = logging.getLogger(__name__)
|
14
|
-
|
15
|
-
DEFAULT_PAGINATE_BY = 40
|
16
|
-
|
17
|
-
|
18
|
-
@dataclass
|
19
|
-
class ClientAction:
|
20
|
-
|
21
|
-
name: str
|
22
|
-
url: str = ''
|
23
|
-
query_params: str = ''
|
24
|
-
attrs: list[str] = field(default_factory=list)
|
25
|
-
submit: bool = False
|
26
|
-
form_id: str = 'form'
|
27
|
-
class_list: list = field(default_factory=list)
|
28
|
-
|
29
|
-
|
30
|
-
@dataclass
|
31
|
-
class BreadCrumb:
|
32
|
-
|
33
|
-
name: str
|
34
|
-
url: str
|
35
|
-
|
36
|
-
|
37
|
-
class TableFieldAlignment(Enum):
|
38
|
-
|
39
|
-
LEFT = 'left'
|
40
|
-
CENTER = 'center'
|
41
|
-
RIGHT = 'right'
|
42
|
-
|
43
|
-
|
44
|
-
@dataclass
|
45
|
-
class TableField:
|
46
|
-
|
47
|
-
label: str
|
48
|
-
name: str
|
49
|
-
align: type[TableFieldAlignment] = TableFieldAlignment.LEFT
|
50
|
-
|
51
|
-
|
52
|
-
@dataclass
|
53
|
-
class ListContext:
|
54
|
-
|
55
|
-
model: type[Model]
|
56
|
-
get_params: dict
|
57
|
-
title: str = None
|
58
|
-
context: dict = field(default_factory=dict)
|
59
|
-
queryset: QuerySet = None
|
60
|
-
extra_query: Q = None
|
61
|
-
related_fields: list[str] = field(default_factory=list)
|
62
|
-
prefetch_fields: list[str] = field(default_factory=list)
|
63
|
-
paginate_by: int = DEFAULT_PAGINATE_BY
|
64
|
-
order_by: list[str] = None
|
65
|
-
column_width: int = 12
|
66
|
-
filter_relation_depth: int = 4
|
67
|
-
default_filter_term: str = ''
|
68
|
-
actions: list[ClientAction] = field(default_factory=list)
|
69
|
-
breadcrumbs: list[BreadCrumb] = field(default_factory=list)
|
70
|
-
obj_label: str = None
|
71
|
-
fields: list[TableField] = field(default_factory=list)
|
72
|
-
unselect_button: bool = False
|
73
|
-
|
74
|
-
def get_queryset(self):
|
75
|
-
order = self.get_order()
|
76
|
-
if isinstance(order, str):
|
77
|
-
if order == 'None':
|
78
|
-
order = None
|
79
|
-
return (self.queryset or queryset_from_querystring(
|
80
|
-
self.model, self.get_params.get('q', '[]'), order)
|
81
|
-
.filter(self.extra_query or Q())
|
82
|
-
.select_related(*self.related_fields)
|
83
|
-
.prefetch_related(*self.prefetch_fields)
|
84
|
-
.distinct())
|
85
|
-
|
86
|
-
def get_page_number(self, paginator):
|
87
|
-
page_number = self.get_params.get('page', '1')
|
88
|
-
|
89
|
-
try:
|
90
|
-
page_number = int(page_number)
|
91
|
-
except ValueError:
|
92
|
-
page_number = 1
|
93
|
-
|
94
|
-
if page_number < 1:
|
95
|
-
page_number = 1
|
96
|
-
elif page_number > paginator.num_pages:
|
97
|
-
page_number = paginator.num_pages
|
98
|
-
return page_number
|
99
|
-
|
100
|
-
def get_order(self):
|
101
|
-
return self.get_params.get('order_by', None) or self.order_by
|
102
|
-
|
103
|
-
def get_paginate_by(self):
|
104
|
-
paginate_by = self.get_params.get('paginate_by', self.paginate_by)
|
105
|
-
try:
|
106
|
-
paginate_by = int(paginate_by)
|
107
|
-
except ValueError:
|
108
|
-
paginate_by = self.paginate_by
|
109
|
-
return paginate_by
|
110
|
-
|
111
|
-
def get_context(self):
|
112
|
-
paginate_by = self.get_paginate_by()
|
113
|
-
paginator = Paginator(self.get_queryset(), paginate_by)
|
114
|
-
page = paginator.page(self.get_page_number(paginator))
|
115
|
-
context = {
|
116
|
-
'paginate_by': paginate_by,
|
117
|
-
'order_by': self.get_params.get('order_by', self.model._meta.ordering),
|
118
|
-
'paginator': paginator,
|
119
|
-
'page': page,
|
120
|
-
'list_pagination': True,
|
121
|
-
'column_width': self.column_width,
|
122
|
-
'title': self.title or self.model._meta.verbose_name_plural,
|
123
|
-
'filter_terms': Filter(self.model, self.filter_relation_depth).get_query_terms(),
|
124
|
-
'default_filter_term': self.default_filter_term,
|
125
|
-
'view_type': 'list',
|
126
|
-
'breadcrumbs': self.breadcrumbs,
|
127
|
-
'querystring': build_querystring(self.get_params),
|
128
|
-
'actions': self.actions,
|
129
|
-
'obj_label': self.obj_label or _('Name'),
|
130
|
-
'fields': self.fields
|
131
|
-
}
|
132
|
-
context.update(self.context)
|
133
|
-
return context
|
134
|
-
|
135
|
-
|
136
|
-
@dataclass
|
137
|
-
class DetailContext:
|
138
|
-
|
139
|
-
obj: Model
|
140
|
-
get_params: dict
|
141
|
-
order_by: str = None
|
142
|
-
paginate_by: int = DEFAULT_PAGINATE_BY
|
143
|
-
title: str = None
|
144
|
-
queryset: type[QuerySet] = None
|
145
|
-
extra_query: Q = None
|
146
|
-
related_fields: list[str] = field(default_factory=list)
|
147
|
-
prefetch_fields: list[str] = field(default_factory=list)
|
148
|
-
actions: list[ClientAction] = field(default_factory=list)
|
149
|
-
breadcrumbs: list[BreadCrumb] = field(default_factory=list),
|
150
|
-
context: dict = field(default_factory=dict)
|
151
|
-
|
152
|
-
def get_queryset(self):
|
153
|
-
order = self.get_order()
|
154
|
-
if isinstance(order, str):
|
155
|
-
if order == 'None':
|
156
|
-
order = None
|
157
|
-
return (self.queryset or queryset_from_querystring(
|
158
|
-
self.obj._meta.model, self.get_params.get('q', '[]'), order)
|
159
|
-
.filter(self.extra_query or Q())
|
160
|
-
.select_related(*self.related_fields)
|
161
|
-
.prefetch_related(*self.prefetch_fields)
|
162
|
-
.distinct())
|
163
|
-
|
164
|
-
def get_order(self):
|
165
|
-
return self.get_params.get('order_by', None) or self.order_by
|
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 get_context(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 get_context(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
|
285
|
-
|
286
|
-
|
287
|
-
def build_querystring(get_params: dict, extra_params: list[str] = None) -> str:
|
288
|
-
querystring = f'?q={get_params.get("q", "[]")}'
|
289
|
-
if paginate_by := get_params.get('paginate_by', False):
|
290
|
-
querystring += f'&paginate_by={paginate_by}'
|
291
|
-
# if page := get_params.get('page', False):
|
292
|
-
# querystring += f'&page={page}'
|
293
|
-
if order_by := get_params.get('order_by', False):
|
294
|
-
querystring += f'&order_by={order_by}'
|
295
|
-
for param in extra_params or []:
|
296
|
-
if value := get_params.get(param, False):
|
297
|
-
querystring += f'&{param}={value}'
|
298
|
-
return querystring
|
299
|
-
|
300
|
-
|
301
|
-
def queryset_from_querystring(
|
302
|
-
model: type[Model|Manager],
|
303
|
-
query_string: str,
|
304
|
-
order_by: str|list[str] = None,
|
305
|
-
) -> QuerySet:
|
306
|
-
"""
|
307
|
-
param url_query_string: json serializable string
|
308
|
-
[[{"name__iexact": "asdf"}, {"active": true}], [{"group__name__isnull": false}]]
|
309
|
-
"""
|
310
|
-
|
311
|
-
if not isinstance(model, Manager):
|
312
|
-
model = model.objects
|
313
|
-
|
314
|
-
query_data = json.loads(query_string)
|
315
|
-
query = Q()
|
316
|
-
|
317
|
-
for query_block in query_data:
|
318
|
-
block_query = Q()
|
319
|
-
for query_term in query_block:
|
320
|
-
if isinstance(query_term, dict):
|
321
|
-
for param, value in query_term.items():
|
322
|
-
block_query &= get_query(model.model, param, value)
|
323
|
-
elif isinstance(query_term, list):
|
324
|
-
inner_block_query = Q()
|
325
|
-
for inner_query_term in query_term:
|
326
|
-
for param, value in inner_query_term.items():
|
327
|
-
inner_block_query |= get_query(model.model, param, value)
|
328
|
-
block_query &= inner_block_query
|
329
|
-
query |= block_query
|
330
|
-
|
331
|
-
queryset = model.filter(query)
|
332
|
-
|
333
|
-
if order_by is None:
|
334
|
-
return queryset
|
335
|
-
|
336
|
-
try:
|
337
|
-
order = order_by.split(',')
|
338
|
-
except AttributeError:
|
339
|
-
order = order_by or []
|
340
|
-
|
341
|
-
for o in order_by:
|
342
|
-
desc = False
|
343
|
-
if o.startswith('-'):
|
344
|
-
o = o[1:]
|
345
|
-
desc = True
|
346
|
-
try:
|
347
|
-
model_field = model.model._meta.get_field(o)
|
348
|
-
except FieldDoesNotExist:
|
349
|
-
model_field = None
|
350
|
-
if o == 'pk':
|
351
|
-
pass
|
352
|
-
if isinstance(model_field, CharField):
|
353
|
-
if desc:
|
354
|
-
order.append(Lower(o).desc())
|
355
|
-
else:
|
356
|
-
order.append(Lower(o).asc())
|
357
|
-
else:
|
358
|
-
order.append(f'{"-" if desc else ""}{o}')
|
359
|
-
return queryset.order_by(*order)
|
360
|
-
|
361
|
-
|
362
|
-
def get_related_model(model, path):
|
363
|
-
related_model = model
|
364
|
-
for part in path:
|
365
|
-
try:
|
366
|
-
related_model = related_model._meta.fields_map[part].related_model
|
367
|
-
except (AttributeError, KeyError):
|
368
|
-
related_model = getattr(related_model, part).field.related_model
|
369
|
-
return related_model
|
370
|
-
|
371
|
-
|
372
|
-
def get_query(model, param: str, value) -> Q:
|
373
|
-
invert = False
|
374
|
-
parts = param.split('__')
|
375
|
-
if param.startswith('~'):
|
376
|
-
param = param[1:]
|
377
|
-
invert = True
|
378
|
-
if len(parts) == 1:
|
379
|
-
parts.append('exact')
|
380
|
-
attribute = parts[-2]
|
381
|
-
|
382
|
-
if not attribute.startswith('_c_'):
|
383
|
-
query = Q(**{param: value})
|
384
|
-
return ~query if invert else query
|
385
|
-
|
386
|
-
operator = parts[-1]
|
387
|
-
related_model = get_related_model(model, parts[:-2])
|
388
|
-
func = getattr(related_model, attribute[3:])
|
389
|
-
|
390
|
-
obj_ids = [
|
391
|
-
obj.id for obj in
|
392
|
-
filter(
|
393
|
-
lambda instance:
|
394
|
-
evaluate(instance, func, value, operator),
|
395
|
-
related_model.objects.all()
|
396
|
-
)
|
397
|
-
]
|
398
|
-
|
399
|
-
query = Q(**{
|
400
|
-
f'{"__".join(parts[:-2])}{"__" if parts[:-2] else ""}id__in': obj_ids
|
401
|
-
})
|
402
|
-
return ~query if invert else query
|
403
|
-
|
404
|
-
|
405
|
-
def evaluate(instance, func, value, operator):
|
406
|
-
return_type = func.__annotations__.get('return', str)
|
407
|
-
try:
|
408
|
-
value = return_type(value)
|
409
|
-
except TypeError as e:
|
410
|
-
if isinstance(return_type, datetime.datetime):
|
411
|
-
value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M')
|
412
|
-
elif isinstance(return_type, datetime.date):
|
413
|
-
value = datetime.datetime.strptime(value, '%Y-%m-%d')
|
414
|
-
else:
|
415
|
-
raise e
|
416
|
-
|
417
|
-
return_value = func(instance)
|
418
|
-
|
419
|
-
if operator == 'exact':
|
420
|
-
return value == return_value
|
421
|
-
elif operator == 'icontains':
|
422
|
-
return value.lower() in return_value.lower()
|
423
|
-
elif operator == 'contains':
|
424
|
-
return value in return_value
|
425
|
-
elif operator == 'gt':
|
426
|
-
return return_value > value
|
427
|
-
elif operator == 'gte':
|
428
|
-
return return_value >= value
|
429
|
-
elif operator == 'lte':
|
430
|
-
return return_value <= value
|
431
|
-
elif operator == 'lt':
|
432
|
-
return return_value < value
|
File without changes
|
File without changes
|