Skip to content

feat: add dal_alight backend — autocomplete-light web component alternative to Select2#1411

Merged
jpic merged 64 commits into
yourlabs:masterfrom
PurpleToti:alight-backend
May 21, 2026
Merged

feat: add dal_alight backend — autocomplete-light web component alternative to Select2#1411
jpic merged 64 commits into
yourlabs:masterfrom
PurpleToti:alight-backend

Conversation

@PurpleToti
Copy link
Copy Markdown

Summary

This PR introduces dal_alight, a new backend for django-autocomplete-light that replaces Select2 with the autocomplete-light vanilla web component.

  • New app dal_alight with full parity to dal_select2: queryset views, grouped views, queryset-sequence, GenericForeignKey, Taggit, create_field, forwarding, list choices, and nested inline admin popups.
  • Test coverage: unit tests (test_units.py) and a full suite of Splinter/Selenium functional tests mirroring every select2_* test app — all passing in CI.
  • Documentation: new docs/alight.rst tutorial, docs/backends.rst comparison page (feature matrix, when-to-use, known gaps), and docs/index.rst entry.
  • No breaking changes: all existing dal_select2 tests and apps are untouched; the existing dal-django.js helper 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 pass
  • BROWSER=firefox MOZ_HEADLESS=1 pytest -v -k alight — all functional tests pass headless
  • cd docs && make html — docs build without Sphinx warnings
  • Existing select2 tests unchanged

🤖 Generated with Claude Code

archTortugax and others added 30 commits May 5, 2026 17:07
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>
Comment thread src/dal_alight/views.py Outdated
Comment thread src/dal_alight/widgets.py Outdated
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/dal_alight/widgets.py Outdated
return super().render(name, values, attrs=attrs, renderer=renderer)
finally:
self.choices = original_choices
return super().render(name, values, attrs=attrs, renderer=renderer)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not convinced by this class, what other options do we have?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread test_project/select2_forward_different_fields/forms.py
Comment thread test_project/select2_linked_data/test_views.py
Comment thread test_project/select2_secure_data/admin.py Outdated
archTortugax and others added 6 commits May 19, 2026 12:50
- 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>
@jpic
Copy link
Copy Markdown
Member

jpic commented May 20, 2026

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.

  • dal -> contains code generic to making an autocompolete in django
  • dal_select2 -> goes on top of that and adds select2-specific code
  • dal_alight -> goes on top of that and adds alight-specific code

Feel free to add extra methods in WidgetMixin so that they can be overridden by their select2 and alight counterparts though.

@jpic
Copy link
Copy Markdown
Member

jpic commented May 20, 2026

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

archTortugax and others added 5 commits May 20, 2026 10:46
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>
Comment thread src/dal/widgets.py Outdated
return mark_safe(widget + conf)
html = widget + conf
if getattr(self, 'component', None):
html = f'<{self.component}>{html}</{self.component}>'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really still need that ? I thought the AlightWidget would return the HTML with the component in its render() override

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread src/dal_alight/static/dal_alight/dal-django.js Outdated
return base
},
configurable: true,
})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we add that in the web component?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread src/dal_alight/static/dal_alight/dal-django.js
Comment thread src/dal_alight/views.py Outdated
archTortugax and others added 2 commits May 21, 2026 12:34
- 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>
@PurpleToti
Copy link
Copy Markdown
Author

same code in dal_select2.fields and dal_alight.fields, move it to dal/fields.py

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.

@PurpleToti
Copy link
Copy Markdown
Author

Select2InitialRenderingMixin → WidgetMixin / AlightWidgetMixin needs a diet

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()`.

@jpic jpic merged commit 8d2289d into yourlabs:master May 21, 2026
8 checks passed
@PurpleToti PurpleToti deleted the alight-backend branch May 22, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants