Cum să filtrezi utilizatorii în pagina de administrare după un câmp meta personalizat?

4 ian. 2016, 05:07:23
Vizualizări: 27.4K
Voturi: 12

Problema

WordPress pare să elimine valoarea variabilei query înainte să fie folosită pentru filtrarea listei de utilizatori.

Codul Meu

Această funcție adaugă o coloană personalizată în tabelul Utilizatori din /wp-admin/users.php:

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

Această funcție spune WordPress-ului cum să completeze valorile în coloană:

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

Aceasta adaugă un dropdown și butonul Filtrare deasupra tabelului Utilizatori:

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

Această funcție modifică interogarea utilizatorilor pentru a adăuga 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' );

Alte Informații

Creează dropdown-ul corect. Când selectez o secțiune de curs și dau click pe Filtrare pagina se reîncarcă și course_section apare în URL, dar nu are nicio valoare asociată. Dacă verific cererile HTTP, se arată că este trimis cu valoarea variabilei corectă, dar apoi există o 302 Redirect care pare să elimine valoarea pe care am selectat-o.

Dacă trimit variabila course_section scriind-o direct în URL, filtrul funcționează așa cum era de așteptat.

Codul meu se bazează aproximativ pe acest cod de la Dave Court.

Am încercat de asemenea să adaug variabila query în lista albă folosind acest cod, dar fără succes:

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

Folosesc WordPress 4.4. Aveți idei de ce nu funcționează filtrul meu?

1
Comentarii

FYI, am adăugat un tichet pe site-ul WP Trac care ar preveni dezvoltatorii să mai treacă prin toate obstacolele descrise mai jos.

morphatic morphatic
4 ian. 2016 22:47:14
Toate răspunsurile la întrebare 7
4
16

ACTUALIZARE 2018-06-28

Deși codul de mai jos funcționează în mare parte bine, iată o rescriere a codului pentru WP >=4.6.0 (folosind PHP 7):

function add_course_section_filter( $which ) {

    // crește șabloane sprintf pentru <select> și <option>s
    $st = '<select name="course_section_%s" style="float:none;"><option value="">%s</option>%s</select>';
    $ot = '<option value="%s" %s>Secțiunea %s</option>';

    // determină ce buton de filtrare a fost apăsat, dacă există și setează secțiunea
    $button = key( array_filter( $_GET, function($v) { return __( 'Filtrează' ) === $v; } ) );
    $section = $_GET[ 'course_section_' . $button ] ?? -1;

    // generează codul pentru <option> și <select>
    $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, __( 'Secțiunea cursului...' ), $options );

    // afișează <select> și butonul de trimitere
    echo $select;
    submit_button(__( 'Filtrează' ), 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 __( 'Filtrează' ) === $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');

Am încorporat câteva idei de la @birgire și @cale_b care oferă, de asemenea, soluții mai jos care merită citite. Mai exact, am:

  1. Folosit variabila $which care a fost adăugată în v4.6.0
  2. Folosit cele mai bune practici pentru i18n prin utilizarea șirurilor traductibile, de ex. __( 'Filtrează' )
  3. Înlocuit buclele cu (mai la modă?) array_map(), array_filter() și range()
  4. Folosit sprintf() pentru generarea șabloanelor de markup
  5. Folosit notația de matrice cu paranteze pătrate în loc de array()

În cele din urmă, am descoperit un bug în soluțiile mele anterioare. Acele soluții întotdeauna favorizează <select>-ul de SUS față de <select>-ul de JOS. Deci, dacă selectați o opțiune de filtrare din meniul derulant de sus și apoi selectați una din cel de jos, filtrul va folosi doar valoarea care era sus (dacă nu este goală). Această nouă versiune corectează acest bug.

ACTUALIZARE 2018-02-14

Această problemă a fost rezolvată începând cu WP 4.6.0 și modificările sunt documentate în documentația oficială. Soluția de mai jos funcționează totuși.

Ce a Cauzat Problema (WP <4.6.0)

Problema a fost că acțiunea restrict_manage_users este apelată de două ori: o dată DEASUPRA tabelului Utilizatori și o dată DEDESUBTUL acestuia. Acest lucru înseamnă că două meniuri derulante select sunt create cu același nume. Când butonul Filtrează este apăsat, orice valoare se află în al doilea element select (adică cel DEDESUBTUL tabelului) înlocuiește valoarea din primul, adică cel DEASUPRA tabelului.

În cazul în care doriți să vă scufundați în sursa WP, acțiunea restrict_manage_users este declanșată din WP_Users_List_Table::extra_tablenav($which), care este funcția care creează meniul derulant nativ pentru a schimba rolul unui utilizator. Acea funcție are ajutorul variabilei $which care îi spune dacă creează select-ul deasupra sau dedesubtul formularului și îi permite să dea celor două meniuri derulante atribute name diferite. Din păcate, variabila $which nu este transmisă acțiunii restrict_manage_users, așa că trebuie să găsim o altă modalitate de a diferenția elementele noastre personalizate.

O modalitate de a face acest lucru, așa cum sugerează @Linnea, ar fi să adăugăm niște JavaScript pentru a prinde clicul pe Filtrează și a sincroniza valorile celor două meniuri derulante. Am ales o soluție doar în PHP pe care o voi descrie acum.

Cum să o Reparați

Puteți profita de capacitatea de a transforma input-urile HTML în matrice de valori și apoi să filtrați matricea pentru a elimina orice valori nedefinite. Iată codul:

    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="">Secțiunea cursului...</option>';
        for ( $i = 1; $i <= 3; ++$i ) {
            $selected = $i == $section ? ' selected="selected"' : '';
            echo '<option value="' . $i . '"' . $selected . '>Secțiunea ' . $i . '</option>';
        }
        echo '</select>';
        echo '<input type="submit" class="button" value="Filtrează">';
    }
    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: Refactorizare PHP 7

Deoarece sunt entuziasmat de PHP 7, în cazul în care rulați WP pe un server PHP 7, iată o versiune mai scurtă, mai atrăgătoare folosind operatorul de coalescență nulă ??:

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="">Secțiunea cursului...</option>';
    for ( $i = 1; $i <= 3; ++$i ) {
        $selected = $i == $section ? ' selected="selected"' : '';
        echo '<option value="' . $i . '"' . $selected . '>Secțiunea ' . $i . '</option>';
    }
    echo '</select>';
    echo '<input type="submit" class="button" value="Filtrează">';
}
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' );

Distracție!

4 ian. 2016 08:32:14
Comentarii

Deci soluția ta încă funcționează după versiunea 4.6.0? Există o cale mai ușoară de a face asta cu cea mai nouă versiune de WordPress? Nu reușesc să găsesc niciun ghid făcut în acest an.

Jeremy Muckel Jeremy Muckel
26 iun. 2018 23:05:15

@JeremyMuckel răspunsul scurt la întrebarea ta este "da". Vechea mea soluție încă funcționează. Am folosit-o în producție în mod regulat de luni de zile și majoritatea site-urilor mele sunt actualizate la cea mai recentă versiune stabilă de WP (în prezent 4.9.6). Cu toate acestea, am oferit o soluție actualizată care folosește noul patch și care rezolvă și o mică eroare din soluția mea anterioară.

morphatic morphatic
28 iun. 2018 07:40:52

Aceasta a fost utilă, dar codul tău de formular din secțiunea "How to Fix It" și "Bonus: PHP 7 Refactor" lipsește un </select>. De asemenea, am constatat că pentru a funcționa a trebuit să pun <form method="get"> înaintea meniului select și un </form> după butonul de filtrare.

cogdog cogdog
21 oct. 2019 17:36:20

@cogdog bună observație pentru tag-urile lipsă </select>! Le-am adăugat. Ciudat că a trebuit să le înfășori într-un <form> deoarece întreaga pagină este deja înfășurată într-un formular mare, iar acest cod este injectat în mijlocul lui. Mă bucur că ai reușit să-l faci să funcționeze, totuși. :)

morphatic morphatic
22 oct. 2019 06:23:17
3

Am testat codul tău atât în WordPress 4.4 cât și în WordPress 4.3.1. Cu versiunea 4.4, întâmpin exact aceeași problemă ca și tine. Totuși, codul tău funcționează corect în versiunea 4.3.1!

Cred că aceasta este o eroare în WordPress. Nu știu dacă a fost raportată încă. Cred că motivul din spatele acestei erori ar putea fi că butonul de trimitere trimite variabilele de interogare de două ori. Dacă te uiți la variabilele de interogare, vei vedea că course_section este listat de două ori, o dată cu valoarea corectă și o dată gol.

Edit: Aceasta este soluția JavaScript

Pur și simplu adaugă acest cod în fișierul functions.php al temei tale și schimbă NAME_OF_YOUR_INPUT_FIELD cu numele câmpului tău de introducere! Deoarece WordPress încarcă automat jQuery în partea de administrare, nu trebuie să încarci scripturi suplimentare. Acest fragment de cod adaugă pur și simplu un ascultător de schimbare la meniurile derulante și apoi actualizează automat celălalt meniu derulant pentru a avea aceeași valoare. Mai multe explicații aici.

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

Sper că acest lucru te ajută!

4 ian. 2016 07:18:01
Comentarii

Mulțumesc Linnea. Da, am observat același lucru, că atunci când apeși pe Filtru trimite valoarea corectă, dar apoi redirecționează înapoi către pagină, de data aceasta eliminând valoarea. Cred că este un fel de "caracteristică" de securitate pentru a preveni trimiterea de valori aleatorii, potențial malicioase, dar nu știu cum să o ocolesc. Of.

morphatic morphatic
4 ian. 2016 07:22:20

OH! Am înțeles de ce variabila apare de două ori. Pentru că există o listă derulantă atât DEASUPRA cât și DEDESUBTUL tabelului de utilizatori și ambele au același atribut name. Dacă folosesc lista derulantă DE DEDESUBTUL tabelului pentru filtrare, funcționează cum trebuie. Deoarece acel câmp vine după cel de deasupra, valoarea sa nulă suprascrie pe cea anterioară. Hmmm....

morphatic morphatic
4 ian. 2016 07:38:04

Bună observație! Încercam să înțeleg de unde vine duplicatul. Cred că puțin JavaScript ar putea rezolva asta. Să seteze aceeași valoare în cealaltă listă derulantă înainte de trimiterea formularului.

Linnea Huxford Linnea Huxford
4 ian. 2016 07:54:56
2

În nucleu, numele de intrare din jos sunt marcate cu numărul instanței, de exemplu new_role (sus) și new_role2 (jos). Iată două abordări pentru o convenție de denumire similară, și anume course_section1 (sus) și course_section2 (jos):

Abordarea #1

Deoarece variabila $which (sus, jos) nu este transmisă către hook-ul restrict_manage_users, am putea ocoli acest lucru prin crearea propriei noastre versiuni a acelui hook:

Să creăm hook-ul de acțiune wpse_restrict_manage_users care are acces la o variabilă $which:

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

} );

Apoi putem să-l conectăm cu:

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

    // codul tău aici
} );

unde acum avem $name ca course_section1 în sus și course_section2 în jos.

Abordarea #2

Să conectăm în restrict_manage_users pentru a afișa meniuri derulante, cu un nume diferit pentru fiecare instanță:

function add_course_section_filter() 
{
    static $instance= 0;    

    // Opțiuni meniu derulant         
    $options = '';
    foreach( range( 1, 3 ) as $rng )
    {
        $options = sprintf( 
            '<option value="%1$d" %2$s>Secțiunea %1$d</option>',
            $rng,
            selected( $rng, get_selected_course_section(), 0 )
        );
    }

    // Afișează meniul derulant cu un nume diferit pentru fiecare instanță
    printf( 
        '<select name="%s" style="float:none;"><option value="0">%s</option>%s</select>', 
        'course_section' . ++$instance,
        __( 'Secțiunea Cursului...' ),
        $options 
    );


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

unde am folosit funcția din nucleu selected() și funcția ajutătoare:

/**
 * Obține secțiunea cursului selectată 
 * @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; // implicit

    return (int) $course_section;
}

Apoi am putea folosi acest lucru și când verificăm secțiunea cursului selectată în callback-ul acțiunii pre_get_users.

4 ian. 2016 12:41:29
Comentarii

Este o abordare fascinantă. Nu am folosit niciodată cuvântul cheie static în acest fel (doar în cadrul claselor). Devine $instance o variabilă globală atunci când faci asta? Trebuie să-ți faci griji privind coliziunile de nume de variabile? De asemenea, îmi place tehnica de a crea o acțiune nouă care se atașează la una existentă. Mulțumesc!

morphatic morphatic
4 ian. 2016 22:04:31

Această abordare poate fi utilă uneori și este folosită în nucleu pentru a număra, de exemplu, instanțe de shortcode (galerie, playlist, audio). Scope-ul variabilei statice aici nu va interfera cu scope-ul variabilelor globale. Valoarea variabilei statice va fi păstrată între apelurile acelei funcții, ceea ce nu este cazul cu variabilele locale. Am căutat și am găsit acest tutorial util care oferă mai multe detalii. @morphatic

birgire birgire
4 ian. 2016 23:46:22
0

Acesta este o altă soluție Javascript care poate fi utilă pentru unii oameni. În cazul meu, am eliminat complet a doua listă de selecție (cea de jos). Am observat că nu folosesc niciodată câmpurile de jos...

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
} );
25 feb. 2016 23:19:33
0

Soluție fără JavaScript

Dați elementului select un nume în stil "array", astfel:

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

Apoi AMBII parametri sunt transmiși (de la partea de sus și de jos a tabelului), și acum într-un format array cunoscut.

Apoi, valoarea poate fi folosită astfel în funcția pre_get_users:

function filter_users_by_course_section( $query ) {
    global $pagenow;

    // dacă nu suntem pe pagina de utilizatori în admin, ieșim
    if ( ! is_admin() || 'users.php' != $pagenow ) {
        return;
    } 

    // dacă nu a fost selectată nicio secțiune, ieșim
    if ( empty( $_GET['course_section'] ) ) {
        return;
    }

    // course_section este acum cunoscut a fi setat, așa că îl încărcăm
    $section = $_GET['course_section'];

    // valoarea este un array, și unul dintre cele două select box-uri probabil
    // nu a fost setat la nimic, așa că folosim array_filter pentru a elimina elementele goale
    $section = array_filter( $section );

    // valoarea este încă un array, așa că luăm prima valoare
    $section = reset( $section );

    // acum valoarea este o singură valoare, cum ar fi 1
    $meta_query = array(
        array(
            'key' => 'course_section',
            'value' => $section
        )
    );

    $query->set( 'meta_key', 'course_section' );
    $query->set( 'meta_query', $meta_query );
}
5 iul. 2017 23:37:50
0

o altă soluție

poți pune caseta de selecție a filtrului într-un fișier separat precum user_list_filter.php

și folosește require_once 'user_list_filter.php' în funcția ta de callback pentru acțiune

fișierul user_list_filter.php:

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

și în callback-ul tău pentru acțiune:

function add_course_section_filter() {
    require_once 'user_list_filter.php';
}
13 feb. 2018 09:13:54
0

Am luat soluția lui morphatic și am făcut-o puțin mai dinamică:

// Adaugă o opțiune de filtrare personalizată în tabelul utilizatorilor din panou
function add_custom_users_table_filter($id, $placeholder_text, $filter_options, $query_modifier, $button_text = 'Filtrează') {
  $html_function_name = "users_table_{$id}_filter";
  $query_function_name = $html_function_name . '_query';

  $GLOBALS[$html_function_name] = function($which) use($id, $button_text, $placeholder_text, $filter_options) {
    $select_template = '<label class="screen-reader-text" for="%s_%s">%s</label>';
    $select_template .= '<select name="%s_%s" id="%s_%s">';
    $select_template .= '<option value="">%s</option>';
    $select_template .= '%s</select>';
    $option_template = '<option value="%s" %s>%s</option>';

    $button = key(array_filter($_GET, function($v) use($button_text) {
      return $button_text === $v;
    }));

    $selection = $_GET[$id . '_' . $button] ?? -1;

    $options = implode('', array_map(function($value, $key) use($option_template, $selection) {
      return sprintf($option_template, $key, selected($key, $selection, false), $value);
    }, array_values($filter_options), array_keys($filter_options)));

    $select = sprintf($select_template, $id, $which, $placeholder_text, $id, $which, $id, $which, $placeholder_text, $options);

    echo '</div><div class="alignleft actions">';
    echo $select;

    submit_button($button_text, null, $which, false);
  };

  $GLOBALS[$query_function_name] = function($query) use($id, $button_text, $query_modifier) {
    global $pagenow;

    if (is_admin() && $pagenow == 'users.php') {
      $button = key(array_filter($_GET, function($v) use($button_text) {
        return $button_text === $v;
      }));

      if (isset($_GET[$id . '_' . $button]) && ($selection = $_GET[$id . '_' . $button])) {
        return $query_modifier($query, $selection);
      }
    }

    return $query;
  };

  add_action('restrict_manage_users', $GLOBALS[$html_function_name]);
  add_filter('pre_get_users', $GLOBALS[$query_function_name]);
}

și apoi o poți folosi astfel:

add_action('load-users.php', 'init_custom_users_table_functions');
function init_custom_users_table_functions() {
  // Adaugă filtrul "Secțiune Curs" în tabelul utilizatorilor
  add_custom_users_table_filter(
    'course_section',
    'Secțiune Curs...',
    array(
      '1' => 'Secțiunea 1',
      '2' => 'Secțiunea 2',
      '3' => 'Secțiunea 3'
    ),
    function($query, $selection) {
      $meta_query = array(
        array(
          'key'   => 'course_section',
          'value' => $selection
        )
      );
  
      if (isset($query->query_vars['meta_query']) && ($og_meta_query = $query->query_vars['meta_query'])) {
        $meta_query = array('relation' => 'AND', $og_meta_query, $meta_query);
      }
  
      $query->set('meta_query', $meta_query);

      return $query;
    }
  );
}

Cel mai bine este că funcția poate fi apelată de mai multe ori pentru a adăuga mai multe filtre.

7 aug. 2023 17:53:30