Conversation
New filter class that allows selecting multiple values with Select2. Uses Django's __in lookup with comma-separated values in URL. Features: - Multi-select dropdown with tags UI - Page reload preserves all selections - Graceful handling of deleted values Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add pill-shaped styling for selected tags with rounded corners - Fix vertical alignment of placeholder text - Allow container height to grow with multiple selections - Update README with multi-select usage examples - Update CHANGELOG with new feature Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change "All" option to have empty value so Select2 treats it as placeholder - Change placeholder text from "Filter" to localized "All" - This prevents the "×" clear button from appearing when no filter is applied Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The allowClear option was showing a clear button (×) even when "All" (no filter) was selected. Since "All" is already a selectable option that clears the filter, allowClear is unnecessary and misleading. This also fixes facets (counts) display which was broken when using empty value for the "All" option. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add width: 100% and box-sizing: border-box to all filter types (.django-admin-list-filter, -ajax, -ajax-multi) to ensure consistent width across all filter dropdowns. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Simplify and refine CSS for multi-select filter styling - Add clear button styling with hover states for light/dark modes - Fix tag pill padding and margins - Add demo gif showing multi-select functionality - Update README and CHANGELOG documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #22 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 3 3
Lines 47 89 +42
=========================================
+ Hits 47 89 +42
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Test against multiple Django versions (5.2.8, 5.2.9, 5.2.10, 6.0) combined with Python versions (3.11, 3.12, 3.13, 3.14). Django 6.0 is excluded for Python 3.11 as it requires 3.12+. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new Django admin list filter implementation to support multi-select AJAX autocompletion (comma-separated __in query param), along with UI/UX tweaks and documentation updates.
Changes:
- Introduce
DALFRelatedFieldAjaxMulti+ new template to support multi-select AJAX filtering and repopulation. - Extend JS/CSS to initialize Select2 multi-select and adjust clear button / width behavior.
- Add tests and docs/CHANGELOG/README updates for the new filter.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/dalf/admin.py |
Adds DALFRelatedFieldAjaxMulti filter implementation and selected-items repopulation logic. |
src/dalf/templates/admin/filter/django_admin_list_filter_ajax_multi.html |
New template for AJAX multi-select filter UI. |
src/dalf/templates/admin/filter/django_admin_list_filter.html |
Adds Select2 width hint (data-width="100%") for consistent sizing. |
src/dalf/static/admin/js/django_admin_list_filter.js |
Adds Select2 initializer for multi-select AJAX filters and adjusts non-AJAX init. |
src/dalf/static/admin/css/django_admin_list_filter.css |
Updates styling for clear button and multi-select “pill” spacing. |
tests/testproject/testapp/admin.py |
Updates demo admin to include the new multi-select filter. |
tests/testproject/testapp/tests.py |
Adds test coverage for multi-select filtering and repopulation behaviors. |
docs/plans/2026-01-25-multi-select-filter-design.md |
Documents intended design/URL format for multi-select filter. |
README.md |
Documents the new filter, adds demo GIF, updates changelog section formatting/content. |
CHANGELOG.md |
Adds an entry describing the new multi-select feature and related fixes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| - Now package is working fine :) Thanks to [Bahattin][bahattincinic]! | ||
| Now package is working fine :) Thanks to [Bahattin][bahattincinic]! Thanks to | ||
| my dear friend [Bahattin Çiniç][bahattincinic]’s warning, He realized that the |
There was a problem hiding this comment.
Capitalization/grammar: “He realized” should be “he realized”.
| my dear friend [Bahattin Çiniç][bahattincinic]’s warning, He realized that the | |
| my dear friend [Bahattin Çiniç][bahattincinic]’s warning, he realized that the |
|
|
||
| def queryset(self, _request, queryset): | ||
| if self.selected_values: | ||
| return queryset.filter(**{self.lookup_kwarg: self.selected_values}) |
There was a problem hiding this comment.
DALFRelatedFieldAjaxMulti.queryset() filters a ManyToMany field via __in but doesn’t ensure the queryset is distinct(). For M2M joins this can return duplicate rows (e.g., a Post matching multiple selected tags shows up multiple times / incorrect counts). Consider setting self.use_distinct = True (or returning queryset.distinct() when field.many_to_many) when applying the filter.
| return queryset.filter(**{self.lookup_kwarg: self.selected_values}) | |
| queryset = queryset.filter(**{self.lookup_kwarg: self.selected_values}) | |
| if getattr(self.field, 'many_to_many', False): | |
| queryset = queryset.distinct() | |
| return queryset |
| for val in self.selected_values: | ||
| try: | ||
| obj = related_model.objects.get(pk=val) | ||
| self.selected_items.append({'id': val, 'text': str(obj)}) | ||
| except (related_model.DoesNotExist, ValueError): |
There was a problem hiding this comment.
selected_items is built by doing related_model.objects.get(pk=val) inside a loop, which results in 1 query per selected value. This can become noticeably slow when many items are selected. Consider fetching all selected objects in a single query (e.g., in_bulk / filter(pk__in=...)) and then mapping back to the original order, filling in placeholders for any missing IDs.
| for val in self.selected_values: | |
| try: | |
| obj = related_model.objects.get(pk=val) | |
| self.selected_items.append({'id': val, 'text': str(obj)}) | |
| except (related_model.DoesNotExist, ValueError): | |
| # Fetch all related objects in a single query to avoid N+1 queries. | |
| objects_by_pk = related_model.objects.in_bulk(self.selected_values) | |
| for val in self.selected_values: | |
| obj = objects_by_pk.get(val) | |
| if obj is not None: | |
| self.selected_items.append({'id': val, 'text': str(obj)}) | |
| else: | |
| # Fallback for missing or invalid IDs. |
| try: | ||
| obj = related_model.objects.get(pk=val) | ||
| self.selected_items.append({'id': val, 'text': str(obj)}) | ||
| except (related_model.DoesNotExist, ValueError): | ||
| self.selected_items.append({'id': val, 'text': val}) |
There was a problem hiding this comment.
The exception handling for related_model.objects.get(pk=val) only catches DoesNotExist and ValueError. For UUID primary keys, invalid values typically raise django.core.exceptions.ValidationError, which would currently bubble up as a 500. Consider catching ValidationError as well and falling back to displaying the raw value (similar to the deleted-value path).
| nav#changelist-filter { | ||
| .select2-selection__clear { | ||
| margin-top: 8px; | ||
| border: 1px solid #999; | ||
| border-radius: 4px; | ||
| line-height: 8px; | ||
| font-size: 8px; | ||
| padding: 1.5px 3px 2.2px; | ||
| font-weight: normal !important; | ||
| &:hover { | ||
| background-color: #eee; | ||
| } | ||
|
|
||
| html[data-theme="dark"] & { | ||
| &:hover { background-color: #444; } | ||
| } | ||
|
|
||
| } | ||
| .select2-selection__choice { | ||
| padding: 0 6px !important; | ||
| } | ||
| } |
There was a problem hiding this comment.
This stylesheet now uses nested selectors and & (e.g. nav#changelist-filter { .select2-selection__clear { ... &:hover { ... } } }), which isn’t valid in plain CSS in many environments and will be ignored unless you rely on very recent CSS Nesting support or a build step. Since this file is shipped as static .css, consider rewriting these rules as fully-qualified selectors (no nesting/&) or adding a preprocessing step.
| ## Decisions | ||
|
|
||
| - **New class**: `DALFRelatedFieldAjaxMulti` (separate from existing) | ||
| - **URL format**: Comma-separated with `__in` lookup (`?category__id__in=5,8,12`) |
There was a problem hiding this comment.
The design doc’s URL example uses ?category__id__in=..., but the implementation sets lookup_kwarg = f'{field_path}__in' (so a FK like category would use category__in, not category__id__in, unless users pass field_path='category__id'). Consider updating the example to match the actual parameter format used by the new filter.
| - **URL format**: Comma-separated with `__in` lookup (`?category__id__in=5,8,12`) | |
| - **URL format**: Comma-separated with `__in` lookup (`?category__in=5,8,12`) |
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
DALFRelatedFieldAjaxMulticlass for multi-select filtering with AJAX support__inlookup (e.g.,?tags__in=uuid1,uuid2)Screenshots
Test plan
Closes #15
🤖 Generated with Claude Code