Modifying Admin Post Lists In WordPress

Advertisement

Have you ever created a custom post type and then found that only the titles and dates of your posts are displayed in the admin lists? While WordPress will add taxonomies for you, that’s the most it can do. Adding relevant at-a-glance information is easy; in this article, we’ll look how to modify admin post lists with WordPress.

To make sure we’re on the same page, an admin list is the table of posts shown in the admin section when you click on “Posts,” “Pages” or another custom post type. Before we delve in, it is worth noting that admin tables are created using the WP_List_Table class. Jeremy Desvaux de Marigny has written a great article on native admin tables1 that explains how to make these from scratch.

We’ll focus in this article on how to extend existing tables. We’ll do this using an example from a theme that we recently built, named Rock Band2. Rock Band includes event management, which means that we needed some custom event-specific interface elements and details to make the admin section more useful!

Creating A Custom Post Type

This process is fairly straightforward and is documented well in “The Complete Guide to Custom Post Types3.” All we need is a definition of the labels that we’re going to use and a few settings. Open up your functions.php file and drop the following into it.

add_action( 'init', 'bs_post_types' );
function bs_post_types() {

  $labels = array(
    'name'                => __( 'Events', THEMENAME ),
    'singular_name'       => __( 'Event', THEMENAME ),
    'add_new'             => __( 'Add New', THEMENAME ),
    'add_new_item'        => __( 'Add New Event', THEMENAME ),
    'edit_item'           => __( 'Edit Event', THEMENAME ),
    'new_item'            => __( 'New Event', THEMENAME ),
    'all_items'           => __( 'All Event', THEMENAME ),
    'view_item'           => __( 'View Event', THEMENAME ),
    'search_items'        => __( 'Search Events', THEMENAME ),
    'not_found'           => __( 'No events found', THEMENAME ),
    'not_found_in_trash'  => __( 'No events found in Trash', THEMENAME ),
    'menu_name'           => __( 'Events', THEMENAME ),
  );

  $supports = array( 'title', 'editor' );

  $slug = get_theme_mod( 'event_permalink' );
  $slug = ( empty( $slug ) ) ? 'event' : $slug;

  $args = array(
    'labels'              => $labels,
    'public'              => true,
    'publicly_queryable'  => true,
    'show_ui'             => true,
    'show_in_menu'        => true,
    'query_var'           => true,
    'rewrite'             => array( 'slug' => $slug ),
    'capability_type'     => 'post',
    'has_archive'         => true,
    'hierarchical'        => false,
    'menu_position'       => null,
    'supports'            => $supports,
  );

  register_post_type( 'event', $args );

}

Quick Tip

By pulling the permalink from the theme settings, you can make sure that users of your theme are able to set their own permalinks. This is important for multilingual websites, on which administrators might want to make sure that URLs are readable by their users.

events_original4

What we get is the post list above. It’s better than nothing, but it has no at-a-glance information at all. Event venue, start time and ticket status would be great additions, so let’s get cracking!

Adding Custom Table Headers

Throughout this whole process, we will never have to touch the WP_Lists_Table class directly. This is wonderful news! Because we’ll be doing everything with hooks, our code will be nice and modular, easily customizable.

Adding the header is as simple as modifying the value of an array. This sounds like a job for a filter!

add_filter('manage_event_posts_columns', 'bs_event_table_head');
function bs_event_table_head( $defaults ) {
    $defaults['event_date']  = 'Event Date';
    $defaults['ticket_status']    = 'Ticket Status';
    $defaults['venue']   = 'Venue';
    $defaults['author'] = 'Added By';
    return $defaults;
}

Note the name of the filter: It corresponds to the name of the post type we have created. This means you can modify the table of any post type, not only your custom ones. Just use manage_post_posts_columns to modify the columns for the table of regular posts.

Once this code has been placed in our functions file, you should see the four new table headers. The fields don’t have any content yet; it is for us to decide what goes in them.

Fill ’er Up!

Adding data for each column is about as “complex” as it was to create the columns.

add_action( 'manage_event_posts_custom_column', 'bs_event_table_content', 10, 2 );

function bs_event_table_content( $column_name, $post_id ) {
    if ($column_name == 'event_date') {
    $event_date = get_post_meta( $post_id, '_bs_meta_event_date', true );
      echo  date( _x( 'F d, Y', 'Event date format', 'textdomain' ), strtotime( $event_date ) );
    }
    if ($column_name == 'ticket_status') {
    $status = get_post_meta( $post_id, '_bs_meta_event_ticket_status', true );
    echo $status;
    }

    if ($column_name == 'venue') {
    echo get_post_meta( $post_id, '_bs_meta_event_venue', true );
    }

}

As is obvious from the structure of this function, it gets called separately for each column. Because of this, we need to check which column is currently being displayed and then spit out the corresponding data.

The data we need for this is stored in the postmeta table. Here’s how to do it:

  • The event’s date is stored using the _bs_meta_event_date key.
  • The ticket’s status uses the _bs_meta_event_ticket_status key.
  • The venue is stored using the _bs_meta_event_venue meta key.

Because these are all postmeta values, we just need to use the get_post_meta() function to retrieve them. With the exception of the date, we can echo these values right away.

This brings us to an important point. You are not restricted to showing individual snippets of data or showing links. Whatever you output will be shown. With sufficient time, you could attach a calendar to the dates, which would be shown on hover. You could create flyout menus that open up on click, and so on.

events_data5

As you can see, this is much better. The event’s date, ticket status, venue and author can be seen, which makes this table actually informative, rather than just a way to get to edit pages. However, we can do more.

Ordering Columns

Enabling column ordering takes two steps but is fairly straightforward. First, use a filter to specify which of your columns should be sortable by adding it to an array. Then, create a filter for each column to modify the query when a user clicks to sorts the column.

add_filter( 'manage_edit-event_sortable_columns', 'bs_event_table_sorting' );
function bs_event_table_sorting( $columns ) {
  $columns['event_date'] = 'event_date';
  $columns['ticket_status'] = 'ticket_status';
  $columns['venue'] = 'venue';
  return $columns;
}

add_filter( 'request', 'bs_event_date_column_orderby' );
function bs_event_date_column_orderby( $vars ) {
    if ( isset( $vars['orderby'] ) && 'event_date' == $vars['orderby'] ) {
        $vars = array_merge( $vars, array(
            'meta_key' => '_bs_meta_event_date',
            'orderby' => 'meta_value'
        ) );
    }

    return $vars;
}

add_filter( 'request', 'bs_ticket_status_column_orderby' );
function bs_ticket_status_column_orderby( $vars ) {
    if ( isset( $vars['orderby'] ) && 'ticket_status' == $vars['orderby'] ) {
        $vars = array_merge( $vars, array(
            'meta_key' => '_bs_meta_event_ticket_status',
            'orderby' => 'meta_value'
        ) );
    }

    return $vars;
}

add_filter( 'request', 'bs_venue_column_orderby' );
function bs_venue_column_orderby( $vars ) {
    if ( isset( $vars['orderby'] ) && 'venue' == $vars['orderby'] ) {
        $vars = array_merge( $vars, array(
            'meta_key' => '_bs_meta_event_venue',
            'orderby' => 'meta_value'
        ) );
    }

    return $vars;
}

Here is what’s happening in each of these cases. Whenever posts are listed, an array of arguments is passed that determines what is shown — things like how many to show per page, which post type to display, and so on. WordPress knows how to construct the array of arguments for each of its built-in features.

When we say, “order by venue,” WordPress doesn’t know what this means. Results are ordered before they are displayed, not after the fact. Therefore, WordPress needs to know what order to pull posts in before it actually retrieves them. Thus, we tell WordPress which meta_key to filter by and how to treat the values (meta_value for strings, meta_value_num for integers).

As with displaying data, you can go nuts here. You can use all of the arguments that WP_Query takes to perform taxonomy filtering, meta field queries and so on.

By adding the code above, we can now click to order based on date, status and venue. We’re almost there. One more thing would help out a lot, especially when dealing with hundreds of events.

Data Filtering

Setting up the filters is analogous to setting up ordering. First, we tell WordPress which controls we want to use. Then, we need to make sure those controls actually do something. Let’s get started.

add_action( 'restrict_manage_posts', 'bs_event_table_filtering' );
function bs_event_table_filtering() {
  global $wpdb;
  if ( $screen->post_type == 'event' ) {

    $dates = $wpdb->get_results( "SELECT EXTRACT(YEAR FROM meta_value) as year,  EXTRACT( MONTH FROM meta_value ) as month FROM $wpdb->postmeta WHERE meta_key = '_bs_meta_event_date' AND post_id IN ( SELECT ID FROM $wpdb->posts WHERE post_type = 'event' AND post_status != 'trash' ) GROUP BY year, month " ) ;

    echo '';
      echo '' . __( 'Show all event dates', 'textdomain' ) . '';
    foreach( $dates as $date ) {
      $month = ( strlen( $date->month ) == 1 ) ? 0 . $date->month : $date->month;
      $value = $date->year . '-' . $month . '-' . '01 00:00:00';
      $name = date( 'F Y', strtotime( $value ) );

      $selected = ( !empty( $_GET['event_date'] ) AND $_GET['event_date'] == $value ) ? 'selected="select"' : '';
      echo '' . $name . '';
    }
    echo '';

    $ticket_statuses = get_ticket_statuses();
    echo '';
      echo '' . __( 'Show all ticket statuses', 'textdomain' ) . '';
    foreach( $ticket_statuses as $value => $name ) {
      $selected = ( !empty( $_GET['ticket_status'] ) AND $_GET['ticket_status'] == $value ) ? 'selected="selected"' : '';
      echo '' . $name . '';
    }
    echo '';

  }
}

I know, this is a bit scarier! Initially, all we are doing is making sure that we add filters to the right page. As you can see from the hook, this is not specific to the post’s type, so we need to check manually.

Once we’re sure that we’re on the events page, we add two controls: a selector for event dates and a selector for ticket statuses.

We have one custom function in there, get_ticket_statuses(), which is used to retrieve a list of ticket statuses. These are all defined by the user, so describing how it works would be overkill. Suffice it to say that it contains an array with the key-value pairs that we need for the selector.

events_final6

Once this is done, the table will reach its final form. We now have our filters along the top, but they don’t work yet. Let’s fix that, shall we?

Filtering data is simply a matter of adding arguments to the query again. This time, instead of ordering our data, we’ll add parameters to narrow down or broaden our returned list of posts.

add_filter( 'parse_query','bs_event_table_filter' );
function bs_event_table_filter( $query ) {
  if( is_admin() AND $query->query['post_type'] == 'event' ) {
    $qv = &$query->query_vars;
    $qv['meta_query'] = array();


    if( !empty( $_GET['event_date'] ) ) {
      $start_time = strtotime( $_GET['event_date'] );
      $end_time = mktime( 0, 0, 0, date( 'n', $start_time ) + 1, date( 'j', $start_time ), date( 'Y', $start_time ) );
      $end_date = date( 'Y-m-d H:i:s', $end_time );
      $qv['meta_query'][] = array(
        'field' => '_bs_meta_event_date',
        'value' => array( $_GET['event_date'], $end_date ),
        'compare' => 'BETWEEN',
        'type' => 'DATETIME'
      );

    }

    if( !empty( $_GET['ticket_status'] ) ) {
      $qv['meta_query'][] = array(
        'field' => '_bs_meta_event_ticket_status',
        'value' => $_GET['ticket_status'],
        'compare' => '=',
        'type' => 'CHAR'
      );
    }

    if( !empty( $_GET['orderby'] ) AND $_GET['orderby'] == 'event_date' ) {
      $qv['orderby'] = 'meta_value';
      $qv['meta_key'] = '_bs_meta_event_date';
      $qv['order'] = strtoupper( $_GET['order'] );
    }

  }
}

For each filter, we need to add rules to the query. When we’re filtering for events, we need to add a meta_query7. This will return only results for which the custom field key is _bs_meta_event_ticket_status and the value is the given ticket’s status.

Once this final piece of the puzzle is added, we will have a customized WordPress admin list, complete with filtering, ordering and custom data. Well done!

Overview

Adding custom data to a table is a great way to draw information to the attention of users. Plugin developers can hook their functionality into posts without touching any other functionality, and theme authors can add advanced information about custom post types and other things to relevant places.

Showing the right information in the right place can make a huge difference in the salability and likability of any product. That being said, don’t overexploit your newfound power. Don’t add fields just because you can, especially to WordPress’ main tables.

Don’t forget that others know about this, too, and many developers of SEO plugins and similar products already add their own columns to posts. If you’re going to add things to the default post types, I suggest including settings to enable and disable them.

If you’ve used these techniques in one of your products or are wondering how to show some tidbit of information in a table, let us know in the comments!

(al, il)

Footnotes

  1. 1 http://www.smashingmagazine.com/2011/11/03/native-admin-tables-wordpress/
  2. 2 http://themeforest.net/item/rock-band-awesome-music-template/5581920
  3. 3 http://www.smashingmagazine.com/2012/11/08/complete-guide-custom-post-types/
  4. 4 http://www.smashingmagazine.com/wp-content/uploads/2013/09/events_original.png
  5. 5 http://www.smashingmagazine.com/wp-content/uploads/2013/09/events_data.png
  6. 6 http://www.smashingmagazine.com/wp-content/uploads/2013/09/events_final.png
  7. 7 http://codex.wordpress.org/Class_Reference/WP_Query#Custom_Field_Parameters

↑ Back to topShare on Twitter

Daniel is not only a web developer who is in love with WordPress, but also the Editor of the Smashing WordPress section. He owns a company called Bonsai Shed where they make web apps and premium themes like Musico and Estatement.

Advertising
  1. 1

    When creating a custom post type, use the “Post Type Generator” at http://GenerateWP.com/post-type/
    it’s faster and easier.

    0
  2. 3

    Great article. For those willing to do this with a plugin; here’s a well-made one that does all this. Codepress Admin Columns: http://wordpress.org/plugins/codepress-admin-columns/

    They have a premium add-on for sorting and filtering too.

    Highly recommended!

    0
    • 4

      That.

      I have been creating custom post types and accompanying tables — even sortable and with all sorts of info customizations — by hand pretty much since custom post types became available in WordPress. Back then there were no plugins to do the job in a satisfactory way.

      Then I recently discovered the existence “Codepress Admin Columns”, and I must say it does the job just as if I had the customizations programmed myself. Which I am really picky about.

      The only thing it (naturally) doesn’t do is fill the columns with data that needs to be processed before it can be displayed in a column. But that’s a fringe case really.

      0
  3. 5

    Great one, thank you :)

    0
  4. 6

    Geert van der Heide

    December 5, 2013 2:26 am

    Thanks for this article! It’s a topic I haven’t read much about. Nice to have a quick but complete reference for when I need it.

    0
  5. 7

    “By pulling the permalink from the theme settings, you can make sure that users of your theme are able to set their own permalinks. This is important for multilingual websites, on which administrators might want to make sure that URLs are readable by their users.”

    Just a quick note: permalinks should be also translation-ready – so don’t forget to wrap those into __(); :)

    Tom

    0
    • 8

      Hi Tom!

      Good point! I did include a ‘get_theme_mod()’ reference which would allow the user to change it. This would work when creating a site in a different language but not for multilingual site, you’re right :)

      Daniel

      0
  6. 9

    You can use this class to create and manage the custom post type columns in a much faster and easier way.

    0
  7. 10

    awesome, I needed this stuff

    0
  8. 11

    This is very good, one of the most complete guides to managing custom post columns that I’ve seen yet. Will surely use aspects of this for theme development. Thank you Daniel.

    0
  9. 12

    Here’s a challenge:

    Imagine you have a CPT that has a date and an AM/PM selector, and for each date there can only be one post for each half of that day. Then imagine if in the backend we want to list these items so that each line has a date, then a link for the AM entry, and a link for the PM entry. Is this possible without heavily rewriting how the List Table class works?

    0
    • 13

      Edit: I’m trying not to strain the edit function, so I deleted this comment and elaborate in the next one.

      0
      • 14

        I struggled a bit with your description at first, but I think I got you. You’re trying to have a line in the post table that contains the “title” columns (i.e. the main column with all the edit links and such) for two posts next to each other.

        I dug a bit into WordPress’ guts and I came to the conclusion that it could be done, but it’s not really elegant and not really easy. The solution would be two-fold: a) filter out the posts that should be combined into another table row, and b) create the table cells for that second post in the first post’s row.

        I can see a way of coercing the table generation to suppress rows, by checking beforehand which posts not to display and supplying that info as a post__not_in parameter to the ‘request’ filter. While you’re at it you could also create a lookup table for the first post to look up which other post to include in the row.

        But the real ugliness comes when the actual table is generated from the post list. Creation of the checkbox and title cells is hardcoded into a switch statement and cannot be called externally, so you’d have to duplicate that part of the code, which is very ugly and not particularly future-proof. Personally, I wouldn’t do it.

        It would be much more straightforward to have the list sorted by a “date” custom field and another for “AM/PM” (sorting by two fields would be a bit icky, however, making it the default sorting then is easy).

        I think the easiest, actually two-klicks-in-a-plugin solution would to sort the table by the date field, and display AM/PM in the column next to it. Sometimes the AM entry would come after PM but I think it’s a reasonable tradeoff.

        You’d still have two lines for two halves of a day, but they would be next to each other and the single entries would carry all the info and modification instruments for the respective posts.

        0
  10. 15

    Very cool! Here a small addon for categories ‘sorry Dutch labels’: http://pastebin.com/CZcQeqDJ
    The only thing what i’am still searching for is how to add a category into the slug?

    Now its build like this;

    $slug = get_theme_mod( ‘event_permalink’ );
    $slug = ( empty( $slug ) ) ? ‘event’ : $slug;

    $args = array(
    ‘rewrite’ => array( ‘slug’ => $slug ),
    );

    Also i did added customfields into the posstype, used this script;
    https://github.com/jaredatch/Custom-Metaboxes-and-Fields-for-WordPress

    0
  11. 16

    Nice tutorial, but I’ve got a question though:

    it seems to be from an event calendar. This is what I’m also working on at the moment. I already made one writing my own plugin and now I try to do it using custom post types. All working fine so far, but I’d like to do the following:

    I want to have two lists available for the admin. One with upcoming events and one with past ones. I know how to do it in my own plugin, but how do I go about it when using WPs standard edit page? Can I have there two tables?

    0
    • 17

      Hi Holder,

      Sure, everything is possible :) There are two ways you could tackle this. You can add a simple filter to the top which would allow you to show “all”, “past” or “future”. You can use the code in the data filtering section to set this up.

      If you’d like to add another table to the list the easiest way to do it is to just create your own admin page and add both tables as a custom WP_List_Table. This requires a bit more work but will allow you to be much more flexible with how you show data.

      0
  12. 18

    Hi. Nice tutorial.
    Seems to be a quite popular thing to be able to change the way the post list looks.

    However, what I really would like to have is the possibility to edit custom fields right in the post list. And I am wondering why nobody else seems to need this. There is a plugin that makes it possible to sort posts via drag and drop and I saw one that makes it possible to change some standard post meta data (Admin Management Xtended). But editing custom fields would be really great.

    Sorry if I am wrong. I am quite new to wordpress. I never really got into it because I always had the feeling that it is much too opinionated and unflexible, being mainly a blogging software. But now I know much more is possible than I thought.

    0
  13. 19

    I was walking through the filtering section and ran across an error in your examples above. I think Smashing’s WYSIWYG may be filtering out the HTML in the post, because there aren’t any or tags around the data-filtering lists :-)

    It should look something like this:

    echo '<select name="resource_section">';
    echo '<option value="">' . __( 'Show all sections', 'textdomain' ) . '</option>';
    foreach( $sectionOptions as $value => $name ) {
    $selected = ( !empty( $_GET['resource_section'] ) AND $_GET['resource_section'] == $value ) ? 'selected="selected"' : '';
    echo '<option ' .$selected . ' value="'.$value.'">' . $name . '</option>'; }
    echo '</select>';

    Thanks for the article! It’s been very helpful!

    3
  14. 20

    There was also an issue with the variable $screen in line 4 of Data Filtering: if ( $screen->post_type == 'sdi_resources' ) {.

    To resolve this, I used $current_screen instead:
    global $wpdb, $current_screen;
    if ( $current_screen->post_type == 'sdi_resources' ) {

    4
  15. 21

    I’m having trouble getting the parse_query hook to work. Perhaps the $qv array is not setting the query vars? I’m on 3.6.1 for reference. If anyone could shed some light on this that would be great.

    Otherwise, this tutorial was extremely beneficial. Thanks also to the users who found the bugs and posted them in comments.

    0
  16. 22

    I’ve developed a PHP class to help developers build custom post types quickly.

    You can create post types and taxonomies and customise the admin edit screen with columns and filters. Check it out.

    https://github.com/jjgrainger/wp-custom-post-type-class

    0
  17. 23

    This is very good, one of the most complete guides to managing custom post columns that I’ve seen yet. Will surely use aspects of this for theme development.

    0
  18. 24

    A really nice addition to this (superb) article would be how to actually change the order of the columns. In the example, the published date is the second column…what if I wanted that to be the last column?

    0
    • 25

      To answer my own question, you just unset the columns you want to move, then set them again:

      function property_columns( $defaults ) {
      unset($defaults['date']);
      unset($defaults['title']);
      $defaults['featured_image'] = ‘Image’;
      $defaults['title'] = ‘Property Name’;
      $defaults['reference'] = ‘Ref. No’;
      $defaults['price'] = ‘Price’;
      $defaults['availability'] = ‘Availability’;
      $defaults['date'] = ‘Date’;
      return $defaults;
      }

      0
  19. 26

    Very helpful but I ran into one issue: the last portion regarding filtering seems to conflict with meta_query when used within get_posts on other plugins in the admin screens (the dashboard for example). Specifically, commenting out the line “$qv = &$query->query_vars;” seems to fix the problems though being somewhat of a novice I’m not sure why this fixes the conflict. The codex is a little weak on query_vars so I don’t understand what causes the conflict.

    Great tutorial though!!!

    0
  20. 27

    If your data is not filtering correctly, i’d suggest changing this line:

    ‘field’ => ‘_bs_meta_event_ticket_status’,

    to this:

    ‘key’ => ‘_bs_meta_event_ticket_status’,

    It worked for me.

    0
  21. 28

    Thanks for this Great article :)

    0
  22. 29

    Please note, the following must be set: ‘hierarchical’ => false.

    Custom columns will not work if hierarchical set to true on custom post type pages, but will for normal posts.

    Also, the following link is very helpful to figure out how how to do things like add columns to ALL post types’ admin posts tables (eg: custom post types and default post types):

    http://code.tutsplus.com/articles/add-a-custom-column-in-posts-and-custom-post-types-admin-screen–wp-24934

    0

Leave a Comment

Yay! You've decided to leave a comment. That's fantastic! Please keep in mind that comments are moderated and rel="nofollow" is in use. So, please do not use a spammy keyword or a domain as your name, or else it will be deleted. Let's have a personal and meaningful conversation instead. Thanks for dropping by!

↑ Back to top