Añadir mu-plugins y scripts de feadulta

This commit is contained in:
2026-06-28 15:10:46 -04:00
parent bce7e42f44
commit b6116b066d
106 changed files with 17600 additions and 2 deletions
+298
View File
@@ -0,0 +1,298 @@
#!/usr/bin/env python3
"""
Regenera `clasificacion_articulos.csv` recorriendo las cartas semanales y
extrayendo los links que cada una agrupa por encabezado (Artículos, Evangelio,
Eucaristía, Multimedia, EFFA). Paridad con `wp-content/mu-plugins/fea-carta-portada.php`.
Output: post_id, post_title, categoria_propuesta, seccion_original, carta_id, carta_titulo, carta_fecha
ALCANCE (vs. CSV histórico de marzo 2026):
- Cubre las 5 secciones estándar: comentario, articulo, eucaristia, multimedia, effa.
- Sub-clasifica por posición dentro de "Evangelio y comentarios al Evangelio":
· pos 1 → `lectura` (cita del evangelio)
· pos 2 → `comentario_editorial`
· pos 3+ → `comentario`
(regla del editor, confirmada contra el CSV histórico)
- NO cubre todavía:
· `lectura` dentro de eucaristía (lecturas bíblicas — el viejo las separa, aquí van todas a `eucaristia`)
· `otro` (catch-all del catch-all)
· `noticia` (subgrupo poco usado, 12 filas en el viejo)
· Encabezados de fiestas especiales ("Domingo de Resurrección", "Navidad", "Vigilia Pascual", etc.)
Para regenerar el CSV con cobertura completa habría que ampliar el mapping en
SECTION_PATTERNS y SECTION_LABELS con reglas adicionales. El CSV histórico
existente (raíz del repo) sirve como baseline para esa cobertura granular.
Uso:
python3 regenerar_clasificacion_csv.py [--out /path/clasificacion_articulos.csv] [--diff /path/csv_marzo.csv]
Issue: rafa/feadulta#42
"""
import argparse, csv, os, re, subprocess, sys
try:
import pymysql
except ImportError:
sys.exit('pymysql requerido: pip install --user pymysql')
# Mapping sección encabezado → cat slug (debe espejar fea-carta-portada.php)
SECTION_PATTERNS = [
('comentario', re.compile(r'Evangelio\s+y\s+comentarios\s+al\s+Evangelio', re.I)),
('articulo', re.compile(r'Art[ií]culos\s+seleccionados\s+para\s+la\s+semana', re.I)),
('eucaristia', re.compile(r'Para\s+unas\s+eucarist[ií]as\s+m[áa]s\s+participativas', re.I)),
('multimedia', re.compile(r'Material\s+multimedia', re.I)),
('effa', re.compile(r'Escuela\s+EFFA', re.I)),
]
# Nombres “bonitos” usados en seccion_original (verbatim del CSV histórico)
SECTION_LABELS = {
'comentario': 'Evangelio y comentarios al Evangelio',
'articulo': 'Artículos seleccionados para la semana',
'eucaristia': 'Para unas eucaristías más participativas y actuales',
'multimedia': 'Material multimedia',
'effa': 'Escuela EFFA',
}
CAT_PROPUESTA = {
'comentario': 'comentario',
'articulo': 'articulo',
'eucaristia': 'eucaristia',
'multimedia': 'multimedia',
'effa': 'effa',
}
# Sub-clasificación posicional dentro de la sección "Evangelio y comentarios al Evangelio".
# El editor SIEMPRE coloca: 1º lectura del evangelio, 2º comentario editorial, 3º+ comentarios.
SUBCAT_EVANGELIO_BY_POS = ['lectura', 'comentario_editorial'] # resto = 'comentario'
HREF_RX = re.compile(r'href=["\']([^"\']+)["\']', re.I)
WP_SLUG_RX = re.compile(r'(?:^|/)fea/([a-z0-9\-]+)/?(?:[?#]|$)', re.I)
K2_ITEM_RX = re.compile(r'/item/(\d+)-[^/"]+\.html', re.I)
RESERVED_SLUGS = {'wp-admin','wp-content','category','tag','author','page','en','fr','it','pt'}
def get_conn():
ip = subprocess.run(
['docker', 'inspect', 'wordpress-mysql', '--format',
'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'],
capture_output=True, text=True, check=True,
).stdout.strip()
return pymysql.connect(
host=ip, user='wordpress_user', password='wordpress_pass',
database='wordpress_db', charset='utf8mb4', autocommit=True,
)
def mysql(query, conn=None):
"""Ejecuta query y devuelve filas (lista de tuplas) — pymysql, sin parsing CLI."""
own = False
if conn is None:
conn = get_conn(); own = True
try:
with conn.cursor() as c:
c.execute(query)
return list(c.fetchall())
finally:
if own: conn.close()
def fetch_cartas(conn):
"""Todas las cartas (cat 6 actual, 22 semana pasada, 21 otras) + sus contenidos."""
q = """
SELECT p.ID, p.post_title, DATE(p.post_date), p.post_content
FROM wp_posts p
WHERE p.post_status='publish' AND p.post_type='post'
AND p.ID IN (
SELECT DISTINCT tr.object_id FROM wp_term_relationships tr
JOIN wp_term_taxonomy tt ON tt.term_taxonomy_id=tr.term_taxonomy_id
WHERE tt.term_id IN (6, 21, 22) AND tt.taxonomy='category'
)
ORDER BY p.post_date DESC;
"""
return mysql(q, conn)
def build_lookups(conn):
"""Construye dicts slug→post_id y k2_id→post_id para no machacar la BD por cada link.
Para slugs duplicados (varios posts con mismo slug), se usa el MÁS RECIENTE
(criterio espejo del mu-plugin fea-carta-portada.php tras el bug detectado en #38).
"""
print('Cargando lookups (slug y k2_id) ...', file=sys.stderr, flush=True)
slug_to_id = {}
rows = mysql("""
SELECT p1.post_name, p1.ID
FROM wp_posts p1
WHERE p1.post_status='publish' AND p1.post_type='post' AND p1.post_name<>''
ORDER BY p1.post_date DESC;
""", conn)
for r in rows:
slug = r[0]
if slug not in slug_to_id: # primero (más reciente) gana
slug_to_id[slug] = int(r[1])
k2_to_id = {}
rows = mysql("""
SELECT meta_value, MAX(post_id) FROM wp_postmeta
WHERE meta_key='_fgj2wp_old_k2_id' AND meta_value<>''
GROUP BY meta_value;
""", conn)
for r in rows:
try:
k2_to_id[int(r[0])] = int(r[1])
except (ValueError, TypeError):
continue
print(f' slugs: {len(slug_to_id)} k2_ids: {len(k2_to_id)}', file=sys.stderr)
return slug_to_id, k2_to_id
def fetch_titles(ids, conn):
if not ids: return {}
ids_str = ','.join(str(i) for i in ids)
rows = mysql(f"SELECT ID, post_title FROM wp_posts WHERE ID IN ({ids_str});", conn)
return {int(r[0]): r[1] for r in rows}
def url_to_post_id(url, slug_to_id, k2_to_id):
m = WP_SLUG_RX.search(url)
if m:
slug = m.group(1).lower()
if slug not in RESERVED_SLUGS and slug in slug_to_id:
return slug_to_id[slug]
m = K2_ITEM_RX.search(url)
if m:
k2 = int(m.group(1))
if k2 in k2_to_id:
return k2_to_id[k2]
return None
def extract_sections(html_content):
"""Devuelve dict {section_slug: [post_id, ...]} basándose en encabezados.
NOTA: los post_ids aún no están resueltos aquí — devuelve hrefs en su lugar.
"""
positions = []
for slug, rx in SECTION_PATTERNS:
m = rx.search(html_content)
if m:
positions.append((m.start(), slug))
if not positions:
return {}
positions.sort()
positions.append((len(html_content), None))
out = {}
for i in range(len(positions) - 1):
start, slug = positions[i]
end = positions[i+1][0]
segment = html_content[start:end]
hrefs = HREF_RX.findall(segment)
# Dedup preservando orden
seen, urls = set(), []
for h in hrefs:
if h not in seen:
seen.add(h); urls.append(h)
out[slug] = urls
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--out', default='/tmp/clasificacion_articulos_regen.csv')
ap.add_argument('--diff', help='CSV de referencia para mostrar diff')
args = ap.parse_args()
conn = get_conn()
slug_to_id, k2_to_id = build_lookups(conn)
print('Leyendo cartas ...', file=sys.stderr, flush=True)
cartas = fetch_cartas(conn)
print(f' cartas: {len(cartas)}', file=sys.stderr)
rows_out = []
needed_titles = set()
n_unresolved = 0
n_resolved = 0
for c in cartas:
carta_id, carta_title, carta_fecha, content = c
carta_id = int(carta_id)
if not content:
continue
sections = extract_sections(content)
for slug, urls in sections.items():
label = SECTION_LABELS[slug]
default_cat = CAT_PROPUESTA[slug]
# Filtrar a posts resueltos manteniendo orden
resolved = []
for url in urls:
pid = url_to_post_id(url, slug_to_id, k2_to_id)
if pid is None:
n_unresolved += 1
continue
resolved.append(pid)
for pos, pid in enumerate(resolved):
# Sub-clasificación posicional para evangelio
if slug == 'comentario' and pos < len(SUBCAT_EVANGELIO_BY_POS):
cat = SUBCAT_EVANGELIO_BY_POS[pos]
else:
cat = default_cat
n_resolved += 1
needed_titles.add(pid)
rows_out.append({
'post_id': pid,
'categoria_propuesta': cat,
'seccion_original': label,
'carta_id': carta_id,
'carta_titulo': carta_title,
'carta_fecha': carta_fecha,
})
print(f'Resueltos: {n_resolved} Sin resolver: {n_unresolved}', file=sys.stderr)
print('Cargando títulos ...', file=sys.stderr, flush=True)
titles = fetch_titles(list(needed_titles), conn)
conn.close()
# Escribir CSV
cols = ['post_id', 'post_title', 'categoria_propuesta', 'seccion_original', 'carta_id', 'carta_titulo', 'carta_fecha']
with open(args.out, 'w', newline='', encoding='utf-8') as f:
w = csv.DictWriter(f, fieldnames=cols, quoting=csv.QUOTE_MINIMAL)
w.writeheader()
for r in rows_out:
r['post_title'] = titles.get(r['post_id'], '')
w.writerow(r)
print(f'Escrito: {args.out} ({len(rows_out)} filas)')
# Diff opcional
if args.diff and os.path.exists(args.diff):
from collections import defaultdict, Counter
def load(path):
d = defaultdict(set) # (post_id, cat) → set((carta_id, seccion))
cats_by_post = defaultdict(set)
with open(path, encoding='utf-8-sig') as fh:
r = csv.DictReader(fh)
for row in r:
pid = row.get('post_id','')
cat = row.get('categoria_propuesta','')
if not pid: continue
cats_by_post[pid].add(cat)
return cats_by_post
old = load(args.diff)
new = load(args.out)
old_keys = set(old.keys())
new_keys = set(new.keys())
print('\n=== DIFF ===')
print(f'posts en CSV viejo: {len(old_keys)}')
print(f'posts en CSV nuevo: {len(new_keys)}')
print(f'solo en viejo: {len(old_keys - new_keys)}')
print(f'solo en nuevo: {len(new_keys - old_keys)}')
common = old_keys & new_keys
same_cats = sum(1 for k in common if old[k] == new[k])
diff_cats = len(common) - same_cats
print(f'común con mismas cats: {same_cats}')
print(f'común con cats distintas: {diff_cats}')
if __name__ == '__main__':
main()