5

I'm trying to display a WP_List_table inside a metabox. The metabox is for a custom taxonomy called service which is registered to a custom post type called office. The table columns contain input fields which manipulate the taxonomy and allow me to link additional data to each individual taxonomy term. I don't think the fact that I'm trying to this for a custom taxonomy which is on a custom post type edit screen is relevant. I only merely mention to for clarity sake.

What's happening is that it all looks fine until I click the Publish/Update button on the custom post type edit screen. If the WP_List_Table has bulk actions it will redirect back to the /wp-admin/edit.php page, it I remove the bulk actions then it displays a Wordpress Failure Notice "Are you sure you want to do this? - Please try again", which is a nonce validation error. And in both cases, the none of the data I change for the post is saved.

I've included a link below to the full script that registers the taxonomy, creates the table and saves the metabox data. Feel free to fiddle with it to get it working if you like.

http://www.writeurl.com/text/aawwgq1od5913fmyfjyu/0y84g3vcyptvrkvvfxq4/0dow449a06lj6ctkqjqo

A NOTE TO THE STACKOVERFLOW EDITORS: I have already checked through questions suggested while I type mine and also all questions tagged with wp-list-table plus scoured the depths of Google and none of them seem relevant to my situation.

UPDATE: If possible I'd prefer to not to have to use any bulk_actions. I've no need for them on this table. I only added the delete action for testing purposes. And the class method process_bulk_action doesn't even get called on submit anyway.

<?php
if(!class_exists('WP_List_Table')) {
    require_once(ABSPATH.'wp-admin/includes/class-wp-list-table.php');
}

class servicesTable extends WP_List_Table {
    private $allServices;
    private $currentServices;
    private $currentServicesData;

    public function __construct(){
        parent::__construct(
            array(
                'singular' => 'service',
                'plural' => 'services',
                'ajax' => false
            )
        );
    }

    private function table_data() {
        $this->currentServices = wp_get_object_terms(get_the_ID(),array('service'),array('fields' => 'ids'));
        $this->currentServicesData = (array) maybe_unserialize(get_post_meta(get_the_ID(),'currentServices',true));
        $this->allServices = array();
        foreach((array) get_terms(array('service'),array('hide_empty' => false)) as $service) {
            $this->allServices[] = array(
                'id' => $service->term_id,
                'serviceTitle' => $service->name,
                'attachments' => array_key_exists($service->term_id,$this->currentServicesData) ? explode(',',$this->currentServicesData[$service->term_id]['attachments']) : array(),
                'lastServiced' =>  (array_key_exists($service->term_id,$this->currentServicesData) && strlen($this->currentServicesData[$service->term_id]['last_serviced']) > 0) ? date('Y-m-d',strtotime($this->currentServicesData[$service->term_id]['last_serviced'])) : '',
                'nextService' =>  (array_key_exists($service->term_id,$this->currentServicesData) && strlen($this->currentServicesData[$service->term_id]['next_service']) > 0) ? date('Y-m-d',strtotime($this->currentServicesData[$service->term_id]['next_service'])) : '',
                'invoices' => ''
            );
        }
        return $this->allServices;
    }

    public function column_default($item,$columnName) {
        return array_key_exists($columnName,$item) ? $item[$columnName] : print_r($item,true);
    }

    public function get_columns(){
        return array(
            'cb' => '<input type="checkbox" />',
            'id' => 'ID',
            'serviceTitle' => __('Title','brokertech'),
            'attachments' => __('Attachments','brokertech'),
            'lastServiced' => __('Last Service Date','brokertech'),
            'nextService' => __('Next Service Date','brokertech'),
            'invoices' => __('Invoices','brokertech')
        );
    }

    public function get_hidden_columns() {
        return array('id');
    }

    public function get_sortable_columns() {
        return array(
            'lastServiced' => array('lastServiced',true),
            'nextService' => array('nextService',true)
        );
    }

    private function sort_data($a,$b) {
        $orderby = empty($_GET['orderby']) ? 'nextService' : $_GET['orderby'];
        $order = empty($_GET['order']) ? 'asc' : $_GET['order'];
        $result = strcmp($a[$orderby],$b[$orderby]);
        return ($order === 'asc') ? $result : -$result;
    }

    public function prepare_items() {
        usort($this->table_data(),array(&$this,'sort_data'));
        $this->_column_headers = array($this->get_columns(),$this->get_hidden_columns(),$this->get_sortable_columns());
        $this->items = $this->allServices;
        wp_enqueue_script('services',JS_URI.'servicesSelector.js',array('thickbox','media-upload'));
        wp_enqueue_style('thickbox');
        wp_localize_script(
        'services',
        'servicesData',
            array(
                'editAttachment' => __('Edit'),
                'removeAttachment' => __('Remove')
            )
        );
    }

    public function column_cb($item) {
        return sprintf('<input type="checkbox" name="services[%s][enable]" value="%s" %s />',$item['id'],$item['serviceTitle'],checked(true,in_array($item['id'],$this->currentServices),false));
    }

    public function column_id($item) {
        return sprintf('<input type="hidden" name="services[%s][id]" value="%s" />',$item['id'],$item['id']);
    }

    public function column_attachments($item) {
        $html  = sprintf(
            '<input type="hidden" name="services[%s][attachments]" value="%s" class="listAttachments" />',
            $item['id'],
            empty($item['attachments']) ? '' : implode(',',$item['attachments'])
        );
        $html .= sprintf('<input type="button" value="%s" class="button button-secondary addServiceAttachment" data-service-id="%s" />',__('Add'),$item['id']);
        $html .= "&nbsp;";
        if(empty($item['attachments'])) {
            $html .= sprintf('<input type="button" value="%s" class="button button-secondary clearAll" data-service-id="%s" disabled="disabled" />',__('Clear'),$item['id']);
        }
        else {
            $html .= sprintf('<input type="button" value="%s" class="button button-secondary clearAll" data-service-id="%s" />',__('Clear'),$item['id']);
            $html .= "<br /><br />";
            foreach($item['attachments'] as $attachment) {
                if(is_numeric($attachment)) {
                    $html .= sprintf(
                        "<div class='serviceAttachment' style='margin-bottom: 1em;'><span>%s</span><br /><a href='%s' target='_blank' class='button button-secondary'>%s</a><input type='button' class='removeServiceAttachment button button-secondary' value='%s' style='float:right;' /><div style='clear:both;'></div></div>",
                        get_the_title($attachment),
                        get_admin_url(get_current_blog_id(),'post.php?post='.$attachment.'&action=edit'),
                        __('Edit'),
                        __('Remove')
                    );
                }
            }
        }
        return $html;
    }

    public function column_lastServiced($item) {
        return sprintf('<input type="date" name="services[%s][last_serviced]" value="%s" />',$item['id'],$item['lastServiced']);
    }

    public function column_nextService($item) {
        return sprintf('<input type="date" name="services[%s][next_service]" value="%s" />',$item['id'],$item['nextService']);
    }

    public function column_invoices($item) {
        $html  = sprintf(
            '<input type="hidden" name="services[%s][invoices]" value="%s" class="listInvoices" />',
            $item['id'],
            empty($item['invoices']) ? '' : implode(',',$item['invoices'])
        );
        $html .= sprintf('<input type="button" value="%s" class="button button-secondary addInvoice" data-service-id="%s" />',__('Add'),$item['id']);
        $html .= "&nbsp;";
        if(!empty($item['invoices'])) {
            $html .= "<br /><br />";
            foreach($item['invoices'] as $invoice) {
                if(is_numeric($invoice)) {
                    $html .= sprintf(
                        "<div class='serviceInvoice' style='margin-bottom: 1em;'><span>%s</span><br /><a href='%s' target='_blank' class='button button-secondary'>%s</a><input type='button' class='removeInvoice button button-secondary' value='%s' style='float:right;' /><div style='clear:both;'></div></div>",
                        get_the_title($invoice),
                        get_admin_url(get_current_blog_id(),'post.php?post='.$attachment.'&action=edit'),
                        __('Edit'),
                        __('Remove')
                    );
                }
            }
        }
        return $html;
    }
}

function saveServices($postID) {
    if(empty($_POST) || (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)) {
        return;
    }
    $servicesData = array();
    $services = array();
    foreach($_POST['services'] as $service) {
        if(isset($service['enable'])) {
            $services[] = (string) $service['enable'];
                $servicesData[$service['id']] = $service;
        }
    }
    empty($servicesData) ? delete_post_meta($postID,'currentServices') : update_post_meta($postID,'currentServices',$servicesData);
    wp_set_object_terms($postID,$services,'service');
}
add_action('save_post','saveServices');

function servicesMetaBox() {
    $servicesTable = new servicesTable();
    $servicesTable->prepare_items();
    $servicesTable->display();
}

$serviceTaxonomy = new brokertechPostType;
$serviceTaxonomy->createTaxonomy(
    'service',
    array(
        'office'
    ),
    __('Service'),
    __('Services'),
    array(
        'public' => true,
        'hierarchical' => false,
        'query_var' => true,
        'update_count_callback' => '_update_post_term_count',
        'show_ui' => true,
        'rewrite' => array(
            'slug' => __('services')
        ),
        'meta_box_cb' => 'servicesMetaBox'
    )
);

function addServicesTaxonomyPage() {
    add_submenu_page(
        'edit.php?post_type=company',
        __('Services'),
        __('Services'),
        'edit_posts',
        'edit-tags.php?taxonomy=service&post_type=office'
    );
}
add_action('admin_menu','addServicesTaxonomyPage');

function changeServicesMetaBoxPosition() {
    global $wp_meta_boxes;
    unset($wp_meta_boxes['office']['side']['core']['tagsdiv-service']);
    add_meta_box(
        'tagsdiv-service',
        __('Services'),
        'servicesMetaBox',
        'office',
        'normal',
        'low'
    );
}
add_action('add_meta_boxes','changeServicesMetaBoxPosition',0);
7
  • Removing the nonce field via javascript is a temporary fix, although it's not ideal and I feel I shouldn't have to do that anyway as it's meant to have a nonce field.
    – Ben
    Mar 19, 2014 at 10:51
  • Ben, take a look at my Internal Links Checker Plugin on GitHub. I'm using a list table in a meta box there. Btw, the code in the link is unreadable and we prefer code to be in the question instead of an outside resource. Could you please move it over here? Thanks.
    – kaiser
    Mar 19, 2014 at 11:14
  • Thanks @kaiser, I've added my latest code as I've made some changes to the columns, although still having the same issue. I always thought that large amounts of code is preferred to be referenced externally. Your plugin does work on my site however I'm not able to test it on my custom post type, so I'm therefore not able to tell if that's the cause of the problem.
    – Ben
    Mar 19, 2014 at 14:24
  • Ben, just change this line in your copy of the Internal Link Checker. And could you please rework your code - the indentation seems to be gone.
    – kaiser
    Mar 19, 2014 at 16:29
  • @kaiser, yes, your list_table does work perfectly on my post_type however I can see that you don't have the nonce or referer fields added to yours, actually you don't have any input fields at all whereas mine does. Could it possibly be that the metabox is for a custom taxonomy instead a standard metabox? I could quite easily that and get back to you. Indentation has been fixed for you btw.
    – Ben
    Mar 20, 2014 at 10:22

2 Answers 2

3

You just need to override the function display_tablenav() with a blank function in you parent class.

It is explained here.

1

I just ran into this issue recently.

If you are using a Custom List Table (A/K/A WP List Table) within a Meta Box (located on the post edit screen post.php or post-new.php.)

The reason you get redirected to the posts listing screen (edit.php) is because you're Custom List Table generates a hidden nonce field, which is also generated by the post.php or post-new.php pages. They both have the same names, there for ... unexpected problems occur.

Simple solution would be to override the function creating the nonce field in your Custom List Table.

The problem in your Custom List Table is that your pagination is creating a nonce.

If you open up the list-table.php class and search for function display_tablenav( you will find the function which is creating the nonce.

I'm sure there's a better approach to this, but I simple just copied that entire function, then pasted it into my Custom List Table class, and commented out the part which creates the nonce field.

Example:

/**
 * Generate the table navigation above or below the table
 *
 * @since 3.1.0
 * @access protected
 *
 * @param string $which
 */
protected function display_tablenav( $which ) {

    // REMOVED NONCE -- INTERFERING WITH SAVING POSTS ON METABOXES
    // Add better detection if this class is used on meta box or not.
    /*
    if ( 'top' == $which ) {
        wp_nonce_field( 'bulk-' . $this->_args['plural'] );
    }
    */

    ?>
    <div class="tablenav <?php echo esc_attr( $which ); ?>">

        <div class="alignleft actions bulkactions">
            <?php $this->bulk_actions( $which ); ?>
        </div>
        <?php
        $this->extra_tablenav( $which );
        $this->pagination( $which );
        ?>

        <br class="clear"/>
    </div>
<?php
}

If you comment out that section of code, your post page should successfully submit like normal once again.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.