Custom Filters in Post, Comment, User, and Taxonomy Tables
This article discusses how to add additional filters for post lists (the post table in the admin panel), for the comments list (the comments table), or for the users list (the users table). The article includes theory and examples.
Recall the posts table in the admin panel - above the table, there are two dropdown lists: by date and by categories. We are talking about such filters. Below is shown how to add your filters (dropdown lists) and then process the request, trimming the list of posts to the filter values.
The capabilities of the filters described below can be extended with the help of additional sortable columns in the table. Filters can "trim" the list of posts, and then this list can be sorted by columns.
All Hooks for Inserting HTML Filters
Filtering posts in the admin panel. Filters in the post list table can be added for any post type, comments, or users. All this is done through the necessary hook. The logic is the same everywhere: we add the HTML code of the dropdown list - the form field <select>, and then we process the request.
Now, let's look at all possible hooks.
Posts (including attachments)
// Adds html only above the post table in the standard filter block. do_action( 'restrict_manage_posts', $post_type, $which );
// Adds html above or below the post table, after the submit button of the standard filters. do_action( 'manage_posts_extra_tablenav', $which );
Comments
// Adds html to the top of the comments table in the standard filter block. do_action( 'restrict_manage_comments' );
// Adds HTML above and below the comments table, after the submit button of the filters. do_action( 'manage_comments_nav', $comment_status );
Users
// Adds html above and below the users table, in the standard filter block. do_action( 'restrict_manage_users', $which );
Taxonomies
There are no such filters for taxonomies. See the example below for how to add filters there.
Parameter $which
In all filters, there can be top or bottom, which respectively means to output the specified code at the top or bottom of the table.
Examples
Filter in the Post Table
Suppose there is a post type event and a taxonomy season. We need to add a filter so that only posts from the specified taxonomy element season can be left in the post table.
For this, we use two filters: restrict_manage_posts and pre_get_posts:
// filter - add dropdown list
add_action( 'restrict_manage_posts', 'add_event_table_filters');
// Filtering: request processing
add_action( 'pre_get_posts', 'add_event_table_filters_handler' );
function add_event_table_filters( $post_type ){
echo '
<select name="sel_season">
<option value="-1">- all seasons -</option>
<option value="203" '. selected(203, @ $_GET['sel_season'], 0) .'>2016-2017</option>
<option value="83"'. selected(83, @ $_GET['sel_season'], 0) .'>2015-2016</option>
<option value="154"'. selected(154, @ $_GET['sel_season'], 0) .'>2014-2015</option>
</select>';
// for dynamic select construction, you can use wp_dropdown_categories()
}
function add_event_table_filters_handler( $query ){
$cs = function_exists('get_current_screen') ? get_current_screen() : null;
// make sure we are on the right admin page
if( ! is_admin() || empty($cs->post_type) || $cs->post_type != 'event' || $cs->id != 'edit-event' )
return;
// season
if( @ $_GET['sel_season'] != -1 ){
$selected_id = @ $_GET['sel_season'] ?: 20;
$query->set( 'tax_query', array([ 'taxonomy'=>'season', 'terms'=>$selected_id ]) );
}
//if( empty($_GET['orderby']) && @ $_GET['sel_season'] != -1 ){
// $query->set( 'orderby', 'menu_order date' );
//}
}
We will get a working filter:
Filter in the Comments Table
Conditions: for comments, a status is set, which is recorded in the comment's meta-field. We need to make it possible to filter the list by status in the comments table. We have 3 statuses: question (Question), thanks (Thank you), useful (Useful).
Task: display these statuses in a "select" in the comments table filters and process the request - if a status is selected, change the request, leaving only the comments with the specified status in the table.
For this, we use two filters: restrict_manage_comments and parse_comment_query:
<?php
// Add HTML filter
add_action( 'restrict_manage_comments', 'add_comment_filter_select' );
// change request
add_action( 'parse_comment_query', 'change_comment_request2' );
function add_comment_filter_select(){
$cond = @ $_GET['condition'];
?>
<select name="condition" class="comm_condition" onchange="window.add_param_to_URL(this);">
<option value="" <?php selected('', $cond) ?> >- All statuses -</option>
<option value="question" <?php selected('need_answer', $cond) ?> >Question</option>
<option value="thanks" <?php selected('thanks', $cond) ?> >Thank You</option>
<option value="useful" <?php selected('useful', $cond) ?> >Useful</option>
</select>
<script>
// adds query parameter to URL and redirects on select change
window.add_param_to_URL = function(el){
var href = window.location.href,
sep = /[?]/.test(href) ? "&" : "?",
name = el.name.replace(/[^a-z_-]/i,'');
window.location = (new RegExp(name+'=?')).test(href)
? href.replace( (new RegExp('([?&]'+name+'=?)[^&]*')), (el.value ? "$1"+ el.value : '') )
: (href + sep + name + "="+ el.value);
}
</script>
<?php
}
function change_comment_request2( $query ){
// not our request
if( empty( $_GET['condition'] ) || ! is_admin() )
return;
// make sure we are on the comments table page
$cs = get_current_screen();
if( $cs->base != 'edit-comments' || $cs->id != 'edit-comments' )
return;
// exit if this is a request to get the page number get_page_of_comment()
$qv = $query->query_vars;
if( $qv['fields'] == 'ids' && $qv['count'] )
return;
// change the request
$query->query_vars['meta_query'] = array(
array(
'key' => 'condition',
'value' => sanitize_key($_GET['condition'])
),
);
}
As a result, we will get such a working filter:
Javascript and the onchange attribute are added for convenience. So that when selecting a value, the filter is applied immediately, rather than after clicking the filter button.
Another Hook for Changing Comment Requests
There is also an option using the comments_clauses hook to change the request.
It allows you to modify the SQL query itself. This option is more complex but offers more possibilities; sometimes it can be useful:
// Change the request
add_filter( 'comments_clauses', 'change_comment_request', 10, 2 );
function change_comment_request( $clauses, $query ){
// not our request
if( empty($_GET['condition']) || ! is_admin() )
return $clauses;
// make sure we are on the comments table page
$cs = get_current_screen();
if( $cs->base != 'edit-comments' || $cs->id != 'edit-comments' )
return $clauses;
// exit if this is a request to get the page number get_page_of_comment()
$qv = $query->query_vars;
if( $qv['fields'] == 'ids' && $qv['count'] )
return $clauses;
// change the request
global $wpdb;
$clauses['join'] = " LEFT JOIN $wpdb->commentmeta cm ON (cm.comment_id = $wpdb->comments.comment_ID)";
$clauses['where'] .= $wpdb->prepare(
" AND cm.meta_key='condition' AND cm.meta_value = %s",
$_GET['condition']
);
return $clauses;
}
Filter in the Users Table
Suppose every user on the site must be activated, and during registration, an activation key is recorded in the meta-field activation_key. At the time of activation, this field is removed.
Task: display a filter in the users table to show only activated users or, conversely, only non-activated ones. That is, we need to display a dropdown list with the choice of user status (activated, not activated). And we need to process the request, leaving the users table according to the request.
<?php
// Adds html above and below the users table, in the standard filter block.
add_action( 'restrict_manage_users', 'add_users_list_filters' );
// change request - option 1. Since version 4.0
add_action( 'pre_get_users', 'users_filter_handler' );
function add_users_list_filters( $which ){
// only at the top
if( $which != 'top' )
return;
$activated = @ $_GET['u_activated'];
?>
<select name="u_activated" onchange="window.add_param_to_URL(this)">
<option value="">Change status...</option>
<option value="yes" <?php selected('yes', $activated) ?> >Activated</option>
<option value="no" <?php selected('no', $activated) ?> >Not Activated</option>
</select>
<script>
// adds query parameter to URL and redirects on select change
window.add_param_to_URL = function(el){
var href = window.location.href, sep = /[?]/.test(href) ? "&" : "?", name = el.name.replace(/[^a-z_-]/i,'');
window.location = (new RegExp(name+'=?')).test(href) ? href.replace( (new RegExp('([?&]'+name+'=?)[^&]*')), (el.value ? "$1"+ el.value : '') ) : (href + sep + name + "="+ el.value);
}
</script>
<?php
}
function users_filter_handler( $uquery ){
if( ! is_admin() || get_current_screen()->id !== 'users' )
return;
global $wpdb;
if( ! empty($_GET['u_activated']) ){
$compare = $_GET['u_activated'] == 'yes' ? 'NOT EXISTS' : 'EXISTS';
$uquery->set('meta_query', array( array('key'=>'activation_key', 'compare'=>$compare) ) );
if( empty($_GET['orderby']) ){
$uquery->set('order', 'DESC');
$uquery->set('orderby', 'user_registered');
}
}
}
Another Hook for Changing User Requests
You can also use the pre_user_query hook:
// change request - option 2
add_action( 'pre_user_query', 'users_filter_handler2' );
function users_filter_handler2( $uquery ){
if( ! is_admin() || get_current_screen()->id !== 'users' ) return;
global $wpdb;
$vars = $uquery->query_vars;
if( ! empty($_GET['u_activated']) && $activated = $_GET['u_activated'] ){
$sql_sub = "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = 'activation_key'";
if( $activated == 'yes' )
$uquery->query_where .= " AND ID NOT IN ($sql_sub) ";
else
$uquery->query_where .= " AND ID IN ($sql_sub) ";
$uquery->query_orderby = ' ORDER BY user_registered '. $vars['order'];
}
}
We will get such a working filter:
As in the previous example, I added JavaScript so that when the value of the select changes, it is applied immediately without unnecessary actions - clicking the "filter" button.
Filter in the Taxonomy Terms Table
As of WP version 4.6, no such filter is provided for the taxonomy terms table. Moreover, I tried to find workarounds, commonly referred to as hacks, but there is also little room for maneuver...
Below is perhaps the only way to add filters to the taxonomy terms table:
This is a demonstration example where we will add the ability to trim the list of terms in the table to only parents...
<?php
$taxonomy = 'category'; // the taxonomy for which everything is done
// invoke ob_start()
add_action( "{$taxonomy}_add_form", function($taxonomy){
ob_start();
} );
// call ob_get_clean(), where we get the ready HTML of the entire table
// through preg_replace we insert the HTML we need in the right place
// Output the result on the screen
add_action( "after-{$taxonomy}-table", function($taxonomy){
$html = ob_get_clean();
$__preg_replace_callback = function( $match ){
$val = @ $_GET['parent_only'];
ob_start();
?>
<div class="alignleft actions">
<select name="parent_only" onchange="window.add_param_to_URL(this)">
<option value="">All levels...</option>
<option value="yes" <?php selected('yes', $val) ?> >Only Parents</option>
</select>
</div>
<script>
// adds query parameter to URL and redirects on select change
window.add_param_to_URL = function(el){
var href = window.location.href,
sep = /[?]/.test(href) ? "&" : "?",
name = el.name.replace(/[^a-z_-]/i,'');
window.location = (new RegExp(name+'=?')).test(href)
? href.replace( (new RegExp('([?&]'+name+'=?)[^&]*')), (el.value ? "$1"+ el.value : '') )
: (href + sep + name + "="+ el.value);
}
</script>
<?php
return $match[1] . ob_get_clean();
};
echo preg_replace_callback('~(id="doaction[^<]+</div>)~', $__preg_replace_callback, $html );
} );
To dig in this direction, you need to look at the following files:
Hook for Changing the Request
// Change the request - option 1
// $this->query_vars = apply_filters( 'get_terms_args', $query, $taxonomies );
add_filter( 'get_terms_args', 'my_terms_filter_handler' );
function my_terms_filter_handler( $query ){
// check that we are where we need to be, or else the sky will fall...
if( empty($_GET['parent_only']) || ! is_admin() )
return $query;
// make sure that the get_terms function is called from the class with the table
// it is also called on this same page in the dropdown when creating a term...
if( ! ( $query['fields'] == 'count' /*counting*/ || isset($query['page']) /*terms table*/ ) )
return $query;
/*
// make sure that the get_terms function is called from the class with the table
$backtrace = debug_backtrace(false);
$backtrace = array_pop( $backtrace );
// for the terms table itself and for counting
if( in_array( @ $backtrace['class'], array('WP_List_Table','WP_Terms_List_Table') ) )
echo $backtrace['class'];
*/
$query['parent'] = 0; // only parents
return $query;
}
Another Hook for Modifying the SQL Query: terms_clauses
This example is not related to the previous code. This code stands alone, showing how to add query parameters by meta-fields to filter terms.
It is assumed that two filtering parameters subject or grade can be passed. Each of them corresponds to its own meta-field for terms. The parameters can be passed together or separately...
// modify the SQL query
add_filter( 'terms_clauses', 'subject_section_sortable_orderby', 10, 3 );
function subject_section_sortable_orderby( $pieces, $taxonomies, $args ){
// check that we are where we need to be, or else it's a disaster...
if( (! @ $_GET['subject'] && ! @ $_GET['grade']) || ! is_admin() )
return $pieces;
// make sure that the get_terms function is called from the class with the table
$backtrace = debug_backtrace(false);
$backtrace = array_pop( $backtrace );
if(
( @ $backtrace['class'] != 'WP_List_Table') && // for the terms table itself
( @ $backtrace['class'] != 'WP_Terms_List_Table' ) // for counting
)
return $pieces;
// extend the query
global $wpdb;
if( @ $_GET['subject'] ){
$pieces['join'] .= " LEFT JOIN $wpdb->termmeta AS tm ON t.term_id = tm.term_id ";
$pieces['where'] .= " AND tm.meta_key = 'ss_subject' AND tm.meta_value = ". intval($_GET['subject']) ." ";
}
if( @ $_GET['grade'] ){
$pieces['join'] .= " LEFT JOIN $wpdb->termmeta AS tm2 ON t.term_id = tm2.term_id ";
$pieces['where'] .= " AND tm2.meta_key = 'ss_grade' AND tm2.meta_value = ". intval($_GET['grade']) ." ";
}
return $pieces;
}