feat: add dal_alight backend — autocomplete-light web component alternative to Select2#1411
Conversation
yourlabs#1332) Port of upstream PR yourlabs#1332. Introduces dal_alight as a new optional autocomplete backend with no Select2 dependency. Opt-in via INSTALLED_APPS. Changes vs original PR: - AlightQuerySetView: fixed misleading comment, join list before HttpResponse, explicit content_type charset - AlightWidgetMixin: cleaned up string formatting - Migration regenerated against Django 5.2+ (original was Django 4.2.1) - ci.yml: added submodules: true to all checkout steps - alight_foreign_key added to tox test commands - pyproject.toml: no changes needed (src/ auto-discovery picks up dal_alight) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- views.py: use format_html + mark_safe(template output) instead of raw f-strings to prevent XSS when result labels contain HTML characters - widgets.py: use format_html for the autocomplete-select-input attribute and name interpolation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- exclude submodule static dir from ruff linting - empty placeholder files (admin, models, tests) had unused imports - fix import ordering in views, widgets, and alight_foreign_key app Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Point submodule to PurpleToti fork (latest CI cleanup commit de1d57f) - Add src/dal_alight/TODO.md listing concrete gaps to replace dal_select2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renamed four test apps to follow the consistent select2_* naming convention already used by the newer apps: linked_data → select2_linked_data forward_different_fields → select2_forward_different_fields rename_forward → select2_rename_forward secure_data → select2_secure_data Also pointed alight_foreign_key at select2_foreign_key's DB table (managed=False, db_table) so both backends share the same 53-row dataset for side-by-side testing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All compatible alight_* apps now point to their select2_* counterpart's
table via managed=False + db_table, so both backends read and write the
same data for side-by-side testing:
alight_one_to_one → select2_one_to_one_tmodel
alight_rename_forward → select2_rename_forward_tmodel
alight_secure_data → select2_secure_data_tmodel
alight_forward_different_fields → select2_forward_different_fields_tmodel
alight_djhacker_formfield → select2_djhacker_formfield_tmodel
alight_nestedadmin → select2_nestedadmin_tmodel{one,two,three}
alight_generic_foreign_key → select2_generic_foreign_key_tmodel
alight_taggit → select2_taggit_tmodel
Also renamed the four legacy tables that still carried pre-rename names
after the previous commit (linked_data_tmodel → select2_linked_data_tmodel, etc.)
Not shared (schema incompatible):
alight_many_to_many (different M2M symmetry, missing for_inline)
alight_linked_data (alight has group FK, select2 has owner FK)
alight_list (different field set)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… select2 counterparts Share DB tables with select2 apps via managed=False + db_table so both backends see the same data. Mirrors the schema (for_inline FK, owner FK, through model for M2M junction table), adds TestInline to admin, and updates forms/views to use owner-based forwarding in linked_data instead of group. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add for_inline field to alight_list form (parity with select2_list) - Switch alight_list from static fruit list to dynamic DB-driven choices (same table as select2_list via managed=False), so inline test fields reflect the same data in both admins - Migrations 0005-0007 restore own table then re-fuse with select2_list_tmodel - Mark alight_list and alight_linked_data as tested in TO_TEST.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- dal_alight_queryset_sequence: new package with AlightQuerySetSequenceView, QuerySetSequenceAlight/Multiple widgets, AlightGenericForeignKeyModelField; fix URL name collision with select2 sibling (alight_ prefix); fix KeyError on missing forwarded fields; fix ValueError on composite ctype-pk values in AlightInitialRenderMixin by bypassing pk__in for queryset-sequence widgets - dal_alight: add AlightGroupQuerySetView, AlightGroupListView, AlightListView, AlightQuerySetView with create_field; Alight/AlightMultiple/ListAlight/ TagAlight/TaggitAlight widgets; AlightListChoiceField/AlightListCreateChoiceField; AlightStory test helper - dal/autocomplete.py: export all new alight and queryset_sequence classes - dal-django.js (submodule): prefix-aware getFormPrefixes for nested inline forwarding; updateAlightRelatedLinks for view/change/delete buttons; drop debug console.log statements - pyproject.toml: add dal_alight_queryset_sequence to package discovery - GAPS.md, PLAN.md, TODO.md: implementation tracking docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sts done - alight_secure_data + select2_secure_data: guard get_queryset against AnonymousUser (was crashing with ValueError); add save_model to auto-assign owner=request.user on new records so they appear in the secure autocomplete without manual owner selection - TO_TEST.md: all alight features verified Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The autocomplete-light submodule is platform-agnostic; dal-django.js is
Django-specific so it must live in the main repo. Restructured static
layout: submodule now mounted at dal_alight/static/dal_alight/component/
(reference only), while autocomplete-light.{js,css} and dal-django.js
are regular tracked files at dal_alight/static/dal_alight/.
Also drop the non-existent dal-django.css from AlightWidgetMixin.media.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d CHANGELOG
Dev artifacts removed:
- src/dal_alight/{PLAN,TODO,GAPS}.md — internal planning notes, all done
- test_project/TO_TEST.md — manual QA checklist, all items checked off
- demo/ — standalone dev demo; coverage lives in test_project/alight_*/
- src/dal_alight/{admin,models}.py and migrations/ — empty placeholders,
no dal_alight models (parity with dal_select2)
CHANGELOG: expand 4.0.2 → 4.1.0, document full feature set
Code fixes:
- views.py: drop unused Sequence/mark_safe imports; use self.has_more()
instead of inline page_obj.has_next() guard
- widgets.py: add comment on AlightWidgetMixin.component (read by
WidgetMixin.render — removing it would break the <autocomplete-select>
wrap); simplify _iter_tag_values dead branch; note TagAlight intentionally
omits AlightInitialRenderMixin; fix pre-existing E501 line length errors
- dal_alight_queryset_sequence/widgets.py: drop AlightInitialRenderMixin from
both widget bases (it was included but immediately bypassed via super() —
dal_select2_queryset_sequence likewise omits Select2InitialRenderMixin);
delete the now-redundant render() overrides; move the pk__in explanation
to a module-level comment
- dal_alight_queryset_sequence/fields.py: super(Class, self) → super()
- tox.ini: add all 13 missing alight_* apps to the pytest command (only
alight_foreign_key was being collected; the rest were silently skipped)
- pyproject.toml: extend ruff exclude to cover
test_project/public/static/dal_alight (vendored component files were
not excluded despite the src/dal_alight/static/dal_alight entry)
- test_project/: fix ruff I001/F401 import issues in alight_* test files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… ViewMixin decoding Fixes: - test_renders_deck_div: widget emits <span slot="deck">, not <div>; fix assertion New test classes in alight_foreign_key/test_units.py: - AlightGroupQuerySetViewTest: group header rendered, parent name in header, data-value on grouped items, content-type, ImproperlyConfigured without group_by_related (uses self-referential test FK to avoid extra model) - AlightGroupListViewTest: group header, group names, items under group, ungrouped items appear before group headers, query filtering, tuple value/label, content-type - AlightListViewPostEdgeCasesTest: missing text → 400, create() returning None → 400, no create() method → ImproperlyConfigured - TaggitAlightTest: single-tag trailing comma, multi-tag no extra comma, plain string option_value, TaggedItem.tag.name unwrapping, empty value New file alight_linked_data/test_views.py (mirrors select2_linked_data/test_views.py): - 200 + HTML content-type with no forward params - forward not a dict → 400 "Not a JSON object" - forward invalid JSON → 400 "Invalid JSON data" - invalid HTTP method → 405 - forward owner pk filters queryset correctly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- alight_linked_data: rewrite test to use OwnedFixtures/owner filtering (test referenced non-existent Group model; app actually forwards owner) - select2_rename_forward: update import from linked_data → select2_linked_data after the select2_ rename refactor Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- alight_gfk/test_forms: use reverse('alight_TForm_autocomp_test') —
the alight field generates a URL name prefixed with 'alight_'
- alight_gfk/test_views: assert status 200 + HTML content-type instead
of .json() — AlightQuerySetSequenceView returns HTML, not JSON
- select2_linked_data/test_views: drop test_no_data; the view correctly
returns all objects when no owner is forwarded, but post_migrate seeds
4 rows so the empty-results assertion was always wrong (this test was
never in the tox command before the select2_ rename)
- stories.InlineSelectOption: scope input_selector to the field container
when case.input_in_field_container is True; add that flag to AlightStory
so alight inline tests enter text in the right widget (alight embeds
the input inside the component, unlike select2 whose search field lives
in a global dropdown)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tionMultiple InlineSelectOption now scopes input_selector when input_in_field_container=True (e.g. AlightStory). InlineSelectOptionMultiple called super() and then scoped again, producing a broken double-prefixed selector. Guard the extra scoping with the same flag so it only runs for backends where the parent didn't already scope (e.g. Select2Story). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A - stories.get_label(): return '' when deck is empty (unselect tests)
B - AlightStory: add create_option_selector (includes [data-create] items);
CreateOption.create_option() uses it so one-to-one create tests work
C - alight_many_to_many: remove custom through='TModelTest' that caused
Django admin to refuse M2M form widget; add migration 0004 to drop
TModelTest from migration state (auto-through reuses select2's table)
D - alight_linked_data: set owner before first toggle_autocomplete so the
forwarded-field filter is active from the start
- alight_rename_forward: same — set owner via JS before opening dropdown
E - alight_list: return static fruit list instead of querying empty DB
F - alight_tag: remove fill('name') — TModel has no name field
G - alight_taggit: create both labels[0] and labels[1] in setUp so both
tags exist when the test tries to select them
H - alight_nestedadmin: add wait loop after toggle_autocomplete to let
the 200ms debounce fire before reading window.forward_val
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tests - stories.py: delegate clean_label() to case.clean_label() so AlightStory's × stripping reaches MultipleMixin.get_labels() calls - stories.py: catch Exception (not IndexError) in is_searching() — Splinter 0.21 raises ElementDoesNotExist (not a subclass of IndexError) on empty list - stories.py: wait for create_sel CSS presence instead of exact text match — alight's create option text is 'Create "foo"' not 'foo', causing a 5-second implicit-wait timeout during which the dropdown could close - alight_list/urls.py: replace 'pineapple' with 'grape' — 'pineapple' contains 'ap', causing query 'ap' to return 3 results instead of 2 - alight_taggit/urls.py: override get_result_value to return tag name instead of PK; TaggitAlight stores tag names as option values so the autocomplete endpoint must return names to keep values consistent pre/post submit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…re empty The previous fix (except Exception → return True) caused is_searching() to return True for an empty ElementList, making the while loop spin forever when the dropdown returns zero results (e.g. filtered queryset with no matches). Replace with an explicit empty-list check: if not options → return False. This exits the while loop immediately, returning []. The tenacity retry in assert_suggestion_labels_are() (3s window) handles the "not loaded yet" case at the appropriate level. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t tests - autocomplete-light.js: abort in-flight XHRs on new input (readyState !== 4), preventing stale responses from overwriting the box - AlightStory: add toggle_autocomplete_widget that blurs before click so focus re-fires on an already-focused input, fixing refresh_autocomplete - BaseStory.toggle_autocomplete: delegate to toggle_autocomplete_widget when present, keeping select2 behaviour unchanged - BaseStory.assert_suggestion_labels_are: raise tenacity window 3s → 10s so a 5s implicit-wait find_by_css doesn't exhaust the retry budget - AlightStory: add get_create_option_selector and name-specific [data-create] selector so create_option never clicks a stale element - AutocompleteTestCase: add js_click for mousedown-before-focusout create option - Stories.create_option: use js_click + name-specific selector for alight only - alight_list: swap 'grape' for 'mango' (grape matched 'ap' filter, broke count) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…g, Select2 create flow - dal_alight/test.py: wait_script() now waits for data-bound on all autocomplete-select-input elements before proceeding, preventing toggle_autocomplete from firing before connectedCallback attaches the focus listener (fixes alight_linked_data and alight_secure_data). - alight_linked_data/test_functional.py: quote the attribute value in the set_owner() querySelector so hyphenated inline prefixes like [name="inline_test_models-0-owner"] are valid CSS selectors. - dal-django.js: fix getFieldValue to use !== '' instead of || so falsy-but-valid values (e.g. pk "0") are forwarded correctly (fixes alight_rename_forward). - stories.py: split create_option into two code paths — alight uses js_click + option-count wait (immune to focusout race), while Select2 restores the original is_element_present_by_text wait + native click + dropdown-close wait (fixes all 3 Select2 failures). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…length Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code fixes: - AlightGroupQuerySetView: annotate queryset before pagination via get_queryset override instead of calling .annotate() on the already-evaluated object_list - AlightInitialRenderMixin: build choices list once and derive dict from it, eliminating the double iteration of self.choices - AlightQuerySetSequenceAutoView: replace bare except IndexError with explicit len(model_args) > 2 guard to avoid silently swallowing unrelated errors - AlightWidgetMixin.render: build url_attr conditionally and use a single format_html call instead of two near-identical branches Tests: - Add unit tests for AlightQuerySetSequenceView (HTML grouping) and AlightQuerySetSequenceAutoView (forwarding logic) - Add AlightGroupListView URL and functional path test for alight_list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| url_attr = format_html(' url="{}"', self.url) if self.url else '' | ||
| input_el = format_html( | ||
| '<autocomplete-select-input slot="input"{}>' | ||
| '<input name="{}-input" slot="input" class="vTextField" placeholder="Search…" />' # noqa: E501 |
There was a problem hiding this comment.
Can we render a django forms Input widget inside here instead of hard-coding the input HTML?
Wondering if this could allow the user to customize the input widget, including the placeholder which needs to be translated, if possible, find a string that's already translated in Django itself so that we wouldn't have to maintain translations here
There was a problem hiding this comment.
Done. Now uses forms.TextInput(attrs={…}) rendered via input_widget.render(…, renderer=renderer). Placeholder is gettext_lazy('Search') — 'Search' is already present in Django's own admin translations, so no new strings to maintain.
| return super().render(name, values, attrs=attrs, renderer=renderer) | ||
| finally: | ||
| self.choices = original_choices | ||
| return super().render(name, values, attrs=attrs, renderer=renderer) |
There was a problem hiding this comment.
Not convinced by this class, what other options do we have?
There was a problem hiding this comment.
AlightInitialRenderMixin is gone. Its logic (narrowing the queryset to current values on render so only relevant <option> elements are emitted) was merged directly into AlightWidgetMixin.render(), guarded by a _narrow_choices_on_render flag. The flag is True only on ModelAlight and ModelAlightMultiple — this prevents GFK widgets (which use composite 'content_type_id-object_pk' values) from having their queryset filtered with those strings, which would crash. The mixin class is deleted.
- Remove submodule (.gitmodules + component dir); JS already in repo
- Remove submodules: true from all CI jobs
- Drop AlightInitialRenderMixin; merge into AlightWidgetMixin.render()
with _narrow_choices_on_render flag to guard GFK widgets
- Use forms.TextInput + gettext_lazy('Search') instead of hard-coded HTML
- Simplify AlightListChoiceField.__init__ to (*args, **kwargs)
- Keep AlightListCreateChoiceField.validate — MRO alone does not bypass
ChoiceField.validate; override is load-bearing for created values
- Return HTTP 405 instead of raising ImproperlyConfigured on POST without create
- Change create POST response from JSON to HTML fragment <div data-value>
- Update dal-django.js create handler to parse HTML fragment response
- Split CreateOption into AlightCreateOption / Select2CreateOption (+Multiple)
and update 9 functional test files to use backend-specific class
- Rename test_units tests to reflect 405 response and HTML response
- Restore test_no_data in select2_linked_data/test_views.py
- Remove accidental save_model override in select2_secure_data/admin.py
- docs: remove select2 mentions from alight.rst; add upgrade_from_select2_to_alight.rst
- docs: extract _forward.rst; update backends.rst, taggit.rst, conf.py (drop u'')
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add dal.forward automodule to api.rst (fixes :py:mod:`dal.forward` refs) - Add dal_alight_queryset_sequence automodule section to api.rst (fixes unresolved :py:class: refs in alight.rst, backends.rst, upgrade doc) - Add upgrade_from_select2_to_alight to index.rst toctree - Remove AlightInitialRenderMixin reference from alight.rst (class was deleted) - Fix :py:meth: ref to get_result_label: points to dal.views.BaseQuerySetView where the method is actually defined, not AlightQuerySetView - Fix pre-existing RST formatting in dal/forward.py docstrings (bullet list indentation, code-block indent, py:attribute body indent) - Replace :py:mod:`dal_queryset_sequence` with plain text in dal_alight_queryset_sequence docstring (package ref, not a module) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For FK/O2O/M2M create-on-the-fly flows the new option does not pre-exist
in <select>. The old code called this.select.value = value before creating
the option (silent no-op), then used option.setAttribute('selected', ...)
which only sets the default/reset state — option.selected stayed false so
option:checked never matched and assert_values / assert_value both failed.
Fix: ensure option exists first, then use option.selected = true (current
state), and for non-multiple also set select.value after the option is
guaranteed to be in the DOM.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BaseQuerySetView.post() returns JSON {id, text} which dal-django.js
cannot parse — it expects <div data-value="…">…</div>. Add a post()
override on AlightQuerySetView that returns the HTML fragment, so
choiceSelect() is actually called after a create-on-the-fly POST.
Validation errors now return HTTP 422 (dal-django.js ignores non-2xx).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AlightQuerySetView.post() now returns an HTML fragment instead of JSON, so the unit test that was asserting JSON keys is updated to check for the data-value attribute and label text instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Excellent work really. But I need some of the code to move to be in order: Select2InitialRenderingMixin -> I don't see anything that is specific to select2 here, as such, I believe this code should be in the global WidgetMixin, sorry I didn't note that when I reviewed 9eabac0 I guess I was so happy we didn't merge the ast eval ... As such, AlightWidgetMixin need a diet, because it also contains almost the same code as in Select2InitialRenderingMixin. All at logic about choices rendering is the responsibility of WidgetMixin, because it is generic and needed to make an autocomplete widget no matter what the backend is.
Feel free to add extra methods in WidgetMixin so that they can be overridden by their select2 and alight counterparts though. |
|
Also, I see the same code in dal_select2.fields and dal_alight.fields, as such, that code must move into dal/fields.py and be shared by both implementations |
Move all backend-agnostic logic out of dal_select2 and dal_alight and into dal so both backends build cleanly on top of the shared base: - dal/fields.py (new): ChoiceCallable, ListChoiceField, ListCreateChoiceField — shared by both backends via re-export aliases - dal/views.py: add _should_show_create() + case_sensitive_create to BaseQuerySetView; eliminates duplicate guard logic that existed in both Select2ViewMixin.get_create_option() and AlightQuerySetView._should_show_create() - dal/widgets.py: enhance WidgetMixin.filter_choices_to_render() to inject missing values for AJAX-backed plain-list widgets - dal_select2/widgets.py: delete Select2InitialRenderMixin (not select2-specific); drop it from four widget MROs - dal_alight/widgets.py: remove redundant queryset-narrowing block from AlightWidgetMixin.render() and _narrow_choices_on_render flag — already handled by WidgetMixin.optgroups() + QuerySetSelectMixin All 219 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reorganise index: alight tutorial first, then taggit/gfk, then select2 migration, then frontend comparison - Rename backends.rst → frontends.rst, s/backend/frontend/ everywhere - install.rst: configure dal_alight by default - tutorial.rst: retitle as "Select2 Tutorial" - gfk.rst: primary examples use alight classes - taggit.rst: alight sections are primary, select2 sections labelled (dal_select2) - Fix bare :: code blocks to .. code-block:: python/console for syntax highlighting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- frontends.rst: swap all table columns (alight first), reorder "When to use" sections (alight before select2), drop .. warning:: - taggit.rst: remove dal_select2 sections, alight sections are the default - README: mention dal_alight first in widget support feature list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused ChoiceCallable import and fix import order (ruff) - Make AlightList*Field and Select2List*Field proper subclasses so Sphinx can resolve :py:class: cross-references - Remove stale Select2InitialRenderMixin reference from tutorial Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| return mark_safe(widget + conf) | ||
| html = widget + conf | ||
| if getattr(self, 'component', None): | ||
| html = f'<{self.component}>{html}</{self.component}>' |
There was a problem hiding this comment.
Do we really still need that ? I thought the AlightWidget would return the HTML with the component in its render() override
There was a problem hiding this comment.
Agreed — removed. The if getattr(self, 'component', None) wrapping block and the component = 'autocomplete-select' class attribute have been removed. AlightWidgetMixin.render() now owns the full assembly: calls render_forward_conf() directly and returns mark_safe('<autocomplete-select>…</autocomplete-select>') with the forward conf div inside (required since dal-django.js queries .dal-forward-conf within the wrapper).
| return base | ||
| }, | ||
| configurable: true, | ||
| }) |
There was a problem hiding this comment.
Can't we add that in the web component?
There was a problem hiding this comment.
Done, with a small architectural note. Rather than patching AutocompleteSelectInput.prototype.url from outside (fragile — depends on property descriptor internals and whenDefined timing), dal-django.js now registers window.AutocompleteLightBuildForward = buildForward. The url getter in AutocompleteSelectInput calls this hook fresh on every request, appending &forward=… if the hook is present. This gives the component full ownership of URL construction while keeping the Django-specific forward logic in dal-django.js. A data-forward attribute approach was also tried but failed for dynamically-added inline rows (stale between events).
- WidgetMixin.render(): remove component-wrapping logic; AlightWidgetMixin.render() now owns the full <autocomplete-select> assembly including render_forward_conf() - BaseQuerySetView: replace post() with _post() containing shared logic; Select2QuerySetView and AlightQuerySetView each define their own post() that calls _post() and formats the response for their frontend (JSON vs HTML) - dal-django.js: remove AutocompleteLightDarkMode (CSS handles theming without JS); replace prototype-patch forward mechanism with window.AutocompleteLightBuildForward hook called fresh on every request; simplify dismissChangeRelatedObjectPopup to use new component choiceUpdate() method - autocomplete-light.js: AutocompleteSelectInput.url calls window.AutocompleteLightBuildForward for forward data; add choiceUpdate() to AutocompleteSelect - autocomplete-light.css: remove dead alight-dark/light-mode rules; simplify media query guard to :root:not([data-theme="light"]) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ValidationError and gettext_lazy were left over after moving post() logic into _post() and subclass post() methods. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Done. `ListChoiceField` and `ListCreateChoiceField` (including the `validate()` override that skips `ChoiceField.validate` for newly-created values) now live in `dal/fields.py`. Both `dal_alight/fields.py` and `dal_select2/fields.py` are thin pass-subclasses: `class AlightListChoiceField(ListChoiceField): pass` etc. |
Done. `Select2InitialRenderingMixin` has been absorbed — `WidgetMixin` now owns `filter_choices_to_render()` and `optgroups()`, and `QuerySetSelectMixin` overrides `filter_choices_to_render()` for queryset-backed widgets. `AlightInitialRenderMixin` was deleted entirely; its logic flows through the base `WidgetMixin.optgroups()`. Additionally, `AlightWidgetMixin` no longer holds the outer `` wrapping via a `component` attribute — `WidgetMixin.render()` has been cleaned of that logic, and `AlightWidgetMixin.render()` now owns the complete assembly including `render_forward_conf()`. |
Summary
This PR introduces
dal_alight, a new backend for django-autocomplete-light that replaces Select2 with theautocomplete-lightvanilla web component.dal_alightwith full parity todal_select2: queryset views, grouped views, queryset-sequence, GenericForeignKey, Taggit,create_field, forwarding, list choices, and nested inline admin popups.test_units.py) and a full suite of Splinter/Selenium functional tests mirroring everyselect2_*test app — all passing in CI.docs/alight.rsttutorial,docs/backends.rstcomparison page (feature matrix, when-to-use, known gaps), anddocs/index.rstentry.dal_select2tests and apps are untouched; the existingdal-django.jshelper is now vendored in-tree (moved out of the submodule).Test plan
cd test_project && pytest -v --liveserver 127.0.0.1:9999— all dal_alight unit tests passBROWSER=firefox MOZ_HEADLESS=1 pytest -v -k alight— all functional tests pass headlesscd docs && make html— docs build without Sphinx warningsselect2tests unchanged🤖 Generated with Claude Code