. copyright (c) 2005 Scott Merrill (skippy@skippy.net) Copyright 2006, 2007 Travis Snoozy (ai2097@users.sourceforge.net) Released under the terms of the GNU GPL v2 This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ load_plugin_textdomain('in_series'); //////////////////////////////////////// /* * Takes a series name (case-sensitive string). * * Returns an array: * - Keys == post_ids * - Values == post_titles * - Ordered by _series_order * * Must be called only in the context of a post (FIXME; series should be used to * look up posts, not the other way around) * * Only returns posts by the same author as the current post (FIXME; multiple * authors should be allowed to post in the same series) * * Matches are case-insensitive (TODO: audit to make sure that we're using a * normalized series name in all the places where we might display it) */ function get_all_in_series($series = '') { global $post, $wpdb; if ('' == $series) { return; } // no sense looking for blank data $author = $post->post_author; $now = current_time('mysql'); $arrPost_IDs = $wpdb->get_col( "SELECT post_id FROM $wpdb->postmeta INNER JOIN $wpdb->posts ON $wpdb->postmeta.post_id=$wpdb->posts.ID WHERE meta_key='_series_name' AND meta_value='$series' AND $wpdb->posts.post_author = '$author' AND $wpdb->posts.post_date < '$now' AND($wpdb->posts.post_status='publish' OR $wpdb->posts.post_status='static')"); if (0 == count($arrPost_IDs)) { return; } $post_IDs = implode(',', $arrPost_IDs); $ids = $wpdb->get_col( "SELECT post_id FROM $wpdb->postmeta WHERE post_id IN ($post_IDs) AND meta_key='_series_order' ORDER BY CAST(meta_value AS SIGNED) ASC"); // now prepare an array of empty items ordered by post ID $this_series = array(); foreach ($ids as $this_id) { $this_series[$this_id] = ''; } $s = implode(',', $ids); $series_posts = $wpdb->get_results( "SELECT ID, post_title FROM $wpdb->posts WHERE ID IN ($s)", OBJECT); // now populate the array of posts with data foreach ($series_posts as $s_post) { $this_series[intval($s_post->ID)] = $s_post->post_title; } return $this_series; } /////////////////////////////////// /* * Takes a separator (string), prefix (string), and suffix (string) * * Returns nothing * * Outputs an HTML block of the format: * $before post title[$sepID, '_series_name', true)); if ('' == $series) { return null; } $series_posts = get_all_in_series($series); $output = ''; $count = 1; foreach ($series_posts as $s_id => $s_title) { if ('' != $output) { $output .= $sep; } if ($s_id == $post->ID) { $output .= $count; } else { $output .= "$count"; } $count++; } if($before === false) { $before = __('« Read the whole series:', 'in_series')." "; } $output = $before . $output . $after; echo $output; } /////////////////////////// /* * Takes a an order (string; "id" or "name"), and a published state (bool) * * Returns an array: * - Keys == post_id of the last/first post (for the series in the value) * - Values == series names (unique) * - Ordered by: * - If "id" is passed, the series are ordered (roughly) by the series * whose latest post has the highest post ID. The key/value pairs relate * to the LAST post in the series. * - Otherwise, the series are returned in alphabetical order. The * key/value pairs relate to the FIRST post in the series. * * If no series exist, returns FALSE (FIXME?) * * Returns ALL series. However, if two authors use the same name for a series, * this function will treat them as the same series, although other functions * will treat them as separate series. (FIXME) * * If "published" is true, then only posts that are marked as published are * returned. If a series has no posts that are marked published, then * get_all_series will not have an entry for that series in its return array. If * "published" is false, then the published status is ignored when creating the * return array. * */ function get_all_series($order = 'name', $published = true) { global $wpdb; $inner_order = ''; $outer_order = ''; if ('id' == $order) { $inner_order='DESC'; $outer_order='CAST(d.post_id AS SIGNED) DESC'; } else { $inner_order='ASC'; $outer_order='NULL'; } $filter_unpublished=""; if($published !== false) { $filter_unpublished= " JOIN (SELECT ID FROM $wpdb->posts WHERE post_status='publish') c ON c.ID=a.post_id"; } // FIXME: Ordering by post_id probably doesn't work as expected. $result = $wpdb->get_results( "SELECT d.post_id,d.meta_value FROM (SELECT a.post_id,a.meta_value FROM (SELECT post_id,meta_value FROM $wpdb->postmeta WHERE meta_key='_series_name') a JOIN (SELECT post_id,meta_value FROM $wpdb->postmeta WHERE meta_key='_series_order') b ON a.post_id=b.post_id $filter_unpublished ORDER BY a.meta_value,CAST(b.meta_value AS SIGNED) $inner_order) d GROUP BY d.meta_value ORDER BY $outer_order"); if ( (! $result) || ( count($result) == 0 ) ) { return FALSE; } $all = Array(); foreach ($result as $r) { $all["{$r->post_id}"] = $r->meta_value; } return $all; } /////////////////////// /* * Takes a prefix (string), a suffix (string), and an order (string; "id" or * "name") * * Returns nothing * * Outputs an HTML block of the following format: * $before
  • post title
  • * $after * (FIXME; hard-coded LIs are not good) * * If no series exist, outputs nothing * * Same problem with per-author series differentiation as get_all_series (FIXME) * */ function all_series($before = '
      ', $after = '
    ', $order = 'name') { $all_series = get_all_series($order); if (! $all_series) { return; } $output = $before; foreach ($all_series as $id => $title) { $output .= "
  • $title
  • "; } $output .= $after; echo $output; } ///////////////////////////////////// /* * Takes a (CSS-style) class (string), a prfix (string), and a suffix (string) * * Returns nothing * * Outputs an HTML block of the following format: * $before
  • post * title
  • $after * (FIXME; hard-coded LIs are not good) * * Ordered by _series_order * * Must be called in the context of a post (FIXME) * * Outputs nothing if the current post is not in a series * * Outputs nothing UNLESS it is called from the context of a "single" article * (FIXME) * */ function series_table_of_contents($class='', $before = '
      ', $after = '
    ') { global $post; if (! is_single() ) { return; } echo get_series_table_of_contents($post->ID, $class, $before, $after); } /* * Takes a post ID, (CSS-style) class (string), a prfix (string), and a suffix (string) * * Returns an HTML block (string) of the following format: * $before
  • post * title
  • $after * (FIXME; hard-coded LIs are not good) * * Ordered by _series_order * * Returns nothing if the post (associated with the given id) is not in a series * */ function get_series_table_of_contents($id, $class = '', $before = '
      ', $after = '
    ') { $series = strtolower(get_post_meta($id, '_series_name', true)); if ('' == $series) { return null; } $series_posts = get_all_in_series($series); $output = $before; foreach ($series_posts as $s_id => $s_title) { $output .= '"; } $output .= $s_title; if ($s_id != $post->ID) { $output .= ''; } $output .= ''; } $output .= $after; return $output; } //////////////////////////////// /* * INTERNAL USE ONLY * * Takes a post id (number) and a direction (string; 'next' or 'previous') * * Returns an object representing the adjacent post in the series (next or * previous, depending on the direction passed): * - ID (string) indicates the post_id * - post_title (string) indicates the post_title * * If the post indicated by id is not in a series, returns nothing * * If there is not an adjacent post in the direction specified, returns nothing * */ function get_adjacent_in_series($id, $direction) { global $wpdb; $series = get_post_meta($id, '_series_name', true); if (empty($series)) { return null; } if ($direction == "next") { $rank = ">"; $order = "ASC"; } else if ($direction == "previous") { $rank = "<"; $order = "DESC"; } else { return null; } $position = get_post_meta($id, '_series_order', true); $posts_in_series = implode(',', $wpdb->get_col( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_series_name' AND meta_value='$series'")); $author = get_post($id); if($author) { $author = $author->post_author; } $now = current_time('mysql'); $adjacent_id = $wpdb->get_var( "SELECT post_id FROM $wpdb->postmeta INNER JOIN $wpdb->posts ON $wpdb->postmeta.post_id=$wpdb->posts.ID WHERE post_id IN ($posts_in_series) AND meta_key='_series_order' AND CAST(meta_value AS SIGNED) $rank $position AND $wpdb->posts.post_author = '$author' AND $wpdb->posts.post_date < '$now' AND ($wpdb->posts.post_status='publish' OR $wpdb->posts.post_status='static') ORDER BY CAST(meta_value AS SIGNED) $order LIMIT 1"); if ($adjacent_id) { return @$wpdb->get_row( "SELECT ID, post_title FROM $wpdb->posts WHERE ID = $adjacent_id LIMIT 1"); } else { return null; } } //////////////////////////////// /* * Takes an optional "single only" indicator (bool; defaults to true) * * Returns an object representing the previous post in the series: * - ID (string) indicates the post_id * - post_title (string) indicates the post_title * * Must be called in the context of a post (FIXME) * * If the current post is not in a series, returns nothing * * If the current post is the first in its series, returns nothing * * If single_only is true, and the function is not called in the context of a * "single" post, returns nothing * * If the current post does not belong to a series, returns nothing * */ function get_previous_in_series($single_only = true) { global $post; if(! is_single() && $single_only) { return null; } return get_adjacent_in_series($post->ID, "previous"); } ///////////////////////////// /* * Takes an optional "single only" indicator (bool; defaults to true) * * Returns an object representing the next post in the series: * - ID (string) indicates the post_id * - post_title (string) indicates the post_title * * Must be called in the context of a post (FIXME) * * If the current post is not in a series, returns nothing * * If the current post is the last in its series, returns nothing * * If single_only is true, and the function is not called in the context of a * "single" post, returns nothing * * If the current post does not belong to a series, returns nothing * */ function get_next_in_series($single_only = true) { global $post; if(! is_single() && $single_only) { return null; } return get_adjacent_in_series($post->ID, "next"); } ///////////////////////////// /* * Takes a format (string; surrounding text) and a link (string; goes between * and ) * * %link in the format is replaced with the link () * %title in the link is replaced with the post_title for the next post * * Returns nothing * * Outputs HTML as described by the format parameter. Any %link has an href * attribute referring to the previous post. * * Must be called in the context of a post (FIXME) * * If the current post is not in a series, outputs nothing * * If the current post is the first post in the series, outputs nothing * * If the function is not called within the context of a "single" post, outputs * nothing (FIXME) * */ function previous_in_series($format='« %link', $link='%title') { $p_post = get_previous_in_series(); if(! $p_post) { return; } $title = apply_filters('the_title', $p_post->post_title, $p_post); $string = ''; $link = str_replace('%title', $title, $link); $link = $string . $link . ''; $format = str_replace('%link', $link, $format); echo $format; } ///////////////////////// /* * Takes a format (string; surrounding text) and a link (string; goes between * and ) * * %link in the format is replaced with the link () * %title in the link is replaced with the post_title for the next post * * Returns nothing * * Outputs HTML as described by the format parameter. Any %link has an href * attribute referring to the next post. * * Must be called in the context of a post (FIXME) * * If the current post is not in a series, outputs nothing * * If the current post is the last post in the series, outputs nothing * * If the function is not called within the context of a "single" post, outputs * nothing (FIXME) * */ function next_in_series($format='%link »', $link='%title') { $n_post = get_next_in_series(); if(! $n_post) { return; } $title = apply_filters('the_title', $n_post->post_title, $n_post); $string = ''; $link = str_replace('%title', $title, $link); $link = $string . $link . ''; $format = str_replace('%link', $link, $format); echo $format; } ///////////////////////// function display_manage_posts_in_series_column($colname, $id) { if(! $colname || $colname != "in_series") { return; } echo get_post_meta($id, '_series_name', true); } add_action('manage_posts_custom_column', 'display_manage_posts_in_series_column', 10, 2); ///////////////////////// function add_manage_posts_in_series_column($posts_columns) { $posts_columns['in_series'] = __('Series', 'in_series'); return $posts_columns; } add_filter('manage_posts_columns', 'add_manage_posts_in_series_column'); ///////////////////////// function add_write_post_in_series_sidebar() { global $post; $series = get_post_meta($post->ID, '_series_name', true); ?>

    :

    $series"; } ?>
    query( "DELETE FROM $wpdb->postmeta WHERE post_id='$id' AND (meta_key='_series_name' OR meta_key='_series_order')"); //XXX: This means you should NOT have a series called 'delete'. if($series == 'delete') { return; } $sort = "DESC"; $offset = 1; if($place != 'append') { $sort = "ASC"; $offset = -1; } //SQL query to get largest/smallest _series_order value. $series_order = intval($wpdb->get_var( "SELECT sorder.meta_value FROM $wpdb->postmeta AS sorder INNER JOIN $wpdb->postmeta AS sname ON sorder.post_id=sname.post_id WHERE sname.meta_key='_series_name' AND sorder.meta_key='_series_order' ORDER BY CAST(sorder.meta_value AS SIGNED) $sort LIMIT 1")); $series_order += $offset; // Don't allow a series order of 0; skip to 1 or -1. if($series_order == 0) { $series_order += $offset; } //SQL query to insert _series_name and _series_order values. $wpdb->query( "INSERT INTO $wpdb->postmeta (post_id,meta_key,meta_value,meta_id) VALUES ($id, '_series_name', '$series', DEFAULT), ($id, '_series_order', '$series_order', DEFAULT)"); } add_action('save_post', 'process_save_in_series'); ///////////////////////// // Convert old in-series metadata to the new format. function in_series_pre_2_0_metadata_convert() { global $wpdb; $wpdb->query( "UPDATE $wpdb->postmeta SET meta_key='_series_name' WHERE meta_key='series_name'"); $wpdb->query( "UPDATE $wpdb->postmeta SET meta_key='_series_order' WHERE meta_key='series_order'"); } function initialize_in_series_options() { $in_series_opts = get_option('in_series'); $series = get_all_series(); $value = Array(); $value['in_series_version'] = '2.2'; $value['meta_links'] = empty($series); $value['title_prefix'] = false; $value['toc'] = empty($series); $value['single_view_links'] = empty($series); $value['multi_view_links'] = empty($series); $value['toc_title'] = __('Article Series - %series', 'in_series'); $value['toc_position'] = 'bottom'; $value['prev_text'] = __('Previous in series', 'in_series'); $value['next_text'] = __('Next in series', 'in_series'); if(empty($in_series_opts)) { add_option('in_series', $value, __('Determines how In Series stores and displays series data', 'in_series')); $in_series_opts = $value; } } function initialize_in_series() { in_series_pre_2_0_metadata_convert(); initialize_in_series_options(); } add_action('activate_in-series.php', 'initialize_in_series'); /******************************************************************************\ * Hooks & UI for automatic series hyperlink insertion * \******************************************************************************/ function insert_in_series_doc_rel_links() { $options = get_option('in_series'); if(!is_single() || !$options['meta_links']) { return; } $prev = get_previous_in_series(false); $next = get_next_in_series(false); if($prev) { echo ''; } if($next) { echo ''; } } add_action('wp_head', 'insert_in_series_doc_rel_links'); function append_in_series_hyper_rel_links($content) { $options = get_option('in_series'); if(is_single()) { if(!$options['single_view_links']) { return $content; } } else { if(!$options['multi_view_links']) { return $content; } } $prev = get_previous_in_series(false); $next = get_next_in_series(false); if(!$prev && !$next) { return $content; } $content .= '

    '; if($prev) { $content .= ' '; } if($next) { $content .= ''; } $content .= '

    '; return $content; } function add_in_series_toc_block($content) { global $post; $options = get_option('in_series'); $series = get_post_meta($post->ID, '_series_name', true); if(empty($series) || !is_single() || !$options['toc']) { return $content; } $title = str_replace("%series", $series, $options['toc_title']); $toc = ""; $toc .= "

    $title

    "; $toc .= get_series_table_of_contents($post->ID); $toc .= "
    "; if($options['toc_position'] == 'top') { $content = $toc . $content; } else { $content .= $toc; } return $content; } function in_series_linker_content_filter($content) { $content = add_in_series_toc_block($content); $content = append_in_series_hyper_rel_links($content); return $content; } add_filter('the_content', 'in_series_linker_content_filter'); function prepend_in_series_title($title) { global $wpdb, $post; $options = get_option('in_series'); if(!$options['title_prefix']) { return $title; } $series = $wpdb->get_var("SELECT meta_value FROM $wpdb->postmeta WHERE meta_key='_series_name' AND post_id=".$post->ID.";"); if(!$series) { return $title; } return "[$series] $title"; } add_filter('the_title', 'prepend_in_series_title'); function in_series_linker_subpanel() { $in_series_opts = get_option('in_series'); if(isset($_POST['in_series']['update'])) { $in_series_opts['meta_links'] = isset($_POST['in_series']['meta_links']); $in_series_opts['title_prefix'] = isset($_POST['in_series']['title_prefix']); $in_series_opts['toc'] = isset($_POST['in_series']['toc']); $in_series_opts['single_view_links'] = isset($_POST['in_series']['single_view_links']); $in_series_opts['multi_view_links'] = isset($_POST['in_series']['multi_view_links']); if(isset($_POST['in_series']['toc_position'])) { if($_POST['in_series']['toc_position'] == 'top') { $in_series_opts['toc_position'] = 'top'; } else { $in_series_opts['toc_position'] = 'bottom'; } } if(isset($_POST['in_series']['prev'])) { $in_series_opts['prev_text'] = htmlspecialchars($_POST['in_series']['prev']); } if(isset($_POST['in_series']['next'])) { $in_series_opts['next_text'] = htmlspecialchars($_POST['in_series']['next']); } if(isset($_POST['in_series']['toc_title'])) { $in_series_opts['toc_title'] = htmlspecialchars($_POST['in_series']['toc_title']); } update_option('in_series', $in_series_opts); } $toc = $in_series_opts['toc'] ? "checked='checked'" : ""; $meta_links = $in_series_opts['meta_links'] ? "checked='checked'" : ""; $title_prefix = $in_series_opts['title_prefix'] ? "checked='checked'" : ""; $single_view_links = $in_series_opts['single_view_links'] ? "checked='checked'" : ""; $multi_view_links = $in_series_opts['multi_view_links'] ? "checked='checked'" : ""; $toc_top = $in_series_opts['toc_position'] == 'top' ? "selected='selected'" : ""; $toc_bottom = empty($toc_top) ? "selected='selected'" : ""; $singleview_label = __('Show links in single view:', 'in_series'); $multiview_label = __('Show links in multi views:', 'in_series'); $prevtext_label = __('Previous link text:', 'in_series'); $nexttext_label = __('Next link text:', 'in_series'); $toc_label = __('Insert ToC:', 'in_series'); $toctitle_label = __('ToC title:', 'in_series'); $toclocation_label = __('ToC location:', 'in_series'); $top = __('Top', 'in_series'); $bottom = __('Bottom', 'in_series'); $metalinks_label = __('Insert meta links:', 'in_series'); $seriestitleprefix_label = __('Insert series title prefix:', 'in_series'); $update_label = __('Update', 'in_series'); $output = "

    In Series

    "; echo $output; } function in_series_add_admin_panels() { add_options_page('In Series Configuration', 'Series', 8, basename(__FILE__), 'in_series_linker_subpanel'); } add_action('admin_menu', 'in_series_add_admin_panels'); ?>