django-unfold 0.54.0__py3-none-any.whl → 0.55.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.54.0.dist-info → django_unfold-0.55.0.dist-info}/METADATA +24 -11
- {django_unfold-0.54.0.dist-info → django_unfold-0.55.0.dist-info}/RECORD +39 -17
- unfold/contrib/filters/admin/choice_filters.py +71 -32
- unfold/contrib/filters/admin/dropdown_filters.py +15 -1
- unfold/contrib/filters/admin/mixins.py +25 -0
- unfold/layout.py +23 -0
- unfold/sites.py +17 -6
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/fonts/material-symbols/LICENSE +202 -0
- unfold/static/unfold/fonts/material-symbols/Material-Symbols-Outlined.woff2 +0 -0
- unfold/static/unfold/js/alpine.js +2 -2
- unfold/static/unfold/js/select2.init.js +4 -0
- unfold/templates/unfold/components/button.html +1 -1
- unfold/templates/unfold/widgets/clearable_file_input.html +1 -1
- unfold/templates/unfold/widgets/clearable_file_input_small.html +1 -1
- unfold/templates/unfold_crispy/display_form.html +9 -0
- unfold/templates/unfold_crispy/errors.html +3 -0
- unfold/templates/unfold_crispy/field.html +23 -0
- unfold/templates/unfold_crispy/inputs.html +7 -0
- unfold/templates/unfold_crispy/layout/attrs.html +1 -0
- unfold/templates/unfold_crispy/layout/baseinput.html +9 -0
- unfold/templates/unfold_crispy/layout/button.html +1 -0
- unfold/templates/unfold_crispy/layout/buttonholder.html +3 -0
- unfold/templates/unfold_crispy/layout/checkbox.html +19 -0
- unfold/templates/unfold_crispy/layout/column.html +3 -0
- unfold/templates/unfold_crispy/layout/div.html +4 -0
- unfold/templates/unfold_crispy/layout/field_errors.html +7 -0
- unfold/templates/unfold_crispy/layout/fieldset.html +9 -0
- unfold/templates/unfold_crispy/layout/help_text.html +5 -0
- unfold/templates/unfold_crispy/layout/help_text_and_errors.html +3 -0
- unfold/templates/unfold_crispy/layout/radio_checkbox_select.html +21 -0
- unfold/templates/unfold_crispy/layout/row.html +3 -0
- unfold/templates/unfold_crispy/layout/table_inline_formset.html +100 -0
- unfold/templates/unfold_crispy/uni_form.html +11 -0
- unfold/templates/unfold_crispy/whole_uni_form.html +14 -0
- unfold/templatetags/unfold.py +15 -9
- unfold/widgets.py +86 -1
- {django_unfold-0.54.0.dist-info → django_unfold-0.55.0.dist-info}/LICENSE.md +0 -0
- {django_unfold-0.54.0.dist-info → django_unfold-0.55.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
<fieldset {% if fieldset.css_id %}id="{{ fieldset.css_id }}"{% endif %} class="fieldset group flex flex-col gap-5 grow rounded border-base-200 shadow-sm aligned border p-3 relative dark:border-base-800 {% if fieldset.css_class %} {{ fieldset.css_class }}{% endif %}" {{ fieldset.flat_attrs }}>
|
2
|
+
{% if legend %}
|
3
|
+
<legend class="border-b border-base-200 font-semibold float-left pb-3 -mx-3 px-3 text-font-important-light dark:text-font-important-dark dark:border-base-800">
|
4
|
+
{{ legend|safe }}
|
5
|
+
</legend>
|
6
|
+
{% endif %}
|
7
|
+
|
8
|
+
{{ fields|safe }}
|
9
|
+
</fieldset>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
{% load crispy_forms_filters l10n %}
|
2
|
+
|
3
|
+
<div class="flex flex-col gap-2 {% if field_class %}{{ field_class }}{% endif %}"{% if flat_attrs %} {{ flat_attrs }}{% endif %}>
|
4
|
+
{% for group, options, index in field|optgroups %}
|
5
|
+
{% if group %}
|
6
|
+
<strong>{{ group }}</strong>
|
7
|
+
{% endif %}
|
8
|
+
|
9
|
+
{% for option in options %}
|
10
|
+
<div>
|
11
|
+
<label for="{{ option.attrs.id }}" class="flex flex-row items-center gap-2">
|
12
|
+
<input type="{{ option.type }}" class="{% if field.errors %}errors{% endif %} {% if option.type == "radio" %}{{ form_classes.radio }}{% else %}{{ form_classes.checkbox }}{% endif %}" name="{{ field.html_name }}" value="{{ option.value|unlocalize }}" {% include "unfold_crispy/layout/attrs.html" with widget=option %}>
|
13
|
+
|
14
|
+
{{ option.label|unlocalize }}
|
15
|
+
</label>
|
16
|
+
</div>
|
17
|
+
{% endfor %}
|
18
|
+
{% endfor %}
|
19
|
+
</div>
|
20
|
+
|
21
|
+
{% include 'unfold_crispy/layout/help_text_and_errors.html' %}
|
@@ -0,0 +1,100 @@
|
|
1
|
+
{% load crispy_forms_tags crispy_forms_utils crispy_forms_field i18n %}
|
2
|
+
|
3
|
+
{% specialspaceless %}
|
4
|
+
{% if formset_tag %}
|
5
|
+
<form {{ flat_attrs }} method="{{ form_method }}" {% if formset.is_multipart %} enctype="multipart/form-data"{% endif %}>
|
6
|
+
{% endif %}
|
7
|
+
|
8
|
+
{% if formset_method|lower == 'post' and not disable_csrf %}
|
9
|
+
{% csrf_token %}
|
10
|
+
{% endif %}
|
11
|
+
|
12
|
+
<div class="overflow-x-auto border border-base-200 rounded shadow-sm dark:border-base-800" {% if form_id %} id="{{ form_id }}"{% endif %}>
|
13
|
+
{{ formset.management_form|crispy }}
|
14
|
+
|
15
|
+
<table class="w-full">
|
16
|
+
<thead>
|
17
|
+
{% if formset.readonly and not formset.queryset.exists %}
|
18
|
+
{% else %}
|
19
|
+
<tr>
|
20
|
+
{% for field in formset.forms.0 %}
|
21
|
+
{% if field.label and not field.is_hidden %}
|
22
|
+
<th for="{{ field.auto_id }}" class="align-middle font-semibold py-2 text-left text-font-important-light dark:text-font-important-dark whitespace-nowrap px-3 {% if field.name == "DELETE" %}w-0{% endif %}">
|
23
|
+
{{ field.label }}{% if field.field.required and not field|is_checkbox %} <span class="asteriskField">*</span>{% endif %}
|
24
|
+
</th>
|
25
|
+
{% endif %}
|
26
|
+
{% endfor %}
|
27
|
+
</tr>
|
28
|
+
{% endif %}
|
29
|
+
</thead>
|
30
|
+
|
31
|
+
<tbody {% if formset_id %}id="{{ formset_id }}-rows"{% endif %}>
|
32
|
+
{% for form in formset %}
|
33
|
+
{% if form_show_errors and form.non_field_errors and not form.is_extra %}
|
34
|
+
<tr>
|
35
|
+
<td colspan="100%" class="border-t border-base-200 px-3 pt-3 dark:border-base-800">
|
36
|
+
{% include "unfold_crispy/errors.html" %}
|
37
|
+
</td>
|
38
|
+
</tr>
|
39
|
+
{% endif %}
|
40
|
+
|
41
|
+
<tr>
|
42
|
+
{% for field in form %}
|
43
|
+
{% include 'unfold_crispy/field.html' with tag="td" form_show_labels=False %}
|
44
|
+
{% endfor %}
|
45
|
+
</tr>
|
46
|
+
{% endfor %}
|
47
|
+
</tbody>
|
48
|
+
|
49
|
+
{% if form_add and formset_id %}
|
50
|
+
<tbody>
|
51
|
+
<tr class="empty-form">
|
52
|
+
{% for field in formset.empty_form %}
|
53
|
+
{% include 'unfold_crispy/field.html' with tag="td" form_show_labels=False %}
|
54
|
+
{% endfor %}
|
55
|
+
</tr>
|
56
|
+
|
57
|
+
<tr id="{{ formset_id }}-add-row-wrapper">
|
58
|
+
<td colspan="100%" class="border-t border-base-200 p-3 dark:border-base-800">
|
59
|
+
<div class="flex justify-end">
|
60
|
+
<button type="button" id="{{ formset_id }}-add-row-button" class="border border-base-200 font-medium px-3 py-2 rounded transition-all w-full hover:bg-base-50 lg:block lg:w-auto dark:border-base-700 dark:hover:text-base-200 dark:hover:bg-base-900">
|
61
|
+
{% trans "Add row" %}
|
62
|
+
</button>
|
63
|
+
</div>
|
64
|
+
</td>
|
65
|
+
</tr>
|
66
|
+
</tbody>
|
67
|
+
{% endif %}
|
68
|
+
</table>
|
69
|
+
|
70
|
+
{% include "unfold_crispy/inputs.html" %}
|
71
|
+
</div>
|
72
|
+
|
73
|
+
{% if formset_tag %}
|
74
|
+
</form>
|
75
|
+
{% endif %}
|
76
|
+
|
77
|
+
{% if form_add and formset_id %}
|
78
|
+
<script>
|
79
|
+
document.getElementById('{{ formset_id }}-add-row-button').addEventListener('click', function() {
|
80
|
+
const formTotalEl = document.querySelector(`#{{ formset_id }} input[name*="TOTAL_FORMS"]`)
|
81
|
+
const formCount = parseInt(formTotalEl.value);
|
82
|
+
const newForm = document.querySelector('#{{ formset_id }} .empty-form').cloneNode(true);
|
83
|
+
|
84
|
+
newForm.classList.remove('empty-form');
|
85
|
+
newForm.innerHTML = newForm.innerHTML.replaceAll(/__prefix__/g, formCount);
|
86
|
+
|
87
|
+
document.getElementById('{{ formset_id }}-rows').insertBefore(
|
88
|
+
newForm,
|
89
|
+
document.getElementById("#{{ formset_id }}-add-row-wrapper")
|
90
|
+
)
|
91
|
+
|
92
|
+
formTotalEl.value = formCount + 1;
|
93
|
+
|
94
|
+
newForm.dispatchEvent(new CustomEvent('formset:added', {
|
95
|
+
bubbles: true
|
96
|
+
}));
|
97
|
+
});
|
98
|
+
</script>
|
99
|
+
{% endif %}
|
100
|
+
{% endspecialspaceless %}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
{% load crispy_forms_utils %}
|
2
|
+
|
3
|
+
{% specialspaceless %}
|
4
|
+
{% if include_media %}{{ form.media }}{% endif %}
|
5
|
+
{% if form_show_errors %}
|
6
|
+
{% include "unfold_crispy/errors.html" %}
|
7
|
+
{% endif %}
|
8
|
+
{% for field in form %}
|
9
|
+
{% include field_template %}
|
10
|
+
{% endfor %}
|
11
|
+
{% endspecialspaceless %}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
{% load crispy_forms_utils %}
|
2
|
+
|
3
|
+
{% specialspaceless %}
|
4
|
+
{% if form_tag %}<form {{ flat_attrs }} method="{{ form_method }}" {% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>{% endif %}
|
5
|
+
{% if form_method|lower == 'post' and not disable_csrf %}
|
6
|
+
{% csrf_token %}
|
7
|
+
{% endif %}
|
8
|
+
|
9
|
+
{% include "unfold_crispy/display_form.html" %}
|
10
|
+
|
11
|
+
{% include "unfold_crispy/inputs.html" %}
|
12
|
+
|
13
|
+
{% if form_tag %}</form>{% endif %}
|
14
|
+
{% endspecialspaceless %}
|
unfold/templatetags/unfold.py
CHANGED
@@ -8,7 +8,7 @@ from django.contrib.admin.views.main import ChangeList
|
|
8
8
|
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
|
9
9
|
from django.db.models import Model
|
10
10
|
from django.db.models.options import Options
|
11
|
-
from django.forms import BoundField, Field
|
11
|
+
from django.forms import BoundField, CheckboxSelectMultiple, Field
|
12
12
|
from django.http import HttpRequest
|
13
13
|
from django.template import Context, Library, Node, RequestContext, TemplateSyntaxError
|
14
14
|
from django.template.base import NodeList, Parser, Token, token_kwargs
|
@@ -18,7 +18,7 @@ from django.utils.safestring import SafeText, mark_safe
|
|
18
18
|
from unfold.components import ComponentRegistry
|
19
19
|
from unfold.dataclasses import UnfoldAction
|
20
20
|
from unfold.enums import ActionVariant
|
21
|
-
from unfold.widgets import UnfoldAdminSplitDateTimeWidget
|
21
|
+
from unfold.widgets import UnfoldAdminMoneyWidget, UnfoldAdminSplitDateTimeWidget
|
22
22
|
|
23
23
|
register = Library()
|
24
24
|
|
@@ -495,7 +495,7 @@ def action_item_classes(context: Context, action: UnfoldAction) -> str:
|
|
495
495
|
|
496
496
|
@register.filter
|
497
497
|
def changeform_data(adminform: AdminForm) -> str:
|
498
|
-
fields =
|
498
|
+
fields = {}
|
499
499
|
|
500
500
|
for fieldset in adminform:
|
501
501
|
for line in fieldset:
|
@@ -503,15 +503,19 @@ def changeform_data(adminform: AdminForm) -> str:
|
|
503
503
|
if isinstance(field.field, dict):
|
504
504
|
continue
|
505
505
|
|
506
|
-
if isinstance(
|
506
|
+
if isinstance(
|
507
|
+
field.field.field.widget, UnfoldAdminSplitDateTimeWidget
|
508
|
+
) or isinstance(field.field.field.widget, UnfoldAdminMoneyWidget):
|
507
509
|
for index, _widget in enumerate(field.field.field.widget.widgets):
|
508
|
-
fields
|
510
|
+
fields[
|
509
511
|
f"{field.field.name}{field.field.field.widget.widgets_names[index]}"
|
510
|
-
|
512
|
+
] = None
|
513
|
+
elif isinstance(field.field.field.widget, CheckboxSelectMultiple):
|
514
|
+
fields[field.field.name] = []
|
511
515
|
else:
|
512
|
-
fields
|
516
|
+
fields[field.field.name] = None
|
513
517
|
|
514
|
-
return mark_safe(json.dumps(
|
518
|
+
return mark_safe(json.dumps(fields))
|
515
519
|
|
516
520
|
|
517
521
|
@register.filter(takes_context=True)
|
@@ -524,7 +528,9 @@ def changeform_condition(field: BoundField) -> BoundField:
|
|
524
528
|
field.field.field.widget.widget.attrs["x-init"] = mark_safe(
|
525
529
|
f"const $ = django.jQuery; $(function () {{ const select = $('#{field.field.auto_id}'); select.on('change', (ev) => {{ {field.field.name} = select.val(); }}); }});"
|
526
530
|
)
|
527
|
-
elif isinstance(
|
531
|
+
elif isinstance(
|
532
|
+
field.field.field.widget, UnfoldAdminSplitDateTimeWidget
|
533
|
+
) or isinstance(field.field.field.widget, UnfoldAdminMoneyWidget):
|
528
534
|
for index, widget in enumerate(field.field.field.widget.widgets):
|
529
535
|
field_name = (
|
530
536
|
f"{field.field.name}{field.field.field.widget.widgets_names[index]}"
|
unfold/widgets.py
CHANGED
@@ -33,6 +33,20 @@ from django.utils.translation import gettext_lazy as _
|
|
33
33
|
|
34
34
|
from .exceptions import UnfoldException
|
35
35
|
|
36
|
+
BUTTON_CLASSES = [
|
37
|
+
"border",
|
38
|
+
"cursor-pointer",
|
39
|
+
"font-medium",
|
40
|
+
"px-3",
|
41
|
+
"py-2",
|
42
|
+
"rounded",
|
43
|
+
"text-center",
|
44
|
+
"whitespace-nowrap",
|
45
|
+
"bg-primary-600",
|
46
|
+
"border-transparent",
|
47
|
+
"text-white",
|
48
|
+
]
|
49
|
+
|
36
50
|
LABEL_CLASSES = [
|
37
51
|
"block",
|
38
52
|
"font-semibold",
|
@@ -106,7 +120,7 @@ TEXTAREA_CLASSES = [
|
|
106
120
|
TEXTAREA_EXPANDABLE_CLASSES = [
|
107
121
|
"block",
|
108
122
|
"field-sizing-content",
|
109
|
-
"
|
123
|
+
"min-h-[38px]",
|
110
124
|
]
|
111
125
|
|
112
126
|
SELECT_CLASSES = [
|
@@ -254,6 +268,28 @@ SWITCH_CLASSES = [
|
|
254
268
|
"dark:checked:bg-green-700",
|
255
269
|
]
|
256
270
|
|
271
|
+
FILE_CLASSES = [
|
272
|
+
"border",
|
273
|
+
"border-base-200",
|
274
|
+
"flex",
|
275
|
+
"grow",
|
276
|
+
"items-center",
|
277
|
+
"overflow-hidden",
|
278
|
+
"rounded",
|
279
|
+
"shadow-sm",
|
280
|
+
"max-w-2xl",
|
281
|
+
"focus-within:ring",
|
282
|
+
"group-[.errors]:border-red-600",
|
283
|
+
"group-[.errors]:focus-within:ring-red-200",
|
284
|
+
"focus-within:ring-primary-300",
|
285
|
+
"focus-within:border-primary-600",
|
286
|
+
"dark:border-base-700",
|
287
|
+
"dark:focus-within:border-primary-600",
|
288
|
+
"dark:focus-within:ring-primary-700",
|
289
|
+
"dark:group-[.errors]:border-red-500",
|
290
|
+
"dark:group-[.errors]:focus-within:ring-red-600/40",
|
291
|
+
]
|
292
|
+
|
257
293
|
|
258
294
|
class UnfoldAdminTextInputWidget(AdminTextInputWidget):
|
259
295
|
def __init__(self, attrs: Optional[dict[str, Any]] = None) -> None:
|
@@ -342,9 +378,11 @@ class UnfoldAdminEmailInputWidget(AdminEmailInputWidget):
|
|
342
378
|
class FileFieldMixin:
|
343
379
|
def get_context(self, name, value, attrs):
|
344
380
|
widget = super().get_context(name, value, attrs)
|
381
|
+
|
345
382
|
widget["widget"].update(
|
346
383
|
{
|
347
384
|
"class": " ".join([*CHECKBOX_CLASSES, *["form-check-input"]]),
|
385
|
+
"file_wrapper_class": " ".join(FILE_CLASSES),
|
348
386
|
"file_input_class": " ".join(
|
349
387
|
[
|
350
388
|
self.attrs.get("class", ""),
|
@@ -356,6 +394,7 @@ class FileFieldMixin:
|
|
356
394
|
),
|
357
395
|
}
|
358
396
|
)
|
397
|
+
|
359
398
|
return widget
|
360
399
|
|
361
400
|
|
@@ -390,6 +429,13 @@ class UnfoldAdminDateWidget(AdminDateWidget):
|
|
390
429
|
}
|
391
430
|
super().__init__(attrs=attrs, format=format)
|
392
431
|
|
432
|
+
class Media:
|
433
|
+
js = [
|
434
|
+
"admin/js/core.js",
|
435
|
+
"admin/js/calendar.js",
|
436
|
+
"admin/js/admin/DateTimeShortcuts.js",
|
437
|
+
]
|
438
|
+
|
393
439
|
|
394
440
|
class UnfoldAdminSingleDateWidget(AdminDateWidget):
|
395
441
|
template_name = "unfold/widgets/date.html"
|
@@ -430,6 +476,13 @@ class UnfoldAdminTimeWidget(AdminTimeWidget):
|
|
430
476
|
}
|
431
477
|
super().__init__(attrs=attrs, format=format)
|
432
478
|
|
479
|
+
class Media:
|
480
|
+
js = [
|
481
|
+
"admin/js/core.js",
|
482
|
+
"admin/js/calendar.js",
|
483
|
+
"admin/js/admin/DateTimeShortcuts.js",
|
484
|
+
]
|
485
|
+
|
433
486
|
|
434
487
|
class UnfoldAdminSingleTimeWidget(AdminTimeWidget):
|
435
488
|
template_name = "unfold/widgets/time.html"
|
@@ -502,6 +555,13 @@ class UnfoldAdminSplitDateTimeWidget(AdminSplitDateTime):
|
|
502
555
|
]
|
503
556
|
MultiWidget.__init__(self, widgets, attrs)
|
504
557
|
|
558
|
+
class Media:
|
559
|
+
js = [
|
560
|
+
"admin/js/core.js",
|
561
|
+
"admin/js/calendar.js",
|
562
|
+
"admin/js/admin/DateTimeShortcuts.js",
|
563
|
+
]
|
564
|
+
|
505
565
|
|
506
566
|
class UnfoldAdminSplitDateTimeVerticalWidget(AdminSplitDateTime):
|
507
567
|
template_name = "unfold/widgets/split_datetime_vertical.html"
|
@@ -599,6 +659,31 @@ class UnfoldAdminSelectWidget(Select):
|
|
599
659
|
super().__init__(attrs, choices)
|
600
660
|
|
601
661
|
|
662
|
+
class UnfoldAdminSelect2Widget(Select):
|
663
|
+
def __init__(self, attrs=None, choices=()):
|
664
|
+
if attrs is None:
|
665
|
+
attrs = {}
|
666
|
+
|
667
|
+
attrs["data-theme"] = "admin-autocomplete"
|
668
|
+
attrs["class"] = "unfold-admin-autocomplete admin-autocomplete"
|
669
|
+
|
670
|
+
super().__init__(attrs, choices)
|
671
|
+
|
672
|
+
class Media:
|
673
|
+
js = (
|
674
|
+
"admin/js/vendor/jquery/jquery.js",
|
675
|
+
"admin/js/vendor/select2/select2.full.js",
|
676
|
+
"admin/js/jquery.init.js",
|
677
|
+
"unfold/js/select2.init.js",
|
678
|
+
)
|
679
|
+
css = {
|
680
|
+
"screen": (
|
681
|
+
"admin/css/vendor/select2/select2.css",
|
682
|
+
"admin/css/autocomplete.css",
|
683
|
+
),
|
684
|
+
}
|
685
|
+
|
686
|
+
|
602
687
|
class UnfoldAdminSelectMultipleWidget(SelectMultiple):
|
603
688
|
def __init__(self, attrs=None, choices=()):
|
604
689
|
if attrs is None:
|
File without changes
|
File without changes
|