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.
- {django_unfold-0.67.0.dist-info → django_unfold-0.69.0.dist-info}/METADATA +33 -41
- {django_unfold-0.67.0.dist-info → django_unfold-0.69.0.dist-info}/RECORD +54 -47
- unfold/admin.py +46 -15
- 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 +6 -6
- 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 +90 -0
- unfold/decorators.py +19 -19
- unfold/fields.py +3 -5
- unfold/forms.py +41 -22
- unfold/mixins/__init__.py +2 -1
- unfold/mixins/action_model_admin.py +11 -10
- unfold/mixins/base_model_admin.py +6 -6
- unfold/mixins/dataset_model_admin.py +62 -0
- unfold/settings.py +1 -0
- unfold/sites.py +19 -18
- unfold/static/admin/js/actions.js +246 -0
- unfold/static/unfold/css/styles.css +2 -2
- unfold/static/unfold/fonts/material-symbols/Material-Symbols-Outlined.woff2 +0 -0
- unfold/static/unfold/js/app.js +3 -1
- unfold/styles.css +21 -16
- unfold/templates/admin/actions.html +2 -2
- unfold/templates/admin/change_form.html +10 -2
- unfold/templates/admin/change_list.html +1 -1
- unfold/templates/admin/change_list_results.html +10 -62
- unfold/templates/admin/dataset_actions.html +50 -0
- unfold/templates/admin/edit_inline/stacked.html +2 -8
- unfold/templates/admin/edit_inline/tabular.html +1 -7
- unfold/templates/admin/includes/fieldset.html +1 -3
- unfold/templates/admin/search_form.html +6 -4
- unfold/templates/registration/password_change_done.html +3 -4
- unfold/templates/registration/password_change_form.html +10 -6
- unfold/templates/unfold/helpers/change_list_actions.html +1 -1
- unfold/templates/unfold/helpers/change_list_headers.html +65 -0
- unfold/templates/unfold/helpers/dataset.html +31 -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_file.html +1 -1
- unfold/templates/unfold/helpers/fieldsets_tabs.html +9 -11
- unfold/templates/unfold/helpers/inline_heading.html +11 -0
- unfold/templates/unfold/helpers/tab_items.html +9 -1
- unfold/templatetags/unfold.py +64 -82
- unfold/templatetags/unfold_list.py +76 -8
- unfold/typing.py +5 -6
- unfold/utils.py +9 -9
- unfold/views.py +15 -1
- unfold/widgets.py +31 -31
- {django_unfold-0.67.0.dist-info → django_unfold-0.69.0.dist-info}/WHEEL +0 -0
- {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
|
|
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,
|
|
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
|
-
) ->
|
|
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[
|
|
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,
|
|
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,
|
|
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:
|
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,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
|
|
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
|
@@ -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) ->
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
"
|
|
57
|
-
"font-medium",
|
|
58
|
-
"grow",
|
|
59
|
-
"px-3",
|
|
60
|
-
"py-2",
|
|
61
|
-
"pr-8",
|
|
62
|
-
"rounded-default",
|
|
63
|
-
"
|
|
64
|
-
"truncate",
|
|
65
|
-
"
|
|
66
|
-
"dark:!outline-primary-700",
|
|
67
|
-
"
|
|
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(
|
|
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:
|
|
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:
|
|
196
|
-
request:
|
|
197
|
-
per_page:
|
|
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:
|
|
202
|
-
per_page:
|
|
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
|
|
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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import copy
|
|
2
|
-
from typing import Any
|
|
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:
|
|
34
|
+
object_id: str | None = None,
|
|
35
35
|
form_url: str = "",
|
|
36
|
-
extra_context:
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
|