Coverage for heritrace/apis/orcid.py: 94%
62 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-08-01 22:12 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-08-01 22:12 +0000
1from functools import lru_cache
2from urllib.parse import urlparse
4import requests
5from flask import current_app
8def is_orcid_url(url):
9 """Check if a URL is an ORCID URL."""
10 try:
11 parsed = urlparse(url)
12 return parsed.netloc == "orcid.org"
13 except:
14 return False
17def extract_orcid_id(url):
18 """Extract ORCID ID from URL."""
19 try:
20 parsed = urlparse(url)
21 path = parsed.path.strip("/")
22 if path.startswith("https://orcid.org/"):
23 path = path[len("https://orcid.org/") :]
24 return path
25 except:
26 return None
29@lru_cache(maxsize=1000)
30def get_orcid_data(orcid_id):
31 """
32 Fetch researcher data from ORCID API with caching.
34 In demo mode, this function returns synthetic data without calling the external API.
36 Args:
37 orcid_id (str): The ORCID identifier
39 Returns:
40 dict: Researcher data including name and other details
41 """
42 if current_app.config.get("ENV") == "demo":
43 return {
44 "name": f"Demo User ({orcid_id})",
45 "other_names": [],
46 "biography": "This is a synthetic user account for demo purposes.",
47 "orcid": orcid_id,
48 }
50 headers = {"Accept": "application/json"}
52 try:
53 response = requests.get(
54 f"https://pub.orcid.org/v3.0/{orcid_id}/person", headers=headers, timeout=5
55 )
57 if response.status_code == 200:
58 data = response.json()
60 # Extract relevant information
61 result = {
62 "name": None,
63 "other_names": [],
64 "biography": None,
65 "orcid": orcid_id,
66 }
68 # Get main name
69 if "name" in data:
70 given_name = data["name"].get("given-names", {}).get("value", "")
71 family_name = data["name"].get("family-name", {}).get("value", "")
72 if given_name or family_name:
73 result["name"] = f"{given_name} {family_name}".strip()
75 # Get other names
76 if "other-names" in data and "other-name" in data["other-names"]:
77 result["other_names"] = [
78 name.get("content", "")
79 for name in data["other-names"]["other-name"]
80 if "content" in name
81 ]
83 # Get biography
84 if "biography" in data and data["biography"]:
85 result["biography"] = data["biography"].get("content", "")
87 return result
89 except Exception:
90 return None
92 return None
95def get_responsible_agent_uri(user_identifier):
96 """
97 Get the appropriate URI for a responsible agent.
99 This function handles both ORCID IDs and full URIs flexibly:
100 - If the identifier is already a full URI (starts with http/https), use it as-is
101 - If it's an ORCID ID, convert it to the standard ORCID URI format
102 - Otherwise, treat it as a generic identifier
104 Args:
105 user_identifier (str): User identifier (ORCID ID, URI, or other)
107 Returns:
108 str: Full URI for the responsible agent
109 """
110 if not user_identifier:
111 return None
113 if user_identifier.startswith(('http://', 'https://')):
114 return user_identifier
116 if len(user_identifier) == 19 and user_identifier.count('-') == 3:
117 return f"https://orcid.org/{user_identifier}"
119 return user_identifier
122def format_orcid_attribution(url):
123 """
124 Format ORCID attribution for display.
126 Args:
127 url (str): The ORCID URL
129 Returns:
130 str: Formatted HTML for displaying ORCID attribution
131 """
133 orcid_id = extract_orcid_id(url)
134 if not orcid_id:
135 return f'<a href="{url}" target="_blank">{url}</a>'
137 researcher_data = get_orcid_data(orcid_id)
138 if not researcher_data:
139 return f'<a href="{url}" target="_blank">{url}</a>'
141 name = researcher_data["name"] or url
143 html = f'<a href="{url}" target="_blank" class="orcid-attribution">'
144 html += f'<img src="/static/images/orcid-logo.png" alt="ORCID iD" class="orcid-icon mx-1 mb-1" style="width: 16px; height: 16px;">'
145 html += f"{name} [orcid:{orcid_id}]</a>"
147 return html