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.
- {django_unfold-0.66.0.dist-info → django_unfold-0.68.0.dist-info}/METADATA +8 -5
- {django_unfold-0.66.0.dist-info → django_unfold-0.68.0.dist-info}/RECORD +46 -42
- unfold/admin.py +73 -13
- unfold/components.py +2 -2
- unfold/contrib/filters/admin/choice_filters.py +13 -1
- unfold/contrib/filters/admin/mixins.py +3 -3
- unfold/contrib/filters/admin/numeric_filters.py +8 -6
- unfold/contrib/filters/forms.py +25 -4
- unfold/contrib/filters/static/unfold/filters/js/admin-numeric-filter.js +62 -28
- unfold/contrib/filters/templates/unfold/filters/filters_numeric_slider.html +2 -11
- unfold/contrib/forms/widgets.py +5 -5
- unfold/contrib/inlines/admin.py +3 -3
- unfold/contrib/inlines/forms.py +5 -4
- unfold/dataclasses.py +13 -13
- unfold/datasets.py +69 -0
- unfold/decorators.py +19 -19
- unfold/fields.py +40 -1
- unfold/forms.py +19 -7
- unfold/mixins/action_model_admin.py +11 -10
- unfold/mixins/base_model_admin.py +6 -6
- unfold/sites.py +14 -17
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/js/app.js +65 -5
- unfold/static/unfold/js/select2.init.js +2 -9
- unfold/styles.css +22 -21
- unfold/templates/admin/change_form.html +5 -1
- unfold/templates/admin/change_list_results.html +10 -62
- unfold/templates/admin/edit_inline/stacked.html +1 -1
- unfold/templates/admin/search_form.html +5 -3
- unfold/templates/unfold/components/card.html +12 -3
- unfold/templates/unfold/components/progress.html +9 -3
- unfold/templates/unfold/helpers/change_list_headers.html +65 -0
- unfold/templates/unfold/helpers/dataset.html +19 -0
- unfold/templates/unfold/helpers/edit_inline/tabular_field.html +1 -1
- unfold/templates/unfold/helpers/empty_results.html +6 -4
- unfold/templates/unfold/helpers/field_readonly_value.html +1 -1
- unfold/templates/unfold/helpers/field_readonly_value_file.html +18 -0
- unfold/templates/unfold/helpers/tab_items.html +6 -0
- unfold/templatetags/unfold.py +18 -13
- unfold/templatetags/unfold_list.py +64 -8
- unfold/typing.py +5 -6
- unfold/utils.py +9 -9
- unfold/views.py +15 -1
- unfold/widgets.py +30 -29
- {django_unfold-0.66.0.dist-info → django_unfold-0.68.0.dist-info}/WHEEL +0 -0
- {django_unfold-0.66.0.dist-info → django_unfold-0.68.0.dist-info}/licenses/LICENSE.md +0 -0
unfold/contrib/filters/forms.py
CHANGED
@@ -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(
|
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__(
|
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={
|
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={
|
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(
|
2
|
-
|
3
|
-
|
4
|
-
|
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
|
-
|
8
|
-
|
9
|
+
const fromInput = slider
|
10
|
+
.closest(".admin-numeric-filter-wrapper")
|
11
|
+
.querySelectorAll(".admin-numeric-filter-wrapper-group input")[0];
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
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
|
-
|
17
|
-
|
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">
|
unfold/contrib/forms/widgets.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Any
|
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:
|
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:
|
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:
|
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:
|
115
|
+
def __init__(self, attrs: dict[str, Any] | None = None) -> None:
|
116
116
|
super().__init__(attrs)
|
117
117
|
|
118
118
|
self.attrs.update(
|
unfold/contrib/inlines/admin.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from functools import partial
|
2
|
-
from typing import Any
|
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:
|
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:
|
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:
|
unfold/contrib/inlines/forms.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
from
|
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:
|
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:
|
37
|
+
queryset: QuerySet | None = None,
|
37
38
|
formset: BaseModelFormSet = NonrelatedInlineModelFormSet,
|
38
|
-
save_new_instance:
|
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:
|
16
|
-
object_id:
|
17
|
-
icon:
|
18
|
-
variant:
|
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:
|
26
|
+
icon: str | None
|
27
27
|
|
28
28
|
|
29
29
|
@dataclass
|
30
30
|
class Favicon:
|
31
|
-
href:
|
32
|
-
rel:
|
33
|
-
type:
|
34
|
-
sizes:
|
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:
|
41
|
-
icon:
|
42
|
-
attrs:
|
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
|
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:
|
15
|
+
function: Callable | None = None,
|
16
16
|
*,
|
17
|
-
permissions:
|
18
|
-
description:
|
19
|
-
url_path:
|
20
|
-
attrs:
|
21
|
-
icon:
|
22
|
-
variant:
|
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
|
-
) ->
|
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:
|
99
|
+
function: Callable[[Model], Any] | None = None,
|
100
100
|
*,
|
101
|
-
boolean:
|
102
|
-
image:
|
103
|
-
ordering:
|
104
|
-
description:
|
105
|
-
empty_value:
|
106
|
-
dropdown:
|
107
|
-
label:
|
108
|
-
header:
|
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,
|
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
|
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:
|
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:
|
196
|
-
request:
|
197
|
-
per_page:
|
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:
|
202
|
-
per_page:
|
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
|
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:
|
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:
|
64
|
+
object_id: str | None = None,
|
64
65
|
form_url: str = "",
|
65
|
-
extra_context:
|
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[
|
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[
|
232
|
+
provided_actions: list[str | dict],
|
232
233
|
allowed_actions: list[UnfoldAction],
|
233
|
-
object_id:
|
234
|
-
) -> list[
|
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) ->
|
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:
|
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.
|