From 6de277d4f0eeab602b1388e3b42bb857a1c7ae48 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Thu, 7 May 2026 12:43:10 +0200 Subject: [PATCH] feat: add display name resolution for researchers from ORCID - Introduced a new function to fetch and extract the display name of researchers from the ORCID API. - Updated the researcher search response to set the display name if it is not already defined, enhancing researcher data accuracy. --- backend/app/api/researchers.py | 12 ++++++- backend/app/services/orcid_client.py | 47 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/backend/app/api/researchers.py b/backend/app/api/researchers.py index 927aafa..c13b8e3 100644 --- a/backend/app/api/researchers.py +++ b/backend/app/api/researchers.py @@ -15,7 +15,7 @@ from app.schema.researcher import ( ResearcherWithPublicationsSchema, ) from app.services.normalizer import PublicationNormalizer -from app.services.orcid_client import get_works_summary, get_work_detail +from app.services.orcid_client import get_display_name, get_works_summary, get_work_detail from app.schema.publication import PublicationSchema from app.db.models import PublicationDownload from app.security.jwt import get_optional_current_researcher @@ -159,6 +159,16 @@ def build_search_response(orcid_id: str, db: Session, current: Researcher | None db.add(researcher) db.flush() + # Si todavía no conocemos el nombre del investigador (por ejemplo, recién + # creado al sincronizarse desde el buscador), lo resolvemos contra el + # endpoint `/record` público de ORCID. No tocamos un nombre ya existente + # para no pisar valores establecidos por el flujo de autenticación. + if not researcher.name: + display_name = get_display_name(orcid_id) + if display_name: + researcher.name = display_name + db.flush() + publications = _upsert_researcher_publications(researcher, orcid_id, db) publications_out = _decorate_downloaded_by_me(db=db, current=current, publications=publications) stats = build_researcher_stats(publications_out) diff --git a/backend/app/services/orcid_client.py b/backend/app/services/orcid_client.py index 2e15add..f0535dd 100644 --- a/backend/app/services/orcid_client.py +++ b/backend/app/services/orcid_client.py @@ -148,3 +148,50 @@ def get_works_summary(orcid_id: str) -> dict: def get_work_detail(orcid_id: str, put_code: int) -> dict | None: client = ORCIDClient() return client.fetch_work_detail(orcid_id, put_code) + + +def get_record(orcid_id: str) -> dict: + client = ORCIDClient() + return client.fetch_record(orcid_id) + + +def extract_display_name(record: dict | None) -> str | None: + """ + Devuelve un nombre legible a partir de la respuesta de `/record` de ORCID. + + Prioriza `credit-name` (el nombre tal y como el investigador prefiere mostrarlo); + si no está disponible, compone `given-names` + `family-name`. + """ + if not record: + return None + + name = (record.get("person") or {}).get("name") or {} + + credit = name.get("credit-name") + if isinstance(credit, dict): + credit_value = credit.get("value") + if credit_value: + return credit_value + + given_obj = name.get("given-names") + family_obj = name.get("family-name") + given = given_obj.get("value") if isinstance(given_obj, dict) else None + family = family_obj.get("value") if isinstance(family_obj, dict) else None + + full = " ".join(part for part in (given, family) if part) + return full or None + + +def get_display_name(orcid_id: str) -> str | None: + """ + Obtiene el nombre público del investigador desde ORCID. + + Devuelve `None` (sin propagar la excepción) si la API de ORCID no responde + o el `record` no contiene un nombre utilizable, para no romper el flujo de + búsqueda cuando solo falla la resolución del nombre. + """ + try: + record = get_record(orcid_id) + except Exception: + return None + return extract_display_name(record)