Come filtrare gli utenti sulla pagina degli utenti amministratori in base al meta campo personalizzato?


9

Il problema

WP sembra rimuovere il valore della mia variabile di query prima che venga utilizzato per filtrare l'elenco di utenti.

Il mio codice

Questa funzione aggiunge una colonna personalizzata alla mia tabella Users su /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' );

Questa funzione dice a WP come riempire i valori nella colonna:

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

Ciò aggiunge un menu a discesa e un Filterpulsante sopra la tabella 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' );

Questa funzione modifica la query dell'utente per aggiungere my 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' );

Altre informazioni

Crea il mio menu a discesa correttamente. Quando seleziono una sezione del corso e faccio clic Filtersulla pagina viene aggiornata e course_sectionvisualizzata nell'URL, ma non ha alcun valore associato ad essa. Se controllo le richieste HTTP, mostra che viene inviato con il valore della variabile corretto, ma poi c'è un 302 Redirectche sembra eliminare il valore che ho selezionato.

Se invio la course_sectionvariabile digitandola direttamente nell'URL, il filtro funziona come previsto.

Il mio codice si basa approssimativamente su questo codice di Dave Court .

Ho anche provato a inserire nella whitelist la mia query var usando questo codice, ma senza fortuna:

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

Sto usando WP 4.4. Qualche idea sul perché il mio filtro non funziona?


Cordiali saluti, ho aggiunto un biglietto sul sito WP Trac che impedirebbe agli sviluppatori di saltare attraverso uno dei cerchi descritti di seguito.
morfico

Risposte:


6

AGGIORNAMENTO 2018-06-28

Mentre il codice seguente funziona principalmente bene, ecco una riscrittura del codice per WP> = 4.6.0 (usando 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');

Ho incorporato diverse idee di @birgire e @cale_b che offre anche soluzioni di seguito che vale la pena leggere. In particolare, io:

  1. Utilizzata la $whichvariabile che è stata aggiuntav4.6.0
  2. Best practice utilizzata per i18n utilizzando stringhe traducibili, ad es __( 'Filter' )
  3. Loop scambiato con il (più di moda?) array_map(), array_filter()Erange()
  4. Utilizzato sprintf()per generare i modelli di markup
  5. Utilizzata la notazione di matrice parentesi quadra anziché array()

Infine, ho scoperto un bug nelle mie precedenti soluzioni. Queste soluzioni favoriscono sempre il TOP <select>rispetto al FONDO <select>. Pertanto, se hai selezionato un'opzione di filtro dal menu a discesa in alto, e successivamente ne hai selezionato uno dal menu a discesa in basso, il filtro utilizzerà comunque solo il valore in alto (se non è vuoto). Questa nuova versione corregge quel bug.

AGGIORNAMENTO 2018-02-14

Questo problema è stato corretto da WP 4.6.0 e le modifiche sono documentate nei documenti ufficiali . La soluzione di seguito funziona comunque, comunque.

Cosa ha causato il problema (WP <4.6.0)

Il problema era che l' restrict_manage_usersazione veniva chiamata due volte: una volta SOPRA la tabella Users e una volta SOTTO. Ciò significa che DUE selectmenu a discesa vengono creati con lo stesso nome . Quando si Filterfa clic sul pulsante, qualunque valore si trovi nel secondo selectelemento (ovvero quello SOTTO la tabella) sovrascrive il valore nel primo, ovvero quello SOPRA la tabella.

Nel caso in cui si desideri immergersi nella fonte WP, l' restrict_manage_usersazione viene attivata dall'interno WP_Users_List_Table::extra_tablenav($which), che è la funzione che crea il menu a discesa nativo per modificare il ruolo di un utente. Tale funzione ha l'aiuto della $whichvariabile che indica se sta creando selectil modulo sopra o sotto il modulo e gli consente di assegnare ai due menu a discesa nameattributi diversi . Sfortunatamente, la $whichvariabile non viene passata restrict_manage_usersall'azione, quindi dobbiamo trovare un altro modo per differenziare i nostri elementi personalizzati.

Un modo per farlo, come suggerisce @Linnea , sarebbe quello di aggiungere un po 'di JavaScript per catturare il Filterclic e sincronizzare i valori dei due menu a discesa. Ho scelto una soluzione solo PHP che descriverò ora.

Come sistemarlo

È possibile sfruttare la possibilità di trasformare input HTML in array di valori e quindi filtrare l'array per eliminare eventuali valori non definiti. Ecco il codice:

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

Bonus: PHP 7 Refactor

Dato che sono entusiasta di PHP 7, nel caso in cui tu stia eseguendo WP su un server PHP 7, ecco una versione più breve e sexy usando l' operatore 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' );

Godere!


Quindi la tua soluzione funziona ancora dopo la 4.6.0? C'è un modo più semplice per farlo con la versione più recente di wordpress? Non riesco a trovare nessuna guida fatta come quest'anno
Jeremy Muckel il

1
@JeremyMuckel la risposta breve alla tua domanda è "sì". La mia vecchia soluzione funziona ancora. Lo sto usando in produzione regolarmente da mesi ormai e la maggior parte dei miei siti sono aggiornati all'ultima versione WP stabile (attualmente 4.9.6). Detto questo, ho fornito una soluzione aggiornata che utilizza la nuova patch e che risolve anche un bug sottile nella mia soluzione precedente.
morfico,

Questo è stato utile ma manca il codice del modulo in "Come risolverlo" e "Bonus: PHP 7 Refactor" e </select>ho anche scoperto che per farlo funzionare ho dovuto mettere <form method="get">prima il menu di selezione e </form>dopo il pulsante filtro.
cogdog

@cogdog buona cattura dei </select>tag mancanti ! Li ho aggiunti. Strano che fosse necessario racchiuderlo in <form>poiché l'intera pagina è racchiusa in un'unica grande forma e questo codice viene iniettato nel mezzo di esso. Sono contento che tu abbia funzionato, però. :)
morfico

4

Nel core, i nomi degli input in basso sono contrassegnati con il numero di istanza, ad esempio new_role(in alto) e new_role2(in basso). Ecco due approcci per una convenzione di denominazione simile, vale a dire course_section1(in alto) e course_section2(in basso):

Approccio n. 1

Poiché la $whichvariabile (in alto , in basso ) non viene passata restrict_manage_usersall'hook, potremmo aggirare ciò creando la nostra versione di quell'hook:

Creiamo l'hook dell'azione wpse_restrict_manage_usersche ha accesso a una $whichvariabile:

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

} );

Quindi possiamo collegarlo con:

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

    // your stuff here
} );

dove ora abbiamo $namecome course_section1in alto e course_section2in basso .

Approccio n. 2

Agganciamo restrict_manage_users, per visualizzare i menu a discesa, con un nome diverso per ogni istanza:

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

dove abbiamo usato la funzione core selected()e la funzione helper:

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

Quindi potremmo anche usarlo quando controlliamo la sezione del corso selezionata nel pre_get_userscallback dell'azione.


Questo è un approccio affascinante. Non ho mai usato la staticparola chiave in questo modo (solo all'interno delle classi). Fa $instancediventare una variabile globale quando si esegue questa operazione? Devi preoccuparti delle collisioni di nomi variabili? Mi piace anche la tecnica di creare una nuova azione che trasporta sulle spalle di una esistente. Grazie!
morfico

Questo approccio può essere utile a volte e viene utilizzato nel core per ad es. Contare istanze di shortcode (galleria, playlist, audio). L'ambito della variabile statica qui non interferirà con l'ambito della variabile globale. Il valore della variabile statica verrà conservato tra quelle chiamate di funzione, il che non è il caso delle variabili locali. Ho cercato e trovato questo simpatico tutorial che ha maggiori dettagli. @morphatic
birgire

4

Ho testato il tuo codice in Wordpress 4.4 e Wordpress 4.3.1. Con la versione 4.4, riscontro esattamente lo stesso problema. Tuttavia, il codice funziona correttamente nella versione 4.3.1!

Penso che questo sia un bug di Wordpress. Non so se sia stato ancora segnalato. Penso che la ragione dietro il bug potrebbe essere che il pulsante di invio sta inviando la query vars due volte. Se guardi le query vars, vedrai che course_section è elencato due volte, una volta con il valore corretto e una volta vuota.

Modifica: questa è la soluzione JavaScript

Aggiungilo semplicemente al file Functions.php del tuo tema e cambia NAME_OF_YOUR_INPUT_FIELD con il nome del tuo campo di input! Poiché WordPress carica automaticamente jQuery sul lato amministratore, non è necessario accodare alcun script. Questo frammento di codice aggiunge semplicemente un listener di modifica agli input del menu a discesa e quindi aggiorna automaticamente l'altro menu a discesa in modo che corrisponda allo stesso valore. Altre spiegazioni qui.

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

Spero che sia di aiuto!


Grazie Linnea. Sì, ho trovato la stessa cosa, che quando fai clic su di Filteresso invia il valore corretto, ma poi reindirizza nuovamente alla pagina, questa volta rimuovendo il valore. La mia ipotesi è che sia una sorta di "caratteristica" di sicurezza per impedire l'invio di valori casuali, potenzialmente dannosi, ma non so come aggirarlo. Sospiro.
morfico

OH! Ho capito perché il var si presenta due volte. Perché è presente un menu a discesa SOPRA e SOTTO la tabella degli utenti ed entrambi hanno lo stesso nameattributo. Se utilizzo il menu a discesa SOTTO la tabella per eseguire il filtro, funziona come previsto. Dal momento che quel campo viene dopo quello sopra di esso, il suo valore null ha la precedenza su quello precedente. Hmmm ....
morfico

Buona scoperta! Stavo cercando di capire da dove provenisse il duplicato. Penso che forse un po 'di JavaScript potrebbe risolvere questo problema. È necessario impostare l'altro menu a discesa con lo stesso valore prima di inviare il modulo.
Linnea Huxford il

1

Questa è una diversa soluzione Javascript che può essere utile per alcune persone. Nel mio caso ho semplicemente rimosso completamente il secondo elenco di selezione (in basso). Trovo che non uso mai gli input inferiori comunque ...

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

Soluzione non JavaScript

Assegna alla selezione un nome "stile array", in questo modo:

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

Quindi ENTRAMBI i parametri vengono passati (dall'alto e dal basso della tabella) e ora in un formato array noto.

Quindi, il valore può essere utilizzato in questo modo nella pre_get_usersfunzione:

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

un'altra soluzione

puoi mettere la casella di selezione del filtro in un file separato come user_list_filter.php

e usare require_once 'user_list_filter.php' nella funzione di callback dell'azione

user_list_filter.php file:

<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 nel tuo callback di azione:

function add_course_section_filter() {
    require_once 'user_list_filter.php';
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.