django-unfold 0.66.0__py3-none-any.whl → 0.68.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 (46) hide show
  1. {django_unfold-0.66.0.dist-info → django_unfold-0.68.0.dist-info}/METADATA +8 -5
  2. {django_unfold-0.66.0.dist-info → django_unfold-0.68.0.dist-info}/RECORD +46 -42
  3. unfold/admin.py +73 -13
  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 +8 -6
  8. unfold/contrib/filters/forms.py +25 -4
  9. unfold/contrib/filters/static/unfold/filters/js/admin-numeric-filter.js +62 -28
  10. unfold/contrib/filters/templates/unfold/filters/filters_numeric_slider.html +2 -11
  11. unfold/contrib/forms/widgets.py +5 -5
  12. unfold/contrib/inlines/admin.py +3 -3
  13. unfold/contrib/inlines/forms.py +5 -4
  14. unfold/dataclasses.py +13 -13
  15. unfold/datasets.py +69 -0
  16. unfold/decorators.py +19 -19
  17. unfold/fields.py +40 -1
  18. unfold/forms.py +19 -7
  19. unfold/mixins/action_model_admin.py +11 -10
  20. unfold/mixins/base_model_admin.py +6 -6
  21. unfold/sites.py +14 -17
  22. unfold/static/unfold/css/styles.css +1 -1
  23. unfold/static/unfold/js/app.js +65 -5
  24. unfold/static/unfold/js/select2.init.js +2 -9
  25. unfold/styles.css +22 -21
  26. unfold/templates/admin/change_form.html +5 -1
  27. unfold/templates/admin/change_list_results.html +10 -62
  28. unfold/templates/admin/edit_inline/stacked.html +1 -1
  29. unfold/templates/admin/search_form.html +5 -3
  30. unfold/templates/unfold/components/card.html +12 -3
  31. unfold/templates/unfold/components/progress.html +9 -3
  32. unfold/templates/unfold/helpers/change_list_headers.html +65 -0
  33. unfold/templates/unfold/helpers/dataset.html +19 -0
  34. unfold/templates/unfold/helpers/edit_inline/tabular_field.html +1 -1
  35. unfold/templates/unfold/helpers/empty_results.html +6 -4
  36. unfold/templates/unfold/helpers/field_readonly_value.html +1 -1
  37. unfold/templates/unfold/helpers/field_readonly_value_file.html +18 -0
  38. unfold/templates/unfold/helpers/tab_items.html +6 -0
  39. unfold/templatetags/unfold.py +18 -13
  40. unfold/templatetags/unfold_list.py +64 -8
  41. unfold/typing.py +5 -6
  42. unfold/utils.py +9 -9
  43. unfold/views.py +15 -1
  44. unfold/widgets.py +30 -29
  45. {django_unfold-0.66.0.dist-info → django_unfold-0.68.0.dist-info}/WHEEL +0 -0
  46. {django_unfold-0.66.0.dist-info → django_unfold-0.68.0.dist-info}/licenses/LICENSE.md +0 -0
@@ -59,7 +59,11 @@ class AutocompleteDropdownForm(forms.Form):
59
59
  label=label,
60
60
  required=False,
61
61
  queryset=field.remote_field.model.objects,
62
- widget=self.widget(field, model_admin.admin_site),
62
+ widget=self.widget(
63
+ field,
64
+ model_admin.admin_site,
65
+ attrs={"class": "unfold-filter-autocomplete"},
66
+ ),
63
67
  )
64
68
 
65
69
  class Media:
@@ -175,22 +179,39 @@ class SingleNumericForm(forms.Form):
175
179
 
176
180
 
177
181
  class RangeNumericForm(forms.Form):
178
- def __init__(self, name: str, *args, **kwargs) -> None:
182
+ def __init__(
183
+ self, name: str, min: float = None, max: float = None, *args, **kwargs
184
+ ) -> None:
179
185
  self.name = name
180
186
  super().__init__(*args, **kwargs)
181
187
 
188
+ min_max = {}
189
+
190
+ if min:
191
+ min_max["min"] = min
192
+ if max:
193
+ min_max["max"] = max
194
+
182
195
  self.fields[self.name + "_from"] = forms.FloatField(
183
196
  label="",
184
197
  required=False,
185
198
  widget=forms.NumberInput(
186
- attrs={"placeholder": _("From"), "class": " ".join(INPUT_CLASSES)}
199
+ attrs={
200
+ "placeholder": _("From"),
201
+ "class": " ".join(INPUT_CLASSES),
202
+ **min_max,
203
+ }
187
204
  ),
188
205
  )
189
206
  self.fields[self.name + "_to"] = forms.FloatField(
190
207
  label="",
191
208
  required=False,
192
209
  widget=forms.NumberInput(
193
- attrs={"placeholder": _("To"), "class": " ".join(INPUT_CLASSES)}
210
+ attrs={
211
+ "placeholder": _("To"),
212
+ "class": " ".join(INPUT_CLASSES),
213
+ **min_max,
214
+ }
194
215
  ),
195
216
  )
196
217
 
@@ -1,35 +1,69 @@
1
- document.addEventListener('DOMContentLoaded', function() {
2
- Array.from(document.getElementsByClassName('admin-numeric-filter-slider')).forEach(function(slider) {
3
- if (Array.from(slider.classList).includes("noUi-target")) {
4
- return;
5
- }
1
+ document.addEventListener("DOMContentLoaded", function () {
2
+ Array.from(
3
+ document.getElementsByClassName("admin-numeric-filter-slider")
4
+ ).forEach(function (slider) {
5
+ if (Array.from(slider.classList).includes("noUi-target")) {
6
+ return;
7
+ }
6
8
 
7
- var from = parseFloat(slider.closest('.admin-numeric-filter-wrapper').querySelectorAll('.admin-numeric-filter-wrapper-group input')[0].value);
8
- var to = parseFloat(slider.closest('.admin-numeric-filter-wrapper').querySelectorAll('.admin-numeric-filter-wrapper-group input')[1].value);
9
+ const fromInput = slider
10
+ .closest(".admin-numeric-filter-wrapper")
11
+ .querySelectorAll(".admin-numeric-filter-wrapper-group input")[0];
9
12
 
10
- noUiSlider.create(slider, {
11
- start: [from, to],
12
- step: parseFloat(slider.getAttribute('data-step')),
13
- connect: true,
14
- format: wNumb({
15
- decimals: parseFloat(slider.getAttribute('data-decimals'))
16
- }),
17
- range: {
18
- 'min': parseFloat(slider.getAttribute('data-min')),
19
- 'max': parseFloat(slider.getAttribute('data-max'))
20
- }
21
- });
13
+ const toInput = slider
14
+ .closest(".admin-numeric-filter-wrapper")
15
+ .querySelectorAll(".admin-numeric-filter-wrapper-group input")[1];
22
16
 
23
- slider.noUiSlider.on('update', function(values, handle) {
24
- var parent = this.target.closest('.admin-numeric-filter-wrapper');
25
- var from = parent.querySelectorAll('.admin-numeric-filter-wrapper-group input')[0];
26
- var to = parent.querySelectorAll('.admin-numeric-filter-wrapper-group input')[1];
17
+ noUiSlider.create(slider, {
18
+ start: [parseFloat(fromInput.value), parseFloat(toInput.value)],
19
+ step: parseFloat(slider.getAttribute("data-step")),
20
+ connect: true,
21
+ format: wNumb({
22
+ decimals: parseFloat(slider.getAttribute("data-decimals")),
23
+ }),
24
+ range: {
25
+ min: parseFloat(slider.getAttribute("data-min")),
26
+ max: parseFloat(slider.getAttribute("data-max")),
27
+ },
28
+ });
29
+
30
+ /*************************************************************
31
+ * Update slider when input values change
32
+ *************************************************************/
33
+ fromInput.addEventListener("keyup", function () {
34
+ clearTimeout(this._sliderUpdateTimeout);
35
+ this._sliderUpdateTimeout = setTimeout(() => {
36
+ slider.noUiSlider.set([
37
+ parseFloat(this.value),
38
+ parseFloat(toInput.value),
39
+ ]);
40
+ }, 500);
41
+ });
42
+
43
+ toInput.addEventListener("keyup", function () {
44
+ clearTimeout(this._sliderUpdateTimeout);
45
+ this._sliderUpdateTimeout = setTimeout(() => {
46
+ slider.noUiSlider.set([
47
+ parseFloat(fromInput.value),
48
+ parseFloat(this.value),
49
+ ]);
50
+ }, 500);
51
+ });
27
52
 
28
- parent.querySelectorAll('.admin-numeric-filter-slider-tooltip-from')[0].innerHTML = values[0];
29
- parent.querySelectorAll('.admin-numeric-filter-slider-tooltip-to')[0].innerHTML = values[1];
53
+ /*************************************************************
54
+ * Updated inputs when slider is moved
55
+ *************************************************************/
56
+ slider.noUiSlider.on("update", function (values, handle) {
57
+ const parent = this.target.closest(".admin-numeric-filter-wrapper");
58
+ const from = parent.querySelectorAll(
59
+ ".admin-numeric-filter-wrapper-group input"
60
+ )[0];
61
+ const to = parent.querySelectorAll(
62
+ ".admin-numeric-filter-wrapper-group input"
63
+ )[1];
30
64
 
31
- from.value = values[0];
32
- to.value = values[1];
33
- });
65
+ from.value = values[0];
66
+ to.value = values[1];
34
67
  });
68
+ });
35
69
  });
@@ -8,22 +8,13 @@
8
8
  </h3>
9
9
 
10
10
  {% if choice.min is not None and choice.max is not None and choice.step %}
11
- <div class="admin-numeric-filter-slider-tooltips">
12
- <span class="admin-numeric-filter-slider-tooltip-from border border-base-200 cursor-not-allowed flex grow flex-row items-center mr-auto rounded-default shadow-xs px-3 py-2 w-full dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark">
13
- {{ choice.value_from }}
14
- </span>
15
11
 
16
- <span class="admin-numeric-filter-slider-tooltip-to border border-base-200 cursor-not-allowed flex grow flex-row items-center rounded-default shadow-xs px-3 py-2 w-full dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark">
17
- {{ choice.value_to }}
18
- </span>
12
+ <div class="admin-numeric-filter-wrapper-group flex flex-row gap-4 mb-4">
13
+ {{ choice.form.as_p }}
19
14
  </div>
20
15
 
21
16
  <div class="admin-numeric-filter-slider" data-min="{{ choice.min|unlocalize }}" data-max="{{ choice.max|unlocalize }}" data-decimals="{{ choice.decimals }}" data-step="{{ choice.step|unlocalize }}">
22
17
  </div>
23
-
24
- <div class="admin-numeric-filter-wrapper-group hidden">
25
- {{ choice.form.as_p }}
26
- </div>
27
18
  {% else %}
28
19
  <div class="admin-numeric-filter-slider-error dark:text-font-default-dark">
29
20
  <p class="border border-base-300 border-dashed leading-[36px] h-[38px] rounded-default text-center dark:border-base-700">
@@ -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,69 @@
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
+ return render_to_string(
27
+ "unfold/helpers/dataset.html",
28
+ request=self.request,
29
+ context={
30
+ "dataset": self,
31
+ "cl": self.cl(),
32
+ "opts": self.model._meta,
33
+ },
34
+ )
35
+
36
+ def cl(self) -> DatasetChangeList:
37
+ list_display = self.model_admin_instance.get_list_display(self.request)
38
+ list_display_links = self.model_admin_instance.get_list_display_links(
39
+ self.request, list_display
40
+ )
41
+ sortable_by = self.model_admin_instance.get_sortable_by(self.request)
42
+ search_fields = self.model_admin_instance.get_search_fields(self.request)
43
+ cl = DatasetChangeList(
44
+ request=self.request,
45
+ model=self.model,
46
+ model_admin=self.model_admin_instance,
47
+ list_display=list_display,
48
+ list_display_links=list_display_links,
49
+ list_filter=[],
50
+ date_hierarchy=[],
51
+ search_fields=search_fields,
52
+ list_select_related=[],
53
+ list_per_page=10,
54
+ list_max_show_all=False,
55
+ list_editable=[],
56
+ sortable_by=sortable_by,
57
+ search_help_text=[],
58
+ )
59
+ cl.formset = None
60
+
61
+ return cl
62
+
63
+ @property
64
+ def model_name(self) -> str:
65
+ return self.model._meta.model_name
66
+
67
+ @property
68
+ def model_verbose_name(self) -> str:
69
+ 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
@@ -3,6 +3,7 @@ from django.contrib.admin.utils import lookup_field, quote
3
3
  from django.core.exceptions import ObjectDoesNotExist
4
4
  from django.db import models
5
5
  from django.db.models import (
6
+ FileField,
6
7
  ForeignObjectRel,
7
8
  ImageField,
8
9
  JSONField,
@@ -36,6 +37,30 @@ class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
36
37
 
37
38
  return format_html("<label{}>{}</label>", flatatt(attrs), capfirst(label))
38
39
 
40
+ @property
41
+ def url(self) -> str | bool:
42
+ field, obj, model_admin = (
43
+ self.field["field"],
44
+ self.form.instance,
45
+ self.model_admin,
46
+ )
47
+
48
+ try:
49
+ f, attr, value = lookup_field(field, obj, model_admin)
50
+ except (AttributeError, ValueError, ObjectDoesNotExist):
51
+ return False
52
+
53
+ if not self.is_file():
54
+ return False
55
+
56
+ if hasattr(obj, field):
57
+ field_value = getattr(obj, field)
58
+
59
+ if field_value and hasattr(field_value, "url"):
60
+ return field_value.url
61
+
62
+ return False
63
+
39
64
  def is_json(self) -> bool:
40
65
  field, obj, model_admin = (
41
66
  self.field["field"],
@@ -73,6 +98,20 @@ class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
73
98
 
74
99
  return isinstance(f, ImageField)
75
100
 
101
+ def is_file(self) -> bool:
102
+ field, obj, model_admin = (
103
+ self.field["field"],
104
+ self.form.instance,
105
+ self.model_admin,
106
+ )
107
+
108
+ try:
109
+ f, attr, value = lookup_field(field, obj, model_admin)
110
+ except (AttributeError, ValueError, ObjectDoesNotExist):
111
+ return False
112
+
113
+ return isinstance(f, ImageField | FileField)
114
+
76
115
  def contents(self) -> str:
77
116
  contents = self._get_contents()
78
117
  contents = self._preprocess_field(contents)
@@ -126,7 +165,7 @@ class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
126
165
  if isinstance(f.remote_field, ManyToManyRel) and value is not None:
127
166
  result_repr = ", ".join(map(str, value.all()))
128
167
  elif (
129
- isinstance(f.remote_field, (ForeignObjectRel, OneToOneField))
168
+ isinstance(f.remote_field, ForeignObjectRel | OneToOneField)
130
169
  and value is not None
131
170
  ):
132
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
  )
@@ -85,7 +86,7 @@ class ActionForm(forms.Form):
85
86
  class AuthenticationForm(AdminAuthenticationForm):
86
87
  def __init__(
87
88
  self,
88
- request: Optional[HttpRequest] = None,
89
+ request: HttpRequest | None = None,
89
90
  *args,
90
91
  **kwargs,
91
92
  ) -> None:
@@ -192,14 +193,14 @@ class Fieldline(BaseFieldline):
192
193
 
193
194
 
194
195
  class PaginationFormSetMixin:
195
- queryset: Optional[QuerySet] = None
196
- request: Optional[HttpRequest] = None
197
- per_page: Optional[int] = None
196
+ queryset: QuerySet | None = None
197
+ request: HttpRequest | None = None
198
+ per_page: int | None = None
198
199
 
199
200
  def __init__(
200
201
  self,
201
- request: Optional[HttpRequest] = None,
202
- per_page: Optional[int] = None,
202
+ request: HttpRequest | None = None,
203
+ per_page: int | None = None,
203
204
  *args,
204
205
  **kwargs,
205
206
  ):
@@ -240,3 +241,14 @@ class PaginationInlineFormSet(PaginationFormSetMixin, BaseInlineFormSet):
240
241
 
241
242
  class PaginationGenericInlineFormSet(PaginationFormSetMixin, BaseGenericInlineFormSet):
242
243
  pass
244
+
245
+
246
+ class DatasetChangeListSearchForm(ChangeListSearchForm):
247
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
248
+ super().__init__(*args, **kwargs)
249
+
250
+ from django.contrib.admin.views.main import SEARCH_VAR
251
+
252
+ self.fields = {
253
+ SEARCH_VAR: forms.CharField(required=False, strip=False),
254
+ }
@@ -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.