Coverage for heritrace / routes / entity / _rendering.py: 99%

138 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-07-02 10:16 +0000

1# SPDX-FileCopyrightText: 2024-2026 Arcangelo Massari <arcangelo.massari@unibo.it> 

2# 

3# SPDX-License-Identifier: ISC 

4 

5from flask_babel import gettext 

6from rdflib import RDF, Graph, Literal, URIRef 

7from rdflib.term import Node 

8 

9from heritrace.routes.entity._types import ( 

10 EntityIdentity, 

11 EntityRenderContext, 

12 HistoryContext, 

13) 

14from heritrace.utils.display_rules_utils import ( 

15 get_highest_priority_class, 

16 get_predicate_ordering_info, 

17 get_property_order_from_rules, 

18 get_shape_order_from_display_rules, 

19) 

20from heritrace.utils.shacl_utils import ( 

21 determine_shape_for_entity_triples, 

22 get_entity_position_in_sequence, 

23) 

24from heritrace.utils.sparql_utils import get_triples_from_graph 

25from heritrace.utils.uri_utils import is_valid_url 

26 

27 

28def determine_object_class_and_shape( 

29 object_value: str, relevant_snapshot: Graph | None 

30) -> tuple[str | None, str | None]: 

31 if not is_valid_url(str(object_value)) or not relevant_snapshot: 

32 return None, None 

33 

34 object_triples = list( 

35 get_triples_from_graph(relevant_snapshot, (URIRef(object_value), None, None)) 

36 ) 

37 if not object_triples: 

38 return None, None 

39 

40 object_shape_uri = determine_shape_for_entity_triples(object_triples) 

41 object_classes = [ 

42 str(o) 

43 for _, _, o in get_triples_from_graph( 

44 relevant_snapshot, (URIRef(object_value), RDF.type, None) 

45 ) 

46 ] 

47 object_class = ( 

48 get_highest_priority_class(object_classes) if object_classes else None 

49 ) 

50 

51 return object_class, object_shape_uri 

52 

53 

54def _build_modification_caches( 

55 triples: list[tuple[Node, Node, Node]], 

56 relevant_snapshot: Graph | None, 

57) -> tuple[dict[str, str | None], dict[str, str | None]]: 

58 object_shapes_cache: dict[str, str | None] = {} 

59 object_classes_cache: dict[str, str | None] = {} 

60 

61 if relevant_snapshot: 

62 for triple in triples: 

63 object_value = str(triple[2]) 

64 object_class, object_shape = determine_object_class_and_shape( 

65 object_value, relevant_snapshot 

66 ) 

67 object_classes_cache[object_value] = object_class 

68 object_shapes_cache[object_value] = object_shape 

69 

70 return object_shapes_cache, object_classes_cache 

71 

72 

73def _build_predicate_shape_groups( 

74 triples: list[tuple[Node, Node, Node]], 

75 object_shapes_cache: dict[str, str | None], 

76 identity: EntityIdentity, 

77) -> tuple[dict, dict, dict]: 

78 predicate_shape_groups: dict[tuple[str, str | None], list] = {} 

79 predicate_ordering_cache: dict[str, str | None] = {} 

80 entity_position_cache: dict[tuple[str, str], int | None] = {} 

81 

82 for triple in triples: 

83 predicate = str(triple[1]) 

84 object_value = str(triple[2]) 

85 object_shape_uri = object_shapes_cache.get(object_value) 

86 

87 if predicate not in predicate_ordering_cache: 

88 predicate_ordering_cache[predicate] = get_predicate_ordering_info( 

89 predicate, identity.highest_priority_class, identity.entity_shape 

90 ) 

91 

92 order_property = predicate_ordering_cache[predicate] 

93 if order_property and is_valid_url(object_value) and identity.relevant_snapshot: 

94 position_key = (object_value, predicate) 

95 if position_key not in entity_position_cache: 

96 entity_position_cache[position_key] = get_entity_position_in_sequence( 

97 object_value, 

98 identity.entity_uri, 

99 predicate, 

100 order_property, 

101 identity.relevant_snapshot, 

102 ) 

103 

104 group_key = (predicate, object_shape_uri) 

105 if group_key not in predicate_shape_groups: 

106 predicate_shape_groups[group_key] = [] 

107 predicate_shape_groups[group_key].append(triple) 

108 

109 return predicate_shape_groups, predicate_ordering_cache, entity_position_cache 

110 

111 

112def _get_cached_position( 

113 triple: tuple[URIRef, URIRef, URIRef | Literal], 

114 predicate_uri: str, 

115 cache: dict, 

116) -> int | float: 

117 object_value = str(triple[2]) 

118 position_key = (object_value, predicate_uri) 

119 if position_key in cache: 

120 return cache[position_key] 

121 return float("inf") 

122 

123 

124def _sort_and_format_group( 

125 group_triples: list[tuple[URIRef, URIRef, URIRef | Literal]], 

126 predicate_uri: str, 

127 ctx: EntityRenderContext, 

128) -> str: 

129 order_property = ctx.predicate_ordering_cache.get(predicate_uri) 

130 sorted_triples = ( 

131 sorted( 

132 group_triples, 

133 key=lambda t: _get_cached_position( 

134 t, predicate_uri, ctx.entity_position_cache 

135 ), 

136 ) 

137 if order_property and ctx.relevant_snapshot 

138 else group_triples 

139 ) 

140 

141 text = "" 

142 for triple in sorted_triples: 

143 text += format_triple_modification(triple, ctx) 

144 return text 

145 

146 

147def _render_ordered_groups( 

148 predicate_shape_groups: dict, 

149 ordered_properties: list, 

150 ctx: EntityRenderContext, 

151) -> tuple[str, set]: 

152 text = "" 

153 processed_predicates: set[tuple[str, str | None]] = set() 

154 

155 for predicate in ordered_properties: 

156 shape_order = get_shape_order_from_display_rules( 

157 ctx.highest_priority_class, ctx.entity_shape, predicate 

158 ) 

159 predicate_groups = [] 

160 for group_key, group_triples in predicate_shape_groups.items(): 

161 predicate_uri, object_shape_uri = group_key 

162 if predicate_uri == predicate: 

163 if object_shape_uri and object_shape_uri in shape_order: 

164 shape_priority = shape_order.index(object_shape_uri) 

165 else: 

166 shape_priority = len(shape_order) 

167 

168 predicate_groups.append((shape_priority, group_key, group_triples)) 

169 

170 predicate_groups.sort(key=lambda x: x[0]) 

171 for _, group_key, group_triples in predicate_groups: 

172 processed_predicates.add(group_key) 

173 predicate_uri, _ = group_key 

174 text += _sort_and_format_group(group_triples, predicate_uri, ctx) 

175 

176 return text, processed_predicates 

177 

178 

179def _render_remaining_groups( 

180 predicate_shape_groups: dict, 

181 processed_predicates: set, 

182 ctx: EntityRenderContext, 

183) -> str: 

184 text = "" 

185 for group_key, group_triples in predicate_shape_groups.items(): 

186 if group_key not in processed_predicates: 

187 predicate_uri, _ = group_key 

188 text += _sort_and_format_group(group_triples, predicate_uri, ctx) 

189 return text 

190 

191 

192def generate_modification_text( 

193 modifications: dict[str, list[tuple[Node, Node, Node]]], 

194 ctx: HistoryContext, 

195 current_snapshot: Graph, 

196 current_snapshot_timestamp: str, 

197) -> str: 

198 modification_text = "<p><strong>" + gettext("Modifications") + "</strong></p>" 

199 

200 ordered_properties = get_property_order_from_rules( 

201 ctx.highest_priority_class, ctx.entity_shape 

202 ) 

203 

204 for mod_type, triples in modifications.items(): 

205 modification_text += "<ul class='list-group mb-3'><p>" 

206 if mod_type == gettext("Additions"): 

207 modification_text += '<i class="bi bi-plus-circle-fill text-success"></i>' 

208 elif mod_type == gettext("Deletions"): 

209 modification_text += '<i class="bi bi-dash-circle-fill text-danger"></i>' 

210 modification_text += " <em>" + gettext(mod_type) + "</em></p>" 

211 

212 relevant_snapshot = None 

213 if ( 

214 mod_type == gettext("Deletions") 

215 and ctx.history 

216 and ctx.entity_uri 

217 and current_snapshot_timestamp 

218 ): 

219 current_index = ctx.sorted_timestamps.index(current_snapshot_timestamp) 

220 if current_index > 0: 

221 relevant_snapshot = ctx.history[ctx.entity_uri][ 

222 ctx.sorted_timestamps[current_index - 1] 

223 ] 

224 else: 

225 relevant_snapshot = current_snapshot 

226 

227 object_shapes_cache, object_classes_cache = _build_modification_caches( 

228 triples, relevant_snapshot 

229 ) 

230 

231 identity = EntityIdentity( 

232 entity_uri=ctx.entity_uri, 

233 highest_priority_class=ctx.highest_priority_class, 

234 entity_shape=ctx.entity_shape, 

235 relevant_snapshot=relevant_snapshot, 

236 ) 

237 

238 predicate_shape_groups, predicate_ordering_cache, entity_position_cache = ( 

239 _build_predicate_shape_groups( 

240 triples, 

241 object_shapes_cache, 

242 identity, 

243 ) 

244 ) 

245 

246 render_ctx = EntityRenderContext( 

247 entity_uri=ctx.entity_uri, 

248 entity_shape=ctx.entity_shape, 

249 highest_priority_class=ctx.highest_priority_class, 

250 relevant_snapshot=relevant_snapshot, 

251 predicate_ordering_cache=predicate_ordering_cache, 

252 entity_position_cache=entity_position_cache, 

253 object_shapes_cache=object_shapes_cache, 

254 object_classes_cache=object_classes_cache, 

255 custom_filter=ctx.custom_filter, 

256 ) 

257 

258 ordered_text, processed_predicates = _render_ordered_groups( 

259 predicate_shape_groups, ordered_properties, render_ctx 

260 ) 

261 modification_text += ordered_text 

262 

263 modification_text += _render_remaining_groups( 

264 predicate_shape_groups, processed_predicates, render_ctx 

265 ) 

266 

267 modification_text += "</ul>" 

268 

269 return modification_text 

270 

271 

272def format_triple_modification( 

273 triple: tuple[URIRef, URIRef, URIRef | Literal], 

274 ctx: EntityRenderContext, 

275) -> str: 

276 predicate = triple[1] 

277 object_value = triple[2] 

278 

279 object_shape_uri = ctx.object_shapes_cache.get(str(object_value)) 

280 

281 predicate_label = ctx.custom_filter.human_readable_predicate( 

282 predicate, 

283 (ctx.highest_priority_class, ctx.entity_shape), 

284 object_shape_uri=object_shape_uri, 

285 ) 

286 

287 object_class = ctx.object_classes_cache.get(str(object_value)) 

288 object_label = get_object_label( 

289 object_value, 

290 predicate, 

291 object_shape_uri, 

292 object_class, 

293 ctx, 

294 ) 

295 

296 order_info = "" 

297 if is_valid_url(str(object_value)): 

298 order_property = ctx.predicate_ordering_cache.get(str(predicate)) 

299 if order_property: 

300 position_key = (str(object_value), str(predicate)) 

301 position = ctx.entity_position_cache.get(position_key) 

302 if position is not None: 

303 order_info = f' <span class="order-position-badge">#{position}</span>' 

304 

305 return f""" 

306 <li class='d-flex align-items-center'> 

307 <span class='flex-grow-1 d-flex flex-column 

308 justify-content-center ms-3 mb-2 w-100'> 

309 <strong>{predicate_label}{order_info}</strong> 

310 <span class="object-value word-wrap">{object_label}</span> 

311 </span> 

312 </li>""" 

313 

314 

315def get_object_label( 

316 object_value: str, 

317 predicate: str, 

318 object_shape_uri: str | None, 

319 object_class: str | None, 

320 ctx: EntityRenderContext, 

321) -> str: 

322 predicate = str(predicate) 

323 

324 if predicate == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": 

325 subject_entity_key = (ctx.highest_priority_class or "", ctx.entity_shape) 

326 return ctx.custom_filter.human_readable_class(subject_entity_key) 

327 

328 if is_valid_url(object_value): 

329 if object_shape_uri or object_class: 

330 return ctx.custom_filter.human_readable_entity( 

331 object_value, (object_class, object_shape_uri), ctx.relevant_snapshot 

332 ) 

333 return str(object_value) 

334 

335 return str(object_value)