-
-
Notifications
You must be signed in to change notification settings - Fork 99
[feat] Add Select2Widget for choice fields #254 #642
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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")); | ||
| }); | ||
| }); | ||
| 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) | ||||||||||||||||||
|
Comment on lines
+31
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Avoid mutating caller-provided Copy Suggested refactor def __init__(self, attrs=None, choices=()):
- attrs = attrs or {}
+ attrs = dict(attrs or {})
attrs["class"] = "ow-select2 {0}".format(attrs.get("class", "")).strip()
super().__init__(attrs, choices)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| 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") | ||
|
|
||
|
Comment on lines
+37
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add-page Selenium test should also assert Select2 initialization, not just CSS class. This test currently proves widget markup but not JS enhancement on add view. Please also assert the Select2 container is present on this path. Suggested test tightening 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")
+ self.wait_for_presence(By.CSS_SELECTOR, ".select2-container")Based on learnings: Features: Add tests for new features and ensure coverage does not decrease significantly; prefer Selenium browser tests for UI-impacting features. 🤖 Prompt for AI Agents |
||
| 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") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: openwisp/openwisp-utils
Length of output: 595
🌐 Web query:
Django admin formset:added event signature event target row Django 4.1💡 Result:
In Django 4.1 admin, the formset:added event is a JavaScript CustomEvent triggered when a new inline form row is added. Event signature: formset:added (dispatched on document). Event properties: - event.target: The newly added row element (HTMLElement). - event.detail.formsetName: The name of the formset (string). Usage example: document.addEventListener('formset:added', (event) => { if (event.detail.formsetName === 'my_formset') { // Access the new row const row = event.target; // Perform actions on the row } }); This changed in Django 4.1 from pre-4.1 jQuery events (event, $row, formsetName). For backward compatibility, check event.detail. Register in admin change_form.html by extending admin_change_form_document_ready block and including your JS.
Citations:
🏁 Script executed:
Repository: openwisp/openwisp-utils
Length of output: 450
🏁 Script executed:
Repository: openwisp/openwisp-utils
Length of output: 684
🏁 Script executed:
Repository: openwisp/openwisp-utils
Length of output: 494
Normalize the added formset row before calling
.find()for Django 4.1+ compatibility.The
formset:addedevent API changed in Django 4.1: the added row is now available viaevent.targetas a CustomEvent, not as a second parameter ($row). The current code will fail with$rowundefined and prevent Select2 initialization on dynamically added inline rows in Django 4.1+. The codebase supports Django >= 2.2 with no upper bound, so compatibility is required.🐛 Proposed cross-version fix
📝 Committable suggestion
🤖 Prompt for AI Agents