feat: implement _parse_bool_nullable and _filter_boolean_radio helper
This commit is contained in:
@@ -6,7 +6,7 @@ from django.db import models
|
|||||||
from django.utils.safestring import SafeText, mark_safe
|
from django.utils.safestring import SafeText, mark_safe
|
||||||
|
|
||||||
from common.components.core import Component
|
from common.components.core import Component
|
||||||
from common.components.primitives import Div, Input, Label, Span
|
from common.components.primitives import Checkbox, Div, Input, Label, Radio, Span
|
||||||
from common.components.search_select import (
|
from common.components.search_select import (
|
||||||
DEFAULT_PREFETCH,
|
DEFAULT_PREFETCH,
|
||||||
FilterSelect,
|
FilterSelect,
|
||||||
@@ -43,6 +43,12 @@ _FILTER_CHECKBOX_CLASS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_FILTER_RADIO_CLASS = (
|
||||||
|
"rounded-full border-default-medium bg-neutral-secondary-medium "
|
||||||
|
"text-brand focus:ring-brand"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_FILTER_GRID_CLASS = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4"
|
_FILTER_GRID_CLASS = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4"
|
||||||
|
|
||||||
|
|
||||||
@@ -90,6 +96,24 @@ def _parse_bool(existing: dict, key: str) -> bool:
|
|||||||
return bool(field.get("value", False))
|
return bool(field.get("value", False))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_bool_nullable(existing: dict, key: str) -> bool | None:
|
||||||
|
"""Extract a nullable boolean value from a filter criterion."""
|
||||||
|
if key not in existing:
|
||||||
|
return None
|
||||||
|
field = existing[key]
|
||||||
|
if not isinstance(field, dict):
|
||||||
|
return None
|
||||||
|
val = field.get("value")
|
||||||
|
if val is None:
|
||||||
|
return None
|
||||||
|
if isinstance(val, str):
|
||||||
|
if val.lower() in ("true", "1", "yes"):
|
||||||
|
return True
|
||||||
|
if val.lower() in ("false", "0", "no"):
|
||||||
|
return False
|
||||||
|
return bool(val)
|
||||||
|
|
||||||
|
|
||||||
# ── FilterSelect adapters ────────────────────────────────────────────────────
|
# ── FilterSelect adapters ────────────────────────────────────────────────────
|
||||||
# Each list filter is a FilterSelect. Enum fields pre-render their small, fixed
|
# Each list filter is a FilterSelect. Enum fields pre-render their small, fixed
|
||||||
# option set; model-backed fields fetch from a search endpoint on demand, with
|
# option set; model-backed fields fetch from a search endpoint on demand, with
|
||||||
@@ -231,19 +255,26 @@ def _filter_field(label: str, widget, for_widget: str = None) -> SafeText:
|
|||||||
|
|
||||||
|
|
||||||
def _filter_checkbox(name: str, label: str, checked: bool) -> SafeText:
|
def _filter_checkbox(name: str, label: str, checked: bool) -> SafeText:
|
||||||
return Label(
|
"""Thin adapter mapping legacy checkbox filters to the generalized Checkbox primitive."""
|
||||||
attributes=[("class", "flex items-center gap-2 text-sm text-heading")],
|
return Checkbox(name=name, label=label, checked=checked)
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_boolean_radio(name: str, label: str, value: bool | None) -> SafeText:
|
||||||
|
"""Renders a filter-specific boolean radio button group with 'True' and 'False' options."""
|
||||||
|
return Div(
|
||||||
|
attributes=[("class", "flex flex-col gap-1")],
|
||||||
children=[
|
children=[
|
||||||
Input(
|
Span(
|
||||||
attributes=[
|
attributes=[("class", _FILTER_LABEL_CLASS)],
|
||||||
("type", "checkbox"),
|
children=[label],
|
||||||
("name", name),
|
),
|
||||||
("value", "1"),
|
Div(
|
||||||
*([("checked", "true")] if checked else []),
|
attributes=[("class", "flex items-center gap-4 h-9")],
|
||||||
("class", _FILTER_CHECKBOX_CLASS),
|
children=[
|
||||||
|
Radio(name=name, label="True", checked=value is True, value="true"),
|
||||||
|
Radio(name=name, label="False", checked=value is False, value="false"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
label,
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
from common.components.filters import _parse_bool, _parse_range
|
from common.components.filters import _parse_bool, _parse_range, _parse_bool_nullable
|
||||||
|
|
||||||
|
|
||||||
class ParseRangeTest(SimpleTestCase):
|
class ParseRangeTest(SimpleTestCase):
|
||||||
@@ -66,3 +66,23 @@ class ParseBoolTest(SimpleTestCase):
|
|||||||
|
|
||||||
def test_missing_value_in_field(self):
|
def test_missing_value_in_field(self):
|
||||||
self.assertFalse(_parse_bool({"field": {}}, "field"))
|
self.assertFalse(_parse_bool({"field": {}}, "field"))
|
||||||
|
|
||||||
|
|
||||||
|
class ParseBoolNullableTest(SimpleTestCase):
|
||||||
|
def test_missing_key(self):
|
||||||
|
self.assertIsNone(_parse_bool_nullable({}, "field"))
|
||||||
|
|
||||||
|
def test_null_value(self):
|
||||||
|
self.assertIsNone(_parse_bool_nullable({"field": None}, "field"))
|
||||||
|
self.assertIsNone(_parse_bool_nullable({"field": {}}, "field"))
|
||||||
|
|
||||||
|
def test_boolean_values(self):
|
||||||
|
self.assertTrue(_parse_bool_nullable({"field": {"value": True}}, "field"))
|
||||||
|
self.assertFalse(_parse_bool_nullable({"field": {"value": False}}, "field"))
|
||||||
|
|
||||||
|
def test_string_values(self):
|
||||||
|
self.assertTrue(_parse_bool_nullable({"field": {"value": "true"}}, "field"))
|
||||||
|
self.assertTrue(_parse_bool_nullable({"field": {"value": "1"}}, "field"))
|
||||||
|
self.assertFalse(_parse_bool_nullable({"field": {"value": "false"}}, "field"))
|
||||||
|
self.assertFalse(_parse_bool_nullable({"field": {"value": "0"}}, "field"))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user