Commit ae9f336e authored by webchick's avatar webchick

Issue #1807776 by plach, Berdir, YesCT, Gábor Hojtsy, bforchhammer, Bojhan,...

Issue #1807776 by plach, Berdir, YesCT, Gábor Hojtsy, bforchhammer, Bojhan, webchick: Support both simple and editorial workflows for translating entities.
parent afeed9ed
......@@ -120,6 +120,11 @@
* entity.
* - menu_path_wildcard: (optional) A string identifying the menu loader in the
* router path.
* - permission_granularity: (optional) Specifies whether a module exposing
* permissions for the current entity type should use entity-type level
* granularity, bundle level granularity or just skip this entity. The allowed
* values are respectively "entity_type", "bundle" or FALSE. Defaults to
* "entity_type".
*
* The defaults for the plugin definition are provided in
* \Drupal\Core\Entity\EntityManager::defaults.
......@@ -159,6 +164,7 @@ class EntityManager extends PluginManagerBase {
'access_controller_class' => 'Drupal\Core\Entity\EntityAccessController',
'static_cache' => TRUE,
'translation' => array(),
'permission_granularity' => 'entity_type',
);
/**
......
......@@ -9,12 +9,29 @@
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
use Drupal\translation_entity\EntityTranslationController;
use Drupal\translation_entity\EntityTranslationControllerNG;
/**
* Defines the translation controller class for comments.
*/
class CommentTranslationController extends EntityTranslationController {
class CommentTranslationController extends EntityTranslationControllerNG {
/**
* Overrides EntityTranslationController::getAccess().
*/
public function getAccess(EntityInterface $entity, $op) {
switch ($op) {
case 'view':
return user_access('access comments');
case 'update':
return comment_access('edit', $entity);
case 'delete':
return user_access('administer comments');
case 'create':
return user_access('post comments');
}
return parent::getAccess($entity, $op);
}
/**
* Overrides EntityTranslationController::entityFormTitle().
......
......@@ -25,7 +25,7 @@
* form_controller_class = {
* "default" = "Drupal\comment\CommentFormController"
* },
* translation_controller_class = "Drupal\translation_entity\EntityTranslationControllerNG",
* translation_controller_class = "Drupal\comment\CommentTranslationController",
* base_table = "comment",
* uri_callback = "comment_uri",
* fieldable = TRUE,
......
......@@ -34,9 +34,6 @@ public static function getInfo() {
);
}
/**
* Overrides \Drupal\simpletest\WebTestBase::setUp().
*/
function setUp() {
$this->entityType = 'comment';
$this->nodeBundle = 'article';
......@@ -58,7 +55,7 @@ function setupBundle() {
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission().
*/
function getTranslatorPermissions() {
return array('post comments', 'administer comments', "translate $this->entityType entities", 'edit original values');
return array_merge(parent::getTranslatorPermissions(), array('post comments', 'administer comments'));
}
/**
......@@ -100,7 +97,7 @@ protected function getNewEntityValues($langcode) {
*/
function testTranslateLinkCommentAdminPage() {
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'page'));
$this->admin_user = $this->drupalCreateUser(array('access administration pages', 'administer comments', 'translate any entity'));
$this->admin_user = $this->drupalCreateUser(array_merge(parent::getTranslatorPermissions(), array('access administration pages', 'administer comments')));
$this->drupalLogin($this->admin_user);
$cid_translatable = $this->createEntity(array(), $this->langcodes[0], $this->nodeBundle);
......
......@@ -336,7 +336,7 @@ protected function actions(array $form, array &$form_state) {
}
$element['preview'] = array(
'#access' => $preview_mode != DRUPAL_DISABLED,
'#access' => $preview_mode != DRUPAL_DISABLED && (node_access('create', $node) || node_access('update', $node)),
'#value' => t('Preview'),
'#weight' => 20,
'#validate' => array(
......@@ -429,6 +429,9 @@ public function submit(array $form, array &$form_state) {
* A reference to a keyed array containing the current state of the form.
*/
public function preview(array $form, array &$form_state) {
// @todo Remove this: we should not have explicit includes in autoloaded
// classes.
module_load_include('inc', 'node', 'node.pages');
drupal_set_title(t('Preview'), PASS_THROUGH);
$form_state['node_preview'] = node_preview($this->getEntity($form_state));
$form_state['rebuild'] = TRUE;
......
......@@ -39,7 +39,8 @@
* },
* bundle_keys = {
* "bundle" = "type"
* }
* },
* permission_granularity = "bundle"
* )
*/
class Node extends Entity implements ContentEntityInterface {
......
......@@ -34,9 +34,6 @@ public static function getInfo() {
);
}
/**
* Overrides \Drupal\simpletest\WebTestBase::setUp().
*/
function setUp() {
$this->entityType = 'node';
$this->bundle = 'article';
......@@ -56,7 +53,7 @@ protected function setupBundle() {
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission().
*/
function getTranslatorPermissions() {
return array("edit any $this->bundle content", "translate $this->entityType entities", 'edit original values');
return array_merge(parent::getTranslatorPermissions(), array("edit any $this->bundle content"));
}
/**
......@@ -80,7 +77,7 @@ function testTranslateLinkContentAdminPage() {
* Tests field translation form.
*/
function testFieldTranslationForm() {
$admin_user = $this->drupalCreateUser(array('translate any entity', 'access administration pages', 'bypass node access', 'administer node fields'));
$admin_user = $this->drupalCreateUser(array_merge($this->getTranslatorPermissions(), array('access administration pages', 'bypass node access', 'administer node fields')));
$this->drupalLogin($admin_user);
$article = $this->drupalCreateNode(array('type' => 'article', 'langcode' => 'en'));
......
......@@ -462,12 +462,14 @@ protected function drupalCompareFiles($file1, $file2) {
* @param array $permissions
* Array of permission names to assign to user. Note that the user always
* has the default permissions derived from the "authenticated users" role.
* @param $name
* The user name.
*
* @return object|false
* A fully loaded user object with pass_raw property, or FALSE if account
* creation fails.
*/
protected function drupalCreateUser(array $permissions = array()) {
protected function drupalCreateUser(array $permissions = array(), $name = NULL) {
// Create a role with the given permission set, if any.
$rid = FALSE;
if ($permissions) {
......@@ -479,7 +481,7 @@ protected function drupalCreateUser(array $permissions = array()) {
// Create a user assigned to that role.
$edit = array();
$edit['name'] = $this->randomName();
$edit['name'] = !empty($name) ? $name : $this->randomName();
$edit['mail'] = $edit['name'] . '@example.com';
$edit['pass'] = user_password();
$edit['status'] = 1;
......
......@@ -68,12 +68,12 @@ function testEntityAccess() {
'view' => TRUE,
), $entity);
// The custom user is not allowed to view test entities.
// The custom user is not allowed to perform any operation on test entities.
$custom_user = $this->drupalCreateUser();
$this->assertEntityAccess(array(
'create' => TRUE,
'update' => TRUE,
'delete' => TRUE,
'create' => FALSE,
'update' => FALSE,
'delete' => FALSE,
'view' => FALSE,
), $entity, $custom_user);
}
......
......@@ -30,21 +30,21 @@ public function viewAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT
* Implements \Drupal\Core\Entity\EntityAccessControllerInterface::createAccess().
*/
public function createAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
return TRUE;
return user_access('administer entity_test content', $account);
}
/**
* Implements \Drupal\Core\Entity\EntityAccessControllerInterface::updateAccess().
*/
public function updateAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
return TRUE;
return user_access('administer entity_test content', $account);
}
/**
* Implements \Drupal\Core\Entity\EntityAccessControllerInterface::deleteAccess().
*/
public function deleteAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
return TRUE;
return user_access('administer entity_test content', $account);
}
}
......@@ -38,7 +38,8 @@
* bundle_keys = {
* "bundle" = "vid"
* },
* menu_base_path = "taxonomy/term/%taxonomy_term"
* menu_base_path = "taxonomy/term/%taxonomy_term",
* permission_granularity = "bundle"
* )
*/
class Term extends Entity implements ContentEntityInterface {
......
......@@ -15,6 +15,20 @@
*/
class TermTranslationController extends EntityTranslationController {
/**
* Overrides EntityTranslationController::getAccess().
*/
public function getAccess(EntityInterface $entity, $op) {
switch ($op) {
case 'create':
case 'update':
return taxonomy_term_access('edit', $entity);
case 'delete':
return taxonomy_term_access('delete', $entity);
}
return parent::getAccess($entity, $op);
}
/**
* Overrides EntityTranslationController::entityFormAlter().
*/
......
......@@ -41,9 +41,6 @@ public static function getInfo() {
);
}
/**
* Overrides \Drupal\simpletest\WebTestBase::setUp().
*/
function setUp() {
$this->entityType = 'taxonomy_term';
$this->bundle = 'tags';
......@@ -73,7 +70,7 @@ protected function setupBundle() {
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission().
*/
function getTranslatorPermissions() {
return array('administer taxonomy', "translate $this->entityType entities", 'edit original values');
return array_merge(parent::getTranslatorPermissions(), array('administer taxonomy'));
}
/**
......@@ -102,7 +99,7 @@ public function testTranslationUI() {
* Tests translate link on vocabulary term list.
*/
function testTranslateLinkVocabularyAdminPage() {
$this->admin_user = $this->drupalCreateUser(array('access administration pages', 'administer taxonomy', 'translate any entity'));
$this->admin_user = $this->drupalCreateUser(array_merge(parent::getTranslatorPermissions(), array('access administration pages', 'administer taxonomy')));
$this->drupalLogin($this->admin_user);
$translatable_tid = $this->createEntity(array(), $this->langcodes[0], $this->vocabulary->id());
......
......@@ -99,9 +99,17 @@ public function getAccess(EntityInterface $entity, $op) {
/**
* Implements EntityTranslationControllerInterface::getTranslationAccess().
*/
public function getTranslationAccess(EntityInterface $entity, $langcode) {
$entity_type = $entity->entityType();
return (user_access('translate any entity') || user_access("translate $entity_type entities")) && ($langcode != $entity->language()->langcode || user_access('edit original values'));
public function getTranslationAccess(EntityInterface $entity, $op) {
// @todo Move this logic into a translation access controller checking also
// the translation language and the given account.
$info = $entity->entityInfo();
$translate_permission = TRUE;
// If no permission granularity is defined this entity type does not need an
// explicit translate permission.
if (!user_access('translate any entity') && !empty($info['permission_granularity'])) {
$translate_permission = user_access($info['permission_granularity'] == 'bundle' ? "translate {$entity->bundle()} {$entity->entityType()}" : "translate {$entity->entityType()}");
}
return $translate_permission && user_access("$op entity translations");
}
/**
......@@ -203,6 +211,7 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac
'#value' => t('Delete translation'),
'#weight' => $weight,
'#submit' => array(array($this, 'entityFormDeleteTranslation')),
'#access' => $this->getTranslationAccess($entity, 'delete'),
);
}
......@@ -220,7 +229,7 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac
'#collapsed' => TRUE,
'#tree' => TRUE,
'#weight' => 10,
'#access' => $this->getTranslationAccess($entity, $form_langcode),
'#access' => $this->getTranslationAccess($entity, $source_langcode ? 'create' : 'update'),
'#multilingual' => TRUE,
);
......@@ -259,17 +268,11 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac
}
/**
* Process callback: Determines which elements get clue in the form.
*
* @param array $element
* Form API element.
*
* @return array
* A processed element with the shared elements marked with a clue.
* Process callback: determines which elements get clue in the form.
*
* @see \Drupal\translation_entity\EntityTranslationController::entityFormAlter()
*/
public function entityFormSharedElements($element) {
public function entityFormSharedElements($element, $form_state, $form) {
static $ignored_types;
// @todo Find a more reliable way to determine if a form element concerns a
......@@ -280,7 +283,7 @@ public function entityFormSharedElements($element) {
foreach (element_children($element) as $key) {
if (!isset($element[$key]['#type'])) {
$this->entityFormSharedElements($element[$key]);
$this->entityFormSharedElements($element[$key], $form_state, $form);
}
else {
// Ignore non-widget form elements.
......@@ -289,7 +292,15 @@ public function entityFormSharedElements($element) {
}
// Elements are considered to be non multilingual by default.
if (empty($element[$key]['#multilingual'])) {
$this->addTranslatabilityClue($element[$key]);
// If we are displaying a multilingual entity form we need to provide
// translatability clues, otherwise the shared form elements should be
// hidden.
if (empty($form_state['translation_entity']['translation_form'])) {
$this->addTranslatabilityClue($element[$key]);
}
else {
$element[$key]['#access'] = FALSE;
}
}
}
}
......
......@@ -132,17 +132,21 @@ public function getViewPath(EntityInterface $entity);
public function getAccess(EntityInterface $entity, $op);
/**
* Checks if a user is allowed to edit the given translation.
* Checks if the user can perform the given operation on translations of the
* wrapped entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity whose translation has to be accessed.
* @param string $langcode
* The language code identifying the translation to be accessed.
* @param $op
* The operation to be performed on the translation. Possible values are:
* - "create"
* - "update"
* - "delete"
*
* @return boolean
* TRUE if the operation may be performed, FALSE otherwise.
*/
public function getTranslationAccess(EntityInterface $entity, $langcode);
public function getTranslationAccess(EntityInterface $entity, $op);
/**
* Retrieves the source language for the translation being created.
......
......@@ -15,7 +15,14 @@
class EntityTranslationControllerNG extends EntityTranslationController {
/**
* Overrides EntityTranslationController::removeTranslation().
* Overrides \Drupal\translation_entity\EntityTranslationController::getAccess().
*/
public function getAccess(EntityInterface $entity, $op) {
return $entity->access($op);
}
/**
* Overrides \Drupal\translation_entity\EntityTranslationControllerInterface::removeTranslation().
*/
public function removeTranslation(EntityInterface $entity, $langcode) {
$translation = $entity->getTranslation($langcode);
......@@ -23,4 +30,5 @@ public function removeTranslation(EntityInterface $entity, $langcode) {
$translation->$property_name = array();
}
}
}
......@@ -29,21 +29,11 @@ public static function getInfo() {
);
}
/**
* Overrides \Drupal\simpletest\WebTestBase::setUp().
*/
function setUp() {
$this->entityType = 'config_test';
parent::setUp();
}
/**
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission().
*/
function getTranslatorPermissions() {
return array("translate $this->entityType entities", 'edit original values');
}
/**
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues().
*/
......
......@@ -21,7 +21,7 @@ class EntityTestTranslationUITest extends EntityTranslationUITest {
public static function getInfo() {
return array(
'name' => 'Entity Test Translation UI',
'name' => 'Entity Test translation UI',
'description' => 'Tests the test entity translation UI with the test entity.',
'group' => 'Entity Translation UI',
);
......@@ -40,7 +40,7 @@ function setUp() {
* Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission().
*/
function getTranslatorPermissions() {
return array('administer entity_test content', "translate $this->entityType entities", 'edit original values');
return array_merge(parent::getTranslatorPermissions(), array('administer entity_test content'));
}
/**
......
<?php
/**
* @file
* Contains \Drupal\entity\Tests\EntityTranslationTestBase.
*/
namespace Drupal\translation_entity\Tests;
use Drupal\Core\Entity\DatabaseStorageControllerNG;
use Drupal\Core\Language\Language;
use Drupal\simpletest\WebTestBase;
/**
* Tests entity translation workflows.
*/
abstract class EntityTranslationTestBase extends WebTestBase {
/**
* The entity type being tested.
*
* @var string
*/
protected $entityType = 'entity_test_mul';
/**
* The bundle being tested.
*
* @var string
*/
protected $bundle;
/**
* The enabled languages.
*
* @var array
*/
protected $langcodes;
/**
* The account to be used to test translation operations.
*
* @var \Drupal\user\Plugin\Core\Entity\User
*/
protected $translator;
/**
* The account to be used to test multilingual entity editing.
*
* @var \Drupal\user\Plugin\Core\Entity\User
*/
protected $editor;
/**
* The account to be used to test access to both workflows.
*
* @var \Drupal\user\Plugin\Core\Entity\User
*/
protected $administrator;
/**
* The name of the field used to test translation.
*
* @var string
*/
protected $fieldName;
/**
* The translation controller for the current entity type.
*
* @var \Drupal\translation_entity\EntityTranslationControllerInterface
*/
protected $controller;
function setUp() {
parent::setUp();
$this->setupLanguages();
$this->setupBundle();
$this->enableTranslation();
$this->setupUsers();
$this->setupTestFields();
$this->controller = translation_entity_controller($this->entityType);
}
/**
* Enables additional languages.
*/
protected function setupLanguages() {
$this->langcodes = array('it', 'fr');
foreach ($this->langcodes as $langcode) {
language_save(new Language(array('langcode' => $langcode)));
}
array_unshift($this->langcodes, language_default()->langcode);
}
/**
* Returns an array of permissions needed for the translator.
*/
protected function getTranslatorPermissions() {
return array_filter(array($this->getTranslatePermission(), 'create entity translations', 'update entity translations', 'delete entity translations'));
}
/**
* Returns the translate permissions for the current entity and bundle.
*/
protected function getTranslatePermission() {
$info = entity_get_info($this->entityType);
if (!empty($info['permission_granularity'])) {
return $info['permission_granularity'] == 'bundle' ? "translate {$this->bundle} {$this->entityType}" : "translate {$this->entityType}";
}
}
/**
* Returns an array of permissions needed for the editor.
*/
protected function getEditorPermissions() {
// Every entity-type-specific test needs to define these.
return array();
}
/**
* Creates and activates translator, editor and admin users.
*/
protected function setupUsers() {
$this->translator = $this->drupalCreateUser($this->getTranslatorPermissions(), 'translator');
$this->editor = $this->drupalCreateUser($this->getEditorPermissions(), 'editor');
$this->administrator = $this->drupalCreateUser(array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions()), 'administrator');
$this->drupalLogin($this->translator);
}
/**
* Creates or initializes the bundle date if needed.
*/
protected function setupBundle() {
if (empty($this->bundle)) {
$this->bundle = $this->entityType;
}
}
/**
* Enables translation for the current entity type and bundle.
*/
protected function enableTranslation() {
// Enable translation for the current entity type and ensure the change is
// picked up.
translation_entity_set_config($this->entityType, $this->bundle, 'enabled', TRUE);
drupal_static_reset();
entity_info_cache_clear();
menu_router_rebuild();
}
/**
* Creates the test fields.
*/
protected function setupTestFields() {
$this->fieldName = 'field_test_et_ui_test';
$field = array(
'field_name' => $this->fieldName,
'type' => 'text',
'cardinality' => 1,
'translatable' => TRUE,
);
field_create_field($field);
$instance = array(
'entity_type' => $this->entityType,
'field_name' => $this->fieldName,
'bundle' => $this->bundle,
'label' => 'Test translatable text-field',
'widget' => array(
'type' => 'text_textfield',
'weight' => 0,
),
);
field_create_instance($instance);
}
/**
* Creates the entity to be translated.
*
* @param array $values
* An array of initial values for the entity.
* @param string $langcode
* The initial language code of the entity.
* @param string $bundle_name
* (optional) The entity bundle, if the entity uses bundles. Defaults to
* NULL. If left NULL, $this->bundle will be used.
*
* @return
* The entity id.
*/
protected function createEntity($values, $langcode, $bundle_name = NULL) {
$entity_values = $values;
$entity_values['langcode'] = $langcode;
$info = entity_get_info($this->entityType);
if (!empty($info['entity_keys']['bundle'])) {
$entity_values[$info['entity_keys']['bundle']] = $bundle_name ?: $this->bundle;
}
$controller = $this->container->get('plugin.manager.entity')->getStorageController($this->entityType);
if (!($controller instanceof DatabaseStorageControllerNG)) {
foreach ($values as $property => $value) {
if (is_array($value)) {
$entity_values[$property] = array($langcode => $value);
}
}
}
$entity = entity_create($this->entityType, $entity_values);
$entity->save();
return $entity->id();
}
}
......@@ -7,45 +7,14 @@
namespace Drupal\translation_entity\Tests;
use Drupal\Core\Entity\DatabaseStorageControllerNG;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityNG;
use Drupal\Core\Language\Language;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\simpletest\WebTestBase;