From: Tim Otten Date: Wed, 28 Apr 2021 06:03:46 +0000 (-0700) Subject: dev/translation#67 - Add APIv4 support for "Translation" entity. Expand tests. X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=7782deca2ab3ac47582989b53e0a933e9cb2e68f;p=civicrm-core.git dev/translation#67 - Add APIv4 support for "Translation" entity. Expand tests. --- diff --git a/CRM/Core/BAO/Translation.php b/CRM/Core/BAO/Translation.php index c9db2cb98a..88c9cd0e56 100644 --- a/CRM/Core/BAO/Translation.php +++ b/CRM/Core/BAO/Translation.php @@ -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')); + } + } + } + } diff --git a/Civi/Api4/Translation.php b/Civi/Api4/Translation.php new file mode 100644 index 0000000000..e94ab00f0c --- /dev/null +++ b/Civi/Api4/Translation.php @@ -0,0 +1,18 @@ + ['access CiviCRM'], + 'default' => ['translate CiviCRM'], + ]; + } + +} diff --git a/tests/phpunit/api/v4/Entity/TranslationTest.php b/tests/phpunit/api/v4/Entity/TranslationTest.php new file mode 100644 index 0000000000..730d00d3d5 --- /dev/null +++ b/tests/phpunit/api/v4/Entity/TranslationTest.php @@ -0,0 +1,236 @@ + '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; + } + +} diff --git a/tests/phpunit/api/v4/Service/TestCreationParameterProvider.php b/tests/phpunit/api/v4/Service/TestCreationParameterProvider.php index 757258b48e..92bb30428c 100644 --- a/tests/phpunit/api/v4/Service/TestCreationParameterProvider.php +++ b/tests/phpunit/api/v4/Service/TestCreationParameterProvider.php @@ -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']);