.
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
* $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 .= "
";
}
$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
$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
$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 .= $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);
?>
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 .= '