django-unfold 0.24.0__py3-none-any.whl → 0.26.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. {django_unfold-0.24.0.dist-info → django_unfold-0.26.0.dist-info}/METADATA +51 -6
  2. {django_unfold-0.24.0.dist-info → django_unfold-0.26.0.dist-info}/RECORD +36 -26
  3. unfold/admin.py +11 -3
  4. unfold/contrib/import_export/forms.py +28 -5
  5. unfold/contrib/import_export/templates/admin/import_export/change_form.html +10 -0
  6. unfold/contrib/import_export/templates/admin/import_export/export.html +38 -3
  7. unfold/contrib/import_export/templates/admin/import_export/import_form.html +9 -20
  8. unfold/contrib/import_export/templates/admin/import_export/resource_fields_list.html +24 -0
  9. unfold/contrib/inlines/__init__.py +0 -0
  10. unfold/contrib/inlines/admin.py +141 -0
  11. unfold/contrib/inlines/apps.py +6 -0
  12. unfold/contrib/inlines/checks.py +18 -0
  13. unfold/contrib/inlines/forms.py +43 -0
  14. unfold/forms.py +6 -0
  15. unfold/static/unfold/css/simplebar.css +230 -0
  16. unfold/static/unfold/css/styles.css +1 -1
  17. unfold/static/unfold/js/simplebar.js +10 -0
  18. unfold/styles.css +10 -2
  19. unfold/templates/admin/app_list.html +1 -1
  20. unfold/templates/admin/change_form.html +11 -9
  21. unfold/templates/admin/change_list_results.html +67 -65
  22. unfold/templates/admin/edit_inline/stacked.html +7 -7
  23. unfold/templates/admin/edit_inline/tabular.html +111 -109
  24. unfold/templates/admin/includes/fieldset.html +1 -1
  25. unfold/templates/unfold/helpers/app_list.html +1 -1
  26. unfold/templates/unfold/helpers/display_header.html +11 -8
  27. unfold/templates/unfold/helpers/field.html +20 -6
  28. unfold/templates/unfold/helpers/fieldsets_tabs.html +4 -4
  29. unfold/templates/unfold/helpers/form_label.html +1 -1
  30. unfold/templates/unfold/layouts/skeleton.html +2 -0
  31. unfold/templates/unfold/widgets/foreign_key_raw_id.html +21 -0
  32. unfold/templates/unfold/widgets/textarea.html +1 -7
  33. unfold/templates/unfold/widgets/textarea_expandable.html +7 -0
  34. unfold/widgets.py +36 -3
  35. unfold/contrib/import_export/admin.py +0 -37
  36. {django_unfold-0.24.0.dist-info → django_unfold-0.26.0.dist-info}/LICENSE.md +0 -0
  37. {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,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DefaultAppConfig(AppConfig):
5
+ name = "unfold.contrib.inlines"
6
+ label = "unfold_inlines"
@@ -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
+ }