Skip to content

Fixes #366: Filter form and filterset support for boolean, select, and multiselect fields#479

Open
bctiemann wants to merge 2 commits intofeaturefrom
366-filter-form-boolean-select-multiselect
Open

Fixes #366: Filter form and filterset support for boolean, select, and multiselect fields#479
bctiemann wants to merge 2 commits intofeaturefrom
366-filter-form-boolean-select-multiselect

Conversation

@bctiemann
Copy link
Copy Markdown
Contributor

Summary

Closes #366.

  • BooleanFieldType: get_filterform_field() returns a NullBooleanField with a yes/no Select widget
  • SelectFieldType: get_filterform_field() returns a DynamicMultipleChoiceField backed by the choice set API URL, enabling multi-value selection
  • MultiSelectFieldType: Same as select — DynamicMultipleChoiceField backed by the choice set API
  • get_filterset_class(): Adds explicit MultipleChoiceFilter for select fields (OR/field__in semantics) and a custom OR-of-__contains filter for multiselect ArrayField columns, replacing the inadequate auto-generated CharFilter(exact)

Test plan

  • BooleanFieldFilterFormFieldTestCase — form field is NullBooleanField, not required, Select widget, choices include true/false
  • BooleanFieldFiltersetTestCase — ?active=true narrows to True-only rows; ?active=false to False-only; no param returns all
  • SelectFieldFilterFormFieldTestCase — form field is DynamicMultipleChoiceField, not required
  • SelectFieldFiltersetTestCase — single value exact match; multiple values OR; no param returns all
  • MultiSelectFieldFilterFormFieldTestCase — form field is DynamicMultipleChoiceField, not required
  • MultiSelectFieldFiltersetTestCase — array containment match; OR union across multiple values; no duplicates; no param returns all

🤖 Generated with Claude Code

@bctiemann
Copy link
Copy Markdown
Contributor Author

@claude /review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 21, 2026

Claude finished @bctiemann's task in 1m 51s —— View job


PR Review: Filter form and filterset support for boolean, select, and multiselect fields

  • Read changed files
  • Review field_types.py changes
  • Review filtersets.py changes
  • Review test coverage
  • Post review feedback

Overview

This PR correctly implements get_filterform_field() for BooleanFieldType, SelectFieldType, and MultiSelectFieldType, and adds explicit MultipleChoiceFilter registrations in get_filterset_class() for select/multiselect fields to replace the inadequate auto-generated CharFilter(exact). Test coverage is comprehensive and well-organized. The logic is sound overall, with a few things worth noting.


Issues / Observations

1. Boolean filterset relies entirely on auto-generated django-filters behavior

get_filterset_class() adds explicit filters for TYPE_SELECT and TYPE_MULTISELECT (lines 107–137), but does nothing for TYPE_BOOLEAN. The tests in BooleanFieldFiltersetTestCase pass {"active": "true"} and {"active": "false"} as strings, which works today only because django-filters auto-generates a BooleanFilter for BooleanField(null=True) — and that filter's underlying NullBooleanField.to_python() handles the string values.

This implicit reliance is fragile. A future django-filters version bump or change to the Meta.filter_overrides dict could silently break boolean filtering with no obvious cause. Adding an explicit filter, consistent with the select/multiselect approach, would make the intent clear:

# in get_filterset_class() alongside the select/multiselect block
elif co_field.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
    attrs[co_field.name] = django_filters.BooleanFilter(field_name=co_field.name)

2. Identical implementations in SelectFieldType and MultiSelectFieldType [field_types.py:354-416]

get_filterform_field() is copy-pasted identically between the two field types. A minimal refactor would be to push the shared logic to a base class or a helper, but this is a style nit and low priority.

3. No null-guard for co_field.choice_set in filtersets.py:112

choices = list(co_field.choice_set.choices)

If a TYPE_SELECT or TYPE_MULTISELECT field somehow lacks a choice_set (e.g., data migration artifact), this raises AttributeError during filterset class construction rather than at query time, making it hard to diagnose. A guard or a descriptive error would help:

if co_field.choice_set is None:
    continue
choices = list(co_field.choice_set.choices)

4. filter_overrides for ArrayField is now partially redundant [filtersets.py:48-54]

The Meta.filter_overrides dict still maps ArrayField → CharFilter(icontains). For multiselect fields with explicit MultipleChoiceFilter registered, the explicit filter wins. But the override still applies to any hypothetical future ArrayField that isn't a multiselect — which is a non-issue today but could surprise someone reading the code. A comment noting the relationship would help.

5. Missing widget type assertion in select/multiselect form field tests

SelectFieldFilterFormFieldTestCase and MultiSelectFieldFilterFormFieldTestCase assert the field type and required=False, but don't verify the widget is APISelectMultiple. The boolean tests check widget type (test_form_field_widget_is_select); consistency here would be nice. [Low priority]


Positives

  • The make_multiselect_filter() factory correctly captures fname by value, avoiding the classic closure capture bug — good.
  • Setting filter_multiselect.__name__ explicitly is required for django-filters method dispatch and is correctly done.
  • Test data isolation via separate choice set names per test class (e.g., "SelectFFChoices366" vs "SelectFSChoices366") avoids cross-test pollution.
  • test_filter_multiple_values_no_duplicates for multiselect is a thoughtful edge case that's easy to miss.
  • The no_filter_returns_all tests correctly verify no unintended filtering occurs on empty params.

Summary

The implementation is correct and the test coverage is solid. The main actionable item worth addressing before merge is item 1 (implicit boolean filter behavior) — it works today but is inconsistent with the explicit approach taken for select/multiselect and could silently regress. Items 2–5 are minor.

…, and multiselect fields

- BooleanFieldType.get_filterform_field() returns NullBooleanField with yes/no Select widget
- SelectFieldType.get_filterform_field() returns DynamicMultipleChoiceField backed by choice set API
- MultiSelectFieldType.get_filterform_field() returns DynamicMultipleChoiceField backed by choice set API
- get_filterset_class() adds MultipleChoiceFilter for select fields (OR/field__in semantics)
- get_filterset_class() adds custom OR-of-__contains filter for multiselect (ArrayField) fields
- Tests covering form field shape and filterset queryset filtering for all three types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bctiemann bctiemann force-pushed the 366-filter-form-boolean-select-multiselect branch from 2c37a52 to 8ec83d1 Compare April 21, 2026 18:22
NetBoxModelFilterSet adds a built-in 'tag' filter that generates a
tags__slug additional lookup. Naming the test multiselect field 'tags'
caused get_additional_lookups() to try resolving tags__slug on our
ArrayField, raising ValueError. Renaming to 'labels' avoids the clash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bctiemann
Copy link
Copy Markdown
Contributor Author

Item 1 is fixed.

@bctiemann bctiemann requested a review from arthanson April 21, 2026 18:49
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.

1 participant