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.
Files changed (31) hide show
  1. accrete/annotation.py +46 -0
  2. accrete/contrib/ui/__init__.py +12 -3
  3. accrete/contrib/ui/context.py +184 -257
  4. accrete/contrib/ui/elements.py +78 -2
  5. accrete/contrib/ui/filter.py +110 -44
  6. accrete/contrib/ui/static/css/accrete.css +128 -128
  7. accrete/contrib/ui/static/css/accrete.css.map +1 -1
  8. accrete/contrib/ui/static/css/accrete.scss +9 -9
  9. accrete/contrib/ui/static/js/filter.js +24 -0
  10. accrete/contrib/ui/static/js/htmx.min.js +1 -0
  11. accrete/contrib/ui/templates/ui/layout.html +134 -129
  12. accrete/contrib/ui/templates/ui/list.html +3 -3
  13. accrete/contrib/ui/templates/ui/partials/filter.html +29 -5
  14. accrete/contrib/ui/templates/ui/partials/header.html +7 -7
  15. accrete/contrib/ui/templates/ui/partials/pagination_detail.html +3 -3
  16. accrete/contrib/ui/templates/ui/partials/pagination_list.html +4 -4
  17. accrete/contrib/ui/templates/ui/table.html +18 -13
  18. accrete/contrib/ui/templatetags/accrete_ui.py +12 -1
  19. accrete/contrib/user/forms.py +0 -4
  20. accrete/contrib/user/templates/user/login.html +6 -12
  21. accrete/contrib/user/views.py +31 -21
  22. accrete/middleware.py +15 -0
  23. accrete/models.py +9 -7
  24. accrete/querystring.py +11 -8
  25. accrete/utils/models.py +14 -0
  26. {accrete-0.0.35.dist-info → accrete-0.0.37.dist-info}/METADATA +1 -1
  27. {accrete-0.0.35.dist-info → accrete-0.0.37.dist-info}/RECORD +29 -28
  28. accrete/contrib/ui/components.py +0 -96
  29. accrete/contrib/ui/querystring.py +0 -19
  30. {accrete-0.0.35.dist-info → accrete-0.0.37.dist-info}/WHEEL +0 -0
  31. {accrete-0.0.35.dist-info → accrete-0.0.37.dist-info}/licenses/LICENSE +0 -0
@@ -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__(self, model, query_relation_depth: int = 4, default_exclude: list = None):
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.fields = {}
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.update(self.get_relation_fields(model_path, field_path))
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[field.name] = {
64
- 'label': label,
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
- res.update({
82
- f'{path}{"__" if path else ""}{field.name}': {
83
- 'label': field.verbose_name,
84
- 'type': field.get_internal_type(),
85
- 'choices': field.choices or [],
86
- 'null': field.null,
87
- 'step': hasattr(field, 'decimal_places')
88
- and self.cast_decimal_places_to_step(field.decimal_places)
89
- or 1
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
- for annotation in getattr(model, 'annotations', []):
93
- res.update({
94
- f'{path}{"__" if path else ""}_a_{annotation["name"]}': {
95
- 'label': annotation['label'],
96
- 'type': annotation['type'].__name__,
97
- 'choices': [],
98
- 'null': False,
99
- 'step': self.cast_decimal_places_to_step(annotation.get('step', 1))
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-terms-{self.model.__module__}.{self.model.__name__}'
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 k, v in self.fields.items():
115
- html += self.field_params(k, v)
116
- html = mark_safe(html.strip().replace('\n', ''))
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, key, value):
140
+ def field_params(self, field):
122
141
  params = ''
123
- params += self.params(key, value)
124
- for k, v in value.get('fields', {}).items():
125
- params += self.field_params(k, v)
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="{key}" data-param-label="{value['label']}">
128
- <p class="px-1 arrow">{value['label']}</p>
129
- <div class="query-params is-hidden" data-param="{key}">
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, key, value):
158
- return self.field_map().get(value['type'], self.no_param)(key, value)
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([('true', _('True')), ('false', _('False'))])
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)