¿Cómo filtrar usuarios en la página de usuarios administradores por metacampo personalizado?

9

El problema

WP parece eliminar el valor de mi variable de consulta antes de que se use para filtrar la lista de usuarios.

Mi código

Esta función agrega una columna personalizada a mi tabla Usuarios en /wp-admin/users.php:

function add_course_section_to_user_meta( $columns ) {
    $columns['course_section'] = 'Section';
    return $columns;
}
add_filter( 'manage_users_columns', 'add_course_section_to_user_meta' );

Esta función le dice a WP cómo llenar valores en la columna:

function manage_users_course_section( $val, $col, $uid ) {
    if ( 'course_section' === $col )
        return get_the_author_meta( 'course_section', $uid );
}
add_filter( 'manage_users_custom_column', 'manage_users_course_section' );

Esto agrega un menú desplegable y un Filterbotón sobre la tabla Usuarios:

function add_course_section_filter() {
    echo '<select name="course_section" style="float:none;">';
    echo '<option value="">Course Section...</option>';
    for ( $i = 1; $i <= 3; ++$i ) {
        if ( $i == $_GET[ 'course_section' ] ) {
            echo '<option value="'.$i.'" selected="selected">Section '.$i.'</option>';
        } else {
            echo '<option value="'.$i.'">Section '.$i.'</option>';
        }
    }
    echo '<input id="post-query-submit" type="submit" class="button" value="Filter" name="">';
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );

Esta función altera la consulta del usuario para agregar mi meta_query:

function filter_users_by_course_section( $query ) {
    global $pagenow;

    if ( is_admin() && 
         'users.php' == $pagenow && 
         isset( $_GET[ 'course_section' ] ) && 
         !empty( $_GET[ 'course_section' ] ) 
       ) {
        $section = $_GET[ 'course_section' ];
        $meta_query = array(
            array(
                'key'   => 'course_section',
                'value' => $section
            )
        );
        $query->set( 'meta_key', 'course_section' );
        $query->set( 'meta_query', $meta_query );
    }
}
add_filter( 'pre_get_users', 'filter_users_by_course_section' );

Otra información

Crea mi menú desplegable correctamente. Cuando selecciono una sección del curso y hago clic en Filterla página se actualiza y course_sectionaparece en la URL, pero no tiene ningún valor asociado. Si reviso las solicitudes HTTP, muestra que se envía con el valor de la variable correcta, pero luego hay una 302 Redirectque parece eliminar el valor que seleccioné.

Si envío la course_sectionvariable escribiéndola directamente en la URL, el filtro funciona como se esperaba.

Mi código se basa aproximadamente en este código de Dave Court .

También intenté incluir en la lista blanca mi consulta var usando este código, pero sin suerte:

function add_course_section_query_var( $qvars ) {
    $qvars[] = 'course_section';
    return $qvars;
}
add_filter( 'query_vars', 'add_course_section_query_var' );

Estoy usando WP 4.4. ¿Alguna idea de por qué mi filtro no funciona?

morfático
fuente
Para su información, agregué un ticket en el sitio WP Trac que evitaría que los desarrolladores tengan que saltar a través de cualquiera de los aros que se describen a continuación.
morfático

Respuestas:

6

ACTUALIZACIÓN 2018-06-28

Si bien el siguiente código funciona bien, aquí hay una reescritura del código para WP> = 4.6.0 (usando PHP 7):

function add_course_section_filter( $which ) {

    // create sprintf templates for <select> and <option>s
    $st = '<select name="course_section_%s" style="float:none;"><option value="">%s</option>%s</select>';
    $ot = '<option value="%s" %s>Section %s</option>';

    // determine which filter button was clicked, if any and set section
    $button = key( array_filter( $_GET, function($v) { return __( 'Filter' ) === $v; } ) );
    $section = $_GET[ 'course_section_' . $button ] ?? -1;

    // generate <option> and <select> code
    $options = implode( '', array_map( function($i) use ( $ot, $section ) {
        return sprintf( $ot, $i, selected( $i, $section, false ), $i );
    }, range( 1, 3 ) ));
    $select = sprintf( $st, $which, __( 'Course Section...' ), $options );

    // output <select> and submit button
    echo $select;
    submit_button(__( 'Filter' ), null, $which, false);
}
add_action('restrict_manage_users', 'add_course_section_filter');

function filter_users_by_course_section($query)
{
    global $pagenow;
    if (is_admin() && 'users.php' == $pagenow) {
        $button = key( array_filter( $_GET, function($v) { return __( 'Filter' ) === $v; } ) );
        if ($section = $_GET[ 'course_section_' . $button ]) {
            $meta_query = [['key' => 'courses','value' => $section, 'compare' => 'LIKE']];
            $query->set('meta_key', 'courses');
            $query->set('meta_query', $meta_query);
        }
    }
}
add_filter('pre_get_users', 'filter_users_by_course_section');

Incorporé varias ideas de @birgire y @cale_b que también ofrecen soluciones a continuación que vale la pena leer. Específicamente, yo:

  1. Usó la $whichvariable que se agregó env4.6.0
  2. Se utilizan las mejores prácticas para i18n mediante el uso de cadenas traducibles, p. Ej. __( 'Filter' )
  3. Bucles intercambiado por el (más de moda?) array_map(), array_filter()Yrange()
  4. Se usa sprintf()para generar las plantillas de marcado
  5. Se utilizó la notación de matriz de corchetes en lugar de array()

Por último, descubrí un error en mis soluciones anteriores. Esas soluciones siempre favorecen lo SUPERIOR <select>sobre lo INFERIOR <select>. Entonces, si seleccionó una opción de filtro en el menú desplegable superior y luego selecciona una del menú desplegable inferior, el filtro solo usará el valor que esté arriba (si no está en blanco). Esta nueva versión corrige ese error.

ACTUALIZACIÓN 2018-02-14

Este problema ha sido parcheado desde WP 4.6.0 y los cambios están documentados en los documentos oficiales . Sin embargo, la solución a continuación todavía funciona.

Qué causó el problema (WP <4.6.0)

El problema era que la restrict_manage_usersacción se llamaba dos veces: una vez POR ENCIMA de la tabla Usuarios, y una vez POR DEBAJO. Esto significa que selectse crean DOS menús desplegables con el mismo nombre . Cuando Filterse hace clic en el botón, cualquier valor que esté en el segundo selectelemento (es decir, el DEBAJO de la tabla) anula el valor en el primero, es decir, el que está SOBRE LA tabla.

En caso de que desee sumergirse en la fuente WP, la restrict_manage_usersacción se activa desde dentro WP_Users_List_Table::extra_tablenav($which), que es la función que crea el menú desplegable nativo para cambiar la función de un usuario. Esa función tiene la ayuda de la $whichvariable que le dice si está creando selectel formulario anterior o inferior, y le permite otorgar a los dos menús desplegables nameatributos diferentes . Desafortunadamente, la $whichvariable no se pasa a la restrict_manage_usersacción, por lo que tenemos que encontrar otra forma de diferenciar nuestros propios elementos personalizados.

Una forma de hacerlo, como sugiere @Linnea , sería agregar algo de JavaScript para captar el Filterclic y sincronizar los valores de los dos menús desplegables. Elegí una solución solo para PHP que describiré ahora.

Como arreglarlo

Puede aprovechar la capacidad de convertir las entradas HTML en matrices de valores y luego filtrar la matriz para deshacerse de los valores indefinidos. Aquí está el código:

    function add_course_section_filter() {
        if ( isset( $_GET[ 'course_section' ]) ) {
            $section = $_GET[ 'course_section' ];
            $section = !empty( $section[ 0 ] ) ? $section[ 0 ] : $section[ 1 ];
        } else {
            $section = -1;
        }
        echo ' <select name="course_section[]" style="float:none;"><option value="">Course Section...</option>';
        for ( $i = 1; $i <= 3; ++$i ) {
            $selected = $i == $section ? ' selected="selected"' : '';
            echo '<option value="' . $i . '"' . $selected . '>Section ' . $i . '</option>';
        }
        echo '</select>';
        echo '<input type="submit" class="button" value="Filter">';
    }
    add_action( 'restrict_manage_users', 'add_course_section_filter' );

    function filter_users_by_course_section( $query ) {
        global $pagenow;

        if ( is_admin() && 
             'users.php' == $pagenow && 
             isset( $_GET[ 'course_section' ] ) && 
             is_array( $_GET[ 'course_section' ] )
            ) {
            $section = $_GET[ 'course_section' ];
            $section = !empty( $section[ 0 ] ) ? $section[ 0 ] : $section[ 1 ];
            $meta_query = array(
                array(
                    'key' => 'course_section',
                    'value' => $section
                )
            );
            $query->set( 'meta_key', 'course_section' );
            $query->set( 'meta_query', $meta_query );
        }
    }
    add_filter( 'pre_get_users', 'filter_users_by_course_section' );

Bonus: PHP 7 Refactor

Como estoy entusiasmado con PHP 7, en caso de que esté ejecutando WP en un servidor PHP 7, aquí hay una versión más corta y más sexy que utiliza el operador de fusión nula?? :

function add_course_section_filter() {
    $section = $_GET[ 'course_section' ][ 0 ] ?? $_GET[ 'course_section' ][ 1 ] ?? -1;
    echo ' <select name="course_section[]" style="float:none;"><option value="">Course Section...</option>';
    for ( $i = 1; $i <= 3; ++$i ) {
        $selected = $i == $section ? ' selected="selected"' : '';
        echo '<option value="' . $i . '"' . $selected . '>Section ' . $i . '</option>';
    }
    echo '</select>';
    echo '<input type="submit" class="button" value="Filter">';
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );

function filter_users_by_course_section( $query ) {
    global $pagenow;

    if ( is_admin() && 'users.php' == $pagenow) {
        $section = $_GET[ 'course_section' ][ 0 ] ?? $_GET[ 'course_section' ][ 1 ] ?? null;
        if ( null !== $section ) {
            $meta_query = array(
                array(
                    'key' => 'course_section',
                    'value' => $section
                )
            );
            $query->set( 'meta_key', 'course_section' );
            $query->set( 'meta_query', $meta_query );
        }
    }
}
add_filter( 'pre_get_users', 'filter_users_by_course_section' );

¡Disfrutar!

morfático
fuente
Entonces, ¿su solución aún funciona después de 4.6.0? ¿Hay alguna manera más fácil de hacerlo con la versión más reciente de WordPress? Parece que no puedo encontrar ninguna guía hecha este año
Jeremy Muckel
1
@JeremyMuckel la respuesta corta a su pregunta es "sí". Mi vieja solución aún funciona. Lo he estado utilizando en producción regularmente durante meses y la mayoría de mis sitios están actualizados a la última versión estable de WP (actualmente 4.9.6). Dicho esto, proporcioné una solución actualizada que utiliza el nuevo parche y que también corrige un error sutil en mi solución anterior.
morpático
Esto fue útil, pero su código de formulario en "Cómo solucionarlo" y "Bonus: PHP 7 Refactor" falta </select>, también encontré que para que funcione, tuve que ponerlo <form method="get">antes del menú de selección y </form>después del botón de filtro.
cogdog
@cogdog buena captura de las </select>etiquetas que faltan ! Los agregué. Es extraño que necesitaras envolverlo <form>ya que toda esta página está envuelta en una gran forma, y ​​este código se inyecta en el medio. Aunque me alegra que lo hayas hecho funcionar. :)
morpático
4

En el núcleo, los nombres de entrada inferiores están marcados con el número de instancia, por ejemplo new_role(arriba) y new_role2(abajo). Aquí hay dos enfoques para una convención de nomenclatura similar, a saber course_section1(arriba) y course_section2(abajo):

Enfoque n. ° 1

Como la $whichvariable ( arriba , abajo ) no se pasa al restrict_manage_usersgancho, podríamos evitarlo creando nuestra propia versión de ese gancho:

Creemos el gancho de acción wpse_restrict_manage_usersque tiene acceso a una $whichvariable:

add_action( 'restrict_manage_users', function() 
{
    static $instance = 0;   
    do_action( 'wpse_restrict_manage_users', 1 === ++$instance ? 'top' : 'bottom'  );

} );

Entonces podemos conectarlo con:

add_action( 'wpse_restrict_manage_users', function( $which )
{
    $name = 'top' === $which ? 'course_section1' : 'course_section2';

    // your stuff here
} );

donde ahora tenemos $nameque course_section1en la parte superior y course_section2en la parte inferior .

Enfoque n. ° 2

Enganchemos restrict_manage_users, para mostrar menús desplegables, con un nombre diferente para cada instancia:

function add_course_section_filter() 
{
    static $instance= 0;    

    // Dropdown options         
    $options = '';
    foreach( range( 1, 3 ) as $rng )
    {
        $options = sprintf( 
            '<option value="%1$d" %2$s>Section %1$d</option>',
            $rng,
            selected( $rng, get_selected_course_section(), 0 )
        );
    }

    // Display dropdown with a different name for each instance
    printf( 
        '<select name="%s" style="float:none;"><option value="0">%s</option>%s</select>', 
        'course_section' . ++$instance,
        __( 'Course Section...' ),
        $options 
    );


    // Button
    printf (
        '<input id="post-query-submit" type="submit" class="button" value="%s" name="">',
        __( 'Filter' )
    );
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );

donde usamos la función central selected()y la función auxiliar:

/**
 * Get the selected course section 
 * @return int $course_section
 */
function get_selected_course_section()
{
    foreach( range( 1, 2) as $rng )
        $course_section = ! empty( $_GET[ 'course_section' . $rng ] )
            ? $_GET[ 'course_section' . $rng ]
            : -1; // default

    return (int) $course_section;
}

Entonces también podríamos usar esto cuando verifiquemos la sección del curso seleccionado en la pre_get_usersdevolución de llamada de acción.

Birgire
fuente
Este es un enfoque fascinante. Nunca he usado la staticpalabra clave de esta manera (solo dentro de las clases). ¿Se $instanceconvierte en una variable global cuando haces esto? ¿Tiene que preocuparse por las colisiones de nombres variables? También me gusta la técnica de crear una nueva acción que se apoya en una existente. ¡Gracias!
morfático
Este enfoque puede ser útil a veces y se usa en el núcleo para, por ejemplo, contar instancias de código corto (galería, lista de reproducción, audio). El alcance de la variable estática aquí no interferirá con el alcance de la variable global. El valor de la variable estática se conservará entre esas llamadas de función, que no es el caso con las variables locales. Busqué y encontré este bonito tutorial que tiene más detalles. @morphatic
birgire
4

Probé su código tanto en Wordpress 4.4 como en Wordpress 4.3.1. Con la versión 4.4, encuentro exactamente el mismo problema que tú. ¡Sin embargo, su código funciona correctamente en la versión 4.3.1!

Creo que este es un error de Wordpress. No sé si ya se ha informado. Creo que la razón detrás del error podría ser que el botón enviar envía dos veces la consulta. Si observa los vars de consulta, verá que course_section aparece dos veces, una con el valor correcto y otra vacía.

Editar: esta es la solución de JavaScript

¡Simplemente agregue esto al archivo functions.php de su tema y cambie NAME_OF_YOUR_INPUT_FIELD al nombre de su campo de entrada! Como WordPress carga automáticamente jQuery en el lado del administrador, no tiene que poner en cola ningún script. Este fragmento de código simplemente agrega un detector de cambios a las entradas desplegables y luego actualiza automáticamente el otro menú desplegable para que coincida con el mismo valor. Más explicaciones aquí.

add_action( 'in_admin_footer', function() {
?>
<script type="text/javascript">
    var el = jQuery("[name='NAME_OF_YOUR_INPUT_FIELD']");
    el.change(function() {
        el.val(jQuery(this).val());
    });
</script>
<?php
} );

¡Espero que esto ayude!

Linnea Huxford
fuente
Gracias linnea. Sí, encontré lo mismo, que cuando haces clic Filterenvía el valor correcto, pero luego vuelve a dirigir a la página, esta vez eliminando el valor. Supongo que es una especie de "característica" de seguridad para evitar el envío de valores aleatorios, potencialmente maliciosos, pero no sé cómo solucionarlo. Suspiro.
morfático
¡OH! Descubrí por qué la var aparece dos veces. Debido a que hay un menú desplegable ARRIBA y ABAJO en la tabla de usuarios y ambos tienen el mismo nameatributo. Si uso el menú desplegable ABAJO de la tabla para hacer el filtrado, funciona como se esperaba. Dado que ese campo viene después del que está arriba, su valor nulo anula al anterior. Hmmm ....
morfático
¡Buen descubrimiento! Estaba tratando de averiguar de dónde venía el duplicado. Creo que un poco de JavaScript podría solucionar esto. Haga que establezca el otro menú desplegable como el mismo valor antes de enviar el formulario.
Linnea Huxford
1

Esta es una solución Javascript diferente que puede ser útil para algunas personas. En mi caso, simplemente eliminé por completo la segunda lista de selección (inferior). Me parece que nunca uso las entradas inferiores de todos modos ...

add_action( 'in_admin_footer', function() {
    ?>
    <script type="text/javascript">
        jQuery(".tablenav.bottom select[name='course_section']").remove();
        jQuery(".tablenav.bottom input#post-query-submit").remove();
    </script>
    <?php
} );
locomo
fuente
1

Solución sin JavaScript

Dé a la selección un nombre que sea "estilo matriz", de esta manera:

echo '<select name="course_section[]" style="float:none;">';

Luego se pasan AMBOS parámetros (desde la parte superior y la parte inferior de la tabla), y ahora en un formato de matriz conocido.

Entonces, el valor se puede usar así en la pre_get_usersfunción:

function filter_users_by_course_section( $query ) {
    global $pagenow;

    // if not on users page in admin, get out
    if ( ! is_admin() || 'users.php' != $pagenow ) {
        return;
    } 

    // if no section selected, get out
    if ( empty( $_GET['course_section'] ) ) {
        return;
    }

    // course_section is known to be set now, so load it
    $section = $_GET['course_section'];

    // the value is an array, and one of the two select boxes was likely
    // not set to anything, so use array_filter to eliminate empty elements
    $section = array_filter( $section );

    // the value is still an array, so get the first value
    $section = reset( $section );

    // now the value is a single value, such as 1
    $meta_query = array(
        array(
            'key' => 'course_section',
            'value' => $section
        )
    );

    $query->set( 'meta_key', 'course_section' );
    $query->set( 'meta_query', $meta_query );
}
nombre_usuario_aleatorio
fuente
0

otra solución

puedes poner tu cuadro de selección de filtro en un archivo separado como user_list_filter.php

y usar require_once 'user_list_filter.php'en su función de devolución de llamada de acción

user_list_filter.php archivo:

<select name="course_section" style="float:none;">
    <option value="">Course Section...</option>
    <?php for ( $i = 1; $i <= 3; ++$i ) {
        if ( $i == $_GET[ 'course_section' ] ) { ?>
        <option value="<?=$i?>" selected="selected">Section <?=$i?></option>
        <?php } else { ?>
        <option value="<?=$i?>">Section <?=$i?></option>
        <?php }
     }?>
</select>
<input id="post-query-submit" type="submit" class="button" value="Filter" name="">

y en su devolución de llamada de acción:

function add_course_section_filter() {
    require_once 'user_list_filter.php';
}
Alfa Elf
fuente