accrete 0.0.23__py3-none-any.whl → 0.0.25__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/ui/__init__.py +1 -4
- accrete/contrib/ui/components.py +7 -5
- accrete/contrib/ui/context.py +11 -11
- accrete/contrib/ui/filter.py +9 -16
- accrete/contrib/ui/querystring.py +3 -77
- accrete/contrib/ui/static/css/accrete.css +131 -63
- accrete/contrib/ui/static/css/accrete.css.map +1 -1
- accrete/contrib/ui/static/css/accrete.scss +116 -22
- accrete/contrib/ui/static/css/icons.css +18 -0
- accrete/contrib/ui/static/js/filter.js +563 -476
- accrete/contrib/ui/static/js/filter_old.js +734 -0
- accrete/contrib/ui/templates/ui/layout.html +7 -7
- accrete/contrib/ui/templates/ui/partials/filter.html +15 -9
- accrete/contrib/ui/templates/ui/partials/header.html +5 -3
- accrete/contrib/ui/templates/ui/partials/pagination_detail.html +2 -2
- accrete/contrib/ui/templates/ui/partials/pagination_list.html +2 -2
- accrete/contrib/ui/templates/ui/partials/table_field.html +1 -0
- accrete/contrib/ui/templates/ui/table.html +14 -12
- accrete/contrib/user/models.py +5 -0
- accrete/contrib/user/views.py +1 -1
- accrete/forms.py +7 -5
- accrete/models.py +21 -0
- accrete/queries.py +0 -1
- accrete/querystring.py +89 -0
- accrete/tenant.py +4 -0
- accrete/utils/dates.py +2 -1
- {accrete-0.0.23.dist-info → accrete-0.0.25.dist-info}/METADATA +1 -1
- {accrete-0.0.23.dist-info → accrete-0.0.25.dist-info}/RECORD +30 -29
- {accrete-0.0.23.dist-info → accrete-0.0.25.dist-info}/WHEEL +1 -1
- accrete/shortcuts.py +0 -8
- {accrete-0.0.23.dist-info → accrete-0.0.25.dist-info}/licenses/LICENSE +0 -0
accrete/contrib/ui/__init__.py
CHANGED
accrete/contrib/ui/components.py
CHANGED
@@ -22,12 +22,14 @@ class TableField:
|
|
22
22
|
|
23
23
|
label: str
|
24
24
|
name: str
|
25
|
-
alignment:
|
26
|
-
header_alignment:
|
27
|
-
|
25
|
+
alignment: TableFieldAlignment | Enum = TableFieldAlignment.LEFT
|
26
|
+
header_alignment: TableFieldAlignment | Enum = None
|
27
|
+
header_info: str = None
|
28
|
+
field_type: TableFieldType | Enum = TableFieldType.NONE
|
28
29
|
prefix: str = ''
|
29
30
|
suffix: str = ''
|
30
31
|
truncate_after: int = 0
|
32
|
+
template: str = None
|
31
33
|
|
32
34
|
|
33
35
|
@dataclass
|
@@ -43,7 +45,7 @@ class ClientAction:
|
|
43
45
|
name: str
|
44
46
|
url: str = ''
|
45
47
|
query_params: str = ''
|
46
|
-
attrs: list[str] = field(default_factory=list)
|
48
|
+
attrs: list[tuple[str, str]] = field(default_factory=list)
|
47
49
|
submit: bool = False
|
48
50
|
form_id: str = 'form'
|
49
|
-
class_list: list = field(default_factory=list)
|
51
|
+
class_list: list[str] = field(default_factory=list)
|
accrete/contrib/ui/context.py
CHANGED
@@ -4,8 +4,9 @@ from dataclasses import dataclass, field
|
|
4
4
|
from django.utils.translation import gettext_lazy as _
|
5
5
|
from django.db.models import Model, QuerySet, Q
|
6
6
|
from django.core.paginator import Paginator
|
7
|
+
from accrete.querystring import parse_querystring
|
7
8
|
|
8
|
-
from .querystring import
|
9
|
+
from .querystring import load_querystring, build_url_params
|
9
10
|
from .components import ClientAction, BreadCrumb, TableField
|
10
11
|
from .filter import Filter
|
11
12
|
|
@@ -17,7 +18,7 @@ DEFAULT_PAGINATE_BY = 40
|
|
17
18
|
@dataclass
|
18
19
|
class ListContext:
|
19
20
|
|
20
|
-
model: Model
|
21
|
+
model: type[Model]
|
21
22
|
get_params: dict
|
22
23
|
title: str = None
|
23
24
|
context: dict = field(default_factory=dict)
|
@@ -31,7 +32,7 @@ class ListContext:
|
|
31
32
|
default_filter_term: str = ''
|
32
33
|
actions: list[ClientAction] = field(default_factory=list)
|
33
34
|
breadcrumbs: list[BreadCrumb] = field(default_factory=list)
|
34
|
-
|
35
|
+
object_label: str = None
|
35
36
|
fields: list[TableField] = field(default_factory=list)
|
36
37
|
unselect_button: bool = False
|
37
38
|
|
@@ -103,12 +104,13 @@ class ListContext:
|
|
103
104
|
'page': page,
|
104
105
|
'list_pagination': True,
|
105
106
|
'title': self.title or self.model._meta.verbose_name_plural,
|
107
|
+
'object_label': self.object_label or self.model._meta.verbose_name or _('Name'),
|
106
108
|
'filter_terms': Filter(self.model, self.filter_relation_depth).get_query_terms(),
|
107
109
|
'default_filter_term': self.default_filter_term,
|
108
110
|
'breadcrumbs': self.breadcrumbs,
|
109
|
-
'querystring':
|
111
|
+
'querystring': load_querystring(self.get_params),
|
112
|
+
'url_params': build_url_params(self.get_params),
|
110
113
|
'actions': self.actions,
|
111
|
-
'obj_label': self.obj_label or _('Name'),
|
112
114
|
'fields': self.fields
|
113
115
|
}
|
114
116
|
context.update(self.context)
|
@@ -118,7 +120,7 @@ class ListContext:
|
|
118
120
|
@dataclass
|
119
121
|
class DetailContext:
|
120
122
|
|
121
|
-
obj: Model
|
123
|
+
obj: Model | type[Model]
|
122
124
|
get_params: dict
|
123
125
|
order_by: str = None
|
124
126
|
paginate_by: int = DEFAULT_PAGINATE_BY
|
@@ -205,9 +207,8 @@ class DetailContext:
|
|
205
207
|
'order_by': self.get_params.get('order_by', self.obj._meta.model._meta.ordering),
|
206
208
|
'paginate_by': paginate_by,
|
207
209
|
'detail_pagination': False,
|
208
|
-
'view_type': 'detail',
|
209
210
|
'breadcrumbs': self.breadcrumbs,
|
210
|
-
'
|
211
|
+
'url_params': build_url_params(self.get_params, ['page']),
|
211
212
|
'actions': self.actions
|
212
213
|
}
|
213
214
|
if self.paginate_by > 0:
|
@@ -238,7 +239,7 @@ class FormContext:
|
|
238
239
|
)
|
239
240
|
]
|
240
241
|
try:
|
241
|
-
url = self.discard_url or self.model.get_absolute_url()
|
242
|
+
url = self.discard_url or (self.model.pk and self.model.get_absolute_url())
|
242
243
|
except TypeError:
|
243
244
|
raise TypeError(
|
244
245
|
'Supply the discard_url parameter if FormContext is called '
|
@@ -272,9 +273,8 @@ class FormContext:
|
|
272
273
|
def dict(self):
|
273
274
|
ctx = {
|
274
275
|
'title': self.get_title(),
|
275
|
-
'view_type': 'form',
|
276
276
|
'form_id': self.form_id,
|
277
|
-
'
|
277
|
+
'url_params': build_url_params(self.get_params, ['page']),
|
278
278
|
'actions': []
|
279
279
|
}
|
280
280
|
if self.add_default_actions:
|
accrete/contrib/ui/filter.py
CHANGED
@@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
|
|
7
7
|
class Filter:
|
8
8
|
|
9
9
|
query_relation_depth = 4
|
10
|
-
exclude_from_terms = []
|
11
10
|
query_float_fields = ['DecimalField', 'FloatField']
|
12
11
|
query_char_fields = ['CharField', 'TextField']
|
13
12
|
query_boolean_fields = ['BooleanField']
|
@@ -27,14 +26,9 @@ class Filter:
|
|
27
26
|
LABEL_SET = _('Is Set')
|
28
27
|
LABEL_NOT_SET = _('Is Not Set')
|
29
28
|
|
30
|
-
def __init__(
|
31
|
-
self, model,
|
32
|
-
query_relation_depth: int = 4,
|
33
|
-
exclude_from_terms: list = None,
|
34
|
-
):
|
29
|
+
def __init__(self, model, query_relation_depth: int = 4):
|
35
30
|
self.model = model
|
36
31
|
self.query_relation_depth = query_relation_depth
|
37
|
-
self.exclude_from_terms = exclude_from_terms or []
|
38
32
|
|
39
33
|
def get_choice_char_query_term(self, label, param, choices):
|
40
34
|
choices = [(choice[0], str(choice[1])) for choice in choices]
|
@@ -289,12 +283,10 @@ class Filter:
|
|
289
283
|
|
290
284
|
def get_relation_query_terms(self, model, path):
|
291
285
|
terms = []
|
292
|
-
|
293
|
-
|
286
|
+
filter_exclude = getattr(model, 'filter_exclude', [])
|
287
|
+
filter_exclude.append('tenant')
|
294
288
|
fields = filter(
|
295
|
-
lambda x:
|
296
|
-
x.is_relation and
|
297
|
-
x.name not in self.exclude_from_terms,
|
289
|
+
lambda x: x.is_relation and x.name not in filter_exclude,
|
298
290
|
sorted(model._meta.get_fields(), key=lambda x: x.name.lower())
|
299
291
|
)
|
300
292
|
cpath = path.copy()
|
@@ -317,10 +309,11 @@ class Filter:
|
|
317
309
|
|
318
310
|
def get_local_query_terms(self, model):
|
319
311
|
terms = []
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
312
|
+
filter_exclude = getattr(model, 'filter_exclude', [])
|
313
|
+
fields = filter(
|
314
|
+
lambda x: not x.is_relation and x.name not in filter_exclude,
|
315
|
+
model._meta.get_fields()
|
316
|
+
)
|
324
317
|
for field in fields:
|
325
318
|
term = self.get_query_term(field)
|
326
319
|
if field.null is True and term.get('params'):
|
@@ -1,85 +1,11 @@
|
|
1
1
|
import json
|
2
|
-
import logging
|
3
|
-
import operator
|
4
|
-
from django.db.models import Model, Q
|
5
2
|
|
6
|
-
_logger = logging.getLogger(__name__)
|
7
3
|
|
4
|
+
def load_querystring(get_params: dict) -> list:
|
5
|
+
return json.loads(get_params.get('q', '[]'))
|
8
6
|
|
9
|
-
def parse_querystring(model: type[Model], query_string: str) -> Q:
|
10
7
|
|
11
|
-
|
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:
|
8
|
+
def build_url_params(get_params: dict, extra_params: list[str] = None) -> str:
|
83
9
|
querystring = f'?q={get_params.get("q", "[]")}'
|
84
10
|
if paginate_by := get_params.get('paginate_by', False):
|
85
11
|
querystring += f'&paginate_by={paginate_by}'
|