Commit 5e61bc75 authored by catch's avatar catch

Issue #1807800 by plach: Added status and authoring information as generic...

Issue #1807800 by plach: Added status and authoring information as generic entity translation metadata.
parent c61c0dfc
......@@ -155,6 +155,13 @@ public function __unset($name) {
$value = array();
}
/**
* Implements the magic method for clone().
*/
function __clone() {
$this->decorated = clone $this->decorated;
}
/**
* Forwards the call to the decorated entity.
*/
......
......@@ -88,10 +88,38 @@ protected function getNewEntityValues($langcode) {
// Comment subject is not translatable hence we use a fixed value.
return array(
'subject' => $this->subject,
'comment_body' => array(array('value' => $this->randomString(16))),
'comment_body' => array(array('value' => $this->randomName(16))),
) + parent::getNewEntityValues($langcode);
}
/**
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::assertPublishedStatus().
*/
protected function assertPublishedStatus() {
parent::assertPublishedStatus();
$entity = entity_load($this->entityType, $this->entityId);
$user = $this->drupalCreateUser(array('access comments'));
$this->drupalLogin($user);
$languages = language_list();
// Check that simple users cannot see unpublished field translations.
$path = $this->controller->getViewPath($entity);
foreach ($this->langcodes as $index => $langcode) {
$translation = $this->getTranslation($entity, $langcode);
$value = $this->getValue($translation, 'comment_body', $langcode);
$this->drupalGet($path, array('language' => $languages[$langcode]));
if ($index > 0) {
$this->assertNoRaw($value, 'Unpublished field translation is not shown.');
}
else {
$this->assertRaw($value, 'Published field translation is shown.');
}
}
// Login as translator again to ensure subsequent tests do not break.
$this->drupalLogin($this->translator);
}
/**
* Tests translate link on comment content admin page.
*/
......
......@@ -29,14 +29,20 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac
parent::entityFormAlter($form, $form_state, $entity);
// Move the translation fieldset to a vertical tab.
if (isset($form['translation'])) {
$form['translation'] += array(
if (isset($form['translation_entity'])) {
$form['translation_entity'] += array(
'#group' => 'additional_settings',
'#weight' => 100,
'#attributes' => array(
'class' => array('node-translation-options'),
),
);
// We do not need to show these values on node forms: they inherit the
// basic node property values.
$form['translation_entity']['status']['#access'] = FALSE;
$form['translation_entity']['name']['#access'] = FALSE;
$form['translation_entity']['created']['#access'] = FALSE;
}
}
......@@ -47,4 +53,19 @@ protected function entityFormTitle(EntityInterface $entity) {
$type_name = node_get_type_label($entity);
return t('<em>Edit @type</em> @title', array('@type' => $type_name, '@title' => $entity->label()));
}
/**
* Overrides EntityTranslationController::entityFormEntityBuild().
*/
public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, array &$form_state) {
if (isset($form_state['values']['translation_entity'])) {
$form_controller = translation_entity_form_controller($form_state);
$translation = &$form_state['values']['translation_entity'];
$translation['status'] = $form_controller->getEntity($form_state)->status;
$translation['name'] = $form_state['values']['name'];
$translation['created'] = $form_state['values']['date'];
}
parent::entityFormEntityBuild($entity_type, $entity, $form, $form_state);
}
}
......@@ -53,7 +53,84 @@ protected function setupBundle() {
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission().
*/
protected function getTranslatorPermissions() {
return array_merge(parent::getTranslatorPermissions(), array("edit any $this->bundle content"));
return array_merge(parent::getTranslatorPermissions(), array('administer nodes', "edit any $this->bundle content"));
}
/**
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues().
*/
protected function getNewEntityValues($langcode) {
// Node title is not translatable yet, hence we use a fixed value.
return array('title' => $this->title) + parent::getNewEntityValues($langcode);
}
/**
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getFormSubmitAction().
*/
protected function getFormSubmitAction() {
return t('Save and keep unpublished');
}
/**
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::assertPublishedStatus().
*/
protected function assertPublishedStatus() {
$entity = entity_load($this->entityType, $this->entityId, TRUE);
$path = $this->controller->getEditPath($entity);
$languages = language_list();
$actions = array(
array(t('Save and publish'), t('Save and keep published')),
array(t('Save and unpublish'), t('Save and keep unpublished')),
);
foreach ($actions as $index => $status_actions) {
// (Un)publish the node translations and check that the translation
// statuses are (un)published accordingly.
foreach ($this->langcodes as $langcode) {
if (!empty($status_actions)) {
$action = array_shift($status_actions);
}
$this->drupalPost($path, array(), $action, array('language' => $languages[$langcode]));
}
$entity = entity_load($this->entityType, $this->entityId, TRUE);
foreach ($this->langcodes as $langcode) {
// The node is created as unpulished thus we switch to the published
// status first.
$status = !$index;
$this->assertEqual($status, $entity->translation[$langcode]['status'], 'The translation has been correctly unpublished.');
}
}
}
/**
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::assertAuthoringInfo().
*/
protected function assertAuthoringInfo() {
$entity = entity_load($this->entityType, $this->entityId, TRUE);
$path = $this->controller->getEditPath($entity);
$languages = language_list();
$values = array();
// Post different authoring information for each translation.
foreach ($this->langcodes as $index => $langcode) {
$user = $this->drupalCreateUser();
$values[$langcode] = array(
'uid' => $user->uid,
'created' => REQUEST_TIME - mt_rand(0, 1000),
);
$edit = array(
'name' => $user->name,
'date' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'),
);
$this->drupalPost($path, $edit, $this->getFormSubmitAction(), array('language' => $languages[$langcode]));
}
$entity = entity_load($this->entityType, $this->entityId, TRUE);
foreach ($this->langcodes as $langcode) {
$this->assertEqual($entity->translation[$langcode]['uid'] == $values[$langcode]['uid'], 'Translation author correctly stored.');
$this->assertEqual($entity->translation[$langcode]['created'] == $values[$langcode]['created'], 'Translation date correctly stored.');
}
}
/**
......@@ -94,14 +171,6 @@ function testFieldTranslationForm() {
$this->assertRaw('no translatable fields');
}
/**
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues().
*/
protected function getNewEntityValues($langcode) {
// Node title is not translatable yet, hence we use a fixed value.
return array('title' => $this->title) + parent::getNewEntityValues($langcode);
}
/**
* Test that no metadata is stored for a disabled bundle.
*/
......
......@@ -64,7 +64,7 @@ public function retranslate(EntityInterface $entity, $langcode = NULL) {
$updated_langcode = !empty($langcode) ? $langcode : $entity->language()->langcode;
$translations = $entity->getTranslationLanguages();
foreach ($translations as $langcode => $language) {
$entity->retranslate[$langcode] = $langcode != $updated_langcode;
$entity->translation[$langcode]['outdated'] = $langcode != $updated_langcode;
}
}
......@@ -221,7 +221,7 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac
// We need to display the translation tab only when there is at least one
// translation available or a new one is about to be created.
if ($new_translation || $has_translations) {
$form['translation'] = array(
$form['translation_entity'] = array(
'#type' => 'details',
'#title' => t('Translation'),
'#collapsed' => TRUE,
......@@ -231,9 +231,35 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac
'#multilingual' => TRUE,
);
$translate = !$new_translation && $entity->retranslate[$form_langcode];
// A new translation is enabled by default.
$status = $new_translation || $entity->translation[$form_langcode]['status'];
// If there is only one published translation we cannot unpublish it,
// since there would be nothing left to display.
$enabled = TRUE;
if ($status) {
// A new translation is not available in the translation metadata, hence
// it should count as one more.
$published = $new_translation;
foreach ($entity->translation as $langcode => $translation) {
$published += $translation['status'];
}
$enabled = $published > 1;
}
$description = $enabled ?
t('An unpublished translation will not be visible without translation permissions.') :
t('Only this translation is published. You must publish at least one more translation to unpublish this one.');
$form['translation_entity']['status'] = array(
'#type' => 'checkbox',
'#title' => t('This translation is published'),
'#default_value' => $status,
'#description' => $description,
'#disabled' => !$enabled,
);
$translate = !$new_translation && $entity->translation[$form_langcode]['outdated'];
if (!$translate) {
$form['translation']['retranslate'] = array(
$form['translation_entity']['retranslate'] = array(
'#type' => 'checkbox',
'#title' => t('Flag other translations as outdated'),
'#default_value' => FALSE,
......@@ -241,7 +267,7 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac
);
}
else {
$form['translation']['translate'] = array(
$form['translation_entity']['outdated'] = array(
'#type' => 'checkbox',
'#title' => t('This translation needs to be updated'),
'#default_value' => $translate,
......@@ -249,6 +275,25 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac
);
}
$name = $new_translation ? $GLOBALS['user']->name : user_load($entity->translation[$form_langcode]['uid'])->name;
$form['translation_entity']['name'] = array(
'#type' => 'textfield',
'#title' => t('Authored by'),
'#maxlength' => 60,
'#autocomplete_path' => 'user/autocomplete',
'#default_value' => $name,
'#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
);
$date = $new_translation ? REQUEST_TIME : $entity->translation[$form_langcode]['created'];
$form['translation_entity']['created'] = array(
'#type' => 'textfield',
'#title' => t('Authored on'),
'#maxlength' => 25,
'#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to use the time of form submission.', array('%time' => format_date($date, 'custom', 'Y-m-d H:i:s O'), '%timezone' => format_date($date, 'custom', 'O'))),
'#default_value' => $new_translation ? '' : format_date($date, 'custom', 'Y-m-d H:i:s O'),
);
if ($language_widget) {
$form['langcode']['#multilingual'] = TRUE;
}
......@@ -259,6 +304,11 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac
// Process the submitted values before they are stored.
$form['#entity_builders'][] = array($this, 'entityFormEntityBuild');
// Handle entity validation.
if (isset($form['actions']['submit'])) {
$form['actions']['submit']['#validate'][] = array($this, 'entityFormValidate');
}
// Handle entity deletion.
if (isset($form['actions']['delete'])) {
$form['actions']['delete']['#submit'][] = array($this, 'entityFormDelete');
......@@ -357,24 +407,50 @@ protected function addTranslatabilityClue(&$element) {
public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, array &$form_state) {
$form_controller = translation_entity_form_controller($form_state);
$form_langcode = $form_controller->getFormLangcode($form_state);
$source_langcode = $this->getSourceLangcode($form_state);
if ($source_langcode) {
// @todo Use the entity setter when all entities support multilingual
// properties.
$entity->source[$form_langcode] = $source_langcode;
if (!isset($entity->translation[$form_langcode])) {
$entity->translation[$form_langcode] = array();
}
$values = isset($form_state['values']['translation_entity']) ? $form_state['values']['translation_entity'] : array();
$translation = &$entity->translation[$form_langcode];
// Ensure every key has at least a default value. Subclasses may provide
// entity-specific values to alter them.
$values = isset($form_state['values']['translation']) ? $form_state['values']['translation'] : array();
$entity->retranslate[$form_langcode] = isset($values['translate']) && $values['translate'];
// @todo Use the entity setter when all entities support multilingual
// properties.
$translation['uid'] = !empty($values['name']) && ($account = user_load_by_name($values['name'])) ? $account->uid : 0;
$translation['status'] = !empty($values['status']);
$translation['created'] = !empty($values['created']) ? strtotime($values['created']) : REQUEST_TIME;
$translation['changed'] = REQUEST_TIME;
$source_langcode = $this->getSourceLangcode($form_state);
if ($source_langcode) {
$translation['source'] = $source_langcode;
}
$translation['outdated'] = !empty($values['outdated']);
if (!empty($values['retranslate'])) {
$this->retranslate($entity, $form_langcode);
}
}
/**
* Form validation handler for EntityTranslationController::entityFormAlter().
*
* Validates the submitted entity translation metadata.
*/
function entityFormValidate($form, &$form_state) {
if (!empty($form_state['values']['translation_entity'])) {
$translation = $form_state['values']['translation_entity'];
// Validate the "authored by" field.
if (!empty($translation['name']) && !($account = user_load_by_name($translation['name']))) {
form_set_error('translation_entity][name', t('The translation authoring username %name does not exist.', array('%name' => $translation['name'])));
}
// Validate the "authored on" field.
if (!empty($translation['created']) && strtotime($translation['created']) === FALSE) {
form_set_error('translation_entity][created', t('You have to specify a valid translation authoring date.'));
}
}
}
/**
* Form submission handler for EntityTranslationController::entityFormAlter().
*
......
......@@ -16,6 +16,13 @@
*/
abstract class EntityTranslationUITest extends EntityTranslationTestBase {
/**
* The id of the entity being translated.
*
* @var mixed
*/
protected $entityId;
/**
* Whether the behavior of the language selector should be tested.
*
......@@ -27,11 +34,22 @@ abstract class EntityTranslationUITest extends EntityTranslationTestBase {
* Tests the basic translation UI.
*/
function testTranslationUI() {
$this->assertBasicTranslation();
$this->assertOutdatedStatus();
$this->assertPublishedStatus();
$this->assertAuthoringInfo();
$this->assertTranslationDeletion();
}
/**
* Tests the basic translation workflow.
*/
protected function assertBasicTranslation() {
// Create a new test entity with original values in the default language.
$default_langcode = $this->langcodes[0];
$values[$default_langcode] = $this->getNewEntityValues($default_langcode);
$id = $this->createEntity($values[$default_langcode], $default_langcode);
$entity = entity_load($this->entityType, $id, TRUE);
$this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
$entity = entity_load($this->entityType, $this->entityId, TRUE);
$this->assertTrue($entity, t('Entity found in the database.'));
$translation = $this->getTranslation($entity, $default_langcode);
......@@ -48,11 +66,11 @@ function testTranslationUI() {
$base_path = $this->controller->getBasePath($entity);
$path = $langcode . '/' . $base_path . '/translations/add/' . $default_langcode . '/' . $langcode;
$this->drupalPost($path, $this->getEditValues($values, $langcode), t('Save'));
$this->drupalPost($path, $this->getEditValues($values, $langcode), $this->getFormSubmitAction());
if ($this->testLanguageSelector) {
$this->assertNoFieldByXPath('//select[@id="edit-langcode"]', NULL, 'Language selector correclty disabled on translations.');
}
$entity = entity_load($this->entityType, $entity->id(), TRUE);
$entity = entity_load($this->entityType, $this->entityId, TRUE);
// Switch the source language.
$langcode = 'fr';
......@@ -64,9 +82,9 @@ function testTranslationUI() {
// Add another translation and mark the other ones as outdated.
$values[$langcode] = $this->getNewEntityValues($langcode);
$edit = $this->getEditValues($values, $langcode) + array('translation[retranslate]' => TRUE);
$this->drupalPost($path, $edit, t('Save'));
$entity = entity_load($this->entityType, $entity->id(), TRUE);
$edit = $this->getEditValues($values, $langcode) + array('translation_entity[retranslate]' => TRUE);
$this->drupalPost($path, $edit, $this->getFormSubmitAction());
$entity = entity_load($this->entityType, $this->entityId, TRUE);
// Check that the entered values have been correctly stored.
foreach ($values as $langcode => $property_values) {
......@@ -78,6 +96,20 @@ function testTranslationUI() {
$this->assertEqual($stored_value, $value, $message);
}
}
}
/**
* Tests up-to-date status tracking.
*/
protected function assertOutdatedStatus() {
$entity = entity_load($this->entityType, $this->entityId, TRUE);
$langcode = 'fr';
$default_langcode = $this->langcodes[0];
// Mark translations as outdated.
$edit = array('translation_entity[retranslate]' => TRUE);
$this->drupalPost($langcode . '/' . $this->controller->getEditPath($entity), $edit, $this->getFormSubmitAction());
$entity = entity_load($this->entityType, $this->entityId, TRUE);
// Check that every translation has the correct "outdated" status.
foreach ($this->langcodes as $enabled_langcode) {
......@@ -85,26 +117,97 @@ function testTranslationUI() {
$path = $prefix . $this->controller->getEditPath($entity);
$this->drupalGet($path);
if ($enabled_langcode == $langcode) {
$this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.');
$this->assertFieldByXPath('//input[@name="translation_entity[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.');
}
else {
$this->assertFieldByXPath('//input[@name="translation[translate]"]', TRUE, 'The translate flag is checked by default.');
$edit = array('translation[translate]' => FALSE);
$this->drupalPost($path, $edit, t('Save'));
$this->assertFieldByXPath('//input[@name="translation_entity[outdated]"]', TRUE, 'The translate flag is checked by default.');
$edit = array('translation_entity[outdated]' => FALSE);
$this->drupalPost($path, $edit, $this->getFormSubmitAction());
$this->drupalGet($path);
$this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.');
$entity = entity_load($this->entityType, $entity->id(), TRUE);
$this->assertFalse($entity->retranslate[$enabled_langcode], 'The "outdated" status has been correctly stored.');
$this->assertFieldByXPath('//input[@name="translation_entity[retranslate]"]', FALSE, 'The retranslate flag is now shown.');
$entity = entity_load($this->entityType, $this->entityId, TRUE);
$this->assertFalse($entity->translation[$enabled_langcode]['outdated'], 'The "outdated" status has been correctly stored.');
}
}
}
/**
* Tests the translation publishing status.
*/
protected function assertPublishedStatus() {
$entity = entity_load($this->entityType, $this->entityId, TRUE);
$path = $this->controller->getEditPath($entity);
// Unpublish translations.
foreach ($this->langcodes as $index => $langcode) {
if ($index > 0) {
$edit = array('translation_entity[status]' => FALSE);
$this->drupalPost($langcode . '/' . $path, $edit, $this->getFormSubmitAction());
$entity = entity_load($this->entityType, $this->entityId, TRUE);
$this->assertFalse($entity->translation[$langcode]['status'], 'The translation has been correctly unpublished.');
}
}
// Check that the last published translation cannot be unpublished.
$this->drupalGet($path);
$this->assertFieldByXPath('//input[@name="translation_entity[status]" and @disabled="disabled"]', TRUE, 'The last translation is published and cannot be unpublished.');
}
/**
* Tests the translation authoring information.
*/
protected function assertAuthoringInfo() {
$entity = entity_load($this->entityType, $this->entityId, TRUE);
$path = $this->controller->getEditPath($entity);
$values = array();
// Post different authoring information for each translation.
foreach ($this->langcodes as $index => $langcode) {
$user = $this->drupalCreateUser();
$values[$langcode] = array(
'uid' => $user->uid,
'created' => REQUEST_TIME - mt_rand(0, 1000),
);
$edit = array(
'translation_entity[name]' => $user->name,
'translation_entity[created]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'),
);
$prefix = $index > 0 ? $langcode . '/' : '';
$this->drupalPost($prefix . $path, $edit, $this->getFormSubmitAction());
}
$entity = entity_load($this->entityType, $this->entityId, TRUE);
foreach ($this->langcodes as $langcode) {
$this->assertEqual($entity->translation[$langcode]['uid'] == $values[$langcode]['uid'], 'Translation author correctly stored.');
$this->assertEqual($entity->translation[$langcode]['created'] == $values[$langcode]['created'], 'Translation date correctly stored.');
}
// Try to post non valid values and check that they are rejected.
$langcode = 'en';
$edit = array(
// User names have by default length 8.
'translation_entity[name]' => $this->randomName(12),
'translation_entity[created]' => $this->randomName(),
);
$this->drupalPost($path, $edit, $this->getFormSubmitAction());
$this->assertTrue($this->xpath('//div[@id="messages"]//div[contains(@class, "error")]//ul'), 'Invalid values generate a list of form errors.');
$this->assertEqual($entity->translation[$langcode]['uid'] == $values[$langcode]['uid'], 'Translation author correctly kept.');
$this->assertEqual($entity->translation[$langcode]['created'] == $values[$langcode]['created'], 'Translation date correctly kept.');
}
/**
* Tests translation deletion.
*/
protected function assertTranslationDeletion() {
// Confirm and delete a translation.
$this->drupalPost($path, array(), t('Delete translation'));
$langcode = 'fr';
$entity = entity_load($this->entityType, $this->entityId, TRUE);
$this->drupalPost($langcode . '/' . $this->controller->getEditPath($entity), array(), t('Delete translation'));
$this->drupalPost(NULL, array(), t('Delete'));
$entity = entity_load($this->entityType, $entity->id(), TRUE);
$entity = entity_load($this->entityType, $this->entityId, TRUE);
if ($this->assertTrue(is_object($entity), 'Entity found')) {
$translations = $entity->getTranslationLanguages();
$this->assertTrue(count($translations) == 2 && empty($translations[$enabled_langcode]), 'Translation successfully deleted.');
$this->assertTrue(count($translations) == 2 && empty($translations[$langcode]), 'Translation successfully deleted.');
}
}
......@@ -130,6 +233,13 @@ protected function getEditValues($values, $langcode, $new = FALSE) {
return $edit;
}
/**
* Returns the form action value to be used to submit the entity form.
*/
protected function getFormSubmitAction() {
return t('Save');
}
/**
* Returns the translation object to use to retrieve the translated values.
*
......
......@@ -40,12 +40,36 @@ function translation_entity_schema() {
'default' => '',
'description' => 'The source language from which this translation was created.',
),
'translate' => array(
'outdated' => array(
'description' => 'A boolean indicating whether this translation needs to be updated.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'uid' => array(
'description' => 'The author of this translation.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'status' => array(
'description' => 'Boolean indicating whether the translation is visible to non-translators.',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
'created' => array(
'description' => 'The Unix timestamp when the translation was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'changed' => array(
'description' => 'The Unix timestamp when the translation was most recently saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('entity_type', 'entity_id', 'langcode'),
);
......
......@@ -284,6 +284,22 @@ function translation_entity_translate_access(EntityInterface $entity) {
(user_access('create entity translations') || user_access('update entity translations') || user_access('delete entity translations'));
}
/**
* Checks whether the given user can view the specified translation.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity whose translation overview should be displayed.
* @param $langcode
* The language code of the translation to be displayed.
* @param \Drupal\user\Plugin\Core\Entity\User $account
* (optional) The account for which view access should be checked. Defaults to
* the current user.
*/
function translation_entity_view_access(EntityInterface $entity, $langcode, $account = NULL) {
$entity_type = $entity->entityType();