From f083b363693e50fc4d6cbee2ddc8dc59931194aa Mon Sep 17 00:00:00 2001 From: Valentin Le Moign Date: Tue, 7 Jul 2026 13:44:08 +0200 Subject: [PATCH] =?UTF-8?q?Import=20HAL=20:=20match=20des=20auteurs=20sur?= =?UTF-8?q?=20toutes=20les=20formes=20d'idHAL=20(r=C3=A9solution=20en=20fo?= =?UTF-8?q?nd=20via=20cron)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- includes/class-admin-page.php | 21 ++++-- includes/class-hal-api.php | 89 ++++++++++++++++++++++ includes/class-hal-forms-cache.php | 115 +++++++++++++++++++++++++++++ tests/run-tests.php | 37 ++++++++++ thalim-hal-importer.php | 5 ++ 5 files changed, 262 insertions(+), 5 deletions(-) create mode 100644 includes/class-hal-forms-cache.php diff --git a/includes/class-admin-page.php b/includes/class-admin-page.php index 94684fd..e2a49df 100644 --- a/includes/class-admin-page.php +++ b/includes/class-admin-page.php @@ -561,15 +561,18 @@ class Thalim_HAL_Admin_Page { $validity = $this->get_hal_ids_validity(array_keys($this->wp_users_by_hal_id)); $invalid_count = count(array_filter($validity, fn($v) => $v === false)); + // The map may hold several idHAL forms per user (alternate valid forms); + // count distinct users for the label, not raw form keys. + $distinct_users = count(array_unique(array_map(fn($u) => $u['id'], $this->wp_users_by_hal_id))); ?>
- Utilisateurs WordPress avec identifiant HAL (wp_users_by_hal_id); ?> utilisateurs 0) echo ', ' . $invalid_count . ' invalide(s)'; + Utilisateurs WordPress avec identifiant HAL ( utilisateurs 0) echo ', ' . $invalid_count . ' forme(s) invalide(s)'; ?>) — Cliquer pour déplier - + wp_users_by_hal_id as $hal_id => $user): $valid = $validity[$hal_id] ?? null; @@ -715,12 +718,20 @@ class Thalim_HAL_Admin_Page { foreach ($users as $user) { $hal_id = get_user_meta($user->ID, 'identifiant_hal', true); if (!empty($hal_id)) { - $normalized = strtolower(trim($hal_id)); - $this->wp_users_by_hal_id[$normalized] = [ + $entry = [ 'id' => $user->ID, 'name' => $user->display_name, 'hal_id' => trim($hal_id), // original value for API filter ]; + // Register the primary form AND every alternate valid idHAL form + // of the same person (resolved in the background, read from cache + // here — no network call). Lets a publication tagged under an + // alternate form still match the member. See Thalim_HAL_Forms_Cache. + foreach (Thalim_HAL_Forms_Cache::forms_for_user($user->ID, $hal_id) as $form) { + if (!isset($this->wp_users_by_hal_id[$form])) { + $this->wp_users_by_hal_id[$form] = $entry; + } + } } } } diff --git a/includes/class-hal-api.php b/includes/class-hal-api.php index 4384c4a..fbc7981 100644 --- a/includes/class-hal-api.php +++ b/includes/class-hal-api.php @@ -120,6 +120,95 @@ class Thalim_HAL_API { return $result; } + /** + * Expand a set of idHAL slugs to ALL valid idHAL forms of the same people. + * + * HAL frequently registers a person under several PREFERRED idHAL forms + * (e.g. "xavier-garnier" AND "xaviergarnier"). A publication may be tagged + * with any one of them, so matching a WP profile against a single stored + * form silently misses those tagged under an alternate form. + * + * ref/author exposes every form under the same person, sharing the same + * numeric root of `docid` ("6951-1022", "6951-180497" => person 6951). + * We resolve each input slug to its person root(s), then collect every + * idHAL form attached to those roots. + * + * NOTE: this hits the network. It is meant to be called from the background + * cron resolver (see Thalim_HAL_Forms_Cache), never from the UI/import path. + * + * @param string[] $idhals Slugs to expand (a member's stored identifiant_hal). + * @return array Map [input_slug_lowercased => [form1, form2, ...]]. + * On API error a slug maps to just itself. + */ + public function expand_hal_id_forms(array $idhals) { + $out = []; + $clean = []; + foreach ($idhals as $id) { + $id = strtolower(trim((string) $id)); + if ($id !== '') { $clean[$id] = true; $out[$id] = [$id]; } + } + if (empty($clean)) return $out; + + $endpoint = 'https://api.archives-ouvertes.fr/ref/author/'; + + // Step 1: for each slug, find the person root(s) of its docid. + $slug_roots = []; // slug => [root => true] + $all_roots = []; // root => true + foreach (array_chunk(array_keys($clean), 50) as $chunk) { + $quoted = array_map(fn($id) => '"' . str_replace('"', '', $id) . '"', $chunk); + $params = [ + 'q=' . urlencode('idHal_s:(' . implode(' OR ', $quoted) . ')'), + 'fl=' . urlencode('docid,idHal_s'), + 'rows=' . (count($chunk) * 5), + 'wt=json', + ]; + $data = $this->request($endpoint . '?' . implode('&', $params)); + if (is_wp_error($data)) continue; + foreach ($data['response']['docs'] ?? [] as $doc) { + $slug = strtolower($doc['idHal_s'] ?? ''); + $root = isset($doc['docid']) ? explode('-', $doc['docid'])[0] : ''; + if ($slug !== '' && $root !== '') { + $slug_roots[$slug][$root] = true; + $all_roots[$root] = true; + } + } + usleep(250000); + } + if (empty($all_roots)) return $out; + + // Step 2: collect every idHAL form attached to those person roots. + // NB: do NOT quote the "root-*" terms — quoting disables the Solr wildcard. + // Roots are numeric, so they are safe to inline; keep only digits defensively. + $root_forms = []; // root => [form => true] + foreach (array_chunk(array_keys($all_roots), 50) as $chunk) { + $terms = array_map(fn($r) => preg_replace('/\D/', '', $r) . '-*', $chunk); + $params = [ + 'q=' . urlencode('docid:(' . implode(' OR ', $terms) . ')'), + 'fl=' . urlencode('docid,idHal_s'), + 'rows=' . (count($chunk) * 10), + 'wt=json', + ]; + $data = $this->request($endpoint . '?' . implode('&', $params)); + if (is_wp_error($data)) continue; + foreach ($data['response']['docs'] ?? [] as $doc) { + $root = isset($doc['docid']) ? explode('-', $doc['docid'])[0] : ''; + $form = strtolower($doc['idHal_s'] ?? ''); + if ($root !== '' && $form !== '') $root_forms[$root][$form] = true; + } + usleep(250000); + } + + // Step 3: map each input slug to the union of forms across its roots. + foreach ($clean as $slug => $_) { + $forms = [$slug => true]; + foreach (array_keys($slug_roots[$slug] ?? []) as $root) { + foreach (array_keys($root_forms[$root] ?? []) as $f) $forms[$f] = true; + } + $out[$slug] = array_keys($forms); + } + return $out; + } + /** * Test API connection */ diff --git a/includes/class-hal-forms-cache.php b/includes/class-hal-forms-cache.php new file mode 100644 index 0000000..df9680d --- /dev/null +++ b/includes/class-hal-forms-cache.php @@ -0,0 +1,115 @@ + fall back to the single stored form. + return [$base]; + } + + /** + * Cron worker: resolve forms for a batch of members whose cache is missing + * or stale. Runs in the background; safe to be interrupted (idempotent). + */ + public static function run_batch() { + $users = get_users([ + 'meta_key' => 'identifiant_hal', + 'meta_compare' => 'EXISTS', + 'fields' => ['ID'], + 'number' => 500, + ]); + + // Select those needing (re)resolution: stored form != cached source. + $todo = []; + foreach ($users as $u) { + $idhal = strtolower(trim((string) get_user_meta($u->ID, 'identifiant_hal', true))); + if ($idhal === '') continue; + $src = get_user_meta($u->ID, self::META_SOURCE, true); + if ($src !== $idhal) { + $todo[$idhal][] = (int) $u->ID; // group users sharing a slug + } + if (count($todo) >= self::BATCH_SIZE) break; + } + if (empty($todo)) return; + + $api = new Thalim_HAL_API(); + $expanded = $api->expand_hal_id_forms(array_keys($todo)); + + foreach ($todo as $idhal => $user_ids) { + $forms = $expanded[$idhal] ?? [$idhal]; + foreach ($user_ids as $uid) { + update_user_meta($uid, self::META_FORMS, wp_json_encode(array_values($forms))); + update_user_meta($uid, self::META_SOURCE, $idhal); + } + } + } +} diff --git a/tests/run-tests.php b/tests/run-tests.php index 4e7b9b7..1074886 100644 --- a/tests/run-tests.php +++ b/tests/run-tests.php @@ -61,5 +61,42 @@ check('champ etiquettes = 652',Thalim_HAL_Pods_Storage::field_id('etiquettes'), check('champ axes = 270', Thalim_HAL_Pods_Storage::field_id('axes_thematiques'), 270); check('champ inconnu = 0', Thalim_HAL_Pods_Storage::field_id('champ_bidon'), 0); +echo "== Thalim_HAL_Forms_Cache::forms_for_user (lecture cache, sans réseau) ==\n"; +// Dégradation gracieuse : cache absent -> forme unique. +check('cache absent -> forme unique', + Thalim_HAL_Forms_Cache::forms_for_user(999999999, 'xavier-garnier'), + ['xavier-garnier']); +check('idHAL vide -> tableau vide', + Thalim_HAL_Forms_Cache::forms_for_user(999999999, ''), + []); + +// Cache présent et à jour, sur un vrai user (métas écrites puis restaurées). +$real = get_users(['meta_key' => 'identifiant_hal', 'meta_compare' => 'EXISTS', + 'fields' => ['ID'], 'number' => 1]); +if (!empty($real)) { + $ruid = (int) $real[0]->ID; + $idhal = strtolower(trim((string) get_user_meta($ruid, 'identifiant_hal', true))); + $bak_forms = get_user_meta($ruid, Thalim_HAL_Forms_Cache::META_FORMS, true); + $bak_src = get_user_meta($ruid, Thalim_HAL_Forms_Cache::META_SOURCE, true); + + update_user_meta($ruid, Thalim_HAL_Forms_Cache::META_FORMS, + wp_json_encode([$idhal, $idhal . '-alt'])); + update_user_meta($ruid, Thalim_HAL_Forms_Cache::META_SOURCE, $idhal); + $got = Thalim_HAL_Forms_Cache::forms_for_user($ruid, $idhal); sort($got); + $exp = [$idhal, $idhal . '-alt']; sort($exp); + check('cache présent -> formes alternatives incluses', $got, $exp); + + // Cache périmé (source != idHAL courant) -> forme unique. + update_user_meta($ruid, Thalim_HAL_Forms_Cache::META_SOURCE, 'obsolete-source'); + check('cache périmé -> forme unique', + Thalim_HAL_Forms_Cache::forms_for_user($ruid, $idhal), [$idhal]); + + // Restauration de l'état initial. + if ($bak_src === '') { delete_user_meta($ruid, Thalim_HAL_Forms_Cache::META_SOURCE); } + else { update_user_meta($ruid, Thalim_HAL_Forms_Cache::META_SOURCE, $bak_src); } + if ($bak_forms === '') { delete_user_meta($ruid, Thalim_HAL_Forms_Cache::META_FORMS); } + else { update_user_meta($ruid, Thalim_HAL_Forms_Cache::META_FORMS, $bak_forms); } +} + echo "\n$count tests, $failures échec(s)\n"; exit($failures ? 1 : 0); diff --git a/thalim-hal-importer.php b/thalim-hal-importer.php index 107b316..59cee19 100644 --- a/thalim-hal-importer.php +++ b/thalim-hal-importer.php @@ -46,6 +46,7 @@ class Thalim_HAL_Importer { private function load_dependencies() { require_once THALIM_HAL_PLUGIN_DIR . 'includes/class-hal-api.php'; + require_once THALIM_HAL_PLUGIN_DIR . 'includes/class-hal-forms-cache.php'; // Traits must be loaded before the class that `use`s them. require_once THALIM_HAL_PLUGIN_DIR . 'includes/trait-admin-page-config.php'; require_once THALIM_HAL_PLUGIN_DIR . 'includes/trait-admin-page-csv-legacy.php'; @@ -56,6 +57,8 @@ class Thalim_HAL_Importer { private function init_hooks() { add_action('admin_menu', [$this, 'add_admin_menu']); + // Background resolver of alternate idHAL forms (WP-Cron; no UI cost). + Thalim_HAL_Forms_Cache::init(); } public function add_admin_menu() { @@ -100,6 +103,8 @@ register_activation_hook(__FILE__, function() { // Deactivation hook register_deactivation_hook(__FILE__, function() { delete_transient('thalim_hal_preview_data'); + require_once THALIM_HAL_PLUGIN_DIR . 'includes/class-hal-forms-cache.php'; + Thalim_HAL_Forms_Cache::clear_schedule(); }); // Initialize plugin
UtilisateurIdentifiant HALValidité HALDebug (brut)Modifier
UtilisateurIdentifiant HAL (formes)Validité HALDebug (brut)Modifier