Coverage for src / lexigram / admin / resources / config.py: 46%

147 statements  

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

1"""Admin resource and table configuration types. 

2 

3Provides :class:`TableConfiguration` (static DataTable config) and 

4:class:`ResourceConfig` (fluent builder for resource metadata). 

5""" 

6 

7from __future__ import annotations 

8 

9from dataclasses import dataclass 

10from typing import Any, Literal 

11 

12from lexigram.domain import DomainModel 

13from lexigram.validation import ConfigDict, Field 

14 

15__all__ = ["ResourceConfig", "TableConfiguration"] 

16 

17 

18@dataclass(init=False) 

19class TableConfiguration(DomainModel): 

20 """Static configuration for a DataTable.""" 

21 

22 model_config = ConfigDict(arbitrary_types_allowed=True) 

23 

24 columns: list[Any] = Field(default_factory=list) 

25 actions: list[Any] = Field(default_factory=list) 

26 header_actions: list[Any] = Field(default_factory=list) 

27 bulk_actions: list[Any] = Field(default_factory=list) 

28 filter_options: Any | None = None 

29 default_view: str = "tabular" 

30 default_layout: str = "stack" 

31 action_layout: Literal["horizontal", "stack"] = "horizontal" 

32 default_sort_by: str | None = None 

33 default_sort_order: str = "asc" 

34 per_page: int = 20 

35 enable_search: bool = True 

36 search_fields: list[str] | None = None 

37 reorderable_columns: bool = False 

38 group_by: str | None = None 

39 resource_name: str | None = None 

40 resource_prefix: str | None = None 

41 expandable_relationship: str | None = None 

42 density: str = "normal" 

43 

44 @property 

45 def filters(self) -> Any | None: 

46 """Alias for ``filter_options`` used by the toolbar renderer.""" 

47 return self.filter_options 

48 

49 @property 

50 def density_css_class(self) -> str: 

51 """CSS class for current density setting.""" 

52 return f"table-density-{self.density}" 

53 

54 @property 

55 def density_row_height(self) -> str: 

56 """Row height in px for current density.""" 

57 heights = {"compact": "32px", "normal": "48px", "comfortable": "64px"} 

58 return heights.get(self.density, "48px") 

59 

60 

61class ResourceConfig: 

62 """Fluent configuration builder for Resource metadata.""" 

63 

64 def __init__(self) -> None: 

65 self._layout: str = "sidebar" 

66 self._view: str = "tabular" 

67 self._per_page: int = 20 

68 self._default_sort_field: str | None = None 

69 self._default_sort_order: str = "asc" 

70 self._display_name: str | None = None 

71 self._description: str | None = None 

72 self._icon: str = "box" 

73 self._expandable: bool = False 

74 self._record_title_func: Any = None 

75 self._columns: list[Any] = [] 

76 self._actions: list[Any] = [] 

77 self._filters_list: list[Any] = [] 

78 self._views_list: list[Any] = [] 

79 self._action_layout: str = "horizontal" 

80 self._form_display_mode: str = "modal" 

81 self._name: str | None = None 

82 self._group: str | None = None 

83 self._group_label: str | None = None 

84 self._group_icon: str | None = None 

85 self._group_order: int | None = None 

86 

87 @staticmethod 

88 def builder() -> ResourceConfig: 

89 """Return a new :class:`ResourceConfig` builder instance.""" 

90 return ResourceConfig() 

91 

92 def layout(self, layout: Literal["sidebar", "stack"]) -> ResourceConfig: 

93 """Set the resource layout.""" 

94 self._layout = layout 

95 return self 

96 

97 def name(self, name: str) -> ResourceConfig: 

98 """Set the resource registration name.""" 

99 self._name = name 

100 return self 

101 

102 def group( 

103 self, 

104 group: str, 

105 *, 

106 label: str | None = None, 

107 icon: str | None = None, 

108 order: int | None = None, 

109 ) -> ResourceConfig: 

110 """Set the navigation group this resource belongs to. 

111 

112 Args: 

113 group: Navigation group key. 

114 label: Group display label (used when auto-creating the group). 

115 icon: Group icon (used when auto-creating the group). 

116 order: Group sort order (used when auto-creating the group). 

117 """ 

118 self._group = group 

119 if label is not None: 

120 self._group_label = label 

121 if icon is not None: 

122 self._group_icon = icon 

123 if order is not None: 

124 self._group_order = order 

125 return self 

126 

127 def view(self, view: Literal["tabular", "grid", "kanban"] | Any) -> ResourceConfig: 

128 """Set the default view.""" 

129 self._view = str(view) if hasattr(view, "__str__") else "tabular" 

130 return self 

131 

132 def views(self, views: list[Any]) -> ResourceConfig: 

133 """Add multiple view definitions.""" 

134 self._views_list.extend(views) 

135 return self 

136 

137 def per_page(self, count: int) -> ResourceConfig: 

138 """Set items per page.""" 

139 self._per_page = max(1, count) 

140 return self 

141 

142 def sort(self, field: str, order: Literal["asc", "desc"] = "asc") -> ResourceConfig: 

143 """Set the default sort field and order.""" 

144 self._default_sort_field = field 

145 self._default_sort_order = order if order in ("asc", "desc") else "asc" 

146 return self 

147 

148 def label(self, label: str) -> ResourceConfig: 

149 """Set the display label.""" 

150 self._display_name = label 

151 return self 

152 

153 def resource_name(self, name: str) -> ResourceConfig: 

154 """Set the resource name (alias for label).""" 

155 self._display_name = name 

156 return self 

157 

158 def icon(self, icon: str) -> ResourceConfig: 

159 """Set the icon name.""" 

160 self._icon = icon 

161 return self 

162 

163 def description(self, description: str) -> ResourceConfig: 

164 """Set the resource description.""" 

165 self._description = description 

166 return self 

167 

168 def record_title(self, func: Any) -> ResourceConfig: 

169 """Set the record title function.""" 

170 self._record_title_func = func 

171 return self 

172 

173 def column(self, col: Any) -> ResourceConfig: 

174 """Add a single column definition.""" 

175 self._columns.append(col) 

176 return self 

177 

178 def columns(self, cols: list[Any]) -> ResourceConfig: 

179 """Add multiple column definitions.""" 

180 self._columns.extend(cols) 

181 return self 

182 

183 def action(self, action: Any) -> ResourceConfig: 

184 """Add a single action.""" 

185 self._actions.append(action) 

186 return self 

187 

188 def actions(self, actions: list[Any]) -> ResourceConfig: 

189 """Add multiple actions.""" 

190 self._actions.extend(actions) 

191 return self 

192 

193 def filter(self, filter_obj: Any) -> ResourceConfig: 

194 """Add a single filter.""" 

195 self._filters_list.append(filter_obj) 

196 return self 

197 

198 def filters(self, filters: list[Any]) -> ResourceConfig: 

199 """Add multiple filters.""" 

200 self._filters_list.extend(filters) 

201 return self 

202 

203 def action_layout(self, layout: Literal["horizontal", "stack"]) -> ResourceConfig: 

204 """Set the action layout direction.""" 

205 self._action_layout = layout 

206 return self 

207 

208 def form_display_mode( 

209 self, mode: Literal["page", "modal", "slider"] 

210 ) -> ResourceConfig: 

211 """Set how forms are displayed.""" 

212 if mode in ("page", "modal", "slider"): 

213 self._form_display_mode = mode 

214 return self 

215 

216 @property 

217 def display_name(self) -> str | None: 

218 """Return the configured display name.""" 

219 return self._display_name 

220 

221 @property 

222 def default_sort_field(self) -> str | None: 

223 """Return the configured sort field.""" 

224 return self._default_sort_field 

225 

226 @property 

227 def default_sort_order(self) -> str: 

228 """Return the configured sort order.""" 

229 return self._default_sort_order 

230 

231 @property 

232 def filters_list(self) -> list[Any]: 

233 """Return the configured filters.""" 

234 return self._filters_list 

235 

236 @property 

237 def views_list(self) -> list[Any]: 

238 """Return the configured views.""" 

239 return self._views_list