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.
Files changed (32) hide show
  1. accrete/contrib/sequence/forms.py +2 -2
  2. accrete/contrib/sequence/queries.py +3 -4
  3. accrete/contrib/ui/__init__.py +10 -5
  4. accrete/contrib/ui/components.py +49 -0
  5. accrete/contrib/ui/context.py +284 -0
  6. accrete/contrib/ui/filter.py +16 -17
  7. accrete/contrib/ui/querystring.py +93 -0
  8. accrete/contrib/ui/static/css/accrete.css +225 -162
  9. accrete/contrib/ui/static/css/accrete.css.map +1 -1
  10. accrete/contrib/ui/static/css/accrete.scss +90 -21
  11. accrete/contrib/ui/static/js/filter.js +18 -11
  12. accrete/contrib/ui/templates/ui/detail.html +1 -3
  13. accrete/contrib/ui/templates/ui/layout.html +3 -3
  14. accrete/contrib/ui/templates/ui/list.html +3 -3
  15. accrete/contrib/ui/templates/ui/partials/filter.html +1 -0
  16. accrete/contrib/ui/templates/ui/partials/header.html +1 -1
  17. accrete/contrib/ui/templates/ui/partials/pagination_detail.html +2 -2
  18. accrete/contrib/ui/templates/ui/partials/pagination_list.html +5 -5
  19. accrete/contrib/ui/templates/ui/partials/table_field.html +15 -0
  20. accrete/contrib/ui/templates/ui/partials/table_field_float.html +1 -0
  21. accrete/contrib/ui/templates/ui/partials/table_field_monetary.html +4 -0
  22. accrete/contrib/ui/templates/ui/partials/table_field_string.html +1 -0
  23. accrete/contrib/ui/templates/ui/table.html +13 -4
  24. accrete/contrib/user/templates/user/user_detail.html +9 -9
  25. accrete/contrib/user/templates/user/user_form.html +2 -0
  26. accrete/contrib/user/views.py +2 -2
  27. accrete/queries.py +5 -1
  28. {accrete-0.0.21.dist-info → accrete-0.0.23.dist-info}/METADATA +1 -1
  29. {accrete-0.0.21.dist-info → accrete-0.0.23.dist-info}/RECORD +31 -25
  30. accrete/contrib/ui/helper.py +0 -432
  31. {accrete-0.0.21.dist-info → accrete-0.0.23.dist-info}/WHEEL +0 -0
  32. {accrete-0.0.21.dist-info → accrete-0.0.23.dist-info}/licenses/LICENSE +0 -0
@@ -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