Як відфільтрувати користувачів на сторінці користувачів адміністратора за спеціальним мета-полем?


9

Проблема

Здається, що WP видаляє значення моєї змінної запитів, перш ніж вона звикне фільтрувати список користувачів.

Мій код

Ця функція додає спеціальний стовпець до моєї таблиці користувачів на /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кнопку над таблицею Користувачі:

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-адресу, фільтр працює як очікувалося.

Мій код приблизно базується на цьому коді від Дейв Корта .

Я також спробував додати білий список ваших запитів за допомогою цього коду, але не пощастило:

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

Я використовую WP 4.4. Будь-які ідеї, чому мій фільтр не працює?


FYI, я додав квиток на веб-сайті 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дію викликають двічі: один раз ПРО ТАБЛИВО таблицю користувачів та один раз НІЖ. Це означає, що ДВІ 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, ось коротша, сексуальна версія, використовуючи оператор null coalescing?? :

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

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


Тож ваше рішення все ще працює після 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змінна ( вгорі , внизу ) не передається 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зворотному дзвінку дії.


Це захоплюючий підхід. Я ніколи не використовував staticключове слово таким чином (лише в межах класів). Чи $instanceстає глобальною змінною, коли ви це робите? Чи потрібно турбуватися про зіткнення змінних імен? Мені також подобається техніка створення нової дії, що скарбничка спирається на існуючу. Дякую!
— морфатичний

Такий підхід іноді може бути зручним і використовується в основі, наприклад, для підрахунку випадків короткого коду (галерея, список відтворення, аудіо). Область статичної змінної тут не зіб’ється з глобальною областю змінної. Значення статичної змінної зберігатиметься між тими викликами функцій, що не стосується локальних змінних. Я шукав і знайшов цей приємний підручник, який має більше деталей. @morphatic
— birgire

4

Я перевірив ваш код і в Wordpress 4.4, і в Wordpress 4.3.1. З версією 4.4 я стикаюся з точно таким же питанням, як і ви. Однак ваш код працює правильно у версії 4.3.1!

Я думаю, що це помилка Wordpress. Я не знаю, чи про це ще не повідомлялося. Я думаю, що причина помилки може полягати в тому, що кнопка "Відправити" надсилає запит двічі. Якщо ви подивитесь на кількість запитів, ви побачите, що перелік__секцій перераховано двічі, один раз із правильним значенням та один раз порожнім.

Редагувати: Це рішення JavaScript

Просто додайте це до файлу function.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
} );

1

Рішення без JavaScript

Укажіть назву вибору, яка є "в масиві", наприклад:

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

Потім передаються параметри BOTH (зверху та ботом таблиці), і тепер у відомому форматі масиву.

Тоді значення можна використовувати у 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 );
}

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';
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.