django-unfold 0.59.0__py3-none-any.whl → 0.61.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 (58) hide show
  1. {django_unfold-0.59.0.dist-info → django_unfold-0.61.0.dist-info}/METADATA +17 -7
  2. {django_unfold-0.59.0.dist-info → django_unfold-0.61.0.dist-info}/RECORD +58 -47
  3. unfold/admin.py +45 -13
  4. unfold/contrib/import_export/templates/admin/import_export/change_list_import.html +5 -0
  5. unfold/contrib/inlines/admin.py +11 -6
  6. unfold/contrib/inlines/forms.py +3 -1
  7. unfold/contrib/location_field/__init__.py +0 -0
  8. unfold/contrib/location_field/apps.py +6 -0
  9. unfold/contrib/location_field/templates/location_field/map_widget.html +5 -0
  10. unfold/contrib/simple_history/templates/simple_history/submit_line.html +16 -18
  11. unfold/fields.py +17 -19
  12. unfold/forms.py +96 -2
  13. unfold/mixins/base_model_admin.py +21 -2
  14. unfold/sites.py +18 -31
  15. unfold/static/admin/js/admin/RelatedObjectLookups.js +2 -2
  16. unfold/static/unfold/css/styles.css +1 -1
  17. unfold/static/unfold/js/app.js +61 -0
  18. unfold/styles.css +6 -8
  19. unfold/templates/admin/app_list.html +4 -1
  20. unfold/templates/admin/auth/user/add_form.html +1 -5
  21. unfold/templates/admin/change_form.html +3 -1
  22. unfold/templates/admin/change_list.html +4 -2
  23. unfold/templates/admin/change_list_results.html +3 -3
  24. unfold/templates/admin/edit_inline/stacked.html +18 -8
  25. unfold/templates/admin/edit_inline/tabular.html +2 -0
  26. unfold/templates/admin/includes/fieldset.html +9 -3
  27. unfold/templates/admin/login.html +45 -88
  28. unfold/templates/admin/search_form.html +14 -5
  29. unfold/templates/admin/submit_line.html +1 -1
  30. unfold/templates/unfold/components/button.html +10 -1
  31. unfold/templates/unfold/components/card.html +4 -4
  32. unfold/templates/unfold/helpers/add_link.html +3 -1
  33. unfold/templates/unfold/helpers/app_list.html +5 -2
  34. unfold/templates/unfold/helpers/app_list_default.html +2 -1
  35. unfold/templates/unfold/helpers/field.html +5 -3
  36. unfold/templates/unfold/helpers/fieldsets_tabs.html +3 -3
  37. unfold/templates/unfold/helpers/help_text.html +1 -1
  38. unfold/templates/unfold/helpers/messages.html +4 -4
  39. unfold/templates/unfold/helpers/pagination.html +1 -1
  40. unfold/templates/unfold/helpers/pagination_inline.html +28 -0
  41. unfold/templates/unfold/helpers/popup_header.html +27 -0
  42. unfold/templates/unfold/helpers/search.html +16 -12
  43. unfold/templates/unfold/helpers/search_results.html +10 -9
  44. unfold/templates/unfold/helpers/shortcut.html +3 -0
  45. unfold/templates/unfold/helpers/site_branding.html +2 -2
  46. unfold/templates/unfold/helpers/tab_action.html +4 -3
  47. unfold/templates/unfold/helpers/unauthenticated_header.html +15 -0
  48. unfold/templates/unfold/helpers/unauthenticated_title.html +11 -0
  49. unfold/templates/unfold/layouts/unauthenticated.html +37 -0
  50. unfold/templates/unfold/widgets/text.html +28 -0
  51. unfold/templates/unfold_crispy/field.html +12 -10
  52. unfold/templates/unfold_crispy/layout/checkbox.html +20 -4
  53. unfold/templates/unfold_crispy/layout/fieldset.html +3 -3
  54. unfold/templatetags/unfold.py +34 -4
  55. unfold/templatetags/unfold_list.py +4 -1
  56. unfold/widgets.py +42 -1
  57. {django_unfold-0.59.0.dist-info → django_unfold-0.61.0.dist-info}/LICENSE.md +0 -0
  58. {django_unfold-0.59.0.dist-info → django_unfold-0.61.0.dist-info}/WHEEL +0 -0
unfold/forms.py CHANGED
@@ -1,4 +1,5 @@
1
- from typing import Optional
1
+ from collections.abc import Generator
2
+ from typing import Optional, Union
2
3
 
3
4
  from django import forms
4
5
  from django.contrib.admin.forms import (
@@ -11,14 +12,23 @@ from django.contrib.auth.forms import (
11
12
  AdminPasswordChangeForm as BaseAdminPasswordChangeForm,
12
13
  )
13
14
  from django.contrib.auth.models import User
15
+ from django.contrib.contenttypes.forms import BaseGenericInlineFormSet
16
+ from django.core.paginator import Page, Paginator
17
+ from django.db.models import QuerySet
18
+ from django.forms import BaseInlineFormSet
19
+ from django.http import HttpRequest
20
+
21
+ from unfold.fields import UnfoldAdminField, UnfoldAdminReadonlyField
14
22
 
15
23
  try:
16
24
  from django.contrib.auth.forms import AdminUserCreationForm as BaseUserCreationForm
17
25
  except ImportError:
18
26
  from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm
27
+ from django.contrib.admin.helpers import AdminForm as BaseAdminForm
28
+ from django.contrib.admin.helpers import Fieldline as BaseFieldline
29
+ from django.contrib.admin.helpers import Fieldset as BaseFieldset
19
30
  from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
20
31
  from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
21
- from django.http import HttpRequest
22
32
  from django.utils.safestring import mark_safe
23
33
  from django.utils.translation import gettext_lazy as _
24
34
 
@@ -146,3 +156,87 @@ class AdminOwnPasswordChangeForm(BaseAdminOwnPasswordChangeForm):
146
156
  self.fields["old_password"].widget.attrs["class"] = " ".join(INPUT_CLASSES)
147
157
  self.fields["new_password1"].widget.attrs["class"] = " ".join(INPUT_CLASSES)
148
158
  self.fields["new_password2"].widget.attrs["class"] = " ".join(INPUT_CLASSES)
159
+
160
+
161
+ class AdminForm(BaseAdminForm):
162
+ def __iter__(self) -> Generator["Fieldset", None, None]:
163
+ for name, options in self.fieldsets:
164
+ yield Fieldset(
165
+ self.form,
166
+ name,
167
+ readonly_fields=self.readonly_fields,
168
+ model_admin=self.model_admin,
169
+ **options,
170
+ )
171
+
172
+
173
+ class Fieldset(BaseFieldset):
174
+ def __iter__(self) -> Generator["Fieldline", None, None]:
175
+ for field in self.fields:
176
+ yield Fieldline(
177
+ self.form, field, self.readonly_fields, model_admin=self.model_admin
178
+ )
179
+
180
+
181
+ class Fieldline(BaseFieldline):
182
+ def __iter__(
183
+ self,
184
+ ) -> Generator[Union["UnfoldAdminReadonlyField", "UnfoldAdminField"], None, None]:
185
+ for i, field in enumerate(self.fields):
186
+ if field in self.readonly_fields:
187
+ yield UnfoldAdminReadonlyField(
188
+ self.form, field, is_first=(i == 0), model_admin=self.model_admin
189
+ )
190
+ else:
191
+ yield UnfoldAdminField(self.form, field, is_first=(i == 0))
192
+
193
+
194
+ class PaginationFormSetMixin:
195
+ queryset: Optional[QuerySet] = None
196
+ request: Optional[HttpRequest] = None
197
+ per_page: Optional[int] = None
198
+
199
+ def __init__(
200
+ self,
201
+ request: Optional[HttpRequest] = None,
202
+ per_page: Optional[int] = None,
203
+ *args,
204
+ **kwargs,
205
+ ):
206
+ self.request = request
207
+ self.per_page = per_page
208
+
209
+ super().__init__(*args, **kwargs)
210
+
211
+ if self.per_page:
212
+ self.paginator = Paginator(self.queryset, self.per_page)
213
+ self.page = self.get_page(self.paginator, self.get_page_num())
214
+ self._queryset = self.page.object_list
215
+
216
+ def get_pagination_key(self) -> str:
217
+ return f"{self.prefix}-page"
218
+
219
+ def get_page_num(self) -> int:
220
+ page = self.request.GET.get(self.get_pagination_key())
221
+ if page and page.isnumeric() and page > "0":
222
+ return int(page)
223
+
224
+ page = self.request.POST.get(self.get_pagination_key())
225
+ if page and page.isnumeric() and page > "0":
226
+ return int(page)
227
+
228
+ return 1
229
+
230
+ def get_page(self, paginator: Paginator, page: int) -> Page:
231
+ if page <= paginator.num_pages:
232
+ return paginator.page(page)
233
+
234
+ return paginator.page(1)
235
+
236
+
237
+ class PaginationInlineFormSet(PaginationFormSetMixin, BaseInlineFormSet):
238
+ pass
239
+
240
+
241
+ class PaginationGenericInlineFormSet(PaginationFormSetMixin, BaseGenericInlineFormSet):
242
+ pass
@@ -1,6 +1,7 @@
1
1
  import copy
2
- from typing import Optional
2
+ from typing import Any, Optional
3
3
 
4
+ from django.contrib.admin import helpers
4
5
  from django.contrib.admin.sites import AdminSite
5
6
  from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
6
7
  from django.db import models
@@ -27,6 +28,19 @@ class BaseModelAdminMixin:
27
28
 
28
29
  super().__init__(model, admin_site)
29
30
 
31
+ def changeform_view(
32
+ self,
33
+ request: HttpRequest,
34
+ object_id: Optional[str] = None,
35
+ form_url: str = "",
36
+ extra_context: Optional[dict[str, Any]] = None,
37
+ ) -> Any:
38
+ from unfold.forms import AdminForm, Fieldline
39
+
40
+ helpers.AdminForm = AdminForm
41
+ helpers.Fieldline = Fieldline
42
+ return super().changeform_view(request, object_id, form_url, extra_context)
43
+
30
44
  def formfield_for_choice_field(
31
45
  self, db_field: Field, request: HttpRequest, **kwargs
32
46
  ) -> TypedChoiceField:
@@ -90,7 +104,12 @@ class BaseModelAdminMixin:
90
104
  self, db_field: Field, request: HttpRequest, **kwargs
91
105
  ) -> Optional[Field]:
92
106
  if "widget" not in kwargs:
93
- kwargs["widget"] = widgets.UnfoldAdminNullBooleanSelectWidget()
107
+ if db_field.choices:
108
+ kwargs["widget"] = widgets.UnfoldAdminSelectWidget(
109
+ choices=db_field.choices
110
+ )
111
+ else:
112
+ kwargs["widget"] = widgets.UnfoldAdminNullBooleanSelectWidget()
94
113
 
95
114
  return db_field.formfield(**kwargs)
96
115
 
unfold/sites.py CHANGED
@@ -4,15 +4,12 @@ from typing import Any, Callable, Optional, Union
4
4
  from urllib.parse import parse_qs, urlparse
5
5
 
6
6
  from django.contrib.admin import AdminSite
7
- from django.contrib.auth import REDIRECT_FIELD_NAME
8
7
  from django.core.validators import EMPTY_VALUES
9
8
  from django.http import HttpRequest, HttpResponse
10
9
  from django.template.response import TemplateResponse
11
10
  from django.urls import URLPattern, path, reverse, reverse_lazy
12
- from django.utils.decorators import method_decorator
13
11
  from django.utils.functional import lazy
14
12
  from django.utils.module_loading import import_string
15
- from django.views.decorators.cache import never_cache
16
13
 
17
14
  from unfold.dataclasses import DropdownItem, Favicon
18
15
 
@@ -91,6 +88,9 @@ class UnfoldAdminSite(AdminSite):
91
88
  "site_icon": self._get_theme_images("SITE_ICON", request),
92
89
  "site_symbol": self._get_config("SITE_SYMBOL", request),
93
90
  "site_favicons": self._get_favicons("SITE_FAVICONS", request),
91
+ "login_image": self._get_value(
92
+ get_config(self.settings_name)["LOGIN"].get("image"), request
93
+ ),
94
94
  "show_history": self._get_config("SHOW_HISTORY", request),
95
95
  "show_view_on_site": self._get_config("SHOW_VIEW_ON_SITE", request),
96
96
  "show_languages": self._get_config("SHOW_LANGUAGES", request),
@@ -170,6 +170,7 @@ class UnfoldAdminSite(AdminSite):
170
170
  ) -> TemplateResponse:
171
171
  query = request.GET.get("s").lower()
172
172
  app_list = super().get_app_list(request)
173
+ apps = []
173
174
  results = []
174
175
 
175
176
  if query in EMPTY_VALUES:
@@ -177,7 +178,7 @@ class UnfoldAdminSite(AdminSite):
177
178
 
178
179
  for app in app_list:
179
180
  if query in app["name"].lower():
180
- results.append(app)
181
+ apps.append(app)
181
182
  continue
182
183
 
183
184
  models = []
@@ -188,7 +189,16 @@ class UnfoldAdminSite(AdminSite):
188
189
 
189
190
  if len(models) > 0:
190
191
  app["models"] = models
191
- results.append(app)
192
+ apps.append(app)
193
+
194
+ for app in apps:
195
+ for model in app["models"]:
196
+ results.append(
197
+ {
198
+ "app": app,
199
+ "model": model,
200
+ }
201
+ )
192
202
 
193
203
  return TemplateResponse(
194
204
  request,
@@ -196,34 +206,11 @@ class UnfoldAdminSite(AdminSite):
196
206
  context={
197
207
  "results": results,
198
208
  },
209
+ headers={
210
+ "HX-Trigger": "search",
211
+ },
199
212
  )
200
213
 
201
- @method_decorator(never_cache)
202
- @login_not_required
203
- def login(
204
- self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
205
- ) -> HttpResponse:
206
- extra_context = {} if extra_context is None else extra_context
207
- image = self._get_value(
208
- get_config(self.settings_name)["LOGIN"].get("image"), request
209
- )
210
-
211
- redirect_field_name = self._get_value(
212
- get_config(self.settings_name)["LOGIN"].get("redirect_after"), request
213
- )
214
-
215
- if image not in EMPTY_VALUES:
216
- extra_context.update(
217
- {
218
- "image": image,
219
- }
220
- )
221
-
222
- if redirect_field_name not in EMPTY_VALUES:
223
- extra_context.update({REDIRECT_FIELD_NAME: redirect_field_name})
224
-
225
- return super().login(request, extra_context)
226
-
227
214
  def password_change(
228
215
  self, request: HttpRequest, extra_context: Optional[dict[str, Any]] = None
229
216
  ) -> HttpResponse:
@@ -80,8 +80,8 @@
80
80
  // );
81
81
 
82
82
  const siblings = $this
83
- .parent()
84
- .nextAll(".view-related, .change-related, .delete-related");
83
+ .closest(".related-widget-wrapper")
84
+ .find(".view-related, .change-related, .delete-related");
85
85
 
86
86
  if (!siblings.length) {
87
87
  return;