Skip to content

Add DALFRelatedFieldAjaxMulti for multi-select filtering (#15) - #22

Merged
vigo merged 9 commits into
mainfrom
issue-15
Jan 25, 2026
Merged

Add DALFRelatedFieldAjaxMulti for multi-select filtering (#15)#22
vigo merged 9 commits into
mainfrom
issue-15

Conversation

@vigo

@vigo vigo commented Jan 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add DALFRelatedFieldAjaxMulti class for multi-select filtering with AJAX support
  • Selected values use comma-separated format with __in lookup (e.g., ?tags__in=uuid1,uuid2)
  • Selected items display as pill-shaped tags with remove buttons
  • Fix clear button (×) incorrectly showing on non-AJAX filters
  • Fix Select2 container width consistency across all filter types
  • Add demo gif and documentation

Screenshots

Multi-select filter

Test plan

  • Single tag selection works
  • Multiple tag selection works
  • Tags display as pills with × remove button
  • Filter column doesn't shift when tags selected
  • Works in both light and dark mode
  • Facets/counts display correctly
  • Clear all button works

Closes #15

🤖 Generated with Claude Code

vigo and others added 7 commits January 25, 2026 11:04
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>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@codecov-commenter

codecov-commenter commented Jan 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (900f40d) to head (6a43b0c).
⚠️ Report is 51 commits behind head on main.

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     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread README.md

- 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

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

Capitalization/grammar: “He realized” should be “he realized”.

Suggested change
my dear friend [Bahattin Çiniç][bahattincinic]’s warning, He realized that the
my dear friend [Bahattin Çiniç][bahattincinic]’s warning, he realized that the

Copilot uses AI. Check for mistakes.
Comment thread src/dalf/admin.py

def queryset(self, _request, queryset):
if self.selected_values:
return queryset.filter(**{self.lookup_kwarg: self.selected_values})

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment thread src/dalf/admin.py
Comment on lines +143 to +147
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):

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.
Comment thread src/dalf/admin.py
Comment on lines +144 to +148
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})

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +22
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;
}
}

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
## Decisions

- **New class**: `DALFRelatedFieldAjaxMulti` (separate from existing)
- **URL format**: Comma-separated with `__in` lookup (`?category__id__in=5,8,12`)

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
- **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`)

Copilot uses AI. Check for mistakes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vigo
vigo merged commit ad5a1aa into main Jan 25, 2026
19 checks passed
@vigo
vigo deleted the issue-15 branch January 25, 2026 14:50
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.

[FEATURE] Multi Select List Filer

3 participants