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
« 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
5from flask_babel import gettext
6from rdflib import RDF, Graph, Literal, URIRef
7from rdflib.term import Node
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
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
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
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 )
51 return object_class, object_shape_uri
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] = {}
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
70 return object_shapes_cache, object_classes_cache
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] = {}
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)
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 )
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 )
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)
109 return predicate_shape_groups, predicate_ordering_cache, entity_position_cache
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")
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 )
141 text = ""
142 for triple in sorted_triples:
143 text += format_triple_modification(triple, ctx)
144 return text
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()
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)
168 predicate_groups.append((shape_priority, group_key, group_triples))
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)
176 return text, processed_predicates
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
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>"
200 ordered_properties = get_property_order_from_rules(
201 ctx.highest_priority_class, ctx.entity_shape
202 )
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>"
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
227 object_shapes_cache, object_classes_cache = _build_modification_caches(
228 triples, relevant_snapshot
229 )
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 )
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 )
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 )
258 ordered_text, processed_predicates = _render_ordered_groups(
259 predicate_shape_groups, ordered_properties, render_ctx
260 )
261 modification_text += ordered_text
263 modification_text += _render_remaining_groups(
264 predicate_shape_groups, processed_predicates, render_ctx
265 )
267 modification_text += "</ul>"
269 return modification_text
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]
279 object_shape_uri = ctx.object_shapes_cache.get(str(object_value))
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 )
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 )
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>'
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>"""
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)
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)
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)
335 return str(object_value)