accrete 0.0.36__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 +5 -12
- accrete/contrib/ui/context.py +91 -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 +12 -1
- 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 +3 -9
- accrete/utils/models.py +14 -0
- {accrete-0.0.36.dist-info → accrete-0.0.37.dist-info}/METADATA +1 -1
- {accrete-0.0.36.dist-info → accrete-0.0.37.dist-info}/RECORD +25 -23
- accrete/contrib/ui/querystring.py +0 -19
- {accrete-0.0.36.dist-info → accrete-0.0.37.dist-info}/WHEEL +0 -0
- {accrete-0.0.36.dist-info → accrete-0.0.37.dist-info}/licenses/LICENSE +0 -0
accrete/annotation.py
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
from django.db.models import Field, QuerySet
|
2
|
+
from django.db.models.expressions import Func
|
3
|
+
from django.db.models.aggregates import Aggregate
|
4
|
+
|
5
|
+
|
6
|
+
class Annotation:
|
7
|
+
|
8
|
+
def __init__(
|
9
|
+
self,
|
10
|
+
verbose_name: str,
|
11
|
+
field: type[Field],
|
12
|
+
function: type[Func] | type[Aggregate],
|
13
|
+
help_text: str = '',
|
14
|
+
**kwargs
|
15
|
+
):
|
16
|
+
self.verbose_name = verbose_name or self
|
17
|
+
self.field = field
|
18
|
+
self.function = function
|
19
|
+
self.help_text = help_text
|
20
|
+
self.__dict__.update(kwargs)
|
21
|
+
|
22
|
+
|
23
|
+
class AnnotationModelMixin:
|
24
|
+
|
25
|
+
@classmethod
|
26
|
+
def get_annotations(cls) -> list[dict]:
|
27
|
+
return list({'name': a, 'annotation': getattr(cls, a)} for a in filter(
|
28
|
+
lambda a:
|
29
|
+
not a.startswith('__')
|
30
|
+
and isinstance(getattr(cls, a), Annotation),
|
31
|
+
cls.__dict__
|
32
|
+
))
|
33
|
+
|
34
|
+
|
35
|
+
class AnnotationManagerMixin:
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
def add_annotations(queryset: QuerySet):
|
39
|
+
model = queryset.model
|
40
|
+
if not hasattr(model, 'get_annotations'):
|
41
|
+
return queryset
|
42
|
+
return queryset.annotate(**{
|
43
|
+
annotation['name']: annotation['annotation'].function
|
44
|
+
for annotation in model.get_annotations()
|
45
|
+
})
|
46
|
+
|
accrete/contrib/ui/__init__.py
CHANGED
@@ -4,18 +4,15 @@ from .context import (
|
|
4
4
|
TableContext,
|
5
5
|
ListContext,
|
6
6
|
FormContext,
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
DetailContent,
|
11
|
-
FormContent,
|
12
|
-
get_list_page,
|
13
|
-
get_detail_page,
|
7
|
+
form_actions,
|
8
|
+
list_page,
|
9
|
+
detail_page,
|
14
10
|
cast_param,
|
15
11
|
prepare_url_params,
|
16
12
|
extract_url_params,
|
17
13
|
exclude_params,
|
18
|
-
url_param_str
|
14
|
+
url_param_str,
|
15
|
+
get_table_fields
|
19
16
|
)
|
20
17
|
from .elements import (
|
21
18
|
ClientAction,
|
@@ -26,7 +23,3 @@ from .elements import (
|
|
26
23
|
TableFieldType,
|
27
24
|
Icon
|
28
25
|
)
|
29
|
-
from .querystring import (
|
30
|
-
load_querystring,
|
31
|
-
build_querystring,
|
32
|
-
)
|
accrete/contrib/ui/context.py
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
import logging
|
2
|
-
from typing import TypedDict
|
3
2
|
from dataclasses import dataclass, field
|
4
|
-
from
|
5
|
-
from django.
|
3
|
+
from typing import TypedDict
|
4
|
+
from django.utils.translation import gettext_lazy as _t
|
6
5
|
from django.db.models import Model, QuerySet
|
7
6
|
from django.core.paginator import Paginator
|
8
7
|
from django.core import paginator
|
9
8
|
from django.forms import Form, ModelForm
|
10
|
-
from .
|
9
|
+
from accrete.utils.models import get_related_model
|
10
|
+
from .elements import ClientAction, BreadCrumb, TableField, TableFieldType
|
11
11
|
from .filter import Filter
|
12
|
+
from ...annotation import Annotation
|
12
13
|
|
13
14
|
_logger = logging.getLogger(__name__)
|
14
15
|
|
@@ -22,31 +23,55 @@ class DetailPagination(TypedDict):
|
|
22
23
|
total_objects: int
|
23
24
|
|
24
25
|
|
25
|
-
|
26
|
+
@dataclass(kw_only=True)
|
27
|
+
class Context:
|
28
|
+
|
29
|
+
title: str = ''
|
30
|
+
breadcrumbs: list[BreadCrumb] = field(default_factory=list)
|
31
|
+
actions: list[ClientAction] = field(default_factory=list)
|
32
|
+
kwargs: dict = field(default_factory=dict)
|
33
|
+
|
34
|
+
def __post_init__(self):
|
35
|
+
for key, value in self.kwargs.items():
|
36
|
+
setattr(self, key, value)
|
37
|
+
|
38
|
+
def dict(self):
|
39
|
+
return {
|
40
|
+
attr: getattr(self, attr, '') for attr
|
41
|
+
in filter(lambda x: not x.startswith('_'), self.__dict__)
|
42
|
+
}
|
43
|
+
|
44
|
+
|
45
|
+
@dataclass
|
46
|
+
class TableContext(Context):
|
26
47
|
|
27
|
-
title: str
|
28
48
|
object_label: str
|
29
|
-
object_param_str: str
|
30
49
|
fields: list[TableField]
|
31
|
-
breadcrumbs: list[BreadCrumb]
|
32
|
-
actions: list[ClientAction]
|
33
50
|
list_page: paginator.Page
|
34
51
|
pagination_param_str: str
|
35
52
|
endless_scroll: bool
|
36
53
|
filter: Filter
|
54
|
+
object_param_str: str = field(default='', kw_only=True)
|
37
55
|
|
38
56
|
|
39
|
-
|
57
|
+
@dataclass
|
58
|
+
class ListContext(Context):
|
40
59
|
|
41
|
-
title: str
|
42
60
|
object_label: str
|
43
61
|
object_param_str: str
|
44
|
-
breadcrumbs: list[BreadCrumb]
|
45
|
-
actions: list[ClientAction]
|
46
62
|
list_page: paginator.Page
|
47
63
|
pagination_param_str: str
|
48
|
-
endless_scroll: bool
|
49
64
|
filter: Filter
|
65
|
+
endless_scroll: bool = True
|
66
|
+
column_height: int = 4
|
67
|
+
column_width: int = 12
|
68
|
+
|
69
|
+
def __post_init__(self):
|
70
|
+
super().__post_init__()
|
71
|
+
if self.column_width not in range(1, 13):
|
72
|
+
_logger.warning(
|
73
|
+
'ListContext parameter column_width should be in range 1 - 12'
|
74
|
+
)
|
50
75
|
|
51
76
|
|
52
77
|
class DetailContext(TypedDict, total=False):
|
@@ -59,7 +84,7 @@ class DetailContext(TypedDict, total=False):
|
|
59
84
|
actions: list[ClientAction]
|
60
85
|
|
61
86
|
|
62
|
-
class FormContext(TypedDict):
|
87
|
+
class FormContext(TypedDict, total=False):
|
63
88
|
|
64
89
|
title: str
|
65
90
|
breadcrumbs: list[BreadCrumb]
|
@@ -68,204 +93,9 @@ class FormContext(TypedDict):
|
|
68
93
|
actions: list[ClientAction]
|
69
94
|
|
70
95
|
|
71
|
-
# @dataclass
|
72
|
-
# class ListContext:
|
73
|
-
#
|
74
|
-
# title: str
|
75
|
-
# actions: list[ClientAction] = field(default_factory=list)
|
76
|
-
# object_label: str = ''
|
77
|
-
# fields: list[TableField] = field(default_factory=list)
|
78
|
-
# page: paginator.Page = None
|
79
|
-
# list_pagination: bool = True
|
80
|
-
# endless_scroll: bool = True
|
81
|
-
# params: dict = field(default_factory=dict)
|
82
|
-
# filter: Filter = None
|
83
|
-
#
|
84
|
-
# def dict(self):
|
85
|
-
# return {
|
86
|
-
# "title": self.title,
|
87
|
-
# "actions": self.actions,
|
88
|
-
# "object_label": self.object_label,
|
89
|
-
# "fields": self.fields,
|
90
|
-
# "page": self.page,
|
91
|
-
# "list_pagination": self.list_pagination,
|
92
|
-
# "endless_scroll": self.endless_scroll,
|
93
|
-
# "params": self.params,
|
94
|
-
# "filter": self.filter
|
95
|
-
# }
|
96
|
-
|
97
|
-
|
98
|
-
class GenericContent:
|
99
|
-
|
100
|
-
def __init__(self, *args, **kwargs):
|
101
|
-
self.page: Page | None = None
|
102
|
-
|
103
|
-
def dict(self):
|
104
|
-
return {}
|
105
|
-
|
106
|
-
|
107
|
-
class ListContent:
|
108
|
-
|
109
|
-
def __init__(
|
110
|
-
self, queryset: QuerySet, paginate_by: int = DEFAULT_PAGINATE_BY,
|
111
|
-
page_number: int = 1, filter_obj: Filter = None,
|
112
|
-
endless_scroll: bool = True, fields: list[TableField] = None,
|
113
|
-
object_label: str = None, object_url_params: dict = None
|
114
|
-
):
|
115
|
-
self.queryset = queryset
|
116
|
-
self.paginate_by = paginate_by or DEFAULT_PAGINATE_BY
|
117
|
-
self.page_number = page_number
|
118
|
-
self.filter = filter_obj
|
119
|
-
self.endless_scroll = endless_scroll
|
120
|
-
self.fields = fields or []
|
121
|
-
self.object_label = object_label or _('Name')
|
122
|
-
self.object_url_params = object_url_params or {}
|
123
|
-
self.page: Page | None = None
|
124
|
-
|
125
|
-
def get_page_number(self, paginator):
|
126
|
-
if self.page_number < 1:
|
127
|
-
return 1
|
128
|
-
if self.page_number > paginator.num_pages:
|
129
|
-
return paginator.num_pages
|
130
|
-
return self.page_number
|
131
|
-
|
132
|
-
def dict(self):
|
133
|
-
paginator = Paginator(self.queryset, self.paginate_by)
|
134
|
-
page = paginator.page(self.get_page_number(paginator))
|
135
|
-
context = {
|
136
|
-
'page': page,
|
137
|
-
'object_label': self.object_label,
|
138
|
-
'object_url_params': url_param_str(prepare_url_params(self.object_url_params)),
|
139
|
-
'fields': self.fields,
|
140
|
-
'list_pagination': True if self.paginate_by > 0 else False,
|
141
|
-
'filter': self.filter,
|
142
|
-
'endless_scroll': self.endless_scroll
|
143
|
-
}
|
144
|
-
return context
|
145
|
-
|
146
|
-
|
147
|
-
class DetailContent:
|
148
|
-
|
149
|
-
def __init__(
|
150
|
-
self, obj: Model | type[Model], queryset: QuerySet = None,
|
151
|
-
):
|
152
|
-
self.obj = obj
|
153
|
-
self.queryset = queryset
|
154
|
-
self.page: Page | None = None
|
155
|
-
|
156
|
-
def get_detail_pagination(self):
|
157
|
-
return detail_pagination(self.queryset, self.obj)
|
158
|
-
|
159
|
-
def dict(self):
|
160
|
-
ctx = {
|
161
|
-
'object': self.obj,
|
162
|
-
'detail_pagination': False,
|
163
|
-
}
|
164
|
-
if self.queryset:
|
165
|
-
ctx.update(self.get_detail_pagination())
|
166
|
-
return ctx
|
167
|
-
|
168
|
-
|
169
|
-
class FormContent:
|
170
|
-
|
171
|
-
def __init__(
|
172
|
-
self, model: Model | type[Model], form: Form | ModelForm,
|
173
|
-
form_id: str = 'form', add_default_actions: bool = True,
|
174
|
-
discard_url: str = None
|
175
|
-
):
|
176
|
-
self.model = model
|
177
|
-
self.form = form
|
178
|
-
self.form_id = form_id
|
179
|
-
self.add_default_actions = add_default_actions
|
180
|
-
self.discard_url = discard_url
|
181
|
-
self.page: Page | None = None
|
182
|
-
|
183
|
-
def add_default_form_actions(self):
|
184
|
-
actions = [
|
185
|
-
ClientAction(
|
186
|
-
name=_('Save'),
|
187
|
-
submit=True,
|
188
|
-
class_list=['is-success'],
|
189
|
-
form_id=self.form_id
|
190
|
-
)
|
191
|
-
]
|
192
|
-
try:
|
193
|
-
url = self.discard_url or (self.model.pk and self.model.get_absolute_url())
|
194
|
-
except TypeError:
|
195
|
-
raise TypeError(
|
196
|
-
'Supply the discard_url parameter if Form is called '
|
197
|
-
'with a model class instead of an instance.'
|
198
|
-
)
|
199
|
-
except AttributeError as e:
|
200
|
-
_logger.error(
|
201
|
-
'Supply the discard_url parameter if Form is '
|
202
|
-
'called with a model instance that has the get_absolute_url '
|
203
|
-
'method not defined.'
|
204
|
-
)
|
205
|
-
raise e
|
206
|
-
|
207
|
-
actions.append(
|
208
|
-
ClientAction(
|
209
|
-
name=_('Discard'),
|
210
|
-
url=url,
|
211
|
-
)
|
212
|
-
)
|
213
|
-
if self.page:
|
214
|
-
self.page.actions = actions.extend(self.page.actions)
|
215
|
-
|
216
|
-
def get_title(self):
|
217
|
-
try:
|
218
|
-
int(self.model.pk)
|
219
|
-
return _('Edit')
|
220
|
-
except TypeError:
|
221
|
-
return _('Add')
|
222
|
-
except Exception as e:
|
223
|
-
_logger.exception(e)
|
224
|
-
return ''
|
225
|
-
|
226
|
-
def dict(self):
|
227
|
-
ctx = {
|
228
|
-
'form': self.form,
|
229
|
-
'form_id': self.form_id,
|
230
|
-
}
|
231
|
-
if self.add_default_actions:
|
232
|
-
self.add_default_form_actions()
|
233
|
-
if self.page and not self.page.title:
|
234
|
-
self.page.title = self.get_title()
|
235
|
-
return ctx
|
236
|
-
|
237
|
-
|
238
|
-
class Page:
|
239
|
-
|
240
|
-
def __init__(
|
241
|
-
self, *, title: str = None,
|
242
|
-
content: GenericContent | ListContent | DetailContent | FormContent = None,
|
243
|
-
breadcrumbs: list[BreadCrumb] = None, get_params: dict = None,
|
244
|
-
actions: list[ClientAction] = None,
|
245
|
-
):
|
246
|
-
self.title = title or ''
|
247
|
-
self.content = content
|
248
|
-
self.breadcrumbs = breadcrumbs or []
|
249
|
-
self.actions = actions or []
|
250
|
-
self.get_params = get_params or {}
|
251
|
-
if self.content:
|
252
|
-
self.content.page = self
|
253
|
-
|
254
|
-
def dict(self):
|
255
|
-
url_params = prepare_url_params(self.get_params)
|
256
|
-
ctx = {
|
257
|
-
'title': self.title,
|
258
|
-
'breadcrumbs': self.breadcrumbs,
|
259
|
-
'actions': self.actions,
|
260
|
-
'url_params': url_params,
|
261
|
-
'url_params_str': url_param_str(url_params)
|
262
|
-
}
|
263
|
-
if self.content:
|
264
|
-
ctx.update(self.content.dict())
|
265
|
-
return ctx
|
266
|
-
|
267
|
-
|
268
96
|
def cast_param(params: dict, param: str, cast_to: callable, default):
|
97
|
+
if param not in params:
|
98
|
+
return default
|
269
99
|
try:
|
270
100
|
return cast_to(params.get(param, default))
|
271
101
|
except Exception as e:
|
@@ -281,7 +111,7 @@ def url_param_str(params: dict, extract: list[str] = None) -> str:
|
|
281
111
|
"""
|
282
112
|
Return a URL Querystring from the given parameters
|
283
113
|
If extract is supplied, extract the value from the dictionary and prepare
|
284
|
-
them, so that each value is formatted
|
114
|
+
them, so that each value is formatted e.g. {'page': '&page=1'}
|
285
115
|
"""
|
286
116
|
if extract:
|
287
117
|
params = prepare_url_params(extract_url_params(params, extract))
|
@@ -302,12 +132,12 @@ def exclude_params(params: dict, keys: list[str]) -> dict:
|
|
302
132
|
return {key: val for key, val in params.items() if key not in keys}
|
303
133
|
|
304
134
|
|
305
|
-
def
|
135
|
+
def list_page(queryset: QuerySet, paginate_by: int, page_number: int) -> paginator.Page:
|
306
136
|
pages = Paginator(queryset, per_page=paginate_by)
|
307
137
|
return pages.page(page_number <= pages.num_pages and page_number or pages.num_pages)
|
308
138
|
|
309
139
|
|
310
|
-
def
|
140
|
+
def detail_page(queryset: QuerySet, obj: Model) -> dict:
|
311
141
|
if not hasattr(obj, 'get_absolute_url'):
|
312
142
|
_logger.warning(
|
313
143
|
'Detail pagination disabled for models without the '
|
@@ -330,16 +160,59 @@ def get_detail_page(queryset: QuerySet, obj: Model) -> dict:
|
|
330
160
|
}
|
331
161
|
|
332
162
|
|
333
|
-
def
|
163
|
+
def form_actions(discard_url: str, form_id: str = 'form') -> list[ClientAction]:
|
334
164
|
return [
|
335
165
|
ClientAction(
|
336
|
-
name=
|
166
|
+
name=_t('Save'),
|
337
167
|
submit=True,
|
338
168
|
class_list=['is-success'],
|
339
169
|
form_id=form_id,
|
340
170
|
),
|
341
171
|
ClientAction(
|
342
|
-
name=
|
172
|
+
name=_t('Discard'),
|
343
173
|
url=discard_url
|
344
174
|
)
|
345
175
|
]
|
176
|
+
|
177
|
+
|
178
|
+
def get_table_fields(
|
179
|
+
fields: list[str],
|
180
|
+
model: type[Model],
|
181
|
+
field_definition: dict[str | TableField] = None
|
182
|
+
) -> list[TableField]:
|
183
|
+
print(fields, model, field_definition)
|
184
|
+
|
185
|
+
if field_definition is None:
|
186
|
+
field_definition = {}
|
187
|
+
|
188
|
+
def get_field_definition(f_name: str) -> TableField:
|
189
|
+
if definition := field_definition.get(f_name):
|
190
|
+
return definition
|
191
|
+
if len(f_name.split('__')) == 1:
|
192
|
+
rel_model = model
|
193
|
+
else:
|
194
|
+
rel_model = get_related_model(model, f_name)
|
195
|
+
print(rel_model)
|
196
|
+
try:
|
197
|
+
return rel_model.table_field_definition[f_name]
|
198
|
+
except (AttributeError, KeyError):
|
199
|
+
attr = getattr(rel_model, f_name)
|
200
|
+
if isinstance(attr, Annotation):
|
201
|
+
attr = attr
|
202
|
+
elif hasattr(attr, 'field'):
|
203
|
+
attr = attr.field
|
204
|
+
return TableField(
|
205
|
+
label=str(attr.verbose_name),
|
206
|
+
name=f_name,
|
207
|
+
header_info=str(attr.help_text),
|
208
|
+
truncate_after=50
|
209
|
+
)
|
210
|
+
|
211
|
+
table_fields = []
|
212
|
+
for field_name in fields:
|
213
|
+
try:
|
214
|
+
table_fields.append(get_field_definition(field_name))
|
215
|
+
except AttributeError as e:
|
216
|
+
print(e)
|
217
|
+
pass
|
218
|
+
return table_fields
|