Coverage for src / lexigram / admin / ui / filters / types / toggle.py: 0%

58 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-13 18:58 +0800

1"""Toggle filter type.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from lexigram.admin.data.query import EqualSpec 

8from lexigram.admin.ui.filters.base import Filter 

9from lexigram.serialization import dumps_str 

10from lexigram.ui import Zones 

11 

12 

13class ToggleFilter(Filter): 

14 """ 

15 Boolean toggle filter. 

16 

17 Example: 

18 ToggleFilter( 

19 label="Neutered Status", 

20 true_label="Neutered/Spayed", 

21 false_label="Not Neutered" 

22 ) 

23 """ 

24 

25 def __init__( 

26 self, 

27 name: str = "", 

28 label: str | None = None, 

29 true_label: str = "Yes", 

30 false_label: str = "No", 

31 ): 

32 """ 

33 Initialize toggle filter. 

34 

35 Args: 

36 name: Filter field name (auto-set by column if empty) 

37 label: Display label 

38 true_label: Label for true option 

39 false_label: Label for false option 

40 """ 

41 super().__init__(name, label) 

42 self._true_label = true_label 

43 self._false_label = false_label 

44 

45 def render(self, current_value: Any = None, url: str | None = None) -> str: # type: ignore[override] 

46 """Render as checkbox or radio buttons using atomic components.""" 

47 from lexigram.ui import Select 

48 

49 # Convert to select with three options: All, True, False 

50 choices = [ 

51 ("", "All"), 

52 ("true", self._true_label), 

53 ("false", self._false_label), 

54 ] 

55 

56 # Normalize current value 

57 value = "" 

58 current = current_value if current_value is not None else self.value 

59 

60 if current is not None: 

61 if isinstance(current, bool): 

62 value = "true" if current else "false" 

63 elif str(current).lower() in ["true", "1", "yes"]: 

64 value = "true" 

65 elif str(current).lower() in ["false", "0", "no"]: 

66 value = "false" 

67 

68 # Get state and resource prefix 

69 state = getattr(self, "_state", None) 

70 resource_prefix = getattr(state, "_resource_prefix", None) if state else url 

71 base_url = resource_prefix.rstrip("/") if resource_prefix else "" 

72 

73 # Canonical HTMX attrs: prefer stored from FilterBar, else fallback 

74 stored = self.get_htmx_attrs() 

75 if stored: 

76 htmx_attrs = stored 

77 elif state: 

78 params = state.to_query_params() 

79 params.pop(self.name, None) 

80 params.pop("page", None) 

81 params.pop("cursor", None) 

82 htmx_attrs = { 

83 "hx-get": f"{base_url}/", 

84 "hx-target": Zones.DATA.selector, 

85 "hx-swap": Zones.DATA.swap_mode.value, 

86 "hx-select": Zones.DATA.selector, 

87 "hx-push-url": "true", 

88 "hx-vals": dumps_str(params), 

89 } 

90 else: 

91 htmx_attrs = { 

92 "hx-get": f"{base_url}/", 

93 "hx-trigger": "change", 

94 "hx-target": Zones.DATA.selector, 

95 "hx-swap": Zones.DATA.swap_mode.value, 

96 "hx-select": Zones.DATA.selector, 

97 "hx-push-url": "true", 

98 "hx-include": f"{Zones.DATA.selector} [data-state='true'], #{Zones.SEARCH.id}", 

99 "hx-params": "*", 

100 } 

101 

102 # Convert to component-friendly names (hx_*) 

103 comp_attrs = {k.replace("-", "_"): v for k, v in htmx_attrs.items()} 

104 

105 select = Select( 

106 name=f"filter_{self.name}", 

107 choices=choices, 

108 value=value, 

109 label=self.label, 

110 **comp_attrs, # type: ignore[arg-type] 

111 ) 

112 

113 return select.render() 

114 

115 def from_url_param(self, param: Any) -> Any: 

116 """Convert string param to boolean.""" 

117 if param is None or param == "": 

118 return None 

119 

120 s = str(param).lower() 

121 if s in ["true", "1", "yes"]: 

122 return True 

123 if s in ["false", "0", "no"]: 

124 return False 

125 return None 

126 

127 def apply(self, query: Any, value: Any) -> Any: 

128 """ 

129 Apply toggle filter to query. 

130 

131 Converts string value to boolean. 

132 """ 

133 parsed = self.from_url_param(value) 

134 if parsed is None: 

135 return query 

136 

137 return query.filter(**{self.name: parsed}) 

138 

139 def to_spec(self, value: Any) -> Any | None: 

140 """Convert to EqualSpec with boolean value.""" 

141 

142 parsed = self.from_url_param(value) 

143 if parsed is None: 

144 return None 

145 return EqualSpec(self.name, parsed)