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.
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
+ }