django-unfold 0.24.0__py3-none-any.whl → 0.26.0__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.
- {django_unfold-0.24.0.dist-info → django_unfold-0.26.0.dist-info}/METADATA +51 -6
- {django_unfold-0.24.0.dist-info → django_unfold-0.26.0.dist-info}/RECORD +36 -26
- unfold/admin.py +11 -3
- unfold/contrib/import_export/forms.py +28 -5
- unfold/contrib/import_export/templates/admin/import_export/change_form.html +10 -0
- unfold/contrib/import_export/templates/admin/import_export/export.html +38 -3
- unfold/contrib/import_export/templates/admin/import_export/import_form.html +9 -20
- unfold/contrib/import_export/templates/admin/import_export/resource_fields_list.html +24 -0
- unfold/contrib/inlines/__init__.py +0 -0
- unfold/contrib/inlines/admin.py +141 -0
- unfold/contrib/inlines/apps.py +6 -0
- unfold/contrib/inlines/checks.py +18 -0
- unfold/contrib/inlines/forms.py +43 -0
- unfold/forms.py +6 -0
- unfold/static/unfold/css/simplebar.css +230 -0
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/js/simplebar.js +10 -0
- unfold/styles.css +10 -2
- unfold/templates/admin/app_list.html +1 -1
- unfold/templates/admin/change_form.html +11 -9
- unfold/templates/admin/change_list_results.html +67 -65
- unfold/templates/admin/edit_inline/stacked.html +7 -7
- unfold/templates/admin/edit_inline/tabular.html +111 -109
- unfold/templates/admin/includes/fieldset.html +1 -1
- unfold/templates/unfold/helpers/app_list.html +1 -1
- unfold/templates/unfold/helpers/display_header.html +11 -8
- unfold/templates/unfold/helpers/field.html +20 -6
- unfold/templates/unfold/helpers/fieldsets_tabs.html +4 -4
- unfold/templates/unfold/helpers/form_label.html +1 -1
- unfold/templates/unfold/layouts/skeleton.html +2 -0
- unfold/templates/unfold/widgets/foreign_key_raw_id.html +21 -0
- unfold/templates/unfold/widgets/textarea.html +1 -7
- unfold/templates/unfold/widgets/textarea_expandable.html +7 -0
- unfold/widgets.py +36 -3
- unfold/contrib/import_export/admin.py +0 -37
- {django_unfold-0.24.0.dist-info → django_unfold-0.26.0.dist-info}/LICENSE.md +0 -0
- {django_unfold-0.24.0.dist-info → django_unfold-0.26.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
from functools import partial
|
2
|
+
from typing import Any, Optional
|
3
|
+
|
4
|
+
from django import forms
|
5
|
+
from django.contrib.admin.utils import NestedObjects, flatten_fieldsets
|
6
|
+
from django.core.exceptions import ValidationError
|
7
|
+
from django.db import router
|
8
|
+
from django.db.models import Model
|
9
|
+
from django.forms.formsets import DELETION_FIELD_NAME
|
10
|
+
from django.forms.models import modelform_defines_fields
|
11
|
+
from django.http import HttpRequest
|
12
|
+
from django.utils.text import get_text_list
|
13
|
+
from django.utils.translation import gettext_lazy as _
|
14
|
+
from unfold.admin import StackedInline, TabularInline
|
15
|
+
|
16
|
+
from .checks import NonrelatedModelAdminChecks
|
17
|
+
from .forms import NonrelatedInlineModelFormSet, nonrelated_inline_formset_factory
|
18
|
+
|
19
|
+
|
20
|
+
class NonrelatedInlineMixin:
|
21
|
+
checks_class = NonrelatedModelAdminChecks
|
22
|
+
formset = NonrelatedInlineModelFormSet
|
23
|
+
|
24
|
+
def get_formset(
|
25
|
+
self, request: HttpRequest, obj: Optional[Model] = None, **kwargs: Any
|
26
|
+
):
|
27
|
+
defaults = self._get_formset_defaults(request, obj, **kwargs)
|
28
|
+
|
29
|
+
defaults["queryset"] = (
|
30
|
+
self.get_form_queryset(obj) if obj else self.model.objects.none()
|
31
|
+
)
|
32
|
+
|
33
|
+
return nonrelated_inline_formset_factory(
|
34
|
+
self.model, save_new_instance=self.save_new_instance, **defaults
|
35
|
+
)
|
36
|
+
|
37
|
+
def _get_formset_defaults(
|
38
|
+
self, request: HttpRequest, obj: Optional[Model] = None, **kwargs: Any
|
39
|
+
):
|
40
|
+
"""Return a BaseInlineFormSet class for use in admin add/change views."""
|
41
|
+
if "fields" in kwargs:
|
42
|
+
fields = kwargs.pop("fields")
|
43
|
+
else:
|
44
|
+
fields = flatten_fieldsets(self.get_fieldsets(request, obj))
|
45
|
+
excluded = self.get_exclude(request, obj)
|
46
|
+
exclude = [] if excluded is None else list(excluded)
|
47
|
+
exclude.extend(self.get_readonly_fields(request, obj))
|
48
|
+
if excluded is None and hasattr(self.form, "_meta") and self.form._meta.exclude:
|
49
|
+
# Take the custom ModelForm's Meta.exclude into account only if the
|
50
|
+
# InlineModelAdmin doesn't define its own.
|
51
|
+
exclude.extend(self.form._meta.exclude)
|
52
|
+
# If exclude is an empty list we use None, since that's the actual
|
53
|
+
# default.
|
54
|
+
exclude = exclude or None
|
55
|
+
can_delete = self.can_delete and self.has_delete_permission(request, obj)
|
56
|
+
defaults = {
|
57
|
+
"form": self.form,
|
58
|
+
"formset": self.formset,
|
59
|
+
# "fk_name": self.fk_name,
|
60
|
+
"fields": fields,
|
61
|
+
"exclude": exclude,
|
62
|
+
"formfield_callback": partial(self.formfield_for_dbfield, request=request),
|
63
|
+
"extra": self.get_extra(request, obj, **kwargs),
|
64
|
+
"min_num": self.get_min_num(request, obj, **kwargs),
|
65
|
+
"max_num": self.get_max_num(request, obj, **kwargs),
|
66
|
+
"can_delete": can_delete,
|
67
|
+
**kwargs,
|
68
|
+
}
|
69
|
+
|
70
|
+
base_model_form = defaults["form"]
|
71
|
+
can_change = self.has_change_permission(request, obj) if request else True
|
72
|
+
can_add = self.has_add_permission(request, obj) if request else True
|
73
|
+
|
74
|
+
class DeleteProtectedModelForm(base_model_form):
|
75
|
+
def hand_clean_DELETE(self):
|
76
|
+
"""
|
77
|
+
We don't validate the 'DELETE' field itself because on
|
78
|
+
templates it's not rendered using the field information, but
|
79
|
+
just using a generic "deletion_field" of the InlineModelAdmin.
|
80
|
+
"""
|
81
|
+
if self.cleaned_data.get(DELETION_FIELD_NAME, False):
|
82
|
+
using = router.db_for_write(self._meta.model)
|
83
|
+
collector = NestedObjects(using=using)
|
84
|
+
if self.instance._state.adding:
|
85
|
+
return
|
86
|
+
collector.collect([self.instance])
|
87
|
+
if collector.protected:
|
88
|
+
objs = []
|
89
|
+
for p in collector.protected:
|
90
|
+
objs.append(
|
91
|
+
# Translators: Model verbose name and instance representation,
|
92
|
+
# suitable to be an item in a list.
|
93
|
+
_("%(class_name)s %(instance)s")
|
94
|
+
% {
|
95
|
+
"class_name": p._meta.verbose_name,
|
96
|
+
"instance": p,
|
97
|
+
}
|
98
|
+
)
|
99
|
+
params = {
|
100
|
+
"class_name": self._meta.model._meta.verbose_name,
|
101
|
+
"instance": self.instance,
|
102
|
+
"related_objects": get_text_list(objs, _("and")),
|
103
|
+
}
|
104
|
+
msg = _(
|
105
|
+
"Deleting %(class_name)s %(instance)s would require "
|
106
|
+
"deleting the following protected related objects: "
|
107
|
+
"%(related_objects)s"
|
108
|
+
)
|
109
|
+
raise ValidationError(
|
110
|
+
msg, code="deleting_protected", params=params
|
111
|
+
)
|
112
|
+
|
113
|
+
def is_valid(self):
|
114
|
+
result = super().is_valid()
|
115
|
+
self.hand_clean_DELETE()
|
116
|
+
return result
|
117
|
+
|
118
|
+
def has_changed(self):
|
119
|
+
# Protect against unauthorized edits.
|
120
|
+
if not can_change and not self.instance._state.adding:
|
121
|
+
return False
|
122
|
+
if not can_add and self.instance._state.adding:
|
123
|
+
return False
|
124
|
+
return super().has_changed()
|
125
|
+
|
126
|
+
defaults["form"] = DeleteProtectedModelForm
|
127
|
+
|
128
|
+
if defaults["fields"] is None and not modelform_defines_fields(
|
129
|
+
defaults["form"]
|
130
|
+
):
|
131
|
+
defaults["fields"] = forms.ALL_FIELDS
|
132
|
+
|
133
|
+
return defaults
|
134
|
+
|
135
|
+
|
136
|
+
class NonrelatedStackedInline(NonrelatedInlineMixin, StackedInline):
|
137
|
+
pass
|
138
|
+
|
139
|
+
|
140
|
+
class NonrelatedTabularInline(NonrelatedInlineMixin, TabularInline):
|
141
|
+
pass
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from django.contrib.admin.checks import InlineModelAdminChecks
|
4
|
+
from django.contrib.admin.options import InlineModelAdmin
|
5
|
+
from django.core.checks import CheckMessage
|
6
|
+
from django.db.models import Model
|
7
|
+
|
8
|
+
|
9
|
+
class NonrelatedModelAdminChecks(InlineModelAdminChecks):
|
10
|
+
def _check_exclude_of_parent_model(
|
11
|
+
self, obj: InlineModelAdmin, parent_model: Model
|
12
|
+
) -> List[CheckMessage]:
|
13
|
+
return []
|
14
|
+
|
15
|
+
def _check_relation(
|
16
|
+
self, obj: InlineModelAdmin, parent_model: Model
|
17
|
+
) -> List[CheckMessage]:
|
18
|
+
return []
|
@@ -0,0 +1,43 @@
|
|
1
|
+
from typing import Any, Callable, Optional
|
2
|
+
|
3
|
+
from django.db.models import Model, QuerySet
|
4
|
+
from django.forms import BaseModelFormSet, ModelForm, modelformset_factory
|
5
|
+
|
6
|
+
|
7
|
+
class NonrelatedInlineModelFormSet(BaseModelFormSet):
|
8
|
+
def __init__(
|
9
|
+
self,
|
10
|
+
instance: Optional[Model] = None,
|
11
|
+
save_as_new: bool = False,
|
12
|
+
**kwargs: Any,
|
13
|
+
) -> None:
|
14
|
+
self.instance = instance
|
15
|
+
self.queryset = self.provided_queryset
|
16
|
+
|
17
|
+
super().__init__(**kwargs)
|
18
|
+
|
19
|
+
@classmethod
|
20
|
+
def get_default_prefix(cls: BaseModelFormSet) -> str:
|
21
|
+
return f"{cls.model._meta.app_label}-{cls.model._meta.model_name}"
|
22
|
+
|
23
|
+
def save_new(self, form: ModelForm, commit: bool = True):
|
24
|
+
obj = super().save_new(form, commit=False)
|
25
|
+
self.save_new_instance(self.instance, obj)
|
26
|
+
|
27
|
+
if commit:
|
28
|
+
obj.save()
|
29
|
+
|
30
|
+
return obj
|
31
|
+
|
32
|
+
|
33
|
+
def nonrelated_inline_formset_factory(
|
34
|
+
model: Model,
|
35
|
+
queryset: Optional[QuerySet] = None,
|
36
|
+
formset: BaseModelFormSet = NonrelatedInlineModelFormSet,
|
37
|
+
save_new_instance: Optional[Callable] = None,
|
38
|
+
**kwargs: Any,
|
39
|
+
) -> BaseModelFormSet:
|
40
|
+
inline_formset = modelformset_factory(model, formset=formset, **kwargs)
|
41
|
+
inline_formset.provided_queryset = queryset
|
42
|
+
inline_formset.save_new_instance = save_new_instance
|
43
|
+
return inline_formset
|
unfold/forms.py
CHANGED
@@ -10,6 +10,7 @@ from django.contrib.admin.forms import (
|
|
10
10
|
from django.contrib.auth.forms import (
|
11
11
|
AdminPasswordChangeForm as BaseAdminPasswordChangeForm,
|
12
12
|
)
|
13
|
+
from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
|
13
14
|
from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
|
14
15
|
from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm
|
15
16
|
from django.http import HttpRequest
|
@@ -19,6 +20,10 @@ from django.utils.translation import gettext_lazy as _
|
|
19
20
|
from .widgets import BASE_INPUT_CLASSES, INPUT_CLASSES, SELECT_CLASSES
|
20
21
|
|
21
22
|
|
23
|
+
class UnfoldReadOnlyPasswordHashWidget(ReadOnlyPasswordHashWidget):
|
24
|
+
pass
|
25
|
+
|
26
|
+
|
22
27
|
class ActionForm(forms.Form):
|
23
28
|
action = forms.ChoiceField(
|
24
29
|
label="",
|
@@ -72,6 +77,7 @@ class UserChangeForm(BaseUserChangeForm):
|
|
72
77
|
**kwargs,
|
73
78
|
) -> None:
|
74
79
|
super().__init__(request, *args, **kwargs)
|
80
|
+
self.fields["password"].widget = UnfoldReadOnlyPasswordHashWidget()
|
75
81
|
|
76
82
|
self.fields["password"].help_text = _(
|
77
83
|
"Raw passwords are not stored, so there is no way to see this "
|
@@ -0,0 +1,230 @@
|
|
1
|
+
[data-simplebar] {
|
2
|
+
position: relative;
|
3
|
+
flex-direction: column;
|
4
|
+
flex-wrap: wrap;
|
5
|
+
justify-content: flex-start;
|
6
|
+
align-content: flex-start;
|
7
|
+
align-items: flex-start;
|
8
|
+
}
|
9
|
+
|
10
|
+
.simplebar-wrapper {
|
11
|
+
overflow: hidden;
|
12
|
+
width: inherit;
|
13
|
+
height: inherit;
|
14
|
+
max-width: inherit;
|
15
|
+
max-height: inherit;
|
16
|
+
}
|
17
|
+
|
18
|
+
.simplebar-mask {
|
19
|
+
direction: inherit;
|
20
|
+
position: absolute;
|
21
|
+
overflow: hidden;
|
22
|
+
padding: 0;
|
23
|
+
margin: 0;
|
24
|
+
left: 0;
|
25
|
+
top: 0;
|
26
|
+
bottom: 0;
|
27
|
+
right: 0;
|
28
|
+
width: auto !important;
|
29
|
+
height: auto !important;
|
30
|
+
z-index: 0;
|
31
|
+
}
|
32
|
+
|
33
|
+
.simplebar-offset {
|
34
|
+
direction: inherit !important;
|
35
|
+
box-sizing: inherit !important;
|
36
|
+
resize: none !important;
|
37
|
+
position: absolute;
|
38
|
+
top: 0;
|
39
|
+
left: 0;
|
40
|
+
bottom: 0;
|
41
|
+
right: 0;
|
42
|
+
padding: 0;
|
43
|
+
margin: 0;
|
44
|
+
-webkit-overflow-scrolling: touch;
|
45
|
+
}
|
46
|
+
|
47
|
+
.simplebar-content-wrapper {
|
48
|
+
direction: inherit;
|
49
|
+
box-sizing: border-box !important;
|
50
|
+
position: relative;
|
51
|
+
display: block;
|
52
|
+
height: 100%; /* Required for horizontal native scrollbar to not appear if parent is taller than natural height */
|
53
|
+
width: auto;
|
54
|
+
max-width: 100%; /* Not required for horizontal scroll to trigger */
|
55
|
+
max-height: 100%; /* Needed for vertical scroll to trigger */
|
56
|
+
overflow: auto;
|
57
|
+
scrollbar-width: none;
|
58
|
+
-ms-overflow-style: none;
|
59
|
+
}
|
60
|
+
|
61
|
+
.simplebar-content-wrapper::-webkit-scrollbar,
|
62
|
+
.simplebar-hide-scrollbar::-webkit-scrollbar {
|
63
|
+
display: none;
|
64
|
+
width: 0;
|
65
|
+
height: 0;
|
66
|
+
}
|
67
|
+
|
68
|
+
.simplebar-content:before,
|
69
|
+
.simplebar-content:after {
|
70
|
+
content: ' ';
|
71
|
+
display: table;
|
72
|
+
}
|
73
|
+
|
74
|
+
.simplebar-placeholder {
|
75
|
+
max-height: 100%;
|
76
|
+
max-width: 100%;
|
77
|
+
width: 100%;
|
78
|
+
pointer-events: none;
|
79
|
+
}
|
80
|
+
|
81
|
+
.simplebar-height-auto-observer-wrapper {
|
82
|
+
box-sizing: inherit !important;
|
83
|
+
height: 100%;
|
84
|
+
width: 100%;
|
85
|
+
max-width: 1px;
|
86
|
+
position: relative;
|
87
|
+
float: left;
|
88
|
+
max-height: 1px;
|
89
|
+
overflow: hidden;
|
90
|
+
z-index: -1;
|
91
|
+
padding: 0;
|
92
|
+
margin: 0;
|
93
|
+
pointer-events: none;
|
94
|
+
flex-grow: inherit;
|
95
|
+
flex-shrink: 0;
|
96
|
+
flex-basis: 0;
|
97
|
+
}
|
98
|
+
|
99
|
+
.simplebar-height-auto-observer {
|
100
|
+
box-sizing: inherit;
|
101
|
+
display: block;
|
102
|
+
opacity: 0;
|
103
|
+
position: absolute;
|
104
|
+
top: 0;
|
105
|
+
left: 0;
|
106
|
+
height: 1000%;
|
107
|
+
width: 1000%;
|
108
|
+
min-height: 1px;
|
109
|
+
min-width: 1px;
|
110
|
+
overflow: hidden;
|
111
|
+
pointer-events: none;
|
112
|
+
z-index: -1;
|
113
|
+
}
|
114
|
+
|
115
|
+
.simplebar-track {
|
116
|
+
z-index: 1;
|
117
|
+
position: absolute;
|
118
|
+
right: 0;
|
119
|
+
bottom: 0;
|
120
|
+
pointer-events: none;
|
121
|
+
overflow: hidden;
|
122
|
+
}
|
123
|
+
|
124
|
+
[data-simplebar].simplebar-dragging {
|
125
|
+
pointer-events: none;
|
126
|
+
-webkit-touch-callout: none;
|
127
|
+
-webkit-user-select: none;
|
128
|
+
-khtml-user-select: none;
|
129
|
+
-moz-user-select: none;
|
130
|
+
-ms-user-select: none;
|
131
|
+
user-select: none;
|
132
|
+
}
|
133
|
+
|
134
|
+
[data-simplebar].simplebar-dragging .simplebar-content {
|
135
|
+
pointer-events: none;
|
136
|
+
-webkit-touch-callout: none;
|
137
|
+
-webkit-user-select: none;
|
138
|
+
-khtml-user-select: none;
|
139
|
+
-moz-user-select: none;
|
140
|
+
-ms-user-select: none;
|
141
|
+
user-select: none;
|
142
|
+
}
|
143
|
+
|
144
|
+
[data-simplebar].simplebar-dragging .simplebar-track {
|
145
|
+
pointer-events: all;
|
146
|
+
}
|
147
|
+
|
148
|
+
.simplebar-scrollbar {
|
149
|
+
position: absolute;
|
150
|
+
left: 0;
|
151
|
+
right: 0;
|
152
|
+
min-height: 10px;
|
153
|
+
}
|
154
|
+
|
155
|
+
.simplebar-scrollbar:before {
|
156
|
+
position: absolute;
|
157
|
+
content: '';
|
158
|
+
background: black;
|
159
|
+
border-radius: 7px;
|
160
|
+
left: 2px;
|
161
|
+
right: 2px;
|
162
|
+
opacity: 0;
|
163
|
+
transition: opacity 0.2s 0.5s linear;
|
164
|
+
}
|
165
|
+
|
166
|
+
.simplebar-scrollbar.simplebar-visible:before {
|
167
|
+
opacity: 0.5;
|
168
|
+
transition-delay: 0s;
|
169
|
+
transition-duration: 0s;
|
170
|
+
}
|
171
|
+
|
172
|
+
.simplebar-track.simplebar-vertical {
|
173
|
+
top: 0;
|
174
|
+
width: 11px;
|
175
|
+
}
|
176
|
+
|
177
|
+
.simplebar-scrollbar:before {
|
178
|
+
top: 2px;
|
179
|
+
bottom: 2px;
|
180
|
+
left: 2px;
|
181
|
+
right: 2px;
|
182
|
+
}
|
183
|
+
|
184
|
+
.simplebar-track.simplebar-horizontal {
|
185
|
+
left: 0;
|
186
|
+
height: 11px;
|
187
|
+
}
|
188
|
+
|
189
|
+
.simplebar-track.simplebar-horizontal .simplebar-scrollbar {
|
190
|
+
right: auto;
|
191
|
+
left: 0;
|
192
|
+
top: 0;
|
193
|
+
bottom: 0;
|
194
|
+
min-height: 0;
|
195
|
+
min-width: 10px;
|
196
|
+
width: auto;
|
197
|
+
}
|
198
|
+
|
199
|
+
/* Rtl support */
|
200
|
+
[data-simplebar-direction='rtl'] .simplebar-track.simplebar-vertical {
|
201
|
+
right: auto;
|
202
|
+
left: 0;
|
203
|
+
}
|
204
|
+
|
205
|
+
.simplebar-dummy-scrollbar-size {
|
206
|
+
direction: rtl;
|
207
|
+
position: fixed;
|
208
|
+
opacity: 0;
|
209
|
+
visibility: hidden;
|
210
|
+
height: 500px;
|
211
|
+
width: 500px;
|
212
|
+
overflow-y: hidden;
|
213
|
+
overflow-x: scroll;
|
214
|
+
-ms-overflow-style: scrollbar !important;
|
215
|
+
}
|
216
|
+
|
217
|
+
.simplebar-dummy-scrollbar-size > div {
|
218
|
+
width: 200%;
|
219
|
+
height: 200%;
|
220
|
+
margin: 10px 0;
|
221
|
+
}
|
222
|
+
|
223
|
+
.simplebar-hide-scrollbar {
|
224
|
+
position: fixed;
|
225
|
+
left: 0;
|
226
|
+
visibility: hidden;
|
227
|
+
overflow-y: scroll;
|
228
|
+
scrollbar-width: none;
|
229
|
+
-ms-overflow-style: none;
|
230
|
+
}
|