Skip to content


Custom filtering in the WordPress Manage Posts screen

This entry is part 4 of 5 in the series Pimp my Manage panel

This is the most recent post my series on customizing the Manage Posts/Pages screen. The first couple posts dealt with adding custom columns to the Manage Posts and Manage Pages screens and this post will demonstrate how one can add custom filtering to the Manage Posts screen using the restrict_manage_posts action.

Introduction

As was the case with the manage_posts_columns and manage_posts_custom_column hooks, a search on Google or the Codex turns up little in the way of documentation. The only things that approach being useful are the source code available at redalt.com and the one sentence on the Codex:

“Runs before the list of posts to edit is put on the screen in the admin menus.”

This article will expand on the code that was developed in a previous one. That code adds a new attachments column to the Manage Posts screen. The code that will developed below will allow filtering on this column. WordPress versions 2.2 — 2.5 will be targeted as these are the only versions with the needed hook.

Overview

The Manage Posts screen we're starting with

The Manage Posts screen
we’re starting with

Your Manage Posts screen should look like the one to the right, minus the red and green boxes. This is our canvas. Inside the red box, you can see the column that we added previously, which we will be filtering on now. Inside the green box, we will be adding the UI elements we want: a dropdown box and a button just like the other filters.

There are two steps that need to be taken to add a custom filter to the Manage Posts screen. First, the UI elements need to be added. These will allow the user to select the criteria he/she wants to the filter on. The criteria selected are used in the second step to actually do the filtering by modifying the WordPress query.

Adding the interface

We will be adding two UI elements to the Manage Posts screen: a dropdown box and a button. The dropdown box will contain the criteria that we want to filter on (“Has attachment(s)”/”Has no attachments”). The button will trigger the filtering. It should be immediately obvious that this interface is patterned after the existing filters provided with WordPress.

The hook that we’ll be using to add these elements is the restrict_manage_posts action, which is executed at exactly that green-box location from above. The action handler needs to just echo a bunch of HTML corresponding to the dropdown and button. The code below does just that.

add_action('restrict_manage_posts', 'scompt_restrict_manage_posts');
function scompt_restrict_manage_posts() {
    ?>
        <form name="scompt_attachmentform" id="scompt_attachmentform" action="" method="get">
            <fieldset>
            <legend><?php _e('Attachments'); ?>&hellip;</legend>
            <select name='scompt_attachments' id='scompt_attachments' class='postform'>
                <option value=""><?php _e('All'); ?></option>
                <option value="has"><?php _e('Has attachments'); ?></option>
                <option value="hasnt"><?php _e('Has no attachments'); ?></option>
            </select>
            <input type="submit" name="submit" value="<?php _e('Filter') ?>" class="button" />
        </fieldset>
        </form>
    <?php
}

After adding the UI elements

After adding the UI elements

You’ll notice that there is a form tag in there. Unfortunately, WordPress calls the restrict_manage_posts action after the form tag ends. That means we’ve got to do it on our own. Also, it means the styling is a bit funky. If you toss this code in a plugin or in your theme’s functions.php file, you’ll see something similar to the image on the right.

Clicking on the button won’t do anything at this point. All we’re doing right now is sending some POST data to WordPress, but we’re not telling it to do anything with it. That’s what we’ll do in the next step.

Filtering the posts

To do the actual filtering, we need to modify the WordPress query that finds the posts to be displayed. This is documented in a couple Codex articles, so I won’t go too deeply into the philosophy behind it here. The particular hook that we need to use here is posts_where, which is used to change the WHERE clause of the query that is sent to the database.

add_filter('posts_where', 'scompt_posts_where');
function scompt_posts_where($where) {
    if( is_admin() ) {
        global $wpdb;
        if( $_GET['scompt_attachments'] == 'has' ) {
            $where .= " AND ID IN (SELECT post_parent FROM {$wpdb->posts} WHERE post_type='attachment' )";
        } else if( $_GET['scompt_attachments'] == 'hasnt' ) {
            $where .= " AND ID NOT IN (SELECT post_parent FROM {$wpdb->posts} WHERE post_type='attachment' )";
        }
    }
    return $where;
}

You’ll see that the first line of the scompt_posts_where function calls the is_admin function. This is to make sure that this filter handler will only be executed in the WordPress administrative backend. From there, we look at the $_GET variable to see if anything should be done. If the scompt_attachments variable is set to ‘has’ or ‘hasnt’, then we need to modify the WHERE clause.

Posts filtered on attachment status

Posts filtered on attachment status

With this code in place, you should be able to select a value from the Attachments dropdown again and see the results filtered. You Manage Posts screen should now look similar to the one at the right. That’s all well and good, but we can do better.

Sprucing it up

There are three code changes that can be made that will make the end result much more usable. The first two deal with the UI side of things and the last one is a performance issue. The UI changes also bring our new interface inline with what WordPress already does. Take a look at the Category dropdown next to our new interface. Notice that there are post counts next to the individual categories. We want that. Now select a category and filter on it. Notice that the choice of category is maintained. We want that too. The code below adds both of those features along with some nicer styling.

add_action('restrict_manage_posts', 'scompt_restrict_manage_posts');
function scompt_restrict_manage_posts() {
    global $wpdb;
    $has_count = $wpdb->get_var("SELECT COUNT(*) FROM wp_posts WHERE post_type='post' AND ID IN (SELECT post_parent FROM wp_posts WHERE post_type='attachment')");
    $hasnt_count = $wpdb->get_var("SELECT COUNT(*) FROM wp_posts WHERE post_type='post' AND ID NOT IN (SELECT post_parent FROM wp_posts WHERE post_type='attachment')");
    ?>
		<form name="scompt_attachmentform" id="scompt_attachmentform" action="" method="get">
			<fieldset style="margin: 0pt 1em 1em 1.5em; padding: 0pt; float: left;">
    			<legend><?php _e('Attachments'); ?>&hellip;</legend>
    			<select name='scompt_attachments' id='scompt_attachments' class='postform'>
        			<option value=""><?php _e('All'); ?></option>
        			<option value="has" <?php if( isset($_GET['scompt_attachments']) && $_GET['scompt_attachments']=='has') echo 'selected="selected"' ?>><?php _e('Has attachments'); ?>  (<?php echo $has_count ?>)</option>
        			<option value="hasnt" <?php if( isset($_GET['scompt_attachments']) && $_GET['scompt_attachments']=='hasnt') echo 'selected="selected"' ?>><?php _e('Has no attachments'); ?>  (<?php echo $hasnt_count ?>)</option>
        		</select>
			</fieldset>
			<input type="submit" name="submit" value="<?php _e('Filter &#187;'); ?/>" class="button" style="float:left;margin:14px 0pt 1em;position:relative;top:0.35em;"/>
		</form>
	<?php
}

With a bit of visual finery

With a bit of visual finery

Lines 4 and 5 query the database to get the count of posts with and without any attachments. Two separate, complicated queries isn’t optimal, but this isn’t an MySQL tutorial. Nevertheless, if you have a better solution, please let me know. With these counts in hand, we display them to the user in lines 12 and 13. In those same lines, we check the $_GET variable to see if an option should be pre-selected. The final version of the dropdown can be seen in the image to the right.

The performance improvement to be made has to do with how often the scompt_posts_where filter hook gets executed. As the code stands right now, the function will be executed every time WordPress makes a query, in the admin, out of the admin, in feeds … everywhere. The is_admin() call prevents too much work from being invested for no reason, but an even better solution would be to just not execute anything unless we’re in the administrative backend. To accomplish that, we change the filter code to the following:

add_action('admin_head', 'scompt_admin_head');
function scompt_admin_head() {
    add_filter('posts_where', 'scompt_posts_where');
}
function scompt_posts_where($where) {
    global $wpdb;
    if( $_GET['scompt_attachments'] == 'has' ) {
        $where .= " AND ID IN (SELECT post_parent FROM {$wpdb->posts} WHERE post_type='attachment' )";
    } else if( $_GET['scompt_attachments'] == 'hasnt' ) {
        $where .= " AND ID NOT IN (SELECT post_parent FROM {$wpdb->posts} WHERE post_type='attachment' )";
    }
    return $where;
}

Did you notice the difference? Now, instead of always adding the posts_where filter, it is only added when the admin_head action is called, which conveniently only happens in the administrative backend. This will save a couple function calls, which might be useful for a couple more seconds of uptime during your next Slashdotting.

What about pages?

Similar to the case of the manage_posts_custom_columns hooks, the Manage Pages screen is still a second-class citizen. It doesn’t call the restrict_manage_posts action, so there’s no chance to do any filtering. However, keep your eyes on scompt.com as I plan on releasing a plugin that will provide this functionality soon.

Complete Code

The code shown below is the final version of what we’ve been developing in this article. It has been tested in WordPress 2.3.3, but should work in old 2.2 and the new 2.5 versions, based on a quick look at the code.

add_action('restrict_manage_posts', 'scompt_restrict_manage_posts');
function scompt_restrict_manage_posts() {
    global $wpdb;
    $has_count = $wpdb->get_var("SELECT COUNT(*) FROM wp_posts WHERE post_type='post' AND ID IN (SELECT post_parent FROM wp_posts WHERE post_type='attachment')");
    $hasnt_count = $wpdb->get_var("SELECT COUNT(*) FROM wp_posts WHERE post_type='post' AND ID NOT IN (SELECT post_parent FROM wp_posts WHERE post_type='attachment')");
    ?>
		<form name="scompt_attachmentform" id="scompt_attachmentform" action="" method="get">
			<fieldset style="margin: 0pt 1em 1em 1.5em; padding: 0pt; float: left;">
    			<legend><?php _e('Attachments'); ?>&hellip;</legend>
    			<select name='scompt_attachments' id='scompt_attachments' class='postform'>
        			<option value=""><?php _e('All'); ?></option>
        			<option value="has" <?php if( isset($_GET['scompt_attachments']) && $_GET['scompt_attachments']=='has') echo 'selected="selected"' ?>><?php _e('Has attachments'); ?>  (<?php echo $has_count ?>)</option>
        			<option value="hasnt" <?php if( isset($_GET['scompt_attachments']) && $_GET['scompt_attachments']=='hasnt') echo 'selected="selected"' ?>><?php _e('Has no attachments'); ?>  (<?php echo $hasnt_count ?>)</option>
        		</select>
			</fieldset>
			<input type="submit" name="submit" value="<?php _e('Filter &#187;'); ?>" class="button" style="float:left;margin:14px 0pt 1em;position:relative;top:0.35em;"/>
		</form>
	<?php
}

add_action('admin_head', 'scompt_admin_head');
function scompt_admin_head() {
    add_filter('posts_where', 'scompt_posts_where');
}
function scompt_posts_where($where) {
    global $wpdb;
    if( $_GET['scompt_attachments'] == 'has' ) {
        $where .= " AND ID IN (SELECT post_parent FROM {$wpdb->posts} WHERE post_type='attachment' )";
    } else if( $_GET['scompt_attachments'] == 'hasnt' ) {
        $where .= " AND ID NOT IN (SELECT post_parent FROM {$wpdb->posts} WHERE post_type='attachment' )";
    }
    return $where;
}

Tagged with wordpress, writing.


7 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. Hugh Todd says

    Edward, this is great.

    If, instead, you wanted to use a search field (assuming that the custom column contained custom strings), would you still use “posts_where”?

  2. scompt says

    You’ll somehow need to change the query. Whether you do it with posts_where or with one of the hooks described on the Codex (here and here) shouldn’t matter.

  3. Darren says

    Great series and will be useful for plugin authors! An update would be good for this particular post as WordPress 2.5 changed the location of the restrict_manage_posts action to back inside the form so there is no need for adding your own (and when set up correctly you can also filter in combination with the other filters). For an example you can look at my plugin which uses it (and incidentally falls back to the old method for 2.3.x installs).

  4. Edward Dale says

    Yep, I noticed that last week when I finally got around to installing 2.5. I’ve started something and will hopefully publish it in the next few days. Thanks for the tip!

  5. John Kolbert says

    How would you change this so you can filter posts based on custom fields?

  6. John Kolbert says

    Sorry for the double comment. To clarify my last comment, I mean something like: only show posts that contain a certain custom field?

    Great tutorials, by the way! I enjoy your site very much!

  7. Edward Dale says

    Thanks John, glad you like it!

    To filter based on custom field, you’ll still have to use the posts_where hook, in addition to posts_join. You’ll need to join with the postsmeta table on the post ID and the custom field name. The Search Everything plugin has code that provides searching in metadata that might be enlightening.