From 43ab971f286956191b9f897d52744c22570f0a6a Mon Sep 17 00:00:00 2001 From: Ester Beltrami Date: Thu, 21 May 2026 15:08:37 +0100 Subject: [PATCH 1/2] feat(grants): add needs/visa/accommodation/country-type filters to shortlist Add four new client-side filters to the grants review shortlist page: needs_funds_for_travel, need_visa, need_accommodation, and country_type. Refactors the existing status-only filter JS into a unified applyFilters() function that ANDs all active filter groups together. --- backend/reviews/adapters.py | 1 + .../reviews/templates/grants-shortlist.html | 125 ++++++++++++++---- 2 files changed, 98 insertions(+), 28 deletions(-) diff --git a/backend/reviews/adapters.py b/backend/reviews/adapters.py index e07863e137..68df670bf2 100644 --- a/backend/reviews/adapters.py +++ b/backend/reviews/adapters.py @@ -467,6 +467,7 @@ def get_shortlist_context( all_reimbursement_categories=GrantReimbursementCategory.objects.for_conference( conference=review_session.conference ), + country_type_choices=Grant.CountryType.choices, review_session=review_session, title="Shortlist", ) diff --git a/backend/reviews/templates/grants-shortlist.html b/backend/reviews/templates/grants-shortlist.html index 7c2d883b7e..e66b64941f 100644 --- a/backend/reviews/templates/grants-shortlist.html +++ b/backend/reviews/templates/grants-shortlist.html @@ -452,34 +452,41 @@ }); }); - const filterByStatusInputs = [...document.querySelectorAll('input[name="filter-by-status"]')]; - filterByStatusInputs.forEach( - filterByStatusInput => { - filterByStatusInput.addEventListener('change', e => { - e.preventDefault(); - - const filterValue = e.target.value; - const visibleStatuses = filterByStatusInputs.filter( - input => input.checked - ).map( - input => input.value - ); - - document.querySelectorAll('.grant-item').forEach( - grantRow => { - const grantId = parseInt(grantRow.id.split('-')[1], 10); - const grantData = grantsById[grantId]; - - if (visibleStatuses.includes(grantData.originalStatus)) { - grantRow.classList.remove('hidden') - } else { - grantRow.classList.add('hidden') - } - } - ); - }); - } - ); + const getCheckedValues = (name) => + [...document.querySelectorAll(`input[name="${name}"]`)] + .filter(i => i.checked) + .map(i => i.value); + + const applyFilters = () => { + const visibleStatuses = getCheckedValues('filter-by-status'); + const visibleNeedsFunds = getCheckedValues('filter-needs-funds-for-travel'); + const visibleNeedVisa = getCheckedValues('filter-need-visa'); + const visibleNeedAccommodation = getCheckedValues('filter-need-accommodation'); + const visibleCountryTypes = getCheckedValues('filter-country-type'); + + document.querySelectorAll('.grant-item').forEach(grantRow => { + const matches = + visibleStatuses.includes(grantRow.dataset.originalStatus) && + visibleNeedsFunds.includes(grantRow.dataset.needsFundsForTravel) && + visibleNeedVisa.includes(grantRow.dataset.needVisa) && + visibleNeedAccommodation.includes(grantRow.dataset.needAccommodation) && + visibleCountryTypes.includes(grantRow.dataset.countryType); + + grantRow.classList.toggle('hidden', !matches); + }); + }; + + [ + 'input[name="filter-by-status"]', + 'input[name="filter-needs-funds-for-travel"]', + 'input[name="filter-need-visa"]', + 'input[name="filter-need-accommodation"]', + 'input[name="filter-country-type"]', + ].forEach(selector => { + document.querySelectorAll(selector).forEach(input => { + input.addEventListener('change', applyFilters); + }); + }); @@ -663,6 +670,60 @@

{% endfor %} +
+

Needs funds for travel:

+
+ + +
+
+
+

Need visa:

+
+ + +
+
+
+

Need accommodation:

+
+ + +
+
+
+

Country type:

+
+ {% for ct in country_type_choices %} + + {% endfor %} + +
+
@@ -742,6 +803,10 @@

status: "{{ item.current_or_pending_status }}", originalStatus: "{{ item.current_or_pending_status }}", numOfVotes: {{item.userreview_set.count}}, + needsFundsForTravel: "{{ item.needs_funds_for_travel|yesno:'true,false' }}", + needVisa: "{{ item.need_visa|yesno:'true,false' }}", + needAccommodation: "{{ item.need_accommodation|yesno:'true,false' }}", + countryType: "{{ item.country_type|default:'' }}", }; data-num-of-votes="{{ item.userreview_set.count }}" data-score="{{ item.score|default_if_none:'-999' }}" data-std-dev="{{ item.std_dev|default_if_none:'-1' }}" + data-needs-funds-for-travel="{{ item.needs_funds_for_travel|yesno:'true,false' }}" + data-need-visa="{{ item.need_visa|yesno:'true,false' }}" + data-need-accommodation="{{ item.need_accommodation|yesno:'true,false' }}" + data-country-type="{{ item.country_type|default:'' }}" > {{ forloop.counter }} From 5a6991b228d26298b0d0d6a0e45419916be30c91 Mon Sep 17 00:00:00 2001 From: Ester Beltrami Date: Thu, 21 May 2026 15:20:06 +0100 Subject: [PATCH 2/2] fix(grants): address shortlist filter code review feedback - Remove dead fields from grantsById JS object; filters read from data-* attributes only, so the object fields were a false source-of-truth - Move the null country_type sentinel ("", "Unknown") into the adapter context so the template loop handles all options uniformly - Call applyFilters() on page load to sync rows with bfcache-restored state - Treat all-unchecked filter group as "show all" via matchesFilter helper, preventing silent blank tables when every option in a group is unchecked - Add three pytest tests: country_type_choices present in context, grant with no departure_country maps to the empty-string sentinel, and needs_funds_for_travel boolean is correctly exposed on queryset items --- backend/reviews/adapters.py | 2 +- .../reviews/templates/grants-shortlist.html | 25 +++-- backend/reviews/tests/test_admin.py | 100 +++++++++++++++++- 3 files changed, 109 insertions(+), 18 deletions(-) diff --git a/backend/reviews/adapters.py b/backend/reviews/adapters.py index 68df670bf2..2c05f8c2c9 100644 --- a/backend/reviews/adapters.py +++ b/backend/reviews/adapters.py @@ -467,7 +467,7 @@ def get_shortlist_context( all_reimbursement_categories=GrantReimbursementCategory.objects.for_conference( conference=review_session.conference ), - country_type_choices=Grant.CountryType.choices, + country_type_choices=[*Grant.CountryType.choices, ("", "Unknown")], review_session=review_session, title="Shortlist", ) diff --git a/backend/reviews/templates/grants-shortlist.html b/backend/reviews/templates/grants-shortlist.html index e66b64941f..e5a3a689ee 100644 --- a/backend/reviews/templates/grants-shortlist.html +++ b/backend/reviews/templates/grants-shortlist.html @@ -457,6 +457,10 @@ .filter(i => i.checked) .map(i => i.value); + // An empty checked list means the filter group is ignored (show all). + const matchesFilter = (checkedValues, rowValue) => + checkedValues.length === 0 || checkedValues.includes(rowValue); + const applyFilters = () => { const visibleStatuses = getCheckedValues('filter-by-status'); const visibleNeedsFunds = getCheckedValues('filter-needs-funds-for-travel'); @@ -466,11 +470,11 @@ document.querySelectorAll('.grant-item').forEach(grantRow => { const matches = - visibleStatuses.includes(grantRow.dataset.originalStatus) && - visibleNeedsFunds.includes(grantRow.dataset.needsFundsForTravel) && - visibleNeedVisa.includes(grantRow.dataset.needVisa) && - visibleNeedAccommodation.includes(grantRow.dataset.needAccommodation) && - visibleCountryTypes.includes(grantRow.dataset.countryType); + matchesFilter(visibleStatuses, grantRow.dataset.originalStatus) && + matchesFilter(visibleNeedsFunds, grantRow.dataset.needsFundsForTravel) && + matchesFilter(visibleNeedVisa, grantRow.dataset.needVisa) && + matchesFilter(visibleNeedAccommodation, grantRow.dataset.needAccommodation) && + matchesFilter(visibleCountryTypes, grantRow.dataset.countryType); grantRow.classList.toggle('hidden', !matches); }); @@ -488,6 +492,9 @@ }); }); + // Sync visible rows with any restored checkbox state (e.g. bfcache back-nav). + applyFilters(); + }; @@ -718,10 +725,6 @@

Country type:

{{ ct.1 }} {% endfor %} -

@@ -803,10 +806,6 @@

Country type:

status: "{{ item.current_or_pending_status }}", originalStatus: "{{ item.current_or_pending_status }}", numOfVotes: {{item.userreview_set.count}}, - needsFundsForTravel: "{{ item.needs_funds_for_travel|yesno:'true,false' }}", - needVisa: "{{ item.need_visa|yesno:'true,false' }}", - needAccommodation: "{{ item.need_accommodation|yesno:'true,false' }}", - countryType: "{{ item.country_type|default:'' }}", };