Comment filtrer les utilisateurs sur la page des utilisateurs admin par champ méta personnalisé?


9

Le problème

WP semble supprimer la valeur de ma variable de requête avant de l'utiliser pour filtrer la liste des utilisateurs.

Mon code

Cette fonction ajoute une colonne personnalisée à ma table Utilisateurs sur /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' );

Cette fonction indique à WP comment remplir les valeurs dans la colonne:

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

Cela ajoute une liste déroulante et un Filterbouton au-dessus du tableau des utilisateurs:

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

Cette fonction modifie la requête de l'utilisateur pour ajouter mon 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' );

les autres informations

Cela crée ma liste déroulante correctement. Lorsque je sélectionne une section de cours et clique sur Filterla page, elle s'actualise et course_sectionapparaît dans l'URL, mais elle n'a aucune valeur associée. Si je vérifie les requêtes HTTP, cela montre qu'elles sont soumises avec la valeur de variable correcte, mais il y en a une 302 Redirectqui semble supprimer la valeur que j'ai sélectionnée.

Si je soumets la course_sectionvariable en la tapant directement dans l'URL, le filtre fonctionne comme prévu.

Mon code est à peu près basé sur ce code de Dave Court .

J'ai également essayé de mettre en liste blanche ma requête var en utilisant ce code, mais sans succès:

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

J'utilise WP 4.4. Des idées pourquoi mon filtre ne fonctionne pas?


Pour info, j'ai ajouté un ticket sur le site WP Trac qui empêcherait les développeurs d'avoir à sauter à travers l'un des cerceaux décrits ci-dessous.
morphatique

Réponses:


6

MISE À JOUR 2018-06-28

Alors que le code ci-dessous fonctionne généralement bien, voici une réécriture du code pour WP> = 4.6.0 (en utilisant 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');

J'ai incorporé plusieurs idées de @birgire et @cale_b qui propose également des solutions ci-dessous qui valent la peine d'être lues. Plus précisément, je:

  1. Utilisé la $whichvariable qui a été ajoutée dansv4.6.0
  2. Meilleure pratique utilisée pour i18n en utilisant des chaînes traduisibles, par exemple __( 'Filter' )
  3. Boucles échangées pour la (plus à la mode?) array_map(), array_filter()Etrange()
  4. Utilisé sprintf()pour générer les modèles de balisage
  5. A utilisé la notation du tableau entre crochets au lieu de array()

Enfin, j'ai découvert un bug dans mes solutions précédentes. Ces solutions privilégient toujours le TOP par <select>rapport au BAS <select>. Donc, si vous avez sélectionné une option de filtre dans la liste déroulante supérieure, puis que vous en avez sélectionné une dans la liste déroulante inférieure, le filtre n'utilisera toujours que la valeur en haut (si elle n'est pas vide). Cette nouvelle version corrige ce bogue.

MISE À JOUR 2018-02-14

Ce problème a été corrigé depuis WP 4.6.0 et les modifications sont documentées dans les documents officiels . Cependant, la solution ci-dessous fonctionne toujours.

Qu'est-ce qui a causé le problème (WP <4.6.0)

Le problème était que l' restrict_manage_usersaction est appelée deux fois: une fois au-dessus de la table des utilisateurs et une fois en dessous. Cela signifie que DEUX selectlistes déroulantes sont créées avec le même nom . Lorsque le Filterbouton est cliqué, la valeur du deuxième selectélément (c'est-à-dire celui EN-DESSOUS du tableau) remplace la valeur du premier, c'est-à-dire celle AU-DESSUS du tableau.

Si vous souhaitez plonger dans la source WP, l' restrict_manage_usersaction est déclenchée de l'intérieur WP_Users_List_Table::extra_tablenav($which), qui est la fonction qui crée la liste déroulante native pour modifier le rôle d'un utilisateur. Cette fonction a l'aide de la $whichvariable qui lui indique si elle crée le selectdessus ou le dessous du formulaire, et lui permet de donner aux deux listes déroulantes des nameattributs différents . Malheureusement, la $whichvariable n'est pas transmise à l' restrict_manage_usersaction, nous devons donc trouver une autre façon de différencier nos propres éléments personnalisés.

Une façon de le faire, comme le suggère @Linnea , serait d'ajouter du JavaScript pour capturer le Filterclic et synchroniser les valeurs des deux listes déroulantes. J'ai choisi une solution PHP uniquement que je vais décrire maintenant.

Comment le réparer

Vous pouvez profiter de la possibilité de transformer les entrées HTML en tableaux de valeurs, puis de filtrer le tableau pour éliminer toutes les valeurs non définies. Voici le code:

    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

Étant donné que je suis enthousiasmé par PHP 7, au cas où vous exécuteriez WP sur un serveur PHP 7, voici une version plus courte et plus sexy utilisant l' opérateur de coalescence nulle?? :

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

Prendre plaisir!


Votre solution fonctionne-t-elle toujours après 4.6.0? Existe-t-il un moyen plus simple de le faire avec la dernière version de wordpress? Je n'arrive pas à trouver de guides comme cette année
Jeremy Muckel

1
@JeremyMuckel la réponse courte à votre question est "oui". Mon ancienne solution fonctionne toujours. Je l'utilise régulièrement en production depuis des mois maintenant et la plupart de mes sites sont mis à jour vers la dernière version stable de WP (actuellement 4.9.6). Cela étant dit, j'ai fourni une solution mise à jour qui utilise le nouveau correctif et qui corrige également un bug subtil dans ma solution précédente.
morphatique

Cela a été utile mais votre code de formulaire sous "Comment le réparer" et "Bonus: PHP 7 Refactor" manque un </select>J'ai aussi trouvé pour le faire fonctionner que je devais mettre <form method="get">avant le menu de sélection et </form>après le bouton de filtre.
cogdog

@cogdog bonne capture sur les </select>balises manquantes ! Je les ai ajoutés. Étrange que vous deviez l'envelopper dans un <form>car cette page entière est enveloppée dans un grand formulaire, et ce code est injecté au milieu. Heureux que vous l'ayez fait fonctionner, cependant. :)
morphatique

4

Dans le noyau, les noms d'entrée inférieurs sont marqués du numéro d'instance, par exemple new_role(haut) et new_role2(bas). Voici deux approches pour une convention de dénomination similaire, à savoir course_section1(en haut) et course_section2(en bas):

Approche n ° 1

Puisque la $whichvariable ( haut , bas ) n'est pas transmise au restrict_manage_usershook, nous pouvons contourner cela en créant notre propre version de ce hook:

Créons le hook d'action wpse_restrict_manage_usersqui a accès à une $whichvariable:

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

} );

Ensuite, nous pouvons l'accrocher avec:

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

    // your stuff here
} );

où nous avons maintenant $namecomme course_section1en haut et course_section2en bas .

Approche n ° 2

Accrochons-nous restrict_manage_users, pour afficher les listes déroulantes, avec un nom différent pour chaque instance:

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

où nous avons utilisé la fonction principale selected()et la fonction d'aide:

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

Ensuite, nous pourrions également l'utiliser lorsque nous vérifierons la section de cours sélectionnée dans le pre_get_usersrappel d'action.


Il s'agit d'une approche fascinante. Je n'ai jamais utilisé le staticmot clé de cette façon (uniquement dans les classes). Devient $instanceune variable globale lorsque vous faites cela? Avez-vous à vous soucier des collisions de noms de variables? J'aime aussi la technique de création d'une nouvelle action qui s'appuie sur une action existante. Merci!
morphatique

Cette approche peut parfois être pratique et est utilisée dans le noyau pour par exemple compter les instances de shortcode (galerie, playlist, audio). La portée variable statique ici ne gâchera pas la portée variable globale. La valeur de la variable statique sera conservée entre ces appels de fonction, ce qui n'est pas le cas avec les variables locales. J'ai cherché et trouvé ce joli tutoriel qui a plus de détails. @morphatic
birgire

4

J'ai testé votre code dans Wordpress 4.4 et dans Wordpress 4.3.1. Avec la version 4.4, je rencontre exactement le même problème que vous. Cependant, votre code fonctionne correctement dans la version 4.3.1!

Je pense que c'est un bug Wordpress. Je ne sais pas si cela a encore été signalé. Je pense que la raison du bug pourrait être que le bouton d'envoi envoie deux fois les variables de requête. Si vous regardez les variables de requête, vous verrez que course_section est répertorié deux fois, une fois avec la valeur correcte et une fois vide.

Edit: Ceci est la solution JavaScript

Ajoutez simplement ceci au fichier functions.php de votre thème et remplacez NAME_OF_YOUR_INPUT_FIELD par le nom de votre champ de saisie! Étant donné que WordPress charge automatiquement jQuery du côté administrateur, vous n'avez pas à mettre en file d'attente les scripts. Cet extrait de code ajoute simplement un écouteur de modification aux entrées de la liste déroulante, puis met automatiquement à jour l'autre liste déroulante pour correspondre à la même valeur. Plus d'explications ici.

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

J'espère que cela t'aides!


Merci Linnea. Oui, j'ai trouvé la même chose, que lorsque vous cliquez dessus, Filteril soumet la valeur correcte, mais redirige ensuite à nouveau vers la page, cette fois en supprimant la valeur. Je suppose qu'il s'agit d'une sorte de "fonctionnalité" de sécurité pour empêcher la soumission de valeurs aléatoires, potentiellement malveillantes, mais je ne sais pas comment contourner ce problème. Soupir.
morphatique

OH! J'ai compris pourquoi le var apparaît deux fois. Parce qu'il existe une liste déroulante au-dessus et en dessous de la table des utilisateurs et les deux ont le même nameattribut. Si j'utilise la liste déroulante EN DESSOUS du tableau pour effectuer le filtrage, cela fonctionne comme prévu. Étant donné que ce champ vient après celui au-dessus, sa valeur nulle remplace le précédent. Hmmm ....
morphatique

Bonne trouvaille! J'essayais de comprendre d'où venait le doublon. Je pense que peut-être un peu de JavaScript pourrait résoudre ce problème. Demandez-lui de définir l'autre liste déroulante sur la même valeur avant de soumettre le formulaire.
Linnea Huxford

1

Il s'agit d'une solution Javascript différente qui peut être utile pour certaines personnes. Dans mon cas, j'ai simplement supprimé complètement la 2ème liste de sélection (en bas). Je trouve que je n'utilise jamais les entrées du bas de toute façon ...

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

Solution non JavaScript

Donnez à la sélection un nom de "style tableau", comme ceci:

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

Ensuite, les DEUX paramètres sont passés (du haut et du bas de la table), et maintenant dans un format de tableau connu.

Ensuite, la valeur peut être utilisée comme ceci dans la pre_get_usersfonction:

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

une autre solution

vous pouvez mettre votre boîte de sélection de filtre dans un fichier séparé comme user_list_filter.php

et utiliser require_once 'user_list_filter.php'dans votre fonction de rappel d'action

user_list_filter.php fichier:

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

et dans votre rappel d'action:

function add_course_section_filter() {
    require_once 'user_list_filter.php';
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.