Managed - Backfill default values when reverting APIv4 entity
authorcolemanw <coleman@civicrm.org>
Mon, 2 Oct 2023 20:04:03 +0000 (16:04 -0400)
committercolemanw <coleman@civicrm.org>
Tue, 3 Oct 2023 13:39:30 +0000 (09:39 -0400)
CRM/Core/ManagedEntities.php
Civi/Api4/Generic/Traits/ManagedEntity.php
tests/phpunit/api/v4/Entity/ManagedEntityTest.php

index c6bc824419f160df49bef2fb51699c5168668800..daacdca7a5922f3a1bffd505cfaf969669b7e1a6 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 use Civi\Api4\Managed;
+use Civi\Api4\Utils\CoreUtil;
 
 /**
  * The ManagedEntities system allows modules to add records to the database
@@ -116,12 +117,14 @@ class CRM_Core_ManagedEntities {
 
   /**
    * Force-revert a record back to its original state.
-   * @param array $params
-   *   Key->value properties of CRM_Core_DAO_Managed used to match an existing record
+   * @param string $entityType
+   * @param $entityId
+   * @return bool
    */
-  public function revert(array $params) {
+  public function revert(string $entityType, $entityId): bool {
     $mgd = new \CRM_Core_DAO_Managed();
-    $mgd->copyValues($params);
+    $mgd->entity_type = $entityType;
+    $mgd->entity_id = $entityId;
     $mgd->find(TRUE);
     $declarations = $this->getDeclarations([$mgd->module]);
     $declarations = CRM_Utils_Array::findAll($declarations, [
@@ -130,12 +133,39 @@ class CRM_Core_ManagedEntities {
       'entity' => $mgd->entity_type,
     ]);
     if ($mgd->id && isset($declarations[0])) {
-      $this->updateExistingEntity(['update' => 'always'] + $declarations[0] + $mgd->toArray());
+      $item = ['update' => 'always'] + $declarations[0] + $mgd->toArray();
+      $this->backfillDefaults($item);
+      $this->updateExistingEntity($item);
       return TRUE;
     }
     return FALSE;
   }
 
+  /**
+   * Backfill default values to restore record to a pristine state
+   *
+   * @param array $item Managed APIv4 record
+   */
+  private function backfillDefaults(array &$item): void {
+    if ($item['params']['version'] != 4) {
+      return;
+    }
+    // Fetch default values for fields that are writeable
+    $condition = [['type', '=', 'Field'], ['readonly', 'IS EMPTY'], ['default_value', '!=', 'now']];
+    // Exclude "weight" as that auto-adjusts
+    if (in_array('SortableEntity', CoreUtil::getInfoItem($item['entity_type'], 'type'), TRUE)) {
+      $weightCol = CoreUtil::getInfoItem($item['entity_type'], 'order_by');
+      $condition[] = ['name', '!=', $weightCol];
+    }
+    $getFields = civicrm_api4($item['entity_type'], 'getFields', [
+      'checkPermissions' => FALSE,
+      'action' => 'create',
+      'where' => $condition,
+    ]);
+    $defaultValues = $getFields->indexBy('name')->column('default_value');
+    $item['params']['values'] += $defaultValues;
+  }
+
   /**
    * Take appropriate action on every managed entity.
    *
@@ -332,7 +362,7 @@ class CRM_Core_ManagedEntities {
 
       case 'unused':
         if (CRM_Core_BAO_Managed::isApi4ManagedType($item['entity_type'])) {
-          $getRefCount = \Civi\Api4\Utils\CoreUtil::getRefCount($item['entity_type'], $item['entity_id']);
+          $getRefCount = CoreUtil::getRefCount($item['entity_type'], $item['entity_id']);
         }
         else {
           $getRefCount = civicrm_api3($item['entity_type'], 'getrefcount', [
index f014bc76a69e516f55ee543a20d5464cca3ac32d..1e0ebe4b2edc73a0db512fec1493e509bfc4567e 100644 (file)
@@ -27,8 +27,7 @@ trait ManagedEntity {
    */
   public static function revert($checkPermissions = TRUE) {
     return (new BasicBatchAction(static::getEntityName(), __FUNCTION__, function($item, BasicBatchAction $action) {
-      $params = ['entity_type' => $action->getEntityName(), 'entity_id' => $item['id']];
-      if (\CRM_Core_ManagedEntities::singleton()->revert($params)) {
+      if (\CRM_Core_ManagedEntities::singleton()->revert($action->getEntityName(), $item['id'])) {
         return $item;
       }
       else {
index 7c739a4e2d9e48955971ac46f43f90ed67abeff0..85e6b2624163d41606e246a6d426e5a8120c5be2 100644 (file)
@@ -85,6 +85,17 @@ class ManagedEntityTest extends TestCase implements HeadlessInterface, Transacti
    * @throws \CRM_Core_Exception
    */
   public function testRevertSavedSearch(): void {
+    $originalState = [
+      'name' => 'TestManagedSavedSearch',
+      'label' => 'Test Saved Search',
+      'description' => 'Original state',
+      'api_entity' => 'Contact',
+      'api_params' => [
+        'version' => 4,
+        'select' => ['id'],
+        'orderBy' => ['id', 'ASC'],
+      ],
+    ];
     $this->_managedEntities[] = [
       // Setting module to 'civicrm' works for the test but not sure we should actually support that
       // as it's probably better to package stuff in a core extension instead of core itself.
@@ -95,17 +106,7 @@ class ManagedEntityTest extends TestCase implements HeadlessInterface, Transacti
       'update' => 'never',
       'params' => [
         'version' => 4,
-        'values' => [
-          'name' => 'TestManagedSavedSearch',
-          'label' => 'Test Saved Search',
-          'description' => 'Original state',
-          'api_entity' => 'Contact',
-          'api_params' => [
-            'version' => 4,
-            'select' => ['id'],
-            'orderBy' => ['id', 'ASC'],
-          ],
-        ],
+        'values' => $originalState,
       ],
     ];
 
@@ -113,20 +114,24 @@ class ManagedEntityTest extends TestCase implements HeadlessInterface, Transacti
 
     $search = SavedSearch::get(FALSE)
       ->addWhere('name', '=', 'TestManagedSavedSearch')
-      ->addSelect('description', 'local_modified_date')
+      ->addSelect('*', 'local_modified_date')
       ->execute()->single();
-    $this->assertEquals('Original state', $search['description']);
+    foreach ($originalState as $fieldName => $originalValue) {
+      $this->assertEquals($originalValue, $search[$fieldName]);
+    }
+    $this->assertNull($search['expires_date']);
     $this->assertNull($search['local_modified_date']);
 
     SavedSearch::update(FALSE)
       ->addValue('id', $search['id'])
       ->addValue('description', 'Altered state')
+      ->addValue('expires_date', 'now + 1 year')
       ->execute();
 
     $time = $this->getCurrentTimestamp();
     $search = SavedSearch::get(FALSE)
       ->addWhere('name', '=', 'TestManagedSavedSearch')
-      ->addSelect('description', 'has_base', 'base_module', 'local_modified_date')
+      ->addSelect('*', 'has_base', 'base_module', 'local_modified_date')
       ->execute()->single();
     $this->assertEquals('Altered state', $search['description']);
     // Check calculated fields
@@ -135,6 +140,7 @@ class ManagedEntityTest extends TestCase implements HeadlessInterface, Transacti
     // local_modified_date should reflect the update just made
     $this->assertGreaterThanOrEqual($time, $search['local_modified_date']);
     $this->assertLessThanOrEqual($this->getCurrentTimestamp(), $search['local_modified_date']);
+    $this->assertGreaterThan($time, $search['expires_date']);
 
     SavedSearch::revert(FALSE)
       ->addWhere('name', '=', 'TestManagedSavedSearch')
@@ -143,10 +149,13 @@ class ManagedEntityTest extends TestCase implements HeadlessInterface, Transacti
     // Entity should be revered to original state
     $result = SavedSearch::get(FALSE)
       ->addWhere('name', '=', 'TestManagedSavedSearch')
-      ->addSelect('description', 'has_base', 'base_module', 'local_modified_date')
+      ->addSelect('*', 'has_base', 'base_module', 'local_modified_date')
       ->execute();
     $search = $result->single();
-    $this->assertEquals('Original state', $search['description']);
+    foreach ($originalState as $fieldName => $originalValue) {
+      $this->assertEquals($originalValue, $search[$fieldName]);
+    }
+    $this->assertNull($search['expires_date']);
     // Check calculated fields
     $this->assertTrue($search['has_base']);
     $this->assertEquals('civicrm', $search['base_module']);