Index: modules/book/book.info =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.info,v retrieving revision 1.4 diff -u -p -r1.4 book.info --- modules/book/book.info 8 Jun 2007 05:50:53 -0000 1.4 +++ modules/book/book.info 18 Jul 2007 01:16:29 -0000 @@ -1,6 +1,6 @@ ; $Id: book.info,v 1.4 2007/06/08 05:50:53 dries Exp $ name = Book -description = Allows users to collaboratively author a book. +description = Allows users to structure site pages in a hierarchy or outline. package = Core - optional version = VERSION core = 6.x Index: modules/book/book.install =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.install,v retrieving revision 1.7 diff -u -p -r1.7 book.install --- modules/book/book.install 25 May 2007 12:46:43 -0000 1.7 +++ modules/book/book.install 18 Jul 2007 01:16:29 -0000 @@ -7,12 +7,199 @@ function book_install() { // Create tables. drupal_install_schema('book'); + // Add the node type. + _book_install_type_create(); } /** * Implementation of hook_uninstall(). */ function book_uninstall() { + // Delete menu links. + db_query("DELETE FROM {menu_links} WHERE module = 'book'"); + menu_cache_clear_all(); // Remove tables. drupal_uninstall_schema('book'); } + +function _book_install_type_create() { + // Create an additional node type + $book_node_type = array( + 'type' => 'book', + 'name' => t('Book page'), + 'module' => 'node', + 'description' => t("A static page. These posts (as well as other types) may be added to a book outline to create a hierarchical structure for your site."), + 'custom' => TRUE, + 'modified' => TRUE, + 'locked' => FALSE, + ); + + $book_node_type = (object)_node_type_set_defaults($book_node_type); + node_type_save($book_node_type); + // Default to not promoted. + variable_set('node_options_book', array('status')); + // Use this default type for adding content to books. + variable_set('book_allowed_types', array('book')); + variable_set('book_child_type', 'book'); +} + +/** + * Drupal 5.x to 6.x update. + */ +function book_update_6000() { + $ret = array(); + + // Set up for a multi-part update. + if (!isset($_SESSION['book_update_6000'])) { + + $schema['book'] = array( + 'fields' => array( + 'mlid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'book_id' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + ), + 'indexes' => array( + 'nid' => array('nid'), + 'book_id' => array('book_id') + ), + 'primary key' => array('mlid'), + ); + // Add the node type. + _book_install_type_create(); + + // Determine whether there are any existing nodes in the book hierarchy. + if (db_result(db_query("SELECT COUNT(*) FROM {book}"))) { + // Temporary table for the old book hierarchy; we'll discard revision info. + $schema['book_temp'] = array( + 'fields' => array( + 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'parent' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + 'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny') + ), + 'indexes' => array( + 'parent' => array('parent') + ), + 'primary key' => array('nid'), + ); + + db_create_table($ret, 'book_temp', $schema['book_temp']); + + // Insert each node in the old table into the temporary table. + $ret[] = update_sql("INSERT INTO {book_temp} (nid, parent, weight) SELECT b.nid, b.parent, b.weight FROM {book} b INNER JOIN {node} n on b.vid = n.vid"); + $ret[] = update_sql("DROP TABLE {book}"); + + db_create_table($ret, 'book', $schema['book']); + + $_SESSION['book_update_6000_orphans']['from'] = 0; + $_SESSION['book_update_6000'] = array(); + $result = db_query("SELECT * from {book_temp} WHERE parent = 0"); + + // Collect all books - top-level nodes. + while ($a = db_fetch_array($result)) { + $_SESSION['book_update_6000'][] = $a; + } + $ret['#finished'] = FALSE; + return $ret; + } + else { + // No exising nodes in the hierarchy, so drop the table and re-create it. + $ret[] = update_sql("DROP TABLE {book}"); + db_create_table($ret, 'book', $schema['book']); + return $ret; + } + } + elseif ($_SESSION['book_update_6000_orphans']) { + // Do the first batched part of the update - collect orphans. + $update_count = 400; // Update this many at a time + + $result = db_query_range("SELECT * FROM {book_temp}", $_SESSION['book_update_6000_orphans']['from'], $update_count); + + if (db_num_rows($result)) { + $_SESSION['book_update_6000_orphans']['from'] += $update_count; + } + else { + // Done with this part + if (!empty($_SESSION['book_update_6000_orphans']['book'])) { + // The orphans' parent is added last, so it will be processed first. + $_SESSION['book_update_6000'][] = $_SESSION['book_update_6000_orphans']['book']; + } + $_SESSION['book_update_6000_orphans'] = FALSE; + } + // Go through the next $update_count book pages and locate the orphans. + while ($book = db_fetch_array($result)) { + if ($book['parent'] && !db_result(db_query("SELECT COUNT(*) FROM {book_temp} WHERE nid = %d", $book['parent']))) { + if (empty($_SESSION['book_update_6000_orphans']['book'])) { + // The first orphan becomes the parent for all other orphans. + $book['parent'] = 0; + $_SESSION['book_update_6000_orphans']['book'] = $book; + $ret[] = array('success' => TRUE, 'query' => t('Relocated orphan book pages.')); + } + else { + // Re-assign the parent value of the book, and add it to the stack. + $book['parent'] = $_SESSION['book_update_6000_orphans']['book']['nid']; + $_SESSION['book_update_6000'][] = $book; + } + } + } + $ret['#finished'] = FALSE; + return $ret; + } + else { + // Do the next batched part of the update + $update_count = 100; // Update this many at a time + + while ($update_count && $_SESSION['book_update_6000']) { + // Get the last node off the stack. + $book = array_pop($_SESSION['book_update_6000']); + + $result = db_query("SELECT * FROM {book_temp} WHERE parent = %d", $book['nid']); + while ($a = db_fetch_array($result)) { + $_SESSION['book_update_6000'][] = $a; + } + + if ($book['parent']) { + // If its not a top level page, get its parent's mlid. + $parent = db_fetch_array(db_query("SELECT b.mlid AS plid, b.book_id FROM {book} b WHERE b.nid = %d", $book['parent'])); + $book = array_merge($book, $parent); + } + else { + // There is not a parent - this is a new book. + $book['plid'] = 0; + $book['book_id'] = $book['nid']; + } + + $book += array( + 'module' => 'book', + 'link_path' => 'node/'. $book['nid'], + 'router_path' => 'node/%', + 'menu_name' => book_menu_name($book['book_id']), + ); + $book = array_merge($book, db_fetch_array(db_query("SELECT title AS link_title FROM {node} WHERE nid = %d", $book['nid']))); + + // Items with depth > MENU_MAX_DEPTH cannot be saved. + if (menu_link_save($book)) { + db_query("INSERT INTO {book} (mlid, nid, book_id) VALUES (%d, %d, %d)", $book['mlid'], $book['nid'], $book['book_id']); + } + else { + // The depth was greater then MENU_MAX_DEPTH, so attach it to the + // closest valid parent. + $book['plid'] = db_result(db_query("SELECT plid FROM {menu_links} WHERE mlid = %d", $book['plid'])); + if (menu_link_save($book)) { + db_query("INSERT INTO {book} (mlid, nid, book_id) VALUES (%d, %d, %d)", $book['mlid'], $book['nid'], $book['book_id']); + } + } + $update_count--; + } + $ret['#finished'] = FALSE; + } + + if (empty($_SESSION['book_update_6000'])) { + $ret['#finished'] = TRUE; + $ret[] = update_sql("DROP TABLE {book_temp}"); + unset($_SESSION['book_update_6000']); + unset($_SESSION['book_update_6000_orphans']); + } + + return $ret; +} + Index: modules/book/book.js =================================================================== RCS file: modules/book/book.js diff -N modules/book/book.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/book/book.js 18 Jul 2007 01:16:29 -0000 @@ -0,0 +1,22 @@ +// $Id$ + +Drupal.behaviors.bookSelect = function(context) { + // This behavior attaches by ID, so is only valid once on a page. + if ($('#edit-book-book-id.bookSelect-processed').size()) { + return; + } + // Hide the button in the node form, since it's not needed when JS is enabled. + $('#edit-book-pick-book').css('display', 'none'); + + $('#edit-book-book-id') + .keyup(Drupal.bookFillSelect) + .change(Drupal.bookFillSelect) + .addClass('bookSelect-processed'); +} + +Drupal.bookFillSelect = function() { + $.get(Drupal.settings.book.formCallback +'/'+ $('#'+ Drupal.settings.book.formId +' input[@name=form_build_id]').val() +'/'+ $('#edit-book-book-id').val(), {}, function(data) { + parsedData = Drupal.parseJson(data); + $('#edit-book-plid-wrapper').html(parsedData['book']); + }); +}; Index: modules/book/book.module =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.module,v retrieving revision 1.428 diff -u -p -r1.428 book.module --- modules/book/book.module 3 Jul 2007 19:29:32 -0000 1.428 +++ modules/book/book.module 18 Jul 2007 01:16:29 -0000 @@ -3,29 +3,16 @@ /** * @file - * Allows users to collaboratively author a book. + * Allows users to structure the pages of a site in a hierarchy or outline. */ /** - * Implementation of hook_node_info(). - */ -function book_node_info() { - return array( - 'book' => array( - 'name' => t('Book page'), - 'module' => 'book', - 'description' => t("A book is a collaborative writing effort: users can collaborate writing the pages of the book, positioning the pages in the right order, and reviewing or modifying pages previously written. So when you have some information to share or when you read a page of the book and you didn't like it, or if you think a certain page could have been written better, you can do something about it."), - ) - ); -} - -/** * Implementation of hook_theme() */ function book_theme() { return array( 'book_navigation' => array( - 'arguments' => array('node' => NULL), + 'arguments' => array('book_link' => NULL), ), 'book_export_html' => array( 'arguments' => array('title' => NULL, 'content' => NULL), @@ -33,6 +20,9 @@ function book_theme() { 'book_admin_table' => array( 'arguments' => array('form' => NULL), ), + 'book_title_link' => array( + 'arguments' => array('link' => NULL), + ), ); } @@ -40,50 +30,23 @@ function book_theme() { * Implementation of hook_perm(). */ function book_perm() { - return array('outline posts in books', 'create book pages', 'create new books', 'edit book pages', 'edit own book pages', 'see printer-friendly version'); -} - -/** - * Implementation of hook_access(). - */ -function book_access($op, $node) { - global $user; - - if ($op == 'create') { - // Only registered users can create book pages. Given the nature - // of the book module this is considered to be a good/safe idea. - return user_access('create book pages'); - } - - if ($op == 'update') { - // Only registered users can update book pages. Given the nature - // of the book module this is considered to be a good/safe idea. - // One can only update a book page if there are no suggested updates - // of that page waiting for approval. That is, only updates that - // don't overwrite the current or pending information are allowed. - - if (user_access('edit book pages') || ($node->uid == $user->uid && user_access('edit own book pages'))) { - return TRUE; - } - else { - // do nothing. node-access() will determine further access - } - } + return array('add content to books', 'outline posts in books', 'create new books', 'see printer-friendly version'); } /** * Implementation of hook_link(). */ function book_link($type, $node = NULL, $teaser = FALSE) { - $links = array(); - if ($type == 'node' && isset($node->parent)) { + if ($type == 'node' && isset($node->book)) { if (!$teaser) { - if (book_access('create', $node) && $node->status == 1) { + $child_type = variable_get('book_child_type', 'book'); + if ((user_access('add content to books') || user_access('outline posts in books')) && node_access('create', $child_type) && $node->status == 1 && $node->book['depth'] < MENU_MAX_DEPTH) { $links['book_add_child'] = array( 'title' => t('Add child page'), - 'href' => "node/add/book/parent/$node->nid" + 'href' => "node/add/". str_replace('_', '-', $child_type), + 'query' => "parent=". $node->book['mlid'], ); } if (user_access('see printer-friendly version')) { @@ -95,7 +58,6 @@ function book_link($type, $node = NULL, } } } - return $links; } @@ -105,21 +67,30 @@ function book_link($type, $node = NULL, function book_menu() { $items['admin/content/book'] = array( 'title' => 'Books', - 'description' => "Manage site's books and orphaned book pages.", - 'page callback' => 'book_admin', - 'access arguments' => array('administer nodes'), + 'description' => "Manage your site's book outlines.", + 'page callback' => 'book_admin_overview', + 'access arguments' => array('outline posts in books'), ); $items['admin/content/book/list'] = array( 'title' => 'List', 'type' => MENU_DEFAULT_LOCAL_TASK, ); - $items['admin/content/book/orphan'] = array( - 'title' => 'Orphan pages', + $items['admin/content/book/settings'] = array( + 'title' => 'Settings', 'page callback' => 'drupal_get_form', - 'page arguments' => array('book_admin_orphan'), + 'page arguments' => array('book_admin_settings'), + 'access arguments' => array('administer site configuration'), 'type' => MENU_LOCAL_TASK, 'weight' => 8, ); + $items['admin/content/book/%node'] = array( + 'title' => 'Re-order book pages and change titles', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('book_admin_edit', 3), + 'access callback' => '_book_outline_access', + 'access arguments' => array(3), + 'type' => MENU_CALLBACK, + ); $items['book'] = array( 'title' => 'Books', 'page callback' => 'book_render', @@ -129,6 +100,7 @@ function book_menu() { $items['book/export/%/%'] = array( 'page callback' => 'book_export', 'page arguments' => array(2, 3), + 'access arguments' => array('see printer-friendly version'), 'type' => MENU_CALLBACK, ); $items['node/%node/outline'] = array( @@ -140,15 +112,40 @@ function book_menu() { 'type' => MENU_LOCAL_TASK, 'weight' => 2, ); - + $items['node/%node/outline/remove'] = array( + 'title' => 'Remove from outline', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('book_remove_form', 1), + 'access callback' => '_book_outline_remove_access', + 'access arguments' => array(1), + 'type' => MENU_CALLBACK, + ); + $items['book-form-update/%/%'] = array( + 'page callback' => 'book_form_update', + 'page arguments' => array(1, 2), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); return $items; } +/** + * Menu item access callback - determine if the outline tab is accessible. + */ function _book_outline_access($node) { - // Only add the outline-tab for non-book pages: - return user_access('outline posts in books') && $node && ($node->type != 'book'); + return user_access('outline posts in books') && node_access('view', $node); +} + +/** + * Menu item access callback - determine if the user can remove nodes from the outline. + */ +function _book_outline_remove_access($node) { + return isset($node->book) && ($node->book['book_id'] != $node->nid) && _book_outline_access($node); } +/** + * Implementation of hook_init(). + */ function book_init() { drupal_add_css(drupal_get_path('module', 'book') .'/book.css'); } @@ -166,338 +163,670 @@ function book_block($op = 'list', $delta return $block; } else if ($op == 'view') { - // Only display this block when the user is browsing a book: + // Only display this block when the user is browsing a book. if (arg(0) == 'node' && is_numeric(arg(1))) { - $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.nid = %d'), arg(1)); - if (db_num_rows($result) > 0) { - $node = db_fetch_object($result); - - $path = book_location($node); - $path[] = $node; - - $expand = array(); - foreach ($path as $key => $node) { - $expand[] = $node->nid; + $node = node_load(arg(1)); + if (isset($node->book['book_id'])) { + $title = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $node->book['book_id'])); + // Only show the block if the user has view access for the top-level node. + if ($title) { + $tree = menu_tree_all_data($node->book['menu_name'], $node->book); + $data = array_shift($tree); // Should only be one element. + $block['subject'] = theme('book_title_link', $data['link']); + $block['content'] = ($data['below']) ? menu_tree_output($data['below']) : ''; } - - $block['subject'] = check_plain($path[0]->title); - $block['content'] = book_tree($expand[0], 5, $expand); } } - return $block; } } /** - * Implementation of hook_insert(). + * Generate the HTML output for a link to a book title. + * + * @ingroup themeable */ -function book_insert($node) { - db_query("INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)", $node->nid, $node->vid, $node->parent, $node->weight); +function theme_book_title_link($link) { + $link['options']['attributes']['class'] = 'book-title'; + return l($link['title'], $link['href'], $link['options']); } /** - * Implementation of hook_submit(). + * Implementation of hook_submit(). Handles node form submission. */ -function book_submit(&$form_values) { +function book_submit($form, &$form_state) { global $user; // Set default values for non-administrators. if (!user_access('administer nodes')) { - $form_values['revision'] = 1; - $form_values['uid'] = $user->uid; + $form_state['values']['revision'] = 1; + $form_state['values']['uid'] = $user->uid; } } /** - * Implementation of hook_form(). + * Returns an array of all books. */ -function book_form(&$node) { - $type = node_get_types('type', $node); - if (!empty($node->nid) && !$node->parent && !user_access('create new books')) { - $form['parent'] = array('#type' => 'value', '#value' => $node->parent); +function book_get_books() { + static $all_books; + + if (!isset($all_books)) { + $all_books = array(); + $result = db_query("SELECT DISTINCT(book_id) FROM {book}"); + $nids = array(); + while ($book = db_fetch_array($result)) { + $nids[] = $book['book_id']; + } + if ($nids) { + $result2 = db_query(db_rewrite_sql("SELECT n.type, b.*, ml.* FROM {book} b INNER JOIN {node} n on b.nid = n.nid INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE n.nid IN (". implode(',', $nids). ") AND n.status = 1 ORDER BY ml.weight, ml.link_title")); + while ($link = db_fetch_array($result2)) { + $all_books[] = $link; + } + } } - else { - $form['parent'] = array('#type' => 'select', - '#title' => t('Parent'), - '#default_value' => (isset($node->parent) ? $node->parent : arg(4)), - '#options' => book_toc(isset($node->nid) ? $node->nid : 0), - '#weight' => -4, - '#description' => user_access('create new books') ? t('The parent section in which to place this page. Note that each page whose parent is <top-level> is an independent, top-level book.') : t('The parent that this page belongs in.'), - ); + return $all_books; +} + +/** + * AJAX callback to repalce the book parent select. + * + * @param $build_id + * The form's build_id. + * @param $book_id + * A book_id form from among those on the form. + * @return + * Prints the replacement HTML + */ +function book_form_update($build_id, $book_id) { + + $cid = 'form_'. $build_id; + $cache = cache_get($cid, 'cache_form'); + if ($cache) { + $form = $cache->data; + + // Validate the book_id. + if (isset($form['book']['book_id']['#options'][$book_id])) { + $book_link = $form['#node']->book; + $book_link['book_id'] = $book_id; + // Get the new options and update the cache. + $form['book']['plid'] = _book_parent_select($book_link); + $expire = max(ini_get('session.cookie_lifetime'), 86400); + cache_set($cid, $form, 'cache_form', $expire); + + $form_state = array(); + $form['#post'] = array(); + $form = form_builder($form['form_id']['#value'] , $form, $form_state); + $output = drupal_render($form['book']['plid']); + drupal_json(array('book' => $output)); + } } + exit(); +} - $form['title'] = array('#type' => 'textfield', - '#title' => check_plain($type->title_label), - '#required' => TRUE, - '#default_value' => $node->title, - '#weight' => -5, - ); +/** + * Implementation of hook_form_alter(). + */ +function book_form_alter(&$form, $form_state, $form_id) { - $form['body_field'] = node_body_field($node, $type->body_label, 1); + if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) { + // Add elements to the node form + $node = $form['#node']; - if (user_access('administer nodes')) { - $form['weight'] = array('#type' => 'weight', - '#title' => t('Weight'), - '#default_value' => isset($node->weight) ? $node->weight : 0, - '#delta' => 15, - '#weight' => 5, - '#description' => t('Pages at a given level are ordered first by weight and then by title.'), - ); + $access = user_access('outline posts in books') || user_access('create new books'); + if (!$access) { + if (user_access('add content to books') && ((!empty($node->book['mlid']) && !empty($node->nid)) || book_type_is_allowed($node->type))) { + // Already in the book hierarchy or this node type is allowed + $access = TRUE; + } + } + + if ($access) { + _book_add_form_elements($form, $node); + $form['book']['pick-book'] = array( + '#type' => 'submit', + '#value' => t('Change book (update list of parents)'), + '#submit' => array('book_pick_book_submit'), + '#weight' => 20, + ); + $form['#submit'][] = 'book_submit'; + } + } +} + +/** + * Like a node preview, to update options in the parent select. + */ +function book_pick_book_submit($form, &$form_state) { + // We do not want to execute button level handlers, we want the form level + // handlers to go in and change the submitted values. + unset($form_state['submit_handlers']); + form_execute_handlers('submit', $form, $form_state); + $form_state['rebuild'] = TRUE; + $form_state['node'] = $form_state['values']; +} + +/** + * Build the parent selection form element. + */ +function _book_parent_select($book_link) { + // Offer a drop-down to choose a different parent page. + $form = array('#type' => 'hidden', '#value' => -1, '#prefix' => '
'. t('%title is part of a book outline, and has associated child pages. If you proceed with deletion, the child pages will be relocated automatically.', array('%title' => $node->title)) .'
', + '#weight' => -10, + ); } } /** - * Prepares the links to children (TOC) and forward/backward - * navigation for a node presented as a book page. + * Return an array with default values for a book link. + */ +function _book_link_defaults($nid) { + return array('orig_book_id' => 0, 'menu_name' => '', 'nid' => $nid, 'book_id' => 0, 'router_path' => 'node/%', 'plid' => 0, 'mlid' => 0, 'has_children' => 0, 'weight' => 0, 'module' => 'book', 'options' => array()); +} + +/** + * Prepares the links to the children of the page and the previous/up/next + * navigation for a node in the book outline. * * @ingroup themeable */ -function theme_book_navigation($node) { +function theme_book_navigation($book_link) { $output = ''; $links = ''; - if ($node->nid) { - $tree = book_tree($node->nid); + if ($book_link['mlid']) { + $tree = book_children($book_link); - if ($prev = book_prev($node)) { - drupal_add_link(array('rel' => 'prev', 'href' => url('node/'. $prev->nid))); - $links .= l(t('‹ ') . $prev->title, 'node/'. $prev->nid, array('class' => 'page-previous', 'title' => t('Go to previous page'))); - } - if ($node->parent) { - drupal_add_link(array('rel' => 'up', 'href' => url('node/'. $node->parent))); - $links .= l(t('up'), 'node/'. $node->parent, array('class' => 'page-up', 'title' => t('Go to parent page'))); - } - if ($next = book_next($node)) { - drupal_add_link(array('rel' => 'next', 'href' => url('node/'. $next->nid))); - $links .= l($next->title . t(' ›'), 'node/'. $next->nid, array('class' => 'page-next', 'title' => t('Go to next page'))); + if ($prev = book_prev($book_link)) { + drupal_add_link(array('rel' => 'prev', 'href' => url($prev['href']))); + $links .= l(t('‹ ') . $prev['title'], $prev['href'], array('attributes' => array('class' => 'page-previous', 'title' => t('Go to previous page')))); + } + if ($book_link['plid'] && $parent = book_link_load($book_link['plid'])) { + drupal_add_link(array('rel' => 'up', 'href' => url($parent['href']))); + $links .= l(t('up'), $parent['href'], array('attributes' => array('class' => 'page-up', 'title' => t('Go to parent page')))); + } + if ($next = book_next($book_link)) { + drupal_add_link(array('rel' => 'next', 'href' => url($next['href']))); + $links .= l($next['title'] . t(' ›'), $next['href'], array('attributes' => array('class' => 'page-next', 'title' => t('Go to next page')))); } if (isset($tree) || isset($links)) { @@ -518,108 +847,48 @@ function theme_book_navigation($node) { /** * This is a helper function for book_toc(). */ -function book_toc_recurse($nid, $indent, $toc, $children, $exclude) { - if (!empty($children[$nid])) { - foreach ($children[$nid] as $foo => $node) { - if (!$exclude || $exclude != $node->nid) { - $toc[$node->nid] = $indent .' '. $node->title; - $toc = book_toc_recurse($node->nid, $indent .'--', $toc, $children, $exclude); +function _book_toc_recurse($tree, $indent, &$toc, $exclude) { + foreach ($tree as $data) { + if (!in_array($data['link']['mlid'], $exclude)) { + $toc[$data['link']['mlid']] = $indent .' '. truncate_utf8($data['link']['title'], 30, TRUE, TRUE) ; + if ($data['below'] && $data['link']['depth'] < MENU_MAX_DEPTH - 1) { + _book_toc_recurse($data['below'], $indent .'--', $toc, $exclude); } } } - - return $toc; } /** - * Returns an array of titles and nid entries of book pages in table of contents order. + * Returns an array of titles and mlid entries of book pages in table of + * contents order for use in selecting a book page. + * + * @param $book_id + * The ID of the book whose pages are to be listed. + * @param $exclude + * Optional array of mlid values. Any link whose mlid is in this array + * will be excluded (along with its children). + * @return + * An array of mlid, title pairs. */ -function book_toc($exclude = 0) { - $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.status = 1 ORDER BY b.weight, n.title')); - - $children = array(); - while ($node = db_fetch_object($result)) { - if (empty($children[$node->parent])) { - $children[$node->parent] = array(); - } - $children[$node->parent][] = $node; - } +function book_toc($book_id, $exclude = array()) { + $tree = menu_tree_all_data(book_menu_name($book_id)); $toc = array(); - // If the user has permission to create new books, add the top-level book page to the menu; - if (user_access('create new books')) { - $toc[0] = '<'. t('top-level') .'>'; - } - - $toc = book_toc_recurse(0, '', $toc, $children, $exclude); + _book_toc_recurse($tree, '', $toc, $exclude); return $toc; } /** - * This is a helper function for book_tree() - */ -function book_tree_recurse($nid, $depth, $children, $unfold = array()) { - $output = ''; - if ($depth > 0) { - if (isset($children[$nid])) { - foreach ($children[$nid] as $foo => $node) { - if (in_array($node->nid, $unfold)) { - if ($tree = book_tree_recurse($node->nid, $depth - 1, $children, $unfold)) { - $output .= ''. t('There are no orphan pages.') .'
'); - } - $form['#submit'][] = 'book_admin_edit_submit'; - $form['#validate'][] = 'book_admin_edit_validate'; - $form['#theme'] = 'book_admin_edit'; - return $form; -} - function book_admin_edit_submit($form, &$form_state) { foreach ($form_state['values']['table'] as $row) { $node = node_load($row['nid']); - if ($row['title'] != $node->title || $row['weight'] != $node->weight) { + if ($row['title'] != $node->title || $row['weight'] != $node->book['weight']) { $node->title = $row['title']; - $node->weight = $row['weight']; + $node->book['link_title'] = $row['title']; + $node->book['weight'] = $row['weight']; + $node->revision = 1; node_save($node); watchdog('content', 'book: updated %title.', array('%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid)); } } - if (is_numeric(arg(3))) { - // Updating pages in a single book. - $book = node_load(arg(3)); - drupal_set_message(t('Updated book %title.', array('%title' => $book->title))); - } - else { - // Updating the orphan pages. - drupal_set_message(t('Updated orphan book pages.')); - } -} - -/** - * Menu callback; displays the book administration page. - */ -function book_admin($nid = 0) { - if ($nid) { - return drupal_get_form('book_admin_edit', $nid); - } - else { - return book_admin_overview(); - } + drupal_set_message(t('Updated book %title.', array('%title' => $form['#node']->title))); } /** * Returns an administrative overview of all books. */ function book_admin_overview() { - $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE b.parent = 0 ORDER BY b.weight, n.title')); + $result = db_query(db_rewrite_sql("SELECT n.title, b.* FROM {book} b INNER JOIN {node} n ON n.nid = b.nid WHERE b.nid = b.book_id ORDER BY n.title")); $rows = array(); while ($book = db_fetch_object($result)) { - $rows[] = array(l($book->title, "node/$book->nid"), l(t('outline'), "admin/content/book/$book->nid")); + $rows[] = array(l($book->title, "node/$book->nid"), l(t('edit order and titles'), "admin/content/book/$book->nid")); } $headers = array(t('Book'), t('Operations')); return theme('table', $headers, $rows); } +function book_type_is_allowed($type) { + return in_array($type, variable_get('book_allowed_types', array('book'))); +} + +/** + * Returns the book settings form. + */ +function book_admin_settings() { + + $types = node_get_types('names'); + $form['book_allowed_types'] = array( + '#type' => 'checkboxes', + '#title' => t('Book content types'), + '#default_value' => variable_get('book_allowed_types', array('book')), + '#options' => $types, + '#description' => t('Select content types which users with the %add-perm permission will be allowed to add to the book hierarchy. Users with the %outline-perm permission can add all types.', array('%add-perm' => t('add content to books'), '%outline-perm' => t('outline posts in books'))), + '#required' => TRUE + ); + $form['book_child_type'] = array( + '#type' => 'radios', + '#title' => t('Default child type'), + '#default_value' => variable_get('book_child_type', 'book'), + '#options' => $types, + '#description' => t('Select the content type for the the %add-child link. This must be one of the content types selected above.', array('%add-child' => t('Add child page'))), + '#required' => TRUE + ); + $form['#validate'][] = 'book_admin_settings_validate'; + return system_settings_form($form); +} + +/** + * Returns the book settings form. + */ +function book_admin_settings_validate($form, &$form_state) { + $child_type = $form_state['values']['book_child_type']; + if (empty($form_state['values']['book_allowed_types'][$child_type])) { + form_set_error('book_child_type', t('The content type for the link must be one of the content types selected as an allowed type.')); + } +} + +/** + * Implementation of hook_node_type(). + */ +function book_node_type($op, $type_obj) { + + switch ($op){ + case 'update': + if (!empty($type_obj->old_type) && $type_obj->old_type != $type_obj->type) { + $allowed_types = variable_get('book_allowed_types', array('book')); + $key = array_search($type_obj->old_type, $allowed_types); + if ($key !== FALSE) { + $allowed_types[$type_obj->type] = $allowed_types[$key] ? $type_obj->type : 0; + unset($allowed_types[$key]); + variable_set('book_allowed_types', $allowed_types); + } + if (variable_get('book_child_type', 'book') == $type_obj->old_type) { + variable_set('book_child_type', $type_obj->type); + } + } + break; + } +} + /** * Implementation of hook_help(). */ @@ -994,17 +1272,80 @@ function book_help($path, $arg) { switch ($path) { case 'admin/help#book': $output = ''. t('The book module is suited for creating structured, multi-page hypertexts such as site resource guides, manuals, and Frequently Asked Questions (FAQs). It permits a document to have chapters, sections, subsections, etc. Authors with suitable permissions can add pages to a collaborative book, placing them into the existing document by adding them to a table of contents menu.') .'
'; - $output .= ''. t('Book pages have navigation elements at the bottom of the page for moving through the text. These link to the previous and next pages in the book, as well as a link labeled up, leading to the level above in the structure. More comprehensive navigation may be provided by enabling the book navigation block on the block administration page.', array('@admin-block' => url('admin/build/block'))) .'
'; + $output .= ''. t('Pages in the book hierarchy have navigation elements at the bottom of the page for moving through the text. These link to the previous and next pages in the book, as well as a link labeled up, leading to the level above in the structure. More comprehensive navigation may be provided by enabling the book navigation block on the block administration page.', array('@admin-block' => url('admin/build/block'))) .'
'; $output .= ''. t('Users can select the printer-friendly version link visible at the bottom of a book page to generate a printer-friendly display of the page and all of its subsections. ') .'
'; - $output .= ''. t("Posts of type %book are automatically added to the book hierarchy. Users with the outline posts in books permission can also add content of any other type to a book, placing it into the existing book structure through the interface that's available by clicking on the outline tab while viewing that post.", array('%book' => node_get_types('name', 'book'))) .'
'; - $output .= ''. t('Administrators can view a list of all books on the book administration page. In this list there is a link to an outline page for each book, from which is it possible to change the titles of sections, or to change their weight, thus reordering sections. From this administrative interface, it is also possible to determine whether there are any orphan pages - pages that have become disconnected from the rest of the book structure.', array('@admin-node-book' => url('admin/content/book'))) .'
'; + $output .= ''. t("Users with the outline posts in books permission can add content of any type to a book, placing it into the existing book structure through the edit form or through the interface that's available by clicking on the outline tab while viewing that post.", array('%book' => node_get_types('name', 'book'))) .'
'; + $output .= ''. t('Administrators can view a list of all books on the book administration page. In this list there is a link to an outline page for each book, from which is it possible to change the titles of sections, or to change their weight, thus reordering sections.', array('@admin-node-book' => url('admin/content/book'))) .'
'; $output .= ''. t('For more information please read the configuration and customization handbook Book page.', array('@book' => 'http://drupal.org/handbook/modules/book/')) .'
'; return $output; case 'admin/content/book': return ''. t('The book module offers a means to organize content, authored by many users, in an online manual, outline or FAQ.') .'
'; - case 'admin/content/book/orphan': - return ''. t('Pages in a book are like a tree. As pages are edited, reorganized and removed, child pages might be left with no link to the rest of the book. Such pages are referred to as "orphan pages". On this page, administrators can review their books for orphans and reattach those pages as desired.') .'
'; case 'node/%/outline': - return ''. t('The outline feature allows you to include posts in the book hierarchy.', array('@book' => url('book'))) .'
'; + return ''. t('The outline feature allows you to include posts in the book hierarchy, as well as move them within the hierarchy.', array('@book' => url('book'))) .'
'; + } +} + +/** + * Like menu_link_load(), but get extra data from the {book} table. + * + * Do not call when loading a node, since this function may call node_load(). + */ +function book_link_load($mlid) { + if ($item = db_fetch_array(db_query("SELECT * FROM {menu_links} ml INNER JOIN {book} b ON b.mlid = ml.mlid LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = %d", $mlid))) { + _menu_link_translate($item); + return $item; + } + return FALSE; +} + +/** + * Get the data structure representing a subtree. the root item will be the + * link passed as a parameter, and the returned data will contain all the + * links that are below it in the menu tree. + * + * @param $menu_name + * The named menu links to return + * @param $item + * A fully loaded menu link, + * @return + * An subtree of menu links in an array, in the order they should be rendered. + */ +function book_menu_subtree_data($item) { + static $tree = array(); + + $cid = 'links:'. $item['menu_name'] .':subtree:'. $item['mlid']; + + if (!isset($tree[$cid])) { + $cache = cache_get($cid, 'cache_menu'); + if ($cache && isset($cache->data)) { + $data = $cache->data; + } + else { + $i = 1; + $match = array(); + while ($i <= MENU_MAX_DEPTH && $item["p$i"]) { + $match[] = "p$i = %d"; + $args[] = $item["p$i"]; + $i++; + } + $sql = " + SELECT b.*, m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, ml.* + FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path + INNER JOIN {book} b ON ml.mlid = b.mlid + WHERE ml.hidden >= 0 AND ". implode(' AND ', $match) ." + ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC"; + + $data['tree'] = menu_tree_data(db_query($sql, $args), array(), $item['depth']); + $data['node_links'] = array(); + menu_tree_collect_node_links($data['tree'], $data['node_links']); + // Cache the data. + cache_set($cid, $data, 'cache_menu'); + } + // Check access for the current user to each item in the tree. + menu_tree_check_access($data['tree'], $data['node_links']); + $tree[$cid] = $data['tree']; } + + return $tree[$cid]; } + Index: modules/book/book.schema =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.schema,v retrieving revision 1.1 diff -u -p -r1.1 book.schema --- modules/book/book.schema 25 May 2007 12:46:43 -0000 1.1 +++ modules/book/book.schema 18 Jul 2007 01:16:29 -0000 @@ -4,16 +4,15 @@ function book_schema() { $schema['book'] = array( 'fields' => array( - 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), - 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), - 'parent' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), - 'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny') + 'mlid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'book_id' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), ), 'indexes' => array( - 'nid' => array('nid'), - 'parent' => array('parent') + 'nid' => array('nid'), + 'book_id' => array('book_id') ), - 'primary key' => array('vid'), + 'primary key' => array('mlid'), ); return $schema;