Skip to content

Commit 8d083ab

Browse files
committed
feat(routes): Integrate linked resources functionality and update API endpoint
- Added a new blueprint for linked resources and registered it in the main application. - Updated the `/api/format-source` endpoint to remove the redundant `/api` prefix for cleaner routing. - Refactored the entity's `about` template to dynamically load linked resources with a "Load More" feature, enhancing user experience. - Removed the `get_inverse_references` function and its associated tests to streamline the codebase. - Improved JavaScript logic for handling linked resources, including pagination and error handling.
1 parent a34d94b commit 8d083ab

File tree

13 files changed

+1469
-961
lines changed

13 files changed

+1469
-961
lines changed

heritrace/routes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ def register_blueprints(app: Flask):
99
from heritrace.routes.api import api_bp
1010
from heritrace.errors.handlers import errors_bp
1111
from heritrace.routes.merge import merge_bp
12+
from heritrace.routes.linked_resources import linked_resources_bp
1213

1314
app.register_blueprint(main_bp)
1415
app.register_blueprint(entity_bp)
1516
app.register_blueprint(auth_bp, url_prefix="/auth")
1617
app.register_blueprint(api_bp, url_prefix="/api")
1718
app.register_blueprint(errors_bp, url_prefix="/errors")
1819
app.register_blueprint(merge_bp, url_prefix="/merge")
20+
app.register_blueprint(linked_resources_bp)

heritrace/routes/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ def get_human_readable_entity():
956956
return readable
957957

958958

959-
@api_bp.route('/api/format-source', methods=['POST'])
959+
@api_bp.route('/format-source', methods=['POST'])
960960
@login_required
961961
def format_source_api():
962962
"""

heritrace/routes/entity.py

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ def about(subject):
106106
entity_type = None
107107
data_graph = None
108108
linked_resources = []
109-
inverse_references = []
110109

111110
if not is_deleted:
112111
# Fetch current entity state
@@ -140,13 +139,6 @@ def about(subject):
140139
if isinstance(obj, URIRef) and str(obj) != str(subject) and predicate != RDF.type:
141140
linked_resources.add(str(obj))
142141

143-
# Get inverse references only for non-deleted entities
144-
inverse_references = get_inverse_references(subject)
145-
146-
# Add inverse references to linked resources
147-
for ref in inverse_references:
148-
linked_resources.add(ref["subject"])
149-
150142
# Convert to list
151143
linked_resources = list(linked_resources)
152144

@@ -201,7 +193,6 @@ def about(subject):
201193
dataset_db_text_index_enabled=current_app.config[
202194
"DATASET_DB_TEXT_INDEX_ENABLED"
203195
],
204-
inverse_references=inverse_references,
205196
is_deleted=is_deleted,
206197
context=context_snapshot,
207198
linked_resources=linked_resources,
@@ -1413,63 +1404,6 @@ def find_appropriate_snapshot(provenance_data: dict, target_time: str) -> Option
14131404
return valid_snapshots[-1][1]
14141405

14151406

1416-
def get_inverse_references(subject_uri: str) -> List[Dict]:
1417-
"""
1418-
Get all entities that reference this entity.
1419-
1420-
Args:
1421-
subject_uri: URI of the entity to find references to
1422-
1423-
Returns:
1424-
List of dictionaries containing reference information
1425-
"""
1426-
sparql = get_sparql()
1427-
custom_filter = get_custom_filter()
1428-
1429-
# Build appropriate query based on triplestore type
1430-
if is_virtuoso:
1431-
query = f"""
1432-
SELECT DISTINCT ?s ?p ?g WHERE {{
1433-
GRAPH ?g {{
1434-
?s ?p <{subject_uri}> .
1435-
}}
1436-
FILTER(?g NOT IN (<{'>, <'.join(VIRTUOSO_EXCLUDED_GRAPHS)}>))
1437-
FILTER(?p != <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>)
1438-
}}
1439-
"""
1440-
else:
1441-
query = f"""
1442-
SELECT DISTINCT ?s ?p WHERE {{
1443-
?s ?p <{subject_uri}> .
1444-
FILTER(?p != <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>)
1445-
}}
1446-
"""
1447-
1448-
sparql.setQuery(query)
1449-
sparql.setReturnFormat(JSON)
1450-
results = sparql.query().convert()
1451-
1452-
references = []
1453-
for result in results["results"]["bindings"]:
1454-
subject = result["s"]["value"]
1455-
predicate = result["p"]["value"]
1456-
1457-
# Get the type of the referring entity
1458-
type_query = f"""
1459-
SELECT ?type WHERE {{
1460-
<{subject}> a ?type .
1461-
}}
1462-
"""
1463-
sparql.setQuery(type_query)
1464-
type_results = sparql.query().convert()
1465-
types = [t["type"]["value"] for t in type_results["results"]["bindings"]]
1466-
types = [get_highest_priority_class(types)]
1467-
1468-
references.append({"subject": subject, "predicate": predicate, "types": types})
1469-
1470-
return references
1471-
1472-
14731407
def generate_modification_text(
14741408
modifications,
14751409
subject_classes,
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import traceback
2+
3+
from flask import Blueprint, current_app, jsonify, request
4+
from flask_babel import gettext
5+
from flask_login import login_required
6+
from heritrace.extensions import get_custom_filter, get_sparql
7+
from heritrace.utils.display_rules_utils import get_highest_priority_class
8+
from heritrace.utils.sparql_utils import get_entity_types
9+
from heritrace.utils.virtuoso_utils import (VIRTUOSO_EXCLUDED_GRAPHS,
10+
is_virtuoso)
11+
from SPARQLWrapper import JSON
12+
13+
linked_resources_bp = Blueprint("linked_resources", __name__, url_prefix="/api/linked-resources")
14+
15+
def get_paginated_inverse_references(subject_uri: str, limit: int, offset: int) -> tuple[list[dict], int, bool]:
16+
"""
17+
Get paginated entities that reference this entity.
18+
19+
Args:
20+
subject_uri: URI of the entity to find references to.
21+
limit: Maximum number of references to return.
22+
offset: Number of references to skip.
23+
24+
Returns:
25+
A tuple containing:
26+
- List of dictionaries containing reference information.
27+
- Total count of references.
28+
- Boolean indicating if there are more references.
29+
"""
30+
sparql = get_sparql()
31+
custom_filter = get_custom_filter()
32+
total_count = 0
33+
references = []
34+
35+
try:
36+
# Count Query
37+
count_query_parts = [
38+
"SELECT (COUNT(DISTINCT ?s) as ?count) WHERE {",
39+
]
40+
if is_virtuoso:
41+
count_query_parts.append(" GRAPH ?g { ?s ?p ?o . }")
42+
count_query_parts.append(f" FILTER(?g NOT IN (<{'>, <'.join(VIRTUOSO_EXCLUDED_GRAPHS)}>))")
43+
else:
44+
count_query_parts.append(" ?s ?p ?o .")
45+
46+
count_query_parts.extend([
47+
f" FILTER(?o = <{subject_uri}>)",
48+
" FILTER(?p != <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>)",
49+
"}"
50+
])
51+
count_query = "\n".join(count_query_parts)
52+
53+
sparql.setQuery(count_query)
54+
sparql.setReturnFormat(JSON)
55+
count_results = sparql.query().convert()
56+
if count_results.get("results", {}).get("bindings", []):
57+
count_binding = count_results["results"]["bindings"][0]
58+
if "count" in count_binding:
59+
total_count = int(count_binding["count"]["value"])
60+
61+
# Main Query with pagination
62+
query_parts = [
63+
"SELECT DISTINCT ?s ?p WHERE {",
64+
]
65+
if is_virtuoso:
66+
query_parts.append(" GRAPH ?g { ?s ?p ?o . }")
67+
query_parts.append(f" FILTER(?g NOT IN (<{'>, <'.join(VIRTUOSO_EXCLUDED_GRAPHS)}>))")
68+
else:
69+
query_parts.append(" ?s ?p ?o .")
70+
71+
query_parts.extend([
72+
f" FILTER(?o = <{subject_uri}>)",
73+
" FILTER(?p != <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>)",
74+
f"}} ORDER BY ?s OFFSET {offset} LIMIT {limit}"
75+
])
76+
main_query = "\n".join(query_parts)
77+
78+
sparql.setQuery(main_query)
79+
sparql.setReturnFormat(JSON)
80+
results = sparql.query().convert()
81+
82+
bindings = results.get("results", {}).get("bindings", [])
83+
for result in bindings:
84+
subject = result["s"]["value"]
85+
predicate = result["p"]["value"]
86+
87+
# Get the type of the referring entity
88+
types = get_entity_types(subject)
89+
highest_priority_type = get_highest_priority_class(types) if types else None
90+
display_types = [highest_priority_type] if highest_priority_type else []
91+
type_labels = [custom_filter.human_readable_predicate(t, display_types) for t in display_types] if display_types else []
92+
label = custom_filter.human_readable_entity(subject, display_types)
93+
94+
references.append({
95+
"subject": subject,
96+
"predicate": predicate,
97+
"predicate_label": custom_filter.human_readable_predicate(predicate, display_types),
98+
"types": display_types,
99+
"type_labels": type_labels,
100+
"label": label
101+
})
102+
103+
has_more = offset + limit < total_count
104+
return references, total_count, has_more
105+
106+
except Exception as e:
107+
tb_str = traceback.format_exc()
108+
current_app.logger.error(f"Error fetching inverse references for {subject_uri}: {e}\n{tb_str}")
109+
return [], 0, False
110+
111+
@linked_resources_bp.route("/", methods=["GET"])
112+
@login_required
113+
def get_linked_resources_api():
114+
"""API endpoint to fetch paginated linked resources (inverse references)."""
115+
subject_uri = request.args.get("subject_uri")
116+
try:
117+
limit = int(request.args.get("limit", 5))
118+
offset = int(request.args.get("offset", 0))
119+
except ValueError:
120+
return jsonify({"status": "error", "message": gettext("Invalid limit or offset parameter")}), 400
121+
122+
if not subject_uri:
123+
return jsonify({"status": "error", "message": gettext("Missing subject_uri parameter")}), 400
124+
125+
if limit <= 0 or offset < 0:
126+
return jsonify({"status": "error", "message": gettext("Limit must be positive and offset non-negative")}), 400
127+
128+
references, total_count, has_more = get_paginated_inverse_references(subject_uri, limit, offset)
129+
130+
return jsonify({
131+
"status": "success",
132+
"results": references,
133+
"total_count": total_count,
134+
"has_more": has_more
135+
})

0 commit comments

Comments
 (0)