4

I have a custom post type called Event which is registered like this:

add_action('init', 'register_custom_post_types');

function register_custom_post_types() {
   $event_capabilities = array(
      'publish_posts' => 'publish_events',
      'edit_posts' => 'edit_events',
      'edit_others_posts' => 'edit_others_event',
      'delete_posts' => 'delete_events',
      'delete_published_posts' => 'delete_published_events',
      'delete_others_posts' => 'delete_others_events',
      'read_private_posts' => 'read_private_events',
      'edit_post' => 'edit_event',
      'delete_post' => 'delete_event',
      'read_post' => 'read_event',
   );
   register_post_type('event',
      array(
        'labels' => array(
            'name' => __( 'Event' )
         ),
        'rewrite' => 'event',
        'public' => true,
        'has_archive' => true,
        'show_ui' => true,
        'menu_position' => 8,
        'capability_type' => array('event', 'events'),
        'capabilities' => $event_capabilities,
        'supports' => array('title', 'thumbnail', 'page-attributes'),
        'map_meta_cap' => true,
        'hierarchical' => true,
      )
   );
}

I would like to be able to set an event as the parent of a ordinary page. In other words I would like all my events to show up in the Parent select element under the Page Attributes.

I have read many posts on how to set a CPT as the child of a page, but not the other way around.

  • See if this helps In my previous answer I didn't use the same naming conventions as you cited above but the subject matter was nearly identical. – dswebsme Oct 2 '15 at 16:54
  • 1
    Be aware: $event_capabilities is already an array and later you are writing "'capabilities' => array($event_capabilities) instead of 'capabilities' => $event_capabilities. Weren't you getting an "illegal offset type" warning? – Daniel Muñoz Parsapoormoghadam Oct 2 '15 at 17:25
  • I did the same thing on a few occasions.. Gimme some time and I'll post a working solution. – Abhik Oct 3 '15 at 6:31
  • @Abhik - That would be really nice. – Cyclonecode Oct 3 '15 at 8:44
  • @DanielMuñozParsapoormoghadam - No I did not get any warnings and changing the code did not change the behavior, but still thanks for noticing. – Cyclonecode Oct 3 '15 at 8:55
5
+50

The challenge we try to take on here is how to:

  • not remove or replace the current Page Attribute meta box

  • keep the hierarchical information in the drop-down.

We assume we're on the single page edit screen, and want to add the event post type to the page parent drop-down.

The drop-down is displayed using the wp_dropdown_pages() function, that calls the get_pages() function. It only supports a single post type.

Here are two ways to do this:

Method #1 - Modify SQL

Here's an experimental way, since it deals with SQL modifications.

Since there's no obvious way to filter the generated SQL query in get_pages(), we can use the general query filter to our needs.

To make the dropdown more user friendly, when it contains titles from multiple post types, we use the list_pages filter, to prepend the post type info.

Example:

So instead of displaying the options like:

"(no parent)"
"About the Music Hall"
    "History"
    "Location"
"Concerts"
    "Of Monsters and Men"
    "Vienna Philharmonic Orchestra"
"Tickets"

we get:

"(no parent)"
"page - About the Music Hall"
    "page  - History" 
    "page  - Location"
"event - Concerts"
    "event - Of Monsters and Men"
    "event - Vienna Philharmonic Orchestra"
"page - Tickets"

Demo plugin:

Here's the code snippet that could be placed in a plugin for testing:

/**
 * Add the hierarchical 'event' post type, to the 'page' parent drop-down.
 *
 * @link http://wordpress.stackexchange.com/a/204439/26350
 */ 
is_admin() && add_filter( 'page_attributes_dropdown_pages_args', function( $dropdown_args, $post )
{
     // Only do this for the 'page' post type on the single edit 'page' screen
     if( 'page' === get_current_screen()->id && 'page' === $post->post_type )
     {
        // Modify the options' titles
        add_filter( 'list_pages', 'wpse_add_post_type_info_in_options', 10, 2 );

        // Modify the get_pages() query
        add_filter( 'query', function( $sql )
        {
            // Only run this once
            if( ! did_action( 'wpse_change_cpt' ) )
            {
                do_action( 'wpse_change_cpt' );

                // Adjust the post type part of the query - add the 'event' post type
                $sql = str_replace( 
                    "post_type = 'page' ", 
                    "post_type IN ( 'event', 'page' ) ", 
                    $sql 
                );
            }
            return $sql;
        } );
    }
    return $dropdown_args;
}, 10, 2 );

where:

function wpse_add_post_type_info_in_options ( $title, $page )
{
    return $page->post_type . ' - ' . $title;
}

and a little bit of clean up:

add_filter( 'wp_dropdown_pages', function( $output )
{
    if( did_action( 'wpse_change_cpt' ) )
        remove_filter( 'list_pages', 'wpse_add_post_type_info_in_options', 10, 2 );

    return $output;
} );

Method #2 - Only use wp_dropdown_pages()

Here we letwp_dropdown_pages() run twice and then merge it into a single drop-down; once for the page post type and again for the event post type:

/**
 * Add the hierarchical 'event' post type, to the 'page' parent drop-down.
 *
 * @link http://wordpress.stackexchange.com/a/204439/26350
 */ 
is_admin() && add_filter( 'page_attributes_dropdown_pages_args', 'wpse_attributes', 99, 2 );

function wpse_attributes( $dropdown_args, $post )
{
    // Run this filter callback only once
    remove_filter( current_filter(), __FUNCTION__, 99 );

    // Only do this for the 'page' post type on the edit page screen
    if( 'page' === get_current_screen()->id && 'page' === $post->post_type )
    {
        // Modify the input arguments, for the 'event' drop-down
        $modified_args = $dropdown_args;
        $modified_args['post_type'] = 'page';
        $modified_args['show_option_no_change'] = __( '=== Select Events here below: ===' );            
        $modified_args['show_option_none'] = false;

        // Add the 'event' drop-down
        add_filter( 'wp_dropdown_pages', function( $output ) use ( $modified_args )
        {
            // Only run it once
            if( ! did_action( 'wpse_dropdown' ) )
            {
                do_action( 'wpse_dropdown' );

                // Create our second drop-down for events
                $output .= wp_dropdown_pages( $modified_args );

                // Adjust the output, so we only got a single drop-down
                $output = str_replace( 
                    [ "<select name='parent_id' id='parent_id'>", "</select>"],
                    '', 
                    $output 
                );
                $output = "<select name='parent_id' id='parent_id'>" . $output . "</select>";
            }
            return $output;
        } );
    }
    return $dropdown_args;
}

Here the two hierarchical drop-downs are separated by the empty === Select Events here below: === option.

Example:

"(no parent)"
"About the Music Hall"
    "History"
    "Location"
"Tickets"
"=== Select Events here below: ==="
"Concerts"
    "Of Monsters and Men"
    "Vienna Philharmonic Orchestra"

Adjusting the main query

Assume now that we create a page called Of Monsters And Men - Info with the omam-info slug and select the event Of Monsters And Men as the parent, with the omam slug.

Then the path would be

example.tld/omam/omam-info

but this gives a 404 error. The reason is that the get_page_path() check inside the \WP_Query class, for the main query, fails:

if ( '' != $qv['pagename'] ) {
    $this->queried_object = get_page_by_path($qv['pagename']);
    if ( !empty($this->queried_object) )
        $this->queried_object_id = (int) $this->queried_object->ID;
    else
        unset($this->queried_object);

This is because here get_page_by_path() only checks for the page and attachment post types, not the event post type. Unfortunately there's no explicit filter to change that.

We could of course use the query filter, like we did above, but let's try another workaround.

We can try to adjust the un-assigned properties queried_object_id and queried_object_id of the \WP_Query object with:

/**
 * Support for page slugs with any kind event parent hierarchy
 *
 * @link http://wordpress.stackexchange.com/a/204439/26350
 */
add_action( 'pre_get_posts', function( \WP_Query $q )
{
    if( 
            ! is_admin()               // Front-end only
        &&  $q->is_main_query()        // Target the main query
        &&  $q->get( 'pagename' )      // Check for pagename query variable
        &&  ! $q->get( 'post_type' )   // Target the 'page' post type
        && $page = get_page_by_path( $q->get( 'pagename' ), OBJECT, [ 'page', 'event', 'attachment' ] ) 
    ) {
        if( is_a( $page, '\WP_Post' ) )
        {                
            $q->queried_object_id = $page->ID;
            $q->queried_object = $page;
        }
    }
} );

This should also support any number of event parents, like the following hierarchy:

ecample.tld/event-grandparent/event-parent/event-child/page-slug

Note

For the parent drop-downs in the edit.php screen, we might use the quick_edit_dropdown_pages_args filter, instead of the page_attributes_dropdown_pages_args filter we used above.

  • I haven't yet tried out your solution, but it seems very promising. Think this will be an answer that other people really will find useful if they looking for the same thing. – Cyclonecode Oct 3 '15 at 8:46
  • Hopefully this can be of any use, but it would be easier if get_pages() would support multiple post types ;-) @Cyclone – birgire Oct 3 '15 at 10:48
  • I'm now adding support to link my cpt as a parent using your second method, which works fine. The strange thing is that I now get a 404 when trying to access a page that is linked this way. My guess would be that this has something to do with the rewrite rule for the cpt? – Cyclonecode Oct 8 '15 at 17:19
  • 1
    Yes the get_page_path() check is most likely the reason for your 404. So you should try out the extra code in the "Adjusting the main query" part of the answer. Here's another question that's somewhat related, where we work around the limitations of the get_page_path() check too. @Cyclone – birgire Oct 8 '15 at 17:26
  • Ah, I didn't see that you had updated your answer. Thank you very much, I'm convinced that this will work. – Cyclonecode Oct 8 '15 at 21:32
1

Fisrt, you need to remove the core Page Attributes Meta Box, then add your own keeping everything intact except altering the parent post type. Here are my codes, tweaked to work with your events post type. Tried to make it copy/paste ready, however, if you encounter errors, please let me know.

/* Remove the core Page Attribute Metabox */
function event_remove_pa_meta_boxes() {
  remove_meta_box( 'pageparentdiv', 'page', 'side' );
}
add_action( 'do_meta_boxes', 'event_remove_pa_meta_boxes' );

/* Set the Page Attribute Metabox again*/
function events_pa_meta_box( $post ) {

    add_meta_box(
        'event-select',
        __( 'Page Attributes', 'textdomain' ),
        'events_selectors_box',
        'page',
        'side',
        'core'
    );
}
add_action( 'add_meta_boxes_page', 'events_pa_meta_box' );

/* Recreate the meta box. */
function events_selectors_box( $post ) {

    /* Set Events as Post Parent */
    $parents = get_posts(
        array(
            'post_type'   => 'event', 
            'orderby'     => 'title', 
            'order'       => 'ASC', 
            'numberposts' => -1 
        )
    );

    echo '<p><strong>Parent</strong></p><label class="screen-reader-text" for="parent_id">Parent</label>';
    if ( !empty( $parents ) ) {

        echo '<select id="parent_id" name="parent_id">';

        foreach ( $parents as $parent ) {
            printf( '<option value="%s"%s>%s</option>', esc_attr( $parent->ID ), selected( $parent->ID, $post->post_parent, false ), esc_html( $parent->post_title ) );
        }

        echo '</select>';

    } else {

        echo '<p>Please <a href="' . admin_url( "post-new.php?post_type=event" ) . '">create an event first</a>.</p>';

    }

    /* Page Templates */

    if ( 'page' == $post->post_type && 0 != count( get_page_templates( $post ) ) && get_option( 'page_for_posts' ) != $post->ID ) {

        $template = !empty($post->page_template) ? $post->page_template : false;

        echo '<p><strong>Template</strong></p><label class="screen-reader-text" for="page_template">Page Template</label>';

        echo '<select id="page_template" name="page_template">';

        $default_title = apply_filters( 'default_page_template_title',  __( 'Default Template' ), 'meta-box' );

        echo '<option value="default">' . esc_html( $default_title ) . '</option>'      

            page_template_dropdown($template);

        echo '</select>';

    }

    /* Page Order */
    echo '<p><strong>' . _e('Order') .'</strong></p>';

    echo '<label class="screen-reader-text" for="menu_order">'. _e('Order') . '</label><input name="menu_order" type="text" size="4" id="menu_order" value="'. esc_attr($post->menu_order) .'" />';

    /* Help Paragraph */
    if ( 'page' == $post->post_type && get_current_screen()->get_help_tabs() ) { ?>
        echo '<p>' . _e( 'Need help? Use the Help tab in the upper right of your screen.' ) . '</p>';
    }
}  

EDIT: Just noticed that there's filter available to modify the default args in page attribute meta box:

$dropdown_args = array(
    'post_type'        => $post->post_type,
    'exclude_tree'     => $post->ID,
    'selected'         => $post->post_parent,
    'name'             => 'parent_id',
    'show_option_none' => __('(no parent)'),
    'sort_column'      => 'menu_order, post_title',
    'echo'             => 0,
);

$dropdown_args = apply_filters( 'page_attributes_dropdown_pages_args', $dropdown_args, $post );  

So, there could be a super easy way to do what you want.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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