accrete 0.0.35__py3-none-any.whl → 0.0.37__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/annotation.py +46 -0
- accrete/contrib/ui/__init__.py +12 -3
- accrete/contrib/ui/context.py +184 -257
- accrete/contrib/ui/elements.py +78 -2
- accrete/contrib/ui/filter.py +110 -44
- accrete/contrib/ui/static/css/accrete.css +128 -128
- accrete/contrib/ui/static/css/accrete.css.map +1 -1
- accrete/contrib/ui/static/css/accrete.scss +9 -9
- accrete/contrib/ui/static/js/filter.js +24 -0
- accrete/contrib/ui/static/js/htmx.min.js +1 -0
- accrete/contrib/ui/templates/ui/layout.html +134 -129
- accrete/contrib/ui/templates/ui/list.html +3 -3
- accrete/contrib/ui/templates/ui/partials/filter.html +29 -5
- accrete/contrib/ui/templates/ui/partials/header.html +7 -7
- accrete/contrib/ui/templates/ui/partials/pagination_detail.html +3 -3
- accrete/contrib/ui/templates/ui/partials/pagination_list.html +4 -4
- accrete/contrib/ui/templates/ui/table.html +18 -13
- accrete/contrib/ui/templatetags/accrete_ui.py +12 -1
- accrete/contrib/user/forms.py +0 -4
- accrete/contrib/user/templates/user/login.html +6 -12
- accrete/contrib/user/views.py +31 -21
- accrete/middleware.py +15 -0
- accrete/models.py +9 -7
- accrete/querystring.py +11 -8
- accrete/utils/models.py +14 -0
- {accrete-0.0.35.dist-info → accrete-0.0.37.dist-info}/METADATA +1 -1
- {accrete-0.0.35.dist-info → accrete-0.0.37.dist-info}/RECORD +29 -28
- accrete/contrib/ui/components.py +0 -96
- accrete/contrib/ui/querystring.py +0 -19
- {accrete-0.0.35.dist-info → accrete-0.0.37.dist-info}/WHEEL +0 -0
- {accrete-0.0.35.dist-info → accrete-0.0.37.dist-info}/licenses/LICENSE +0 -0
accrete/contrib/ui/filter.py
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
import time
|
2
2
|
import logging
|
3
|
+
from django.db import models
|
3
4
|
from django.core.cache import cache
|
4
5
|
from django.utils.translation import gettext_lazy as _
|
5
6
|
from django.utils.safestring import mark_safe
|
7
|
+
from django.utils.translation import get_language
|
8
|
+
|
9
|
+
from accrete.annotation import Annotation
|
6
10
|
|
7
11
|
_logger = logging.getLogger(__name__)
|
8
12
|
|
@@ -22,11 +26,15 @@ class Filter:
|
|
22
26
|
LABEL_SET = _('Is Set')
|
23
27
|
LABEL_NOT_SET = _('Is Not Set')
|
24
28
|
|
25
|
-
def __init__(
|
29
|
+
def __init__(
|
30
|
+
self, model, query_relation_depth: int = 4,
|
31
|
+
default_exclude: list[str] = None, default_filter_term: str = None
|
32
|
+
):
|
26
33
|
self.model = model
|
27
34
|
self.query_relation_depth = query_relation_depth
|
28
35
|
self.default_exclude = default_exclude or ['tenant', 'user']
|
29
|
-
self.
|
36
|
+
self.default_filter_term = default_filter_term or ''
|
37
|
+
self.fields = []
|
30
38
|
self.field_paths = []
|
31
39
|
|
32
40
|
@staticmethod
|
@@ -39,8 +47,8 @@ class Filter:
|
|
39
47
|
def get_fields(self, model_path: list, field_path: str):
|
40
48
|
fields = self.get_local_fields(model_path[-1], field_path)
|
41
49
|
if len(model_path) <= self.query_relation_depth:
|
42
|
-
fields.
|
43
|
-
return fields
|
50
|
+
fields.extend(self.get_relation_fields(model_path, field_path))
|
51
|
+
return sorted(fields, key=lambda x: x['label'].lower())
|
44
52
|
|
45
53
|
def get_relation_fields(self, model_path, field_path):
|
46
54
|
filter_exclude = getattr(model_path[-1], 'filter_exclude', [])
|
@@ -49,7 +57,7 @@ class Filter:
|
|
49
57
|
lambda x: x.is_relation and x.name not in filter_exclude,
|
50
58
|
model_path[-1]._meta.get_fields()
|
51
59
|
)
|
52
|
-
res =
|
60
|
+
res = []
|
53
61
|
for field in fields:
|
54
62
|
if field.related_model in model_path:
|
55
63
|
continue
|
@@ -60,13 +68,14 @@ class Filter:
|
|
60
68
|
label = field.verbose_name
|
61
69
|
except AttributeError:
|
62
70
|
label = field.related_model._meta.verbose_name
|
63
|
-
res
|
64
|
-
'
|
71
|
+
res.append({
|
72
|
+
'name': field.name,
|
73
|
+
'label': str(label),
|
65
74
|
'type': field.get_internal_type(),
|
66
75
|
'null': field.null,
|
67
76
|
'choices': [],
|
68
77
|
'fields': self.get_fields(model_path_copy, rel_path)
|
69
|
-
}
|
78
|
+
})
|
70
79
|
return res
|
71
80
|
|
72
81
|
def get_local_fields(self, model, path):
|
@@ -76,34 +85,39 @@ class Filter:
|
|
76
85
|
lambda x: not x.is_relation and x.name not in filter_exclude,
|
77
86
|
model._meta.get_fields()
|
78
87
|
)
|
79
|
-
res =
|
88
|
+
res = []
|
80
89
|
for field in fields:
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
90
|
+
field_path = f'{path}{"__" if path else ""}{field.name}'
|
91
|
+
self.field_paths.append(field_path)
|
92
|
+
step = (hasattr(field, 'decimal_places')
|
93
|
+
and self.cast_decimal_places_to_step(field.decimal_places)
|
94
|
+
or 1)
|
95
|
+
res.append({
|
96
|
+
'name': field_path,
|
97
|
+
'label': str(field.verbose_name),
|
98
|
+
'type': field.get_internal_type(),
|
99
|
+
'choices': field.choices or [],
|
100
|
+
'null': field.null,
|
101
|
+
'step': step
|
91
102
|
})
|
92
|
-
|
93
|
-
res
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
103
|
+
if not hasattr(model, 'get_annotations'):
|
104
|
+
return res
|
105
|
+
for annotation in model.get_annotations():
|
106
|
+
field_path = f'{path}{"__" if path else ""}{annotation["name"]}'
|
107
|
+
self.field_paths.append(field_path)
|
108
|
+
res.append({
|
109
|
+
'name': field_path,
|
110
|
+
'label': str(annotation['annotation'].verbose_name),
|
111
|
+
'type': annotation['annotation'].field.__name__,
|
112
|
+
'choices': [],
|
113
|
+
'null': False,
|
114
|
+
'step': getattr(annotation['annotation'], 'step', '1')
|
101
115
|
})
|
102
116
|
return res
|
103
117
|
|
104
118
|
def to_html(self):
|
105
119
|
start = time.time()
|
106
|
-
key = f'filter-
|
120
|
+
key = f'filter-{self.model.__module__}.{self.model.__name__}-{get_language()}'
|
107
121
|
html = cache.get(key)
|
108
122
|
if html:
|
109
123
|
_logger.info(f'Fetching Filter-Terms from cache in {time.time() - start} seconds')
|
@@ -111,22 +125,27 @@ class Filter:
|
|
111
125
|
if not self.fields:
|
112
126
|
self.fields = self.get_fields([self.model], '')
|
113
127
|
html = ''
|
114
|
-
for
|
115
|
-
html += self.field_params(
|
116
|
-
html =
|
128
|
+
for f in self.fields:
|
129
|
+
html += self.field_params(f)
|
130
|
+
html = {
|
131
|
+
'params': mark_safe(html.strip().replace('\n', '')),
|
132
|
+
'field_paths': mark_safe(
|
133
|
+
self.field_path_selection().strip().replace('\n', '')
|
134
|
+
)
|
135
|
+
}
|
117
136
|
cache.set(key, html, 60 * 15)
|
118
137
|
_logger.info(f'Generated Filter-Terms in {time.time() - start} seconds')
|
119
|
-
return html
|
138
|
+
return {'params': html['params'], 'field_paths': html['field_paths']}
|
120
139
|
|
121
|
-
def field_params(self,
|
140
|
+
def field_params(self, field):
|
122
141
|
params = ''
|
123
|
-
params += self.params(
|
124
|
-
for
|
125
|
-
params += self.field_params(
|
142
|
+
params += self.params(field)
|
143
|
+
for f in field.get('fields', []):
|
144
|
+
params += self.field_params(f)
|
126
145
|
return f"""
|
127
|
-
<div class="query-param" tabindex="-1" data-param="{
|
128
|
-
<p class="px-1 arrow">{
|
129
|
-
<div class="query-params is-hidden" data-param="{
|
146
|
+
<div class="query-param" tabindex="-1" data-param="{field['name']}" data-param-label="{field['label']}">
|
147
|
+
<p class="px-1 arrow">{field['label']}</p>
|
148
|
+
<div class="query-params is-hidden" data-param="{field['name']}">
|
130
149
|
{params}
|
131
150
|
</div>
|
132
151
|
</div>
|
@@ -154,8 +173,8 @@ class Filter:
|
|
154
173
|
for choice in choices
|
155
174
|
])
|
156
175
|
|
157
|
-
def params(self,
|
158
|
-
return self.field_map().get(
|
176
|
+
def params(self, field):
|
177
|
+
return self.field_map().get(field['type'], self.no_param)(field['name'], field)
|
159
178
|
|
160
179
|
def param(
|
161
180
|
self, key: str, value: dict, param: str, data_type: str,
|
@@ -191,7 +210,6 @@ class Filter:
|
|
191
210
|
html += param_div(inverted=True)
|
192
211
|
return html
|
193
212
|
|
194
|
-
|
195
213
|
def char_param(self, key, value):
|
196
214
|
if value.get('choices'):
|
197
215
|
return self.char_choice_param(key, value)
|
@@ -258,7 +276,10 @@ class Filter:
|
|
258
276
|
return ''
|
259
277
|
|
260
278
|
def null_param(self, key, value):
|
261
|
-
options = self.parse_choices([
|
279
|
+
options = self.parse_choices([
|
280
|
+
('true', _('True')),
|
281
|
+
('false', _('False'))
|
282
|
+
])
|
262
283
|
return f"""
|
263
284
|
<div id="filter-id-~{key}__isnull"
|
264
285
|
class="query-param" tabindex="-1" data-type="selection"
|
@@ -274,3 +295,48 @@ class Filter:
|
|
274
295
|
|
275
296
|
def no_param(self, key, value):
|
276
297
|
return ''
|
298
|
+
|
299
|
+
def field_path_selection(self):
|
300
|
+
html = ''
|
301
|
+
for field in sorted(self.field_paths, key=lambda f: (len(f.split('__')), f.lower())):
|
302
|
+
label = self.get_field_path_label(field)
|
303
|
+
if not label:
|
304
|
+
continue
|
305
|
+
html += f"""
|
306
|
+
<label class="checkbox my-1" style="width: 100%">
|
307
|
+
<input type="checkbox" name="{field}">
|
308
|
+
{label}
|
309
|
+
</label>
|
310
|
+
"""
|
311
|
+
return html
|
312
|
+
|
313
|
+
def get_field_path_label(self, field_path):
|
314
|
+
label_parts = []
|
315
|
+
model = self.model
|
316
|
+
for field_part in field_path.split('__'):
|
317
|
+
if field_part.startswith('_a_'):
|
318
|
+
field_part = field_part.removeprefix('_a_')
|
319
|
+
annotations = {
|
320
|
+
annotation['name']: annotation
|
321
|
+
for annotation in getattr(model, 'annotations', [])
|
322
|
+
}
|
323
|
+
if annotations.get(field_part):
|
324
|
+
label_parts.append(str(annotations[field_part]['label']))
|
325
|
+
return ' / '.join(label_parts)
|
326
|
+
return False
|
327
|
+
|
328
|
+
field = getattr(model, field_part, False)
|
329
|
+
|
330
|
+
if not field:
|
331
|
+
return False
|
332
|
+
|
333
|
+
if isinstance(field.field, models.ForeignKey):
|
334
|
+
model = field.field.related_model
|
335
|
+
label_parts.append(str(model._meta.verbose_name))
|
336
|
+
continue
|
337
|
+
|
338
|
+
if not isinstance(field, Annotation):
|
339
|
+
field = field.field
|
340
|
+
|
341
|
+
label_parts.append(str(field.verbose_name))
|
342
|
+
return ' / '.join(label_parts)
|