Coverage for heritrace / routes / entity / _about.py: 88%
81 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 import abort, current_app, render_template
6from flask_login import current_user, login_required
7from rdflib import RDF, Graph, Literal, URIRef
8from time_agnostic_library.agnostic_entity import AgnosticEntity
10from heritrace.extensions import (
11 get_change_tracking_config,
12 get_dataset_is_quadstore,
13 get_display_rules,
14 get_form_fields,
15 get_shacl_graph,
16)
17from heritrace.forms import UpdateTripleForm
18from heritrace.routes.entity._blueprint import entity_bp
19from heritrace.utils.datatypes import get_datatype_options
20from heritrace.utils.display_rules_utils import (
21 get_grouped_triples,
22 get_highest_priority_class,
23)
24from heritrace.utils.primary_source_utils import get_default_primary_source
25from heritrace.utils.shacl_utils import determine_shape_for_entity_triples
26from heritrace.utils.shacl_validation import get_valid_predicates
27from heritrace.utils.sparql_utils import (
28 convert_to_rdflib_graphs,
29 fetch_data_graph_for_subject,
30 get_triples_from_graph,
31)
32from heritrace.utils.virtual_properties import get_virtual_properties_for_entity
35def get_deleted_entity_context_info(
36 *,
37 is_deleted: bool,
38 sorted_timestamps: list[str],
39 history: dict,
40 subject: URIRef,
41) -> tuple[Graph | None, str | None, str | None]:
42 if is_deleted and len(sorted_timestamps) > 1:
43 context_snapshot = history[str(subject)][sorted_timestamps[-2]]
45 subject_classes = [
46 str(o)
47 for _, _, o in get_triples_from_graph(
48 context_snapshot, (subject, RDF.type, None)
49 )
50 ]
52 highest_priority_class = get_highest_priority_class(subject_classes)
53 entity_shape = determine_shape_for_entity_triples(
54 list(get_triples_from_graph(context_snapshot, (subject, None, None)))
55 )
57 return context_snapshot, highest_priority_class, entity_shape
58 return None, None, None
61def _build_live_entity_context(
62 subject_uri: URIRef,
63 history: dict,
64 subject: str,
65) -> tuple[dict, list, list, dict, dict, dict, str | None, str | None]:
66 data_graph = fetch_data_graph_for_subject(subject_uri)
68 if not history.get(subject) and (not data_graph or len(data_graph) == 0):
69 abort(404)
71 if not data_graph:
72 return {}, [], [], {}, {}, {}, None, None
74 triples: list[tuple[URIRef, URIRef, URIRef | Literal]] = [
75 (
76 URIRef(str(s)),
77 URIRef(str(p)),
78 URIRef(str(o)) if isinstance(o, URIRef) else o,
79 ) # type: ignore[misc]
80 for s, p, o in get_triples_from_graph(data_graph, (None, None, None))
81 ]
82 subject_classes = [
83 str(o)
84 for _, _, o in get_triples_from_graph(data_graph, (subject_uri, RDF.type, None))
85 ]
86 subject_triples = list(
87 get_triples_from_graph(data_graph, (subject_uri, None, None))
88 )
90 highest_priority_class = get_highest_priority_class(subject_classes)
91 entity_shape = determine_shape_for_entity_triples(subject_triples)
93 if not highest_priority_class:
94 return {}, [], [], {}, {}, {}, highest_priority_class, entity_shape
96 (
97 can_be_added,
98 can_be_deleted,
99 datatypes,
100 mandatory_values,
101 optional_values,
102 valid_predicates_set,
103 ) = get_valid_predicates(
104 triples, highest_priority_class=URIRef(highest_priority_class)
105 )
106 valid_predicates = list(valid_predicates_set)
108 grouped_triples, relevant_properties = get_grouped_triples(
109 subject_uri,
110 triples,
111 valid_predicates,
112 entity_key=(highest_priority_class, entity_shape),
113 )
115 if entity_shape:
116 virtual_properties = get_virtual_properties_for_entity(
117 highest_priority_class, entity_shape
118 )
119 else:
120 virtual_properties = []
122 can_be_added = [uri for uri in can_be_added if uri in relevant_properties] + [
123 vp[0] for vp in virtual_properties
124 ]
125 can_be_deleted = [uri for uri in can_be_deleted if uri in relevant_properties] + [
126 vp[0] for vp in virtual_properties
127 ]
129 return (
130 grouped_triples,
131 can_be_added,
132 can_be_deleted,
133 datatypes,
134 mandatory_values,
135 optional_values,
136 highest_priority_class,
137 entity_shape,
138 )
141@entity_bp.route("/about/<path:subject>")
142@login_required
143def about(subject: str) -> str:
144 subject_uri = URIRef(subject)
145 change_tracking_config = get_change_tracking_config()
147 default_primary_source = get_default_primary_source(current_user.orcid)
149 agnostic_entity = AgnosticEntity(
150 res=subject,
151 config=change_tracking_config,
152 include_related_objects=False,
153 include_merged_entities=False,
154 include_reverse_relations=False,
155 )
156 history, provenance = agnostic_entity.get_history(include_prov_metadata=True)
157 history = convert_to_rdflib_graphs(history, is_quadstore=get_dataset_is_quadstore())
159 is_deleted = False
160 context_snapshot = None
161 highest_priority_class = None
162 entity_shape = None
164 if history.get(subject):
165 sorted_timestamps = sorted(history[subject].keys())
166 latest_metadata = next(
167 (
168 meta
169 for _, meta in provenance[subject].items()
170 if meta["generatedAtTime"] == sorted_timestamps[-1]
171 ),
172 None,
173 )
175 is_deleted = bool(
176 latest_metadata
177 and "invalidatedAtTime" in latest_metadata
178 and latest_metadata["invalidatedAtTime"]
179 )
181 context_snapshot, highest_priority_class, entity_shape = (
182 get_deleted_entity_context_info(
183 is_deleted=is_deleted,
184 sorted_timestamps=sorted_timestamps,
185 history=history,
186 subject=subject_uri,
187 )
188 )
190 if is_deleted:
191 grouped_triples: dict = {}
192 can_be_added: list = []
193 can_be_deleted: list = []
194 datatypes: dict = {}
195 mandatory_values: dict = {}
196 optional_values: dict = {}
197 else:
198 (
199 grouped_triples,
200 can_be_added,
201 can_be_deleted,
202 datatypes,
203 mandatory_values,
204 optional_values,
205 highest_priority_class,
206 entity_shape,
207 ) = _build_live_entity_context(subject_uri, history, subject)
209 update_form = UpdateTripleForm()
210 form_fields = get_form_fields()
211 datatype_options = get_datatype_options()
213 predicate_details_map = {}
214 for entity_type_key, predicates in form_fields.items():
215 for predicate_uri, details_list in predicates.items():
216 for details in details_list:
217 shape = details.get("nodeShape")
218 key = (predicate_uri, entity_type_key, shape)
219 predicate_details_map[key] = details
221 return render_template(
222 "entity/about.jinja",
223 subject=subject,
224 history=history,
225 can_be_added=can_be_added,
226 can_be_deleted=can_be_deleted,
227 datatypes=datatypes,
228 update_form=update_form,
229 mandatory_values=mandatory_values,
230 optional_values=optional_values,
231 shacl=bool(len(get_shacl_graph())),
232 grouped_triples=grouped_triples,
233 display_rules=get_display_rules(),
234 form_fields=form_fields,
235 entity_type=highest_priority_class,
236 entity_shape=entity_shape,
237 predicate_details_map=predicate_details_map,
238 dataset_db_triplestore=current_app.config["DATASET_DB_TRIPLESTORE"],
239 dataset_db_text_index_enabled=current_app.config[
240 "DATASET_DB_TEXT_INDEX_ENABLED"
241 ],
242 is_deleted=is_deleted,
243 context=context_snapshot,
244 default_primary_source=default_primary_source,
245 datatype_options=datatype_options,
246 )