Un plugin de WordPress para importar hojas de cálculo a tablas con shortcode

Foro sobre el CMS WordPress
Responder
Guillermo
Site Admin
Mensajes: 405
Registrado: 13 May 2023, 00:32
Ubicación: Valencia - España
Contactar:

Un plugin de WordPress para importar hojas de cálculo a tablas con shortcode

Mensaje por Guillermo »

Acabo de publicarlo en Indaga.net: Import Excel to Table: un plugin de WordPress sin dependencias para convertir un Excel en una tabla con shortcode

Aunque solo se recarga la página del plugin en Indaga.net, funciona en otros 3 sitios en los que he probado este plugin. Revisaré el código e intentaré resolver este 'bug'. Uilizaré este foro para ello.

Dejo aquí un resumen del artículo o post:
Para cubrir ese caso he desarrollado Import Excel to Table, un plugin de WordPress que importa archivos o a una tabla propia de la base de datos y los muestra mediante shortcode, con ordenación por columna, paginación y enlaces automáticos.

WP Convert Excel Data To Table And DB — es el que más se parece en planteamiento: importa el Excel a la base de datos de WordPress, gestiona los registros desde un menú propio y permite editarlos manualmente.

Para el caso de uso habitual —una o varias tablas de datos simples que se actualizan de vez en cuando— no supone ningún problema. NOTA: Import Excel to Table nació como una prueba en este mismo sitio, indaga.net, antes de valorar su uso en otros proyectos con necesidades de datos tabulares más específicas.
import_excel_to_table_panel_control.jpg
import_excel_to_table_panel_control.jpg (107.02 KiB) Visto 3099 veces
Descargar plugin (desde mi pCloud) para probarlo. https://e.pcloud.link/publink/show?code ... Quf0bSOR9V
Guillermo
Site Admin
Mensajes: 405
Registrado: 13 May 2023, 00:32
Ubicación: Valencia - España
Contactar:

Re: Un plugin de WordPress para importar hojas de cálculo a tablas con shortcode

Mensaje por Guillermo »

Novedad en Import Excel to Table: elige cuántos registros ver por página

Hasta ahora, cuando una tabla importada desde Excel se mostraba en la web, siempre aparecían 25 filas por página, sin más opción. Con la última actualización, la propia persona que visita la web puede elegir cuántas filas quiere ver de golpe: 25, 50 o 100.

¿Cómo se usa?
Justo encima de la tabla, junto al desplegable de "Ordenar por", aparece ahora uno nuevo que dice "Mostrar". Al elegir una opción (por ejemplo, 100 por página) y pulsar "Aplicar", la tabla se recarga mostrando esa cantidad de filas, y la paginación de abajo (los números de página) se recalcula automáticamente según cuántas filas quepan.

¿Por qué es útil?
Antes, si alguien quería ver más registros sin ir pasando página a página, no tenía forma de hacerlo. Ahora, si el listado no es muy largo, se puede poner en "100 por página" y verlo casi todo de un vistazo; si es muy largo, se puede dejar en 25 para que cargue más rápido y sea más manejable.

Nada que instalar aparte
Es parte del propio plugin, no hace falta activar nada ni configurar nada especial: el selector aparece solo, en cualquier tabla que ya tuvierais publicada con el shortcode [excel_table].

Descargar plugin actualizado a 11/08/2026 de mi pCloud: https://e.pcloud.link/publink/show?code ... Quf0bSOR9V
Guillermo
Site Admin
Mensajes: 405
Registrado: 13 May 2023, 00:32
Ubicación: Valencia - España
Contactar:

Re: Un plugin de WordPress para importar hojas de cálculo a tablas con shortcode

Mensaje por Guillermo »

Sigue fallando el refresco de la página del plugin en webs con gutenberg. Solo en Indaga.net (en la que no utilizo Gutenberg) se recarga la página, aunque en todos los sitios que he probado funciona la importación del excel (o Libreoffice calc) a la tabla. A ver si me decido paa subir directamente un .csv...
Guillermo
Site Admin
Mensajes: 405
Registrado: 13 May 2023, 00:32
Ubicación: Valencia - España
Contactar:

Re: Un plugin de WordPress para importar hojas de cálculo a tablas con shortcode

Mensaje por Guillermo »

He actualizado el plugin. Ahora la paginación va de 25, 50 ó 100 registros por página. Un ejempo en https://www.puntocomunica.com/lecturas- ... -to-table/

Sigue fallando la recarga de la página del plugin excepto en Indaga.net, que no utiliza Gutenberg. Intentaré solventarlo y reportaré aquí.
Guillermo
Site Admin
Mensajes: 405
Registrado: 13 May 2023, 00:32
Ubicación: Valencia - España
Contactar:

Re: Un plugin de WordPress para importar hojas de cálculo a tablas con shortcode

Mensaje por Guillermo »

He actualizado el plugin. Ahora la paginación va de 25, 50 ó 100 registros por página. Un ejempo en https://www.puntocomunica.com/lecturas- ... -to-table/
captura-import-excel-to-table.jpg
captura-import-excel-to-table.jpg (138.27 KiB) Visto 2971 veces
import-excel-to-table.png
import-excel-to-table.png (134.25 KiB) Visto 2978 veces
Sigue fallando la recarga de la página del plugin excepto en Indaga.net, que no utiliza Gutenberg. Intentaré solventarlo y reportaré aquí.
gbeltran
Mensajes: 340
Registrado: 13 May 2023, 01:15
Ubicación: Valencia - España
Contactar:

Re: Un plugin de WordPress para importar hojas de cálculo a tablas con shortcode

Mensaje por gbeltran »

Solucionado el bug, ya nos queda "congelado" en webs con Gutenberg o constructores tipo WBakery. La soculión ha sido cambiar el archivo admin-page.php (ubicado en la subcarpeta includes del plugin).

Este es el código:
<?php

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Despacha las acciones del panel (subida, ajustes, borrados) enganchado a
* admin_init, en lugar de dentro del callback del menú (ietv_admin_page).
*
* WordPress ya ha enviado admin-header.php (y por tanto cabeceras HTTP y
* HTML) cuando se ejecuta el callback de add_menu_page(). Si el
* procesamiento del POST vive ahí y acaba llamando a wp_safe_redirect(),
* la redirección llega tarde: "headers already sent" y la página se queda
* a medio renderizar ("congelada"). Al engancharlo a admin_init se procesa
* antes de que se envíe ninguna salida, así que la redirección funciona
* con normalidad.
*/
add_action( 'admin_init', 'ietv_handle_admin_actions' );
function ietv_handle_admin_actions() {
// Solo actuar en la página de este plugin, para no interceptar el
// admin_init de cualquier otra pantalla del admin.
if ( ! isset( $_GET['page'] ) || 'import-excel-to-table' !== $_GET['page'] ) {
return;
}

if ( ! isset( $_POST['ietv_action'] ) ) {
return;
}

if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'No tienes permiso para realizar esta acción.', 'import-excel-to-table' ) );
}

check_admin_referer( 'ietv_admin_action', 'ietv_nonce' );

if ( 'upload' === $_POST['ietv_action'] ) {
ietv_handle_upload();
} elseif ( 'save_settings' === $_POST['ietv_action'] ) {
ietv_handle_save_settings();
} elseif ( 'delete_import' === $_POST['ietv_action'] ) {
$import_id = isset( $_POST['ietv_import_id'] ) ? sanitize_text_field( wp_unslash( $_POST['ietv_import_id'] ) ) : '';
ietv_delete_import( $import_id );
ietv_redirect_with_message(
/* translators: %s: identificador de la importación borrada */
sprintf( __( 'Importación "%s" eliminada.', 'import-excel-to-table' ), ietv_sanitize_import_id( $import_id ) ),
'success'
);
} elseif ( 'delete_all' === $_POST['ietv_action'] ) {
ietv_delete_all_data();
ietv_redirect_with_message( __( 'Se han borrado todas las importaciones.', 'import-excel-to-table' ), 'success' );
}
}

/**
* Renderiza la página de administración del plugin.
*
* Este callback ya no procesa ningún $_POST: eso ocurre antes, en
* ietv_handle_admin_actions() enganchado a admin_init. Aquí solo se pinta
* el HTML a partir del estado actual (importaciones guardadas, mensaje de
* aviso tras la redirección, vista previa si se ha pedido).
*/
function ietv_admin_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'No tienes permiso para acceder a esta página.', 'import-excel-to-table' ) );
}

$imports = ietv_get_import_list();
$require_login = (bool) get_option( 'ietv_require_login', false );

$preview_id = isset( $_GET['ietv_preview'] ) ? ietv_sanitize_import_id( wp_unslash( $_GET['ietv_preview'] ) ) : '';
$preview_data = $preview_id ? ietv_get_import_data( $preview_id ) : array();
?>
<div class="wrap ietv-container">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>

<?php ietv_render_admin_notice(); ?>

<div class="ietv-upload-section">
<h2><?php esc_html_e( 'Subir archivo', 'import-excel-to-table' ); ?></h2>
<form method="post" enctype="multipart/form-data" novalidate>
<?php wp_nonce_field( 'ietv_admin_action', 'ietv_nonce' ); ?>
<input type="hidden" name="ietv_action" value="upload" />

<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="ietv_file"><?php esc_html_e( 'Archivo', 'import-excel-to-table' ); ?></label></th>
<td>
<input type="file" id="ietv_file" name="ietv_file" accept=".xlsx,.ods" required aria-describedby="ietv_file_help" />
<p id="ietv_file_help" class="description">
<?php esc_html_e( 'Formatos admitidos: .xlsx y .ods. Tamaño máximo: 5 MB. La primera fila debe contener los encabezados de columna.', 'import-excel-to-table' ); ?>
</p>
</td>
</tr>
<tr>
<th scope="row"><label for="ietv_import_id"><?php esc_html_e( 'Identificador', 'import-excel-to-table' ); ?></label></th>
<td>
<input type="text" id="ietv_import_id" name="ietv_import_id" class="regular-text" placeholder="<?php esc_attr_e( 'p. ej. contactos', 'import-excel-to-table' ); ?>" aria-describedby="ietv_import_id_help" />
<p id="ietv_import_id_help" class="description">
<?php esc_html_e( 'Identifica esta importación y su shortcode: [excel_table id="tu-identificador"]. Si lo dejas en blanco, se genera a partir del nombre del archivo. Cada identificador guarda sus datos por separado; importar de nuevo con el mismo identificador solo sustituye esos datos, no los de otras importaciones.', 'import-excel-to-table' ); ?>
</p>
</td>
</tr>
</table>
<p>
<button type="submit" class="button button-primary">
<?php esc_html_e( 'Importar', 'import-excel-to-table' ); ?>
</button>
</p>
</form>
</div>

<div class="ietv-settings-section">
<h2><?php esc_html_e( 'Ajustes de visualización', 'import-excel-to-table' ); ?></h2>
<form method="post">
<?php wp_nonce_field( 'ietv_admin_action', 'ietv_nonce' ); ?>
<input type="hidden" name="ietv_action" value="save_settings" />
<label>
<input type="checkbox" name="ietv_require_login" value="1" <?php checked( $require_login ); ?> />
<?php esc_html_e( 'Mostrar las tablas de shortcode solo a usuarios con sesión iniciada (recomendado si los datos son sensibles / RGPD). Aplica a todas las importaciones.', 'import-excel-to-table' ); ?>
</label>
<p><button type="submit" class="button"><?php esc_html_e( 'Guardar ajustes', 'import-excel-to-table' ); ?></button></p>
</form>
</div>

<div class="ietv-preview-section">
<h2><?php esc_html_e( 'Importaciones', 'import-excel-to-table' ); ?></h2>

<?php if ( empty( $imports ) ) : ?>
<p><?php esc_html_e( 'No hay ninguna importación todavía.', 'import-excel-to-table' ); ?></p>
<?php else : ?>
<table class="widefat striped">
<thead>
<tr>
<th scope="col"><?php esc_html_e( 'Identificador', 'import-excel-to-table' ); ?></th>
<th scope="col"><?php esc_html_e( 'Registros', 'import-excel-to-table' ); ?></th>
<th scope="col"><?php esc_html_e( 'Última importación', 'import-excel-to-table' ); ?></th>
<th scope="col"><?php esc_html_e( 'Shortcode', 'import-excel-to-table' ); ?></th>
<th scope="col"><?php esc_html_e( 'Acciones', 'import-excel-to-table' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $imports as $import ) : ?>
<tr>
<td><?php echo esc_html( $import->import_id ); ?></td>
<td><?php echo intval( $import->total ); ?></td>
<td><?php echo esc_html( mysql2date( 'd/m/Y H:i', $import->last_import ) ); ?></td>
<td><code>[excel_table id="<?php echo esc_html( $import->import_id ); ?>"]</code></td>
<td>
<a class="button button-small" href="<?php echo esc_url( add_query_arg( 'ietv_preview', $import->import_id ) ); ?>">
<?php esc_html_e( 'Ver datos', 'import-excel-to-table' ); ?>
</a>
<form method="post" style="display:inline" onsubmit="return confirm('<?php echo esc_js( __( '¿Seguro que quieres borrar esta importación? Esta acción no se puede deshacer.', 'import-excel-to-table' ) ); ?>');">
<?php wp_nonce_field( 'ietv_admin_action', 'ietv_nonce' ); ?>
<input type="hidden" name="ietv_action" value="delete_import" />
<input type="hidden" name="ietv_import_id" value="<?php echo esc_attr( $import->import_id ); ?>" />
<button type="submit" class="button button-small button-link-delete"><?php esc_html_e( 'Borrar', 'import-excel-to-table' ); ?></button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>

<?php if ( $preview_id && ! empty( $preview_data ) ) : ?>
<h3>
<?php
printf(
/* translators: %s: identificador de la importación */
esc_html__( 'Vista previa: %s', 'import-excel-to-table' ),
esc_html( $preview_id )
);
?>
</h3>
<?php $preview_columns = array_keys( $preview_data[0] ); ?>
<table class="widefat striped">
<thead>
<tr>
<?php foreach ( $preview_columns as $col ) : ?>
<th scope="col"><?php echo esc_html( $col ); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ( array_slice( $preview_data, 0, 10 ) as $row ) : ?>
<tr>
<?php foreach ( $preview_columns as $col ) : ?>
<td><?php echo esc_html( ietv_cell_text( $row[ $col ] ?? '' ) ); ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="description">
<?php
printf(
/* translators: %d: número de registros mostrados como vista previa */
esc_html__( 'Mostrando los primeros %d registros de esta importación.', 'import-excel-to-table' ),
10
);
?>
</p>
<?php endif; ?>

<p style="margin-top: 20px;">
<form method="post" onsubmit="return confirm('<?php echo esc_js( __( '¿Seguro que quieres borrar TODAS las importaciones? Esta acción no se puede deshacer.', 'import-excel-to-table' ) ); ?>');">
<?php wp_nonce_field( 'ietv_admin_action', 'ietv_nonce' ); ?>
<input type="hidden" name="ietv_action" value="delete_all" />
<button type="submit" class="button button-secondary">
<?php esc_html_e( 'Borrar TODAS las importaciones', 'import-excel-to-table' ); ?>
</button>
</form>
</p>
<?php endif; ?>
</div>
</div>
<?php
}

/**
* Procesa la subida del archivo Excel/ODS.
*/
function ietv_handle_upload() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'No tienes permiso para realizar esta acción.', 'import-excel-to-table' ) );
}

// Con archivos grandes, procesar y guardar cientos de filas en una sola
// petición puede acercarse al límite de ejecución por defecto de PHP
// (a menudo 30s). Se pide más margen como salvaguarda; si el corte viene
// de un proxy inverso delante del servidor (no de PHP), esto no basta y
// haría falta ajustar el timeout del proxy.
if ( function_exists( 'set_time_limit' ) ) {
@set_time_limit( 180 ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_set_time_limit, WordPress.PHP.NoSilencedErrors.Discouraged
}

if ( empty( $_FILES['ietv_file'] ) || UPLOAD_ERR_OK !== $_FILES['ietv_file']['error'] ) {
ietv_redirect_with_message( __( 'Error al subir el archivo.', 'import-excel-to-table' ), 'error' );
}

$file = $_FILES['ietv_file'];

$max_size = apply_filters( 'ietv_max_file_size', IETV_MAX_FILE_SIZE );
if ( $file['size'] > $max_size ) {
ietv_redirect_with_message( __( 'El archivo supera el tamaño máximo permitido (5 MB).', 'import-excel-to-table' ), 'error' );
}

if ( ! is_uploaded_file( $file['tmp_name'] ) ) {
ietv_redirect_with_message( __( 'Archivo no válido.', 'import-excel-to-table' ), 'error' );
}

$file_name = sanitize_file_name( $file['name'] );
$ext = strtolower( pathinfo( $file_name, PATHINFO_EXTENSION ) );

$allowed_types = array(
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
);

if ( ! isset( $allowed_types[ $ext ] ) ) {
ietv_redirect_with_message( __( 'Formato no permitido. Usa .xlsx o .ods.', 'import-excel-to-table' ), 'error' );
}

// Validación real del tipo de archivo, no solo por extensión.
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$mime_type = finfo_file( $finfo, $file['tmp_name'] );
finfo_close( $finfo );

// Los .xlsx/.ods son en realidad archivos ZIP; algunos servidores
// devuelven application/zip en lugar del MIME específico de Office,
// así que se acepta también ese caso.
$valid_mime = ( $mime_type === $allowed_types[ $ext ] || 'application/zip' === $mime_type );

if ( ! $valid_mime ) {
ietv_redirect_with_message(
/* translators: %s: tipo MIME detectado */
sprintf( __( 'El contenido del archivo no coincide con su extensión (detectado: %s).', 'import-excel-to-table' ), $mime_type ),
'error'
);
}

if ( ! class_exists( 'ZipArchive' ) ) {
ietv_redirect_with_message( __( 'La extensión PHP "zip" no está activa en este hosting.', 'import-excel-to-table' ), 'error' );
}

// Cronometraje de diagnóstico: si con WP_DEBUG_LOG activo el "TOTAL"
// que aparece en debug.log es bajo (décimas o pocos segundos) pero el
// navegador se queda esperando mucho más tiempo, la causa del bloqueo
// no está en este procesamiento sino en algo posterior (típicamente un
// proxy inverso cortando la conexión antes de que llegue la respuesta).
$ietv_t0 = microtime( true );

try {
$rows = ( 'xlsx' === $ext ) ? ietv_read_xlsx( $file['tmp_name'] ) : ietv_read_ods( $file['tmp_name'] );
} catch ( Exception $e ) {
ietv_log_error( 'Error leyendo archivo: ' . $e->getMessage() );
ietv_redirect_with_message( __( 'No se pudo leer el archivo. Comprueba que no esté dañado.', 'import-excel-to-table' ), 'error' );
return;
}

ietv_log_error( sprintf( 'Lectura de %s: %.3fs (%d filas)', $ext, microtime( true ) - $ietv_t0, count( $rows ) ) );

if ( empty( $rows ) ) {
ietv_redirect_with_message( __( 'El archivo no contiene filas de datos.', 'import-excel-to-table' ), 'error' );
}

$ietv_t1 = microtime( true );
$clean_rows = ietv_sanitize_rows( $rows );
ietv_log_error( sprintf( 'Saneado de datos: %.3fs', microtime( true ) - $ietv_t1 ) );

$import_id = isset( $_POST['ietv_import_id'] ) ? sanitize_text_field( wp_unslash( $_POST['ietv_import_id'] ) ) : '';
if ( '' === trim( $import_id ) ) {
$import_id = pathinfo( $file_name, PATHINFO_FILENAME ); // sin extensión, a partir del nombre subido
}
$import_id = ietv_sanitize_import_id( $import_id );

$ietv_t2 = microtime( true );
$result = ietv_replace_import_data( $import_id, $clean_rows );
ietv_log_error( sprintf( 'Guardado en BD: %.3fs', microtime( true ) - $ietv_t2 ) );
ietv_log_error( sprintf( 'TOTAL importación "%s": %.3fs', $import_id, microtime( true ) - $ietv_t0 ) );

if ( is_wp_error( $result ) ) {
ietv_redirect_with_message( $result->get_error_message(), 'error' );
}

ietv_redirect_with_message(
/* translators: 1: número de registros importados, 2: identificador de la importación */
sprintf( __( 'Importación completada: %1$d registros guardados con el identificador "%2$s". Shortcode: [excel_table id="%2$s"]', 'import-excel-to-table' ), $result, $import_id ),
'success'
);
}

/**
* Guarda los ajustes de visualización.
*/
function ietv_handle_save_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'No tienes permiso para realizar esta acción.', 'import-excel-to-table' ) );
}

$require_login = isset( $_POST['ietv_require_login'] ) ? '1' === $_POST['ietv_require_login'] : false;
update_option( 'ietv_require_login', $require_login );

ietv_redirect_with_message( __( 'Ajustes guardados.', 'import-excel-to-table' ), 'success' );
}

/**
* Redirige a la página del plugin con un mensaje de estado.
* (Sin llamadas HTTP espurias: solo construye la URL y redirige.)
*
* @param string $message
* @param string $type 'success' o 'error'
*/
function ietv_redirect_with_message( $message, $type = 'success' ) {
$url = add_query_arg(
array(
'page' => 'import-excel-to-table',
'ietv_message' => rawurlencode( $message ),
'ietv_type' => $type,
),
admin_url( 'admin.php' )
);

wp_safe_redirect( $url );
exit;
}

/**
* Muestra el aviso de éxito/error tras una redirección.
*/
function ietv_render_admin_notice() {
if ( empty( $_GET['ietv_message'] ) ) {
return;
}

$message = sanitize_text_field( wp_unslash( $_GET['ietv_message'] ) );
$type = isset( $_GET['ietv_type'] ) && 'error' === $_GET['ietv_type'] ? 'error' : 'success';
$class = 'error' === $type ? 'notice-error' : 'notice-success';

printf(
'<div class="notice %s is-dismissible"><p>%s</p></div>',
esc_attr( $class ),
esc_html( $message )
);
}
Responder