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.
Files changed (39) hide show
  1. {django_unfold-0.54.0.dist-info → django_unfold-0.55.0.dist-info}/METADATA +24 -11
  2. {django_unfold-0.54.0.dist-info → django_unfold-0.55.0.dist-info}/RECORD +39 -17
  3. unfold/contrib/filters/admin/choice_filters.py +71 -32
  4. unfold/contrib/filters/admin/dropdown_filters.py +15 -1
  5. unfold/contrib/filters/admin/mixins.py +25 -0
  6. unfold/layout.py +23 -0
  7. unfold/sites.py +17 -6
  8. unfold/static/unfold/css/styles.css +1 -1
  9. unfold/static/unfold/fonts/material-symbols/LICENSE +202 -0
  10. unfold/static/unfold/fonts/material-symbols/Material-Symbols-Outlined.woff2 +0 -0
  11. unfold/static/unfold/js/alpine.js +2 -2
  12. unfold/static/unfold/js/select2.init.js +4 -0
  13. unfold/templates/unfold/components/button.html +1 -1
  14. unfold/templates/unfold/widgets/clearable_file_input.html +1 -1
  15. unfold/templates/unfold/widgets/clearable_file_input_small.html +1 -1
  16. unfold/templates/unfold_crispy/display_form.html +9 -0
  17. unfold/templates/unfold_crispy/errors.html +3 -0
  18. unfold/templates/unfold_crispy/field.html +23 -0
  19. unfold/templates/unfold_crispy/inputs.html +7 -0
  20. unfold/templates/unfold_crispy/layout/attrs.html +1 -0
  21. unfold/templates/unfold_crispy/layout/baseinput.html +9 -0
  22. unfold/templates/unfold_crispy/layout/button.html +1 -0
  23. unfold/templates/unfold_crispy/layout/buttonholder.html +3 -0
  24. unfold/templates/unfold_crispy/layout/checkbox.html +19 -0
  25. unfold/templates/unfold_crispy/layout/column.html +3 -0
  26. unfold/templates/unfold_crispy/layout/div.html +4 -0
  27. unfold/templates/unfold_crispy/layout/field_errors.html +7 -0
  28. unfold/templates/unfold_crispy/layout/fieldset.html +9 -0
  29. unfold/templates/unfold_crispy/layout/help_text.html +5 -0
  30. unfold/templates/unfold_crispy/layout/help_text_and_errors.html +3 -0
  31. unfold/templates/unfold_crispy/layout/radio_checkbox_select.html +21 -0
  32. unfold/templates/unfold_crispy/layout/row.html +3 -0
  33. unfold/templates/unfold_crispy/layout/table_inline_formset.html +100 -0
  34. unfold/templates/unfold_crispy/uni_form.html +11 -0
  35. unfold/templates/unfold_crispy/whole_uni_form.html +14 -0
  36. unfold/templatetags/unfold.py +15 -9
  37. unfold/widgets.py +86 -1
  38. {django_unfold-0.54.0.dist-info → django_unfold-0.55.0.dist-info}/LICENSE.md +0 -0
  39. {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,5 @@
1
+ {% if field.help_text %}
2
+ <div {% if field.id_for_label %}id="{{ field.id_for_label }}_helptext" {% endif %}class="leading-relaxed mt-2 text-xs">
3
+ {{ field.help_text|safe }}
4
+ </div>
5
+ {% endif %}
@@ -0,0 +1,3 @@
1
+ {% include 'unfold_crispy/layout/field_errors.html' %}
2
+
3
+ {% include 'unfold_crispy/layout/help_text.html' %}
@@ -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,3 @@
1
+ <div {% if div.css_id %}id="{{ div.css_id }}"{% endif %} class="flex flex-col gap-5 grow lg:flex-row {{ div.css_class|default:'' }}" {{ div.flat_attrs }}>
2
+ {{ fields|safe }}
3
+ </div>
@@ -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 %}
@@ -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(field.field.field.widget, UnfoldAdminSplitDateTimeWidget):
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.append(
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.append(field.field.name)
516
+ fields[field.field.name] = None
513
517
 
514
- return mark_safe(json.dumps(dict.fromkeys(fields, "")))
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(field.field.field.widget, UnfoldAdminSplitDateTimeWidget):
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
- "!max-w-2xl",
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: