diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php index bc6ae1a0d08131d5b1d8f77d4acadc2cf0bf7bea..c55927fb104e4bcf315eb0c607f88677b8d1619f 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleContentTest.php @@ -212,6 +212,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')); @@ -227,14 +228,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 0000000000000000000000000000000000000000..a30f1cd511f9050b2dd408db51f061bbba125e20 --- /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_data + 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_data + 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_data + 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_data + 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_data + field: status_extra + operator: '=' + value: '' + plugin_id: node_status + status: + id: status + table: node_field_data + 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_data + 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_data + 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/NodeBCDecorator.php b/core/modules/node/lib/Drupal/node/NodeBCDecorator.php new file mode 100644 index 0000000000000000000000000000000000000000..9fa6bbfb776d4fe879fec36e4357193d90cd28b3 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/NodeBCDecorator.php @@ -0,0 +1,16 @@ +<?php + +/** + * @file + * Contains \Drupal\node\NodeBCDecorator. + */ + +namespace Drupal\node; + +use Drupal\Core\Entity\EntityBCDecorator; + +/** + * Defines the node specific entity BC decorator. + */ +class NodeBCDecorator extends EntityBCDecorator implements NodeInterface { +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php index 55031006d24166f9842daa6758c3d410f8bd50c2..eb047c1094f88f9e3db096989cfc1401753fc4df 100644 --- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php +++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\Annotation\EntityType; use Drupal\Core\Annotation\Translation; use Drupal\node\NodeInterface; +use Drupal\node\NodeBCDecorator; /** * Defines the node entity class. @@ -235,4 +236,15 @@ public function getRevisionId() { return $this->get('vid')->value; } + /** + * {@inheritdoc} + */ + public function getBCEntity() { + if (!isset($this->bcEntity)) { + $this->getPropertyDefinitions(); + $this->bcEntity = new NodeBCDecorator($this, $this->fieldDefinitions); + } + return $this->bcEntity; + } + } 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 0000000000000000000000000000000000000000..da633d710dce9c231befa919e2d72b857258d5a7 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/NodeBulkForm.php @@ -0,0 +1,63 @@ +<?php + +/** + * @file + * Contains \Drupal\node\Plugin\views\field\NodeBulkForm. + */ + +namespace Drupal\node\Plugin\views\field; + +use Drupal\Component\Annotation\PluginID; +use Drupal\system\Plugin\views\field\BulkFormBase; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Entity\EntityManager; + +/** + * Defines a node operations bulk form element. + * + * @PluginID("node_bulk_form") + */ +class NodeBulkForm extends BulkFormBase { + + /** + * Constructs a new NodeBulkForm object. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityManager $manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $manager); + + // Filter the actions to only include those for the 'node' entity type. + $this->actions = array_filter($this->actions, function ($action) { + return $action->getType() == 'node'; + }); + } + + /** + * {@inheritdoc} + */ + protected function getBulkOptions() { + return array_map(function ($action) { + return $action->label(); + }, $this->actions); + } + + /** + * {@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) { + parent::views_form_submit($form, $form_state); + if ($form_state['step'] == 'views_form_views_form') { + Cache::invalidateTags(array('content' => TRUE)); + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAdminTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAdminTest.php index e34f61b438186206586ffb03491145f451704e91..c96c68cfca25fe7736cdd7212d39090a375969fb 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. + $this->container->get('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_field_data', '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_field_data', '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 0000000000000000000000000000000000000000..838c553d2cadd3a550d4263e074dbcfa7c6518f5 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Tests/Views/BulkFormTest.php @@ -0,0 +1,55 @@ +<?php + +/** + * @file + * Contains \Drupal\node\Tests\Views\BulkFormTest. + */ + +namespace Drupal\node\Tests\Views; + +/** + * Tests the views bulk form test. + * + * @see \Drupal\node\Plugin\views\field\BulkForm + */ +class BulkFormTest extends NodeTestBase { + + /** + * Views used by this test. + * + * @var array + */ + public static $testViews = array('test_node_bulk_form'); + + public static function getInfo() { + return array( + 'name' => '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), 8, 'All node operations are found.'); + + // Block a node using the bulk form. + $this->assertTrue($node->status); + $edit = array( + 'node_bulk_form[0]' => TRUE, + 'action' => 'node_unpublish_action', + ); + $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 47ff29311a775fca65e5d72f6d8ce7edb7c5d3a4..821f41a537d0ec910c771ef29bf256597c7eaadf 100644 --- a/core/modules/node/node.admin.inc +++ b/core/modules/node/node.admin.inc @@ -5,8 +5,8 @@ * Content administration and module settings user interface. */ -use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Language\Language; +use Drupal\node\NodeInterface; /** * Page callback: Form constructor for the permission rebuild confirmation form. @@ -31,197 +31,6 @@ function node_configure_rebuild_confirm_submit($form, &$form_state) { $form_state['redirect'] = 'admin/reports/status'; } -/** - * 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::STATE_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 ? '<div class="additional-filters">' . t('and where') . '</div>' : ''), - ); - $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. * @@ -230,21 +39,24 @@ 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 string $langcode * (optional) The language updates should be applied to. If none is specified * all available languages are processed. + * @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, $langcode = NULL) { +function node_mass_update(array $nodes, array $updates, $langcode = NULL, $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, $langcode)) + array('_node_mass_update_batch_process', array($nodes, $updates, $langcode, $load)) ), 'finished' => '_node_mass_update_batch_finished', 'title' => t('Processing'), @@ -259,8 +71,11 @@ function node_mass_update($nodes, $updates, $langcode = NULL) { batch_set($batch); } else { - foreach ($nodes as $nid) { - _node_mass_update_helper($nid, $updates, $langcode); + if ($load) { + $nodes = entity_load_multiple('node', $nodes); + } + foreach ($nodes as $node) { + _node_mass_update_helper($node, $updates, $langcode); } drupal_set_message(t('The update has been performed.')); } @@ -269,21 +84,20 @@ function node_mass_update($nodes, $updates, $langcode = NULL) { /** * Updates individual nodes when fewer than 10 are queued. * - * @param $nid - * ID of node to update. - * @param $updates + * @param \Drupal\node\NodeInterface $node + * A node to update. + * @param array $updates * Associative array of updates. * @param string $langcode * (optional) The language updates should be applied to. If none is specified * all available languages are processed. * - * @return object + * @return \Drupal\node\NodeInterface * An updated node object. * * @see node_mass_update() */ -function _node_mass_update_helper($nid, $updates, $langcode = NULL) { - $node = node_load($nid, TRUE); +function _node_mass_update_helper(NodeInterface $node, array $updates, $langcode = NULL) { $langcodes = isset($langcode) ? array($langcode) : array_keys($node->getTranslationLanguages()); // For efficiency manually save the original node before applying any changes. $node->original = clone $node; @@ -303,10 +117,13 @@ function _node_mass_update_helper($nid, $updates, $langcode = NULL) { * 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(array $nodes, array $updates, $load, array &$context) { if (!isset($context['sandbox']['progress'])) { $context['sandbox']['progress'] = 0; $context['sandbox']['max'] = count($nodes); @@ -317,8 +134,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); @@ -361,70 +181,12 @@ 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(); - $actions = entity_load_multiple_by_properties('action', array('type' => 'node')); - foreach ($actions as $id => $action) { - $options[$id] = $action->label(); - } - $form['options']['operation'] = array( - '#type' => 'select', - '#title' => t('Action'), - '#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(); @@ -462,7 +224,6 @@ function node_admin_nodes() { $query = db_select('node_field_data', '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 @@ -573,95 +334,6 @@ 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) { - if ($action = entity_load('action', $form_state['values']['operation'])) { - $nodes = entity_load_multiple('node', array_filter($form_state['values']['nodes'])); - $action->execute($nodes); - $operation_definition = $action->getPluginDefinition(); - if (!empty($operation_definition['confirm_form_path'])) { - $form_state['redirect'] = $operation_definition['confirm_form_path']; - } - 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; - } -} - -/** - * 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' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE); - $node_entities = node_load_multiple(array_keys($nodes)); - // array_filter returns only elements with TRUE values - foreach ($nodes as $nid => $value) { - $form['nodes'][$nid] = array( - '#type' => 'hidden', - '#value' => $nid, - '#prefix' => '<li>', - '#suffix' => check_plain($node_entities[$nid]->label()) . "</li>\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']) { - entity_delete_multiple('node', 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'; -} diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 73f5dcade5ae7a8a20985cf52f7478baaa0816eb..c297fd90a7466d1563de63d009c62361a8f6799d 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1407,7 +1407,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), NULL, TRUE); break; case 'user_cancel_reassign': @@ -1419,7 +1419,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), NULL, TRUE); // Anonymize old revisions. db_update('node_field_revision') ->fields(array('uid' => 0)) @@ -1595,8 +1595,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.views.inc b/core/modules/node/node.views.inc index ac427313f323c937010f7d7f0280972106d39ccb..52608b72f895bbdcd420ed29d1298965fbd5bb55 100644 --- a/core/modules/node/node.views.inc +++ b/core/modules/node/node.views.inc @@ -227,6 +227,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 0000000000000000000000000000000000000000..3e58980709930d82e002c6b7674c6d061d7feeff --- /dev/null +++ b/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTestStorageController.php @@ -0,0 +1,25 @@ +<?php + +/** + * @file + * Contains \Drupal\node_test\NodeTestStorageController. + */ + +namespace Drupal\node_test; + +use Drupal\node\NodeStorageController; +use Drupal\Core\Entity\EntityInterface; + +/** + * Provides a test storage controller for nodes. + */ +class NodeTestStorageController extends NodeStorageController { + + /** + * {@inheritdoc} + */ + protected function preSave(EntityInterface $node) { + // Allow test nodes to specify their updated ('changed') time. + } + +} diff --git a/core/modules/node/tests/modules/node_test/node_test.module b/core/modules/node/tests/modules/node_test/node_test.module index c2ff3b40aea10a9e5771f42f74597da8bf892a0c..e65f9bcd7d9aeffbb0d3989d173da54b408e7f01 100644 --- a/core/modules/node/tests/modules/node_test/node_test.module +++ b/core/modules/node/tests/modules/node_test/node_test.module @@ -180,3 +180,12 @@ function node_test_node_insert(EntityInterface $node) { $node->save(); } } + +/** + * Implements hook_entity_info_alter(). + */ +function node_test_entity_info_alter(&$entity_info) { + if (Drupal::state()->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 0000000000000000000000000000000000000000..f5d998e98e2b253bc6c7c063e952091b07ea2f0f --- /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_data + 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/system/lib/Drupal/system/Plugin/views/field/BulkFormBase.php b/core/modules/system/lib/Drupal/system/Plugin/views/field/BulkFormBase.php index 348085ca014aa1f66dc557f1c12578b6b1958b58..4bc26dd76483aab77e5637ddcead55e828684495 100644 --- a/core/modules/system/lib/Drupal/system/Plugin/views/field/BulkFormBase.php +++ b/core/modules/system/lib/Drupal/system/Plugin/views/field/BulkFormBase.php @@ -152,7 +152,7 @@ public function views_form_submit(&$form, &$form_state) { $operation_definition = $action->getPluginDefinition(); if (!empty($operation_definition['confirm_form_path'])) { - $form_state['confirm_form_path'] = $operation_definition['confirm_form_path']; + $form_state['redirect'] = $operation_definition['confirm_form_path']; } } } diff --git a/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php b/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php index d2d992cb5cbb3c8425ae92cc614c7200f0fac145..b68b7220ccf67761563f83bb1bc293d77aa8bcc0 100644 --- a/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php +++ b/core/modules/tracker/lib/Drupal/tracker/Tests/TrackerTest.php @@ -257,6 +257,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); @@ -271,10 +272,10 @@ function testTrackerAdminUnpublish() { // Unpublish the node and ensure that it's no longer displayed. $edit = array( - 'operation' => 'node_unpublish_action', - 'nodes[' . $node->nid . ']' => $node->nid, + 'action' => 'node_unpublish_action', + 'node_bulk_form[0]' => $node->nid, ); - $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 ae61b93a2840f82327c339ca586822819b425193..2f337d94bdcc9c98dde6537d1b8c7e053b89d007 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), NULL, 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), NULL, TRUE); // Anonymize old revisions. db_update('node_field_revision') ->fields(array('uid' => 0))