Coverage for heritrace / apis / orcid.py: 94%
62 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-21 12:56 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-21 12:56 +0000
1# SPDX-FileCopyrightText: 2024-2025 Arcangelo Massari <arcangelo.massari@unibo.it>
2#
3# SPDX-License-Identifier: ISC
5from functools import lru_cache
6from urllib.parse import urlparse
8import requests
9from flask import current_app
12def is_orcid_url(url):
13 """Check if a URL is an ORCID URL."""
14 try:
15 parsed = urlparse(url)
16 return parsed.netloc == "orcid.org"
17 except:
18 return False
21def extract_orcid_id(url):
22 """Extract ORCID ID from URL."""
23 try:
24 parsed = urlparse(url)
25 path = parsed.path.strip("/")
26 if path.startswith("https://orcid.org/"):
27 path = path[len("https://orcid.org/") :]
28 return path
29 except:
30 return None
33@lru_cache(maxsize=1000)
34def get_orcid_data(orcid_id):
35 """
36 Fetch researcher data from ORCID API with caching.
38 In demo mode, this function returns synthetic data without calling the external API.
40 Args:
41 orcid_id (str): The ORCID identifier
43 Returns:
44 dict: Researcher data including name and other details
45 """
46 if current_app.config.get("ENV") == "demo":
47 return {
48 "name": f"Demo User ({orcid_id})",
49 "other_names": [],
50 "biography": "This is a synthetic user account for demo purposes.",
51 "orcid": orcid_id,
52 }
54 headers = {"Accept": "application/json"}
56 try:
57 response = requests.get(
58 f"https://pub.orcid.org/v3.0/{orcid_id}/person", headers=headers, timeout=5
59 )
61 if response.status_code == 200:
62 data = response.json()
64 # Extract relevant information
65 result = {
66 "name": None,
67 "other_names": [],
68 "biography": None,
69 "orcid": orcid_id,
70 }
72 # Get main name
73 if "name" in data:
74 given_name = data["name"].get("given-names", {}).get("value", "")
75 family_name = data["name"].get("family-name", {}).get("value", "")
76 if given_name or family_name:
77 result["name"] = f"{given_name} {family_name}".strip()
79 # Get other names
80 if "other-names" in data and "other-name" in data["other-names"]:
81 result["other_names"] = [
82 name.get("content", "")
83 for name in data["other-names"]["other-name"]
84 if "content" in name
85 ]
87 # Get biography
88 if "biography" in data and data["biography"]:
89 result["biography"] = data["biography"].get("content", "")
91 return result
93 except Exception:
94 return None
96 return None
99def get_responsible_agent_uri(user_identifier):
100 """
101 Get the appropriate URI for a responsible agent.
103 This function handles both ORCID IDs and full URIs flexibly:
104 - If the identifier is already a full URI (starts with http/https), use it as-is
105 - If it's an ORCID ID, convert it to the standard ORCID URI format
106 - Otherwise, treat it as a generic identifier
108 Args:
109 user_identifier (str): User identifier (ORCID ID, URI, or other)
111 Returns:
112 str: Full URI for the responsible agent
113 """
114 if not user_identifier:
115 return None
117 if user_identifier.startswith(('http://', 'https://')):
118 return user_identifier
120 if len(user_identifier) == 19 and user_identifier.count('-') == 3:
121 return f"https://orcid.org/{user_identifier}"
123 return user_identifier
126def format_orcid_attribution(url):
127 """
128 Format ORCID attribution for display.
130 Args:
131 url (str): The ORCID URL
133 Returns:
134 str: Formatted HTML for displaying ORCID attribution
135 """
137 orcid_id = extract_orcid_id(url)
138 if not orcid_id:
139 return f'<a href="{url}" target="_blank">{url}</a>'
141 researcher_data = get_orcid_data(orcid_id)
142 if not researcher_data:
143 return f'<a href="{url}" target="_blank">{url}</a>'
145 name = researcher_data["name"] or url
147 html = f'<a href="{url}" target="_blank" class="orcid-attribution">'
148 html += f'<img src="/static/images/orcid-logo.png" alt="ORCID iD" class="orcid-icon mx-1 mb-1" style="width: 16px; height: 16px;">'
149 html += f"{name} [orcid:{orcid_id}]</a>"
151 return html