Coverage for src/lexigram/admin/navigation/clusters.py: 95%

59 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 14:56 +0800

1"""Cluster navigation helpers for Lexigram Admin. 

2 

3A cluster groups contributor navigation items (declared via 

4``NavigationContribution(group=...)``) behind a single top-level sidebar 

5entry that opens a center with its own secondary sidebar — mirroring how 

6the Configuration Center works for settings. 

7 

8All helpers accept an explicit :class:`~lexigram.admin.clusters.Cluster`; 

9when omitted they fall back to the built-in infrastructure cluster, so 

10existing call sites keep working. Any cluster registered in the 

11:class:`~lexigram.admin.clusters.ClusterRegistry` gets the same center 

12treatment — landing URL, namespaced child hrefs, secondary sidebar, and 

13primary collapse — with no per-cluster code. 

14""" 

15 

16from __future__ import annotations 

17 

18from typing import Any 

19 

20from lexigram.admin.clusters.base import Cluster 

21from lexigram.admin.clusters.registry import INFRASTRUCTURE_CLUSTER 

22 

23# Backwards-compatible constants describing the built-in cluster. 

24CLUSTER_GROUP = INFRASTRUCTURE_CLUSTER.group or "infrastructure" 

25CLUSTER_LABEL = INFRASTRUCTURE_CLUSTER.label 

26CLUSTER_URL = f"/admin/{INFRASTRUCTURE_CLUSTER.slug or 'infrastructure'}" 

27CLUSTER_ICON = INFRASTRUCTURE_CLUSTER.icon or "server" 

28 

29__all__ = [ 

30 "CLUSTER_GROUP", 

31 "CLUSTER_ICON", 

32 "CLUSTER_LABEL", 

33 "CLUSTER_URL", 

34 "build_secondary_nav", 

35 "cluster_child_href", 

36 "cluster_items", 

37 "collapse_cluster_in_primary", 

38 "is_cluster_path", 

39] 

40 

41 

42def cluster_child_href( 

43 url: str | None, 

44 *, 

45 cluster: Cluster = INFRASTRUCTURE_CLUSTER, 

46) -> str: 

47 """Namespace a cluster child URL under the cluster center prefix. 

48 

49 Maps e.g. ``/admin/web`` -> ``/admin/infrastructure/web`` so cluster 

50 areas live under a single center namespace, mirroring how settings 

51 sub-pages are nested below ``/admin/settings``. URLs already inside 

52 the namespace, non-admin URLs, and empty values are returned unchanged. 

53 

54 Args: 

55 url: Original URL to namespace. 

56 cluster: Cluster whose center namespace is used. 

57 

58 Returns: 

59 Namespaced URL, or the input unchanged. 

60 """ 

61 if not url or not url.startswith("/admin/"): 

62 return url or "" 

63 cleaned = url.rstrip("/") 

64 cluster_url = f"/admin/{cluster.slug}" 

65 if cleaned == cluster_url or cleaned.startswith(cluster_url + "/"): 

66 return url 

67 relative = cleaned.removeprefix("/admin/") 

68 return f"{cluster_url}/{relative}" 

69 

70 

71def cluster_items( 

72 groups: dict[str, Any] | None, 

73 *, 

74 cluster: Cluster = INFRASTRUCTURE_CLUSTER, 

75) -> list[Any]: 

76 """Return the top-level items contributed to the cluster group. 

77 

78 Args: 

79 groups: Assembler groups mapping (group name -> items). 

80 cluster: Cluster whose group is read. 

81 

82 Returns: 

83 List of contributed items (possibly empty). 

84 """ 

85 if not groups: 

86 return [] 

87 return list(groups.get(cluster.group, ()) or ()) 

88 

89 

90def is_cluster_path( 

91 current_path: str | None, 

92 items: list[Any], 

93 *, 

94 cluster: Cluster = INFRASTRUCTURE_CLUSTER, 

95) -> bool: 

96 """Return True when the path belongs to the cluster center. 

97 

98 Matches the landing URL plus every top-level item URL (prefix match, 

99 so child pages count as well) contributed by the cluster group. 

100 

101 Args: 

102 current_path: Request path. 

103 items: Contributed cluster items. 

104 cluster: Cluster to test against. 

105 

106 Returns: 

107 True when the path belongs to the cluster center. 

108 """ 

109 if not current_path: 

110 return False 

111 cluster_url = f"/admin/{cluster.slug}" 

112 if current_path == cluster_url or current_path.startswith(cluster_url + "/"): 

113 return True 

114 current = current_path 

115 return any( 

116 current == item.url or current.startswith(item.url + "/") for item in items 

117 ) 

118 

119 

120def _is_active(current_path: str | None, url: str) -> bool: 

121 if not current_path: 

122 return False 

123 return current_path == url or current_path.startswith(url + "/") 

124 

125 

126def build_secondary_nav( 

127 items: list[Any], 

128 current_path: str | None, 

129 *, 

130 cluster: Cluster = INFRASTRUCTURE_CLUSTER, 

131) -> list[dict[str, Any]]: 

132 """Build secondary sidebar entries for the cluster center. 

133 

134 Each top-level item becomes an entry with an ``active`` flag; child 

135 contributions are nested as ``children`` entries. Parents are marked 

136 active when their own URL or any child URL matches the current path. 

137 

138 Args: 

139 items: Contributed cluster items. 

140 current_path: Request path. 

141 cluster: Cluster whose center namespace is used for hrefs. 

142 

143 Returns: 

144 Secondary nav entries as dicts. 

145 """ 

146 result: list[dict[str, Any]] = [] 

147 for item in items: 

148 children = [ 

149 { 

150 "label": child.label, 

151 "href": cluster_child_href(child.url, cluster=cluster), 

152 "icon": child.icon, 

153 "active": _is_active(current_path, child.url), 

154 } 

155 for child in item.children 

156 ] 

157 entry: dict[str, Any] = { 

158 "label": item.label, 

159 "href": cluster_child_href(item.url, cluster=cluster), 

160 "icon": item.icon, 

161 "active": _is_active(current_path, item.url) 

162 or any(child["active"] for child in children), 

163 } 

164 if children: 

165 entry["children"] = children 

166 result.append(entry) 

167 return result 

168 

169 

170def collapse_cluster_in_primary( 

171 flat_items: list[dict[str, Any]], 

172 current_path: str | None, 

173 items: list[Any], 

174 *, 

175 cluster: Cluster = INFRASTRUCTURE_CLUSTER, 

176) -> list[dict[str, Any]]: 

177 """Remove the cluster group from the primary sidebar. 

178 

179 The group header (``cluster.label``) and all of its items are dropped 

180 entirely; the center is reached from the user dropdown and its 

181 secondary sidebar. Items outside the group are preserved in order. 

182 

183 Args: 

184 flat_items: Flat primary nav items. 

185 current_path: Request path (unused, retained for signature parity). 

186 items: Contributed cluster items. 

187 cluster: Cluster whose group is collapsed. 

188 

189 Returns: 

190 Primary nav without the cluster group. 

191 """ 

192 in_cluster = False 

193 result: list[dict[str, Any]] = [] 

194 for item in flat_items: 

195 if not isinstance(item, dict): 

196 result.append(item) 

197 continue 

198 if item.get("is_group"): 

199 in_cluster = item.get("label") == cluster.label 

200 if not in_cluster: 

201 result.append(item) 

202 continue 

203 if in_cluster: 

204 continue 

205 result.append(item) 

206 return result