Fold the six filter bars into a BaseComponent hierarchy
The *FilterBar family (FilterBar / SessionFilterBar / PurchaseFilterBar / DeviceFilterBar / PlatformFilterBar / PlayEventFilterBar) previously shared the collapsible chrome through a free `_filter_bar(fields, ...)` helper that each function called at the end. Replace that with a `_FilterBarBase` BaseComponent: it owns the chrome render() and declares `media = _FILTER_BAR_MEDIA`, and each bar is now a subclass implementing `build_fields()`. The per-entity field-building bodies move verbatim into module-level `_<entity>_fields(existing, ...)` functions that each subclass delegates to, so the large bodies are untouched (no reindentation) and the diff stays reviewable. Media still bubbles: BaseComponent.collect_media() merges the bar's own filter_bar.js with the search_select.js / range_slider.js / date_range_picker.js declared by the contained widgets. Call sites are unchanged — `FilterBar(filter_json=..., preset_list_url=...)` now instantiates a Node instead of calling a function, and both `str(bar)` and `collect_media(bar)` behave as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ from typing import NamedTuple
|
|||||||
from django.db import models
|
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 Element, Media
|
from common.components.core import BaseComponent, Element, Media, Node
|
||||||
from common.components.date_range_picker import DateRangePicker
|
from common.components.date_range_picker import DateRangePicker
|
||||||
from common.components.primitives import Checkbox, Div, Input, Label, Radio, Span
|
from common.components.primitives import Checkbox, Div, Input, Label, Radio, Span
|
||||||
from common.components.search_select import (
|
from common.components.search_select import (
|
||||||
@@ -695,10 +695,36 @@ def _filter_action_row(preset_list_url: str, preset_save_url: str) -> SafeText:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _filter_bar(fields, filter_json, preset_list_url, preset_save_url) -> SafeText:
|
class _FilterBarBase(BaseComponent):
|
||||||
"""Shared collapsible filter-bar chrome. `fields` is the per-entity body
|
"""Shared collapsible filter-bar chrome.
|
||||||
(grids, sliders, checkboxes); the shell adds the collapse toggle, the form,
|
|
||||||
the hidden filter-json input and the Apply/Clear/preset action row."""
|
Subclasses implement ``build_fields()`` returning the per-entity body
|
||||||
|
(grids, sliders, checkboxes); this base wraps it in the collapse toggle,
|
||||||
|
the form, the hidden filter-json input and the Apply/Clear/preset action
|
||||||
|
row. ``filter_bar.js`` (declared as this component's ``media``) wires the
|
||||||
|
chrome; widget media (search_select.js, range_slider.js,
|
||||||
|
date_range_picker.js) bubbles up from the contained widgets via the node
|
||||||
|
tree, so the view never threads ``scripts=`` by hand.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media = _FILTER_BAR_MEDIA
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
filter_json: str = "",
|
||||||
|
preset_list_url: str = "",
|
||||||
|
preset_save_url: str = "",
|
||||||
|
) -> None:
|
||||||
|
self.filter_json = filter_json
|
||||||
|
self.preset_list_url = preset_list_url
|
||||||
|
self.preset_save_url = preset_save_url
|
||||||
|
self.existing = _filter_parse(filter_json)
|
||||||
|
|
||||||
|
def build_fields(self) -> list:
|
||||||
|
"""Return the per-entity filter body. Implemented by each subclass."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def render(self) -> Node:
|
||||||
return Div(
|
return Div(
|
||||||
attributes=[("id", "filter-bar"), ("class", "mb-6")],
|
attributes=[("id", "filter-bar"), ("class", "mb-6")],
|
||||||
children=[
|
children=[
|
||||||
@@ -725,34 +751,48 @@ def _filter_bar(fields, filter_json, preset_list_url, preset_save_url) -> SafeTe
|
|||||||
("type", "hidden"),
|
("type", "hidden"),
|
||||||
("id", _FILTER_INPUT_ID),
|
("id", _FILTER_INPUT_ID),
|
||||||
("name", "filter"),
|
("name", "filter"),
|
||||||
# NB: Component escapes attribute values, so the
|
# NB: attribute values are escaped, so the
|
||||||
# raw JSON is passed through (no double-escape).
|
# raw JSON passes through (no double-escape).
|
||||||
("value", filter_json),
|
("value", self.filter_json),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
*fields,
|
*self.build_fields(),
|
||||||
_filter_action_row(preset_list_url, preset_save_url),
|
_filter_action_row(
|
||||||
|
self.preset_list_url, self.preset_save_url
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).with_media(_FILTER_BAR_MEDIA)
|
)
|
||||||
|
|
||||||
|
|
||||||
def FilterBar(
|
class FilterBar(_FilterBarBase):
|
||||||
|
"""Collapsible filter bar for the Game list."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
filter_json: str = "",
|
filter_json: str = "",
|
||||||
status_options: list[LabeledOption] | None = None,
|
status_options: list[LabeledOption] | None = None,
|
||||||
preset_list_url: str = "",
|
preset_list_url: str = "",
|
||||||
preset_save_url: str = "",
|
preset_save_url: str = "",
|
||||||
) -> SafeText:
|
) -> None:
|
||||||
"""Collapsible filter bar for the Game list."""
|
super().__init__(filter_json, preset_list_url, preset_save_url)
|
||||||
|
self.status_options = status_options
|
||||||
|
|
||||||
|
def build_fields(self) -> list:
|
||||||
|
return _game_fields(self.existing, self.status_options)
|
||||||
|
|
||||||
|
|
||||||
|
def _game_fields(
|
||||||
|
existing: dict, status_options: list[LabeledOption] | None = None
|
||||||
|
) -> list:
|
||||||
from games.models import Game, Purchase
|
from games.models import Game, Purchase
|
||||||
|
|
||||||
if status_options is None:
|
if status_options is None:
|
||||||
status_options = [(s.value, s.label) for s in Game.Status]
|
status_options = [(s.value, s.label) for s in Game.Status]
|
||||||
|
|
||||||
existing = _filter_parse(filter_json)
|
|
||||||
status_choice = _filter_get_choice(existing, "status")
|
status_choice = _filter_get_choice(existing, "status")
|
||||||
platform_choice = _filter_get_choice(existing, "platform")
|
platform_choice = _filter_get_choice(existing, "platform")
|
||||||
platform_group_choice = _filter_get_choice(existing, "platform_group")
|
platform_group_choice = _filter_get_choice(existing, "platform_group")
|
||||||
@@ -1064,7 +1104,7 @@ def FilterBar(
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def _find_label(options: list[LabeledOption], value: str) -> str:
|
def _find_label(options: list[LabeledOption], value: str) -> str:
|
||||||
@@ -1074,13 +1114,16 @@ def _find_label(options: list[LabeledOption], value: str) -> str:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def SessionFilterBar(
|
class SessionFilterBar(_FilterBarBase):
|
||||||
filter_json="", preset_list_url="", preset_save_url=""
|
|
||||||
) -> SafeText:
|
|
||||||
"""Collapsible filter bar for the Session list."""
|
"""Collapsible filter bar for the Session list."""
|
||||||
|
|
||||||
|
def build_fields(self) -> list:
|
||||||
|
return _session_fields(self.existing)
|
||||||
|
|
||||||
|
|
||||||
|
def _session_fields(existing: dict) -> list:
|
||||||
from games.models import Game, Session
|
from games.models import Game, Session
|
||||||
|
|
||||||
existing = _filter_parse(filter_json)
|
|
||||||
game_choice = _filter_get_choice(existing, "game")
|
game_choice = _filter_get_choice(existing, "game")
|
||||||
device_choice = _filter_get_choice(existing, "device")
|
device_choice = _filter_get_choice(existing, "device")
|
||||||
note_value = existing.get("note", {}).get("value", "")
|
note_value = existing.get("note", {}).get("value", "")
|
||||||
@@ -1178,18 +1221,21 @@ def SessionFilterBar(
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def PurchaseFilterBar(
|
class PurchaseFilterBar(_FilterBarBase):
|
||||||
filter_json="", preset_list_url="", preset_save_url=""
|
|
||||||
) -> SafeText:
|
|
||||||
"""Collapsible filter bar for the Purchase list."""
|
"""Collapsible filter bar for the Purchase list."""
|
||||||
|
|
||||||
|
def build_fields(self) -> list:
|
||||||
|
return _purchase_fields(self.existing)
|
||||||
|
|
||||||
|
|
||||||
|
def _purchase_fields(existing: dict) -> list:
|
||||||
from games.models import Purchase
|
from games.models import Purchase
|
||||||
|
|
||||||
type_options = Purchase.TYPES
|
type_options = Purchase.TYPES
|
||||||
ownership_options = Purchase.OWNERSHIP_TYPES
|
ownership_options = Purchase.OWNERSHIP_TYPES
|
||||||
existing = _filter_parse(filter_json)
|
|
||||||
game_choice = _filter_get_choice(existing, "games")
|
game_choice = _filter_get_choice(existing, "games")
|
||||||
platform_choice = _filter_get_choice(existing, "platform")
|
platform_choice = _filter_get_choice(existing, "platform")
|
||||||
type_choice = _filter_get_choice(existing, "type")
|
type_choice = _filter_get_choice(existing, "type")
|
||||||
@@ -1361,14 +1407,19 @@ def PurchaseFilterBar(
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def DeviceFilterBar(filter_json="", preset_list_url="", preset_save_url="") -> SafeText:
|
class DeviceFilterBar(_FilterBarBase):
|
||||||
"""Collapsible filter bar for the Device list."""
|
"""Collapsible filter bar for the Device list."""
|
||||||
|
|
||||||
|
def build_fields(self) -> list:
|
||||||
|
return _device_fields(self.existing)
|
||||||
|
|
||||||
|
|
||||||
|
def _device_fields(existing: dict) -> list:
|
||||||
from games.models import Device
|
from games.models import Device
|
||||||
|
|
||||||
existing = _filter_parse(filter_json)
|
|
||||||
type_options = Device.DEVICE_TYPES
|
type_options = Device.DEVICE_TYPES
|
||||||
type_choice = _filter_get_choice(existing, "type")
|
type_choice = _filter_get_choice(existing, "type")
|
||||||
|
|
||||||
@@ -1388,15 +1439,17 @@ def DeviceFilterBar(filter_json="", preset_list_url="", preset_save_url="") -> S
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def PlatformFilterBar(
|
class PlatformFilterBar(_FilterBarBase):
|
||||||
filter_json="", preset_list_url="", preset_save_url=""
|
|
||||||
) -> SafeText:
|
|
||||||
"""Collapsible filter bar for the Platform list."""
|
"""Collapsible filter bar for the Platform list."""
|
||||||
existing = _filter_parse(filter_json)
|
|
||||||
|
|
||||||
|
def build_fields(self) -> list:
|
||||||
|
return _platform_fields(self.existing)
|
||||||
|
|
||||||
|
|
||||||
|
def _platform_fields(existing: dict) -> list:
|
||||||
name_value = existing.get("name", {}).get("value", "")
|
name_value = existing.get("name", {}).get("value", "")
|
||||||
name_modifier = existing.get("name", {}).get("modifier", "EQUALS")
|
name_modifier = existing.get("name", {}).get("modifier", "EQUALS")
|
||||||
group_value = existing.get("group", {}).get("value", "")
|
group_value = existing.get("group", {}).get("value", "")
|
||||||
@@ -1427,14 +1480,17 @@ def PlatformFilterBar(
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def PlayEventFilterBar(
|
class PlayEventFilterBar(_FilterBarBase):
|
||||||
filter_json="", preset_list_url="", preset_save_url=""
|
|
||||||
) -> SafeText:
|
|
||||||
"""Collapsible filter bar for the PlayEvent list."""
|
"""Collapsible filter bar for the PlayEvent list."""
|
||||||
existing = _filter_parse(filter_json)
|
|
||||||
|
def build_fields(self) -> list:
|
||||||
|
return _playevent_fields(self.existing)
|
||||||
|
|
||||||
|
|
||||||
|
def _playevent_fields(existing: dict) -> list:
|
||||||
game_choice = _filter_get_choice(existing, "game")
|
game_choice = _filter_get_choice(existing, "game")
|
||||||
days_min, days_max = _parse_range(existing, "days_to_finish")
|
days_min, days_max = _parse_range(existing, "days_to_finish")
|
||||||
|
|
||||||
@@ -1465,7 +1521,7 @@ def PlayEventFilterBar(
|
|||||||
max_placeholder="e.g. 30",
|
max_placeholder="e.g. 30",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def StringFilter(
|
def StringFilter(
|
||||||
|
|||||||
Reference in New Issue
Block a user