Evitar que una entrada se publique si los campos personalizados no están llenos
Tengo un tipo de entrada personalizada Event
que contiene campos personalizados de fecha/hora de inicio y finalización (como metaboxes en la pantalla de edición de entradas).
Me gustaría asegurarme de que un Evento no pueda ser publicado (o programado) sin que las fechas estén completadas, ya que eso causará problemas con las plantillas que muestran los datos del Evento (¡además del hecho de que es un requisito necesario!). Sin embargo, me gustaría poder tener Eventos en Borrador que no contengan una fecha válida mientras están en preparación.
Estaba pensando en usar el hook save_post
para hacer la verificación, pero ¿cómo puedo evitar que ocurra el cambio de estado?
EDICIÓN1: Este es el hook que estoy usando ahora para guardar el post_meta.
// Guardar los Datos del Metabox
function ep_eventposts_save_meta( $post_id, $post ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
return;
if ( !isset( $_POST['ep_eventposts_nonce'] ) )
return;
if ( !wp_verify_nonce( $_POST['ep_eventposts_nonce'], plugin_basename( __FILE__ ) ) )
return;
// ¿Tiene el usuario permiso para editar la entrada o página?
if ( !current_user_can( 'edit_post', $post->ID ) )
return;
// OK, estamos autenticados: necesitamos encontrar y guardar los datos
// Los pondremos en un array para facilitar el recorrido
//depuración
//print_r($_POST);
$metabox_ids = array( '_start', '_end' );
foreach ($metabox_ids as $key ) {
$events_meta[$key . '_date'] = $_POST[$key . '_date'];
$events_meta[$key . '_time'] = $_POST[$key . '_time'];
$events_meta[$key . '_timestamp'] = $events_meta[$key . '_date'] . ' ' . $events_meta[$key . '_time'];
}
$events_meta['_location'] = $_POST['_location'];
if (array_key_exists('_end_timestamp', $_POST))
$events_meta['_all_day'] = $_POST['_all_day'];
// Agregar valores de $events_meta como campos personalizados
foreach ( $events_meta as $key => $value ) { // ¡Recorrer el array $events_meta!
if ( $post->post_type == 'revision' ) return; // No guardar datos personalizados dos veces
$value = implode( ',', (array)$value ); // Si $value es un array, convertirlo a CSV (poco probable)
if ( get_post_meta( $post->ID, $key, FALSE ) ) { // Si el campo personalizado ya tiene un valor
update_post_meta( $post->ID, $key, $value );
} else { // Si el campo personalizado no tiene un valor
add_post_meta( $post->ID, $key, $value );
}
if ( !$value )
delete_post_meta( $post->ID, $key ); // Eliminar si está en blanco
}
}
add_action( 'save_post', 'ep_eventposts_save_meta', 1, 2 );
EDICIÓN2: y esto es lo que estoy tratando de usar para verificar los datos de la entrada después de guardar en la base de datos.
add_action( 'save_post', 'ep_eventposts_check_meta', 99, 2 );
function ep_eventposts_check_meta( $post_id, $post ) {
//verificar que los metadatos estén completos cuando se publica una entrada
//print_r($_POST);
if ( $_POST['post_status'] == 'publish' ) {
$custom = get_post_custom($post_id);
//asegurarse de que ambas fechas estén llenas
if ( !array_key_exists('_start_timestamp', $custom ) || !array_key_exists('_end_timestamp', $custom )) {
$post->post_status = 'draft';
wp_update_post($post);
}
//asegurarse de que inicio < fin
elseif ( $custom['_start_timestamp'] > $custom['_end_timestamp'] ) {
$post->post_status = 'draft';
wp_update_post($post);
}
else {
return;
}
}
}
El principal problema con esto es uno que fue descrito en otra pregunta: usar wp_update_post()
dentro de un hook save_post
provoca un bucle infinito.
EDICIÓN3: Encontré una manera de hacerlo, usando el hook wp_insert_post_data
en lugar de save_post
. El único problema es que ahora el post_status
se revierte, pero aparece un mensaje engañoso que dice "Entrada publicada" (al agregar &message=6
a la URL redirigida), pero el estado se establece en Borrador.
add_filter( 'wp_insert_post_data', 'ep_eventposts_check_meta', 99, 2 );
function ep_eventposts_check_meta( $data, $postarr ) {
//verificar que los metadatos estén completos cuando se publica una entrada, de lo contrario revertir a borrador
if ( $data['post_type'] != 'event' ) {
return $data;
}
if ( $postarr['post_status'] == 'publish' ) {
$custom = get_post_custom($postarr['ID']);
//asegurarse de que ambas fechas estén llenas
if ( !array_key_exists('_start_timestamp', $custom ) || !array_key_exists('_end_timestamp', $custom )) {
$data['post_status'] = 'draft';
}
//asegurarse de que inicio < fin
elseif ( $custom['_start_timestamp'] > $custom['_end_timestamp'] ) {
$data['post_status'] = 'draft';
}
//¡todo bien!
else {
return $data;
}
}
return $data;
}
Como señaló m0r7if3r, no hay forma de evitar que una publicación se publique usando el hook save_post
, ya que cuando se dispara ese hook, la publicación ya está guardada. Sin embargo, lo siguiente te permitirá revertir el estado sin usar wp_insert_post_data
y sin causar un bucle infinito.
Lo siguiente no está probado, pero debería funcionar.
<?php
add_action('save_post', 'my_save_post');
function my_save_post($post_id) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
return;
if ( !isset( $_POST['ep_eventposts_nonce'] ) )
return;
if ( !wp_verify_nonce( $_POST['ep_eventposts_nonce'], plugin_basename( __FILE__ ) ) )
return;
// ¿Tiene el usuario permiso para editar la publicación o página?
if ( !current_user_can( 'edit_post', $post->ID ) )
return;
// Ahora realiza comprobaciones para validar tus datos.
// Ten en cuenta que los campos personalizados (diferentes a los datos en metaboxes personalizados)
// ya se habrán guardado.
$prevent_publish= false;//Establece en true si los datos no son válidos.
if ($prevent_publish) {
// desvincula esta función para evitar un bucle infinito
remove_action('save_post', 'my_save_post');
// actualiza la publicación para cambiar el estado
wp_update_post(array('ID' => $post_id, 'post_status' => 'draft'));
// vuelve a vincular esta función
add_action('save_post', 'my_save_post');
}
}
?>
No lo he verificado, pero al mirar el código, el mensaje de retroalimentación mostrará incorrectamente que la publicación fue publicada. Esto se debe a que WordPress nos redirige a una URL donde la variable message
ahora es incorrecta.
Para cambiarlo, podemos usar el filtro redirect_post_location
:
add_filter('redirect_post_location','my_redirect_location',10,2);
function my_redirect_location($location,$post_id){
// Si la publicación se publicó...
if (isset($_POST['publish'])){
// obtiene el estado actual de la publicación
$status = get_post_status( $post_id );
// La publicación fue 'publicada', pero si sigue siendo un borrador, muestra el mensaje de borrador (10).
if($status=='draft')
$location = add_query_arg('message', 10, $location);
}
return $location;
}
Para resumir el filtro de redirección anterior: si una publicación está configurada para publicarse, pero sigue siendo un borrador, entonces modificamos el mensaje en consecuencia (que es message=10
). Nuevamente, esto no está probado, pero debería funcionar. El Codex de add_query_arg
sugiere que cuando una variable ya está establecida, la función la reemplaza (pero como digo, no he probado esto).

Aparte del punto y coma faltante en tu línea de add_query_arg, este truco del filtro redirect_post_location es exactamente lo que necesitaba. ¡Gracias!

OK, así es como terminé haciéndolo: una llamada Ajax a una función PHP que realiza la verificación, inspirada en parte por esta respuesta y usando un consejo inteligente de una pregunta que hice en StackOverflow. Es importante destacar que me aseguro de que solo cuando queremos Publicar se realice la verificación, de modo que un Borrador siempre se pueda guardar sin la verificación. Esta resultó ser la solución más fácil para realmente evitar la publicación del post. Podría ayudar a alguien más, así que lo escribí aquí.
Primero, añade el Javascript necesario:
//AJAX para validar evento antes de publicar
//adaptado de https://wordpress.stackexchange.com/questions/15546/dont-publish-custom-post-type-post-if-a-meta-data-field-isnt-valid
add_action('admin_enqueue_scripts-post.php', 'ep_load_jquery_js');
add_action('admin_enqueue_scripts-post-new.php', 'ep_load_jquery_js');
function ep_load_jquery_js(){
global $post;
if ( $post->post_type == 'event' ) {
wp_enqueue_script('jquery');
}
}
add_action('admin_head-post.php','ep_publish_admin_hook');
add_action('admin_head-post-new.php','ep_publish_admin_hook');
function ep_publish_admin_hook(){
global $post;
if ( is_admin() && $post->post_type == 'event' ){
?>
<script language="javascript" type="text/javascript">
jQuery(document).ready(function() {
jQuery('#publish').click(function() {
if(jQuery(this).data("valid")) {
return true;
}
var form_data = jQuery('#post').serializeArray();
var data = {
action: 'ep_pre_submit_validation',
security: '<?php echo wp_create_nonce( 'pre_publish_validation' ); ?>',
form_data: jQuery.param(form_data),
};
jQuery.post(ajaxurl, data, function(response) {
if (response.indexOf('true') > -1 || response == true) {
jQuery("#post").data("valid", true).submit();
} else {
alert("Error: " + response);
jQuery("#post").data("valid", false);
}
//ocultar ícono de carga, restaurar botón Publicar a normal
jQuery('#ajax-loading').hide();
jQuery('#publish').removeClass('button-primary-disabled');
jQuery('#save-post').removeClass('button-disabled');
});
return false;
});
});
</script>
<?php
}
}
Luego, la función que maneja la verificación:
add_action('wp_ajax_ep_pre_submit_validation', 'ep_pre_submit_validation');
function ep_pre_submit_validation() {
//verificación simple de seguridad
check_ajax_referer( 'pre_publish_validation', 'security' );
//convertir la cadena de datos recibidos a un array
//de https://wordpress.stackexchange.com/a/26536/10406
parse_str( $_POST['form_data'], $vars );
//verificar que realmente estemos intentando publicar un post
if ( $vars['post_status'] == 'publish' ||
(isset( $vars['original_publish'] ) &&
in_array( $vars['original_publish'], array('Publicar', 'Programar', 'Actualizar') ) ) ) {
if ( empty( $vars['_start_date'] ) || empty( $vars['_end_date'] ) ) {
_e('Ambas fechas de Inicio y Fin deben completarse');
die();
}
//asegurarse que inicio < fin
elseif ( $vars['_start_date'] > $vars['_end_date'] ) {
_e('La fecha de inicio no puede ser posterior a la fecha de fin');
die();
}
//verificar que la hora también esté ingresada en caso de evento que no sea todo el día
elseif ( !isset($vars['_all_day'] ) ) {
if ( empty($vars['_start_time'] ) || empty( $vars['_end_time'] ) ) {
_e('Ambas horas de Inicio y Fin deben especificarse si el evento no es de todo el día');
die();
}
elseif ( strtotime( $vars['_start_date']. ' ' .$vars['_start_time'] ) > strtotime( $vars['_end_date']. ' ' .$vars['_end_time'] ) ) {
_e('La fecha/hora de inicio no puede ser posterior a la fecha/hora de fin');
die();
}
}
}
//todo está bien, permitir envío
echo 'true';
die();
}
Esta función devuelve true
si todo está bien y envía el formulario para publicar el post por el canal normal. De lo contrario, la función devuelve un mensaje de error que se muestra como un alert()
y el formulario no se envía.

He seguido el mismo enfoque y obtengo la publicación guardada como "Borrador" en lugar de "Publicar" cuando la función de validación devuelve verdadero. ¡No estoy seguro de cómo solucionarlo!<br/>Tampoco obtengo los datos para un campo de área de texto (por ejemplo, post_content, cualquier otro campo personalizado de área de texto) durante la llamada ajax.

He aplicado esta solución de manera un poco diferente:
en primer lugar, he utilizado el siguiente código en el javascript en caso de éxito:
delayed_autosave(); //obtener datos del campo textarea/tinymce
jQuery('#publish').data("valid", true).trigger('click'); //publicar post
Muchas gracias.

Creo que la mejor manera de abordar esto no es tanto PREVENIR que ocurra el cambio de estado, sino más bien REVERTIRLO si sucede. Por ejemplo: conectas el hook save_post
con una prioridad muy alta (para que el hook se active muy tarde, específicamente después de insertar tus metadatos), luego verificas el post_status
de la publicación que acaba de guardarse y lo actualizas a "pending" (o "draft" o lo que sea) si no cumple con tus criterios.
Una estrategia alternativa sería conectar el hook wp_insert_post_data
para establecer directamente el post_status. La desventaja de este método, en mi opinión, es que aún no habrás insertado los metadatos en la base de datos, por lo que tendrás que procesarlos, etc. en el lugar para hacer tus verificaciones y luego procesarlos nuevamente para insertarlos en la base de datos... lo que podría generar mucha sobrecarga, ya sea en rendimiento o en código.

Actualmente estoy enganchando save_post
con prioridad 1 para guardar los campos meta de las metaboxes; lo que estás proponiendo entonces es tener un segundo hook para save_post
con prioridad, digamos, 99? ¿Esto aseguraría la integridad? ¿Qué pasa si por alguna razón se ejecuta el primer hook, se insertan los metadatos y se publica el post, pero el segundo hook no se ejecuta, terminando con campos inválidos?

No se me ocurre una situación donde el primer hook se ejecute pero no el segundo... ¿en qué tipo de escenario piensas que podría causar eso? Si estás tan preocupado por ello, puedes insertar los post meta, verificar los post meta, y luego actualizar el post_status
todo en una sola función ejecutándose a partir de una única llamada a un hook si lo prefieres.

Publiqué mi código como una edición a mi pregunta; intenté usar un segundo hook para save_post
pero eso desencadena un bucle infinito.

El mejor método podría ser JAVASCRIPT:
<script type="text/javascript">
var field_id = "My_field_div__ID"; // <----------------- CAMBIA ESTO
var SubmitButton = document.getElementById("save-post") || false;
var PublishButton = document.getElementById("publish") || false;
if (SubmitButton) {SubmitButton.addEventListener("click", SubmCLICKED, false);}
if (PublishButton) {PublishButton.addEventListener("click", SubmCLICKED, false);}
function SubmCLICKED(e){
var passed= false;
if(!document.getElementById(field_id)) { alert("¡No puedo encontrar ese ID de campo!"); }
else {
var Enabled_Disabled= document.getElementById(field_id).value;
if (Enabled_Disabled == "" ) { alert("El campo está vacío"); } else{passed=true;}
}
if (!passed) { e.preventDefault(); return false; }
}
</script>

Lo siento, no puedo darte una respuesta directa, pero recuerdo haber hecho algo similar muy recientemente, simplemente no recuerdo exactamente cómo. Creo que tal vez lo hice de una manera indirecta - algo como tenerlo como un valor predeterminado y si la persona no lo había cambiado, lo detectaba en una sentencia if, así -> if(categoria==categoria predeterminada) {echo "¡No elegiste una categoría!"; devolverlos a la página de creación de publicación; }
lo siento, esto no es una respuesta directa pero espero que ayude un poco.
