Commit 83f9d583 authored by alexpott's avatar alexpott

Issue #1895160 by tim.plunkett: Convert admin/content to a View, keep a...

Issue #1895160 by tim.plunkett: Convert admin/content to a View, keep a non-views fallback with no bulk operations.
parent f45ae901
......@@ -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');
}
......
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
<?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 {
}
......@@ -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;
}
}
<?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));
}
}
}
......@@ -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');
......
<?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);
}
}
This diff is collapsed.
......@@ -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',
......
......@@ -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
......
<?php
/**
* @file
* Contains \Drupal\node_test\NodeTestStorageController.
*/
namespace Drupal\node_test;
use Drupal\node\NodeStorageController;
use Drupal\Core\Entity\EntityInterface;