Skip to content

Commit b34cc16

Browse files
authored
feat: array widget (#441)
1 parent 67d7a03 commit b34cc16

File tree

3 files changed

+95
-4
lines changed

3 files changed

+95
-4
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Did you decide to start using Unfold but you don't have time to make the switch
2828
- **Dependencies:** completely based only on `django.contrib.admin`
2929
- **Actions:** multiple ways how to define actions within different parts of admin
3030
- **WYSIWYG:** built-in support for WYSIWYG (Trix)
31+
- **Array widget:** built-in widget for `django.contrib.postgres.fields.ArrayField`
3132
- **Filters:** custom dropdown, numeric, datetime, and text fields
3233
- **Dashboard:** custom components for rapid dashboard development
3334
- **Model tabs:** define custom tab navigations for models
@@ -297,9 +298,10 @@ def permission_callback(request):
297298

298299
from django import models
299300
from django.contrib import admin
301+
from django.contrib.postgres.fields import ArrayField
300302
from django.db import models
301303
from unfold.admin import ModelAdmin
302-
from unfold.contrib.forms.widgets import WysiwygWidget
304+
from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
303305

304306

305307
@admin.register(MyModel)
@@ -322,6 +324,9 @@ class CustomAdminClass(ModelAdmin):
322324
formfield_overrides = {
323325
models.TextField: {
324326
"widget": WysiwygWidget,
327+
},
328+
ArrayField: {
329+
"widget": ArrayWidget,
325330
}
326331
}
327332
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

src/unfold/contrib/forms/widgets.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
from typing import Any, Dict, Optional
1+
from typing import Any, Dict, List, Optional, Union
22

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
58

69
WYSIWYG_CLASSES = [
710
*PROSE_CLASSES,
@@ -22,6 +25,58 @@
2225
]
2326

2427

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+
2580
class WysiwygWidget(Widget):
2681
template_name = "unfold/forms/wysiwyg.html"
2782

0 commit comments

Comments
 (0)