Como filtrar usuários na página de usuários administrativos por meta campo personalizado?

9

O problema

O WP parece remover o valor da minha variável de consulta antes que ela seja usada para filtrar a lista de usuários.

My Code

Esta função adiciona uma coluna personalizada à minha tabela Usuários em /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 função informa ao WP como preencher valores na coluna:

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' );

Isso adiciona um menu suspenso e um Filterbotão acima da tabela Usuários:

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 função altera a consulta do usuário para adicionar meu 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' );

Outra informação

Ele cria meu menu suspenso corretamente. Quando seleciono uma seção do curso e clico Filterna página, ela é atualizada e course_sectionaparece na URL, mas ela não possui nenhum valor associado. Se eu verificar as solicitações HTTP, isso mostra que ele é enviado com o valor de variável correto, mas há um 302 Redirectque parece remover o valor que eu selecionei.

Se eu enviar a course_sectionvariável digitando-a diretamente na URL, o filtro funcionará conforme o esperado.

Meu código é basicamente baseado neste código de Dave Court .

Também tentei colocar minha consulta var na lista de permissões usando esse código, mas sem sorte:

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

Estou usando o WP 4.4. Alguma idéia de por que meu filtro não está funcionando?

morfático
fonte
Para sua informação, adicionei um tíquete no site do WP Trac que evitaria que os desenvolvedores precisassem passar por qualquer uma das etapas descritas abaixo.
morphatic

Respostas:

6

ATUALIZAÇÃO 28/06/2018

Enquanto o código abaixo funciona principalmente bem, aqui está uma reescrita do código para WP> = 4.6.0 (usando o 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');

Incorporei várias idéias de @birgire e @cale_b, que também oferecem soluções abaixo que valem a pena ser lidas. Especificamente, eu:

  1. Usou a $whichvariável que foi adicionada emv4.6.0
  2. Prática recomendada usada para o i18n usando seqüências de caracteres traduzíveis, por exemplo __( 'Filter' )
  3. Laços trocado pelo (mais elegante?) array_map(), array_filter()Erange()
  4. Usado sprintf()para gerar os modelos de marcação
  5. Usou a notação de matriz entre colchetes em vez de array()

Por fim, descobri um bug em minhas soluções anteriores. Essas soluções sempre favorecem o TOP <select>sobre o BOTTOM <select>. Portanto, se você selecionou uma opção de filtro na lista suspensa superior e, em seguida, selecionou uma na lista suspensa inferior, o filtro ainda usará apenas o valor que estiver acima (se não estiver em branco). Esta nova versão corrige esse bug.

ATUALIZAÇÃO 14-02-2018

Esse problema foi corrigido desde o WP 4.6.0 e as alterações estão documentadas nos documentos oficiais . A solução abaixo ainda funciona, no entanto.

O que causou o problema (WP <4.6.0)

O problema era que a restrict_manage_usersação era chamada duas vezes: uma vez ACIMA da tabela Usuários e uma vez ABAIXO. Isso significa que DUAS selectlistas suspensas são criadas com o mesmo nome . Quando o Filterbotão é clicado, qualquer valor que esteja no segundo selectelemento (ou seja, abaixo da tabela) substitui o valor no primeiro, ou seja, acima da tabela.

Caso você queira mergulhar na origem do WP, a restrict_manage_usersação é acionada de dentro WP_Users_List_Table::extra_tablenav($which), que é a função que cria a lista suspensa nativa para alterar a função de um usuário. Essa função tem a ajuda da $whichvariável que informa se está criando selecto formulário acima ou abaixo do formulário e permite atribuir aos dois menus suspensos nameatributos diferentes . Infelizmente, a $whichvariável não é passada para a restrict_manage_usersação, portanto, precisamos criar outra maneira de diferenciar nossos próprios elementos personalizados.

Uma maneira de fazer isso, como sugere @Linnea , seria adicionar um pouco de JavaScript para capturar o Filterclique e sincronizar os valores das duas caixas suspensas. Eu escolhi uma solução somente PHP que descreverei agora.

Como corrigi-lo

Você pode tirar proveito da capacidade de transformar entradas HTML em matrizes de valores e filtrar a matriz para se livrar de quaisquer valores indefinidos. Aqui está o 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' );

Bônus: Refator do PHP 7

Como estou entusiasmado com o PHP 7, caso você esteja executando o WP em um servidor PHP 7, aqui está uma versão mais curta e mais sexy usando o operador coalescente nulo?? :

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' );

Aproveitar!

morfático
fonte
Então, sua solução ainda funciona após a 4.6.0? Existe uma maneira mais fácil de fazer isso com a versão mais recente do wordpress? Parece que não consigo encontrar guias feitos como este ano
Jeremy Muckel 26/06
11
@ JeremyMuckel, a resposta curta para sua pergunta é "sim". Minha solução antiga ainda funciona. Uso-o regularmente há meses e a maioria dos meus sites é atualizada para a versão estável mais recente do WP (atualmente 4.9.6). Dito isto, forneci uma solução atualizada que usa o novo patch e que também corrige um bug sutil na minha solução anterior.
morphatic
Isso foi útil, mas seu código de formulário em "Como corrigi-lo" e "Bônus: PHP 7 Refactor" está ausente </select>. Também achei que para fazê-lo funcionar, tive que colocar <form method="get">antes do menu de seleção e </form>depois do botão de filtro.
Cogdog 21/10/19
@cogdog boa captura das </select>tags ausentes ! Eu os adicionei. Estranho que você precisava envolvê-lo em uma <form>vez que esta página inteira está envolvida em uma grande forma e esse código é injetado no meio dela. Ainda bem que você conseguiu. :)
morphatic 22/10/19
4

No núcleo, os nomes de entrada inferiores são marcados com o número da instância, por exemplo, new_role(superior) e new_role2(inferior). Aqui estão duas abordagens para uma convenção de nomenclatura semelhante, a saber course_section1(em cima) e course_section2(em baixo):

Abordagem # 1

Como a $whichvariável ( superior , inferior ) não é passada para o restrict_manage_usersgancho, podemos contornar isso criando nossa própria versão desse gancho:

Vamos criar o gancho de ação wpse_restrict_manage_usersque tem acesso a uma $whichvariável:

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

} );

Então podemos conectá-lo com:

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

    // your stuff here
} );

onde nós temos agora $namecomo course_section1no topo e course_section2na parte inferior .

Abordagem # 2

Vamos nos conectar restrict_manage_users, para exibir listas suspensas, com um nome diferente para cada instância:

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' );

onde usamos a função principal selected()e a função 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;
}

Também poderíamos usar isso quando verificamos a seção de curso selecionada no pre_get_usersretorno de chamada da ação.

Birgire
fonte
Esta é uma abordagem fascinante. Eu nunca usei a staticpalavra-chave dessa maneira (apenas dentro das classes). Torna- $instancese uma variável global quando você faz isso? Você precisa se preocupar com colisões de nomes de variáveis? Também gosto da técnica de criar uma nova ação que se baseia em uma já existente. Obrigado!
morphatic
Essa abordagem pode ser útil algumas vezes e é usada no núcleo para, por exemplo, contar instâncias de códigos curtos (galeria, lista de reprodução, áudio). O escopo da variável estática aqui não interferirá no escopo da variável global. O valor da variável estática será preservado entre essas chamadas de função, o que não é o caso das variáveis ​​locais. Eu procurei e encontrei este bom tutorial que tem mais detalhes. @morphatic
birgire
4

Testei seu código no Wordpress 4.4 e no Wordpress 4.3.1. Com a versão 4.4, encontro exatamente o mesmo problema que você. No entanto, seu código funciona corretamente na versão 4.3.1!

Eu acho que isso é um bug do Wordpress. Não sei se já foi relatado. Acho que a razão por trás do bug pode ser que o botão enviar está enviando os vars de consulta duas vezes. Se você observar os vars de consulta, verá que course_section é listado duas vezes, uma com o valor correto e uma vez vazio.

Edit: Esta é a solução JavaScript

Simplesmente adicione isso ao arquivo functions.php do seu tema e altere NAME_OF_YOUR_INPUT_FIELD para o nome do seu campo de entrada! Como o WordPress carrega automaticamente o jQuery no lado do administrador, você não precisa enfileirar nenhum script. Esse trecho de código simplesmente adiciona um ouvinte de alterações às entradas suspensas e atualiza automaticamente a outra lista suspensa para corresponder ao mesmo valor. Mais explicações aqui.

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 isto ajude!

Linnea Huxford
fonte
Obrigado Linnea. Sim, descobri a mesma coisa: quando você clica Filternele, envia o valor correto, mas depois o redireciona novamente para a página, desta vez retirando o valor. Meu palpite é que é algum tipo de "recurso" de segurança para impedir que valores aleatórios, potencialmente maliciosos, sejam enviados, mas não sei como solucionar isso. Suspiro.
morphatic
OH! Eu descobri por que o var aparece duas vezes. Porque há uma lista suspensa ACIMA e ABAIXO da tabela de usuários e os dois têm o mesmo nameatributo. Se eu usar a lista suspensa ABAIXO da tabela para fazer a filtragem, ela funcionará conforme o esperado. Como esse campo vem após o acima, seu valor nulo substitui o anterior. Hmmm ....
morfático
Boa descoberta! Eu estava tentando descobrir de onde vinha a duplicata. Acho que talvez um pouco de JavaScript possa corrigir isso. Defina a outra lista suspensa como o mesmo valor antes de enviar o formulário.
Linnea Huxford 4/16/16
1

Esta é uma solução Javascript diferente que pode ser útil para algumas pessoas. No meu caso, simplesmente removi a segunda lista de seleção (abaixo) por completo. Acho que nunca uso as entradas inferiores ...

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
fonte
1

Solução sem JavaScript

Dê ao select um nome que seja "estilo de matriz", assim:

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

Em seguida, os dois parâmetros são passados ​​(da parte superior e inferior da tabela) e agora em um formato de matriz conhecido.

Então, o valor pode ser usado assim na pre_get_usersfunção:

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 );
}
random_user_name
fonte
0

outra solução

você pode colocar sua caixa de seleção de filtro em um arquivo separado, como user_list_filter.php

e use require_once 'user_list_filter.php'em sua função de retorno de chamada de ação

user_list_filter.php Arquivo:

<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="">

e em sua ação de retorno:

function add_course_section_filter() {
    require_once 'user_list_filter.php';
}
Alpha Elf
fonte