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 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
+
@@ -4,18 +4,15 @@ from .context import (
4
4
  TableContext,
5
5
  ListContext,
6
6
  FormContext,
7
- default_form_actions,
8
- Page,
9
- ListContent,
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
- )
@@ -1,15 +1,20 @@
1
1
  import logging
2
- from typing import TypedDict
3
2
  from dataclasses import dataclass, field
4
- from django.utils.translation import gettext_lazy as _
5
- from django.shortcuts import resolve_url
3
+ from typing import TypedDict
4
+ from django.utils.translation import gettext_lazy as _t
5
+ from django.db import models
6
6
  from django.db.models import Model, QuerySet
7
7
  from django.core.paginator import Paginator
8
8
  from django.core import paginator
9
9
  from django.forms import Form, ModelForm
10
- from .elements import ClientAction, BreadCrumb, TableField
10
+ from accrete.utils.models import get_related_model
11
+ from accrete.annotation import Annotation
12
+ from .elements import (
13
+ ClientAction, BreadCrumb, TableField, TableFieldType, TableFieldAlignment
14
+ )
11
15
  from .filter import Filter
12
16
 
17
+
13
18
  _logger = logging.getLogger(__name__)
14
19
 
15
20
  DEFAULT_PAGINATE_BY = 40
@@ -22,31 +27,55 @@ class DetailPagination(TypedDict):
22
27
  total_objects: int
23
28
 
24
29
 
25
- class TableContext(TypedDict, total=False):
30
+ @dataclass(kw_only=True)
31
+ class Context:
32
+
33
+ title: str = ''
34
+ breadcrumbs: list[BreadCrumb] = field(default_factory=list)
35
+ actions: list[ClientAction] = field(default_factory=list)
36
+ kwargs: dict = field(default_factory=dict)
37
+
38
+ def __post_init__(self):
39
+ for key, value in self.kwargs.items():
40
+ setattr(self, key, value)
41
+
42
+ def dict(self):
43
+ return {
44
+ attr: getattr(self, attr, '') for attr
45
+ in filter(lambda x: not x.startswith('_'), self.__dict__)
46
+ }
47
+
48
+
49
+ @dataclass
50
+ class TableContext(Context):
26
51
 
27
- title: str
28
52
  object_label: str
29
- object_param_str: str
30
53
  fields: list[TableField]
31
- breadcrumbs: list[BreadCrumb]
32
- actions: list[ClientAction]
33
54
  list_page: paginator.Page
34
55
  pagination_param_str: str
35
56
  endless_scroll: bool
36
57
  filter: Filter
58
+ object_param_str: str = field(default='', kw_only=True)
37
59
 
38
60
 
39
- class ListContext(TypedDict):
61
+ @dataclass
62
+ class ListContext(Context):
40
63
 
41
- title: str
42
64
  object_label: str
43
65
  object_param_str: str
44
- breadcrumbs: list[BreadCrumb]
45
- actions: list[ClientAction]
46
66
  list_page: paginator.Page
47
67
  pagination_param_str: str
48
- endless_scroll: bool
49
68
  filter: Filter
69
+ endless_scroll: bool = True
70
+ column_height: int = 4
71
+ column_width: int = 12
72
+
73
+ def __post_init__(self):
74
+ super().__post_init__()
75
+ if self.column_width not in range(1, 13):
76
+ _logger.warning(
77
+ 'ListContext parameter column_width should be in range 1 - 12'
78
+ )
50
79
 
51
80
 
52
81
  class DetailContext(TypedDict, total=False):
@@ -59,7 +88,7 @@ class DetailContext(TypedDict, total=False):
59
88
  actions: list[ClientAction]
60
89
 
61
90
 
62
- class FormContext(TypedDict):
91
+ class FormContext(TypedDict, total=False):
63
92
 
64
93
  title: str
65
94
  breadcrumbs: list[BreadCrumb]
@@ -68,204 +97,9 @@ class FormContext(TypedDict):
68
97
  actions: list[ClientAction]
69
98
 
70
99
 
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
100
  def cast_param(params: dict, param: str, cast_to: callable, default):
101
+ if param not in params:
102
+ return default
269
103
  try:
270
104
  return cast_to(params.get(param, default))
271
105
  except Exception as e:
@@ -281,7 +115,7 @@ def url_param_str(params: dict, extract: list[str] = None) -> str:
281
115
  """
282
116
  Return a URL Querystring from the given parameters
283
117
  If extract is supplied, extract the value from the dictionary and prepare
284
- them, so that each value is formatted eg. {'page': '&page=1'}
118
+ them, so that each value is formatted e.g. {'page': '&page=1'}
285
119
  """
286
120
  if extract:
287
121
  params = prepare_url_params(extract_url_params(params, extract))
@@ -302,12 +136,12 @@ def exclude_params(params: dict, keys: list[str]) -> dict:
302
136
  return {key: val for key, val in params.items() if key not in keys}
303
137
 
304
138
 
305
- def get_list_page(queryset: QuerySet, paginate_by: int, page_number: int) -> paginator.Page:
139
+ def list_page(queryset: QuerySet, paginate_by: int, page_number: int) -> paginator.Page:
306
140
  pages = Paginator(queryset, per_page=paginate_by)
307
141
  return pages.page(page_number <= pages.num_pages and page_number or pages.num_pages)
308
142
 
309
143
 
310
- def get_detail_page(queryset: QuerySet, obj: Model) -> dict:
144
+ def detail_page(queryset: QuerySet, obj: Model) -> dict:
311
145
  if not hasattr(obj, 'get_absolute_url'):
312
146
  _logger.warning(
313
147
  'Detail pagination disabled for models without the '
@@ -330,16 +164,75 @@ def get_detail_page(queryset: QuerySet, obj: Model) -> dict:
330
164
  }
331
165
 
332
166
 
333
- def default_form_actions(discard_url: str, form_id: str = 'form') -> list[ClientAction]:
167
+ def form_actions(discard_url: str, form_id: str = 'form') -> list[ClientAction]:
334
168
  return [
335
169
  ClientAction(
336
- name=_('Save'),
170
+ name=_t('Save'),
337
171
  submit=True,
338
172
  class_list=['is-success'],
339
173
  form_id=form_id,
340
174
  ),
341
175
  ClientAction(
342
- name=_('Discard'),
176
+ name=_t('Discard'),
343
177
  url=discard_url
344
178
  )
345
179
  ]
180
+
181
+
182
+ def get_table_fields(
183
+ fields: list[str],
184
+ model: type[Model],
185
+ field_definition: dict[str | TableField] = None
186
+ ) -> list[TableField]:
187
+
188
+ def get_alignment(field_attr):
189
+ number_field_types = (
190
+ models.DecimalField, models.IntegerField, models.FloatField
191
+ )
192
+ if (
193
+ isinstance(field_attr, number_field_types)
194
+ or (
195
+ isinstance(field_attr, Annotation)
196
+ and field_attr.field in number_field_types
197
+ )):
198
+ return TableFieldAlignment.RIGHT
199
+ return TableFieldAlignment.LEFT
200
+
201
+
202
+ def get_field_definition(f_name: str) -> TableField:
203
+ if definition := field_definition.get(f_name):
204
+ return definition
205
+ parts = f_name.split('__')
206
+ if len(parts) == 1:
207
+ rel_model = model
208
+ names = [rel_model._meta.verbose_name]
209
+ else:
210
+ rel_model, names = get_related_model(model, f_name)
211
+ try:
212
+ return rel_model.table_field_definition[parts[-1]]
213
+ except (AttributeError, KeyError):
214
+ attr = getattr(rel_model, parts[-1])
215
+ if isinstance(attr, Annotation):
216
+ pass
217
+ elif hasattr(attr, 'field'):
218
+ attr = attr.field
219
+ names.append(attr.verbose_name)
220
+ return TableField(
221
+ label=' / '.join([str(name) for name in names[1:]]),
222
+ name=f_name,
223
+ header_info=str(attr.help_text),
224
+ truncate_after=50,
225
+ alignment=get_alignment(attr)
226
+ )
227
+
228
+ if field_definition is None:
229
+ field_definition = {}
230
+
231
+ table_fields = []
232
+ for field_name in fields:
233
+ try:
234
+ table_fields.append(get_field_definition(field_name))
235
+ except AttributeError as e:
236
+ print(e)
237
+ pass
238
+ return table_fields