accrete 0.0.10__py3-none-any.whl → 0.0.23__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/contrib/sequence/forms.py +2 -2
- accrete/contrib/sequence/queries.py +3 -4
- accrete/contrib/ui/__init__.py +11 -5
- accrete/contrib/ui/components.py +49 -0
- accrete/contrib/ui/context.py +284 -0
- accrete/contrib/ui/filter.py +22 -17
- accrete/contrib/ui/querystring.py +93 -0
- accrete/contrib/ui/static/bulma/LICENSE +21 -0
- accrete/contrib/ui/static/bulma/README.md +139 -0
- accrete/contrib/ui/static/bulma/bulma.sass +10 -0
- accrete/contrib/ui/static/bulma/css/bulma-rtl.css +11851 -0
- accrete/contrib/ui/static/bulma/css/bulma-rtl.css.map +1 -0
- accrete/contrib/ui/static/bulma/css/bulma-rtl.min.css +1 -0
- accrete/contrib/ui/static/bulma/css/bulma.css +11851 -0
- accrete/contrib/ui/static/bulma/css/bulma.css.map +1 -0
- accrete/contrib/ui/static/bulma/css/bulma.min.css +1 -0
- accrete/contrib/ui/static/bulma/package.json +56 -0
- accrete/contrib/ui/static/bulma/sass/base/_all.sass +6 -0
- accrete/contrib/ui/static/bulma/sass/base/animations.sass +5 -0
- accrete/contrib/ui/static/bulma/sass/base/generic.sass +145 -0
- accrete/contrib/ui/static/bulma/sass/base/helpers.sass +1 -0
- accrete/contrib/ui/static/bulma/sass/base/minireset.sass +79 -0
- accrete/contrib/ui/static/bulma/sass/components/_all.sass +15 -0
- accrete/contrib/ui/static/bulma/sass/components/breadcrumb.sass +77 -0
- accrete/contrib/ui/static/bulma/sass/components/card.sass +103 -0
- accrete/contrib/ui/static/bulma/sass/components/dropdown.sass +83 -0
- accrete/contrib/ui/static/bulma/sass/components/level.sass +79 -0
- accrete/contrib/ui/static/bulma/sass/components/media.sass +59 -0
- accrete/contrib/ui/static/bulma/sass/components/menu.sass +59 -0
- accrete/contrib/ui/static/bulma/sass/components/message.sass +101 -0
- accrete/contrib/ui/static/bulma/sass/components/modal.sass +117 -0
- accrete/contrib/ui/static/bulma/sass/components/navbar.sass +446 -0
- accrete/contrib/ui/static/bulma/sass/components/pagination.sass +167 -0
- accrete/contrib/ui/static/bulma/sass/components/panel.sass +121 -0
- accrete/contrib/ui/static/bulma/sass/components/tabs.sass +176 -0
- accrete/contrib/ui/static/bulma/sass/elements/_all.sass +16 -0
- accrete/contrib/ui/static/bulma/sass/elements/box.sass +26 -0
- accrete/contrib/ui/static/bulma/sass/elements/button.sass +357 -0
- accrete/contrib/ui/static/bulma/sass/elements/container.sass +29 -0
- accrete/contrib/ui/static/bulma/sass/elements/content.sass +162 -0
- accrete/contrib/ui/static/bulma/sass/elements/form.sass +1 -0
- accrete/contrib/ui/static/bulma/sass/elements/icon.sass +46 -0
- accrete/contrib/ui/static/bulma/sass/elements/image.sass +73 -0
- accrete/contrib/ui/static/bulma/sass/elements/notification.sass +52 -0
- accrete/contrib/ui/static/bulma/sass/elements/other.sass +31 -0
- accrete/contrib/ui/static/bulma/sass/elements/progress.sass +73 -0
- accrete/contrib/ui/static/bulma/sass/elements/table.sass +134 -0
- accrete/contrib/ui/static/bulma/sass/elements/tag.sass +140 -0
- accrete/contrib/ui/static/bulma/sass/elements/title.sass +70 -0
- accrete/contrib/ui/static/bulma/sass/form/_all.sass +9 -0
- accrete/contrib/ui/static/bulma/sass/form/checkbox-radio.sass +22 -0
- accrete/contrib/ui/static/bulma/sass/form/file.sass +184 -0
- accrete/contrib/ui/static/bulma/sass/form/input-textarea.sass +66 -0
- accrete/contrib/ui/static/bulma/sass/form/select.sass +88 -0
- accrete/contrib/ui/static/bulma/sass/form/shared.sass +60 -0
- accrete/contrib/ui/static/bulma/sass/form/tools.sass +215 -0
- accrete/contrib/ui/static/bulma/sass/grid/_all.sass +5 -0
- accrete/contrib/ui/static/bulma/sass/grid/columns.sass +513 -0
- accrete/contrib/ui/static/bulma/sass/grid/tiles.sass +36 -0
- accrete/contrib/ui/static/bulma/sass/helpers/_all.sass +12 -0
- accrete/contrib/ui/static/bulma/sass/helpers/color.sass +39 -0
- accrete/contrib/ui/static/bulma/sass/helpers/flexbox.sass +35 -0
- accrete/contrib/ui/static/bulma/sass/helpers/float.sass +10 -0
- accrete/contrib/ui/static/bulma/sass/helpers/other.sass +14 -0
- accrete/contrib/ui/static/bulma/sass/helpers/overflow.sass +2 -0
- accrete/contrib/ui/static/bulma/sass/helpers/position.sass +7 -0
- accrete/contrib/ui/static/bulma/sass/helpers/spacing.sass +31 -0
- accrete/contrib/ui/static/bulma/sass/helpers/typography.sass +103 -0
- accrete/contrib/ui/static/bulma/sass/helpers/visibility.sass +122 -0
- accrete/contrib/ui/static/bulma/sass/layout/_all.sass +6 -0
- accrete/contrib/ui/static/bulma/sass/layout/footer.sass +11 -0
- accrete/contrib/ui/static/bulma/sass/layout/hero.sass +153 -0
- accrete/contrib/ui/static/bulma/sass/layout/section.sass +17 -0
- accrete/contrib/ui/static/bulma/sass/utilities/_all.sass +9 -0
- accrete/contrib/ui/static/bulma/sass/utilities/animations.sass +1 -0
- accrete/contrib/ui/static/bulma/sass/utilities/controls.sass +49 -0
- accrete/contrib/ui/static/bulma/sass/utilities/derived-variables.sass +114 -0
- accrete/contrib/ui/static/bulma/sass/utilities/extends.sass +25 -0
- accrete/contrib/ui/static/bulma/sass/utilities/functions.sass +135 -0
- accrete/contrib/ui/static/bulma/sass/utilities/initial-variables.sass +79 -0
- accrete/contrib/ui/static/bulma/sass/utilities/mixins.sass +303 -0
- accrete/contrib/ui/static/css/accrete.css +10426 -79
- accrete/contrib/ui/static/css/accrete.css.bak +156 -0
- accrete/contrib/ui/static/css/accrete.css.map +1 -0
- accrete/contrib/ui/static/css/accrete.scss +232 -0
- accrete/contrib/ui/static/css/icons.css +9 -0
- accrete/contrib/ui/static/js/filter.js +129 -57
- accrete/contrib/ui/templates/ui/detail.html +1 -3
- accrete/contrib/ui/templates/ui/layout.html +37 -20
- accrete/contrib/ui/templates/ui/list.html +3 -3
- accrete/contrib/ui/templates/ui/partials/filter.html +10 -5
- accrete/contrib/ui/templates/ui/partials/header.html +5 -16
- accrete/contrib/ui/templates/ui/partials/pagination_detail.html +7 -11
- accrete/contrib/ui/templates/ui/partials/pagination_list.html +8 -10
- accrete/contrib/ui/templates/ui/partials/table_field.html +15 -0
- accrete/contrib/ui/templates/ui/partials/table_field_float.html +1 -0
- accrete/contrib/ui/templates/ui/partials/table_field_monetary.html +4 -0
- accrete/contrib/ui/templates/ui/partials/table_field_string.html +1 -0
- accrete/contrib/ui/templates/ui/table.html +20 -5
- accrete/contrib/ui/templatetags/accrete_ui.py +5 -3
- accrete/contrib/user/forms.py +28 -0
- accrete/contrib/user/models.py +16 -0
- accrete/contrib/user/templates/user/accrete_navbar_end.html +9 -0
- accrete/contrib/user/templates/user/user_detail.html +43 -0
- accrete/contrib/user/templates/user/user_form.html +56 -0
- accrete/contrib/user/urls.py +3 -1
- accrete/contrib/user/views.py +35 -3
- accrete/queries.py +5 -1
- {accrete-0.0.10.dist-info → accrete-0.0.23.dist-info}/METADATA +1 -1
- accrete-0.0.23.dist-info/RECORD +194 -0
- accrete/contrib/ui/helper.py +0 -417
- accrete-0.0.10.dist-info/RECORD +0 -107
- {accrete-0.0.10.dist-info → accrete-0.0.23.dist-info}/WHEEL +0 -0
- {accrete-0.0.10.dist-info → accrete-0.0.23.dist-info}/licenses/LICENSE +0 -0
@@ -1,13 +1,12 @@
|
|
1
|
-
import logging
|
2
1
|
from django.db import transaction
|
3
2
|
from django.db.models import F
|
4
3
|
|
4
|
+
from accrete.tenant import get_tenant
|
5
5
|
from .models import Sequence
|
6
6
|
|
7
|
-
_logger = logging.getLogger(__name__)
|
8
7
|
|
9
|
-
|
10
|
-
|
8
|
+
def get_nextval(name, create_if_none=True):
|
9
|
+
tenant = get_tenant()
|
11
10
|
with transaction.atomic():
|
12
11
|
seq = Sequence.objects.filter(
|
13
12
|
tenant=tenant.id, name=name
|
accrete/contrib/ui/__init__.py
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
from .filter import Filter
|
2
|
-
from .
|
3
|
-
|
2
|
+
from .context import (
|
3
|
+
ListContext,
|
4
4
|
DetailContext,
|
5
5
|
FormContext,
|
6
|
-
|
6
|
+
)
|
7
|
+
from .components import (
|
8
|
+
ClientAction,
|
7
9
|
BreadCrumb,
|
8
10
|
TableField,
|
9
|
-
|
10
|
-
|
11
|
+
TableFieldAlignment,
|
12
|
+
TableFieldType
|
13
|
+
)
|
14
|
+
from .querystring import (
|
15
|
+
parse_querystring,
|
16
|
+
build_querystring
|
11
17
|
)
|
@@ -0,0 +1,49 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from enum import Enum
|
3
|
+
|
4
|
+
|
5
|
+
class TableFieldAlignment(Enum):
|
6
|
+
|
7
|
+
LEFT = 'left'
|
8
|
+
CENTER = 'center'
|
9
|
+
RIGHT = 'right'
|
10
|
+
|
11
|
+
|
12
|
+
class TableFieldType(Enum):
|
13
|
+
|
14
|
+
NONE = ''
|
15
|
+
STRING = '_string'
|
16
|
+
MONETARY = '_monetary'
|
17
|
+
FLOAT = '_float'
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class TableField:
|
22
|
+
|
23
|
+
label: str
|
24
|
+
name: str
|
25
|
+
alignment: type[TableFieldAlignment] = TableFieldAlignment.LEFT
|
26
|
+
header_alignment: type[TableFieldAlignment] = None
|
27
|
+
field_type: type[TableFieldType] = TableFieldType.NONE
|
28
|
+
prefix: str = ''
|
29
|
+
suffix: str = ''
|
30
|
+
truncate_after: int = 0
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class BreadCrumb:
|
35
|
+
|
36
|
+
name: str
|
37
|
+
url: str
|
38
|
+
|
39
|
+
|
40
|
+
@dataclass
|
41
|
+
class ClientAction:
|
42
|
+
|
43
|
+
name: str
|
44
|
+
url: str = ''
|
45
|
+
query_params: str = ''
|
46
|
+
attrs: list[str] = field(default_factory=list)
|
47
|
+
submit: bool = False
|
48
|
+
form_id: str = 'form'
|
49
|
+
class_list: list = field(default_factory=list)
|
@@ -0,0 +1,284 @@
|
|
1
|
+
import logging
|
2
|
+
from dataclasses import dataclass, field
|
3
|
+
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
5
|
+
from django.db.models import Model, QuerySet, Q
|
6
|
+
from django.core.paginator import Paginator
|
7
|
+
|
8
|
+
from .querystring import parse_querystring, build_querystring
|
9
|
+
from .components import ClientAction, BreadCrumb, TableField
|
10
|
+
from .filter import Filter
|
11
|
+
|
12
|
+
_logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
DEFAULT_PAGINATE_BY = 40
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class ListContext:
|
19
|
+
|
20
|
+
model: Model
|
21
|
+
get_params: dict
|
22
|
+
title: str = None
|
23
|
+
context: dict = field(default_factory=dict)
|
24
|
+
queryset: QuerySet = None
|
25
|
+
select_related: list[str] = field(default_factory=list)
|
26
|
+
prefetch_related: list[str] = field(default_factory=list)
|
27
|
+
annotate: dict = field(default_factory=dict)
|
28
|
+
paginate_by: int = DEFAULT_PAGINATE_BY
|
29
|
+
order_by: list[str] = field(default_factory=list)
|
30
|
+
filter_relation_depth: int = 4
|
31
|
+
default_filter_term: str = ''
|
32
|
+
actions: list[ClientAction] = field(default_factory=list)
|
33
|
+
breadcrumbs: list[BreadCrumb] = field(default_factory=list)
|
34
|
+
obj_label: str = None
|
35
|
+
fields: list[TableField] = field(default_factory=list)
|
36
|
+
unselect_button: bool = False
|
37
|
+
|
38
|
+
def get_queryset(self):
|
39
|
+
if self.queryset:
|
40
|
+
return self.queryset
|
41
|
+
|
42
|
+
order = self.order_by or self.model._meta.ordering
|
43
|
+
|
44
|
+
pks = self.model.objects.filter(
|
45
|
+
parse_querystring(self.model, self.get_params.get('q', '[]'))
|
46
|
+
).distinct().values_list('pk', flat=True)
|
47
|
+
|
48
|
+
queryset = self.model.objects.select_related(
|
49
|
+
*self.select_related
|
50
|
+
).prefetch_related(
|
51
|
+
*self.prefetch_related
|
52
|
+
).filter(
|
53
|
+
Q(pk__in=pks)
|
54
|
+
).annotate(
|
55
|
+
**self.get_annotations()
|
56
|
+
).order_by(
|
57
|
+
*order
|
58
|
+
).distinct()
|
59
|
+
|
60
|
+
return queryset
|
61
|
+
|
62
|
+
def get_annotations(self):
|
63
|
+
annotations = {
|
64
|
+
annotation['name']: annotation['func']
|
65
|
+
for annotation in getattr(self.model, 'annotations', [])
|
66
|
+
}
|
67
|
+
if self.annotate:
|
68
|
+
annotations.update(self.annotate)
|
69
|
+
return annotations
|
70
|
+
|
71
|
+
def get_page_number(self, paginator):
|
72
|
+
page_number = self.get_params.get('page', '1')
|
73
|
+
|
74
|
+
try:
|
75
|
+
page_number = int(page_number)
|
76
|
+
except ValueError:
|
77
|
+
page_number = 1
|
78
|
+
|
79
|
+
if page_number < 1:
|
80
|
+
page_number = 1
|
81
|
+
elif page_number > paginator.num_pages:
|
82
|
+
page_number = paginator.num_pages
|
83
|
+
return page_number
|
84
|
+
|
85
|
+
def get_paginate_by(self):
|
86
|
+
paginate_by = self.get_params.get('paginate_by', self.paginate_by)
|
87
|
+
try:
|
88
|
+
paginate_by = int(paginate_by)
|
89
|
+
except ValueError:
|
90
|
+
paginate_by = self.paginate_by
|
91
|
+
return paginate_by
|
92
|
+
|
93
|
+
def dict(self):
|
94
|
+
queryset = self.get_queryset()
|
95
|
+
paginate_by = self.get_paginate_by()
|
96
|
+
paginator = Paginator(queryset, paginate_by)
|
97
|
+
page = paginator.page(self.get_page_number(paginator))
|
98
|
+
context = {
|
99
|
+
'queryset': queryset,
|
100
|
+
'paginate_by': paginate_by,
|
101
|
+
'order_by': self.get_params.get('order_by', self.model._meta.ordering),
|
102
|
+
'paginator': paginator,
|
103
|
+
'page': page,
|
104
|
+
'list_pagination': True,
|
105
|
+
'title': self.title or self.model._meta.verbose_name_plural,
|
106
|
+
'filter_terms': Filter(self.model, self.filter_relation_depth).get_query_terms(),
|
107
|
+
'default_filter_term': self.default_filter_term,
|
108
|
+
'breadcrumbs': self.breadcrumbs,
|
109
|
+
'querystring': build_querystring(self.get_params),
|
110
|
+
'actions': self.actions,
|
111
|
+
'obj_label': self.obj_label or _('Name'),
|
112
|
+
'fields': self.fields
|
113
|
+
}
|
114
|
+
context.update(self.context)
|
115
|
+
return context
|
116
|
+
|
117
|
+
|
118
|
+
@dataclass
|
119
|
+
class DetailContext:
|
120
|
+
|
121
|
+
obj: Model
|
122
|
+
get_params: dict
|
123
|
+
order_by: str = None
|
124
|
+
paginate_by: int = DEFAULT_PAGINATE_BY
|
125
|
+
title: str = None
|
126
|
+
queryset: type[QuerySet] = None
|
127
|
+
select_related: list[str] = field(default_factory=list)
|
128
|
+
prefetch_related: list[str] = field(default_factory=list)
|
129
|
+
annotate: dict = field(default_factory=dict)
|
130
|
+
actions: list[ClientAction] = field(default_factory=list)
|
131
|
+
breadcrumbs: list[BreadCrumb] = field(default_factory=list)
|
132
|
+
context: dict = field(default_factory=dict)
|
133
|
+
|
134
|
+
def get_queryset(self):
|
135
|
+
if self.queryset:
|
136
|
+
return self.queryset
|
137
|
+
|
138
|
+
order = self.order_by or self.obj._meta.model._meta.ordering
|
139
|
+
|
140
|
+
pks = self.obj._meta.model.objects.filter(
|
141
|
+
parse_querystring(self.obj._meta.model, self.get_params.get('q', '[]'))
|
142
|
+
).distinct().values_list('pk', flat=True)
|
143
|
+
|
144
|
+
queryset = self.obj._meta.model.objects.select_related(
|
145
|
+
*self.select_related
|
146
|
+
).prefetch_related(
|
147
|
+
*self.prefetch_related
|
148
|
+
).filter(
|
149
|
+
Q(pk__in=pks)
|
150
|
+
).annotate(
|
151
|
+
**self.get_annotations()
|
152
|
+
).order_by(
|
153
|
+
*order
|
154
|
+
).distinct()
|
155
|
+
|
156
|
+
return queryset
|
157
|
+
|
158
|
+
def get_annotations(self):
|
159
|
+
annotations = {
|
160
|
+
annotation['name']: annotation['func']
|
161
|
+
for annotation in getattr(self.obj._meta.model, 'annotations', [])
|
162
|
+
}
|
163
|
+
if self.annotate:
|
164
|
+
annotations.update(self.annotate)
|
165
|
+
return annotations
|
166
|
+
|
167
|
+
def get_paginate_by(self):
|
168
|
+
paginate_by = self.get_params.get('paginate_by', self.paginate_by)
|
169
|
+
try:
|
170
|
+
paginate_by = int(paginate_by)
|
171
|
+
except ValueError:
|
172
|
+
paginate_by = self.paginate_by
|
173
|
+
return paginate_by
|
174
|
+
|
175
|
+
def get_pagination_context(self):
|
176
|
+
if not hasattr(self.obj, 'get_absolute_url'):
|
177
|
+
_logger.warning(
|
178
|
+
'Detail pagination disabled for models without the '
|
179
|
+
'get_absolute_url attribute. Set paginate_by to 0 to '
|
180
|
+
'deactivate pagination.'
|
181
|
+
)
|
182
|
+
return {}
|
183
|
+
queryset = self.get_queryset()
|
184
|
+
idx = (*queryset,).index(self.obj)
|
185
|
+
previous_object_url = (
|
186
|
+
queryset[idx - 1] if idx - 1 >= 0 else queryset.last()
|
187
|
+
).get_absolute_url()
|
188
|
+
next_object_url = (
|
189
|
+
queryset[idx + 1] if idx + 1 <= queryset.count() - 1 else queryset.first()
|
190
|
+
).get_absolute_url()
|
191
|
+
ctx = {
|
192
|
+
'previous_object_url': previous_object_url,
|
193
|
+
'next_object_url': next_object_url,
|
194
|
+
'current_object_idx': idx + 1,
|
195
|
+
'total_objects': queryset.count(),
|
196
|
+
'detail_pagination': True
|
197
|
+
}
|
198
|
+
return ctx
|
199
|
+
|
200
|
+
def dict(self):
|
201
|
+
paginate_by = self.get_paginate_by()
|
202
|
+
ctx = {
|
203
|
+
'object': self.obj,
|
204
|
+
'title': self.title or self.obj,
|
205
|
+
'order_by': self.get_params.get('order_by', self.obj._meta.model._meta.ordering),
|
206
|
+
'paginate_by': paginate_by,
|
207
|
+
'detail_pagination': False,
|
208
|
+
'view_type': 'detail',
|
209
|
+
'breadcrumbs': self.breadcrumbs,
|
210
|
+
'querystring': build_querystring(self.get_params, ['page']),
|
211
|
+
'actions': self.actions
|
212
|
+
}
|
213
|
+
if self.paginate_by > 0:
|
214
|
+
ctx.update(self.get_pagination_context())
|
215
|
+
ctx.update(self.context)
|
216
|
+
return ctx
|
217
|
+
|
218
|
+
|
219
|
+
@dataclass
|
220
|
+
class FormContext:
|
221
|
+
|
222
|
+
model: Model | type[Model]
|
223
|
+
get_params: dict
|
224
|
+
title: str = None
|
225
|
+
context: dict = field(default_factory=dict)
|
226
|
+
form_id: str = 'form'
|
227
|
+
add_default_actions: bool = True
|
228
|
+
discard_url: str = None
|
229
|
+
actions: list[ClientAction] = field(default_factory=list)
|
230
|
+
|
231
|
+
def get_default_form_actions(self):
|
232
|
+
actions = [
|
233
|
+
ClientAction(
|
234
|
+
name=_('Save'),
|
235
|
+
submit=True,
|
236
|
+
class_list=['is-success'],
|
237
|
+
form_id=self.form_id
|
238
|
+
)
|
239
|
+
]
|
240
|
+
try:
|
241
|
+
url = self.discard_url or self.model.get_absolute_url()
|
242
|
+
except TypeError:
|
243
|
+
raise TypeError(
|
244
|
+
'Supply the discard_url parameter if FormContext is called '
|
245
|
+
'with a model class instead of an instance.'
|
246
|
+
)
|
247
|
+
except AttributeError as e:
|
248
|
+
_logger.error(
|
249
|
+
'Supply the discard_url parameter if FormContext is '
|
250
|
+
'called with a model instance that has the get_absolute_url '
|
251
|
+
'method not defined.'
|
252
|
+
)
|
253
|
+
raise e
|
254
|
+
|
255
|
+
actions.append(
|
256
|
+
ClientAction(
|
257
|
+
name=_('Discard'),
|
258
|
+
url=url,
|
259
|
+
)
|
260
|
+
)
|
261
|
+
return actions
|
262
|
+
|
263
|
+
def get_title(self):
|
264
|
+
if self.title:
|
265
|
+
return self.title
|
266
|
+
try:
|
267
|
+
int(self.model.pk)
|
268
|
+
return f'Edit {self.model}'
|
269
|
+
except TypeError:
|
270
|
+
return f'Add {self.model._meta.verbose_name}'
|
271
|
+
|
272
|
+
def dict(self):
|
273
|
+
ctx = {
|
274
|
+
'title': self.get_title(),
|
275
|
+
'view_type': 'form',
|
276
|
+
'form_id': self.form_id,
|
277
|
+
'querystring': build_querystring(self.get_params, ['page']),
|
278
|
+
'actions': []
|
279
|
+
}
|
280
|
+
if self.add_default_actions:
|
281
|
+
ctx.update({'actions': self.get_default_form_actions()})
|
282
|
+
ctx['actions'].extend(self.actions)
|
283
|
+
ctx.update(self.context)
|
284
|
+
return ctx
|
accrete/contrib/ui/filter.py
CHANGED
@@ -78,6 +78,12 @@ class Filter:
|
|
78
78
|
'label': str(self.LABEL_EXACT),
|
79
79
|
'data_type': 'text',
|
80
80
|
'param': 'exact'
|
81
|
+
},
|
82
|
+
{
|
83
|
+
'label': str(self.LABEL_EXACT_NOT),
|
84
|
+
'data_type': 'text',
|
85
|
+
'invert': True,
|
86
|
+
'param': 'exact'
|
81
87
|
}
|
82
88
|
]
|
83
89
|
}
|
@@ -238,10 +244,11 @@ class Filter:
|
|
238
244
|
}
|
239
245
|
]
|
240
246
|
|
241
|
-
|
242
|
-
|
247
|
+
@staticmethod
|
248
|
+
def cast_decimal_places_to_step(decimal_places):
|
249
|
+
if not decimal_places or decimal_places < 1:
|
243
250
|
return '1'
|
244
|
-
zero_count =
|
251
|
+
zero_count = decimal_places - 1
|
245
252
|
return f'0.{"0" * zero_count}1'
|
246
253
|
|
247
254
|
def get_query_term(self, field):
|
@@ -263,24 +270,22 @@ class Filter:
|
|
263
270
|
elif internal_type in self.query_date_fields:
|
264
271
|
return self.get_date_query_term(label, param)
|
265
272
|
|
266
|
-
def
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
func = getattr(model, method['name'])
|
271
|
-
return_type = func.__annotations__.get('return')
|
273
|
+
def get_annotation_term(self, model, annotation):
|
274
|
+
name = '_a_' + annotation['name']
|
275
|
+
label = annotation.get('label', name)
|
276
|
+
return_type = annotation['type']
|
272
277
|
if return_type == str:
|
273
|
-
return self.get_char_query_term(
|
278
|
+
return self.get_char_query_term(label, name)
|
274
279
|
elif return_type == int:
|
275
|
-
return self.get_int_query_term(
|
280
|
+
return self.get_int_query_term(label, name)
|
276
281
|
elif return_type in [float, Decimal]:
|
277
|
-
return self.get_float_query_term(
|
282
|
+
return self.get_float_query_term(label, name, annotation.get('step', 0))
|
278
283
|
elif return_type == bool:
|
279
|
-
return self.get_boolean_query_term(
|
284
|
+
return self.get_boolean_query_term(label, name)
|
280
285
|
elif return_type == datetime.datetime:
|
281
|
-
return self.get_datetime_query_term(
|
286
|
+
return self.get_datetime_query_term(label, name)
|
282
287
|
elif return_type == datetime.date:
|
283
|
-
return self.get_date_query_term(
|
288
|
+
return self.get_date_query_term(label, name)
|
284
289
|
|
285
290
|
def get_relation_query_terms(self, model, path):
|
286
291
|
terms = []
|
@@ -322,8 +327,8 @@ class Filter:
|
|
322
327
|
term['params'].extend(self.get_null_params())
|
323
328
|
if term is not None:
|
324
329
|
terms.append(term)
|
325
|
-
for
|
326
|
-
terms.append(self.
|
330
|
+
for annotation in getattr(model, 'annotations', []):
|
331
|
+
terms.append(self.get_annotation_term(model, annotation))
|
327
332
|
terms = sorted(terms, key=lambda x: x['label'])
|
328
333
|
return terms
|
329
334
|
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
import operator
|
4
|
+
from django.db.models import Model, Q
|
5
|
+
|
6
|
+
_logger = logging.getLogger(__name__)
|
7
|
+
|
8
|
+
|
9
|
+
def parse_querystring(model: type[Model], query_string: str) -> Q:
|
10
|
+
|
11
|
+
def get_expression(term: str, value) -> Q:
|
12
|
+
invert = False
|
13
|
+
if term.startswith('~'):
|
14
|
+
invert = True
|
15
|
+
term = term[1:]
|
16
|
+
|
17
|
+
parts = term.split('_a_')
|
18
|
+
if len(parts) == 1:
|
19
|
+
expression = Q(**{term: value})
|
20
|
+
return ~expression if invert else expression
|
21
|
+
|
22
|
+
rel_path = parts[0].rstrip('__')
|
23
|
+
term = parts[1]
|
24
|
+
rel_model = get_related_model(rel_path) if rel_path else model
|
25
|
+
objects = rel_model.objects.annotate(**{
|
26
|
+
annotation['name']: annotation['func']
|
27
|
+
for annotation in rel_model.annotations
|
28
|
+
}).filter(Q(**{term: value}))
|
29
|
+
expression = Q(**{
|
30
|
+
f'{rel_path}{"__" if rel_path else ""}id__in': objects.values_list('id', flat=True)
|
31
|
+
})
|
32
|
+
|
33
|
+
return ~expression if invert else expression
|
34
|
+
|
35
|
+
def get_related_model(rel_path: str):
|
36
|
+
related_model = model
|
37
|
+
for part in rel_path.split('__'):
|
38
|
+
try:
|
39
|
+
related_model = related_model._meta.fields_map[part].related_model
|
40
|
+
except (AttributeError, KeyError):
|
41
|
+
try:
|
42
|
+
related_model = getattr(related_model, part).field.related_model
|
43
|
+
except AttributeError:
|
44
|
+
break
|
45
|
+
return related_model
|
46
|
+
|
47
|
+
def parse_query_block(sub_item) -> Q:
|
48
|
+
op = ops['&']
|
49
|
+
parsed_query = Q()
|
50
|
+
for item in sub_item:
|
51
|
+
if isinstance(item, list):
|
52
|
+
parsed_query = op(parsed_query, parse_query_block(item))
|
53
|
+
elif isinstance(item, dict):
|
54
|
+
dict_query = Q()
|
55
|
+
for term, value in item.items():
|
56
|
+
dict_query = ops['&'](dict_query, get_expression(term, value))
|
57
|
+
parsed_query = op(parsed_query, dict_query)
|
58
|
+
elif isinstance(item, str):
|
59
|
+
try:
|
60
|
+
op = ops[item]
|
61
|
+
except KeyError as e:
|
62
|
+
_logger.exception(e)
|
63
|
+
raise ValueError(
|
64
|
+
f'Invalid operator in querystring: {item}.'
|
65
|
+
f'Operator must be one of &, |, ^'
|
66
|
+
)
|
67
|
+
else:
|
68
|
+
raise ValueError(
|
69
|
+
f'Unsupported item in querystring: {item}'
|
70
|
+
)
|
71
|
+
return parsed_query
|
72
|
+
|
73
|
+
query_data = json.loads(query_string)
|
74
|
+
if isinstance(query_data, dict):
|
75
|
+
query_data = [query_data]
|
76
|
+
|
77
|
+
ops = {'&': operator.and_, '|': operator.or_, '^': operator.xor}
|
78
|
+
query = parse_query_block(query_data)
|
79
|
+
return query
|
80
|
+
|
81
|
+
|
82
|
+
def build_querystring(get_params: dict, extra_params: list[str] = None) -> str:
|
83
|
+
querystring = f'?q={get_params.get("q", "[]")}'
|
84
|
+
if paginate_by := get_params.get('paginate_by', False):
|
85
|
+
querystring += f'&paginate_by={paginate_by}'
|
86
|
+
if order_by := get_params.get('order_by', False):
|
87
|
+
querystring += f'&order_by={order_by}'
|
88
|
+
if crumbs := get_params.get('crumbs', False):
|
89
|
+
querystring += f'&crumbs={crumbs}'
|
90
|
+
for param in extra_params or []:
|
91
|
+
if value := get_params.get(param, False):
|
92
|
+
querystring += f'&{param}={value}'
|
93
|
+
return querystring
|
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 Jeremy Thomas
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|