Merge pull request 'feat: preparar migración del evangelio diario (issue #20)' from feat/issue-20-deploy into main
Aprobado por Rafa. Revisión (Claudix, 2026-08-30): importador idempotente con guardas de match dudoso/colisión antes de escribir; despliegue del JSON del calendario cableado en el propio import; fallback civil mientras no está desplegado. Dry-run contra producción limpio tras despejar 5 casos de contenido duplicado real (497/500 en cobertura, 304 existentes + 196 nuevos = 500, 0 ambiguos/duplicados/sin-coincidencia).
This commit is contained in:
@@ -1204,8 +1204,9 @@ add_shortcode('fea_evangelio', function($atts) {
|
|||||||
// "El Evangelio de cada día": dos devocionales diarios indexados por día del año.
|
// "El Evangelio de cada día": dos devocionales diarios indexados por día del año.
|
||||||
// · A la fuente cada día (texto, Fray Marcos) → categoría term_id 14
|
// · A la fuente cada día (texto, Fray Marcos) → categoría term_id 14
|
||||||
// · Otro evangelio es posible (vídeo YouTube) → categoría term_id 15
|
// · Otro evangelio es posible (vídeo YouTube) → categoría term_id 15
|
||||||
// Los posts están titulados "D mes" (ej. "21 junio"). Se muestra el de HOY
|
// El texto de Fray Marcos se resuelve contra el calendario litúrgico versionado
|
||||||
// (zona horaria del sitio) o el día indicado por ?fed=M-D, con pestañas para
|
// (fecha ISO -> book_index), no por el título civil heredado de Joomla. Se muestra
|
||||||
|
// el de HOY (zona horaria del sitio) o el día indicado por ?fed=M-D, con pestañas para
|
||||||
// que el usuario elija formato y navegación día anterior / siguiente.
|
// que el usuario elija formato y navegación día anterior / siguiente.
|
||||||
// Contenido solo en ES (devocional sin traducción) → categorías 14/15 fijas.
|
// Contenido solo en ES (devocional sin traducción) → categorías 14/15 fijas.
|
||||||
add_shortcode('fea_evangelio_diario', function($atts) {
|
add_shortcode('fea_evangelio_diario', function($atts) {
|
||||||
@@ -1220,23 +1221,61 @@ add_shortcode('fea_evangelio_diario', function($atts) {
|
|||||||
$gm = (int) $mm[1]; $gd = (int) $mm[2];
|
$gm = (int) $mm[1]; $gd = (int) $mm[2];
|
||||||
if ($gm >= 1 && $gm <= 12 && $gd >= 1 && $gd <= 31) { $m = $gm; $d = $gd; }
|
if ($gm >= 1 && $gm <= 12 && $gd >= 1 && $gd <= 31) { $m = $gm; $d = $gd; }
|
||||||
}
|
}
|
||||||
$titulo_dia = $d . ' ' . $MESES[$m]; // "21 junio"
|
$date = $now->setDate((int) $now->format('Y'), $m, $d);
|
||||||
|
$calendar_path = defined('FEA_EVANGELIO_CALENDAR_PATH')
|
||||||
|
? FEA_EVANGELIO_CALENDAR_PATH
|
||||||
|
: WP_CONTENT_DIR . '/fea-data/calendario-liturgico-2026-2040.json';
|
||||||
|
$calendar = is_readable($calendar_path)
|
||||||
|
? json_decode((string) file_get_contents($calendar_path), true)
|
||||||
|
: null;
|
||||||
|
$book_index = (int) ($calendar['dates'][$date->format('Y-m-d')]['book_index'] ?? 0);
|
||||||
|
|
||||||
$find = function($cat) use ($titulo_dia) {
|
$find_text = function($index) {
|
||||||
|
global $wpdb;
|
||||||
|
if ($index < 1) return null;
|
||||||
|
$id = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT p.ID FROM {$wpdb->posts} p
|
||||||
|
JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
|
||||||
|
WHERE pm.meta_key = '_fea_book_index' AND pm.meta_value = %d
|
||||||
|
AND p.post_type = 'post' AND p.post_status = 'publish'
|
||||||
|
ORDER BY p.ID ASC LIMIT 1",
|
||||||
|
$index));
|
||||||
|
return $id ? get_post((int) $id) : null;
|
||||||
|
};
|
||||||
|
$texto = $find_text($book_index);
|
||||||
|
// El vídeo queda deliberadamente con el índice civil heredado: su catálogo
|
||||||
|
// no forma parte de la migración litúrgica de #20.
|
||||||
|
$titulo_dia = $d . ' ' . $MESES[$m];
|
||||||
|
$find_video = function() use ($titulo_dia) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$id = $wpdb->get_var($wpdb->prepare(
|
$id = $wpdb->get_var($wpdb->prepare(
|
||||||
"SELECT p.ID FROM {$wpdb->posts} p
|
"SELECT p.ID FROM {$wpdb->posts} p
|
||||||
JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
|
JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
|
||||||
JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
|
JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
|
||||||
WHERE tt.taxonomy = 'category' AND tt.term_id = %d
|
WHERE tt.taxonomy = 'category' AND tt.term_id = 15
|
||||||
AND p.post_type = 'post' AND p.post_status = 'publish'
|
AND p.post_type = 'post' AND p.post_status = 'publish'
|
||||||
AND LOWER(TRIM(p.post_title)) = %s
|
AND LOWER(TRIM(p.post_title)) = %s
|
||||||
ORDER BY p.ID ASC LIMIT 1",
|
ORDER BY p.ID ASC LIMIT 1", $titulo_dia));
|
||||||
$cat, $titulo_dia));
|
|
||||||
return $id ? get_post((int) $id) : null;
|
return $id ? get_post((int) $id) : null;
|
||||||
};
|
};
|
||||||
$texto = $find(14);
|
$video = $find_video();
|
||||||
$video = $find(15);
|
// Hasta que el importador instale el JSON y reindexe, conservar el contenido
|
||||||
|
// civil heredado evita dejar la pestaña de texto en blanco durante el despliegue.
|
||||||
|
if (!$texto && !$book_index) {
|
||||||
|
$legacy_text = function() use ($titulo_dia) {
|
||||||
|
global $wpdb;
|
||||||
|
$id = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT p.ID FROM {$wpdb->posts} p
|
||||||
|
JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
|
||||||
|
JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
|
||||||
|
WHERE tt.taxonomy = 'category' AND tt.term_id = 14
|
||||||
|
AND p.post_type = 'post' AND p.post_status = 'publish'
|
||||||
|
AND LOWER(TRIM(p.post_title)) = %s
|
||||||
|
ORDER BY p.ID ASC LIMIT 1", $titulo_dia));
|
||||||
|
return $id ? get_post((int) $id) : null;
|
||||||
|
};
|
||||||
|
$texto = $legacy_text();
|
||||||
|
}
|
||||||
|
|
||||||
// navegación por día del calendario (año irrelevante; usamos un año bisiesto fijo)
|
// navegación por día del calendario (año irrelevante; usamos un año bisiesto fijo)
|
||||||
$base = get_permalink();
|
$base = get_permalink();
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Migra A la fuente cada día al índice estable del libro.
|
||||||
|
*
|
||||||
|
* Uso (por defecto solo informa):
|
||||||
|
* wp eval-file scripts/import_evangelio_diario.php --path=/var/www/html
|
||||||
|
* Escritura explícita: FEA_EVANGELIO_APPLY=1 wp eval-file ...
|
||||||
|
*
|
||||||
|
* Requiere que entradas-libro.json esté junto a este script durante la ejecución.
|
||||||
|
* Nunca elimina posts: actualiza únicamente los ya identificados de categoría 14 y
|
||||||
|
* crea los índices ausentes. Es idempotente por _fea_book_index.
|
||||||
|
*/
|
||||||
|
$apply = getenv('FEA_EVANGELIO_APPLY') === '1';
|
||||||
|
$book_file = __DIR__ . '/../data/evangelio-diario/entradas-libro.json';
|
||||||
|
$calendar_file = __DIR__ . '/../data/evangelio-diario/calendario-liturgico-2026-2040.json';
|
||||||
|
if (!is_readable($book_file)) { fwrite(STDERR, "No se lee $book_file\n"); exit(2); }
|
||||||
|
if (!is_readable($calendar_file)) { fwrite(STDERR, "No se lee $calendar_file\n"); exit(2); }
|
||||||
|
$book = json_decode(file_get_contents($book_file), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (count($book) !== 500) { fwrite(STDERR, "El libro debe tener 500 entradas\n"); exit(2); }
|
||||||
|
|
||||||
|
$norm = static function ($text) {
|
||||||
|
$text = html_entity_decode(wp_strip_all_tags((string) $text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
return preg_replace('/\s+/u', ' ', trim(mb_strtolower($text, 'UTF-8')));
|
||||||
|
};
|
||||||
|
$by_motto = [];
|
||||||
|
foreach ($book as $i => $entry) {
|
||||||
|
$key = $norm($entry['motto']);
|
||||||
|
if ($key !== '') $by_motto[$key][] = $i + 1;
|
||||||
|
}
|
||||||
|
$existing = get_posts([
|
||||||
|
'post_type' => 'post', 'post_status' => 'any', 'posts_per_page' => -1,
|
||||||
|
'category' => 14, 'orderby' => 'ID', 'order' => 'ASC',
|
||||||
|
]);
|
||||||
|
$used = [];
|
||||||
|
$updated = $ambiguous = $unmatched = $duplicate = 0;
|
||||||
|
$to_update = [];
|
||||||
|
foreach ($existing as $post) {
|
||||||
|
$matches = [];
|
||||||
|
foreach ($by_motto as $motto => $indices) {
|
||||||
|
if (str_contains($norm($post->post_content), $motto)) $matches = array_merge($matches, $indices);
|
||||||
|
}
|
||||||
|
$matches = array_values(array_unique($matches));
|
||||||
|
if (count($matches) !== 1) {
|
||||||
|
$ambiguous += count($matches) > 1; $unmatched += count($matches) === 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isset($used[$matches[0]])) { $duplicate++; continue; }
|
||||||
|
$idx = $matches[0]; $used[$idx] = true;
|
||||||
|
$entry = $book[$idx - 1];
|
||||||
|
$to_update[] = [$post->ID, $idx, $entry];
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
$planned_new = count($book) - count($used);
|
||||||
|
if ($ambiguous || $unmatched || $duplicate) {
|
||||||
|
printf("DRY-RUN: existentes indexados=%d; nuevos=%d; ambiguos=%d; sin coincidencia=%d; duplicados=%d; total=%d\n",
|
||||||
|
$updated, $planned_new, $ambiguous, $unmatched, $duplicate, count($book));
|
||||||
|
fwrite(STDERR, "ABORTAR APLICACIÓN: hay posts existentes sin correspondencia única.\n");
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
// El JSON se instala antes de tocar contenido: el shortcode lo consume desde
|
||||||
|
// WP_CONTENT_DIR/fea-data y así código y calendario llegan como una unidad.
|
||||||
|
if ($apply) {
|
||||||
|
$calendar_target = WP_CONTENT_DIR . '/fea-data/calendario-liturgico-2026-2040.json';
|
||||||
|
if (!wp_mkdir_p(dirname($calendar_target)) || !copy($calendar_file, $calendar_target)) {
|
||||||
|
fwrite(STDERR, "No se pudo instalar el calendario en $calendar_target\n"); exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($apply) foreach ($to_update as [$post_id, $idx, $entry]) {
|
||||||
|
update_post_meta($post_id, '_fea_book_index', (string) $idx);
|
||||||
|
update_post_meta($post_id, '_fea_cita', $entry['citation']);
|
||||||
|
update_post_meta($post_id, '_fea_fuente', 'A la fuente cada día — Fray Marcos, entrada #' . $idx);
|
||||||
|
}
|
||||||
|
$created = 0;
|
||||||
|
foreach ($book as $i => $entry) {
|
||||||
|
$idx = $i + 1;
|
||||||
|
if (isset($used[$idx])) continue;
|
||||||
|
$body = '<h1>' . esc_html($entry['title']) . '</h1>'
|
||||||
|
. '<p><strong>' . esc_html($entry['citation']) . '</strong></p>'
|
||||||
|
. '<p>' . esc_html($entry['gospel']) . '</p>'
|
||||||
|
. '<p><em>' . esc_html($entry['motto']) . '</em></p>';
|
||||||
|
foreach ($entry['paragraphs'] as $paragraph) $body .= '<p>' . esc_html($paragraph) . '</p>';
|
||||||
|
if ($apply) {
|
||||||
|
$id = wp_insert_post(['post_title' => $entry['title'], 'post_content' => $body,
|
||||||
|
'post_status' => 'publish', 'post_category' => [14]], true);
|
||||||
|
if (is_wp_error($id)) { fwrite(STDERR, $id->get_error_message() . "\n"); exit(1); }
|
||||||
|
update_post_meta($id, '_fea_book_index', (string) $idx);
|
||||||
|
update_post_meta($id, '_fea_cita', $entry['citation']);
|
||||||
|
update_post_meta($id, '_fea_fuente', 'A la fuente cada día — Fray Marcos, entrada #' . $idx);
|
||||||
|
}
|
||||||
|
$created++;
|
||||||
|
}
|
||||||
|
printf("%s: existentes indexados=%d; nuevos=%d; ambiguos=%d; sin coincidencia=%d; duplicados=%d; total=%d\n",
|
||||||
|
$apply ? 'APLICADO' : 'DRY-RUN', $updated, $created, $ambiguous, $unmatched, $duplicate, count($book));
|
||||||
Reference in New Issue
Block a user