? book-delete_8.patch ? book-delete_9.patch ? sites/default/files ? sites/default/private ? sites/default/settings.php Index: modules/book/book.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.admin.inc,v retrieving revision 1.22 diff -d -u -p -r1.22 book.admin.inc --- modules/book/book.admin.inc 18 Sep 2009 00:12:45 -0000 1.22 +++ modules/book/book.admin.inc 23 Sep 2009 17:01:16 -0000 @@ -13,15 +13,22 @@ function book_admin_overview() { $rows = array(); $headers = array(t('Book'), t('Operations')); + if (user_access('bypass node access')) { + $headers[] = t('Delete'); + } // Add any recognized books to the table list. foreach (book_get_books() as $book) { - $rows[] = array(l($book['title'], $book['href'], $book['options']), l(t('edit order and titles'), 'admin/content/book/' . $book['nid'])); + $row = array(l($book['title'], $book['href'], $book['options']), l(t('edit order and titles'), 'admin/content/book/' . $book['nid'])); + if (user_access('bypass node access')) { + $row[] = l(t('delete book'), 'admin/content/book/delete/' . $book['nid']); + } + $rows[] = $row; } // If no books were found, let the user know. if (empty($rows)) { - $rows[] = array(array('data' => t('No books available.'), 'colspan' => 2)); + $rows[] = array(array('data' => t('No books available.'), 'colspan' => count($headers))); } return theme('table', $headers, $rows); @@ -258,3 +265,96 @@ function theme_book_admin_table($form) { return theme('table', $header, $rows, array('id' => 'book-outline')); } +/** + * Menu callback. Ask for confirmation of book deletion. + */ +function book_delete_confirm($form, &$form_state, $node) { + $form['bid'] = array( + '#type' => 'value', + '#value' => $node->nid, + ); + $form['book_title'] = array( + '#type' => 'value', + '#value' => $node->title, + ); + + return confirm_form($form, + t('Are you sure you want to delete the entire book %title?', array('%title' => $node->title)), 'admin/content/book', + t('This action cannot be undone.'), + t('Delete'), + t('Cancel') + ); +} + +/** + * Execute full book deletion using batch processing. + */ +function book_delete_confirm_submit($form, &$form_state) { + if ($form_state['values']['confirm']) { + $bid = $form_state['values']['bid']; + $batch = array( + 'title' => t('Deleting book %title', array('%title' => $form_state['values']['book_title'])), + 'operations' => array( + array('book_delete', array($bid)), + ), + 'finished' => '_book_delete_finished', + 'file' => drupal_get_path('module', 'book') . '/book.admin.inc', + ); + batch_set($batch); + batch_process(); + } +} + +/** + * Batch processing callback to delete an entire book. + */ +function book_delete($bid, &$context) { + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['max'] = db_query("SELECT COUNT(nid) FROM {book} WHERE bid = :bid", array(':bid' => $bid))->fetchField(); + $context['sandbox']['progress'] = 0; + $context['sandbox']['highest_nid'] = 0; + $context['sandbox']['bid'] = $bid; + } + + // Delete 5 nodes at a time. This needs to be set fairly low as there can be + // many search index deletions for each node. + $limit = 5; + $nids = db_query_range("SELECT nid FROM {book} WHERE bid = :bid AND nid > :nid AND nid <> bid ORDER BY nid ASC", 0, $limit, array( + ':bid' => $context['sandbox']['bid'], + ':nid' => $context['sandbox']['highest_nid'], + ))->fetchCol(); + if (!empty($nids)) { + node_delete_multiple($nids); + } + + // Update our progress information. + $context['sandbox']['progress'] += count($nids); + $context['sandbox']['highest_nid'] = array_pop($nids); + + // Delete the top book node last. + if ($context['sandbox']['progress'] == $context['sandbox']['max'] - 1) { + node_delete($context['sandbox']['bid']); + $context['sandbox']['progress']++; + } + + // Multistep processing : report progress. + if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + } +} + +/** + * Book delete batch 'finished' callback. + */ +function _book_delete_finished($success, $results, $operations) { + if ($success) { + drupal_set_message(t('The book has been deleted.')); + } + else { + drupal_set_message(t('An error occurred and processing did not complete.'), 'error'); + $message = format_plural(count($results), '1 item successfully deleted:', '@count items successfully deleted:'); + $message .= theme('item_list', $results); + drupal_set_message($message); + } + drupal_goto('admin/content/book'); +} Index: modules/book/book.module =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.module,v retrieving revision 1.515 diff -d -u -p -r1.515 book.module --- modules/book/book.module 22 Sep 2009 15:26:45 -0000 1.515 +++ modules/book/book.module 23 Sep 2009 17:01:17 -0000 @@ -121,7 +121,7 @@ function book_menu() { 'weight' => 8, 'file' => 'book.admin.inc', ); - $items['admin/content/book/%node'] = array( + $items['admin/content/book/%book'] = array( 'title' => 'Re-order book pages and change titles', 'page callback' => 'drupal_get_form', 'page arguments' => array('book_admin_edit', 3), @@ -130,6 +130,15 @@ function book_menu() { 'type' => MENU_CALLBACK, 'file' => 'book.admin.inc', ); + $items['admin/content/book/delete/%book'] = array( + 'title' => 'Delete Book', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('book_delete_confirm', 4), + 'access callback' => '_book_delete_access', + 'access arguments' => array(4), + 'type' => MENU_CALLBACK, + 'file' => 'book.admin.inc', + ); $items['book'] = array( 'title' => 'Books', 'page callback' => 'book_render', @@ -174,6 +183,23 @@ function book_menu() { } /** + * Load a book root node object from the database. + * + * @param $nid + * The node ID of the book to load. + * + * @return + * The fully loaded book node object. + */ +function book_load($nid) { + $node = node_load($nid); + if (isset($node->nid) && isset($node->book) && ($node->nid == $node->book['bid'])) { + return $node; + } + return FALSE; +} + +/** * Menu item access callback - determine if the outline tab is accessible. */ function _book_outline_access($node) { @@ -188,6 +214,13 @@ function _book_outline_remove_access($no } /** + * Menu item access callback - determine if the user can delete entire books. + */ +function _book_delete_access($node) { + return user_access('administer book outlines') && user_access('bypass node access'); +} + +/** * Implement hook_init(). */ function book_init() { Index: modules/book/book.test =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.test,v retrieving revision 1.16 diff -d -u -p -r1.16 book.test --- modules/book/book.test 24 Aug 2009 01:49:41 -0000 1.16 +++ modules/book/book.test 23 Sep 2009 17:01:17 -0000 @@ -7,7 +7,7 @@ class BookTestCase extends DrupalWebTest public static function getInfo() { return array( 'name' => 'Book functionality', - 'description' => 'Create a book, add pages, and test book interface.', + 'description' => 'Create a book, add pages, test book interface, and delete book.', 'group' => 'Book', ); } @@ -23,6 +23,8 @@ class BookTestCase extends DrupalWebTest // Create users. $book_author = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books')); $web_user = $this->drupalCreateUser(array('access printer-friendly version')); + $book_outliner = $this->drupalCreateUser(array('administer book outlines')); + $admin_user = $this->drupalCreateUser(array('administer book outlines', 'bypass node access')); // Create new book. $this->drupalLogin($book_author); @@ -76,6 +78,21 @@ class BookTestCase extends DrupalWebTest $this->book = $other_book; $this->checkBookNode($other_book, array($node), FALSE, FALSE, $node, array()); $this->checkBookNode($node, NULL, $other_book, $other_book, FALSE, array($other_book)); + + // Check that the book can be deleted. + $this->drupalLogout(); + $this->drupalLogin($book_outliner); + // The book outliner user should not be able to delete books. + $this->deleteBook($book, FALSE); + $this->drupalLogout(); + $this->drupalLogin($admin_user); + // Users should only be able to delete book roots, not book subpages. + $this->deleteBook($nodes[0], TRUE); + // The admin user should be able to delete books. + $this->deleteBook($book, TRUE); + // Users should only be able to delete books, not nodes of other types. + $page = $this->drupalCreateNode(); + $this->deleteBook($page, TRUE); } /** @@ -194,6 +211,46 @@ class BookTestCase extends DrupalWebTest return $node; } + + /** + * Delete the entire book. + * + * @param $book Book node object. + * @param $access If the user has access to delete the book. + */ + function deleteBook($book, $access) { + // Make sure users without access don't see the delete link or title and can't go there. + if (!$access) { + $this->drupalGet('admin/content/book/list'); + $this->assertNoRaw(l(t('delete book'), 'admin/content/book/delete/' . $book->nid), t('The delete link does not show up on the book list.')); + $this->assertNoRaw(t('Delete'), t('The Delete header does not show on the page.')); + $this->drupalGet('admin/content/book/delete/' . $book->nid); + $this->assertNoRaw(t('Are you sure you want to delete the entire book %title?', array('%title' => $book->title)), t('Confirm that a user can\'t delete a book.')); + } + else if (!isset($book->book['bid']) || (isset($book->book['bid']) && ($book->nid != $book->book['bid']))) { + $this->drupalGet('admin/content/book/list'); + $this->assertNoRaw(l(t('delete book'), 'admin/content/book/delete/' . $book->nid), t("The delete link does not show up on the book list for nodes which aren't books.")); + $this->drupalGet('admin/content/book/delete/' . $book->nid); + $this->assertNoRaw(t('Are you sure you want to delete the entire book %title?', array('%title' => $book->title)), t("Confirm that a user can't delete a node which isn't a book.")); + } + else { + // Make sure the delete link shows up on the book list page. + $this->drupalGet('admin/content/book/list'); + $this->assertRaw(l(t('delete book'), 'admin/content/book/delete/' . $book->nid), t('The delete link shows up on the book list.')); + + // Delete the book. + $edit = array(); + $this->drupalGet('admin/content/book/delete/' . $book->nid); + $this->assertRaw(t('Are you sure you want to delete the entire book %title?', array('%title' => $book->title)), t('Confirm that the user wants to delete book.')); + $this->drupalPost(NULL, $edit, t('Delete')); + + // Confirm deletion. + $this->assertRaw(t('The book has been deleted.'), t('Message confirms deletion')); + $this->drupalGet('admin/content/book/list'); + $this->assertNoRaw(l(t('delete book'), 'admin/content/book/delete/' . $book->nid), t('The delete link does not show up on the book list.')); + $this->assertTrue(db_query('SELECT COUNT(nid) FROM {book} WHERE bid = :bid', array(':bid' => $book->nid))->fetchField() == 0, t('The book pages are not in the database.')); + } + } } class BookBlockTestCase extends DrupalWebTestCase {