Skip to content
Open
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
12 changes: 12 additions & 0 deletions openwisp_utils/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.core.exceptions import FieldError
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from openwisp_utils.widgets import Select2Widget


class TimeReadonlyAdminMixin(object):
Expand Down Expand Up @@ -217,3 +218,14 @@ def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj, **kwargs)
formset.help_text = self.help_text
return formset


class Select2AdminMixin:
"""Mixin that applies Select2Widget to specified choice fields."""

select2_fields = ()

def formfield_for_choice_field(self, db_field, request, **kwargs):
if db_field.name in self.select2_fields:
kwargs["widget"] = Select2Widget()
return super().formfield_for_choice_field(db_field, request, **kwargs)
18 changes: 18 additions & 0 deletions openwisp_utils/static/openwisp-utils/js/select2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use strict";

django.jQuery(function ($) {
function initSelect2($element) {
$element.not('select[name*="__prefix__"]').each(function () {
var $el = $(this);
if (!$el.hasClass("select2-hidden-accessible")) {
$el.select2();
}
});
}

initSelect2($("select.ow-select2"));

$(document).on("formset:added", function (event, $row) {
initSelect2($row.find("select.ow-select2"));
});
});
34 changes: 34 additions & 0 deletions openwisp_utils/widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from django.conf import settings
from django.contrib.admin.widgets import SELECT2_TRANSLATIONS
from django.forms import Media, Select
from django.utils.translation import get_language


class Select2Widget(Select):
"""Select2 autocomplete widget for Django ChoiceFields."""

@property
def media(self):
extra = "" if getattr(settings, "DEBUG", False) else ".min"
i18n_name = SELECT2_TRANSLATIONS.get(get_language())
i18n_file = (
("admin/js/vendor/select2/i18n/{0}.js".format(i18n_name),)
if i18n_name
else ()
)
return Media(
js=(
"admin/js/vendor/jquery/jquery{0}.js".format(extra),
"admin/js/vendor/select2/select2.full{0}.js".format(extra),
)
+ i18n_file
+ ("admin/js/jquery.init.js", "openwisp-utils/js/select2.js"),
css={
"screen": ("admin/css/vendor/select2/select2{0}.css".format(extra),),
},
)

def __init__(self, attrs=None, choices=()):
attrs = attrs or {}
attrs["class"] = "ow-select2 {0}".format(attrs.get("class", "")).strip()
super().__init__(attrs, choices)
4 changes: 3 additions & 1 deletion tests/test_project/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
HelpTextStackedInline,
ReadOnlyAdmin,
ReceiveUrlAdmin,
Select2AdminMixin,
TimeReadonlyAdminMixin,
UUIDAdmin,
)
Expand Down Expand Up @@ -110,7 +111,7 @@ class AutoOwnerFilter(AutocompleteFilter):


@admin.register(Shelf)
class ShelfAdmin(TimeReadonlyAdminMixin, admin.ModelAdmin):
class ShelfAdmin(Select2AdminMixin, TimeReadonlyAdminMixin, admin.ModelAdmin):
# DO NOT CHANGE: used for testing filters
list_filter = [
ShelfFilter,
Expand All @@ -121,6 +122,7 @@ class ShelfAdmin(TimeReadonlyAdminMixin, admin.ModelAdmin):
ReverseBookFilter,
]
search_fields = ["name"]
select2_fields = ("books_type",)


@admin.register(OrganizationRadiusSettings)
Expand Down
47 changes: 47 additions & 0 deletions tests/test_project/tests/test_widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from channels.testing import ChannelsLiveServerTestCase
from django.test import TestCase, tag
from django.urls import reverse
from openwisp_utils.widgets import Select2Widget
from selenium.webdriver.common.by import By

from ..models import Shelf
from .utils import SeleniumTestMixin


class TestWidgets(TestCase):
def test_select2_widget_attrs(self):
widget = Select2Widget()
html = widget.render("name", "value")
self.assertIn('class="ow-select2"', html)

# test overriding works and class is preserved
widget = Select2Widget(attrs={"class": "my-class"})
html = widget.render("name", "value")
self.assertIn("ow-select2 my-class", html)

def test_select2_widget_media(self):
widget = Select2Widget()
media = str(widget.media)
self.assertIn("admin/css/vendor/select2/select2", media)
self.assertIn("admin/js/vendor/jquery/jquery", media)
self.assertIn("admin/js/vendor/select2/select2.full", media)
self.assertIn("openwisp-utils/js/select2.js", media)


@tag("selenium_tests")
class TestSelect2AdminMixinSelenium(SeleniumTestMixin, ChannelsLiveServerTestCase):
def setUp(self):
super().setUp()
self.login()

def test_select2_widget_renders_on_shelf_add_form(self):
url = reverse("admin:test_project_shelf_add")
self.open(url)
self.wait_for_presence(By.CSS_SELECTOR, "select#id_books_type.ow-select2")

def test_select2_widget_renders_on_shelf_change_form(self):
shelf = Shelf.objects.create(name="Test Shelf", books_type="HORROR")
url = reverse("admin:test_project_shelf_change", args=[shelf.pk])
self.open(url)
self.wait_for_presence(By.CSS_SELECTOR, "select#id_books_type.ow-select2")
self.wait_for_presence(By.CSS_SELECTOR, ".select2-container")
Loading