Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

I have a custom post type Event that contains a starting and ending date/times custom fields (as metaboxes in the post edit screen).

I would like to make sure that an Event cannot get published (or scheduled) without the dates being filled, as that will cause problems with the templates displaying the Event data (besides the fact that it is a necessary requirement!). However, I would like to be able to have Draft events that do not contain a valid date while they are in preparation.

I was thinking of hooking save_post to do the checking, but how can I prevent the status change from happening?

EDIT1: This is the hook I'm using now to save the post_meta.

// Save the Metabox Data
function ep_eventposts_save_meta( $post_id, $post ) {

if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
    return;

if ( !isset( $_POST['ep_eventposts_nonce'] ) )
    return;

if ( !wp_verify_nonce( $_POST['ep_eventposts_nonce'], plugin_basename( __FILE__ ) ) )
    return;

// Is the user allowed to edit the post or page?
if ( !current_user_can( 'edit_post', $post->ID ) )
    return;

// OK, we're authenticated: we need to find and save the data
// We'll put it into an array to make it easier to loop though

//debug
//print_r($_POST);

$metabox_ids = array( '_start', '_end' );

foreach ($metabox_ids as $key ) {
    $events_meta[$key . '_date'] = $_POST[$key . '_date'];
    $events_meta[$key . '_time'] = $_POST[$key . '_time'];
    $events_meta[$key . '_timestamp'] = $events_meta[$key . '_date'] . ' ' . $events_meta[$key . '_time'];
}

$events_meta['_location'] = $_POST['_location'];

if (array_key_exists('_end_timestamp', $_POST))
    $events_meta['_all_day'] = $_POST['_all_day'];

// Add values of $events_meta as custom fields

foreach ( $events_meta as $key => $value ) { // Cycle through the $events_meta array!
    if ( $post->post_type == 'revision' ) return; // Don't store custom data twice
    $value = implode( ',', (array)$value ); // If $value is an array, make it a CSV (unlikely)
    if ( get_post_meta( $post->ID, $key, FALSE ) ) { // If the custom field already has a value
        update_post_meta( $post->ID, $key, $value );
    } else { // If the custom field doesn't have a value
        add_post_meta( $post->ID, $key, $value );
    }
    if ( !$value ) 
                delete_post_meta( $post->ID, $key ); // Delete if blank
}

}

add_action( 'save_post', 'ep_eventposts_save_meta', 1, 2 );

EDIT2: and this is what I'm trying to use to check the post data after saving to the database.

add_action( 'save_post', 'ep_eventposts_check_meta', 99, 2 );
function ep_eventposts_check_meta( $post_id, $post ) {
//check that metadata is complete when a post is published
//print_r($_POST);

if ( $_POST['post_status'] == 'publish' ) {

    $custom = get_post_custom($post_id);

    //make sure both dates are filled
    if ( !array_key_exists('_start_timestamp', $custom ) || !array_key_exists('_end_timestamp', $custom )) {
        $post->post_status = 'draft';
        wp_update_post($post);

    }
    //make sure start < end
    elseif ( $custom['_start_timestamp'] > $custom['_end_timestamp'] ) {
        $post->post_status = 'draft';
        wp_update_post($post);
    }
    else {
        return;
    }
}
}

The main issue with this is a problem that was actually described in another question: using wp_update_post() within a save_post hook triggers an infinite loop.

EDIT3: I figured a way to do it, by hooking wp_insert_post_data instead of save_post. The only problem is that now the post_status is reverted, but now a misleading message saying "Post published" shows up (by adding &message=6 to the redirected URL), but the status is set to Draft.

add_filter( 'wp_insert_post_data', 'ep_eventposts_check_meta', 99, 2 );
function ep_eventposts_check_meta( $data, $postarr ) {
//check that metadata is complete when a post is published, otherwise revert to draft
if ( $data['post_type'] != 'event' ) {
    return $data;
}
if ( $postarr['post_status'] == 'publish' ) {
    $custom = get_post_custom($postarr['ID']);

    //make sure both dates are filled
    if ( !array_key_exists('_start_timestamp', $custom ) || !array_key_exists('_end_timestamp', $custom )) {
        $data['post_status'] = 'draft';
    }
    //make sure start < end
    elseif ( $custom['_start_timestamp'] > $custom['_end_timestamp'] ) {
        $data['post_status'] = 'draft';
    }
    //everything fine!
    else {
        return $data;
    }
}

return $data;
}
share|improve this question
add comment

4 Answers

up vote 3 down vote accepted

OK, this is finally how I ended up doing it: an Ajax call to a PHP function that does the checking, sort of inspired by this answer and using a clever tip from a question I asked on StackOverflow. Importantly, I make sure that only when we want to Publish the checking is done, so that a Draft can always be saved without the checking. This ended up being the easier solution to actually prevent the publication of the post. It might help someone else, so I wrote it up here.

First, add the necessary Javascript:

//AJAX to validate event before publishing
//adapted from http://wordpress.stackexchange.com/questions/15546/dont-publish-custom-post-type-post-if-a-meta-data-field-isnt-valid
add_action('admin_enqueue_scripts-post.php', 'ep_load_jquery_js');   
add_action('admin_enqueue_scripts-post-new.php', 'ep_load_jquery_js');   
function ep_load_jquery_js(){
global $post;
if ( $post->post_type == 'event' ) {
    wp_enqueue_script('jquery');
}
}

add_action('admin_head-post.php','ep_publish_admin_hook');
add_action('admin_head-post-new.php','ep_publish_admin_hook');
function ep_publish_admin_hook(){
global $post;
if ( is_admin() && $post->post_type == 'event' ){
    ?>
    <script language="javascript" type="text/javascript">
        jQuery(document).ready(function() {
            jQuery('#publish').click(function() {
                if(jQuery(this).data("valid")) {
                    return true;
                }
                var form_data = jQuery('#post').serializeArray();
                var data = {
                    action: 'ep_pre_submit_validation',
                    security: '<?php echo wp_create_nonce( 'pre_publish_validation' ); ?>',
                    form_data: jQuery.param(form_data),
                };
                jQuery.post(ajaxurl, data, function(response) {
                    if (response.indexOf('true') > -1 || response == true) {
                        jQuery("#post").data("valid", true).submit();
                    } else {
                        alert("Error: " + response);
                        jQuery("#post").data("valid", false);

                    }
                    //hide loading icon, return Publish button to normal
                    jQuery('#ajax-loading').hide();
                    jQuery('#publish').removeClass('button-primary-disabled');
                    jQuery('#save-post').removeClass('button-disabled');
                });
                return false;
            });
        });
    </script>
    <?php
}
}

Then, the function that handles the checking:

add_action('wp_ajax_ep_pre_submit_validation', 'ep_pre_submit_validation');
function ep_pre_submit_validation() {
//simple Security check
check_ajax_referer( 'pre_publish_validation', 'security' );

//convert the string of data received to an array
//from http://wordpress.stackexchange.com/a/26536/10406
parse_str( $_POST['form_data'], $vars );

//check that are actually trying to publish a post
if ( $vars['post_status'] == 'publish' || 
    (isset( $vars['original_publish'] ) && 
     in_array( $vars['original_publish'], array('Publish', 'Schedule', 'Update') ) ) ) {
    if ( empty( $vars['_start_date'] ) || empty( $vars['_end_date'] ) ) {
        _e('Both Start and End date need to be filled');
        die();
    }
    //make sure start < end
    elseif ( $vars['_start_date'] > $vars['_end_date'] ) {
        _e('Start date cannot be after End date');
        die();
    }
    //check time is also inputted in case of a non-all-day event
    elseif ( !isset($vars['_all_day'] ) ) {
        if ( empty($vars['_start_time'] ) || empty( $vars['_end_time'] ) ) {
            _e('Both Start time and End time need to be specified if the event is not an all-day event');
            die();              
        }
        elseif ( strtotime( $vars['_start_date']. ' ' .$vars['_start_time'] ) > strtotime( $vars['_end_date']. ' ' .$vars['_end_time'] ) ) {
            _e('Start date/time cannot be after End date/time');
            die();
        }
    }
}

//everything ok, allow submission
echo 'true';
die();
}

This function returns true if everything is fine, and submits the form to publish the post by the normal channel. Otherwise, the function returns an error message that is shown as an alert(), and the form is not submitted.

share|improve this answer
    
I have followed the same approach and getting the post being saved as "Draft" instead of "Publish" when the validation function returns true. Not sure how to fix that!!!<br/>Also not getting the data for a textarea field (ex. post_content, any other text area custom field) during the ajax call? –  Mahmudur Aug 10 '12 at 3:47
    
I have applied this solution a little bit differently: first of all I have used the below code in the javascript in case of success: delayed_autosave(); //get data from textarea/tinymce field jQuery('#publish').data("valid", true).trigger('click'); //publish post Many thanks. –  Mahmudur Aug 13 '12 at 2:25
add comment

I think that the best way to go about this is not to PREVENT the status change from happening so much as it is to REVERT it if it does. For example: You hook save_post, with a really high priority (so that the hook will fire very late, namely after you do your meta insert), then check the post_status of the post that's just been saved, and update it to pending (or draft or whatever) if it doesn't meet your criteria.

An alternate strategy would be to hook wp_insert_post_data to set the post_status directly. The disadvantage to this method, as far as I'm concerned, is that you will not have inserted the postmeta into the database yet, so you will have to process it, etc in place to do your checks, then process it again to insert it into the database...which could become a lot of overhead, either in performance or in code.

share|improve this answer
    
I am currently hooking save_post with priority 1 to save the meta fields from the metaboxes; what you are proposing then is to have a second hook to save_post with priority, say, 99? Would this ensure integrity? What if for some reason the first hook fires, the metadata is inserted and the post published, but the second hook doesn't, so you end up with invalid fields? –  englebip Feb 12 '12 at 12:00
    
I can't think of a situation where the first hook would fire but not the second...what kind of scenario are you thinking that could cause that? If you're that worried about it, you can insert the post meta, check the post meta, and then update the post_status all in one function running off of a single call to a hook if you'd prefer. –  m0r7if3r Feb 12 '12 at 13:36
    
I posted my code as an edit to my question; I tried to use a second hook to save_post but that triggers an infinite loop. –  englebip Feb 12 '12 at 19:58
    
Your issue is that you should be checking the created post. So if( get_post_status( $post_id ) == 'publish' ) is what you want to be using, since you will be redefining the data in $wpdb->posts, not the data in $_POST[]. –  m0r7if3r Feb 12 '12 at 21:05
add comment

As m0r7if3r pointed out, there is no way of preventing a post from being published using the save_post hook, since the by the time that hook is fired, the post is already saved. The following, however, will allow you to revert the status without using wp_insert_post_data and without causing an infinite loop.

The following is not tested, but should work.

<?php
add_action('save_post', 'my_save_post');
function my_save_post($post_id) {
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
         return;

    if ( !isset( $_POST['ep_eventposts_nonce'] ) )
         return;

    if ( !wp_verify_nonce( $_POST['ep_eventposts_nonce'], plugin_basename( __FILE__ ) ) )
         return;

    // Is the user allowed to edit the post or page?
     if ( !current_user_can( 'edit_post', $post->ID ) )
         return;

   //Now perform checks to validate your data. 
   //Note custom fields (different from data in custom metaboxes!) 
   //will already have been saved.
    $prevent_publish= false;//Set to true if data was invalid.
    if ($prevent_publish) {
        // unhook this function to prevent indefinite loop
        remove_action('save_post', 'my_save_post');

        // update the post to change post status
        wp_update_post(array('ID' => $post_id, 'post_status' => 'draft'));

        // re-hook this function again
        add_action('save_post', 'save_post');
    }
}
?>

I've not checked, but looking at the code, the feedback message will display the incorrect message that the post was published. This is because WordPress redirects us to an url where the message variable is now incorrect.

To change it, we can use the redirect_post_location filter:

add_filter('redirect_post_location','my_redirect_location',10,2);
function my_redirect_location($location,$post_id){
    //If post was published...
    if (isset($_POST['publish'])){
        //obtain current post status
        $status = get_post_status( $post_id );

        //The post was 'published', but if it is still a draft, display draft message (10).
        if($status=='draft')
            $location = add_query_arg('message', 10, $location);
    }

    return $location;
}

To summarise the above redirect filter: If a post is set to be published, but is still a draft then we alter the message accordingly (which is message=10). Again, this is untested, but should work. The Codex of the add_query_arg suggests that when a variable is already it set, the function replaces it (but as I say, I haven't tested this).

share|improve this answer
    
Other than the missing ; on your add_query_arg line, this redirect_post_location filter trick is exactly what I needed. thanks! –  MadtownLems Apr 2 at 19:04
    
@MadtownLems fixed :) –  Stephen Harris Apr 3 at 0:18
add comment

Sorry I cant give you a straight up answer but I do recall doing something similar very recently I just cant remember exactly how. I think I maybe did it around about way - something like I had it being a default value and if the person hadn't changed it I picked this up in an if statement so -> if(category==default category) {echo "You didn't pick a category!"; return them to the post creation page; } sorry this isn't a straight up answer but hope it helps a bit.

share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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