Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: array widget #441

Merged
merged 6 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Did you decide to start using Unfold but you don't have time to make the switch
- **Dependencies:** completely based only on `django.contrib.admin`
- **Actions:** multiple ways how to define actions within different parts of admin
- **WYSIWYG:** built-in support for WYSIWYG (Trix)
- **Array widget:** built-in widget for `django.contrib.postgres.fields.ArrayField`
- **Filters:** custom dropdown, numeric, datetime, and text fields
- **Dashboard:** custom components for rapid dashboard development
- **Model tabs:** define custom tab navigations for models
Expand Down Expand Up @@ -297,9 +298,10 @@ def permission_callback(request):

from django import models
from django.contrib import admin
from django.contrib.postgres.fields import ArrayField
from django.db import models
from unfold.admin import ModelAdmin
from unfold.contrib.forms.widgets import WysiwygWidget
from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget


@admin.register(MyModel)
Expand All @@ -322,6 +324,9 @@ class CustomAdminClass(ModelAdmin):
formfield_overrides = {
models.TextField: {
"widget": WysiwygWidget,
},
ArrayField: {
"widget": ArrayWidget,
}
}
```
Expand Down
31 changes: 31 additions & 0 deletions src/unfold/contrib/forms/templates/unfold/forms/array.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% load i18n %}

<div class="flex flex-col gap-4" x-data="{items: []}">
{% for subwidget in widget.subwidgets %}
<div class="flex flex-row">
{% with widget=subwidget %}
{% include widget.template_name %}
{% endwith %}

<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">
<span class="material-symbols-outlined text-sm">delete</span>
</a>
</div>
{% endfor %}

<template x-for="(item, index) in items" :key="item.key">
<div class="flex flex-row">
{% include template.template_name with widget=template %}

<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">
<span class="material-symbols-outlined text-sm">delete</span>
</a>
</div>
</template>

<div class="flex flex-row">
<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">
{% trans "Add new item" %}
</div>
</div>
</div>
61 changes: 58 additions & 3 deletions src/unfold/contrib/forms/widgets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional, Union

from django.forms import Widget
from unfold.widgets import PROSE_CLASSES
from django.core.validators import EMPTY_VALUES
from django.forms import MultiWidget, Widget
from django.http import QueryDict
from django.utils.datastructures import MultiValueDict
from unfold.widgets import PROSE_CLASSES, UnfoldAdminTextInputWidget

WYSIWYG_CLASSES = [
*PROSE_CLASSES,
Expand All @@ -22,6 +25,58 @@
]


class ArrayWidget(MultiWidget):
template_name = "unfold/forms/array.html"
widget_class = UnfoldAdminTextInputWidget

def __init__(self, *args: Any, **kwargs: Any) -> None:
widgets = [self.widget_class]
super().__init__(widgets)

def get_context(self, name: str, value: str, attrs: Dict) -> Dict:
self._resolve_widgets(value)
context = super().get_context(name, value, attrs)
template_widget = UnfoldAdminTextInputWidget()
template_widget.name = name

context.update({"template": template_widget})
return context

def value_from_datadict(
self, data: QueryDict, files: MultiValueDict, name: str
) -> List:
values = []

for item in data.getlist(name):
if item not in EMPTY_VALUES:
values.append(item)

return values

def value_omitted_from_data(
self, data: QueryDict, files: MultiValueDict, name: str
) -> List:
return data.getlist(name) not in [[""], *EMPTY_VALUES]

def decompress(self, value: Union[str, List]) -> List:
if isinstance(value, List):
return value.split(",")

return []

def _resolve_widgets(self, value: Optional[Union[List, str]]) -> None:
if value is None:
value = []

elif isinstance(value, List):
self.widgets = [self.widget_class for item in value]
else:
self.widgets = [self.widget_class for item in value.split(",")]

self.widgets_names = ["" for i in range(len(self.widgets))]
self.widgets = [w() if isinstance(w, type) else w for w in self.widgets]


class WysiwygWidget(Widget):
template_name = "unfold/forms/wysiwyg.html"

Expand Down