Skip to content
Snippets Groups Projects
Unverified Commit e223b7ad authored by colemanw's avatar colemanw Committed by GitHub
Browse files

Merge pull request #20584 from totten/master-translation-api

translation#67 - Add APIv4 support for "Translation" entity.
parents 5c56a296 7782deca
Branches
Tags
No related merge requests found
......@@ -14,7 +14,9 @@
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
class CRM_Core_BAO_Translation extends CRM_Core_DAO_Translation {
class CRM_Core_BAO_Translation extends CRM_Core_DAO_Translation implements \Civi\Test\HookInterface {
use CRM_Core_DynamicFKAccessTrait;
/**
* Get a list of valid statuses for translated-strings.
......@@ -104,4 +106,61 @@ class CRM_Core_BAO_Translation extends CRM_Core_DAO_Translation {
return $f;
}
/**
* When manipulating strings via the `Translation` entity (APIv4), ensure that the references are well-formed.
*
* @param \Civi\Api4\Event\ValidateValuesEvent $e
*/
public static function self_civi_api4_validate(\Civi\Api4\Event\ValidateValuesEvent $e) {
$statuses = self::getStatuses('validate');
$dataTypes = [CRM_Utils_Type::T_STRING, CRM_Utils_Type::T_TEXT, CRM_Utils_Type::T_LONGTEXT];
$htmlTypes = ['Text', 'TextArea', 'RichTextEditor'];
foreach ($e->records as $r => $record) {
if (array_key_exists('status_id', $record) && !isset($statuses[$record['status_id']])) {
$e->addError($r, 'status_id', 'invalid', ts('Invalid status'));
}
$entityIdFields = ['entity_table', 'entity_field', 'entity_id'];
$entityIdCount = (empty($record['entity_table']) ? 0 : 1)
+ (empty($record['entity_field']) ? 0 : 1)
+ (empty($record['entity_id']) ? 0 : 1);
if ($entityIdCount === 0) {
continue;
}
elseif ($entityIdCount < 3) {
$e->addError($r, $entityIdFields, 'full_entity', ts('Must specify all entity identification fields'));
}
$simpleName = '/^[a-zA-Z0-9_]+$/';
if (!preg_match($simpleName, $record['entity_table']) || !preg_match($simpleName, $record['entity_field']) || !is_numeric($record['entity_id'])) {
$e->addError($r, $entityIdFields, 'malformed_entity', ts('Entity reference is malformed'));
continue;
}
// Which fields support translation?
// - One could follow the same path as "Multilingual". Use
// $translatable = CRM_Core_I18n_SchemaStructure::columns();
// if (!isset($translatable[$record['entity_table']][$record['entity_field']])) {
// - Or, since we don't need schema-changes, we could be more generous and allow all freeform text fields...
$daoClass = CRM_Core_DAO_AllCoreTables::getClassForTable($record['entity_table']);
if (!$daoClass) {
$e->addError($r, 'entity_table', 'bad_table', ts('Entity reference specifies a non-existent or non-translatable table'));
continue;
}
$dao = new $daoClass();
$dao->id = $record['entity_id'];
$field = $dao->getFieldSpec($record['entity_field']);
if (!$field || !in_array($field['type'] ?? '', $dataTypes) || !in_array($field['html']['type'] ?? '', $htmlTypes)) {
$e->addError($r, 'entity_field', 'bad_field', ts('Entity reference specifies a non-existent or non-translatable field'));
}
if (!$dao->find()) {
$e->addError($r, 'entity_id', 'nonexistent_id', ts('Entity does not exist'));
}
}
}
}
......@@ -39,6 +39,10 @@ trait CRM_Core_DynamicFKAccessTrait {
}
if ($eid && $table) {
$targetEntity = CRM_Core_DAO_AllCoreTables::getBriefName(CRM_Core_DAO_AllCoreTables::getClassForTable($table));
if ($targetEntity === NULL) {
throw new \API_Exception(sprintf('Cannot resolve permissions for dynamic foreign key in "%s". Invalid table reference "%s".',
static::getTableName(), $table));
}
return \Civi\Api4\Utils\CoreUtil::checkAccessDelegated($targetEntity, 'update', ['id' => $eid], $userID);
}
return TRUE;
......
<?php
namespace Civi\Api4;
/**
* Attach supplemental translations to strings stored in the database.
*
* @package Civi\Api4
*/
class Translation extends Generic\DAOEntity {
public static function permissions() {
return [
'meta' => ['access CiviCRM'],
'default' => ['translate CiviCRM'],
];
}
}
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
/**
*
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
namespace api\v4\Entity;
use api\v4\UnitTestCase;
/**
* @group headless
*/
class TranslationTest extends UnitTestCase {
protected $ids = [];
public function getCreateOKExamples() {
$es = [];
$es['asDraft'] = [
[
'status_id:name' => 'draft',
'entity_table' => 'civicrm_event',
'entity_field' => 'description',
'entity_id' => '*EVENT*',
'language' => 'fr_CA',
'string' => 'Hello world',
],
];
$es['defaultStatus'] = [
[
'entity_table' => 'civicrm_event',
'entity_field' => 'title',
'entity_id' => '*EVENT*',
'language' => 'fr_CA',
'string' => 'Hello title',
],
];
return $es;
}
public function getCreateBadExamples() {
$es = [];
$es['badStatus'] = [
[
'status_id:name' => 'jumping',
'entity_table' => 'civicrm_event',
'entity_field' => 'description',
'entity_id' => '*EVENT*',
'language' => 'fr_CA',
'string' => 'Hello world',
],
'/Invalid status/',
];
$es['malformedField'] = [
[
'entity_table' => 'civicrm_event',
'entity_field' => 'ti!tle',
'entity_id' => '*EVENT*',
'language' => 'fr_CA',
'string' => 'Hello title',
],
'/Entity reference is malformed/',
];
$es['badTable'] = [
[
'entity_table' => 'typozcivicrm_event',
'entity_field' => 'title',
'entity_id' => '*EVENT*',
'language' => 'fr_CA',
'string' => 'Hello title',
],
'/(non-existent or non-translatable table|Cannot resolve permissions for dynamic foreign key)/',
];
$es['badFieldName'] = [
[
'status_id:name' => 'active',
'entity_table' => 'civicrm_event',
'entity_field' => 'zoological_taxonomy',
'entity_id' => '*EVENT*',
'language' => 'fr_CA',
'string' => 'Hello world',
],
'/non-existent or non-translatable field/',
];
$es['badFieldType'] = [
[
'status_id:name' => 'active',
'entity_table' => 'civicrm_event',
'entity_field' => 'event_type_id',
'entity_id' => '*EVENT*',
'language' => 'fr_CA',
'string' => '9',
],
'/non-existent or non-translatable field/',
];
$es['badEntityId'] = [
[
'status_id:name' => 'active',
'entity_table' => 'civicrm_event',
'entity_field' => 'description',
'entity_id' => 9999999,
'language' => 'fr_CA',
'string' => 'Hello world',
],
'/Entity does not exist/',
];
return $es;
}
public function getUpdateBadExamples() {
$createOk = $this->getCreateOKExamples()['asDraft'][0];
$bads = $this->getCreateBadExamples();
$es = [];
foreach ($bads as $id => $bad) {
array_unshift($bad, $createOk);
$es[$id] = $bad;
}
return $es;
}
protected function setUp(): void {
parent::setUp();
$this->ids = [];
}
/**
* @dataProvider getCreateOKExamples
* @param array $record
*/
public function testCreateOK($record) {
$record = $this->fillRecord($record);
$createResults = \civicrm_api4('Translation', 'create', [
'checkPermissions' => FALSE,
'values' => $record,
]);
$this->assertEquals(1, $createResults->count());
foreach ($createResults as $createResult) {
$getResult = \civicrm_api4('Translation', 'get', [
'where' => [['id', '=', $createResult['id']]],
]);
$this->assertEquals($record['string'], $getResult->single()['string']);
}
}
/**
* @dataProvider getCreateBadExamples
* @param array $record
* @param string $errorRegex
* Regular expression to compare against the error message.
*/
public function testCreateBad($record, $errorRegex) {
$record = $this->fillRecord($record);
try {
\civicrm_api4('Translation', 'create', [
'checkPermissions' => FALSE,
'values' => $record,
]);
$this->fail('Create should have failed');
}
catch (\API_Exception $e) {
$this->assertRegExp($errorRegex, $e->getMessage());
}
}
/**
* @dataProvider getUpdateBadExamples
* @param $createRecord
* @param $badUpdate
* @param $errorRegex
*
* @throws \API_Exception
* @throws \Civi\API\Exception\NotImplementedException
*/
public function testUpdateBad($createRecord, $badUpdate, $errorRegex) {
$record = $this->fillRecord($createRecord);
$createResults = \civicrm_api4('Translation', 'create', [
'checkPermissions' => FALSE,
'values' => $record,
]);
$this->assertEquals(1, $createResults->count());
foreach ($createResults as $createResult) {
$badUpdate = $this->fillRecord($badUpdate);
try {
\civicrm_api4('Translation', 'update', [
'where' => [['id', '=', $createResult['id']]],
'values' => $badUpdate,
]);
$this->fail('Update should fail');
}
catch (\API_Exception $e) {
$this->assertRegExp($errorRegex, $e->getMessage());
}
}
}
/**
* Fill in mocked values for the would-be record..
*
* @param array $record
*
* @return array
*/
protected function fillRecord($record) {
if ($record['entity_id'] === '*EVENT*') {
$eventId = $this->ids['*EVENT*'] ?? \CRM_Core_DAO::createTestObject('CRM_Event_BAO_Event')->id;
$record['entity_id'] = $this->ids['*EVENT*'] = $eventId;
}
return $record;
}
}
......@@ -48,14 +48,24 @@ class TestCreationParameterProvider {
$requiredParams = [];
foreach ($requiredFields as $requiredField) {
$value = $this->getRequiredValue($requiredField);
if ($entity === 'UFField' && $requiredField->getName() === 'field_name') {
// This is a ruthless hack to avoid a unique constraint - but
// it's also a test class & hard to care enough to do something
// better
$value = 'activity_campaign_id';
}
$requiredParams[$requiredField->getName()] = $value;
$requiredParams[$requiredField->getName()] = $this->getRequiredValue($requiredField);
}
// This is a ruthless hack to avoid peculiar constraints - but
// it's also a test class & hard to care enough to do something
// better
$overrides = [];
$overrides['UFField'] = [
'field_name' => 'activity_campaign_id',
];
$overrides['Translation'] = [
'entity_table' => 'civicrm_event',
'entity_field' => 'description',
'entity_id' => \CRM_Core_DAO::singleValueQuery('SELECT min(id) FROM civicrm_event'),
];
if (isset($overrides[$entity])) {
$requiredParams = array_merge($requiredParams, $overrides[$entity]);
}
unset($requiredParams['id']);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment