Jak filtrować użytkowników na stronie administratorów według niestandardowego pola meta?

9

Problem

WP wydaje się usuwać wartość mojej zmiennej zapytania, zanim zostanie ona użyta do filtrowania listy użytkowników.

Mój kod

Ta funkcja dodaje niestandardową kolumnę do mojej tabeli Użytkownicy na /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' );

Ta funkcja mówi WP, jak wypełnić wartości w kolumnie:

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

To dodaje menu i Filterprzycisk nad tabelą Użytkownicy:

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

Ta funkcja zmienia zapytanie użytkownika, aby dodać moje 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' );

Inne informacje

Poprawnie tworzy moje menu rozwijane. Kiedy wybieram sekcję kursu i klikam, Filterstrona odświeża się i course_sectionpojawia w adresie URL, ale nie ma z nią żadnej wartości. Jeśli sprawdzę żądania HTTP, pokaże, że jest przesyłane z poprawną wartością zmiennej, ale wtedy jest taka, 302 Redirectktóra wydaje się usuwać wybraną wartość.

Jeśli prześlę course_sectionzmienną, wpisując ją bezpośrednio w adresie URL, filtr działa zgodnie z oczekiwaniami.

Mój kod jest z grubsza oparty na tym kodzie z Dave Court .

Próbowałem również dodać białą listę do mojego zapytania var przy użyciu tego kodu, ale bez powodzenia:

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

Używam WP 4.4. Wszelkie pomysły, dlaczego mój filtr nie działa?

morfatyczny
źródło
Do Twojej wiadomości dodałem bilet na stronie WP Trac , który uniemożliwi programistom przeskakiwanie przez którekolwiek z kół opisanych poniżej.
morfatyczny

Odpowiedzi:

6

AKTUALIZACJA 28.06.2018

Chociaż poniższy kod w większości działa dobrze, oto przepisanie kodu dla WP> = 4.6.0 (przy użyciu 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');

Włączyłem kilka pomysłów od @birgire i @cale_b, które oferują również rozwiązania, które warto przeczytać. W szczególności:

  1. Użyto $whichzmiennej, która została dodanav4.6.0
  2. Zastosowano najlepszą praktykę dla i18n, stosując przekładalne ciągi znaków, np __( 'Filter' )
  3. Wymieniane pętle dla (bardziej modne?) array_map(), array_filter()Orazrange()
  4. Służy sprintf()do generowania szablonów znaczników
  5. Zamiast notacji zastosowano tablicę nawiasów kwadratowych array()

Wreszcie odkryłem błąd w moich wcześniejszych rozwiązaniach. Te rozwiązania zawsze faworyzują TOP <select>nad DOLNYM <select>. Jeśli więc wybierzesz opcję filtrowania z górnego menu rozwijanego, a następnie wybierz jeden z dolnego menu rozwijanego, filtr będzie nadal używał tylko tej wartości, która była na górze (jeśli nie jest pusta). Ta nowa wersja naprawia ten błąd.

AKTUALIZACJA 14.02.2018

Ten problem został załatany od wersji WP 4.6.0, a zmiany są udokumentowane w oficjalnych dokumentach . Poniższe rozwiązanie nadal jednak działa.

Co było przyczyną problemu (WP <4.6.0)

Problem polegał na tym, że restrict_manage_usersakcja jest wywoływana dwukrotnie: raz POWYŻEJ tabeli użytkowników i raz PONIŻEJ. Oznacza to, że selecttworzone są DWIE listy rozwijane o tej samej nazwie . Po Filterkliknięciu przycisku dowolna wartość znajdująca się w drugim selectelemencie (tj. PONIŻEJ tabeli) zastępuje wartość w pierwszym elemencie, tj. Powyżej tabeli.

Jeśli chcesz zanurzyć się w źródle WP, restrict_manage_usersakcja jest uruchamiana od wewnątrz WP_Users_List_Table::extra_tablenav($which), czyli funkcji, która tworzy natywne menu rozwijane, aby zmienić rolę użytkownika. Ta funkcja ma $whichzmienną, która mówi jej, czy tworzy selectpowyższą czy pod formularzem, i pozwala nadać obu listom różne nameatrybuty. Niestety$which zmienna nie jest przekazywana do restrict_manage_usersakcji, dlatego musimy wymyślić inny sposób na odróżnienie własnych elementów niestandardowych.

Jednym ze sposobów na zrobienie tego, jak sugeruje @Linnea , byłoby dodanie JavaScript, aby go złapaćFilter kliknięcie i zsynchronizować wartości dwóch list rozwijanych. Wybrałem rozwiązanie oparte tylko na PHP, które opiszę teraz.

Jak to naprawić

Możesz skorzystać z możliwości przekształcania danych HTML w tablice wartości, a następnie filtrować tablicę, aby pozbyć się wszelkich niezdefiniowanych wartości. Oto kod:

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

Premia: Refaktor PHP 7

Ponieważ jestem podekscytowany PHP 7, na wypadek, gdybyś uruchomił WP na serwerze PHP 7, oto krótsza, bardziej seksowna wersja z operatorem koalescencji null?? :

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

Cieszyć się!

morfatyczny
źródło
Czy twoje rozwiązanie nadal działa po wersji 4.6.0? Czy jest łatwiejszy sposób na zrobienie tego za pomocą najnowszej wersji Wordpress? Nie mogę znaleźć żadnych przewodników wykonanych w tym roku
Jeremy Muckel,
1
@JeremyMuckel krótka odpowiedź na twoje pytanie brzmi „tak”. Moje stare rozwiązanie nadal działa. Używam go w produkcji regularnie od miesięcy i większość moich stron jest aktualizowana do najnowszej stabilnej wersji WP (obecnie 4.9.6). To powiedziawszy, dostarczyłem zaktualizowane rozwiązanie, które wykorzystuje nową łatkę, a także naprawia subtelny błąd w moim poprzednim rozwiązaniu.
morfatyczny
Było to pomocne, ale w kodzie formularza w „Jak to naprawić” i „Bonus: PHP 7 Refactor” brakuje </select>również i znalazłem, aby go uruchomić, musiałem umieścić <form method="get">przed menu wyboru i </form>po przycisku filtra.
cogdog
@ dogdog dobrze łapie brakujące </select>tagi! Dodałem je. Dziwne, że musiałeś to zawinąć, <form>ponieważ cała strona jest owinięta w jedną dużą formę, a ten kod zostaje wstrzyknięty w jej środek. Cieszę się, że to działa. :)
morfatyczny
4

W rdzeniu dolne nazwy wejściowe są oznaczone numerem instancji, np. new_role(Góra) i new_role2(dół). Oto dwa podejścia do podobnej konwencji nazewnictwa, a mianowicie course_section1(u góry) i course_section2(u dołu):

Podejście nr 1

Ponieważ $whichzmienna ( góra , dół ) nie jest przekazywana do restrict_manage_usershaka, możemy to obejść, tworząc własną wersję tego haka:

Utwórzmy hak akcji, wpse_restrict_manage_usersktóry ma dostęp do $whichzmiennej:

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

} );

Następnie możemy zaczepić o:

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

    // your stuff here
} );

gdzie mamy teraz $namejak course_section1na górze i course_section2na dole .

Podejście nr 2

Zahaczmy się restrict_manage_users, aby wyświetlić menu rozwijane, z inną nazwą dla każdej instancji:

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

gdzie użyliśmy funkcji podstawowej selected()i funkcji pomocniczej:

/**
 * 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;
}

Następnie moglibyśmy tego użyć, gdy sprawdzimy wybraną sekcję kursu w pre_get_userswywołaniu zwrotnym akcji.

birgire
źródło
To fascynujące podejście. Nigdy nie użyłem staticsłowa kluczowego w ten sposób (tylko w ramach klas). Czy $instancekiedy to zrobisz, stanie się zmienną globalną? Czy musisz się martwić kolizjami nazw zmiennych? Podoba mi się również technika tworzenia nowej akcji, która opiera się na istniejącej. Dzięki!
morfatyczny
Takie podejście może czasem być przydatne i jest wykorzystywane w rdzeniu, np. Do liczenia wystąpień krótkiego kodu (galeria, lista odtwarzania, dźwięk). Zakres zmiennej statycznej nie będzie bałaganu z globalnym zakresem zmiennej. Wartość zmiennej statycznej zostanie zachowana między tymi wywołaniami funkcji, co nie ma miejsca w przypadku zmiennych lokalnych. Szukałem i znalazłem ten fajny samouczek, który zawiera więcej szczegółów. @morphatic
birgire
4

Przetestowałem twój kod zarówno w Wordpress 4.4, jak i Wordpress 4.3.1. W wersji 4.4 napotykam dokładnie ten sam problem, co ty. Jednak Twój kod działa poprawnie w wersji 4.3.1!

Myślę, że to błąd Wordpress. Nie wiem, czy zostało to jeszcze zgłoszone. Myślę, że przyczyną błędu może być to, że przycisk wysyłania przesyła zmienne zapytania dwukrotnie. Jeśli spojrzysz na zmienne zapytania, zobaczysz, że sekcja_kursu jest wymieniona dwa razy, raz z poprawną wartością, a raz pusta.

Edycja: To jest rozwiązanie JavaScript

Po prostu dodaj to do pliku functions.php swojego motywu i zmień NAME_OF_YOUR_INPUT_FIELD na nazwę swojego pola wejściowego! Ponieważ WordPress automatycznie ładuje jQuery po stronie administratora, nie musisz kolejkować żadnych skryptów. Ten fragment kodu po prostu dodaje detektor zmian do danych rozwijanych, a następnie automatycznie aktualizuje inne menu rozwijane, aby dopasować tę samą wartość. Więcej wyjaśnień tutaj.

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

Mam nadzieję że to pomoże!

Linnea Huxford
źródło
Dzięki, Linnea. Tak, znalazłem to samo, że po kliknięciu Filterprzesyła poprawną wartość, ale następnie przekierowuje z powrotem na stronę, tym razem usuwając wartość. Domyślam się, że jest to pewnego rodzaju „funkcja” bezpieczeństwa, która zapobiega przesyłaniu losowych, potencjalnie złośliwych wartości, ale nie wiem, jak sobie z tym poradzić. Westchnienie.
morfatyczny
O! Zrozumiałem, dlaczego var pojawia się dwa razy. Ponieważ istnieje lista rozwijana zarówno POWYŻEJ, jak i PONIŻEJ, tabela użytkowników i oba mają ten sam nameatrybut. Jeśli użyję menu rozwijanego PONIŻEJ tabeli, aby wykonać filtrowanie, działa ono zgodnie z oczekiwaniami. Ponieważ to pole występuje po polu powyżej, jego wartość zerowa zastępuje wcześniejsze. Hmmm ....
morfatyczny
Dobre znalezisko! Próbowałem dowiedzieć się, skąd pochodzi duplikat. Myślę, że może trochę JavaScript może to naprawić. Pozwól ustawić drugą listę rozwijaną jako tę samą wartość przed przesłaniem formularza.
Linnea Huxford
1

To jest inne rozwiązanie JavaScript, które może być pomocne dla niektórych osób. W moim przypadku po prostu całkowicie usunąłem 2. (dolną) listę wyboru. Uważam, że i tak nigdy nie używam dolnych danych wejściowych ...

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
źródło
1

Rozwiązanie inne niż JavaScript

Nadaj wyborowi nazwę „w stylu tablicowym”, taką jak:

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

Następnie przekazywane są parametry OBA (od góry i dołu tabeli), a teraz w znanym formacie tablicowym.

Następnie można użyć tej wartości w pre_get_usersfunkcji:

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 );
}
losowa_nazwa_użytkownika
źródło
0

inne rozwiązanie

możesz umieścić pole wyboru filtra w osobnym pliku, np user_list_filter.php

i użyj require_once 'user_list_filter.php'w swojej funkcji wywołania zwrotnego akcji

user_list_filter.php plik:

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

i w twoim działaniu zwrotnym:

function add_course_section_filter() {
    require_once 'user_list_filter.php';
}
Alpha Elf
źródło