Skip to content

Instantly share code, notes, and snippets.

@geminorum
Forked from danielbachhuber/gist:1666271
Created October 3, 2013 00:33
Show Gist options
  • Save geminorum/6802752 to your computer and use it in GitHub Desktop.
Save geminorum/6802752 to your computer and use it in GitHub Desktop.
<?php
/* Plugin Name: Grist Authors
* Description: Handles a special 'Author' post type and co-authors for posts.
* Author: Andrew Nacin
* Author URI: http://andrewnacin.com/
*/
class Grist_Authors {
static function init() {
add_filter( 'single_template', array( __CLASS__, 'single_template' ) );
add_action( 'init', array( __CLASS__, 'register_post_type' ) );
add_action( 'add_meta_boxes_author', array( __CLASS__, 'add_meta_boxes_author' ) );
add_action( 'admin_head-post-new.php', array( __CLASS__, 'header_inline' ) );
add_action( 'admin_head-post.php', array( __CLASS__, 'header_inline' ) );
add_action( 'admin_footer-post-new.php', array( __CLASS__, 'footer_inline' ) );
add_action( 'admin_footer-post.php', array( __CLASS__, 'footer_inline' ) );
add_action( 'save_post', array( __CLASS__, 'save_post_type_author' ), 10, 2 );
add_filter( 'request', array( __CLASS__, 'request' ) );
add_filter( 'post_row_actions', array( __CLASS__, 'post_row_actions' ), 10, 2 );
add_filter( 'page_row_actions', array( __CLASS__, 'post_row_actions' ), 10, 2 ); // while hierarchical = true
add_filter( 'bulk_actions-edit-author', array( __CLASS__, 'bulk_actions_author' ) );
add_filter( 'manage_post_posts_columns', array( __CLASS__, 'manage_post_posts_columns' ) );
add_filter( 'manage_author_posts_columns', array( __CLASS__, 'manage_author_posts_columns' ) );
add_action( 'manage_post_posts_custom_column', array( __CLASS__, 'manage_post_posts_custom_column' ), 10, 2 );
add_action( 'manage_author_posts_custom_column', array( __CLASS__, 'manage_author_posts_custom_column' ), 10, 2 );
add_action( 'add_meta_boxes_post', array( __CLASS__, 'add_meta_boxes_post' ) );
add_filter( 'wp_insert_post_data', array( __CLASS__, 'wp_insert_post_data' ), 10, 2 );
}
/**
* Forces author-style templates for the /author/ pages, even though these are a post type.
*
* author.php, archive.php, index.php is the template hierarchy.
*/
static function single_template( $template ) {
if ( get_queried_object()->post_type == 'author' )
return locate_template( array( 'author.php', 'archive.php', 'index.php' ) );
return $template;
}
/**
* Adds an 'Authors' column to after the 'title' field.
*
* The current 'author' column does not need to be removed, as it isn't there.
* remove_post_type_support() all standard author UI in our register_post_type() method.
*/
static function manage_post_posts_columns( $columns ) {
$new_columns = array();
foreach ( $columns as $column_key => $column_name ) {
$new_columns[ $column_key ] = $column_name;
if ( $column_key == 'title' )
$new_columns['grist_author'] = 'Authors';
}
return $new_columns;
}
/**
* Render the 'Authors' column.
*
* This caches all author data for all posts on the first run.
*/
static function manage_post_posts_custom_column( $column, $post_id ) {
global $wp_query;
static $cached_users = false;
if ( ! $cached_users ) {
$pids = array();
foreach ( $wp_query->posts as $post ) {
$pids[] = $post->ID;
}
$author_ids = array();
foreach ( $pids as $pid ) {
$author_ids = array_merge( $author_ids, (array) get_post_meta( $pid, '_grist_author_id', false ) );
}
// We don't care about the return value here, only that the posts end up
// in the cache for future get_post() calls.
get_posts( array( 'post_type' => 'author', 'include' => $author_ids, 'nopaging' => true ) );
unset( $author_ids, $pid, $pids, $post );
$cached_users = true;
}
switch ( $column ) {
case 'grist_author' :
$post = get_post( $post_id );
$authors = get_post_meta( $post_id, '_grist_author_id', false );
$output = array();
foreach ( $authors as $author_id ) {
$author = get_post( $author_id );
if ( $author->ID == $post->post_author ) {
echo '<strong>' . esc_html( $author->post_title ) . '</strong><br />';
} else {
if ( $author->post_author ) {
$output[] = '<a href="' . esc_url( add_query_arg( 'author', $author->post_author ) ) . '">' . esc_html( $author->post_title ) . '</a>';
} else {
$output[] = esc_html( $author->post_title );
}
}
}
if ( $output )
echo implode( "<br />\n", $output );
echo '<br />&nbsp;';
break;
}
}
/**
* Adds 'Twitter' and 'Linked User Account' columns to the Authors (post type) list table.
*/
static function manage_author_posts_columns( $columns ) {
unset( $columns['date'] );
$columns['twitter'] = 'Twitter';
$columns['user_account'] = 'Linked User Account';
return $columns;
}
/**
* Renders the 'Twitter' and 'Linked User Account' columns for the Authors (post type) list table.
*/
static function manage_author_posts_custom_column( $column, $post_id ) {
switch ( $column ) {
case 'twitter' :
$twitter = get_post_meta( $post_id, '_grist_author_twitter', true );
if ( $twitter )
echo '<a href="' . esc_url( 'http://twitter.com/' . $twitter ) . '">@' . esc_html( $twitter ) . '</a>';
break;
case 'user_account' :
if ( ! get_the_author_meta( 'ID' ) )
break;
// Provide direct links to the user on users.php if we can.
if ( current_user_can( 'list_users' ) ) {
$user_link = add_query_arg( 's', urlencode( get_the_author_meta( 'user_login' ) ), admin_url( 'users.php' ) ) . '#user-' . get_the_author_meta( 'ID' );
echo '<a href="' . esc_url( $user_link ) . '">' . get_the_author() . '</a>';
} else {
the_author();
}
break;
}
}
/**
* For authors (post type), no bulk actions.
*/
static function bulk_actions_author( $actions ) {
return array();
}
/**
* For authors (post type), no quick edit, trash, or delete action links.
*/
static function post_row_actions( $actions, $post ) {
if ( $post->post_type == 'author' )
unset( $actions['inline hide-if-no-js'], $actions['trash'], $actions['delete'] );
return $actions;
}
/**
* If core's /author/$author/ rewrite rule gets hit, catch it and serve up the post type instead.
*/
static function request( $qvs ) {
if ( ! is_admin() && isset( $qvs['author_name'] ) ) {
$qvs['post_type'] = 'author';
$qvs['name'] = $qvs['author_name'];
unset( $qvs['author_name'] );
}
return $qvs;
}
/**
* Register our author post type and removes support for 'author' from the post post_type.
*/
static function register_post_type() {
$labels = array(
'name' => 'Authors',
'singular_name' => 'Author',
'add_new' => 'Add Author',
'add_new_item' => 'Add New Author',
'edit_item' => 'Edit Author',
'new_item' => 'New Author',
'view_item' => 'View Author',
'search_items' => 'Search Authors',
'not_found' => 'No author found',
'not_found_in_trash' => 'Sorry, no authors found in the trash',
);
$args = array(
'labels' => $labels,
'public' => true,
'show_ui' => true,
'rewrite' => true,
'query_var' => 'author_name',
'has_archive' => false,
'hierarchical' => true, // hack, so wp_dropdown_pages() works.
'capability_type' => 'page',
'map_meta_cap' => true,
'supports' => array( 'title', 'editor', 'thumbnail' ),
'show_in_menu' => 'users.php',
);
register_post_type( 'author', $args );
remove_post_type_support( 'post', 'author' );
}
/**
* Add a meta box to our author post type.
*
* This box ends up holding Twitter and WP.com fields.
*/
static function add_meta_boxes_author( $post ) {
add_meta_box( 'grist_author_meta', 'Grist Author Meta', array( __CLASS__, 'render_meta_box_author' ), null, 'side', 'high' );
}
/**
* Adds a meta box to posts for co-author assignments.
*/
static function add_meta_boxes_post( $post ) {
add_meta_box( 'grist_co_authors', 'Authors', array( __CLASS__, 'render_meta_box_post' ), null, 'side', 'low' );
}
/**
* Render the co-authors meta box.
*/
static function render_meta_box_post( $post ) {
$authors = get_post_meta( $post->ID, '_grist_author_id', false );
$show_option_none = false;
if ( ! $authors ) {
$authors = Grist_Authors::get_author_id_by_user( get_current_user_id() );
if ( ! $authors )
$show_option_none = '(no author)';
$authors = array( $authors );
}
$authors[] = 0;
$primary = array_shift( $authors );
wp_dropdown_pages( array(
'show_option_none' => $show_option_none,
'selected' => $primary,
'name' => 'grist_author_id[]',
'id' => 'grist_author_id_0',
'post_type' => 'author',
'echo' => 1,
'sort_column' => 'post_title',
) );
$i = 1;
echo '<div class="grist-co-authors"><p>Co-authors:</p>';
foreach ( $authors as $author_id ) {
echo '<div>';
wp_dropdown_pages( array(
'show_option_none' => '(no co-author)',
'selected' => $author_id,
'name' => 'grist_author_id[]',
'id' => 'grist_author_id_' . $i,
'post_type' => 'author',
'echo' => 1,
'sort_column' => 'post_title',
) );
echo '<a href="#" class="author-remove">Remove</a>';
echo '</div>';
$i++;
}
echo '</div><p><a href="#" id="author-add">Add</a></p>';
}
/**
* Utility function fetches an author ID (post type) when given a WP.com user ID.
*/
static function get_author_id_by_user( $user_id = null ) {
if ( empty( $user_id ) )
$user_id = get_current_user_id();
$query = new WP_Query( array(
'post_type' => 'author',
'author' => $user_id,
'post_status' => 'publish',
'posts_per_page' => 1,
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
'no_found_rows' => true,
'fields' => 'ids',
) );
if ( isset( $query->posts[0] ) )
return $query->posts[0];
return 0;
}
/**
* Renders the meta box for authors (post type) to hold Twitter and WP.com user information.
*/
static function render_meta_box_author( $post ) {
$twitter = get_post_meta( $post->ID, '_grist_author_twitter', true );
echo '<p><label>Twitter Handle</label> <input style="width:200px" type="text" value="' . esc_attr( $twitter ) . '" name="grist_author[twitter]" /></p>';
echo '<p><label>WP.com User</label> ';
wp_dropdown_users( array(
'show_option_none' => '(No corresponding user)',
'name' => 'grist_author[author]',
// If we're adding an author or if there is no post author (0), then use -1 (which is show_option_none).
// We then take -1 on save and convert it back to 0. (#blamenacin)
'selected' => 'auto-draft' == $post->post_status || ! $post->post_author ? -1 : $post->post_author,
) );
echo '</p>';
}
/**
* Drops in some JS on the post edit screen to handle the co-authors UI.
*/
static function footer_inline() {
$screen = get_current_screen();
if ( 'post' !== $screen->post_type )
return;
?>
<script>
jQuery(document).ready( function($) {
var coauthors = $('.grist-co-authors');
$('#author-add').click( function(e) {
e.preventDefault();
coauthors.find('select').first().parent().clone()
.find('select').val('').end()
.find('.author-remove').show().end()
.appendTo( coauthors );
});
coauthors.delegate( '.author-remove', 'click', function(e) {
e.preventDefault();
if ( coauthors.find('select').length > 1 )
$(this).parent().remove();
else
$(this).prev().val('');
});
});
</script>
<?php
}
/**
* Adds some information to the featured image box for authors (post type)
* that clarifies what size we're looking for.
*/
static function admin_post_thumbnail_html( $content ) {
$content .= '<p>(50 pixels &times; 50 pixels, please.)</p>';
return $content;
}
/**
* Some elegant (and less elegant) hacks for the author edit screen.
*
* - Turns off the visual editor, disables the upload/insert media button.
* - Auto-corrects $parent_file and $submenu_file, though changes in 3.3 should make these redundant. (See #WP19125)
* - Hides the delete link (it doesn't need to be prevented with a cap check, only discouraged).
* - Fixes positioning and styling of the editor. Not sure if late changes in 3.3 made these unnecessary.
*/
static function header_inline() {
$screen = get_current_screen();
switch ( $screen->post_type ) :
case 'author' :
add_filter( 'admin_post_thumbnail_html', array( __CLASS__, 'admin_post_thumbnail_html' ) );
add_filter( 'user_can_richedit', '__return_false' );
remove_action( 'media_buttons', 'media_buttons' );
$GLOBALS['parent_file'] = 'users.php';
$GLOBALS['submenu_file'] = "edit.php?post_type=author";
echo '<style>.misc-pub-section, #delete-action { display: none } #content_resize { top: -2px !important } .wp-editor-container { background: #fff } </style>' . "\n";
break;
endswitch;
}
/**
* Saving the author post type, specifically Twitter. The WP.com user linkage is handled by
* our wp_insert_post_data() method.
*/
static function save_post_type_author( $post_id, $post ) {
if ( 'author' != $post->post_type )
return;
if ( ! isset( $_POST['grist_author'] ) )
return;
$twitter = $_POST['grist_author']['twitter'];
// Sanitize all kinds of possible inputs.
$twitter = str_replace( array( 'http://', 'https://', '#!', 'twitter.com', '/', '@' ), '', $twitter );
$twitter = sanitize_text_field( $twitter );
update_post_meta( $post->ID, '_grist_author_twitter', $twitter );
}
/**
* Saving the post and author post types, specifically stuff related to user IDs.
*
* We need to do this on wp_insert_post_data rather than save_post so have raw,
* unadulterated access to post_author.
*/
static function wp_insert_post_data( $post, $args ) {
switch ( $post['post_type'] ) :
case 'post' :
// New posts have an ID, this just prevents this from running on auto-draft creation.
if ( ! isset( $args['ID'] ) )
break;
// If author data was submitted:
if ( isset( $_POST['grist_author_id'] ) ) {
// Make the authors unique, remove the first one, and consider that the primary.
$ids = array_unique( array_filter( array_map( 'absint', $_POST['grist_author_id'] ) ) );
$primary = array_shift( $ids );
$author_object = get_post( $primary );
// If we have a primary author that has a corresponding WP.com user ID, then
// make the post's post_author keep the WP.com user ID. (Good karma.)
if ( $author_object && $author_object->post_author )
$post['post_author'] = $author_object->post_author;
else
$post['post_author'] = 0;
// Wipe them all out so we can re-add in order.
delete_post_meta( $args['ID'], '_grist_author_id' );
// Add the primary ID to meta, then the rest of them.
add_post_meta( $args['ID'], '_grist_author_id', $primary );
foreach ( $ids as $id ) {
add_post_meta( $args['ID'], '_grist_author_id', $id );
}
} else {
// Okay, no author data was submitted.
// Figure out what is in the DB and do not lose the linked WP.com user.
// This happens, say, during a quick edit.
$from_db = get_post( $args['ID'] );
if ( $from_db )
$post['post_author'] = $from_db->post_author;
else
$post['post_author'] = 0;
}
break;
case 'author' :
// Don't run for auto-drafts. (New posts have IDs.)
if ( ! isset( $args['ID'] ) )
break;
// First, figure out what we have in the DB as the linked WP.com user.
$from_db = get_post( $args['ID'] );
if ( $from_db )
$post['post_author'] = intval( $from_db->post_author );
else
$post['post_author'] = 0;
// If data was passed on save, then use it. But if post_author was -1
// (which is what the dropdowns use for nothing selected), we can't store
// that in an unsigned int. Clarify we want 0 for no author.
if ( isset( $_POST['grist_author'] ) ) {
$post['post_author'] = intval( $_POST['grist_author']['author'] );
if ( $post['post_author'] < 0 )
$post['post_author'] = 0;
}
break;
endswitch;
return $post;
}
}
// Go.
Grist_Authors::init();
/*
TEMPLATE TAGS
*/
/**
* Grist Byline filtering.
*
* @param string $before text before the author name
* @param string $after text after the author name
* @param array|string $args optional attributes for the link
* @param string $prefix text before the link
* Use this template tag for bylines:
* <p class="byline"><?php grist_byline(); ?></p>
*/
function grist_byline( $before = '', $after = '', $args = array(), $prefix = 'By ' ) {
$author_ids = get_post_meta( get_the_ID(), '_grist_author_id', false );
if ( empty( $author_ids ) )
return;
if( !empty( $args ))
{
$args = _parse_html_attributes( $args );
}
$output = array();
foreach ( $author_ids as $author_id ) {
$author_name = $before . get_the_title( $author_id ) . $after;
$output[] = sprintf( '<a href="%s" title="Posts by %s" %s >%s</a>', esc_url( get_permalink( $author_id ) ), esc_attr( $author_name ), $args ,$author_name );
}
echo $prefix . wp_sprintf( '%l', $output );
}
function grist_the_author_bios( $before = '<div>', $after = '</div>' ) {
if ( get_post_type() == 'author' )
$author_ids = array( get_the_ID() );
else
$author_ids = get_post_meta( get_the_ID(), '_grist_author_id', false );
foreach ( $author_ids as $author_id ) {
$author = get_post( $author_id );
echo $before . apply_filters('the_content', $author->post_content) . $after . "\n\n";
}
}
function grist_has_multiple_authors() {
return 1 < count( (array) get_post_meta( get_the_ID(), '_grist_author_id', false ) );
}
function grist_get_the_author($post_id='') {
$id = ($post_id) ?: get_the_ID();
$author_id = 'author' == get_post_type($id) ? $id : get_post_meta( $id, '_grist_author_id', true );
if( !$author_id)
return 'Grist';
return get_the_title( $author_id );
}
function grist_get_author_feed_link($post_id='') {
$id = ($post_id) ?: get_the_ID();
$author_id = 'author' == get_post_type($id) ? $id : get_post_meta( $id, '_grist_author_id', true );
return user_trailingslashit( get_permalink( $author_id ) . '/feed' );
}
// grist_get_author_meta( 'twitter' )
function grist_get_the_author_meta( $meta, $post_id='' ) {
$id = ($post_id) ?: get_the_ID();
$author_id = 'author' == get_post_type($id) ? $id : get_post_meta( $id, '_grist_author_id', true );
return get_post_meta( $author_id, '_grist_author_' . $meta, true );
}
function grist_get_author_post_id($post_id='') {
$id = ($post_id) ?: get_the_ID();
return 'author' == get_post_type($id) ? $id : get_post_meta( $id, '_grist_author_id', true );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment