diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php index d69d083..9a953bb 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php @@ -209,6 +209,7 @@ function testContentTypeDirLang() { * Test filtering Node content by language. */ function testNodeAdminLanguageFilter() { + module_enable(array('views')); // User to add and remove language. $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'access content overview', 'administer nodes', 'bypass node access')); @@ -223,14 +224,8 @@ function testNodeAdminLanguageFilter() { $node_en = $this->drupalCreateNode(array('langcode' => 'en')); $node_zh_hant = $this->drupalCreateNode(array('langcode' => 'zh-hant')); - $this->drupalGet('admin/content'); - // Verify filtering by language. - $edit = array( - 'langcode' => 'zh-hant', - ); - $this->drupalPost(NULL, $edit, t('Filter')); - + $this->drupalGet('admin/content', array('query' => array('langcode' => 'zh-hant'))); $this->assertLinkByHref('node/' . $node_zh_hant->nid . '/edit'); $this->assertNoLinkByHref('node/' . $node_en->nid . '/edit'); } diff --git a/core/modules/node/config/views.view.content.yml b/core/modules/node/config/views.view.content.yml new file mode 100644 index 0000000..fed61e6 --- /dev/null +++ b/core/modules/node/config/views.view.content.yml @@ -0,0 +1,359 @@ +base_field: nid +base_table: node +core: 8.x +description: 'Find and manage content.' +status: '1' +display: + default: + display_options: + access: + type: perm + options: + perm: 'access content overview' + cache: + type: none + query: + type: views_query + exposed_form: + type: basic + options: + submit_button: Filter + reset_button: '0' + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: '1' + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: '50' + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: '1' + row_class_special: '1' + override: '1' + sticky: '1' + summary: '' + columns: + node_bulk_form: node_bulk_form + title: title + type: type + name: name + status: status + changed: changed + edit_node: edit_node + delete_node: delete_node + translation_link: translation_link + dropbutton: dropbutton + info: + node_bulk_form: + sortable: '0' + default_sort_order: asc + responsive: '' + title: + sortable: '1' + default_sort_order: asc + type: + sortable: '1' + default_sort_order: asc + name: + sortable: '0' + default_sort_order: asc + responsive: priority-low + status: + sortable: '1' + default_sort_order: asc + responsive: '' + changed: + sortable: '1' + default_sort_order: desc + responsive: priority-low + edit_node: + sortable: '0' + default_sort_order: asc + responsive: '' + delete_node: + sortable: '0' + default_sort_order: asc + responsive: '' + translation_link: + sortable: '0' + default_sort_order: asc + responsive: '' + dropbutton: + sortable: '0' + default_sort_order: asc + responsive: '' + default: changed + empty_table: '1' + row: + type: fields + fields: + node_bulk_form: + id: node_bulk_form + table: node + field: node_bulk_form + label: '' + exclude: '0' + alter: + alter_text: '0' + element_class: '' + element_default_classes: '1' + empty: '' + hide_empty: '0' + empty_zero: '0' + hide_alter_empty: '1' + plugin_id: node_bulk_form + title: + id: title + table: node + field: title + label: Title + exclude: '0' + alter: + alter_text: '0' + element_class: '' + element_default_classes: '1' + empty: '' + hide_empty: '0' + empty_zero: '0' + hide_alter_empty: '1' + link_to_node: '1' + plugin_id: node + type: + id: type + table: node + field: type + label: 'Content Type' + exclude: '0' + alter: + alter_text: '0' + element_class: '' + element_default_classes: '1' + empty: '' + hide_empty: '0' + empty_zero: '0' + hide_alter_empty: '1' + link_to_node: '0' + machine_name: '0' + plugin_id: node_type + name: + id: name + table: users + field: name + relationship: uid + label: Author + exclude: '0' + alter: + alter_text: '0' + element_class: '' + element_default_classes: '1' + empty: '' + hide_empty: '0' + empty_zero: '0' + hide_alter_empty: '1' + link_to_user: '1' + overwrite_anonymous: '0' + anonymous_text: '' + format_username: '1' + plugin_id: user_name + status: + id: status + table: node + field: status + label: Status + exclude: '0' + alter: + alter_text: '0' + element_class: '' + element_default_classes: '1' + empty: '' + hide_empty: '0' + empty_zero: '0' + hide_alter_empty: '1' + type: published-notpublished + type_custom_true: '' + type_custom_false: '' + not: '0' + plugin_id: boolean + changed: + id: changed + table: node + field: changed + label: Updated + exclude: '0' + alter: + alter_text: '0' + element_class: '' + element_default_classes: '1' + empty: '' + hide_empty: '0' + empty_zero: '0' + hide_alter_empty: '1' + date_format: short + custom_date_format: '' + timezone: '' + plugin_id: date + edit_node: + id: edit_node + table: views_entity_node + field: edit_node + label: '' + exclude: '1' + text: Edit + plugin_id: node_link_edit + delete_node: + id: delete_node + table: views_entity_node + field: delete_node + label: '' + exclude: '1' + text: Delete + plugin_id: node_link_delete + translation_link: + id: translation_link + table: node + field: translation_link + label: '' + exclude: '1' + alter: + alter_text: '0' + element_class: '' + element_default_classes: '1' + hide_alter_empty: '1' + hide_empty: '0' + empty_zero: '0' + empty: '' + text: Translate + optional: '1' + plugin_id: translation_entity_link + dropbutton: + id: dropbutton + table: views + field: dropbutton + label: Operations + fields: + edit_node: edit_node + delete_node: delete_node + translation_link: translation_link + destination: '1' + plugin_id: dropbutton + filters: + status_extra: + id: status_extra + table: node + field: status_extra + operator: '=' + value: '' + plugin_id: node_status + status: + id: status + table: node + field: status + operator: '=' + value: All + exposed: '1' + expose: + operator_id: '' + label: Status + description: '' + use_operator: '0' + operator: status_op + identifier: status + required: '0' + remember: '0' + multiple: '0' + remember_roles: + authenticated: authenticated + plugin_id: boolean + type: + id: type + table: node + field: type + operator: in + value: { } + exposed: '1' + expose: + operator_id: type_op + label: Type + description: '' + use_operator: '0' + operator: type_op + identifier: type + required: '0' + remember: '0' + multiple: '0' + remember_roles: + authenticated: authenticated + reduce: '0' + plugin_id: bundle + langcode: + id: langcode + table: node + field: langcode + operator: in + value: { } + group: '1' + exposed: '1' + expose: + operator_id: langcode_op + label: Language + operator: langcode_op + identifier: langcode + remember_roles: + authenticated: authenticated + optional: '1' + plugin_id: language + sorts: { } + title: Content + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + empty: '1' + content: 'No content available.' + plugin_id: text_custom + arguments: { } + relationships: + uid: + id: uid + table: node + field: uid + admin_label: author + required: '1' + plugin_id: standard + show_admin_links: '0' + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: admin/content/node + menu: + type: 'default tab' + title: Content + description: '' + name: admin + weight: '-10' + context: '0' + tab_options: + type: normal + title: Content + description: 'Find and manage content' + name: admin + weight: '-10' + display_plugin: page + display_title: Page + id: page_1 + position: '1' +label: Content +module: node +id: content +tag: default +langcode: en diff --git a/core/modules/node/lib/Drupal/node/Form/DeleteMultiple.php b/core/modules/node/lib/Drupal/node/Form/DeleteMultiple.php new file mode 100644 index 0000000..cc1e56d --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Form/DeleteMultiple.php @@ -0,0 +1,130 @@ +tempStore = $temp_store_factory->get('node_multiple_delete_confirm'); + $this->manager = $manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.tempstore'), + $container->get('plugin.manager.entity') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'node_multiple_delete_confirm'; + } + + /** + * {@inheritdoc} + */ + protected function getQuestion() { + return format_plural(count($this->nodes), 'Are you sure you want to delete this item?', 'Are you sure you want to delete these items?'); + } + + /** + * {@inheritdoc} + */ + protected function getCancelPath() { + return 'admin/content'; + } + + /** + * {@inheritdoc} + */ + protected function getConfirmText() { + return t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state) { + global $user; + $this->nodes = $this->tempStore->get($user->uid); + if (empty($this->nodes)) { + drupal_goto($this->getCancelPath()); + } + + foreach ($this->nodes as $nid => $node) { + $form['nodes'][$nid] = array( + '#type' => 'hidden', + '#value' => $nid, + '#prefix' => '
  • ', + '#suffix' => String::checkPlain($node->label()) . "
  • \n", + ); + } + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + if ($form_state['values']['confirm'] && !empty($this->nodes)) { + $this->manager->getStorageController('node')->delete($this->nodes); + global $user; + $this->tempStore->delete($user->uid); + $count = count($this->nodes); + watchdog('content', 'Deleted @count posts.', array('@count' => $count)); + drupal_set_message(format_plural($count, 'Deleted 1 post.', 'Deleted @count posts.')); + } + $form_state['redirect'] = 'admin/content'; + } + +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/NodeBulkForm.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/NodeBulkForm.php new file mode 100644 index 0000000..5986cd1 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/NodeBulkForm.php @@ -0,0 +1,77 @@ +invokeAll('node_operations')); + } + + /** + * {@inheritdoc} + */ + public function views_form_validate(&$form, &$form_state) { + $selected = array_filter($form_state['values'][$this->options['id']]); + if (empty($selected)) { + form_set_error('', t('No items selected.')); + } + } + + /** + * {@inheritdoc} + */ + public function views_form_submit(&$form, &$form_state) { + form_load_include($form_state, 'admin.inc', 'node'); + if ($form_state['step'] == 'views_form_views_form') { + // Filter only selected checkboxes. + $selected = array_filter($form_state['values'][$this->options['id']]); + $nodes = array(); + foreach (array_intersect_key($this->view->result, $selected) as $row) { + $node = $this->get_entity($row); + $nodes[$node->id()] = $node; + } + + $operations = \Drupal::moduleHandler()->invokeAll('node_operations'); + $operation = $operations[$form_state['values']['action']]; + // Filter out unchecked nodes + $nodes = array_filter($nodes); + if ($function = $operation['callback']) { + // Add in callback arguments if present. + if (isset($operation['callback arguments'])) { + $args = array_merge(array($nodes), $operation['callback arguments']); + } + else { + $args = array($nodes); + } + call_user_func_array($function, $args); + Cache::invalidateTags(array('content' => TRUE)); + + if (isset($operation['redirect'])) { + $form_state['redirect'] = $operation['redirect']; + } + } + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAdminTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAdminTest.php index bcfbfcd..15db2b4 100644 --- a/core/modules/node/lib/Drupal/node/Tests/NodeAdminTest.php +++ b/core/modules/node/lib/Drupal/node/Tests/NodeAdminTest.php @@ -12,6 +12,13 @@ */ class NodeAdminTest extends NodeTestBase { + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('views'); + public static function getInfo() { return array( 'name' => 'Node administration', @@ -39,38 +46,42 @@ function setUp() { */ function testContentAdminSort() { $this->drupalLogin($this->admin_user); + + // Create nodes that have different node.changed values. + \Drupal::state()->set('node_test.storage_controller', TRUE); + module_enable(array('node_test')); + $changed = REQUEST_TIME; foreach (array('dd', 'aa', 'DD', 'bb', 'cc', 'CC', 'AA', 'BB') as $prefix) { - $this->drupalCreateNode(array('title' => $prefix . $this->randomName(6))); + $changed += 1000; + $this->drupalCreateNode(array('title' => $prefix . $this->randomName(6), 'changed' => $changed)); } // Test that the default sort by node.changed DESC actually fires properly. $nodes_query = db_select('node', 'n') - ->fields('n', array('nid')) + ->fields('n', array('title')) ->orderBy('changed', 'DESC') ->execute() ->fetchCol(); - $nodes_form = array(); $this->drupalGet('admin/content'); - foreach ($this->xpath('//table/tbody/tr/td/div/input/@value') as $input) { - $nodes_form[] = $input; + foreach ($nodes_query as $delta => $string) { + $elements = $this->xpath('//table[contains(@class, :class)]//tr[' . ($delta + 1) . ']/td[2]/a[normalize-space(text())=:label]', array(':class' => 'views-table', ':label' => $string)); + $this->assertTrue(!empty($elements), 'The node was found in the correct order.'); } - $this->assertEqual($nodes_query, $nodes_form, 'Nodes are sorted in the form according to the default query.'); // Compare the rendered HTML node list to a query for the nodes ordered by // title to account for possible database-dependent sort order. $nodes_query = db_select('node', 'n') - ->fields('n', array('nid')) + ->fields('n', array('title')) ->orderBy('title') ->execute() ->fetchCol(); - $nodes_form = array(); - $this->drupalGet('admin/content', array('query' => array('sort' => 'asc', 'order' => 'Title'))); - foreach ($this->xpath('//table/tbody/tr/td/div/input/@value') as $input) { - $nodes_form[] = $input; + $this->drupalGet('admin/content', array('query' => array('sort' => 'asc', 'order' => 'title'))); + foreach ($nodes_query as $delta => $string) { + $elements = $this->xpath('//table[contains(@class, :class)]//tr[' . ($delta + 1) . ']/td[2]/a[normalize-space(text())=:label]', array(':class' => 'views-table', ':label' => $string)); + $this->assertTrue(!empty($elements), 'The node was found in the correct order.'); } - $this->assertEqual($nodes_query, $nodes_form, 'Nodes are sorted in the form the same as they are in the query.'); } /** @@ -95,30 +106,17 @@ function testContentAdminPages() { $this->assertLinkByHref('node/' . $node->nid); $this->assertLinkByHref('node/' . $node->nid . '/edit'); $this->assertLinkByHref('node/' . $node->nid . '/delete'); - // Verify tableselect. - $this->assertFieldByName('nodes[' . $node->nid . ']', '', 'Tableselect found.'); } // Verify filtering by publishing status. - $edit = array( - 'status' => 'status-1', - ); - $this->drupalPost(NULL, $edit, t('Filter')); - - $this->assertRaw(t('where %property is %value', array('%property' => t('status'), '%value' => 'published')), 'Content list is filtered by status.'); + $this->drupalGet('admin/content', array('query' => array('status' => TRUE))); $this->assertLinkByHref('node/' . $nodes['published_page']->nid . '/edit'); $this->assertLinkByHref('node/' . $nodes['published_article']->nid . '/edit'); $this->assertNoLinkByHref('node/' . $nodes['unpublished_page_1']->nid . '/edit'); // Verify filtering by status and content type. - $edit = array( - 'type' => 'page', - ); - $this->drupalPost(NULL, $edit, t('Refine')); - - $this->assertRaw(t('where %property is %value', array('%property' => t('status'), '%value' => 'published')), 'Content list is filtered by status.'); - $this->assertRaw(t('and where %property is %value', array('%property' => t('type'), '%value' => 'Basic page')), 'Content list is filtered by content type.'); + $this->drupalGet('admin/content', array('query' => array('status' => TRUE, 'type' => 'page'))); $this->assertLinkByHref('node/' . $nodes['published_page']->nid . '/edit'); $this->assertNoLinkByHref('node/' . $nodes['published_article']->nid . '/edit'); diff --git a/core/modules/node/lib/Drupal/node/Tests/Views/BulkFormTest.php b/core/modules/node/lib/Drupal/node/Tests/Views/BulkFormTest.php new file mode 100644 index 0000000..128cb4f --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Tests/Views/BulkFormTest.php @@ -0,0 +1,55 @@ + 'Node: Bulk form', + 'description' => 'Tests a node bulk form.', + 'group' => 'Views Modules', + ); + } + + /** + * Tests the node bulk form. + */ + public function testBulkForm() { + $this->drupalLogin($this->drupalCreateUser(array('administer nodes'))); + $node = $this->drupalCreateNode(); + + $this->drupalGet('test-node-bulk-form'); + $elements = $this->xpath('//select[@id="edit-action"]//option'); + $this->assertIdentical(count($elements), 7, 'All node operations are found.'); + + // Block a node using the bulk form. + $this->assertTrue($node->status); + $edit = array( + 'node_bulk_form[0]' => TRUE, + 'action' => 'unpublish', + ); + $this->drupalPost(NULL, $edit, t('Apply')); + // Re-load the node and check their status. + $node = entity_load('node', $node->id()); + $this->assertFalse($node->status); + } + +} diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc index 139b2d2..e931dd1 100644 --- a/core/modules/node/node.admin.inc +++ b/core/modules/node/node.admin.inc @@ -67,204 +67,14 @@ function node_node_operations() { ), 'delete' => array( 'label' => t('Delete selected content'), - 'callback' => NULL, + 'callback' => 'node_multiple_delete_confirm', + 'redirect' => 'admin/content/node/delete', ), ); return $operations; } /** - * Lists node administration filters that can be applied. - * - * @return - * An associative array of filters. - */ -function node_filters() { - // Regular filters - $filters['status'] = array( - 'title' => t('status'), - 'options' => array( - '[any]' => t('any'), - 'status-1' => t('published'), - 'status-0' => t('not published'), - 'promote-1' => t('promoted'), - 'promote-0' => t('not promoted'), - 'sticky-1' => t('sticky'), - 'sticky-0' => t('not sticky'), - ), - ); - // Include translation states if we have this module enabled - if (module_exists('translation')) { - $filters['status']['options'] += array( - 'translate-0' => t('Up to date translation'), - 'translate-1' => t('Outdated translation'), - ); - } - - $filters['type'] = array( - 'title' => t('type'), - 'options' => array( - '[any]' => t('any'), - ) + node_type_get_names(), - ); - - // Language filter if language support is present. - if (language_multilingual()) { - $languages = language_list(LANGUAGE_ALL); - foreach ($languages as $langcode => $language) { - // Make locked languages appear special in the list. - $language_options[$langcode] = $language->locked ? t('- @name -', array('@name' => $language->name)) : $language->name; - } - $filters['langcode'] = array( - 'title' => t('language'), - 'options' => array( - '[any]' => t('- Any -'), - ) + $language_options, - ); - } - return $filters; -} - -/** - * Applies filters for the node administration overview based on session. - * - * @param Drupal\Core\Database\Query\SelectInterface $query - * A SelectQuery to which the filters should be applied. - */ -function node_build_filter_query(SelectInterface $query) { - // Build query - $filter_data = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array(); - foreach ($filter_data as $index => $filter) { - list($key, $value) = $filter; - switch ($key) { - case 'status': - // Note: no exploitable hole as $key/$value have already been checked when submitted - list($key, $value) = explode('-', $value, 2); - case 'type': - case 'langcode': - $query->condition('n.' . $key, $value); - break; - } - } -} - -/** - * Returns the node administration filters form array to node_admin_content(). - * - * @see node_admin_nodes() - * @see node_admin_nodes_submit() - * @see node_admin_nodes_validate() - * @see node_filter_form_submit() - * @see node_multiple_delete_confirm() - * @see node_multiple_delete_confirm_submit() - * - * @ingroup forms - */ -function node_filter_form() { - $session = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array(); - $filters = node_filters(); - - $i = 0; - $form['filters'] = array( - '#type' => 'details', - '#title' => t('Show only items where'), - '#theme' => 'exposed_filters__node', - ); - foreach ($session as $filter) { - list($type, $value) = $filter; - if ($type == 'term') { - // Load term name from DB rather than search and parse options array. - $value = module_invoke('taxonomy', 'term_load', $value); - $value = $value->name; - } - elseif ($type == 'langcode') { - $value = language_name($value); - } - else { - $value = $filters[$type]['options'][$value]; - } - $t_args = array('%property' => $filters[$type]['title'], '%value' => $value); - if ($i++) { - $form['filters']['current'][] = array('#markup' => t('and where %property is %value', $t_args)); - } - else { - $form['filters']['current'][] = array('#markup' => t('where %property is %value', $t_args)); - } - if (in_array($type, array('type', 'langcode'))) { - // Remove the option if it is already being filtered on. - unset($filters[$type]); - } - } - - $form['filters']['status'] = array( - '#type' => 'container', - '#attributes' => array('class' => array('clearfix')), - '#prefix' => ($i ? '
    ' . t('and where') . '
    ' : ''), - ); - $form['filters']['status']['filters'] = array( - '#type' => 'container', - '#attributes' => array('class' => array('filters')), - ); - foreach ($filters as $key => $filter) { - $form['filters']['status']['filters'][$key] = array( - '#type' => 'select', - '#options' => $filter['options'], - '#title' => $filter['title'], - '#default_value' => '[any]', - ); - } - - $form['filters']['status']['actions'] = array( - '#type' => 'actions', - '#attributes' => array('class' => array('container-inline')), - ); - $form['filters']['status']['actions']['submit'] = array( - '#type' => 'submit', - '#value' => count($session) ? t('Refine') : t('Filter'), - ); - if (count($session)) { - $form['filters']['status']['actions']['undo'] = array('#type' => 'submit', '#value' => t('Undo')); - $form['filters']['status']['actions']['reset'] = array('#type' => 'submit', '#value' => t('Reset')); - } - - $form['#attached']['library'][] = array('system', 'drupal.form'); - - return $form; -} - -/** - * Form submission handler for node_filter_form(). - * - * @see node_admin_content() - * @see node_admin_nodes() - * @see node_admin_nodes_submit() - * @see node_admin_nodes_validate() - * @see node_filter_form() - * @see node_multiple_delete_confirm() - * @see node_multiple_delete_confirm_submit() - */ -function node_filter_form_submit($form, &$form_state) { - $filters = node_filters(); - switch ($form_state['values']['op']) { - case t('Filter'): - case t('Refine'): - // Apply every filter that has a choice selected other than 'any'. - foreach ($filters as $filter => $options) { - if (isset($form_state['values'][$filter]) && $form_state['values'][$filter] != '[any]') { - $_SESSION['node_overview_filter'][] = array($filter, $form_state['values'][$filter]); - } - } - break; - case t('Undo'): - array_pop($_SESSION['node_overview_filter']); - break; - case t('Reset'): - $_SESSION['node_overview_filter'] = array(); - break; - } -} - -/** * Updates all nodes in the passed-in array with the passed-in field values. * * IMPORTANT NOTE: This function is intended to work when called from a form @@ -272,18 +82,21 @@ function node_filter_form_submit($form, &$form_state) { * work correctly. * * @param array $nodes - * Array of node nids to update. + * Array of node nids or nodes to update. * @param array $updates * Array of key/value pairs with node field names and the value to update that * field to. + * @param bool $load + * (optional) TRUE if $nodes contains an array of node IDs to be loaded, FALSE + * if it contains fully loaded nodes. Defaults to FALSE. */ -function node_mass_update($nodes, $updates) { +function node_mass_update($nodes, $updates, $load = FALSE) { // We use batch processing to prevent timeout when updating a large number // of nodes. if (count($nodes) > 10) { $batch = array( 'operations' => array( - array('_node_mass_update_batch_process', array($nodes, $updates)) + array('_node_mass_update_batch_process', array($nodes, $updates, $load)) ), 'finished' => '_node_mass_update_batch_finished', 'title' => t('Processing'), @@ -298,8 +111,11 @@ function node_mass_update($nodes, $updates) { batch_set($batch); } else { - foreach ($nodes as $nid) { - _node_mass_update_helper($nid, $updates); + if ($load) { + $nodes = entity_load_multiple('node', $nodes); + } + foreach ($nodes as $node) { + _node_mass_update_helper($node, $updates); } drupal_set_message(t('The update has been performed.')); } @@ -308,8 +124,8 @@ function node_mass_update($nodes, $updates) { /** * Updates individual nodes when fewer than 10 are queued. * - * @param $nid - * ID of node to update. + * @param $node + * A node to update. * @param $updates * Associative array of updates. * @@ -318,8 +134,7 @@ function node_mass_update($nodes, $updates) { * * @see node_mass_update() */ -function _node_mass_update_helper($nid, $updates) { - $node = node_load($nid, TRUE); +function _node_mass_update_helper($node, $updates) { // For efficiency manually save the original node before applying any changes. $node->original = clone $node; foreach ($updates as $name => $value) { @@ -336,10 +151,13 @@ function _node_mass_update_helper($nid, $updates) { * An array of node IDs. * @param array $updates * Associative array of updates. + * @param bool $load + * TRUE if $nodes contains an array of node IDs to be loaded, FALSE if it + * contains fully loaded nodes. * @param array $context * An array of contextual key/values. */ -function _node_mass_update_batch_process($nodes, $updates, &$context) { +function _node_mass_update_batch_process($nodes, $updates, $load, &$context) { if (!isset($context['sandbox']['progress'])) { $context['sandbox']['progress'] = 0; $context['sandbox']['max'] = count($nodes); @@ -350,8 +168,11 @@ function _node_mass_update_batch_process($nodes, $updates, &$context) { $count = min(5, count($context['sandbox']['nodes'])); for ($i = 1; $i <= $count; $i++) { // For each nid, load the node, reset the values, and save it. - $nid = array_shift($context['sandbox']['nodes']); - $node = _node_mass_update_helper($nid, $updates); + $node = array_shift($context['sandbox']['nodes']); + if ($load) { + $node = entity_load('node', $node); + } + $node = _node_mass_update_helper($node, $updates); // Store result for post-processing in the finished callback. $context['results'][] = l($node->label(), 'node/' . $node->nid); @@ -391,68 +212,13 @@ function _node_mass_update_batch_finished($success, $results, $operations) { } /** - * Page callback: Form constructor for the content administration form. - * - * @see node_admin_nodes() - * @see node_admin_nodes_submit() - * @see node_admin_nodes_validate() - * @see node_filter_form() - * @see node_filter_form_submit() - * @see node_menu() - * @see node_multiple_delete_confirm() - * @see node_multiple_delete_confirm_submit() - * @ingroup forms - */ -function node_admin_content($form, $form_state) { - if (isset($form_state['values']['operation']) && $form_state['values']['operation'] == 'delete') { - return node_multiple_delete_confirm($form, $form_state, array_filter($form_state['values']['nodes'])); - } - $form['filter'] = node_filter_form(); - $form['#submit'][] = 'node_filter_form_submit'; - $form['admin'] = node_admin_nodes(); - - return $form; -} - -/** * Returns the admin form object to node_admin_content(). * - * @see node_admin_nodes_submit() - * @see node_filter_form() - * @see node_filter_form_submit() * @see node_multiple_delete_confirm() - * @see node_multiple_delete_confirm_submit() * * @ingroup forms */ function node_admin_nodes() { - $admin_access = user_access('administer nodes'); - - // Build the 'Update options' form. - $form['options'] = array( - '#type' => 'details', - '#title' => t('Update options'), - '#attributes' => array('class' => array('container-inline')), - '#access' => $admin_access, - ); - $options = array(); - foreach (module_invoke_all('node_operations') as $operation => $array) { - $options[$operation] = $array['label']; - } - $form['options']['operation'] = array( - '#type' => 'select', - '#title' => t('Operation'), - '#title_display' => 'invisible', - '#options' => $options, - '#default_value' => 'approve', - ); - $form['options']['submit'] = array( - '#type' => 'submit', - '#value' => t('Update'), - '#tableselect' => TRUE, - '#submit' => array('node_admin_nodes_submit'), - ); - // Enable language column and filter if multiple languages are enabled. $multilingual = language_multilingual(); @@ -490,7 +256,6 @@ function node_admin_nodes() { $query = db_select('node', 'n') ->extend('Drupal\Core\Database\Query\PagerSelectExtender') ->extend('Drupal\Core\Database\Query\TableSortExtender'); - node_build_filter_query($query); if (!user_access('bypass node access')) { // If the user is able to view their own unpublished nodes, allow them @@ -519,35 +284,35 @@ function node_admin_nodes() { // Prepare the list of nodes. $languages = language_list(LANGUAGE_ALL); $destination = drupal_get_destination(); - $form['nodes'] = array( + $build['nodes'] = array( '#type' => 'table', '#header' => $header, '#empty' => t('No content available.'), ); foreach ($nodes as $node) { $l_options = $node->langcode != LANGUAGE_NOT_SPECIFIED && isset($languages[$node->langcode]) ? array('language' => $languages[$node->langcode]) : array(); - $form['nodes'][$node->nid]['title'] = array( + $build['nodes'][$node->nid]['title'] = array( '#type' => 'link', '#title' => $node->label(), '#href' => 'node/' . $node->nid, '#options' => $l_options, '#suffix' => ' ' . theme('mark', array('type' => node_mark($node->nid, $node->changed))), ); - $form['nodes'][$node->nid]['type'] = array( + $build['nodes'][$node->nid]['type'] = array( '#markup' => check_plain(node_get_type_label($node)), ); - $form['nodes'][$node->nid]['author'] = array( + $build['nodes'][$node->nid]['author'] = array( '#theme' => 'username', '#account' => $node, ); - $form['nodes'][$node->nid]['status'] = array( + $build['nodes'][$node->nid]['status'] = array( '#markup' => $node->status ? t('published') : t('not published'), ); - $form['nodes'][$node->nid]['changed'] = array( + $build['nodes'][$node->nid]['changed'] = array( '#markup' => format_date($node->changed, 'short'), ); if ($multilingual) { - $form['nodes'][$node->nid]['language_name'] = array( + $build['nodes'][$node->nid]['language_name'] = array( '#markup' => language_name($node->langcode), ); } @@ -575,10 +340,10 @@ function node_admin_nodes() { 'query' => $destination, ); } - $form['nodes'][$node->nid]['operations'] = array(); + $build['nodes'][$node->nid]['operations'] = array(); if (count($operations) > 1) { // Render an unordered list of operations links. - $form['nodes'][$node->nid]['operations'] = array( + $build['nodes'][$node->nid]['operations'] = array( '#type' => 'operations', '#subtype' => 'node', '#links' => $operations, @@ -587,7 +352,7 @@ function node_admin_nodes() { elseif (!empty($operations)) { // Render the first and only operation as a link. $link = reset($operations); - $form['nodes'][$node->nid]['operations'] = array( + $build['nodes'][$node->nid]['operations'] = array( '#type' => 'link', '#title' => $link['title'], '#href' => $link['href'], @@ -596,102 +361,16 @@ function node_admin_nodes() { } } - // Only use a tableselect when the current user is able to perform any - // operations. - if ($admin_access) { - $form['nodes']['#tableselect'] = TRUE; - } - - $form['pager'] = array('#theme' => 'pager'); - return $form; -} - -/** - * Form submission handler for node_admin_nodes(). - * - * Executes the chosen 'Update option' on the selected nodes. - * - * @see node_admin_nodes() - * @see node_admin_nodes_validate() - * @see node_filter_form() - * @see node_filter_form_submit() - * @see node_multiple_delete_confirm() - * @see node_multiple_delete_confirm_submit() - */ -function node_admin_nodes_submit($form, &$form_state) { - $operations = module_invoke_all('node_operations'); - $operation = $operations[$form_state['values']['operation']]; - // Filter out unchecked nodes - $nodes = array_filter($form_state['values']['nodes']); - if ($function = $operation['callback']) { - // Add in callback arguments if present. - if (isset($operation['callback arguments'])) { - $args = array_merge(array($nodes), $operation['callback arguments']); - } - else { - $args = array($nodes); - } - call_user_func_array($function, $args); - - cache_invalidate_tags(array('content' => TRUE)); - } - else { - // We need to rebuild the form to go to a second step. For example, to - // show the confirmation form for the deletion of nodes. - $form_state['rebuild'] = TRUE; - } + $build['pager'] = array('#theme' => 'pager'); + return $build; } /** * Multiple node deletion confirmation form for node_admin_content(). * * @see node_admin_nodes() - * @see node_admin_nodes_submit() - * @see node_admin_nodes_validate() - * @see node_filter_form() - * @see node_filter_form_submit() - * @see node_multiple_delete_confirm_submit() - * @ingroup forms */ -function node_multiple_delete_confirm($form, &$form_state, $nodes) { - $form['nodes'] = array('#prefix' => '', '#tree' => TRUE); - // array_filter returns only elements with TRUE values - foreach ($nodes as $nid => $value) { - $title = db_query('SELECT title FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchField(); - $form['nodes'][$nid] = array( - '#type' => 'hidden', - '#value' => $nid, - '#prefix' => '
  • ', - '#suffix' => check_plain($title) . "
  • \n", - ); - } - $form['operation'] = array('#type' => 'hidden', '#value' => 'delete'); - $form['#submit'][] = 'node_multiple_delete_confirm_submit'; - $confirm_question = format_plural(count($nodes), - 'Are you sure you want to delete this item?', - 'Are you sure you want to delete these items?'); - return confirm_form($form, - $confirm_question, - 'admin/content', t('This action cannot be undone.'), - t('Delete'), t('Cancel')); -} - -/** - * Form submission handler for node_multiple_delete_confirm(). - * - * @see node_admin_nodes() - * @see node_admin_nodes_submit() - * @see node_admin_nodes_validate() - * @see node_filter_form() - * @see node_filter_form_submit() - * @see node_multiple_delete_confirm() - */ -function node_multiple_delete_confirm_submit($form, &$form_state) { - if ($form_state['values']['confirm']) { - node_delete_multiple(array_keys($form_state['values']['nodes'])); - $count = count($form_state['values']['nodes']); - watchdog('content', 'Deleted @count posts.', array('@count' => $count)); - drupal_set_message(format_plural($count, 'Deleted 1 post.', 'Deleted @count posts.')); - } - $form_state['redirect'] = 'admin/content'; +function node_multiple_delete_confirm($nodes) { + global $user; + Drupal::service('user.tempstore')->get('node_multiple_delete_confirm')->set($user->uid, $nodes); } diff --git a/core/modules/node/node.module b/core/modules/node/node.module index de5b8d5..87c5257 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1463,7 +1463,7 @@ function node_user_cancel($edit, $account, $method) { ->condition('uid', $account->uid) ->execute() ->fetchCol(); - node_mass_update($nodes, array('status' => 0)); + node_mass_update($nodes, array('status' => 0), TRUE); break; case 'user_cancel_reassign': @@ -1474,7 +1474,7 @@ function node_user_cancel($edit, $account, $method) { ->condition('uid', $account->uid) ->execute() ->fetchCol(); - node_mass_update($nodes, array('uid' => 0)); + node_mass_update($nodes, array('uid' => 0), TRUE); // Anonymize old revisions. db_update('node_revision') ->fields(array('uid' => 0)) @@ -1644,8 +1644,7 @@ function node_menu() { $items['admin/content'] = array( 'title' => 'Content', 'description' => 'Find and manage content.', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('node_admin_content'), + 'page callback' => 'node_admin_nodes', 'access arguments' => array('access content overview'), 'weight' => -10, 'file' => 'node.admin.inc', diff --git a/core/modules/node/node.routing.yml b/core/modules/node/node.routing.yml new file mode 100644 index 0000000..0ba0ec2 --- /dev/null +++ b/core/modules/node/node.routing.yml @@ -0,0 +1,7 @@ +node_multiple_delete_confirm: + pattern: '/admin/content/node/delete' + defaults: + _form: '\Drupal\node\Form\DeleteMultiple' + requirements: + _permission: 'administer nodes' + diff --git a/core/modules/node/node.views.inc b/core/modules/node/node.views.inc index 6f3320f..f344cfe 100644 --- a/core/modules/node/node.views.inc +++ b/core/modules/node/node.views.inc @@ -219,6 +219,14 @@ function node_views_data() { ); } + $data['node']['node_bulk_form'] = array( + 'title' => t('Node operations bulk form'), + 'help' => t('Add a form element that lets you run operations on multiple nodes.'), + 'field' => array( + 'id' => 'node_bulk_form', + ), + ); + // Define some fields based upon views_handler_field_entity in the entity // table so they can be re-used with other query backends. // @see views_handler_field_entity diff --git a/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTestStorageController.php b/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTestStorageController.php new file mode 100644 index 0000000..3e58980 --- /dev/null +++ b/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTestStorageController.php @@ -0,0 +1,25 @@ +get('node_test.storage_controller')) { + $entity_info['node']['controllers']['storage'] = 'Drupal\node_test\NodeTestStorageController'; + } +} diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml new file mode 100644 index 0000000..9a0bec6 --- /dev/null +++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml @@ -0,0 +1,45 @@ +base_field: nid +base_table: node +core: 8.x +description: '' +status: '1' +display: + default: + display_plugin: default + id: default + display_title: Master + position: '' + display_options: + style: + type: table + row: + type: fields + fields: + node_bulk_form: + id: node_bulk_form + table: node + field: node_bulk_form + plugin_id: node_bulk_form + title: + id: title + table: node + field: title + plugin_id: node + sorts: + nid: + id: nid + table: node + field: nid + order: ASC + plugin_id: standard + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: '' + display_options: + path: test-node-bulk-form +label: '' +module: views +id: test_node_bulk_form +tag: '' diff --git a/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php b/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php index 0e7c429..9c6e992 100644 --- a/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php +++ b/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php @@ -256,6 +256,7 @@ function testTrackerCronIndexing() { * Tests that publish/unpublish works at admin/content/node. */ function testTrackerAdminUnpublish() { + module_enable(array('views')); $admin_user = $this->drupalCreateUser(array('access content overview', 'administer nodes', 'bypass node access')); $this->drupalLogin($admin_user); @@ -270,10 +271,10 @@ function testTrackerAdminUnpublish() { // Unpublish the node and ensure that it's no longer displayed. $edit = array( - 'operation' => 'unpublish', - 'nodes[' . $node->nid . ']' => $node->nid, + 'action' => 'unpublish', + 'node_bulk_form[0]' => TRUE, ); - $this->drupalPost('admin/content', $edit, t('Update')); + $this->drupalPost('admin/content', $edit, t('Apply')); $this->drupalGet('tracker'); $this->assertText(t('No content available.'), 'Node is displayed on the tracker listing pages.'); diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php index 652c5b5..e9322ff 100644 --- a/core/modules/user/user.api.php +++ b/core/modules/user/user.api.php @@ -124,7 +124,7 @@ function hook_user_cancel($edit, $account, $method) { ->condition('uid', $account->uid) ->execute() ->fetchCol(); - node_mass_update($nodes, array('status' => 0)); + node_mass_update($nodes, array('status' => 0), TRUE); break; case 'user_cancel_reassign': @@ -135,7 +135,7 @@ function hook_user_cancel($edit, $account, $method) { ->condition('uid', $account->uid) ->execute() ->fetchCol(); - node_mass_update($nodes, array('uid' => 0)); + node_mass_update($nodes, array('uid' => 0), TRUE); // Anonymize old revisions. db_update('node_revision') ->fields(array('uid' => 0))