Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176
This commit is contained in:
commit
9921556621
13277 changed files with 1459781 additions and 0 deletions
62
core/modules/history/src/Controller/HistoryController.php
Normal file
62
core/modules/history/src/Controller/HistoryController.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\history\Controller\HistoryController.
|
||||
*/
|
||||
|
||||
namespace Drupal\history\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\node\NodeInterface;
|
||||
|
||||
/**
|
||||
* Returns responses for History module routes.
|
||||
*/
|
||||
class HistoryController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* Returns a set of nodes' last read timestamps.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The request of the page.
|
||||
*
|
||||
* @return Symfony\Component\HttpFoundation\JsonResponse
|
||||
* The JSON response.
|
||||
*/
|
||||
public function getNodeReadTimestamps(Request $request) {
|
||||
if ($this->currentUser()->isAnonymous()) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$nids = $request->request->get('node_ids');
|
||||
if (!isset($nids)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
return new JsonResponse(history_read_multiple($nids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a node as read by the current user right now.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The request of the page.
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The node whose "last read" timestamp should be updated.
|
||||
*/
|
||||
public function readNode(Request $request, NodeInterface $node) {
|
||||
if ($this->currentUser()->isAnonymous()) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
// Update the history table, stating that this user viewed this node.
|
||||
history_write($node->id());
|
||||
|
||||
return new JsonResponse((int)history_read($node->id()));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\history\Plugin\views\field\HistoryUserTimestamp.
|
||||
*/
|
||||
|
||||
namespace Drupal\history\Plugin\views\field;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\views\ResultRow;
|
||||
use Drupal\views\ViewExecutable;
|
||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
|
||||
use Drupal\node\Plugin\views\field\Node;
|
||||
|
||||
/**
|
||||
* Field handler to display the marker for new content.
|
||||
*
|
||||
* The handler is named history_user, because of compatibility reasons, the
|
||||
* table is history.
|
||||
*
|
||||
* @ingroup views_field_handlers
|
||||
*
|
||||
* @ViewsField("history_user_timestamp")
|
||||
*/
|
||||
class HistoryUserTimestamp extends Node {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function usesGroupBy() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\node\Plugin\views\field\Node::init().
|
||||
*/
|
||||
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
|
||||
parent::init($view, $display, $options);
|
||||
|
||||
if (\Drupal::currentUser()->isAuthenticated()) {
|
||||
$this->additional_fields['created'] = array('table' => 'node_field_data', 'field' => 'created');
|
||||
$this->additional_fields['changed'] = array('table' => 'node_field_data', 'field' => 'changed');
|
||||
if (\Drupal::moduleHandler()->moduleExists('comment') && !empty($this->options['comments'])) {
|
||||
$this->additional_fields['last_comment'] = array('table' => 'comment_entity_statistics', 'field' => 'last_comment_timestamp');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function defineOptions() {
|
||||
$options = parent::defineOptions();
|
||||
|
||||
$options['comments'] = array('default' => FALSE);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
|
||||
parent::buildOptionsForm($form, $form_state);
|
||||
if (\Drupal::moduleHandler()->moduleExists('comment')) {
|
||||
$form['comments'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Check for new comments as well'),
|
||||
'#default_value' => !empty($this->options['comments']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function query() {
|
||||
// Only add ourselves to the query if logged in.
|
||||
if (\Drupal::currentUser()->isAnonymous()) {
|
||||
return;
|
||||
}
|
||||
parent::query();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render(ResultRow $values) {
|
||||
// Let's default to 'read' state.
|
||||
// This code shadows node_mark, but it reads from the db directly and
|
||||
// we already have that info.
|
||||
$mark = MARK_READ;
|
||||
if (\Drupal::currentUser()->isAuthenticated()) {
|
||||
$last_read = $this->getValue($values);
|
||||
$changed = $this->getValue($values, 'changed');
|
||||
|
||||
$last_comment = \Drupal::moduleHandler()->moduleExists('comment') && !empty($this->options['comments']) ? $this->getValue($values, 'last_comment') : 0;
|
||||
|
||||
if (!$last_read && $changed > HISTORY_READ_LIMIT) {
|
||||
$mark = MARK_NEW;
|
||||
}
|
||||
elseif ($changed > $last_read && $changed > HISTORY_READ_LIMIT) {
|
||||
$mark = MARK_UPDATED;
|
||||
}
|
||||
elseif ($last_comment > $last_read && $last_comment > HISTORY_READ_LIMIT) {
|
||||
$mark = MARK_UPDATED;
|
||||
}
|
||||
$build = array(
|
||||
'#theme' => 'mark',
|
||||
'#status' => $mark,
|
||||
);
|
||||
return $this->renderLink(drupal_render($build), $values);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\history\Plugin\views\filter\HistoryUserTimestamp.
|
||||
*/
|
||||
|
||||
namespace Drupal\history\Plugin\views\filter;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\views\Plugin\views\filter\FilterPluginBase;
|
||||
|
||||
/**
|
||||
* Filter for new content.
|
||||
*
|
||||
* The handler is named history_user, because of compatibility reasons, the
|
||||
* table is history.
|
||||
*
|
||||
* @ingroup views_filter_handlers
|
||||
*
|
||||
* @ViewsFilter("history_user_timestamp")
|
||||
*/
|
||||
class HistoryUserTimestamp extends FilterPluginBase {
|
||||
|
||||
// Don't display empty space where the operator would be.
|
||||
var $no_operator = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function usesGroupBy() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
public function buildExposeForm(&$form, FormStateInterface $form_state) {
|
||||
parent::buildExposeForm($form, $form_state);
|
||||
// @todo There are better ways of excluding required and multiple (object flags)
|
||||
unset($form['expose']['required']);
|
||||
unset($form['expose']['multiple']);
|
||||
unset($form['expose']['remember']);
|
||||
}
|
||||
|
||||
protected function valueForm(&$form, FormStateInterface $form_state) {
|
||||
// Only present a checkbox for the exposed filter itself. There's no way
|
||||
// to tell the difference between not checked and the default value, so
|
||||
// specifying the default value via the views UI is meaningless.
|
||||
if ($form_state->get('exposed')) {
|
||||
if (isset($this->options['expose']['label'])) {
|
||||
$label = $this->options['expose']['label'];
|
||||
}
|
||||
else {
|
||||
$label = $this->t('Has new content');
|
||||
}
|
||||
$form['value'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $label,
|
||||
'#default_value' => $this->value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function query() {
|
||||
// This can only work if we're authenticated in.
|
||||
if (!\Drupal::currentUser()->isAuthenticated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't filter if we're exposed and the checkbox isn't selected.
|
||||
if ((!empty($this->options['exposed'])) && empty($this->value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hey, Drupal kills old history, so nodes that haven't been updated
|
||||
// since HISTORY_READ_LIMIT are bzzzzzzzt outta here!
|
||||
|
||||
$limit = REQUEST_TIME - HISTORY_READ_LIMIT;
|
||||
|
||||
$this->ensureMyTable();
|
||||
$field = "$this->tableAlias.$this->realField";
|
||||
$node = $this->query->ensureTable('node_field_data', $this->relationship);
|
||||
|
||||
$clause = '';
|
||||
$clause2 = '';
|
||||
if (\Drupal::moduleHandler()->moduleExists('comment')) {
|
||||
$ces = $this->query->ensureTable('comment_entity_statistics', $this->relationship);
|
||||
$clause = ("OR $ces.last_comment_timestamp > (***CURRENT_TIME*** - $limit)");
|
||||
$clause2 = "OR $field < $ces.last_comment_timestamp";
|
||||
}
|
||||
|
||||
// NULL means a history record doesn't exist. That's clearly new content.
|
||||
// Unless it's very very old content. Everything in the query is already
|
||||
// type safe cause none of it is coming from outside here.
|
||||
$this->query->addWhereExpression($this->options['group'], "($field IS NULL AND ($node.changed > (***CURRENT_TIME*** - $limit) $clause)) OR $field < $node.changed $clause2");
|
||||
}
|
||||
|
||||
public function adminSummary() {
|
||||
if (!empty($this->options['exposed'])) {
|
||||
return $this->t('exposed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCacheable() {
|
||||
// This filter depends on the current time and therefore is never cacheable.
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
}
|
152
core/modules/history/src/Tests/HistoryTest.php
Normal file
152
core/modules/history/src/Tests/HistoryTest.php
Normal file
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\history\Tests\HistoryTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\history\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* Tests the History endpoints.
|
||||
*
|
||||
* @group history
|
||||
*/
|
||||
class HistoryTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('node', 'history');
|
||||
|
||||
/**
|
||||
* The main user for testing.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* A page node for which to check content statistics.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $testNode;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
|
||||
|
||||
$this->user = $this->drupalCreateUser(array('create page content', 'access content'));
|
||||
$this->drupalLogin($this->user);
|
||||
$this->testNode = $this->drupalCreateNode(array('type' => 'page', 'uid' => $this->user->id()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node read timestamps from the server for the current user.
|
||||
*
|
||||
* @param array $node_ids
|
||||
* An array of node IDs.
|
||||
*
|
||||
* @return string
|
||||
* The response body.
|
||||
*/
|
||||
protected function getNodeReadTimestamps(array $node_ids) {
|
||||
// Build POST values.
|
||||
$post = array();
|
||||
for ($i = 0; $i < count($node_ids); $i++) {
|
||||
$post['node_ids[' . $i . ']'] = $node_ids[$i];
|
||||
}
|
||||
|
||||
// Serialize POST values.
|
||||
foreach ($post as $key => $value) {
|
||||
// Encode according to application/x-www-form-urlencoded
|
||||
// Both names and values needs to be urlencoded, according to
|
||||
// http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
|
||||
$post[$key] = urlencode($key) . '=' . urlencode($value);
|
||||
}
|
||||
$post = implode('&', $post);
|
||||
|
||||
// Perform HTTP request.
|
||||
return $this->curlExec(array(
|
||||
CURLOPT_URL => \Drupal::url('history.get_last_node_view', array(), array('absolute' => TRUE)),
|
||||
CURLOPT_POST => TRUE,
|
||||
CURLOPT_POSTFIELDS => $post,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a node as read for the current user.
|
||||
*
|
||||
* @param int $node_id
|
||||
* A node ID.
|
||||
*
|
||||
* @return string
|
||||
* The response body.
|
||||
*/
|
||||
protected function markNodeAsRead($node_id) {
|
||||
return $this->curlExec(array(
|
||||
CURLOPT_URL => \Drupal::url('history.read_node', array('node' => $node_id), array('absolute' => TRUE)),
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Accept: application/json',
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the history endpoints work.
|
||||
*/
|
||||
function testHistory() {
|
||||
$nid = $this->testNode->id();
|
||||
|
||||
// Retrieve "last read" timestamp for test node, for the current user.
|
||||
$response = $this->getNodeReadTimestamps(array($nid));
|
||||
$this->assertResponse(200);
|
||||
$json = Json::decode($response);
|
||||
$this->assertIdentical(array(1 => 0), $json, 'The node has not yet been read.');
|
||||
|
||||
// View the node.
|
||||
$this->drupalGet('node/' . $nid);
|
||||
$this->assertCacheContext('user.roles:authenticated');
|
||||
// JavaScript present to record the node read.
|
||||
$settings = $this->getDrupalSettings();
|
||||
$libraries = explode(',', $settings['ajaxPageState']['libraries']);
|
||||
$this->assertTrue(in_array('history/mark-as-read', $libraries), 'history/mark-as-read library is present.');
|
||||
$this->assertEqual([$nid => TRUE], $settings['history']['nodesToMarkAsRead'], 'drupalSettings to mark node as read are present.');
|
||||
|
||||
// Simulate JavaScript: perform HTTP request to mark node as read.
|
||||
$response = $this->markNodeAsRead($nid);
|
||||
$this->assertResponse(200);
|
||||
$timestamp = Json::decode($response);
|
||||
$this->assertTrue(is_numeric($timestamp), 'Node has been marked as read. Timestamp received.');
|
||||
|
||||
// Retrieve "last read" timestamp for test node, for the current user.
|
||||
$response = $this->getNodeReadTimestamps(array($nid));
|
||||
$this->assertResponse(200);
|
||||
$json = Json::decode($response);
|
||||
$this->assertIdentical(array(1 => $timestamp), $json, 'The node has been read.');
|
||||
|
||||
// Failing to specify node IDs for the first endpoint should return a 404.
|
||||
$this->getNodeReadTimestamps(array());
|
||||
$this->assertResponse(404);
|
||||
|
||||
// Accessing either endpoint as the anonymous user should return a 403.
|
||||
$this->drupalLogout();
|
||||
$this->getNodeReadTimestamps(array($nid));
|
||||
$this->assertResponse(403);
|
||||
$this->getNodeReadTimestamps(array());
|
||||
$this->assertResponse(403);
|
||||
$this->markNodeAsRead($nid);
|
||||
$this->assertResponse(403);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\history\Tests\Views\HistoryTimestampTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\history\Tests\Views;
|
||||
|
||||
use Drupal\views\Views;
|
||||
use Drupal\views\Tests\ViewTestBase;
|
||||
|
||||
/**
|
||||
* Tests the history timestamp handlers.
|
||||
*
|
||||
* @group history
|
||||
* @see \Drupal\history\Plugin\views\field\HistoryTimestamp.
|
||||
* @see \Drupal\history\Plugin\views\filter\HistoryTimestamp.
|
||||
*/
|
||||
class HistoryTimestampTest extends ViewTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('history', 'node');
|
||||
|
||||
/**
|
||||
* Views used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = array('test_history');
|
||||
|
||||
/**
|
||||
* Tests the handlers.
|
||||
*/
|
||||
public function testHandlers() {
|
||||
$nodes = array();
|
||||
$nodes[] = $this->drupalCreateNode();
|
||||
$nodes[] = $this->drupalCreateNode();
|
||||
|
||||
$account = $this->drupalCreateUser();
|
||||
$this->drupalLogin($account);
|
||||
\Drupal::currentUser()->setAccount($account);
|
||||
|
||||
db_insert('history')
|
||||
->fields(array(
|
||||
'uid' => $account->id(),
|
||||
'nid' => $nodes[0]->id(),
|
||||
'timestamp' => REQUEST_TIME - 100,
|
||||
))->execute();
|
||||
|
||||
db_insert('history')
|
||||
->fields(array(
|
||||
'uid' => $account->id(),
|
||||
'nid' => $nodes[1]->id(),
|
||||
'timestamp' => REQUEST_TIME + 100,
|
||||
))->execute();
|
||||
|
||||
|
||||
$column_map = array(
|
||||
'nid' => 'nid',
|
||||
);
|
||||
|
||||
// Test the history field.
|
||||
$view = Views::getView('test_history');
|
||||
$view->setDisplay('page_1');
|
||||
$this->executeView($view);
|
||||
$this->assertEqual(count($view->result), 2);
|
||||
$output = $view->preview();
|
||||
$this->setRawContent(\Drupal::service('renderer')->renderRoot($output));
|
||||
$result = $this->xpath('//span[@class=:class]', array(':class' => 'marker'));
|
||||
$this->assertEqual(count($result), 1, 'Just one node is marked as new');
|
||||
|
||||
// Test the history filter.
|
||||
$view = Views::getView('test_history');
|
||||
$view->setDisplay('page_2');
|
||||
$this->executeView($view);
|
||||
$this->assertEqual(count($view->result), 1);
|
||||
$this->assertIdenticalResultset($view, array(array('nid' => $nodes[0]->id())), $column_map);
|
||||
}
|
||||
}
|
Reference in a new issue