Import HAL : match des auteurs sur toutes les formes d'idHAL (résolution en fond via cron)
This commit is contained in:
@@ -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)));
|
||||
?>
|
||||
<details style="margin-bottom:15px;background:#f0f6fc;padding:10px;border-radius:4px;border-left:4px solid #0073aa">
|
||||
<summary style="cursor:pointer;font-weight:bold">
|
||||
Utilisateurs WordPress avec identifiant HAL (<?php echo count($this->wp_users_by_hal_id); ?> utilisateurs<?php
|
||||
if ($invalid_count > 0) echo ', <span style="color:#dc3545">' . $invalid_count . ' invalide(s)</span>';
|
||||
Utilisateurs WordPress avec identifiant HAL (<?php echo $distinct_users; ?> utilisateurs<?php
|
||||
if ($invalid_count > 0) echo ', <span style="color:#dc3545">' . $invalid_count . ' forme(s) invalide(s)</span>';
|
||||
?>) — Cliquer pour déplier
|
||||
</summary>
|
||||
<table class="widefat" style="margin-top:10px;font-size:12px">
|
||||
<thead><tr><th>Utilisateur</th><th>Identifiant HAL</th><th>Validité HAL</th><th>Debug (brut)</th><th>Modifier</th></tr></thead>
|
||||
<thead><tr><th>Utilisateur</th><th>Identifiant HAL (formes)</th><th>Validité HAL</th><th>Debug (brut)</th><th>Modifier</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($this->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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
115
includes/class-hal-forms-cache.php
Normal file
115
includes/class-hal-forms-cache.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
/**
|
||||
* Background resolver + cache for alternate idHAL forms.
|
||||
*
|
||||
* Problem: a member's WP `identifiant_hal` stores ONE idHAL form, but HAL may
|
||||
* tag their publications under other valid forms of the same person. Matching
|
||||
* on the single stored form silently drops those links.
|
||||
*
|
||||
* Fix: resolve every member's full set of valid idHAL forms (via HAL ref/author)
|
||||
* and cache them in usermeta `_hal_id_forms`. Matching then uses the cached
|
||||
* forms — with ZERO network calls in the UI / import path.
|
||||
*
|
||||
* All resolution happens in the background via WP-Cron, in small batches, so the
|
||||
* admin interface and the import flow stay as fast as before.
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Thalim_HAL_Forms_Cache {
|
||||
|
||||
const META_FORMS = '_hal_id_forms'; // usermeta: JSON array of forms
|
||||
const META_SOURCE = '_hal_id_forms_src'; // usermeta: the identifiant_hal the cache was built from
|
||||
const CRON_HOOK = 'thalim_hal_refresh_forms';
|
||||
const BATCH_SIZE = 15; // members resolved per cron tick
|
||||
|
||||
/** Wire the background hooks. Called once at plugins_loaded. */
|
||||
public static function init() {
|
||||
add_action(self::CRON_HOOK, [__CLASS__, 'run_batch']);
|
||||
|
||||
// Ensure the recurring event exists (hourly).
|
||||
if (!wp_next_scheduled(self::CRON_HOOK)) {
|
||||
wp_schedule_event(time() + 60, 'hourly', self::CRON_HOOK);
|
||||
}
|
||||
|
||||
// When a profile's identifiant_hal changes, invalidate its cache so the
|
||||
// next background tick re-resolves it (no synchronous API call here).
|
||||
add_action('updated_user_meta', [__CLASS__, 'on_user_meta_change'], 10, 4);
|
||||
add_action('added_user_meta', [__CLASS__, 'on_user_meta_change'], 10, 4);
|
||||
}
|
||||
|
||||
/** Remove the scheduled event (plugin deactivation). */
|
||||
public static function clear_schedule() {
|
||||
$ts = wp_next_scheduled(self::CRON_HOOK);
|
||||
if ($ts) wp_unschedule_event($ts, self::CRON_HOOK);
|
||||
}
|
||||
|
||||
/** Invalidate a user's cache when their identifiant_hal is edited. */
|
||||
public static function on_user_meta_change($meta_id, $user_id, $meta_key, $_meta_value) {
|
||||
if ($meta_key === 'identifiant_hal') {
|
||||
delete_user_meta($user_id, self::META_SOURCE); // marks the cache stale
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read cached alternate forms for a user's identifiant_hal.
|
||||
* Always returns at least the stored form itself (graceful degradation:
|
||||
* behaves exactly like the old code until the background job has run).
|
||||
*
|
||||
* @return string[] lowercased idHAL forms.
|
||||
*/
|
||||
public static function forms_for_user($user_id, $identifiant_hal) {
|
||||
$base = strtolower(trim((string) $identifiant_hal));
|
||||
if ($base === '') return [];
|
||||
|
||||
$src = get_user_meta($user_id, self::META_SOURCE, true);
|
||||
if ($src === $base) {
|
||||
$cached = json_decode((string) get_user_meta($user_id, self::META_FORMS, true), true);
|
||||
if (is_array($cached) && $cached) {
|
||||
$cached[] = $base;
|
||||
return array_values(array_unique(array_map('strtolower', $cached)));
|
||||
}
|
||||
}
|
||||
// Not resolved yet (or stale) -> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user