dev/translation#67 - Add APIv4 support for "Translation" entity. Expand tests.
authorTim Otten <totten@civicrm.org>
Wed, 28 Apr 2021 06:03:46 +0000 (23:03 -0700)
committerTim Otten <totten@civicrm.org>
Sat, 12 Jun 2021 04:23:18 +0000 (21:23 -0700)
CRM/Core/BAO/Translation.php
Civi/Api4/Translation.php [new file with mode: 0644]
tests/phpunit/api/v4/Entity/TranslationTest.php [new file with mode: 0644]
tests/phpunit/api/v4/Service/TestCreationParameterProvider.php

index c9db2cb98ab76209cf3f43b5f4f8b8b53d5cf986..88c9cd0e56424f14bfe8f8abebb0e18b476669fd 100644 (file)
@@ -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 (file)
index 0000000..e94ab00
--- /dev/null
@@ -0,0 +1,18 @@
+<?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'],
+    ];
+  }
+
+}
diff --git a/tests/phpunit/api/v4/Entity/TranslationTest.php b/tests/phpunit/api/v4/Entity/TranslationTest.php
new file mode 100644 (file)
index 0000000..730d00d
--- /dev/null
@@ -0,0 +1,236 @@
+<?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;
+  }
+
+}
index 757258b48e6804ffad0ae8013011948b2a2fc01b..92bb30428c040ece3c34284b2a49955c224becfb 100644 (file)
@@ -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']);