accrete 0.0.36__py3-none-any.whl → 0.0.38__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 +5 -12
- accrete/contrib/ui/context.py +111 -218
- accrete/contrib/ui/filter.py +105 -43
- 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 +23 -0
- accrete/contrib/ui/static/js/htmx.min.js +1 -0
- accrete/contrib/ui/templates/ui/layout.html +133 -131
- accrete/contrib/ui/templates/ui/list.html +2 -2
- accrete/contrib/ui/templates/ui/partials/filter.html +28 -4
- accrete/contrib/ui/templates/ui/partials/header.html +5 -5
- accrete/contrib/ui/templates/ui/table.html +1 -1
- accrete/contrib/ui/templatetags/accrete_ui.py +10 -6
- accrete/contrib/user/templates/user/login.html +6 -12
- accrete/contrib/user/views.py +10 -9
- accrete/middleware.py +15 -0
- accrete/models.py +9 -7
- accrete/querystring.py +16 -25
- accrete/utils/models.py +19 -0
- {accrete-0.0.36.dist-info → accrete-0.0.38.dist-info}/METADATA +1 -1
- {accrete-0.0.36.dist-info → accrete-0.0.38.dist-info}/RECORD +25 -23
- accrete/contrib/ui/querystring.py +0 -19
- {accrete-0.0.36.dist-info → accrete-0.0.38.dist-info}/WHEEL +0 -0
- {accrete-0.0.36.dist-info → accrete-0.0.38.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
|
|
@@ -30,7 +34,7 @@ class Filter:
|
|
30
34
|
self.query_relation_depth = query_relation_depth
|
31
35
|
self.default_exclude = default_exclude or ['tenant', 'user']
|
32
36
|
self.default_filter_term = default_filter_term or ''
|
33
|
-
self.fields =
|
37
|
+
self.fields = []
|
34
38
|
self.field_paths = []
|
35
39
|
|
36
40
|
@staticmethod
|
@@ -43,8 +47,8 @@ class Filter:
|
|
43
47
|
def get_fields(self, model_path: list, field_path: str):
|
44
48
|
fields = self.get_local_fields(model_path[-1], field_path)
|
45
49
|
if len(model_path) <= self.query_relation_depth:
|
46
|
-
fields.
|
47
|
-
return fields
|
50
|
+
fields.extend(self.get_relation_fields(model_path, field_path))
|
51
|
+
return sorted(fields, key=lambda x: x['label'].lower())
|
48
52
|
|
49
53
|
def get_relation_fields(self, model_path, field_path):
|
50
54
|
filter_exclude = getattr(model_path[-1], 'filter_exclude', [])
|
@@ -53,7 +57,7 @@ class Filter:
|
|
53
57
|
lambda x: x.is_relation and x.name not in filter_exclude,
|
54
58
|
model_path[-1]._meta.get_fields()
|
55
59
|
)
|
56
|
-
res =
|
60
|
+
res = []
|
57
61
|
for field in fields:
|
58
62
|
if field.related_model in model_path:
|
59
63
|
continue
|
@@ -64,13 +68,14 @@ class Filter:
|
|
64
68
|
label = field.verbose_name
|
65
69
|
except AttributeError:
|
66
70
|
label = field.related_model._meta.verbose_name
|
67
|
-
res
|
68
|
-
'
|
71
|
+
res.append({
|
72
|
+
'name': f'{rel_path}',
|
73
|
+
'label': str(label),
|
69
74
|
'type': field.get_internal_type(),
|
70
75
|
'null': field.null,
|
71
76
|
'choices': [],
|
72
77
|
'fields': self.get_fields(model_path_copy, rel_path)
|
73
|
-
}
|
78
|
+
})
|
74
79
|
return res
|
75
80
|
|
76
81
|
def get_local_fields(self, model, path):
|
@@ -80,34 +85,39 @@ class Filter:
|
|
80
85
|
lambda x: not x.is_relation and x.name not in filter_exclude,
|
81
86
|
model._meta.get_fields()
|
82
87
|
)
|
83
|
-
res =
|
88
|
+
res = []
|
84
89
|
for field in fields:
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
95
102
|
})
|
96
|
-
|
97
|
-
res
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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')
|
105
115
|
})
|
106
116
|
return res
|
107
117
|
|
108
118
|
def to_html(self):
|
109
119
|
start = time.time()
|
110
|
-
key = f'filter-
|
120
|
+
key = f'filter-{self.model.__module__}.{self.model.__name__}-{get_language()}'
|
111
121
|
html = cache.get(key)
|
112
122
|
if html:
|
113
123
|
_logger.info(f'Fetching Filter-Terms from cache in {time.time() - start} seconds')
|
@@ -115,22 +125,27 @@ class Filter:
|
|
115
125
|
if not self.fields:
|
116
126
|
self.fields = self.get_fields([self.model], '')
|
117
127
|
html = ''
|
118
|
-
for
|
119
|
-
html += self.field_params(
|
120
|
-
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
|
+
}
|
121
136
|
cache.set(key, html, 60 * 15)
|
122
137
|
_logger.info(f'Generated Filter-Terms in {time.time() - start} seconds')
|
123
|
-
return html
|
138
|
+
return {'params': html['params'], 'field_paths': html['field_paths']}
|
124
139
|
|
125
|
-
def field_params(self,
|
140
|
+
def field_params(self, field):
|
126
141
|
params = ''
|
127
|
-
params += self.params(
|
128
|
-
for
|
129
|
-
params += self.field_params(
|
142
|
+
params += self.params(field)
|
143
|
+
for f in field.get('fields', []):
|
144
|
+
params += self.field_params(f)
|
130
145
|
return f"""
|
131
|
-
<div class="query-param" tabindex="-1" data-param="{
|
132
|
-
<p class="px-1 arrow">{
|
133
|
-
<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']}">
|
134
149
|
{params}
|
135
150
|
</div>
|
136
151
|
</div>
|
@@ -158,8 +173,8 @@ class Filter:
|
|
158
173
|
for choice in choices
|
159
174
|
])
|
160
175
|
|
161
|
-
def params(self,
|
162
|
-
return self.field_map().get(
|
176
|
+
def params(self, field):
|
177
|
+
return self.field_map().get(field['type'], self.no_param)(field['name'], field)
|
163
178
|
|
164
179
|
def param(
|
165
180
|
self, key: str, value: dict, param: str, data_type: str,
|
@@ -195,7 +210,6 @@ class Filter:
|
|
195
210
|
html += param_div(inverted=True)
|
196
211
|
return html
|
197
212
|
|
198
|
-
|
199
213
|
def char_param(self, key, value):
|
200
214
|
if value.get('choices'):
|
201
215
|
return self.char_choice_param(key, value)
|
@@ -262,7 +276,10 @@ class Filter:
|
|
262
276
|
return ''
|
263
277
|
|
264
278
|
def null_param(self, key, value):
|
265
|
-
options = self.parse_choices([
|
279
|
+
options = self.parse_choices([
|
280
|
+
('true', _('True')),
|
281
|
+
('false', _('False'))
|
282
|
+
])
|
266
283
|
return f"""
|
267
284
|
<div id="filter-id-~{key}__isnull"
|
268
285
|
class="query-param" tabindex="-1" data-type="selection"
|
@@ -278,3 +295,48 @@ class Filter:
|
|
278
295
|
|
279
296
|
def no_param(self, key, value):
|
280
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)
|