UPDATE 2018-06-28
While the code below mostly works fine, here is a rewrite of the code for WP >=4.6.0 (using 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');
I incorporated several ideas from @birgire and @cale_b who also offers solutions below that are worth reading. Specifically, I:
- Used the
$which
variable that was added in v4.6.0
- Used best practice for i18n by using translatable strings, e.g.
__( 'Filter' )
- Exchanged loops for the (more fashionable?)
array_map()
, array_filter()
, and range()
- Used
sprintf()
for generating the markup templates
- Used the square bracket array notation instead of
array()
Lastly, I discovered a bug in my earlier solutions. Those solutions always favor the TOP <select>
over the BOTTOM <select>
. So if you selected a filter option from the top dropdown, and then subsequently select one from the bottom dropdown, the filter will still only use whatever value was up top (if it's not blank). This new version corrects that bug.
UPDATE 2018-02-14
This issue has been patched since WP 4.6.0 and the changes are documented in the official docs. The solution below still works, though.
What Caused the Problem (WP <4.6.0)
The problem was that the restrict_manage_users
action gets called twice: once ABOVE the Users table, and once BELOW it. This means that TWO select
dropdowns get created with the same name. When the Filter
button is clicked, whatever value is in the second select
element (i.e. the one BELOW the table) overrides the value in the first one, i.e. the one ABOVE the table.
In case you want to dive into the WP source, the restrict_manage_users
action is triggered from within WP_Users_List_Table::extra_tablenav($which)
, which is the function that creates the native dropdown to change a user's role. That function has the help of the $which
variable that tells it whether it is creating the select
above or below the form, and allows it to give the two dropdowns different name
attributes. Unfortunately, the $which
variable doesn't get passed to the restrict_manage_users
action, so we have to come up with another way to differentiate our own custom elements.
One way to do this, as @Linnea suggests, would be to add some JavaScript to catch the Filter
click and sync up the values of the two dropdowns. I chose a PHP-only solution that I'll describe now.
How to Fix It
You can take advantage of the ability to turn HTML inputs into arrays of values, and then filter the array to get rid of any undefined values. Here's the 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
Since I'm excited about PHP 7, in case you're running WP on a PHP 7 server, here's a shorter, sexier version using the null coalescing operator ??
:
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' );
Enjoy!