Commit 7940793a authored by webchick's avatar webchick

Issue #2465907 by mkalkbrenner, cedric_a, plach, Gábor Hojtsy, matsbla: Node...

Issue #2465907 by mkalkbrenner, cedric_a, plach, Gábor Hojtsy, matsbla: Node revision UI reverts multiple languages when only one language should be reverted
parent 0a67ffba
......@@ -68,6 +68,16 @@ node.revision_revert_confirm:
options:
_node_operation_route: TRUE
node.revision_revert_translation_confirm:
path: '/node/{node}/revisions/{node_revision}/revert/{langcode}'
defaults:
_form: '\Drupal\node\Form\NodeRevisionRevertTranslationForm'
_title: 'Revert to earlier revision of a translation'
requirements:
_access_node_revision: 'update'
options:
_node_operation_route: TRUE
node.revision_delete_confirm:
path: '/node/{node}/revisions/{node_revision}/delete'
defaults:
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\node\NodeTypeInterface;
......@@ -153,11 +154,14 @@ public function revisionPageTitle($node_revision) {
*/
public function revisionOverview(NodeInterface $node) {
$account = $this->currentUser();
$langcode = $this->languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
$langname = $this->languageManager()->getLanguageName($langcode);
$languages = $node->getTranslationLanguages();
$has_translations = (count($languages) > 1);
$node_storage = $this->entityManager()->getStorage('node');
$type = $node->getType();
$build = array();
$build['#title'] = $this->t('Revisions for %title', array('%title' => $node->label()));
$build['#title'] = $has_translations ? $this->t('@langname revisions for %title', ['@langname' => $langname, '%title' => $node->label()]) : $this->t('Revisions for %title', ['%title' => $node->label()]);
$header = array($this->t('Revision'), $this->t('Operations'));
$revert_permission = (($account->hasPermission("revert $type revisions") || $account->hasPermission('revert all revisions') || $account->hasPermission('administer nodes')) && $node->access('update'));
......@@ -167,74 +171,83 @@ public function revisionOverview(NodeInterface $node) {
$vids = $node_storage->revisionIds($node);
$latest_revision = TRUE;
foreach (array_reverse($vids) as $vid) {
/** @var \Drupal\node\NodeInterface $revision */
$revision = $node_storage->loadRevision($vid);
$username = [
'#theme' => 'username',
'#account' => $revision->uid->entity,
];
// Use revision link to link to revisions that are not active.
$date = $this->dateFormatter->format($revision->revision_timestamp->value, 'short');
if ($vid != $node->getRevisionId()) {
$link = $this->l($date, new Url('entity.node.revision', ['node' => $node->id(), 'node_revision' => $vid]));
}
else {
$link = $node->link($date);
}
if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) {
$username = [
'#theme' => 'username',
'#account' => $revision->uid->entity,
];
$row = [];
$column = [
'data' => [
'#type' => 'inline_template',
'#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}',
'#context' => [
'date' => $link,
'username' => $this->renderer->renderPlain($username),
'message' => ['#markup' => $revision->revision_log->value, '#allowed_tags' => Xss::getHtmlTagList()],
],
],
];
// @todo Simplify once https://www.drupal.org/node/2334319 lands.
$this->renderer->addCacheableDependency($column['data'], $username);
$row[] = $column;
if ($vid == $node->getRevisionId()) {
$row[0]['class'] = ['revision-current'];
$row[] = [
// Use revision link to link to revisions that are not active.
$date = $this->dateFormatter->format($revision->revision_timestamp->value, 'short');
if ($vid != $node->getRevisionId()) {
$link = $this->l($date, new Url('entity.node.revision', ['node' => $node->id(), 'node_revision' => $vid]));
}
else {
$link = $node->link($date);
}
$row = [];
$column = [
'data' => [
'#prefix' => '<em>',
'#markup' => $this->t('current revision'),
'#suffix' => '</em>',
'#type' => 'inline_template',
'#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}',
'#context' => [
'date' => $link,
'username' => $this->renderer->renderPlain($username),
'message' => ['#markup' => $revision->revision_log->value, '#allowed_tags' => Xss::getHtmlTagList()],
],
],
'class' => ['revision-current'],
];
}
else {
$links = [];
if ($revert_permission) {
$links['revert'] = [
'title' => $this->t('Revert'),
'url' => Url::fromRoute('node.revision_revert_confirm', ['node' => $node->id(), 'node_revision' => $vid]),
// @todo Simplify once https://www.drupal.org/node/2334319 lands.
$this->renderer->addCacheableDependency($column['data'], $username);
$row[] = $column;
if ($latest_revision) {
$row[] = [
'data' => [
'#prefix' => '<em>',
'#markup' => $this->t('Current revision'),
'#suffix' => '</em>',
],
];
foreach ($row as &$current) {
$current['class'] = ['revision-current'];
}
$latest_revision = FALSE;
}
if ($delete_permission) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => Url::fromRoute('node.revision_delete_confirm', ['node' => $node->id(), 'node_revision' => $vid]),
else {
$links = [];
if ($revert_permission) {
$links['revert'] = [
'title' => $this->t('Revert'),
'url' => $has_translations ?
Url::fromRoute('node.revision_revert_translation_confirm', ['node' => $node->id(), 'node_revision' => $vid, 'langcode' => $langcode]) :
Url::fromRoute('node.revision_revert_confirm', ['node' => $node->id(), 'node_revision' => $vid]),
];
}
if ($delete_permission) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => Url::fromRoute('node.revision_delete_confirm', ['node' => $node->id(), 'node_revision' => $vid]),
];
}
$row[] = [
'data' => [
'#type' => 'operations',
'#links' => $links,
],
];
}
$row[] = [
'data' => [
'#type' => 'operations',
'#links' => $links,
],
];
$rows[] = $row;
}
$rows[] = $row;
}
$build['node_revisions_table'] = array(
......
......@@ -7,6 +7,7 @@
namespace Drupal\node\Form;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
......@@ -33,14 +34,24 @@ class NodeRevisionRevertForm extends ConfirmFormBase {
*/
protected $nodeStorage;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* Constructs a new NodeRevisionRevertForm.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $node_storage
* The node storage.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
*/
public function __construct(EntityStorageInterface $node_storage) {
public function __construct(EntityStorageInterface $node_storage, DateFormatter $date_formatter) {
$this->nodeStorage = $node_storage;
$this->dateFormatter = $date_formatter;
}
/**
......@@ -48,7 +59,8 @@ public function __construct(EntityStorageInterface $node_storage) {
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager')->getStorage('node')
$container->get('entity.manager')->getStorage('node'),
$container->get('date.formatter')
);
}
......@@ -63,7 +75,7 @@ public function getFormId() {
* {@inheritdoc}
*/
public function getQuestion() {
return t('Are you sure you want to revert to the revision from %revision-date?', array('%revision-date' => format_date($this->revision->getRevisionCreationTime())));
return t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
}
/**
......@@ -101,17 +113,16 @@ public function buildForm(array $form, FormStateInterface $form_state, $node_rev
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$revision = $this->prepareRevertedRevision($this->revision);
// The revision timestamp will be updated when the revision is saved. Keep the
// original one for the confirmation message.
$original_revision_timestamp = $revision->getRevisionCreationTime();
$revision->revision_log = t('Copy of the revision from %date.', array('%date' => format_date($original_revision_timestamp)));
// The revision timestamp will be updated when the revision is saved. Keep
// the original one for the confirmation message.
$original_revision_timestamp = $this->revision->getRevisionCreationTime();
$revision->save();
$this->revision = $this->prepareRevertedRevision($this->revision, $form_state);
$this->revision->revision_log = t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]);
$this->revision->save();
$this->logger('content')->notice('@type: reverted %title revision %revision.', array('@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()));
drupal_set_message(t('@type %title has been reverted to the revision from %revision-date.', array('@type' => node_get_type_label($this->revision), '%title' => $this->revision->label(), '%revision-date' => format_date($original_revision_timestamp))));
$this->logger('content')->notice('@type: reverted %title revision %revision.', ['@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]);
drupal_set_message(t('@type %title has been reverted to the revision from %revision-date.', ['@type' => node_get_type_label($this->revision), '%title' => $this->revision->label(), '%revision-date' => $this->dateFormatter->format($original_revision_timestamp)]));
$form_state->setRedirect(
'entity.node.version_history',
array('node' => $this->revision->id())
......@@ -123,34 +134,13 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
*
* @param \Drupal\node\NodeInterface $revision
* The revision to be reverted.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return \Drupal\node\NodeInterface
* The prepared revision ready to be stored.
*/
protected function prepareRevertedRevision(NodeInterface $revision) {
/** @var \Drupal\node\NodeInterface $default_revision */
$default_revision = $this->nodeStorage->load($revision->id());
// If the entity is translated, make sure only translations affected by the
// specified revision are reverted.
$languages = $default_revision->getTranslationLanguages();
if (count($languages) > 1) {
// @todo Instead of processing all the available translations, we should
// let the user decide which translations should be reverted. See
// https://www.drupal.org/node/2465907.
foreach ($languages as $langcode => $language) {
if ($revision->hasTranslation($langcode) && !$revision->getTranslation($langcode)->isRevisionTranslationAffected()) {
$revision_translation = $revision->getTranslation($langcode);
$default_translation = $default_revision->getTranslation($langcode);
foreach ($default_revision->getFieldDefinitions() as $field_name => $definition) {
if ($definition->isTranslatable()) {
$revision_translation->set($field_name, $default_translation->get($field_name)->getValue());
}
}
}
}
}
protected function prepareRevertedRevision(NodeInterface $revision, FormStateInterface $form_state) {
$revision->setNewRevision();
$revision->isDefaultRevision(TRUE);
......
<?php
/**
* @file
* Contains \Drupal\node\Form\NodeRevisionRevertTranslationForm.
*/
namespace Drupal\node\Form;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for reverting a node revision for a single translation.
*/
class NodeRevisionRevertTranslationForm extends NodeRevisionRevertForm {
/**
* The language to be reverted.
*
* @var string
*/
protected $langcode;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a new NodeRevisionRevertTranslationForm.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $node_storage
* The node storage.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(EntityStorageInterface $node_storage, DateFormatter $date_formatter, LanguageManagerInterface $language_manager) {
parent::__construct($node_storage, $date_formatter);
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager')->getStorage('node'),
$container->get('date.formatter'),
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'node_revision_revert_translation_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return t('Are you sure you want to revert @language translation to the revision from %revision-date?', ['@language' => $this->languageManager->getLanguageName($this->langcode), '%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $node_revision = NULL, $langcode = NULL) {
$this->langcode = $langcode;
$form = parent::buildForm($form, $form_state, $node_revision);
$form['revert_untranslated_fields'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Revert content shared among translations'),
'#default_value' => FALSE,
);
return $form;
}
/**
* {@inheritdoc}
*/
protected function prepareRevertedRevision(NodeInterface $revision, FormStateInterface $form_state) {
$revert_untranslated_fields = $form_state->getValue('revert_untranslated_fields');
/** @var \Drupal\node\NodeInterface $default_revision */
$latest_revision = $this->nodeStorage->load($revision->id());
$latest_revision_translation = $latest_revision->getTranslation($this->langcode);
$revision_translation = $revision->getTranslation($this->langcode);
foreach ($latest_revision_translation->getFieldDefinitions() as $field_name => $definition) {
if ($definition->isTranslatable() || $revert_untranslated_fields) {
$latest_revision_translation->set($field_name, $revision_translation->get($field_name)->getValue());
}
}
$latest_revision_translation->setNewRevision();
$latest_revision_translation->isDefaultRevision(TRUE);
return $latest_revision_translation;
}
}
......@@ -7,6 +7,9 @@
namespace Drupal\node\Tests;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
......@@ -34,9 +37,22 @@ protected function setUp() {
ConfigurableLanguage::createFromLangcode('it')->save();
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
$manager = \Drupal::service('content_translation.manager');
$manager->setEnabled('node', 'article', TRUE);
$field_storage_definition = array(
'field_name' => 'untranslatable_string_field',
'entity_type' => 'node',
'type' => 'string',
'cardinality' => 1,
'translatable' => FALSE,
);
$field_storage = FieldStorageConfig::create($field_storage_definition);
$field_storage->save();
$field_definition = array(
'field_storage' => $field_storage,
'bundle' => 'page',
);
$field = FieldConfig::create($field_definition);
$field->save();
// Create and log in user.
$web_user = $this->drupalCreateUser(
......@@ -75,6 +91,7 @@ protected function setUp() {
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
);
$node->untranslatable_string_field->value = $this->randomString();
$node->setNewRevision();
$node->save();
......@@ -227,6 +244,9 @@ function testNodeRevisionWithoutLogMessage() {
public function testRevisionTranslationRevert() {
// Create a node and a few revisions.
$node = $this->drupalCreateNode(['langcode' => 'en']);
$initial_revision_id = $node->getRevisionId();
$initial_title = $node->label();
$this->createRevisions($node, 2);
// Translate the node and create a few translation revisions.
......@@ -234,6 +254,7 @@ public function testRevisionTranslationRevert() {
$this->createRevisions($translation, 3);
$revert_id = $node->getRevisionId();
$translated_title = $translation->label();
$untranslatable_string = $node->untranslatable_string_field->value;
// Create a new revision for the default translation in-between a series of
// translation revisions.
......@@ -247,7 +268,12 @@ public function testRevisionTranslationRevert() {
// Now revert the a translation revision preceding the last default
// translation revision, and check that the desired value was reverted but
// the default translation value was preserved.
$this->drupalPostForm("node/" . $node->id() . "/revisions/" . $revert_id . "/revert", [], t('Revert'));
$revert_translation_url = Url::fromRoute('node.revision_revert_translation_confirm', [
'node' => $node->id(),
'node_revision' => $revert_id,
'langcode' => 'it',
]);
$this->drupalPostForm($revert_translation_url, [], t('Revert'));
/** @var \Drupal\node\NodeStorage $node_storage */
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$node_storage->resetCache();
......@@ -256,6 +282,38 @@ public function testRevisionTranslationRevert() {
$this->assertTrue($node->getRevisionId() > $translation_revision_id);
$this->assertEqual($node->label(), $default_translation_title);
$this->assertEqual($node->getTranslation('it')->label(), $translated_title);
$this->assertNotEqual($node->untranslatable_string_field->value, $untranslatable_string);
$latest_revision_id = $translation->getRevisionId();
// Now revert the a translation revision preceding the last default
// translation revision again, and check that the desired value was reverted
// but the default translation value was preserved. But in addition the
// untranslated field will be reverted as well.
$this->drupalPostForm($revert_translation_url, ['revert_untranslated_fields' => TRUE], t('Revert'));
$node_storage->resetCache();
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->load($node->id());
$this->assertTrue($node->getRevisionId() > $latest_revision_id);
$this->assertEqual($node->label(), $default_translation_title);
$this->assertEqual($node->getTranslation('it')->label(), $translated_title);
$this->assertEqual($node->untranslatable_string_field->value, $untranslatable_string);
$latest_revision_id = $translation->getRevisionId();
// Now revert the entity revision to the initial one where the translation
// didn't exist.
$revert_url = Url::fromRoute('node.revision_revert_confirm', [
'node' => $node->id(),
'node_revision' => $initial_revision_id,
]);
$this->drupalPostForm($revert_url, [], t('Revert'));
$node_storage->resetCache();
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->load($node->id());
$this->assertTrue($node->getRevisionId() > $latest_revision_id);
$this->assertEqual($node->label(), $initial_title);
$this->assertFalse($node->hasTranslation('it'));
}
/**
......@@ -269,6 +327,7 @@ public function testRevisionTranslationRevert() {
protected function createRevisions(NodeInterface $node, $count) {
for ($i = 0; $i < $count; $i++) {
$node->title = $this->randomString();
$node->untranslatable_string_field->value = $this->randomString();
$node->setNewRevision(TRUE);
$node->save();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment