Skip to content

Commit 5b47e23

Browse files
vigoclaude
andauthored
Fix DALFRelatedFieldAjax filter repopulation bug (#21)
* Fix DALFRelatedFieldAjax filter repopulation bug (#18) Pass selected_text from Python to template to avoid AJAX call that only returns first 20 results. Selected values beyond pagination limit now display correctly on page reload. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add Python 3.14 to CI workflow matrix Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Upgrade ruff 0.13.3 → 0.14.14 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Keep selected_value on DoesNotExist, use fallback label When the selected object is deleted, keep selected_value so the filter state matches the URL parameter. Use raw ID as fallback text so users can still clear the filter via Select2. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update CHANGELOG.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update README changelog with issue #18 fix Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 70409de commit 5b47e23

9 files changed

Lines changed: 94 additions & 25 deletions

File tree

.github/workflows/ruff.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
max-parallel: 4
1717
matrix:
18-
python-version: ["3.11", "3.12", "3.13"]
18+
python-version: ["3.11", "3.12", "3.13", "3.14"]
1919

2020
steps:
2121
- name: Check out repository

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
max-parallel: 4
1717
matrix:
18-
python-version: ["3.11", "3.12", "3.13"]
18+
python-version: ["3.11", "3.12", "3.13", "3.14"]
1919

2020
steps:
2121
- name: Check out repository

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Change Log
22

3+
**2026-01-24**
4+
5+
- Fix `DALFRelatedFieldAjax` filter repopulation bug - selected values beyond first 20 results now display correctly on page reload
6+
- Add Python 3.14 to CI workflow matrix
7+
- Upgrade ruff 0.13.3 → 0.14.14
8+
9+
---
10+
311
**2024-09-06**
412

513
- Fix dark-mode text color.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ rake upload:test # Upload package to test distro
277277

278278
## Change Log
279279

280+
**2026-01-24**
281+
282+
- Fix [DALFRelatedFieldAjax filter repopulation bug](https://github.com/vigo/django-admin-list-filter/issues/18) -
283+
selected values beyond pagination limit now display correctly on page reload
284+
280285
**2025-10-06**
281286

282287
- [Fix nested FK problem](https://github.com/vigo/django-admin-list-filter/issues/7), thanks to

requirements-dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ pytest==8.4.2
33
pytest-cov==7.0.0
44
pytest-django==4.11.1
55
pytest-factoryboy==2.8.1
6-
ruff==0.13.3
6+
ruff==0.14.14

src/dalf/admin.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ def __init__(self, field, request, params, model, model_admin, field_path):
8686
selected_value = originial_params.get(self.lookup_kwarg, [])
8787
self.selected_value = selected_value[0] if selected_value else None
8888

89+
self.selected_text = None
90+
if self.selected_value:
91+
try:
92+
related_model = field.remote_field.model
93+
obj = related_model.objects.get(pk=self.selected_value)
94+
self.selected_text = str(obj)
95+
except (related_model.DoesNotExist, ValueError):
96+
self.selected_text = str(self.selected_value)
97+
8998
def field_choices(self, _field, _request, _model_admin):
9099
return []
91100

@@ -97,4 +106,5 @@ def choices(self, changelist):
97106
yield {
98107
**self.custom_template_params,
99108
'selected_value': self.selected_value,
109+
'selected_text': self.selected_text,
100110
}

src/dalf/static/admin/js/django_admin_list_filter.js

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
$.fn.djangoAdminListFilterSelect2 = function() {
66
$.each(this, function(i, element) {
7-
const ajaxURL = $(element).data("ajax--url");
87
const appLabel = element.dataset.appLabel;
98
const modelName = element.dataset.modelName;
109
const fieldName = element.dataset.fieldName;
1110
const lookupKwarg = element.dataset.lookupKwarg;
12-
const selectedValue = $(element).prev('.djal-selected-value').val();
11+
const selectedValue = $(element).prevAll('.djal-selected-value').first().val();
12+
const selectedText = $(element).prevAll('.djal-selected-text').first().val();
1313

1414
$(element).select2({
1515
ajax: {
@@ -35,26 +35,9 @@
3535
window.location.href = navURL.href;
3636
});
3737

38-
if (selectedValue){
39-
$.ajax({
40-
url: ajaxURL,
41-
dataType: "json",
42-
data: {
43-
term: "",
44-
app_label: appLabel,
45-
model_name: modelName,
46-
field_name: fieldName
47-
},
48-
success: function(data){
49-
if (data.results.length > 0) {
50-
const selectedItem = data.results.find(item => item.id === selectedValue);
51-
if (selectedItem) {
52-
const selectedOption = new Option(selectedItem.text, selectedItem.id, true, true);
53-
$(element).append(selectedOption).trigger("change");
54-
}
55-
};
56-
}
57-
});
38+
if (selectedValue && selectedText) {
39+
const selectedOption = new Option(selectedText, selectedValue, true, true);
40+
$(element).append(selectedOption).trigger("change");
5841
}
5942

6043
});

src/dalf/templates/admin/filter/django_admin_list_filter_ajax.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<li>
99
{% with params=choices|last %}
1010
<input class="djal-selected-value" type="hidden" value="{{ params.selected_value|default_if_none:'' }}" />
11+
<input class="djal-selected-text" type="hidden" value="{{ params.selected_text|default_if_none:'' }}" />
1112
<select
1213
class="django-admin-list-filter-ajax"
1314
data-ajax--cache="true"

tests/testproject/testapp/tests.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,65 @@ def test_order_admin_nested_ajax_filter(admin_client):
197197
assert results
198198
supplier_names = set(Supplier.objects.values_list('name', flat=True))
199199
assert any(result['text'] in supplier_names for result in results)
200+
201+
202+
@pytest.mark.django_db
203+
def test_ajax_filter_repopulation_with_selected_value(admin_client):
204+
"""Test that selected_text is correctly passed for filter repopulation."""
205+
from .factories import CategoryFactory, PostFactory
206+
207+
category = CategoryFactory(name='SelectedCategory')
208+
PostFactory(category=category)
209+
210+
response = admin_client.get(
211+
reverse('admin:testapp_post_changelist'),
212+
{'category__id__exact': category.pk},
213+
)
214+
assert response.status_code == HTTPStatus.OK
215+
216+
changelist = response.context['cl']
217+
filter_specs = changelist.filter_specs
218+
219+
ajax_specs = [spec for spec in filter_specs if isinstance(spec, DALFRelatedFieldAjax)]
220+
category_spec = next((spec for spec in ajax_specs if spec.lookup_kwarg == 'category__id__exact'), None)
221+
assert category_spec is not None
222+
223+
filter_choices = list(category_spec.choices(changelist))
224+
custom_params = filter_choices[-1]
225+
226+
assert custom_params['selected_value'] == str(category.pk)
227+
assert custom_params['selected_text'] == 'SelectedCategory'
228+
229+
content = response.content.decode()
230+
assert 'value="SelectedCategory"' in content
231+
232+
233+
@pytest.mark.django_db
234+
def test_ajax_filter_with_deleted_selected_value(admin_client):
235+
"""Test that deleted selected values are gracefully handled."""
236+
from .factories import CategoryFactory, PostFactory
237+
238+
category = CategoryFactory(name='ToBeDeleted')
239+
PostFactory(category=category)
240+
deleted_pk = category.pk
241+
Post.objects.filter(category=category).delete()
242+
category.delete()
243+
244+
response = admin_client.get(
245+
reverse('admin:testapp_post_changelist'),
246+
{'category__id__exact': deleted_pk},
247+
)
248+
assert response.status_code == HTTPStatus.OK
249+
250+
changelist = response.context['cl']
251+
filter_specs = changelist.filter_specs
252+
253+
ajax_specs = [spec for spec in filter_specs if isinstance(spec, DALFRelatedFieldAjax)]
254+
category_spec = next((spec for spec in ajax_specs if spec.lookup_kwarg == 'category__id__exact'), None)
255+
assert category_spec is not None
256+
257+
filter_choices = list(category_spec.choices(changelist))
258+
custom_params = filter_choices[-1]
259+
260+
assert custom_params['selected_value'] == str(deleted_pk)
261+
assert custom_params['selected_text'] == str(deleted_pk)

0 commit comments

Comments
 (0)