Как отфильтровать пользователей на странице администратора с помощью пользовательского метаполя?

9

Проблема

WP, кажется, удаляет значение моей переменной запроса, прежде чем он будет использован для фильтрации списка пользователей.

Мой код

Эта функция добавляет пользовательский столбец в мою таблицу Users /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' );

Эта функция сообщает WP, как заполнять значения в столбце:

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

Это добавляет выпадающий список и Filterкнопку над таблицей Users:

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

Эта функция изменяет пользовательский запрос, чтобы добавить мой 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' );

Другая информация

Это создает мой выпадающий правильно. Когда я выбираю раздел курса и щелкаю Filterстраницу, она обновляется и course_sectionотображается в URL, но она не имеет значения, связанного с ним. Если я проверяю HTTP-запросы, это показывает, что они отправляются с правильным значением переменной, но затем есть такая, 302 Redirectкоторая, кажется, удаляет выбранное мной значение.

Если я отправлю course_sectionпеременную, введя ее непосредственно в URL, фильтр будет работать как положено.

Мой код примерно основан на этом коде от Дейва Корта .

Я также попытался внести в белый список мой запрос var, используя этот код, но безуспешно:

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

Я использую WP 4.4. Есть идеи, почему мой фильтр не работает?

morphatic
источник
К вашему сведению, я добавил на сайт WP Trac тикет , который не позволил бы разработчикам перепрыгивать через любые обручи, описанные ниже.
морфический

Ответы:

6

ОБНОВЛЕНИЕ 2018-06-28

В то время как приведенный ниже код в основном работает нормально, приведем переписывание кода для WP> = 4.6.0 (с использованием 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');

Я включил несколько идей от @birgire и @cale_b, которые также предлагают ниже решения, которые стоит прочитать. В частности, я:

  1. Использовал $whichпеременную, которая была добавлена ​​вv4.6.0
  2. Использовать лучшие практики для i18n с использованием переводимых строк, например __( 'Filter' )
  3. Обменялись петли для (более модным?) array_map(), array_filter()Иrange()
  4. Используется sprintf()для генерации шаблонов разметки
  5. Использовал обозначение массива в квадратных скобках вместо array()

Наконец, я обнаружил ошибку в моих предыдущих решениях. Эти решения всегда предпочитают ТОП <select>над ДНЕМ <select>. Таким образом, если вы выбрали опцию фильтра в верхнем раскрывающемся списке, а затем впоследствии выбрали один из нижних раскрывающихся списков, фильтр все равно будет использовать только то значение, которое было сверху (если оно не пустое). Эта новая версия исправляет эту ошибку.

ОБНОВЛЕНИЕ 2018-02-14

Эта проблема была исправлена ​​начиная с WP 4.6.0, и эти изменения документированы в официальных документах . Решение ниже все еще работает, хотя.

Что вызвало проблему (WP <4.6.0)

Проблема заключалась в том, что restrict_manage_usersдействие вызывалось дважды: один раз над таблицей Users и один раз НИЖЕ. Это означает, что ДВА selectраскрывающихся списка создаются с тем же именем . Когда Filterкнопка нажата, любое значение во втором selectэлементе (т. Е. Значение, находящееся ниже таблицы) переопределяет значение в первом, т. Е. Значение, находящееся над таблицей.

Если вы хотите погрузиться в источник WP, restrict_manage_usersдействие запускается изнутри WP_Users_List_Table::extra_tablenav($which), это функция, которая создает собственный выпадающий список для изменения роли пользователя. Эта функция имеет вспомогательную $whichпеременную, которая сообщает ей, создает ли она selectформу выше или ниже, и позволяет ей давать двум раскрывающимся спискам различные nameатрибуты. К сожалению, $whichпеременная не передается restrict_manage_usersдействию, поэтому нам нужно найти другой способ дифференциации наших собственных пользовательских элементов.

Как предлагает @Linnea , один из способов сделать это - добавить JavaScript, чтобы поймать Filterщелчок и синхронизировать значения двух выпадающих меню. Я выбрал решение только для PHP, которое опишу сейчас.

Как это исправить

Вы можете воспользоваться возможностью превращать входные данные HTML в массивы значений, а затем фильтровать массив, чтобы избавиться от любых неопределенных значений. Вот код:

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

Бонус: PHP 7 Refactor

Поскольку я в восторге от PHP 7, на случай, если вы используете WP на сервере PHP 7, вот более короткая и сексуальная версия с использованием оператора?? объединения нулей :

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

Наслаждайтесь!

morphatic
источник
Так ваше решение все еще работает после 4.6.0? Есть ли более простой способ сделать это с новейшей версией WordPress? Кажется, я не могу найти ни одного подобного гида в этом году
Джереми Макел
1
@JeremyMuckel, короткий ответ на ваш вопрос «да». Мое старое решение все еще работает. Я регулярно использую его в производстве уже несколько месяцев, и большинство моих сайтов обновлены до последней стабильной версии WP (в настоящее время 4.9.6). При этом я предоставил обновленное решение, которое использует новый патч и которое также исправляет небольшую ошибку в моем предыдущем решении.
морфический
Это было полезно, но в коде форм в разделах «Как это исправить» и «Бонус: PHP 7 Refactor» отсутствует элемент, который </select>я также нашел, чтобы заставить его работать, который я должен был поместить <form method="get">перед меню выбора и </form>после кнопки фильтра.
cogdog
@ Cogdog хорошо поймать недостающие </select>теги! Я добавил их. Странно, что вам нужно было обернуть его, <form>поскольку вся эта страница обернута в одну большую форму, и этот код вставляется в ее середину. Рад, что у вас все получилось. :)
морфический
4

В основном нижние входные имена помечены номером экземпляра, например new_role(вверху) и new_role2(внизу). Вот два подхода к подобному соглашению об именах, а именно course_section1(вверху) и course_section2(внизу):

Подход № 1

Поскольку $whichпеременная ( top , bottom ) не передается в restrict_manage_usersловушку, мы можем обойти это, создав собственную версию этой ловушки:

Давайте создадим хук действия, wpse_restrict_manage_usersкоторый имеет доступ к $whichпеременной:

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

} );

Тогда мы можем подключить это с:

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

    // your stuff here
} );

где мы теперь , $nameкак course_section1в верхней , и course_section2в нижней части .

Подход № 2

Давайте рассмотрим restrict_manage_usersвыпадающие списки с разными именами для каждого экземпляра:

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

где мы использовали основную функцию selected()и вспомогательную функцию:

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

Затем мы могли бы также использовать это при проверке выбранного раздела курса в pre_get_usersобратном вызове действия.

birgire
источник
Это увлекательный подход. Я никогда не использовал staticключевое слово таким образом (только в классах). Есть ли $instanceстать глобальной переменной , когда вы это делаете? Вам нужно беспокоиться о конфликтах имен переменных? Мне также нравится техника создания нового действия, которое поддерживает уже существующее. Спасибо!
морфический
Этот подход иногда удобен и используется в основном для подсчета экземпляров шорткода (галерея, плейлист, аудио). Область действия статической переменной здесь не будет связываться с областью действия глобальной переменной. Значение статической переменной будет сохранено между этими вызовами функций, что не относится к локальным переменным. Я искал и нашел этот хороший учебник, который имеет больше деталей. @ morphatic
birgire
4

Я проверил ваш код как в Wordpress 4.4, так и в Wordpress 4.3.1. С версией 4.4 я сталкиваюсь точно с той же проблемой, что и вы. Тем не менее, ваш код работает правильно в версии 4.3.1!

Я думаю, что это ошибка Wordpress. Я не знаю, сообщалось ли об этом еще. Я думаю, что причина ошибки может заключаться в том, что кнопка отправки отправляет запрос дважды. Если вы посмотрите на переменные запроса, то увидите, что поле course_section указано дважды, один раз с правильным значением и один раз пустым.

Редактировать: это решение JavaScript

Просто добавьте это в файл functions.php вашей темы и измените NAME_OF_YOUR_INPUT_FIELD на имя вашего поля ввода! Поскольку WordPress автоматически загружает jQuery на стороне администратора, вам не нужно ставить в очередь какие-либо сценарии. Этот фрагмент кода просто добавляет прослушиватель изменений к раскрывающимся входам, а затем автоматически обновляет другой раскрывающийся список в соответствии с тем же значением. Больше объяснений здесь.

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

Надеюсь это поможет!

Линни Хаксфорд
источник
Спасибо Линнея. Да, я обнаружил то же самое: когда вы нажимаете на кнопку, Filterона отправляет правильное значение, но затем снова перенаправляет обратно на страницу, на этот раз удаляя значение. Я предполагаю, что это своего рода «функция» безопасности, предотвращающая отправку случайных, потенциально вредоносных значений, но я не знаю, как обойти это. Вздох.
морфический
ОЙ! Я выяснил, почему Вар появляется дважды. Потому что есть выпадающий и ВЫШЕ и НИЖЕ таблицы пользователей, и оба они имеют один и тот же nameатрибут. Если я использую раскрывающийся список НИЖЕ таблицы, чтобы выполнить фильтрацию, она работает как положено. Поскольку это поле находится после того, что находится над ним, его нулевое значение переопределяет предыдущее. Хммм ....
морфический
Хорошая находка! Я пытался выяснить, откуда взялся дубликат. Я думаю, что, возможно, немного JavaScript мог бы исправить это. Перед отправкой формы установите для другого раскрывающегося списка то же значение.
Линнея Хаксфорд
1

Это другое решение Javascript, которое может быть полезно для некоторых людей. В моем случае я просто полностью удалил второй (нижний) список выбора. Я считаю, что я никогда не использую нижние входы в любом случае ...

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
источник
1

Решение не на JavaScript

Присвойте select имя в стиле «массив», например:

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

Затем передаются ОБА параметры (сверху и снизу таблицы), и теперь в известном формате массива.

Затем значение можно использовать следующим образом в pre_get_usersфункции:

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
источник
0

другое решение

Вы можете поместить свой фильтр выбора в отдельный файл, как user_list_filter.php

и использовать require_once 'user_list_filter.php'в вашем действии функцию обратного вызова

user_list_filter.php файл:

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

и в вашем действии обратный вызов:

function add_course_section_filter() {
    require_once 'user_list_filter.php';
}
Альфа Эльф
источник