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.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.primitives import Checkbox, Div, Input, Label, Radio, Span
|
||||
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:
|
||||
"""Shared collapsible filter-bar chrome. `fields` is the per-entity body
|
||||
(grids, sliders, checkboxes); the shell adds the collapse toggle, the form,
|
||||
the hidden filter-json input and the Apply/Clear/preset action row."""
|
||||
class _FilterBarBase(BaseComponent):
|
||||
"""Shared collapsible filter-bar chrome.
|
||||
|
||||
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(
|
||||
attributes=[("id", "filter-bar"), ("class", "mb-6")],
|
||||
children=[
|
||||
@@ -725,34 +751,48 @@ def _filter_bar(fields, filter_json, preset_list_url, preset_save_url) -> SafeTe
|
||||
("type", "hidden"),
|
||||
("id", _FILTER_INPUT_ID),
|
||||
("name", "filter"),
|
||||
# NB: Component escapes attribute values, so the
|
||||
# raw JSON is passed through (no double-escape).
|
||||
("value", filter_json),
|
||||
# NB: attribute values are escaped, so the
|
||||
# raw JSON passes through (no double-escape).
|
||||
("value", self.filter_json),
|
||||
],
|
||||
),
|
||||
*fields,
|
||||
_filter_action_row(preset_list_url, preset_save_url),
|
||||
*self.build_fields(),
|
||||
_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 = "",
|
||||
status_options: list[LabeledOption] | None = None,
|
||||
preset_list_url: str = "",
|
||||
preset_save_url: str = "",
|
||||
) -> SafeText:
|
||||
"""Collapsible filter bar for the Game list."""
|
||||
) -> None:
|
||||
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
|
||||
|
||||
if status_options is None:
|
||||
status_options = [(s.value, s.label) for s in Game.Status]
|
||||
|
||||
existing = _filter_parse(filter_json)
|
||||
status_choice = _filter_get_choice(existing, "status")
|
||||
platform_choice = _filter_get_choice(existing, "platform")
|
||||
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:
|
||||
@@ -1074,13 +1114,16 @@ def _find_label(options: list[LabeledOption], value: str) -> str:
|
||||
return value
|
||||
|
||||
|
||||
def SessionFilterBar(
|
||||
filter_json="", preset_list_url="", preset_save_url=""
|
||||
) -> SafeText:
|
||||
class SessionFilterBar(_FilterBarBase):
|
||||
"""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
|
||||
|
||||
existing = _filter_parse(filter_json)
|
||||
game_choice = _filter_get_choice(existing, "game")
|
||||
device_choice = _filter_get_choice(existing, "device")
|
||||
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(
|
||||
filter_json="", preset_list_url="", preset_save_url=""
|
||||
) -> SafeText:
|
||||
class PurchaseFilterBar(_FilterBarBase):
|
||||
"""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
|
||||
|
||||
type_options = Purchase.TYPES
|
||||
ownership_options = Purchase.OWNERSHIP_TYPES
|
||||
existing = _filter_parse(filter_json)
|
||||
game_choice = _filter_get_choice(existing, "games")
|
||||
platform_choice = _filter_get_choice(existing, "platform")
|
||||
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."""
|
||||
|
||||
def build_fields(self) -> list:
|
||||
return _device_fields(self.existing)
|
||||
|
||||
|
||||
def _device_fields(existing: dict) -> list:
|
||||
from games.models import Device
|
||||
|
||||
existing = _filter_parse(filter_json)
|
||||
type_options = Device.DEVICE_TYPES
|
||||
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(
|
||||
filter_json="", preset_list_url="", preset_save_url=""
|
||||
) -> SafeText:
|
||||
class PlatformFilterBar(_FilterBarBase):
|
||||
"""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_modifier = existing.get("name", {}).get("modifier", "EQUALS")
|
||||
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(
|
||||
filter_json="", preset_list_url="", preset_save_url=""
|
||||
) -> SafeText:
|
||||
class PlayEventFilterBar(_FilterBarBase):
|
||||
"""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")
|
||||
days_min, days_max = _parse_range(existing, "days_to_finish")
|
||||
|
||||
@@ -1465,7 +1521,7 @@ def PlayEventFilterBar(
|
||||
max_placeholder="e.g. 30",
|
||||
),
|
||||
]
|
||||
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
|
||||
return fields
|
||||
|
||||
|
||||
def StringFilter(
|
||||
|
||||
Reference in New Issue
Block a user