Skip to content

Commit

Permalink
add paper-checkbox-tree
Browse files Browse the repository at this point in the history
  • Loading branch information
pix666 committed May 25, 2024
1 parent e230647 commit 5503dc3
Show file tree
Hide file tree
Showing 16 changed files with 222 additions and 15 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Change Log

## [7.7.0](https://github.com/dldevinc/paper-admin/tree/v7.7.0) - 2024-05-25

### Features

- Migrate from the `multi.js` plugin for `<select multiple>` to the
[paper-checkbox-tree](https://github.com/dldevinc/paper-checkbox-tree) plugin.
- `FilteredSelectMultiple` widget renamed to `AdminCheckboxTree`.

## [7.6.4](https://github.com/dldevinc/paper-admin/tree/v7.6.4) - 2023-12-31

### Features
Expand Down
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Custom Django admin interface based on Bootstrap 4.
- [Fieldsets](#fieldsets)
- [Table rows](#table-rows)
- [Inline forms](#inline-forms)
- [Additional admin widgets](#Additional-admin-widgets)
- [Settings](#settings)
- [Additional References](#additional-References)

Expand Down Expand Up @@ -720,6 +721,75 @@ class TablularInlines(admin.TabularInline):
![image](https://user-images.githubusercontent.com/6928240/233794982-1ca7248a-dc54-48c4-aea2-44d0d973d083.png)
![image](https://user-images.githubusercontent.com/6928240/233795183-c056b82e-8b01-4f7d-9c09-65c0becbdc33.png)

## Additional Admin Widgets

You can enhance the Django admin interface with various custom widgets provided
by the `paper-admin` package.

### AdminSwitchInput

The `AdminSwitchInput` widget offers a toggle switch input field for boolean fields
in the Django admin interface. This provides a more user-friendly alternative
to the default checkbox input for handling boolean values.

```python
from django.contrib import admin
from django.db import models
from paper_admin.widgets import AdminSwitchInput
from .models import MyModel

class MyModelAdmin(admin.ModelAdmin):
formfield_overrides = {
models.BooleanField: {"widget": AdminSwitchInput},
}

admin.site.register(MyModel, MyModelAdmin)
```

![image](https://github.com/dldevinc/paper-admin/assets/6928240/98a4c4f7-7e63-43a9-9c15-42093f078f4a)

### AdminCheckboxSelectMultiple

The `AdminCheckboxSelectMultiple` widget improves multiple choice selection
by displaying options as checkboxes instead of a dropdown list.

![image](https://github.com/dldevinc/paper-admin/assets/6928240/754fc2ad-254d-4685-a3a2-b1f5149387fd)

### AdminCheckboxTree

The `AdminCheckboxTree` widget is an alternative to the `FilteredSelectMultiple` widget
used with `filter_horizontal` or `filter_vertical` options in Django. It arranges choices
in a hierarchical tree structure, making it ideal for managing hierarchical data like
categories or permissions.

```python
# custom_users/admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import User

from paper_admin.admin.widgets import AdminCheckboxTree


class CustomUserChangeForm(UserChangeForm):
class Meta:
widgets = {
"user_permissions": AdminCheckboxTree,
}


class CustomUserAdmin(UserAdmin):[index.js](paper_admin%2Fstatic%2Fpaper_admin%2Fsrc%2Fwidgets%2Fselect-multiple-field%2Findex.js)
form = CustomUserChangeForm


admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)
```

![image](https://github.com/dldevinc/paper-admin/assets/6928240/72766127-ccc6-4538-ac4f-5dae14a30e1f)

## Settings

`PAPER_FAVICON`<br>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"mini-css-extract-plugin": "^2.9.0",
"multi.js": "github:dldevinc/multi.js#f8c3e53",
"multiselect": "^0.9.12",
"paper-checkbox-tree": "^0.1.10",
"popper.js": "^1.16.1",
"postcss": "^8.4.38",
"postcss-loader": "^8.1.1",
Expand Down
19 changes: 17 additions & 2 deletions paper_admin/admin/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

__all__ = ["AdminIPInput", "AdminUUIDInput", "AdminSwitchInput", "AdminTextarea",
"AdminForeignKeyRawIdWidget", "AdminManyToManyRawIdWidget", "AdminCheckboxInput",
"AdminRadioSelect", "AdminCheckboxSelectMultiple", "FilteredSelectMultiple"]
"AdminRadioSelect", "AdminCheckboxSelectMultiple", "FilteredSelectMultiple",
"AdminCheckboxTree"]


class AdminIPInput(widgets.TextInput):
Expand Down Expand Up @@ -71,4 +72,18 @@ def __init__(self, attrs=None, choices=()):


class FilteredSelectMultiple(widgets.SelectMultiple):
template_name = "django/forms/widgets/select_multiple.html"
template_name = "django/forms/widgets/filtered_select_multiple.html"


class AdminCheckboxTree(widgets.SelectMultiple):
template_name = "django/forms/widgets/checkbox_tree.html"

def use_required_attribute(self, initial):
# Don't use the 'required' attribute because browser validation would
# require all checkboxes to be checked instead of at least one.
return False

def value_omitted_from_data(self, data, files, name):
# HTML checkboxes don't appear in POST data if not checked, so it's
# never known if the value is actually omitted.
return False
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@import "css/env";

.pct-tree {
width: 100%;
font-size: $input-font-size;
line-height: $input-line-height;
border-color: $input-border-color;
border-radius: $input-border-radius-lg;

@at-root .paper-widget--invalid & {
border-color: $input-invalid-border-color;
box-shadow: $input-invalid-box-shadow;
}
}

.pct-group__text {
color: $gray-700;
font-size: 16px;
font-weight: 500;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import XClass from "data-xclass";
import CheckboxTree from "paper-checkbox-tree";
import multi from "multi.js/dist/multi-es6.min.js";

// CSS
import "multi.js/src/multi.css";
import "./index.scss";
import "./filtered-select-multiple.scss";

XClass.register("select-multiple-field", {
import "paper-checkbox-tree/dist/style.css";
import "./checkbox-tree.scss";

XClass.register("filtered-select-multiple", {
init: function (element) {
const select = element.querySelector("select");
select &&
multi(select, {
hide_empty_groups: true
});
multi(select, {
hide_empty_groups: true
});
}
});

XClass.register("checkbox-tree", {
init: function (element) {
const select = element.querySelector("select");
select && new CheckboxTree(select);
}
});
19 changes: 19 additions & 0 deletions paper_admin/templates/django/forms/widgets/checkbox_tree.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div class="select-multiple-field" data-xclass="checkbox-tree">
<select name="{{ widget.name }}" {% include "django/forms/widgets/attrs.html" %}>
{% for group_name, group_choices, group_index in widget.optgroups %}
{% if group_name %}
<optgroup label="{{ group_name }}">
{% endif %}

{% for option in group_choices %}
{% with widget=option %}
{% include option.template_name %}
{% endwith %}
{% endfor %}

{% if group_name %}
</optgroup>
{% endif %}
{% endfor %}
</select>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="select-multiple-field" data-xclass="select-multiple-field">
<div class="select-multiple-field" data-xclass="filtered-select-multiple">
<select name="{{ widget.name }}" {% include "django/forms/widgets/attrs.html" %}>
{% for group_name, group_choices, group_index in widget.optgroups %}
{% if group_name %}
Expand Down
10 changes: 8 additions & 2 deletions tests/app/admin/widgets.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from dal import autocomplete
from django import forms
from django.contrib import admin
from django.db import models
from django.utils.translation import gettext_lazy as _

from paper_admin.admin.widgets import AdminCheckboxSelectMultiple, AdminSwitchInput
from paper_admin.admin.widgets import (
AdminCheckboxSelectMultiple,
AdminCheckboxTree,
AdminSwitchInput,
)

from ..models import Widgets

Expand All @@ -19,6 +24,7 @@ class Meta:
"f_file_input": forms.FileInput,
"f_hidden": forms.HiddenInput,
"f_checkbox_m2m": AdminCheckboxSelectMultiple,
"f_checkbox_tree_m2m": AdminCheckboxTree,
"dal_fk": autocomplete.ModelSelect2(
url="app:ac-tag"
),
Expand Down Expand Up @@ -83,7 +89,7 @@ class WidgetsAdmin(admin.ModelAdmin):
(_("Many-to-Many"), {
"tab": "related-fields",
"fields": (
"f_m2m", "f_checkbox_m2m", "f_radio_m2m", "f_horizontal_m2m",
"f_m2m", "f_checkbox_m2m", "f_checkbox_tree_m2m", "f_horizontal_m2m", "f_radio_m2m",
),
}),
(_("Autocomplete"), {
Expand Down
23 changes: 23 additions & 0 deletions tests/app/migrations/0009_auto_20240525_1004.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.25 on 2024-05-25 10:04

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('app', '0008_alter_message_sender'),
]

operations = [
migrations.AddField(
model_name='widgets',
name='f_checkbox_tree_m2m',
field=models.ManyToManyField(blank=True, help_text='<code>ManyToManyField</code> with <code>AdminCheckboxTree</code> widget', related_name='_app_widgets_f_checkbox_tree_m2m_+', to='app.Tag', verbose_name='Checkbox Tree M2M'),
),
migrations.AlterField(
model_name='widgets',
name='f_checkbox_m2m',
field=models.ManyToManyField(blank=True, help_text='<code>ManyToManyField</code> with <code>AdminCheckboxSelectMultiple</code> widget', related_name='_app_widgets_f_checkbox_m2m_+', to='app.Tag', verbose_name='Checkbox M2M'),
),
]
15 changes: 11 additions & 4 deletions tests/app/models/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,14 +255,14 @@ class Widgets(models.Model):
verbose_name=_("Checkbox M2M"),
related_name="+",
blank=True,
help_text=_("<code>ManyToManyField</code> with <code>AdminCheckboxSelectMultiple</code>")
help_text=_("<code>ManyToManyField</code> with <code>AdminCheckboxSelectMultiple</code> widget")
)
f_radio_m2m = models.ManyToManyField(
f_checkbox_tree_m2m = models.ManyToManyField(
Tag,
verbose_name=_("Radio M2M"),
verbose_name=_("Checkbox Tree M2M"),
related_name="+",
blank=True,
help_text=_("Raw ID <code>ManyToManyField</code>")
help_text=_("<code>ManyToManyField</code> with <code>AdminCheckboxTree</code> widget")
)
f_horizontal_m2m = models.ManyToManyField(
Tag,
Expand All @@ -271,6 +271,13 @@ class Widgets(models.Model):
blank=True,
help_text=_("Horizontal <code>ManyToManyField</code>")
)
f_radio_m2m = models.ManyToManyField(
Tag,
verbose_name=_("Radio M2M"),
related_name="+",
blank=True,
help_text=_("Raw ID <code>ManyToManyField</code>")
)

# Autocomplete
f_ac_fk = models.ForeignKey(
Expand Down
3 changes: 2 additions & 1 deletion tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"logentry_admin",

"app",
"sortables"
"sortables",
"users"
]

MIDDLEWARE = [
Expand Down
Empty file added tests/users/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions tests/users/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import User

from paper_admin.admin.widgets import AdminCheckboxTree


class CustomUserChangeForm(UserChangeForm):
class Meta:
widgets = {
"user_permissions": AdminCheckboxTree,
}


class CustomUserAdmin(UserAdmin):
form = CustomUserChangeForm


admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4449,6 +4449,11 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==

paper-checkbox-tree@^0.1.10:
version "0.1.10"
resolved "https://registry.yarnpkg.com/paper-checkbox-tree/-/paper-checkbox-tree-0.1.10.tgz#b0239ed080cc80c070081e3318a1861de305a0d7"
integrity sha512-jiCCKqSvQd0Z4Qt1ky61u7wUyl1kN6FVijOpoDN1j9BedaObbYOV23JsiB2+a44Rzo5jKhtDfidCTC+A9KFDaw==

param-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
Expand Down

0 comments on commit 5503dc3

Please sign in to comment.