django-unfold 0.26.0__py3-none-any.whl → 0.27.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-unfold
3
- Version: 0.26.0
3
+ Version: 0.27.0
4
4
  Summary: Modern Django admin theme for seamless interface development
5
5
  Home-page: https://unfoldadmin.com
6
6
  License: MIT
@@ -52,11 +52,13 @@ Did you decide to start using Unfold but you don't have time to make the switch
52
52
  - **Dependencies:** completely based only on `django.contrib.admin`
53
53
  - **Actions:** multiple ways how to define actions within different parts of admin
54
54
  - **WYSIWYG:** built-in support for WYSIWYG (Trix)
55
+ - **Array widget:** built-in widget for `django.contrib.postgres.fields.ArrayField`
55
56
  - **Filters:** custom dropdown, numeric, datetime, and text fields
56
57
  - **Dashboard:** custom components for rapid dashboard development
57
58
  - **Model tabs:** define custom tab navigations for models
58
59
  - **Fieldset tabs:** merge several fielsets into tabs in change form
59
60
  - **Colors:** possibility to override default color scheme
61
+ - **Changeform modes:** display fields in changeform in compressed mode
60
62
  - **Third party packages:** default support for multiple popular applications
61
63
  - **Environment label**: distinguish between environments by displaying a label
62
64
  - **Nonrelated inlines**: displays nonrelated model as inline in changeform
@@ -321,13 +323,17 @@ def permission_callback(request):
321
323
 
322
324
  from django import models
323
325
  from django.contrib import admin
326
+ from django.contrib.postgres.fields import ArrayField
324
327
  from django.db import models
325
328
  from unfold.admin import ModelAdmin
326
- from unfold.contrib.forms.widgets import WysiwygWidget
329
+ from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
327
330
 
328
331
 
329
332
  @admin.register(MyModel)
330
333
  class CustomAdminClass(ModelAdmin):
334
+ # Display fields in changeform in compressed mode
335
+ compressed_fields = True # Default: False
336
+
331
337
  # Preprocess content of readonly fields before render
332
338
  readonly_preprocess_fields = {
333
339
  "model_field_name": "html.unescape",
@@ -346,6 +352,9 @@ class CustomAdminClass(ModelAdmin):
346
352
  formfield_overrides = {
347
353
  models.TextField: {
348
354
  "widget": WysiwygWidget,
355
+ },
356
+ ArrayField: {
357
+ "widget": ArrayWidget,
349
358
  }
350
359
  }
351
360
  ```
@@ -1,5 +1,5 @@
1
1
  unfold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- unfold/admin.py,sha256=ATDHEq87eC1zgtoObwqTCpRKqJbl0yrS0rf6AAHZTxM,24133
2
+ unfold/admin.py,sha256=T14cMt5yn-uQQWDS5_Ebb2Jjhsc0pStaKtsutNoJeWg,18921
3
3
  unfold/apps.py,sha256=SlBXPYrUd2uXn67qFbRvbXSUk3XFWrF4-5WELgDCvho,381
4
4
  unfold/checks.py,sha256=Smgji9w19hnYjJElJ_FJnnyTEAE-E-OUB6otHu7lasY,1670
5
5
  unfold/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -23,9 +23,10 @@ unfold/contrib/forms/apps.py,sha256=Di0TMzVuRpVxLG-8Bjdq5ALCSf5r7u2xVhD0jU6H5Sc,
23
23
  unfold/contrib/forms/static/unfold/forms/css/trix.css,sha256=TH9WdnaZrmwI8hAEydwjobdrBzSw_KYdRTSQDuD-8hE,20027
24
24
  unfold/contrib/forms/static/unfold/forms/js/trix.config.js,sha256=spkNBlJVk_pqido_rM6yywQxkJ3Kqb7DMLiBgpKksdA,858
25
25
  unfold/contrib/forms/static/unfold/forms/js/trix.js,sha256=Pao0XiVeDiRawfTkGDg_np6CxB-oXPrUDI9akWc87oc,174157
26
+ unfold/contrib/forms/templates/unfold/forms/array.html,sha256=11silyHbsJA0U_ksS8MvfFOJKC_qKTAwXxoMIB78APk,1507
26
27
  unfold/contrib/forms/templates/unfold/forms/helpers/toolbar.html,sha256=yS8Zy-UrzvZ5RUYwdprQzREffnYq0NlIbXBfZM2UB04,9700
27
28
  unfold/contrib/forms/templates/unfold/forms/wysiwyg.html,sha256=4ZefV6XrjJlUczcuSw8BhvMJUFSZPSXo1IkgkBivh5g,351
28
- unfold/contrib/forms/widgets.py,sha256=_81_fsvK-yEsFIqLU59BTIIs2KAJk61pLs7J9sNi1G0,962
29
+ unfold/contrib/forms/widgets.py,sha256=yWug9YQ6FWI7hqvNZqhCwJhlfC_gMh9GELVGTE2Tw9k,2813
29
30
  unfold/contrib/guardian/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
31
  unfold/contrib/guardian/apps.py,sha256=ObJqwh4vHxkD4XfduP5IQAiYiWZxsXUOUqF1_R1GsRI,136
31
32
  unfold/contrib/guardian/templates/admin/guardian/model/change_form.html,sha256=FSJc4MYYWyzZAy8Ay0b7Ov-cUo-oELHOM5fQehM54Lg,403
@@ -63,13 +64,14 @@ unfold/contrib/simple_history/templates/simple_history/object_history.html,sha25
63
64
  unfold/contrib/simple_history/templates/simple_history/object_history_form.html,sha256=MOL3Tw3Nk3Rnq1koRV7yeCev4CP06_4xqAIOQk1M7FU,2290
64
65
  unfold/contrib/simple_history/templates/simple_history/submit_line.html,sha256=ns9CEkU4HwKHhhj8qj_9UXvzp0viGtD1tp93GV2WRCs,1703
65
66
  unfold/dataclasses.py,sha256=JJdGYzQ8MpeOe2FQPJqrMn_UJcxUz1VJgHCuCtkZCA8,199
66
- unfold/decorators.py,sha256=BVDlxhZxB4ND3f5-5oiENRTv_W_Q_Eu-gZlsrYKOxiU,3272
67
+ unfold/decorators.py,sha256=6E4vPVwK0IQDAiDPg9pgyypRqciX_gR0jwITDcrSc8U,3367
67
68
  unfold/exceptions.py,sha256=gcCj1ox61E137bk_0Cqy4YC3SttdPgB-fiJUqpmyHSE,43
69
+ unfold/fields.py,sha256=01CRr526Fautw58ik5RAGAihwAeLB5mxO6PR-oFto1o,7055
68
70
  unfold/forms.py,sha256=GXEm3CFwglyuEbGdVyEMJTB45Gs-_RvGGlXJEkPy2kw,3688
69
71
  unfold/settings.py,sha256=--TdTSWdOA8TQGW4-vjJkjy_zEyd_kZwBr3BIuQ8hzI,1208
70
72
  unfold/sites.py,sha256=Gy_i43j2nizW2g8-mas5icvtk-beKism_CznATW6Ia8,12586
71
73
  unfold/static/unfold/css/simplebar.css,sha256=5LLaEM11pKi6JFCOLt4XKuZxTpT9rpdq_tNlaQytFlU,4647
72
- unfold/static/unfold/css/styles.css,sha256=hlPYMtAEeIPl7yIFpqMR4DqJwz9K2RLhpbI4aSNq6mU,91675
74
+ unfold/static/unfold/css/styles.css,sha256=CU9SCfH3nREI3C6yP9DFAvvr_23lSg9H6rAwM0_w958,91898
73
75
  unfold/static/unfold/fonts/inter/Inter-Bold.woff2,sha256=O88EyjAeRPE_QEyKBKpK5wf2epUOEu8wwjj5bnhCZqE,46552
74
76
  unfold/static/unfold/fonts/inter/Inter-Medium.woff2,sha256=O88EyjAeRPE_QEyKBKpK5wf2epUOEu8wwjj5bnhCZqE,46552
75
77
  unfold/static/unfold/fonts/inter/Inter-Regular.woff2,sha256=O88EyjAeRPE_QEyKBKpK5wf2epUOEu8wwjj5bnhCZqE,46552
@@ -91,7 +93,7 @@ unfold/templates/admin/auth/user/add_form.html,sha256=iLig-vd2YExXsj0xGBwYhZ4kGU
91
93
  unfold/templates/admin/auth/user/change_password.html,sha256=-Wa9ml3yss-kDz0YQxCiwoxs91KQD8eetCt5l6xekWM,2892
92
94
  unfold/templates/admin/base.html,sha256=MGqtCcydXZPnp6dasaWktyd9D6rdUYX01rFGAv7Zkm4,2226
93
95
  unfold/templates/admin/base_site.html,sha256=3ckWrcAdd7Pw1hk6Zwyknab_Qb-rteV9-mXhMnfo6VI,361
94
- unfold/templates/admin/change_form.html,sha256=OvegZJTH4eGHXf5RJOHfy9tXKCtyml_Yl8pENb9kNq8,5431
96
+ unfold/templates/admin/change_form.html,sha256=s7iHvcH1coJQu7QaDk6vx_tvgxMG8SQWEy4J9bBllzY,5361
95
97
  unfold/templates/admin/change_form_object_tools.html,sha256=eyeH-i2HgEM0Yi-OJA2D1VnKJyC19A_my1IDGxxoP8Y,593
96
98
  unfold/templates/admin/change_list.html,sha256=18GDZswc1c0xtw2BcKti9SX95Ar9e1BX_HSY0K79g_8,5102
97
99
  unfold/templates/admin/change_list_object_tools.html,sha256=cmMiT2nT20Ph5yfpj9aHPr76Z-JP4aSXp0o-Rnad28s,147
@@ -100,9 +102,9 @@ unfold/templates/admin/date_hierarchy.html,sha256=BfUPbsLpHZVa40BHBahz1H9RSVuz36
100
102
  unfold/templates/admin/delete_confirmation.html,sha256=hpa2E14oZEXBBs6W1qdNQuF650TIO2Rhr52Q6UfwVeQ,5166
101
103
  unfold/templates/admin/delete_selected_confirmation.html,sha256=Foka2yvwAMEZre-Kh1KNadRzrCotdKM2U4e6AJQYZu8,4941
102
104
  unfold/templates/admin/edit_inline/stacked.html,sha256=HG-Dj42gcKNofHRVjg0ltIER2oJGYUd9GN_B7lDv7rQ,4580
103
- unfold/templates/admin/edit_inline/tabular.html,sha256=mVlQ5aJr6-GdHM2zaWVUKaa6gok3Vv3MAMQj0sIX-lQ,12976
105
+ unfold/templates/admin/edit_inline/tabular.html,sha256=aMy6kk3ZDBtgZFf1pO30vHWmONvCYor9yoiR9Day4xI,13042
104
106
  unfold/templates/admin/filter.html,sha256=dkrFkei-EAlldIU8DrgvSChzWQuUOu6-LS_qlZxdfFw,1708
105
- unfold/templates/admin/includes/fieldset.html,sha256=qVxXy7KRI8GC4bIBPO9hjQlMtDg83vhEZbmVXqdfrgg,2929
107
+ unfold/templates/admin/includes/fieldset.html,sha256=lMVwBifFWKvLvHqZ6yjP6Xf6BJFzi-EOf5JHIxEHmRI,888
106
108
  unfold/templates/admin/includes/object_delete_summary.html,sha256=Nv69SCzyJHFX14iJFfodxKM0IIpQegKZH0fvKB15QJI,468
107
109
  unfold/templates/admin/index.html,sha256=pkGdKWdD3zzOvkRdELvdb15sleSpfl4eHPA14PAh7z0,684
108
110
  unfold/templates/admin/login.html,sha256=WdOfFLofwBWj9VKCq1U22uLY19J2YQY6vRaE4OOSKfQ,3681
@@ -133,10 +135,12 @@ unfold/templates/unfold/helpers/app_list.html,sha256=lFnW8p9DcZbI9t3_ee9JX9ERHA0
133
135
  unfold/templates/unfold/helpers/app_list_default.html,sha256=vZkw1F7oHOKReNkdHRYjhuNdA1nNdvSD4wbDmf0bnsM,4102
134
136
  unfold/templates/unfold/helpers/boolean.html,sha256=p_WOlytoXvDwta76WgcV4JSWKpBgKf4amhqmHF798F8,564
135
137
  unfold/templates/unfold/helpers/breadcrumb_item.html,sha256=k_1j57UV0WtzFFlMKaewj4NLbR_DhXI6RzCHThblZLw,234
136
- unfold/templates/unfold/helpers/display_header.html,sha256=RR3HexzdO3YazPSJYfF9UfujCDiJ_EDrmnfxDFNLd7U,1160
138
+ unfold/templates/unfold/helpers/display_header.html,sha256=HiuaIJ6Y8DSM99OFypLO_2uQadZIw7tSg1aJJpFEXkM,1161
137
139
  unfold/templates/unfold/helpers/display_label.html,sha256=LS9DWzYjHkYLV27sZDwyXlg2sLJ0AlId9FbjnXpsbfg,317
138
140
  unfold/templates/unfold/helpers/field.html,sha256=Ds-zUHkdyxamfUCVNhxvtM0XoJg9OCA0QcsLbLWv4oo,882
139
- unfold/templates/unfold/helpers/field_readonly.html,sha256=v7-2oSSDgOsuYpP70y8DqdBqbRybubAfSDzstveoBuw,382
141
+ unfold/templates/unfold/helpers/field_readonly.html,sha256=O0gHEW46OWt1oUUk0lZiyR-mztWv_7IH6GpKRm2wUw4,235
142
+ unfold/templates/unfold/helpers/field_readonly_value.html,sha256=wdzHVKaI96S-S1MaV-odKXDdT_MRkfWMcXdUBhRlTDY,490
143
+ unfold/templates/unfold/helpers/fieldset_row.html,sha256=HHGzZ09Irtj4PUGXZkbAAvJcBFGcvJqjR7k-TZzpVyU,2730
140
144
  unfold/templates/unfold/helpers/fieldsets_tabs.html,sha256=V3bgW75eozaBDty-xfciGafhCWq_Ba5HfQkk92yRc9A,1445
141
145
  unfold/templates/unfold/helpers/form_errors.html,sha256=EwerIJptSCWXvtAJ1IZKfEn98qlShBIGavsTThbklAs,266
142
146
  unfold/templates/unfold/helpers/form_label.html,sha256=SR4U6iK9w4oels6iGY_Da-yN4BbXQVN9zCDlBGGXcw8,310
@@ -163,10 +167,10 @@ unfold/templates/unfold/helpers/userlinks.html,sha256=qWjtBt9Q_tU8a874ii0Qqg8t_d
163
167
  unfold/templates/unfold/helpers/welcomemsg.html,sha256=noRysgSENef4_53pXaTiBCy2or6lQm1ZtmCQVODAB1c,1120
164
168
  unfold/templates/unfold/layouts/base_simple.html,sha256=rki7n7QagHFAaCEn488pTOj9dpNL9AwwzKps8Ipiubk,993
165
169
  unfold/templates/unfold/layouts/skeleton.html,sha256=iXrUiggVp36vmBIia5p32c-9Ruy0PxkFFQogDpvENbk,3419
166
- unfold/templates/unfold/widgets/clearable_file_input.html,sha256=vXsyP0-YD-z3z6VL4vXW9pJH9_-ZU9u-3AnmZkni-R4,1994
170
+ unfold/templates/unfold/widgets/clearable_file_input.html,sha256=gAJsfyCnanOyeN4ypp1y7r76znvITV7FYTyWvPsmlDs,1882
167
171
  unfold/templates/unfold/widgets/clearable_file_input_small.html,sha256=rqUnHF4jwL8_RySUuq2aXgj-0P_usgo1HeVT_IcfyFY,2531
168
172
  unfold/templates/unfold/widgets/date.html,sha256=WXo2LG1v_gBZBSg-zocj7oujMKI0MWLYCIFfB04HMLQ,122
169
- unfold/templates/unfold/widgets/foreign_key_raw_id.html,sha256=26UGK04ojS-ZsvhWpPIbr6VE9b91Q2sEhB7FBkMY1NI,917
173
+ unfold/templates/unfold/widgets/foreign_key_raw_id.html,sha256=BgXi4ziywtkWmZuUye5bbJ6yeEoHDJB_2lkwXX8hc6c,1026
170
174
  unfold/templates/unfold/widgets/radio.html,sha256=3WcmclQNg7R_pRjEHL1dHkGjAzWlWNYnhHkAirC4nuA,646
171
175
  unfold/templates/unfold/widgets/radio_option.html,sha256=IZgPx-aWKJuxrSalJ3K50RFd1vwSpb9Qk0yZwfV78_A,368
172
176
  unfold/templates/unfold/widgets/range.html,sha256=28FBtSUgUcG82vpk_I27Lbs5oWZOV_oMzVhx4wj3-Ik,262
@@ -184,7 +188,7 @@ unfold/typing.py,sha256=1P8PWM2oeaceUJtA5j071RbKEBpHYaux441u7Hd6wv4,643
184
188
  unfold/utils.py,sha256=5OIgDcwvIJQbwbnnqHx61cHh-2T1h184mTAuNq5WXLI,4088
185
189
  unfold/views.py,sha256=Ml3XlEoHLcbEWof59Dw8ihKBMcmp-gBAibThtBFj55A,708
186
190
  unfold/widgets.py,sha256=iiI73XznYH34LgXBHu1oVtYb76lLO3HiOD9blmm89rY,15236
187
- django_unfold-0.26.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
188
- django_unfold-0.26.0.dist-info/METADATA,sha256=UUv_H7BwNNquFABkpQ1PObEBBKknsGFxkyDnkk3l5oU,48746
189
- django_unfold-0.26.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
190
- django_unfold-0.26.0.dist-info/RECORD,,
191
+ django_unfold-0.27.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
192
+ django_unfold-0.27.0.dist-info/METADATA,sha256=llKdxPkbhzG4E-UJji8aTaQkjm5YTf9DBP4OSC9pa6c,49139
193
+ django_unfold-0.27.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
194
+ django_unfold-0.27.0.dist-info/RECORD,,
unfold/admin.py CHANGED
@@ -7,50 +7,30 @@ from django.contrib.admin import ModelAdmin as BaseModelAdmin
7
7
  from django.contrib.admin import StackedInline as BaseStackedInline
8
8
  from django.contrib.admin import TabularInline as BaseTabularInline
9
9
  from django.contrib.admin import display, helpers
10
- from django.contrib.admin.utils import lookup_field
11
10
  from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
12
- from django.core.exceptions import ObjectDoesNotExist
13
11
  from django.db import models
14
- from django.db.models import (
15
- BLANK_CHOICE_DASH,
16
- ForeignObjectRel,
17
- JSONField,
18
- ManyToManyRel,
19
- Model,
20
- OneToOneField,
21
- )
12
+ from django.db.models import BLANK_CHOICE_DASH, Model
22
13
  from django.db.models.fields import Field
23
14
  from django.db.models.fields.related import ForeignKey, ManyToManyField
24
15
  from django.forms import Form
25
16
  from django.forms.fields import TypedChoiceField
26
- from django.forms.models import (
27
- ModelChoiceField,
28
- ModelMultipleChoiceField,
29
- )
30
- from django.forms.utils import flatatt
17
+ from django.forms.models import ModelChoiceField, ModelMultipleChoiceField
31
18
  from django.forms.widgets import SelectMultiple
32
19
  from django.http import HttpRequest, HttpResponse
33
20
  from django.shortcuts import redirect
34
- from django.template.defaultfilters import linebreaksbr
35
21
  from django.template.response import TemplateResponse
36
22
  from django.urls import URLPattern, path, reverse
37
- from django.utils.html import conditional_escape, format_html
38
- from django.utils.module_loading import import_string
39
- from django.utils.safestring import SafeText, mark_safe
40
- from django.utils.text import capfirst
23
+ from django.utils.safestring import mark_safe
41
24
  from django.utils.translation import gettext_lazy as _
42
25
  from django.views import View
43
26
 
44
27
  from .checks import UnfoldModelAdminChecks
45
28
  from .dataclasses import UnfoldAction
46
29
  from .exceptions import UnfoldException
30
+ from .fields import UnfoldAdminField, UnfoldAdminReadonlyField
47
31
  from .forms import ActionForm
48
- from .settings import get_config
49
32
  from .typing import FieldsetsType
50
- from .utils import display_for_field
51
33
  from .widgets import (
52
- CHECKBOX_LABEL_CLASSES,
53
- LABEL_CLASSES,
54
34
  SELECT_CLASSES,
55
35
  UnfoldAdminBigIntegerFieldWidget,
56
36
  UnfoldAdminDecimalFieldWidget,
@@ -90,8 +70,6 @@ try:
90
70
  except ImportError:
91
71
  HAS_MONEY = False
92
72
 
93
- checkbox = UnfoldBooleanWidget({"class": "action-select"}, lambda value: False)
94
-
95
73
  FORMFIELD_OVERRIDES = {
96
74
  models.DateTimeField: {
97
75
  "form_class": forms.SplitDateTimeField,
@@ -141,140 +119,10 @@ FORMFIELD_OVERRIDES_INLINE.update(
141
119
  }
142
120
  )
143
121
 
144
-
145
- class UnfoldAdminField(helpers.AdminField):
146
- def label_tag(self) -> SafeText:
147
- classes = []
148
- if not self.field.field.widget.__class__.__name__.startswith(
149
- "Unfold"
150
- ) and not self.field.field.widget.template_name.startswith("unfold"):
151
- return super().label_tag()
152
-
153
- # TODO load config from current AdminSite (override Fieldline.__iter__ method)
154
- for lang, flag in get_config()["EXTENSIONS"]["modeltranslation"][
155
- "flags"
156
- ].items():
157
- if f"[{lang}]" in self.field.label:
158
- self.field.label = self.field.label.replace(f"[{lang}]", flag)
159
- break
160
-
161
- contents = conditional_escape(self.field.label)
162
-
163
- if self.is_checkbox:
164
- classes.append(" ".join(CHECKBOX_LABEL_CLASSES))
165
- else:
166
- classes.append(" ".join(LABEL_CLASSES))
167
-
168
- if self.field.field.required:
169
- classes.append("required")
170
-
171
- attrs = {"class": " ".join(classes)} if classes else {}
172
- required = mark_safe(' <span class="text-red-600">*</span>')
173
-
174
- return self.field.label_tag(
175
- contents=mark_safe(contents),
176
- attrs=attrs,
177
- label_suffix=required if self.field.field.required else "",
178
- )
179
-
122
+ checkbox = UnfoldBooleanWidget({"class": "action-select"}, lambda value: False)
180
123
 
181
124
  helpers.AdminField = UnfoldAdminField
182
125
 
183
-
184
- class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
185
- def label_tag(self) -> SafeText:
186
- if not isinstance(self.model_admin, ModelAdmin) and not isinstance(
187
- self.model_admin, ModelAdminMixin
188
- ):
189
- return super().label_tag()
190
-
191
- attrs = {
192
- "class": " ".join(LABEL_CLASSES + ["mb-2"]),
193
- }
194
-
195
- label = self.field["label"]
196
-
197
- return format_html(
198
- "<label{}>{}{}</label>",
199
- flatatt(attrs),
200
- capfirst(label),
201
- self.form.label_suffix,
202
- )
203
-
204
- def is_json(self) -> bool:
205
- field, obj, model_admin = (
206
- self.field["field"],
207
- self.form.instance,
208
- self.model_admin,
209
- )
210
-
211
- try:
212
- f, attr, value = lookup_field(field, obj, model_admin)
213
- except (AttributeError, ValueError, ObjectDoesNotExist):
214
- return False
215
-
216
- return isinstance(f, JSONField)
217
-
218
- def contents(self) -> str:
219
- contents = self._get_contents()
220
- contents = self._preprocess_field(contents)
221
- return contents
222
-
223
- def _get_contents(self) -> str:
224
- from django.contrib.admin.templatetags.admin_list import _boolean_icon
225
-
226
- field, obj, model_admin = (
227
- self.field["field"],
228
- self.form.instance,
229
- self.model_admin,
230
- )
231
- try:
232
- f, attr, value = lookup_field(field, obj, model_admin)
233
- except (AttributeError, ValueError, ObjectDoesNotExist):
234
- result_repr = self.empty_value_display
235
- else:
236
- if field in self.form.fields:
237
- widget = self.form[field].field.widget
238
- # This isn't elegant but suffices for contrib.auth's
239
- # ReadOnlyPasswordHashWidget.
240
- if getattr(widget, "read_only", False):
241
- return widget.render(field, value)
242
- if f is None:
243
- if getattr(attr, "boolean", False):
244
- result_repr = _boolean_icon(value)
245
- else:
246
- if hasattr(value, "__html__"):
247
- result_repr = value
248
- else:
249
- result_repr = linebreaksbr(value)
250
- else:
251
- if isinstance(f.remote_field, ManyToManyRel) and value is not None:
252
- result_repr = ", ".join(map(str, value.all()))
253
- elif (
254
- isinstance(f.remote_field, (ForeignObjectRel, OneToOneField))
255
- and value is not None
256
- ):
257
- result_repr = self.get_admin_url(f.remote_field, value)
258
- else:
259
- result_repr = display_for_field(value, f, self.empty_value_display)
260
- return conditional_escape(result_repr)
261
- result_repr = linebreaksbr(result_repr)
262
- return conditional_escape(result_repr)
263
-
264
- def _preprocess_field(self, contents: str) -> str:
265
- if (
266
- hasattr(self.model_admin, "readonly_preprocess_fields")
267
- and self.field["field"] in self.model_admin.readonly_preprocess_fields
268
- ):
269
- func = self.model_admin.readonly_preprocess_fields[self.field["field"]]
270
- if isinstance(func, str):
271
- contents = import_string(func)(contents)
272
- elif callable(func):
273
- contents = func(contents)
274
-
275
- return contents
276
-
277
-
278
126
  helpers.AdminReadonlyField = UnfoldAdminReadonlyField
279
127
 
280
128
 
@@ -0,0 +1,31 @@
1
+ {% load i18n %}
2
+
3
+ <div class="flex flex-col gap-4" x-data="{items: []}">
4
+ {% for subwidget in widget.subwidgets %}
5
+ <div class="flex flex-row">
6
+ {% with widget=subwidget %}
7
+ {% include widget.template_name %}
8
+ {% endwith %}
9
+
10
+ <a x-on:click="$el.parentElement.remove()" class="bg-white border cursor-pointer flex items-center h-9.5 justify-center ml-2 rounded shadow-sm shrink-0 text-red-600 text-sm w-9.5 dark:bg-gray-900 dark:border-gray-700 dark:text-red-500">
11
+ <span class="material-symbols-outlined text-sm">delete</span>
12
+ </a>
13
+ </div>
14
+ {% endfor %}
15
+
16
+ <template x-for="(item, index) in items" :key="item.key">
17
+ <div class="flex flex-row">
18
+ {% include template.template_name with widget=template %}
19
+
20
+ <a x-on:click="items.splice(index, 1)" class="bg-white border cursor-pointer flex items-center h-9.5 justify-center ml-2 rounded shadow-sm shrink-0 text-red-600 text-sm w-9.5 dark:bg-gray-900 dark:border-gray-700 dark:text-red-500">
21
+ <span class="material-symbols-outlined text-sm">delete</span>
22
+ </a>
23
+ </div>
24
+ </template>
25
+
26
+ <div class="flex flex-row">
27
+ <div x-on:click="items.push({ key: new Date().getTime()})" class="bg-primary-600 border border-transparent cursor-pointer font-medium inline-block px-3 py-2 rounded-md text-sm text-white w-full lg:w-auto">
28
+ {% trans "Add new item" %}
29
+ </div>
30
+ </div>
31
+ </div>
@@ -1,7 +1,10 @@
1
- from typing import Any, Dict, Optional
1
+ from typing import Any, Dict, List, Optional, Union
2
2
 
3
- from django.forms import Widget
4
- from unfold.widgets import PROSE_CLASSES
3
+ from django.core.validators import EMPTY_VALUES
4
+ from django.forms import MultiWidget, Widget
5
+ from django.http import QueryDict
6
+ from django.utils.datastructures import MultiValueDict
7
+ from unfold.widgets import PROSE_CLASSES, UnfoldAdminTextInputWidget
5
8
 
6
9
  WYSIWYG_CLASSES = [
7
10
  *PROSE_CLASSES,
@@ -22,6 +25,58 @@ WYSIWYG_CLASSES = [
22
25
  ]
23
26
 
24
27
 
28
+ class ArrayWidget(MultiWidget):
29
+ template_name = "unfold/forms/array.html"
30
+ widget_class = UnfoldAdminTextInputWidget
31
+
32
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
33
+ widgets = [self.widget_class]
34
+ super().__init__(widgets)
35
+
36
+ def get_context(self, name: str, value: str, attrs: Dict) -> Dict:
37
+ self._resolve_widgets(value)
38
+ context = super().get_context(name, value, attrs)
39
+ template_widget = UnfoldAdminTextInputWidget()
40
+ template_widget.name = name
41
+
42
+ context.update({"template": template_widget})
43
+ return context
44
+
45
+ def value_from_datadict(
46
+ self, data: QueryDict, files: MultiValueDict, name: str
47
+ ) -> List:
48
+ values = []
49
+
50
+ for item in data.getlist(name):
51
+ if item not in EMPTY_VALUES:
52
+ values.append(item)
53
+
54
+ return values
55
+
56
+ def value_omitted_from_data(
57
+ self, data: QueryDict, files: MultiValueDict, name: str
58
+ ) -> List:
59
+ return data.getlist(name) not in [[""], *EMPTY_VALUES]
60
+
61
+ def decompress(self, value: Union[str, List]) -> List:
62
+ if isinstance(value, List):
63
+ return value.split(",")
64
+
65
+ return []
66
+
67
+ def _resolve_widgets(self, value: Optional[Union[List, str]]) -> None:
68
+ if value is None:
69
+ value = []
70
+
71
+ elif isinstance(value, List):
72
+ self.widgets = [self.widget_class for item in value]
73
+ else:
74
+ self.widgets = [self.widget_class for item in value.split(",")]
75
+
76
+ self.widgets_names = ["" for i in range(len(self.widgets))]
77
+ self.widgets = [w() if isinstance(w, type) else w for w in self.widgets]
78
+
79
+
25
80
  class WysiwygWidget(Widget):
26
81
  template_name = "unfold/forms/wysiwyg.html"
27
82
 
unfold/decorators.py CHANGED
@@ -58,6 +58,7 @@ def display(
58
58
  function: Optional[Callable[[Model], Any]] = None,
59
59
  *,
60
60
  boolean: Optional[bool] = None,
61
+ image: Optional[bool] = None,
61
62
  ordering: Optional[Union[str, Combinable, BaseExpression]] = None,
62
63
  description: Optional[str] = None,
63
64
  empty_value: Optional[str] = None,
@@ -72,6 +73,8 @@ def display(
72
73
  )
73
74
  if boolean is not None:
74
75
  func.boolean = boolean
76
+ if image is not None:
77
+ func.image = image
75
78
  if ordering is not None:
76
79
  func.admin_order_field = ordering
77
80
  if description is not None:
unfold/fields.py ADDED
@@ -0,0 +1,200 @@
1
+ from django.contrib.admin import helpers
2
+ from django.contrib.admin.utils import lookup_field, quote
3
+ from django.core.exceptions import ObjectDoesNotExist
4
+ from django.db import models
5
+ from django.db.models import (
6
+ ForeignObjectRel,
7
+ ImageField,
8
+ JSONField,
9
+ ManyToManyRel,
10
+ OneToOneField,
11
+ )
12
+ from django.forms.utils import flatatt
13
+ from django.template.defaultfilters import linebreaksbr
14
+ from django.urls import NoReverseMatch, reverse
15
+ from django.utils.html import conditional_escape, format_html
16
+ from django.utils.module_loading import import_string
17
+ from django.utils.safestring import SafeText, mark_safe
18
+ from django.utils.text import capfirst
19
+
20
+ from .settings import get_config
21
+ from .utils import display_for_field
22
+ from .widgets import CHECKBOX_LABEL_CLASSES, LABEL_CLASSES
23
+
24
+
25
+ class UnfoldAdminReadonlyField(helpers.AdminReadonlyField):
26
+ def label_tag(self) -> SafeText:
27
+ from .admin import ModelAdmin, ModelAdminMixin
28
+
29
+ if not isinstance(self.model_admin, ModelAdmin) and not isinstance(
30
+ self.model_admin, ModelAdminMixin
31
+ ):
32
+ return super().label_tag()
33
+
34
+ attrs = {
35
+ "class": " ".join(LABEL_CLASSES + ["mb-2"]),
36
+ }
37
+
38
+ label = self.field["label"]
39
+
40
+ return format_html(
41
+ "<label{}>{}{}</label>",
42
+ flatatt(attrs),
43
+ capfirst(label),
44
+ self.form.label_suffix,
45
+ )
46
+
47
+ def is_json(self) -> bool:
48
+ field, obj, model_admin = (
49
+ self.field["field"],
50
+ self.form.instance,
51
+ self.model_admin,
52
+ )
53
+
54
+ try:
55
+ f, attr, value = lookup_field(field, obj, model_admin)
56
+ except (AttributeError, ValueError, ObjectDoesNotExist):
57
+ return False
58
+
59
+ return isinstance(f, JSONField)
60
+
61
+ def is_image(self) -> bool:
62
+ field, obj, model_admin = (
63
+ self.field["field"],
64
+ self.form.instance,
65
+ self.model_admin,
66
+ )
67
+
68
+ try:
69
+ f, attr, value = lookup_field(field, obj, model_admin)
70
+ except (AttributeError, ValueError, ObjectDoesNotExist):
71
+ return False
72
+
73
+ if hasattr(attr, "image"):
74
+ return attr.image
75
+ elif (
76
+ isinstance(attr, property)
77
+ and hasattr(attr, "fget")
78
+ and hasattr(attr.fget, "image")
79
+ ):
80
+ return attr.fget.image
81
+
82
+ return isinstance(f, ImageField)
83
+
84
+ def contents(self) -> str:
85
+ contents = self._get_contents()
86
+ contents = self._preprocess_field(contents)
87
+ return contents
88
+
89
+ def get_admin_url(self, remote_field, remote_obj):
90
+ url_name = f"admin:{remote_field.model._meta.app_label}_{remote_field.model._meta.model_name}_change"
91
+ try:
92
+ url = reverse(
93
+ url_name,
94
+ args=[quote(remote_obj.pk)],
95
+ current_app=self.model_admin.admin_site.name,
96
+ )
97
+ return format_html(
98
+ '<a href="{}" class="text-primary-600 underline whitespace-nowrap">{}</a>',
99
+ url,
100
+ remote_obj,
101
+ )
102
+ except NoReverseMatch:
103
+ return str(remote_obj)
104
+
105
+ def _get_contents(self) -> str:
106
+ from django.contrib.admin.templatetags.admin_list import _boolean_icon
107
+
108
+ field, obj, model_admin = (
109
+ self.field["field"],
110
+ self.form.instance,
111
+ self.model_admin,
112
+ )
113
+ try:
114
+ f, attr, value = lookup_field(field, obj, model_admin)
115
+ except (AttributeError, ValueError, ObjectDoesNotExist):
116
+ result_repr = self.empty_value_display
117
+ else:
118
+ if field in self.form.fields:
119
+ widget = self.form[field].field.widget
120
+ # This isn't elegant but suffices for contrib.auth's
121
+ # ReadOnlyPasswordHashWidget.
122
+ if getattr(widget, "read_only", False):
123
+ return widget.render(field, value)
124
+
125
+ if f is None:
126
+ if getattr(attr, "boolean", False):
127
+ result_repr = _boolean_icon(value)
128
+ else:
129
+ if hasattr(value, "__html__"):
130
+ result_repr = value
131
+ else:
132
+ result_repr = linebreaksbr(value)
133
+ else:
134
+ if isinstance(f.remote_field, ManyToManyRel) and value is not None:
135
+ result_repr = ", ".join(map(str, value.all()))
136
+ elif (
137
+ isinstance(f.remote_field, (ForeignObjectRel, OneToOneField))
138
+ and value is not None
139
+ ):
140
+ result_repr = self.get_admin_url(f.remote_field, value)
141
+ elif isinstance(f, models.URLField):
142
+ return format_html(
143
+ '<a href="{}" class="text-primary-600 underline whitespace-nowrap">{}</a>',
144
+ value,
145
+ value,
146
+ )
147
+ else:
148
+ result_repr = display_for_field(value, f, self.empty_value_display)
149
+ return conditional_escape(result_repr)
150
+ result_repr = linebreaksbr(result_repr)
151
+ return conditional_escape(result_repr)
152
+
153
+ def _preprocess_field(self, contents: str) -> str:
154
+ if (
155
+ hasattr(self.model_admin, "readonly_preprocess_fields")
156
+ and self.field["field"] in self.model_admin.readonly_preprocess_fields
157
+ ):
158
+ func = self.model_admin.readonly_preprocess_fields[self.field["field"]]
159
+ if isinstance(func, str):
160
+ contents = import_string(func)(contents)
161
+ elif callable(func):
162
+ contents = func(contents)
163
+
164
+ return contents
165
+
166
+
167
+ class UnfoldAdminField(helpers.AdminField):
168
+ def label_tag(self) -> SafeText:
169
+ classes = []
170
+ if not self.field.field.widget.__class__.__name__.startswith(
171
+ "Unfold"
172
+ ) and not self.field.field.widget.template_name.startswith("unfold"):
173
+ return super().label_tag()
174
+
175
+ # TODO load config from current AdminSite (override Fieldline.__iter__ method)
176
+ for lang, flag in get_config()["EXTENSIONS"]["modeltranslation"][
177
+ "flags"
178
+ ].items():
179
+ if f"[{lang}]" in self.field.label:
180
+ self.field.label = self.field.label.replace(f"[{lang}]", flag)
181
+ break
182
+
183
+ contents = conditional_escape(self.field.label)
184
+
185
+ if self.is_checkbox:
186
+ classes.append(" ".join(CHECKBOX_LABEL_CLASSES))
187
+ else:
188
+ classes.append(" ".join(LABEL_CLASSES))
189
+
190
+ if self.field.field.required:
191
+ classes.append("required")
192
+
193
+ attrs = {"class": " ".join(classes)} if classes else {}
194
+ required = mark_safe(' <span class="text-red-600">*</span>')
195
+
196
+ return self.field.label_tag(
197
+ contents=mark_safe(contents),
198
+ attrs=attrs,
199
+ label_suffix=required if self.field.field.required else "",
200
+ )