django-unfold 0.67.0__py3-none-any.whl → 0.69.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 (54) hide show
  1. {django_unfold-0.67.0.dist-info → django_unfold-0.69.0.dist-info}/METADATA +33 -41
  2. {django_unfold-0.67.0.dist-info → django_unfold-0.69.0.dist-info}/RECORD +54 -47
  3. unfold/admin.py +46 -15
  4. unfold/components.py +2 -2
  5. unfold/contrib/filters/admin/choice_filters.py +13 -1
  6. unfold/contrib/filters/admin/mixins.py +3 -3
  7. unfold/contrib/filters/admin/numeric_filters.py +6 -6
  8. unfold/contrib/forms/widgets.py +5 -5
  9. unfold/contrib/inlines/admin.py +3 -3
  10. unfold/contrib/inlines/forms.py +5 -4
  11. unfold/dataclasses.py +13 -13
  12. unfold/datasets.py +90 -0
  13. unfold/decorators.py +19 -19
  14. unfold/fields.py +3 -5
  15. unfold/forms.py +41 -22
  16. unfold/mixins/__init__.py +2 -1
  17. unfold/mixins/action_model_admin.py +11 -10
  18. unfold/mixins/base_model_admin.py +6 -6
  19. unfold/mixins/dataset_model_admin.py +62 -0
  20. unfold/settings.py +1 -0
  21. unfold/sites.py +19 -18
  22. unfold/static/admin/js/actions.js +246 -0
  23. unfold/static/unfold/css/styles.css +2 -2
  24. unfold/static/unfold/fonts/material-symbols/Material-Symbols-Outlined.woff2 +0 -0
  25. unfold/static/unfold/js/app.js +3 -1
  26. unfold/styles.css +21 -16
  27. unfold/templates/admin/actions.html +2 -2
  28. unfold/templates/admin/change_form.html +10 -2
  29. unfold/templates/admin/change_list.html +1 -1
  30. unfold/templates/admin/change_list_results.html +10 -62
  31. unfold/templates/admin/dataset_actions.html +50 -0
  32. unfold/templates/admin/edit_inline/stacked.html +2 -8
  33. unfold/templates/admin/edit_inline/tabular.html +1 -7
  34. unfold/templates/admin/includes/fieldset.html +1 -3
  35. unfold/templates/admin/search_form.html +6 -4
  36. unfold/templates/registration/password_change_done.html +3 -4
  37. unfold/templates/registration/password_change_form.html +10 -6
  38. unfold/templates/unfold/helpers/change_list_actions.html +1 -1
  39. unfold/templates/unfold/helpers/change_list_headers.html +65 -0
  40. unfold/templates/unfold/helpers/dataset.html +31 -0
  41. unfold/templates/unfold/helpers/edit_inline/tabular_field.html +1 -1
  42. unfold/templates/unfold/helpers/empty_results.html +6 -4
  43. unfold/templates/unfold/helpers/field_readonly_value_file.html +1 -1
  44. unfold/templates/unfold/helpers/fieldsets_tabs.html +9 -11
  45. unfold/templates/unfold/helpers/inline_heading.html +11 -0
  46. unfold/templates/unfold/helpers/tab_items.html +9 -1
  47. unfold/templatetags/unfold.py +64 -82
  48. unfold/templatetags/unfold_list.py +76 -8
  49. unfold/typing.py +5 -6
  50. unfold/utils.py +9 -9
  51. unfold/views.py +15 -1
  52. unfold/widgets.py +31 -31
  53. {django_unfold-0.67.0.dist-info → django_unfold-0.69.0.dist-info}/WHEEL +0 -0
  54. {django_unfold-0.67.0.dist-info → django_unfold-0.69.0.dist-info}/licenses/LICENSE.md +0 -0
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional
1
+ from typing import Any
2
2
 
3
3
  from django.contrib import admin
4
4
  from django.contrib.admin.options import ModelAdmin
@@ -35,7 +35,7 @@ class SingleNumericFilter(admin.FieldListFilter):
35
35
  ) -> None:
36
36
  super().__init__(field, request, params, model, model_admin, field_path)
37
37
 
38
- if not isinstance(field, (DecimalField, IntegerField, FloatField, AutoField)):
38
+ if not isinstance(field, DecimalField | IntegerField | FloatField | AutoField):
39
39
  raise TypeError(
40
40
  f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
41
41
  )
@@ -54,7 +54,7 @@ class SingleNumericFilter(admin.FieldListFilter):
54
54
 
55
55
  def queryset(
56
56
  self, request: HttpRequest, queryset: QuerySet[Any]
57
- ) -> Optional[QuerySet]:
57
+ ) -> QuerySet | None:
58
58
  if self.value():
59
59
  try:
60
60
  return queryset.filter(**{self.parameter_name: self.value()})
@@ -64,7 +64,7 @@ class SingleNumericFilter(admin.FieldListFilter):
64
64
  def value(self) -> Any:
65
65
  return self.used_parameters.get(self.parameter_name, None)
66
66
 
67
- def expected_parameters(self) -> list[Optional[str]]:
67
+ def expected_parameters(self) -> list[str | None]:
68
68
  return [self.parameter_name]
69
69
 
70
70
  def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
@@ -111,7 +111,7 @@ class RangeNumericFilter(RangeNumericMixin, admin.FieldListFilter):
111
111
  field_path: str,
112
112
  ) -> None:
113
113
  super().__init__(field, request, params, model, model_admin, field_path)
114
- if not isinstance(field, (DecimalField, IntegerField, FloatField, AutoField)):
114
+ if not isinstance(field, DecimalField | IntegerField | FloatField | AutoField):
115
115
  raise TypeError(
116
116
  f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
117
117
  )
@@ -156,7 +156,7 @@ class SliderNumericFilter(RangeNumericFilter):
156
156
  else:
157
157
  max_value = None
158
158
 
159
- if isinstance(self.field, (FloatField, DecimalField)):
159
+ if isinstance(self.field, FloatField | DecimalField):
160
160
  decimals = self.MAX_DECIMALS
161
161
  step = self.STEP if self.STEP else self._get_min_step(self.MAX_DECIMALS)
162
162
  else:
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional, Union
1
+ from typing import Any
2
2
 
3
3
  from django.core.validators import EMPTY_VALUES
4
4
  from django.forms import MultiWidget, Widget
@@ -35,7 +35,7 @@ class ArrayWidget(MultiWidget):
35
35
 
36
36
  def __init__(
37
37
  self,
38
- widget_class: Optional[type[Widget]] = None,
38
+ widget_class: type[Widget] | None = None,
39
39
  *args: Any,
40
40
  **kwargs: Any,
41
41
  ) -> None:
@@ -81,7 +81,7 @@ class ArrayWidget(MultiWidget):
81
81
  ) -> list:
82
82
  return data.getlist(name) not in [[""], *EMPTY_VALUES]
83
83
 
84
- def decompress(self, value: Union[str, list]) -> list:
84
+ def decompress(self, value: str | list) -> list:
85
85
  if isinstance(value, list):
86
86
  return value
87
87
  elif isinstance(value, str):
@@ -89,7 +89,7 @@ class ArrayWidget(MultiWidget):
89
89
 
90
90
  return []
91
91
 
92
- def _resolve_widgets(self, value: Optional[Union[list, str]]) -> None:
92
+ def _resolve_widgets(self, value: list | str | None) -> None:
93
93
  if value is None:
94
94
  value = []
95
95
 
@@ -112,7 +112,7 @@ class WysiwygWidget(Widget):
112
112
  "unfold/forms/js/trix.config.js",
113
113
  )
114
114
 
115
- def __init__(self, attrs: Optional[dict[str, Any]] = None) -> None:
115
+ def __init__(self, attrs: dict[str, Any] | None = None) -> None:
116
116
  super().__init__(attrs)
117
117
 
118
118
  self.attrs.update(
@@ -1,5 +1,5 @@
1
1
  from functools import partial
2
- from typing import Any, Optional
2
+ from typing import Any
3
3
 
4
4
  from django import forms
5
5
  from django.contrib.admin.utils import NestedObjects, flatten_fieldsets
@@ -28,7 +28,7 @@ class NonrelatedInlineMixin:
28
28
  formset = NonrelatedInlineModelFormSet
29
29
 
30
30
  def get_formset(
31
- self, request: HttpRequest, obj: Optional[Model] = None, **kwargs: Any
31
+ self, request: HttpRequest, obj: Model | None = None, **kwargs: Any
32
32
  ):
33
33
  defaults = self._get_formset_defaults(request, obj, **kwargs)
34
34
 
@@ -41,7 +41,7 @@ class NonrelatedInlineMixin:
41
41
  )
42
42
 
43
43
  def _get_formset_defaults(
44
- self, request: HttpRequest, obj: Optional[Model] = None, **kwargs: Any
44
+ self, request: HttpRequest, obj: Model | None = None, **kwargs: Any
45
45
  ):
46
46
  """Return a BaseInlineFormSet class for use in admin add/change views."""
47
47
  if "fields" in kwargs:
@@ -1,4 +1,5 @@
1
- from typing import Any, Callable, Optional
1
+ from collections.abc import Callable
2
+ from typing import Any
2
3
 
3
4
  from django.db.models import Model, QuerySet
4
5
  from django.forms import BaseModelFormSet, ModelForm, modelformset_factory
@@ -9,7 +10,7 @@ from unfold.forms import PaginationFormSetMixin
9
10
  class NonrelatedInlineModelFormSet(PaginationFormSetMixin, BaseModelFormSet):
10
11
  def __init__(
11
12
  self,
12
- instance: Optional[Model] = None,
13
+ instance: Model | None = None,
13
14
  save_as_new: bool = False,
14
15
  **kwargs: Any,
15
16
  ) -> None:
@@ -33,9 +34,9 @@ class NonrelatedInlineModelFormSet(PaginationFormSetMixin, BaseModelFormSet):
33
34
 
34
35
  def nonrelated_inline_formset_factory(
35
36
  model: Model,
36
- queryset: Optional[QuerySet] = None,
37
+ queryset: QuerySet | None = None,
37
38
  formset: BaseModelFormSet = NonrelatedInlineModelFormSet,
38
- save_new_instance: Optional[Callable] = None,
39
+ save_new_instance: Callable | None = None,
39
40
  **kwargs: Any,
40
41
  ) -> BaseModelFormSet:
41
42
  inline_formset = modelformset_factory(model, formset=formset, **kwargs)
unfold/dataclasses.py CHANGED
@@ -1,5 +1,5 @@
1
+ from collections.abc import Callable
1
2
  from dataclasses import dataclass
2
- from typing import Callable, Optional, Union
3
3
 
4
4
  from unfold.enums import ActionVariant
5
5
 
@@ -12,10 +12,10 @@ class UnfoldAction:
12
12
  method: ActionFunction
13
13
  description: str
14
14
  path: str
15
- attrs: Optional[dict] = None
16
- object_id: Optional[Union[int, str]] = None
17
- icon: Optional[str] = None
18
- variant: Optional[ActionVariant] = ActionVariant.DEFAULT
15
+ attrs: dict | None = None
16
+ object_id: int | str | None = None
17
+ icon: str | None = None
18
+ variant: ActionVariant | None = ActionVariant.DEFAULT
19
19
 
20
20
 
21
21
  @dataclass
@@ -23,20 +23,20 @@ class SearchResult:
23
23
  title: str
24
24
  description: str
25
25
  link: str
26
- icon: Optional[str]
26
+ icon: str | None
27
27
 
28
28
 
29
29
  @dataclass
30
30
  class Favicon:
31
- href: Union[str, Callable]
32
- rel: Optional[str] = None
33
- type: Optional[str] = None
34
- sizes: Optional[str] = None
31
+ href: str | Callable
32
+ rel: str | None = None
33
+ type: str | None = None
34
+ sizes: str | None = None
35
35
 
36
36
 
37
37
  @dataclass
38
38
  class DropdownItem:
39
39
  title: str
40
- link: Union[str, Callable]
41
- icon: Optional[str] = None
42
- attrs: Optional[dict] = None
40
+ link: str | Callable
41
+ icon: str | None = None
42
+ attrs: dict | None = None
unfold/datasets.py ADDED
@@ -0,0 +1,90 @@
1
+ from typing import Any
2
+
3
+ from django.contrib import admin
4
+ from django.http import HttpRequest
5
+ from django.template.loader import render_to_string
6
+
7
+ from unfold.views import DatasetChangeList
8
+
9
+
10
+ class BaseDataset:
11
+ tab = False
12
+
13
+ def __init__(
14
+ self, request: HttpRequest, extra_context: dict[str, Any] | None
15
+ ) -> None:
16
+ self.request = request
17
+ self.extra_context = extra_context
18
+
19
+ self.model_admin_instance = self.model_admin(
20
+ model=self.model, admin_site=admin.site
21
+ )
22
+ self.model_admin_instance.extra_context = self.extra_context
23
+
24
+ @property
25
+ def contents(self) -> str:
26
+ if self.model_admin_instance.get_actions(self.request):
27
+ action_form = self.model_admin_instance.action_form(auto_id=None)
28
+ action_form.fields[
29
+ "action"
30
+ ].choices = self.model_admin_instance.get_action_choices(self.request)
31
+ else:
32
+ action_form = None
33
+
34
+ return render_to_string(
35
+ "unfold/helpers/dataset.html",
36
+ request=self.request,
37
+ context={
38
+ "dataset": self,
39
+ "id": self.id,
40
+ "cl": self.cl,
41
+ "tab": self.tab,
42
+ "opts": self.model._meta,
43
+ "action_form": action_form,
44
+ "actions_selection_counter": True,
45
+ },
46
+ )
47
+
48
+ @property
49
+ def cl(self) -> DatasetChangeList:
50
+ list_display = self.model_admin_instance.get_list_display(self.request)
51
+ list_display_links = self.model_admin_instance.get_list_display_links(
52
+ self.request, list_display
53
+ )
54
+ sortable_by = self.model_admin_instance.get_sortable_by(self.request)
55
+ search_fields = self.model_admin_instance.get_search_fields(self.request)
56
+
57
+ if self.model_admin_instance.get_actions(self.request):
58
+ list_display = ["action_checkbox", *list_display]
59
+
60
+ cl = DatasetChangeList(
61
+ request=self.request,
62
+ model=self.model,
63
+ model_admin=self.model_admin_instance,
64
+ list_display=list_display,
65
+ list_display_links=list_display_links,
66
+ list_filter=[],
67
+ date_hierarchy=[],
68
+ search_fields=search_fields,
69
+ list_select_related=[],
70
+ list_per_page=10,
71
+ list_max_show_all=False,
72
+ list_editable=[],
73
+ sortable_by=sortable_by,
74
+ search_help_text=[],
75
+ )
76
+ cl.formset = None
77
+
78
+ return cl
79
+
80
+ @property
81
+ def id(self) -> str:
82
+ return self.__class__.__name__
83
+
84
+ @property
85
+ def model_name(self) -> str:
86
+ return self.model._meta.model_name
87
+
88
+ @property
89
+ def model_verbose_name(self) -> str:
90
+ return self.model._meta.verbose_name_plural
unfold/decorators.py CHANGED
@@ -1,5 +1,5 @@
1
- from collections.abc import Iterable
2
- from typing import Any, Callable, Optional, Union
1
+ from collections.abc import Callable, Iterable
2
+ from typing import Any
3
3
 
4
4
  from django.contrib.admin.options import BaseModelAdmin
5
5
  from django.core.exceptions import PermissionDenied
@@ -12,14 +12,14 @@ from unfold.typing import ActionFunction
12
12
 
13
13
 
14
14
  def action(
15
- function: Optional[Callable] = None,
15
+ function: Callable | None = None,
16
16
  *,
17
- permissions: Optional[Iterable[str]] = None,
18
- description: Optional[str] = None,
19
- url_path: Optional[str] = None,
20
- attrs: Optional[dict[str, Any]] = None,
21
- icon: Optional[str] = None,
22
- variant: Optional[ActionVariant] = ActionVariant.DEFAULT,
17
+ permissions: Iterable[str] | None = None,
18
+ description: str | None = None,
19
+ url_path: str | None = None,
20
+ attrs: dict[str, Any] | None = None,
21
+ icon: str | None = None,
22
+ variant: ActionVariant | None = ActionVariant.DEFAULT,
23
23
  ) -> ActionFunction:
24
24
  def decorator(func: Callable) -> ActionFunction:
25
25
  def inner(
@@ -27,7 +27,7 @@ def action(
27
27
  request: HttpRequest,
28
28
  *args: Any,
29
29
  **kwargs,
30
- ) -> Optional[HttpResponse]:
30
+ ) -> HttpResponse | None:
31
31
  if permissions:
32
32
  permission_rules = []
33
33
 
@@ -96,16 +96,16 @@ def action(
96
96
 
97
97
 
98
98
  def display(
99
- function: Optional[Callable[[Model], Any]] = None,
99
+ function: Callable[[Model], Any] | None = None,
100
100
  *,
101
- boolean: Optional[bool] = None,
102
- image: Optional[bool] = None,
103
- ordering: Optional[Union[str, Combinable, BaseExpression]] = None,
104
- description: Optional[str] = None,
105
- empty_value: Optional[str] = None,
106
- dropdown: Optional[bool] = None,
107
- label: Optional[Union[bool, str, dict[str, str]]] = None,
108
- header: Optional[bool] = None,
101
+ boolean: bool | None = None,
102
+ image: bool | None = None,
103
+ ordering: str | Combinable | BaseExpression | None = None,
104
+ description: str | None = None,
105
+ empty_value: str | None = None,
106
+ dropdown: bool | None = None,
107
+ label: bool | str | dict[str, str] | None = None,
108
+ header: bool | None = None,
109
109
  ) -> Callable:
110
110
  def decorator(func: Callable[[Model], Any]) -> Callable:
111
111
  if boolean is not None and empty_value is not None:
unfold/fields.py CHANGED
@@ -1,5 +1,3 @@
1
- from typing import Union
2
-
3
1
  from django.contrib.admin import helpers
4
2
  from django.contrib.admin.utils import lookup_field, quote
5
3
  from django.core.exceptions import ObjectDoesNotExist
@@ -40,7 +38,7 @@ class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
40
38
  return format_html("<label{}>{}</label>", flatatt(attrs), capfirst(label))
41
39
 
42
40
  @property
43
- def url(self) -> Union[str, bool]:
41
+ def url(self) -> str | bool:
44
42
  field, obj, model_admin = (
45
43
  self.field["field"],
46
44
  self.form.instance,
@@ -112,7 +110,7 @@ class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
112
110
  except (AttributeError, ValueError, ObjectDoesNotExist):
113
111
  return False
114
112
 
115
- return isinstance(f, (ImageField, FileField))
113
+ return isinstance(f, ImageField | FileField)
116
114
 
117
115
  def contents(self) -> str:
118
116
  contents = self._get_contents()
@@ -167,7 +165,7 @@ class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
167
165
  if isinstance(f.remote_field, ManyToManyRel) and value is not None:
168
166
  result_repr = ", ".join(map(str, value.all()))
169
167
  elif (
170
- isinstance(f.remote_field, (ForeignObjectRel, OneToOneField))
168
+ isinstance(f.remote_field, ForeignObjectRel | OneToOneField)
171
169
  and value is not None
172
170
  ):
173
171
  result_repr = self.get_admin_url(f.remote_field, value)
unfold/forms.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from collections.abc import Generator
2
- from typing import Optional, Union
2
+ from typing import Any, Union
3
3
 
4
4
  from django import forms
5
5
  from django.contrib.admin.forms import (
@@ -8,6 +8,7 @@ from django.contrib.admin.forms import (
8
8
  from django.contrib.admin.forms import (
9
9
  AdminPasswordChangeForm as BaseAdminOwnPasswordChangeForm,
10
10
  )
11
+ from django.contrib.admin.views.main import ChangeListSearchForm
11
12
  from django.contrib.auth.forms import (
12
13
  AdminPasswordChangeForm as BaseAdminPasswordChangeForm,
13
14
  )
@@ -52,20 +53,20 @@ class ActionForm(forms.Form):
52
53
  {
53
54
  "class": " ".join(
54
55
  [
55
- "appearance-none",
56
- "!bg-white/20",
57
- "font-medium",
58
- "grow",
59
- "px-3",
60
- "py-2",
61
- "pr-8",
62
- "rounded-default",
63
- "!text-white",
64
- "truncate",
65
- "!outline-primary-400",
66
- "dark:!outline-primary-700",
67
- "*:text-base-700",
68
- "lg:w-72",
56
+ "group-[.changelist-actions]:appearance-none",
57
+ "group-[.changelist-actions]:!bg-white/20",
58
+ "group-[.changelist-actions]:font-medium",
59
+ "group-[.changelist-actions]:grow",
60
+ "group-[.changelist-actions]:px-3",
61
+ "group-[.changelist-actions]:py-2",
62
+ "group-[.changelist-actions]:pr-8",
63
+ "group-[.changelist-actions]:rounded-default",
64
+ "group-[.changelist-actions]:!text-current",
65
+ "group-[.changelist-actions]:truncate",
66
+ "group-[.changelist-actions]:!outline-primary-400",
67
+ "group-[.changelist-actions]:dark:!outline-primary-700",
68
+ "group-[.changelist-actions]:*:text-base-700",
69
+ "group-[.changelist-actions]:lg:w-72",
69
70
  ]
70
71
  ),
71
72
  "aria-label": _("Select action to run"),
@@ -78,19 +79,26 @@ class ActionForm(forms.Form):
78
79
  label="",
79
80
  required=False,
80
81
  initial=0,
81
- widget=forms.HiddenInput({"class": "select-across"}),
82
+ widget=forms.HiddenInput(
83
+ {
84
+ "class": "select-across",
85
+ "x-model": "selectAcross",
86
+ }
87
+ ),
82
88
  )
83
89
 
84
90
 
85
91
  class AuthenticationForm(AdminAuthenticationForm):
86
92
  def __init__(
87
93
  self,
88
- request: Optional[HttpRequest] = None,
94
+ request: HttpRequest | None = None,
89
95
  *args,
90
96
  **kwargs,
91
97
  ) -> None:
92
98
  super().__init__(request, *args, **kwargs)
93
99
 
100
+ self.fields["username"].widget.attrs["autofocus"] = ""
101
+
94
102
  self.fields["username"].widget.attrs["class"] = " ".join(BASE_INPUT_CLASSES)
95
103
  self.fields["password"].widget.attrs["class"] = " ".join(BASE_INPUT_CLASSES)
96
104
 
@@ -192,14 +200,14 @@ class Fieldline(BaseFieldline):
192
200
 
193
201
 
194
202
  class PaginationFormSetMixin:
195
- queryset: Optional[QuerySet] = None
196
- request: Optional[HttpRequest] = None
197
- per_page: Optional[int] = None
203
+ queryset: QuerySet | None = None
204
+ request: HttpRequest | None = None
205
+ per_page: int | None = None
198
206
 
199
207
  def __init__(
200
208
  self,
201
- request: Optional[HttpRequest] = None,
202
- per_page: Optional[int] = None,
209
+ request: HttpRequest | None = None,
210
+ per_page: int | None = None,
203
211
  *args,
204
212
  **kwargs,
205
213
  ):
@@ -240,3 +248,14 @@ class PaginationInlineFormSet(PaginationFormSetMixin, BaseInlineFormSet):
240
248
 
241
249
  class PaginationGenericInlineFormSet(PaginationFormSetMixin, BaseGenericInlineFormSet):
242
250
  pass
251
+
252
+
253
+ class DatasetChangeListSearchForm(ChangeListSearchForm):
254
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
255
+ super().__init__(*args, **kwargs)
256
+
257
+ from django.contrib.admin.views.main import SEARCH_VAR
258
+
259
+ self.fields = {
260
+ SEARCH_VAR: forms.CharField(required=False, strip=False),
261
+ }
unfold/mixins/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from unfold.mixins.action_model_admin import ActionModelAdminMixin
2
2
  from unfold.mixins.base_model_admin import BaseModelAdminMixin
3
+ from unfold.mixins.dataset_model_admin import DatasetModelAdminMixin
3
4
 
4
- __all__ = ["BaseModelAdminMixin", "ActionModelAdminMixin"]
5
+ __all__ = ["BaseModelAdminMixin", "ActionModelAdminMixin", "DatasetModelAdminMixin"]
@@ -1,4 +1,5 @@
1
- from typing import Any, Callable, Optional, Union
1
+ from collections.abc import Callable
2
+ from typing import Any
2
3
 
3
4
  from django.db.models import Model
4
5
  from django.forms import Form
@@ -22,7 +23,7 @@ class ActionModelAdminMixin:
22
23
  actions_submit_line = () # Displayed in changeform in the submit line (form buttons)
23
24
 
24
25
  def changelist_view(
25
- self, request: HttpRequest, extra_context: Optional[dict[str, str]] = None
26
+ self, request: HttpRequest, extra_context: dict[str, str] | None = None
26
27
  ) -> TemplateResponse:
27
28
  """
28
29
  Changelist contains `actions_list` and `actions_row` custom actions. In case of `actions_row` they
@@ -60,9 +61,9 @@ class ActionModelAdminMixin:
60
61
  def changeform_view(
61
62
  self,
62
63
  request: HttpRequest,
63
- object_id: Optional[str] = None,
64
+ object_id: str | None = None,
64
65
  form_url: str = "",
65
- extra_context: Optional[dict[str, bool]] = None,
66
+ extra_context: dict[str, Any] | None = None,
66
67
  ) -> Any:
67
68
  """
68
69
  Changeform contains `actions_submit_line` and `actions_detail` custom actions.
@@ -159,7 +160,7 @@ class ActionModelAdminMixin:
159
160
  request, self._get_base_actions_submit_line(), object_id
160
161
  )
161
162
 
162
- def _extract_action_names(self, actions: list[Union[str, dict]]) -> list[str]:
163
+ def _extract_action_names(self, actions: list[str | dict]) -> list[str]:
163
164
  """
164
165
  Gets the list of only actions names from the actions structure provided in ModelAdmin
165
166
  """
@@ -228,10 +229,10 @@ class ActionModelAdminMixin:
228
229
 
229
230
  def _get_actions_navigation(
230
231
  self,
231
- provided_actions: list[Union[str, dict]],
232
+ provided_actions: list[str | dict],
232
233
  allowed_actions: list[UnfoldAction],
233
- object_id: Optional[str] = None,
234
- ) -> list[Union[str, dict]]:
234
+ object_id: str | None = None,
235
+ ) -> list[str | dict]:
235
236
  """
236
237
  Builds navigation structure for the actions which is going to be provided to the template.
237
238
  """
@@ -272,7 +273,7 @@ class ActionModelAdminMixin:
272
273
  "path": get_action_path(action),
273
274
  }
274
275
 
275
- def build_dropdown(nav_item: dict) -> Optional[dict]:
276
+ def build_dropdown(nav_item: dict) -> dict | None:
276
277
  """
277
278
  Builds a dropdown structure for the action.
278
279
  """
@@ -304,7 +305,7 @@ class ActionModelAdminMixin:
304
305
  self,
305
306
  request: HttpRequest,
306
307
  actions: list[UnfoldAction],
307
- object_id: Optional[Union[int, str]] = None,
308
+ object_id: int | str | None = None,
308
309
  ) -> list[UnfoldAction]:
309
310
  """
310
311
  Filters out actions that the user doesn't have access to.
@@ -1,5 +1,5 @@
1
1
  import copy
2
- from typing import Any, Optional
2
+ from typing import Any
3
3
 
4
4
  from django.contrib.admin import helpers
5
5
  from django.contrib.admin.sites import AdminSite
@@ -31,9 +31,9 @@ class BaseModelAdminMixin:
31
31
  def changeform_view(
32
32
  self,
33
33
  request: HttpRequest,
34
- object_id: Optional[str] = None,
34
+ object_id: str | None = None,
35
35
  form_url: str = "",
36
- extra_context: Optional[dict[str, Any]] = None,
36
+ extra_context: dict[str, Any] | None = None,
37
37
  ) -> Any:
38
38
  from unfold.forms import AdminForm, Fieldline
39
39
 
@@ -61,7 +61,7 @@ class BaseModelAdminMixin:
61
61
 
62
62
  def formfield_for_foreignkey(
63
63
  self, db_field: ForeignKey, request: HttpRequest, **kwargs
64
- ) -> Optional[ModelChoiceField]:
64
+ ) -> ModelChoiceField | None:
65
65
  db = kwargs.get("using")
66
66
 
67
67
  # Overrides widgets for all related fields
@@ -102,7 +102,7 @@ class BaseModelAdminMixin:
102
102
 
103
103
  def formfield_for_nullboolean_field(
104
104
  self, db_field: Field, request: HttpRequest, **kwargs
105
- ) -> Optional[Field]:
105
+ ) -> Field | None:
106
106
  if "widget" not in kwargs:
107
107
  if db_field.choices:
108
108
  kwargs["widget"] = widgets.UnfoldAdminSelectWidget(
@@ -115,7 +115,7 @@ class BaseModelAdminMixin:
115
115
 
116
116
  def formfield_for_dbfield(
117
117
  self, db_field: Field, request: HttpRequest, **kwargs
118
- ) -> Optional[Field]:
118
+ ) -> Field | None:
119
119
  if isinstance(db_field, models.BooleanField) and db_field.null is True:
120
120
  return self.formfield_for_nullboolean_field(db_field, request, **kwargs)
121
121