accrete 0.0.54__py3-none-any.whl → 0.0.56__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/ui/__init__.py +2 -1
- accrete/contrib/ui/context.py +50 -8
- accrete/contrib/ui/elements.py +9 -9
- accrete/contrib/ui/filter.py +5 -5
- accrete/contrib/ui/static/css/accrete.css +17 -6
- accrete/contrib/ui/static/css/accrete.css.map +1 -1
- accrete/contrib/ui/static/css/accrete.scss +25 -0
- accrete/contrib/ui/templates/ui/dashboard.html +7 -0
- accrete/contrib/ui/templates/ui/detail.html +0 -86
- accrete/contrib/ui/templates/ui/form.html +6 -15
- accrete/contrib/ui/templates/ui/layout.html +3 -1
- accrete/contrib/ui/templates/ui/partials/filter.html +2 -2
- accrete/contrib/ui/templates/ui/partials/form_errors.html +33 -20
- accrete/contrib/ui/templates/ui/partials/header.html +1 -1
- accrete/forms.py +1 -248
- accrete/managers.py +1 -2
- accrete/utils/__init__.py +2 -2
- accrete/utils/forms.py +79 -0
- accrete/utils/http.py +14 -3
- {accrete-0.0.54.dist-info → accrete-0.0.56.dist-info}/METADATA +1 -1
- {accrete-0.0.54.dist-info → accrete-0.0.56.dist-info}/RECORD +23 -22
- {accrete-0.0.54.dist-info → accrete-0.0.56.dist-info}/WHEEL +1 -1
- {accrete-0.0.54.dist-info → accrete-0.0.56.dist-info}/licenses/LICENSE +0 -0
@@ -3,28 +3,19 @@
|
|
3
3
|
|
4
4
|
{% block content %}
|
5
5
|
<div class="columns is-desktop m-0">
|
6
|
-
<div class="column p-0 is-8-desktop">
|
6
|
+
<div id="form-content" class="column p-0 is-8-desktop">
|
7
7
|
<div class="box mt-2">
|
8
|
-
{%
|
8
|
+
{% if form.is_saved is False %}
|
9
|
+
{% include 'ui/partials/form_errors.html' with show_field_errors=True %}
|
10
|
+
{% endif %}
|
9
11
|
{% block form %}{% endblock %}
|
10
12
|
</div>
|
11
13
|
</div>
|
12
|
-
<div class="column is-4">
|
13
|
-
<nav
|
14
|
+
<div id="form-info" class="column is-4">
|
15
|
+
<nav>
|
14
16
|
{% block info_panel %}
|
15
17
|
{% endblock %}
|
16
18
|
</nav>
|
17
19
|
</div>
|
18
20
|
</div>
|
19
|
-
|
20
|
-
|
21
|
-
{# <div class="columns is-desktop m-0">#}
|
22
|
-
{# <div class="column p-0 is-8-desktop">#}
|
23
|
-
{# <div>#}
|
24
|
-
{# {% include 'ui/partials/form_errors.html' with show_field_errors=True %}#}
|
25
|
-
{# {% block form %}{% endblock %}#}
|
26
|
-
{# </div>#}
|
27
|
-
{# </div>#}
|
28
|
-
{# <div class="column is-4"></div>#}
|
29
|
-
{# </div>#}
|
30
21
|
{% endblock %}
|
@@ -18,6 +18,8 @@
|
|
18
18
|
{% block htmx %}
|
19
19
|
<script src="{% static "js/htmx.min.js" %}" defer type="text/javascript"></script>
|
20
20
|
{% endblock %}
|
21
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/sort@3.x.x/dist/cdn.min.js"></script>
|
22
|
+
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.10/dist/cdn.min.js" defer type="text/javascript"></script>
|
21
23
|
{% block script %}
|
22
24
|
<script src="{% static "js/ui.js" %}" defer type="text/javascript"></script>
|
23
25
|
{% endblock %}
|
@@ -62,7 +64,7 @@
|
|
62
64
|
<div class="navbar-item has-dropdown is-hoverable" onclick="this.classList.toggle('is-active');">
|
63
65
|
<a class="navbar-link is-arrowless {% if request.member.name %}is-size-7 has-text-centered-desktop{% endif %}">
|
64
66
|
{% if request.member.name %}
|
65
|
-
{{ request.member }}
|
67
|
+
{{ request.member }}
|
66
68
|
{% else %}
|
67
69
|
{{ user }}
|
68
70
|
{% endif %}
|
@@ -56,8 +56,8 @@
|
|
56
56
|
</div>
|
57
57
|
|
58
58
|
<div id="list-customization" style="display: flex; flex-direction: column; flex-wrap: nowrap">
|
59
|
-
<div class="
|
60
|
-
<div class="field has-addons
|
59
|
+
<div class="mt-2">
|
60
|
+
<div class="field has-addons">
|
61
61
|
{% if field_selection %}
|
62
62
|
<div class="control is-expanded">
|
63
63
|
<button id="list-customization-fields-trigger"
|
@@ -1,23 +1,36 @@
|
|
1
|
-
|
2
|
-
<div class="
|
3
|
-
<div class="
|
4
|
-
<
|
5
|
-
|
6
|
-
{
|
7
|
-
|
8
|
-
|
9
|
-
{
|
10
|
-
{
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
</
|
17
|
-
|
1
|
+
<div class="columns py-0">
|
2
|
+
<div class="column is-12">
|
3
|
+
<div class="notification is-danger is-light">
|
4
|
+
<p>{{ form.non_field_errors }}</p>
|
5
|
+
{% for inline_form in form.inline_forms %}
|
6
|
+
<p>{{ inline_form.non_field_errors }}</p>
|
7
|
+
{% endfor %}
|
8
|
+
{% if form.save_error %}
|
9
|
+
<p>{{ form.save_error|safe }}</p>
|
10
|
+
<p>Error ID: {{ form.save_error_id }}</p>
|
11
|
+
{% endif %}
|
12
|
+
{% if show_field_errors %}
|
13
|
+
{% for f in form %}
|
14
|
+
{% if f.errors %}
|
15
|
+
<label>
|
16
|
+
<span class="is-underlined">{{ f.label_tag }}</span>
|
17
|
+
<span class="is-size-7">{{ f.errors }}</span>
|
18
|
+
</label>
|
19
|
+
{% endif %}
|
20
|
+
{% endfor %}
|
21
|
+
{% for formset in form.inline_forms %}
|
22
|
+
{% for inline_form in formset %}
|
23
|
+
{% for field in inline_form %}
|
24
|
+
{% if field.errors %}
|
25
|
+
<label>
|
26
|
+
<span class="is-underlined">{{ field.label_tag }}</span>
|
27
|
+
<span class="is-size-7">{{ field.errors }}</span>
|
28
|
+
</label>
|
29
|
+
{% endif %}
|
30
|
+
{% endfor %}
|
18
31
|
{% endfor %}
|
19
|
-
{%
|
20
|
-
|
32
|
+
{% endfor %}
|
33
|
+
{% endif %}
|
21
34
|
</div>
|
22
35
|
</div>
|
23
|
-
|
36
|
+
</div>
|
@@ -29,7 +29,7 @@
|
|
29
29
|
{% endif %}
|
30
30
|
</div>
|
31
31
|
|
32
|
-
<div class="is-flex is-hidden-fullhd is-justify-content-space-between mb-2
|
32
|
+
<div class="is-flex is-hidden-fullhd is-justify-content-space-between mb-2">
|
33
33
|
<div id="header-actions" class="is-flex py-1 px-1 ml-2" style="overflow-x: auto">
|
34
34
|
{% for action in actions %}
|
35
35
|
{% if action.actions %}
|
accrete/forms.py
CHANGED
@@ -1,250 +1,12 @@
|
|
1
1
|
import logging
|
2
|
-
from uuid import uuid4
|
3
|
-
from functools import partial
|
4
|
-
|
5
2
|
from django import forms
|
6
3
|
from django.contrib.auth import get_user_model
|
7
|
-
from django.db import transaction
|
8
4
|
from accrete.tenant import get_tenant
|
9
5
|
|
10
6
|
_logger = logging.getLogger(__name__)
|
11
7
|
User = get_user_model()
|
12
8
|
|
13
9
|
|
14
|
-
class One2ManyModelForm(forms.ModelForm):
|
15
|
-
"""
|
16
|
-
Template Access:
|
17
|
-
In Templates/Forms the generated fields can be accessed via
|
18
|
-
form.get_>>related_name<< e.g. form.get_items returns
|
19
|
-
a list of dictionaries containing the fields.
|
20
|
-
|
21
|
-
Cleaning:
|
22
|
-
For every One2Many field, clean_>>related_name<< is called.
|
23
|
-
To clean all entries together, self.cleaned_data[>>related_name<<] is
|
24
|
-
available in clean() containing a list of all entries
|
25
|
-
|
26
|
-
Initial data can be supplied by creating a form method called
|
27
|
-
get_initial_>>related_name<<_>>field_name<< which takes the
|
28
|
-
related instance as a parameter
|
29
|
-
or by setting an attribute on the model which can be a callable
|
30
|
-
|
31
|
-
|
32
|
-
One2Many field configuration:
|
33
|
-
o2m_fields = {
|
34
|
-
# related_name
|
35
|
-
'items': {
|
36
|
-
# field that holds the key to self.model
|
37
|
-
'fk_field': 'pricelist',
|
38
|
-
# Order by the given fields value, can be prefixed by '-' to
|
39
|
-
# control ascending/descending order
|
40
|
-
'order': '-pk'
|
41
|
-
# fields of the related model that are needed in the form
|
42
|
-
'fields': {
|
43
|
-
# pk field must be present for updating existing rows.
|
44
|
-
# Otherwise, all related objects will be deleted and recreated
|
45
|
-
# upon saving.
|
46
|
-
'pk': {
|
47
|
-
'field_class': partial(
|
48
|
-
forms.IntegerField,
|
49
|
-
required=False,
|
50
|
-
widget=forms.HiddenInput
|
51
|
-
),
|
52
|
-
# if only data is sent with fields that have
|
53
|
-
# "count_as_empty" set to true, the whole fieldset is
|
54
|
-
# ignored and won't be added to self.fields
|
55
|
-
'count_as_empty': True
|
56
|
-
},
|
57
|
-
'product': {
|
58
|
-
'field_class': partial(
|
59
|
-
forms.ModelChoiceField,
|
60
|
-
Product.objects.all(),
|
61
|
-
label=_('Product')
|
62
|
-
)
|
63
|
-
},
|
64
|
-
'price': {
|
65
|
-
'field_class': partial(
|
66
|
-
forms.FloatField,
|
67
|
-
label=_('Price')
|
68
|
-
)
|
69
|
-
}
|
70
|
-
}
|
71
|
-
}
|
72
|
-
}
|
73
|
-
"""
|
74
|
-
o2m_fields = {}
|
75
|
-
|
76
|
-
def __init__(self, *args, **kwargs):
|
77
|
-
super().__init__(*args, **kwargs)
|
78
|
-
self.o2m_keys = None
|
79
|
-
if self.instance and self.instance.pk and not kwargs.get('data'):
|
80
|
-
self.add_instance_o2m_objects()
|
81
|
-
else:
|
82
|
-
self.add_o2m_fields(**kwargs)
|
83
|
-
self.add_o2m_getter()
|
84
|
-
|
85
|
-
def add_instance_o2m_objects(self):
|
86
|
-
self.o2m_keys = set()
|
87
|
-
for field in self.o2m_fields.keys():
|
88
|
-
related_objects = getattr(self.instance, field)
|
89
|
-
for o in related_objects.all():
|
90
|
-
field_key = f'{field}_{str(uuid4())[:8]}'
|
91
|
-
for k, v in self.o2m_fields[field]['fields'].items():
|
92
|
-
func_name = f'get_initial_{field}_{k}'
|
93
|
-
field_name = f'{field_key}-{k}'
|
94
|
-
self.fields[field_name] = v['field_class']()
|
95
|
-
if hasattr(self, func_name):
|
96
|
-
initial = getattr(self, func_name)(o)
|
97
|
-
else:
|
98
|
-
initial = getattr(o, k) if hasattr(o, k) else False
|
99
|
-
if callable(self.fields[field_name].initial):
|
100
|
-
initial = initial()
|
101
|
-
self.fields[field_name].initial = initial
|
102
|
-
self.o2m_keys.add(field_key)
|
103
|
-
|
104
|
-
def add_o2m_fields(self, **kwargs):
|
105
|
-
self.o2m_keys = set()
|
106
|
-
data = kwargs.get('data', {})
|
107
|
-
for field, config in self.o2m_fields.items():
|
108
|
-
fields = config['fields']
|
109
|
-
field_keys = set(
|
110
|
-
k.split('-')[0] for k in data.keys() if k.startswith(field)
|
111
|
-
)
|
112
|
-
if not field_keys:
|
113
|
-
field_keys = set(
|
114
|
-
f'{k}_1' for k in self.o2m_fields.keys()
|
115
|
-
)
|
116
|
-
for field_key in field_keys:
|
117
|
-
to_add = any([
|
118
|
-
data.get(f'{field_key}-{name}')
|
119
|
-
for name in list(fields.keys())
|
120
|
-
if not fields.get(f'{name}', {}).get('count_as_empty')
|
121
|
-
])
|
122
|
-
if to_add:
|
123
|
-
for k, v in fields.items():
|
124
|
-
fname = f'{field_key}-{k}'
|
125
|
-
self.fields[fname] = v['field_class']()
|
126
|
-
self.fields[fname].initial = kwargs.get(fname)
|
127
|
-
self.o2m_keys.add(field_key)
|
128
|
-
|
129
|
-
def add_o2m_getter(self):
|
130
|
-
for k in self.o2m_fields.keys():
|
131
|
-
func_name = f'get_{k}'
|
132
|
-
setattr(self, func_name, partial(self.get_o2m_fields, k))
|
133
|
-
|
134
|
-
def get_o2m_fields(self, prefix):
|
135
|
-
fields = []
|
136
|
-
for k in self.o2m_keys:
|
137
|
-
if k.startswith(prefix):
|
138
|
-
fnames = list(
|
139
|
-
self.o2m_fields.get(prefix, {}).get('fields', [])
|
140
|
-
)
|
141
|
-
fields.append({
|
142
|
-
fname: self[f'{k}-{fname}']
|
143
|
-
for fname in fnames
|
144
|
-
})
|
145
|
-
|
146
|
-
order = self.o2m_fields.get(prefix, {}).get('order')
|
147
|
-
reverse = False
|
148
|
-
if order and order.startswith('-'):
|
149
|
-
order = order[1:]
|
150
|
-
reverse = True
|
151
|
-
if order and fields:
|
152
|
-
fields = sorted(
|
153
|
-
fields, key=lambda x: x[order].value(), reverse=reverse
|
154
|
-
)
|
155
|
-
return fields
|
156
|
-
|
157
|
-
def _clean_fields(self):
|
158
|
-
super()._clean_fields()
|
159
|
-
self._clean_o2m_fields()
|
160
|
-
|
161
|
-
def _clean_o2m_fields(self):
|
162
|
-
for related_name, related_field_config in self.o2m_fields.items():
|
163
|
-
self.cleaned_data[related_name] = []
|
164
|
-
fields = related_field_config['fields'].keys()
|
165
|
-
entries = filter(
|
166
|
-
lambda x: x.startswith(related_name), self.o2m_keys
|
167
|
-
)
|
168
|
-
for entry in entries:
|
169
|
-
value = {
|
170
|
-
fname: self.cleaned_data.get(f'{entry}-{fname}')
|
171
|
-
for fname in fields
|
172
|
-
}
|
173
|
-
try:
|
174
|
-
self.cleaned_data[related_name].append(
|
175
|
-
getattr(self, f'clean_{related_name}')(value)
|
176
|
-
)
|
177
|
-
except AttributeError:
|
178
|
-
self.cleaned_data[related_name].append(value)
|
179
|
-
except forms.ValidationError as error:
|
180
|
-
self.add_error(entry, error)
|
181
|
-
|
182
|
-
def save_o2m(self):
|
183
|
-
for field in self.o2m_fields.keys():
|
184
|
-
fk_field = self.o2m_fields[field].get('fk_field')
|
185
|
-
if not fk_field:
|
186
|
-
raise KeyError(
|
187
|
-
'One2Many Form definition is missing the '
|
188
|
-
f'"fk_field" attribute on field {field}'
|
189
|
-
)
|
190
|
-
|
191
|
-
related_objects = getattr(self.instance, field)
|
192
|
-
if not self.cleaned_data.get(field, []):
|
193
|
-
related_objects.all().delete()
|
194
|
-
|
195
|
-
related_object_fields = [
|
196
|
-
field.name for field
|
197
|
-
in related_objects.model._meta.get_fields()
|
198
|
-
]
|
199
|
-
update_pks, to_update, to_create = [], [], []
|
200
|
-
|
201
|
-
for item in self.cleaned_data.get(field, []):
|
202
|
-
item.update({fk_field: self.instance})
|
203
|
-
if pk := item.get('pk'):
|
204
|
-
update_pks.append(pk)
|
205
|
-
to_update.append(item)
|
206
|
-
else:
|
207
|
-
to_create.append(item)
|
208
|
-
|
209
|
-
qs = related_objects.filter(pk__in=update_pks).order_by('pk')
|
210
|
-
related_objects.exclude(pk__in=update_pks).delete()
|
211
|
-
to_update = sorted(to_update, key=lambda x: x['pk'])
|
212
|
-
if qs.count() != len(to_update):
|
213
|
-
raise ValueError(
|
214
|
-
'Mismatching length of found objects and update data'
|
215
|
-
)
|
216
|
-
updated = []
|
217
|
-
for obj, update in zip(qs, to_update):
|
218
|
-
for k, v in update.items():
|
219
|
-
if k not in related_object_fields:
|
220
|
-
continue
|
221
|
-
setattr(obj, k, v)
|
222
|
-
updated.append(obj)
|
223
|
-
objects_to_create = []
|
224
|
-
for c in to_create:
|
225
|
-
values = {}
|
226
|
-
for k, v in c.items():
|
227
|
-
if k in related_object_fields:
|
228
|
-
values.update({k: v})
|
229
|
-
objects_to_create.append(related_objects.model(**values))
|
230
|
-
if updated:
|
231
|
-
field_list = [
|
232
|
-
f for f in to_update[0].keys()
|
233
|
-
if f in related_object_fields
|
234
|
-
]
|
235
|
-
related_objects.bulk_update(
|
236
|
-
updated, fields=field_list
|
237
|
-
)
|
238
|
-
if to_create:
|
239
|
-
related_objects.bulk_create(objects_to_create)
|
240
|
-
|
241
|
-
def save(self, commit=True):
|
242
|
-
super().save(commit=commit)
|
243
|
-
if commit:
|
244
|
-
self.save_o2m()
|
245
|
-
return self.instance
|
246
|
-
|
247
|
-
|
248
10
|
class TenantForm(forms.Form):
|
249
11
|
|
250
12
|
def __init__(self, *args, **kwargs):
|
@@ -259,7 +21,7 @@ class TenantForm(forms.Form):
|
|
259
21
|
field.queryset = field.queryset.filter(tenant=self.tenant)
|
260
22
|
|
261
23
|
|
262
|
-
class TenantModelForm(
|
24
|
+
class TenantModelForm(forms.ModelForm):
|
263
25
|
|
264
26
|
def __init__(self, *args, **kwargs):
|
265
27
|
self.tenant = get_tenant()
|
@@ -272,13 +34,6 @@ class TenantModelForm(One2ManyModelForm):
|
|
272
34
|
for field in fields_to_filter:
|
273
35
|
field.queryset = field.queryset.filter(tenant=self.tenant)
|
274
36
|
|
275
|
-
def save_o2m(self):
|
276
|
-
o2m_fields = self.o2m_fields.keys()
|
277
|
-
for field in o2m_fields:
|
278
|
-
for item in self.cleaned_data.get(field, []):
|
279
|
-
item.update({'tenant': self.tenant})
|
280
|
-
return super().save_o2m()
|
281
|
-
|
282
37
|
def save(self, commit=True):
|
283
38
|
super().save(commit=False)
|
284
39
|
if not self.instance.pk or not self.instance.tenant:
|
@@ -287,6 +42,4 @@ class TenantModelForm(One2ManyModelForm):
|
|
287
42
|
if commit:
|
288
43
|
self.instance.save()
|
289
44
|
self.save_m2m()
|
290
|
-
self.save_o2m()
|
291
|
-
|
292
45
|
return self.instance
|
accrete/managers.py
CHANGED
@@ -7,8 +7,7 @@ class TenantManager(models.Manager, AnnotationManagerMixin):
|
|
7
7
|
|
8
8
|
def get_queryset(self):
|
9
9
|
queryset = super().get_queryset()
|
10
|
-
tenant
|
11
|
-
if tenant:
|
10
|
+
if tenant := get_tenant():
|
12
11
|
queryset = queryset.filter(tenant=tenant)
|
13
12
|
return queryset.annotate(**self.get_annotations())
|
14
13
|
|
accrete/utils/__init__.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
1
|
from . import dates
|
2
|
-
from .forms import save_form
|
3
|
-
from .http import filter_from_querystring
|
2
|
+
from .forms import save_form, save_forms, inline_vals_from_post, extend_formset
|
3
|
+
from .http import filter_from_querystring, cast_param
|
4
4
|
from .models import get_related_model
|
accrete/utils/forms.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
+
import re
|
1
2
|
import logging
|
2
3
|
from uuid import uuid4
|
4
|
+
from typing import Type
|
3
5
|
from django.db import transaction
|
6
|
+
from django.forms import BaseFormSet
|
4
7
|
|
5
8
|
_logger = logging.getLogger(__name__)
|
6
9
|
|
@@ -22,3 +25,79 @@ def save_form(form, reraise=False):
|
|
22
25
|
if reraise:
|
23
26
|
raise e
|
24
27
|
return form
|
28
|
+
|
29
|
+
|
30
|
+
def save_forms(form, inline_formsets: list = None, reraise: bool = False):
|
31
|
+
|
32
|
+
def handle_error(error):
|
33
|
+
form.save_error = repr(error)
|
34
|
+
error_id = str(uuid4())[:8]
|
35
|
+
_logger.exception(f'{error_id}: {error}')
|
36
|
+
form.save_error_id = error_id
|
37
|
+
|
38
|
+
form.is_saved = False
|
39
|
+
form.save_error = None
|
40
|
+
form.save_error_id = None
|
41
|
+
form.inline_forms = inline_formsets
|
42
|
+
|
43
|
+
try:
|
44
|
+
form.is_valid()
|
45
|
+
inlines_valid = all([
|
46
|
+
inline_formset.is_valid() for inline_formset in inline_formsets
|
47
|
+
])
|
48
|
+
except Exception as e:
|
49
|
+
handle_error(e)
|
50
|
+
if reraise:
|
51
|
+
raise e
|
52
|
+
return form
|
53
|
+
|
54
|
+
if not form.is_valid() or not inlines_valid:
|
55
|
+
return form
|
56
|
+
|
57
|
+
try:
|
58
|
+
with transaction.atomic():
|
59
|
+
form.save()
|
60
|
+
for inline_formset in inline_formsets:
|
61
|
+
inline_formset.save()
|
62
|
+
except Exception as e:
|
63
|
+
handle_error(e)
|
64
|
+
if reraise:
|
65
|
+
raise e
|
66
|
+
return form
|
67
|
+
|
68
|
+
form.is_saved = True
|
69
|
+
return form
|
70
|
+
|
71
|
+
|
72
|
+
def inline_vals_from_post(post: dict, prefix: str) -> list[dict]:
|
73
|
+
post_keys = set(re.findall(f'{prefix}-[0-9]+', ', '.join(post.keys())))
|
74
|
+
initial_data = {
|
75
|
+
post_key: {}
|
76
|
+
for post_key in post_keys if not post.get(f'{post_key}-DELETE')
|
77
|
+
}
|
78
|
+
for key, val in post.items():
|
79
|
+
post_key = '-'.join(key.split('-')[:-1])
|
80
|
+
if post_key not in initial_data:
|
81
|
+
continue
|
82
|
+
field_name = key.split('-')[-1]
|
83
|
+
initial_data[post_key].update({field_name: val})
|
84
|
+
return [val for val in initial_data.values()]
|
85
|
+
|
86
|
+
|
87
|
+
def extend_formset(formset_class, post: dict, data: list[dict]|dict, **formset_kwargs) -> Type[BaseFormSet]:
|
88
|
+
formset = formset_class(post, **formset_kwargs)
|
89
|
+
if not formset.is_valid():
|
90
|
+
return formset
|
91
|
+
form_data = post.copy()
|
92
|
+
if isinstance(data, dict):
|
93
|
+
data = [data]
|
94
|
+
prefix = formset_kwargs.get('prefix', 'form')
|
95
|
+
total = int(form_data[f'{prefix}-TOTAL_FORMS']) - 1
|
96
|
+
for item in data:
|
97
|
+
total += 1
|
98
|
+
form_data.update({f'{prefix}-{total}-{key}': value for key, value in item.items()})
|
99
|
+
form_data[f'{prefix}-TOTAL_FORMS'] = total + 1
|
100
|
+
formset = formset_class(form_data, **formset_kwargs)
|
101
|
+
for form in formset:
|
102
|
+
form._errors = {}
|
103
|
+
return formset
|
accrete/utils/http.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
import logging
|
2
2
|
import json
|
3
3
|
import operator
|
4
|
+
from typing import Callable
|
4
5
|
from django.db.models import Model, Q, QuerySet
|
5
6
|
from accrete.utils.models import get_related_model
|
6
7
|
from accrete.annotation import Annotation
|
7
8
|
|
8
9
|
_logger = logging.getLogger(__name__)
|
9
10
|
|
10
|
-
|
11
|
+
QUERYSTRING_KEY_MAP = {
|
11
12
|
'querystring': 'q',
|
12
13
|
'order': 'order',
|
13
14
|
'paginate_by': 'paginate_by',
|
@@ -19,7 +20,7 @@ def filter_from_querystring(
|
|
19
20
|
model: type[Model], get_params: dict, key_map: dict = None
|
20
21
|
) -> QuerySet:
|
21
22
|
|
22
|
-
key_map = key_map or
|
23
|
+
key_map = key_map or QUERYSTRING_KEY_MAP
|
23
24
|
querystring = get_params.get(key_map['querystring'], '[]')
|
24
25
|
order = get_params.get(key_map['order']) or model._meta.ordering
|
25
26
|
|
@@ -96,4 +97,14 @@ def parse_querystring(model: type[Model], query_string: str) -> Q:
|
|
96
97
|
|
97
98
|
ops = {'&': operator.and_, '|': operator.or_, '^': operator.xor}
|
98
99
|
query = parse_query_block(query_data)
|
99
|
-
return query
|
100
|
+
return query
|
101
|
+
|
102
|
+
|
103
|
+
def cast_param(params: dict, param: str, cast_to: Callable, default):
|
104
|
+
if param not in params:
|
105
|
+
return default
|
106
|
+
try:
|
107
|
+
return cast_to(params.get(param, default))
|
108
|
+
except Exception as e:
|
109
|
+
_logger.exception(e)
|
110
|
+
return default
|
@@ -3,8 +3,8 @@ accrete/admin.py,sha256=MUYUmCFlGYPowiXTbwl4_Q6Cq0-neiL53WW4P76JCLs,1174
|
|
3
3
|
accrete/annotation.py,sha256=P85kNgf_ka3U8i5cwaiKaAiSm21U-xY9PKmXMZR2ulU,1160
|
4
4
|
accrete/apps.py,sha256=F7ynMLHJr_6bRujWtZVUzCliY2CGKiDvyUmL4F68L2E,146
|
5
5
|
accrete/config.py,sha256=eJUbvyBO3DvAD6xkVKjTAzlXy7V7EK9bVyb91girfUs,299
|
6
|
-
accrete/forms.py,sha256=
|
7
|
-
accrete/managers.py,sha256=
|
6
|
+
accrete/forms.py,sha256=2vUh80qNvPDD8Zl3agKBSJEQeY7bXVLOx_SAB34wf8E,1359
|
7
|
+
accrete/managers.py,sha256=CaIJLeBry4NYIXaVUrdUjp7zx4sEWgv-1-ssI1m-EOs,1156
|
8
8
|
accrete/middleware.py,sha256=bUsvhdVdUlbqB-Hd5Y5w6WL8rO8It1VSGA5EZaEPA3o,3266
|
9
9
|
accrete/models.py,sha256=grvRNXg0ZYAJU3KAIX-svuZXeXlfqP4qEJ00nlbV594,5145
|
10
10
|
accrete/tenant.py,sha256=g3ZuTrQr2zqmIopNBRQeCmHEK2R3dlUme_hOV765J6U,1778
|
@@ -32,12 +32,12 @@ accrete/contrib/system_mail/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2
|
|
32
32
|
accrete/contrib/system_mail/views.py,sha256=xc1IQHrsij7j33TUbo-_oewy3vs03pw_etpBWaMYJl0,63
|
33
33
|
accrete/contrib/system_mail/migrations/0001_initial.py,sha256=6cwkkRXGjXvwXoMjjgmWmcPyXSTlUbhW1vMiHObk9MQ,1074
|
34
34
|
accrete/contrib/system_mail/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
35
|
-
accrete/contrib/ui/__init__.py,sha256=
|
35
|
+
accrete/contrib/ui/__init__.py,sha256=aeRSerct2JWpztNoxWDZXi7FzJhfxptVMVAZl4Sdzqs,504
|
36
36
|
accrete/contrib/ui/admin.py,sha256=suMo4x8I3JBxAFBVIdE-5qnqZ6JAZV0FESABHOSc-vg,63
|
37
37
|
accrete/contrib/ui/apps.py,sha256=E0ao2ox6PQ3ldfeR17FXJUUJuGiWjm2DPCxHbPXGzls,152
|
38
|
-
accrete/contrib/ui/context.py,sha256=
|
39
|
-
accrete/contrib/ui/elements.py,sha256=
|
40
|
-
accrete/contrib/ui/filter.py,sha256=
|
38
|
+
accrete/contrib/ui/context.py,sha256=jVD7w9QIIA2qh04UrO9rYDDrY-0Osr6FLggMlGxvztI,8364
|
39
|
+
accrete/contrib/ui/elements.py,sha256=0F5q0-XLdAWQjdYLMQt6RXqz4xPD_ECrhs0kcBfYkvo,1856
|
40
|
+
accrete/contrib/ui/filter.py,sha256=L7sBpmk454kaSZIQXe9hNj1Xbna8hJ2P-YTvmM7T5FE,12243
|
41
41
|
accrete/contrib/ui/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
|
42
42
|
accrete/contrib/ui/urls.py,sha256=TUBlz_CGs9InTZoxM78GSnucA73I8knoh_obt12RUHM,186
|
43
43
|
accrete/contrib/ui/views.py,sha256=WpBKMsxFFG8eG4IN7TW_TPE6i3OFF7gnLDTK7JMKti8,191
|
@@ -135,9 +135,9 @@ accrete/contrib/ui/static/bulma/versions/bulma-no-dark-mode.scss,sha256=w6Q80mCV
|
|
135
135
|
accrete/contrib/ui/static/bulma/versions/bulma-no-helpers-prefixed.scss,sha256=6HCUc4hxyaj4_rHqUnoy7aMuFZECHDYh5jvp-1CEtfE,344
|
136
136
|
accrete/contrib/ui/static/bulma/versions/bulma-no-helpers.scss,sha256=5dzAXSgReWUO8GXLbYTpOclPPD0xqvvBiCCX_GOR_5U,313
|
137
137
|
accrete/contrib/ui/static/bulma/versions/bulma-prefixed.scss,sha256=Yj7oEO00jy_G_L32y6rwzp2P5p2YtQ2Pvq4aZhvBSE8,138
|
138
|
-
accrete/contrib/ui/static/css/accrete.css,sha256=
|
139
|
-
accrete/contrib/ui/static/css/accrete.css.map,sha256=
|
140
|
-
accrete/contrib/ui/static/css/accrete.scss,sha256=
|
138
|
+
accrete/contrib/ui/static/css/accrete.css,sha256=pgbEKJKk3C0KXn_Vn43V90DGSC-J3Geg-imyTNuT4jk,604948
|
139
|
+
accrete/contrib/ui/static/css/accrete.css.map,sha256=g0TsYsK_LT_sa79uPNMFbzAyiUkaPwrUTw4jrE-g0Cg,94375
|
140
|
+
accrete/contrib/ui/static/css/accrete.scss,sha256=UHa62b7J14bEoiKYAmxm1dMlnb0lnpLDXWNbKT8kxX0,10385
|
141
141
|
accrete/contrib/ui/static/css/fa.css,sha256=wiz7ZSCn_btzhjKDQBms9Hx4sSeUYsDrTLg7roPstac,102641
|
142
142
|
accrete/contrib/ui/static/css/icons.css,sha256=5550KHsaayeEtRaUdf0h7esQhyec-_5ZfecZ_sOC6v0,6334
|
143
143
|
accrete/contrib/ui/static/icons/Logo.svg,sha256=hGZuxrAa-LRpFavFiF8Lnc7X9OQcqmb6Xl_dxx-27hM,1861
|
@@ -160,15 +160,16 @@ accrete/contrib/ui/templates/django/forms/widgets/input.html,sha256=CRu81kTsbPwi
|
|
160
160
|
accrete/contrib/ui/templates/django/forms/widgets/select.html,sha256=jT_UnHizHfdWTdJoSxjcIqTiR7NbVBA4bBSvkuDPRtw,394
|
161
161
|
accrete/contrib/ui/templates/django/forms/widgets/text.html,sha256=MSmLlQc7PsPoDLVtTOOiWNprrsPriNr712yFxaHyDIo,47
|
162
162
|
accrete/contrib/ui/templates/django/forms/widgets/textarea.html,sha256=c9BTedqb3IkXLyVYd0p9pR8DFnsXCNGoxVBWZTk_Fic,278
|
163
|
-
accrete/contrib/ui/templates/ui/
|
164
|
-
accrete/contrib/ui/templates/ui/
|
165
|
-
accrete/contrib/ui/templates/ui/
|
163
|
+
accrete/contrib/ui/templates/ui/dashboard.html,sha256=udnwiSJEcn2wMaJfTs4P0Y20FU79VguK_9Lq4K2BqtM,160
|
164
|
+
accrete/contrib/ui/templates/ui/detail.html,sha256=b1HC1QCGooo6tLh7fLhEDPau1tIOzscLXP6R_PN758I,1093
|
165
|
+
accrete/contrib/ui/templates/ui/form.html,sha256=cd6VUNzfIZH2_iudQa_EkqsPaBjyTam4Fm-Kgh8YpkY,655
|
166
|
+
accrete/contrib/ui/templates/ui/layout.html,sha256=f8K0KDlh_eDz0UKH0IB34dBjPtDiNgsuPgysK-DYLgE,14792
|
166
167
|
accrete/contrib/ui/templates/ui/list.html,sha256=6jmChhCpj6iuSqAG7X_eD5ZjVvwU4cyynmvoH0-juTk,1740
|
167
168
|
accrete/contrib/ui/templates/ui/table.html,sha256=bdPN2F7e7i3FHcQ18e0HJKkYT64PpxPRALRjcirJKGQ,4243
|
168
|
-
accrete/contrib/ui/templates/ui/partials/filter.html,sha256=
|
169
|
-
accrete/contrib/ui/templates/ui/partials/form_errors.html,sha256=
|
169
|
+
accrete/contrib/ui/templates/ui/partials/filter.html,sha256=2vmeL3980rMmkRnmVtZh9mBHe6S0PTMjaGIN1J6SpNM,7184
|
170
|
+
accrete/contrib/ui/templates/ui/partials/form_errors.html,sha256=QjfRriD9Z5SlhIFX__nVXqB3rPg4icbG4G02kML8IcQ,1529
|
170
171
|
accrete/contrib/ui/templates/ui/partials/form_modal.html,sha256=TQVkLypx8y1inZbvUYKtSNHrDXJM1xvYl8IsDDwQwkE,1093
|
171
|
-
accrete/contrib/ui/templates/ui/partials/header.html,sha256=
|
172
|
+
accrete/contrib/ui/templates/ui/partials/header.html,sha256=5ER9E6c-vwDxuIuMSXL4F_fq2zYY17R_0A0Ao--H4Us,6916
|
172
173
|
accrete/contrib/ui/templates/ui/partials/onchange_form.html,sha256=K5twTGqRUW1iM2dGtdWntjsJvJVo5EIzKxX2HK-H1yw,160
|
173
174
|
accrete/contrib/ui/templates/ui/partials/pagination_detail.html,sha256=58nA3X7Il0FAD4VcYyr7tTGWRiVf_FN1TkImmKEpKHU,1014
|
174
175
|
accrete/contrib/ui/templates/ui/partials/pagination_list.html,sha256=Eyx1lsk9UIFFYPICL7RuYeUFaKVlmvWVXnFCGR-II7k,1324
|
@@ -213,12 +214,12 @@ accrete/contrib/user_registration/templates/user_registration/mail_templates/con
|
|
213
214
|
accrete/migrations/0001_initial.py,sha256=azThbc8otEhxJwo8BIgOt5eC30mxXhKJLBAazZFe3BA,4166
|
214
215
|
accrete/migrations/0002_initial.py,sha256=dFOM7kdHlx7pVAh8cTDlZMtciN4O9Z547HAzEKnygZE,1628
|
215
216
|
accrete/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
216
|
-
accrete/utils/__init__.py,sha256=
|
217
|
+
accrete/utils/__init__.py,sha256=YwEzwjz-E92LHhqeLsQ4167zXHHY1PFG6xcsAofnmBU,192
|
217
218
|
accrete/utils/dates.py,sha256=apM6kt6JhGrKgoT0jfav1W-8AUVTxNc9xt3fJQ2n0JI,1492
|
218
|
-
accrete/utils/forms.py,sha256=
|
219
|
-
accrete/utils/http.py,sha256=
|
219
|
+
accrete/utils/forms.py,sha256=UP6vCCTtXD5MqU2LWbNXtk2ZMMEmoty_tjLCbJlqYsY,2984
|
220
|
+
accrete/utils/http.py,sha256=mAtQRgADv7zu1_j7A-EKVyb-oqa5a21i4Gd0QfjzGV0,3540
|
220
221
|
accrete/utils/models.py,sha256=EEhv7-sQVtQD24PEb3XcDUAh3VVhVFoMMLyFrDjGEaI,706
|
221
|
-
accrete-0.0.
|
222
|
-
accrete-0.0.
|
223
|
-
accrete-0.0.
|
224
|
-
accrete-0.0.
|
222
|
+
accrete-0.0.56.dist-info/METADATA,sha256=B_wB5SK-frLdSHpern7BWrGfaHDlycd0A34UGnkXFY0,4892
|
223
|
+
accrete-0.0.56.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
224
|
+
accrete-0.0.56.dist-info/licenses/LICENSE,sha256=_7laeMIHnsd3Y2vJEXDYXq_PEXxIcjgJsGt8UIKTRWc,1057
|
225
|
+
accrete-0.0.56.dist-info/RECORD,,
|