ManagedEntity - Add timestamp field to track user modifications
authorColeman Watts <coleman@civicrm.org>
Sat, 6 Nov 2021 20:33:20 +0000 (16:33 -0400)
committerColeman Watts <coleman@civicrm.org>
Sat, 6 Nov 2021 20:33:20 +0000 (16:33 -0400)
This uses hooks to update a timestamp field whenever a managed record is
manually updated by a user, allowing us to know if a record is still in its
'pristine' managed state or if it has been altered.

CRM/Core/BAO/Managed.php
CRM/Core/DAO/Managed.php
CRM/Core/ManagedEntities.php
CRM/Upgrade/Incremental/php/FiveFortyFive.php
Civi/Api4/Service/Spec/Provider/ManagedEntitySpecProvider.php
tests/phpunit/api/v4/Entity/ManagedEntityTest.php
xml/schema/Core/Managed.xml

index fcbb8b0960034aa1b30a6b089eb708d929f78ac3..ae64d877bbfa39f3d46ce19b6f344482505c5eee 100644 (file)
@@ -32,6 +32,13 @@ class CRM_Core_BAO_Managed extends CRM_Core_DAO_Managed implements Civi\Test\Hoo
         ->addWhere('entity_id', '=', $event->id)
         ->execute();
     }
+    // When an entity is updated, update the timestamp in corresponding Managed record
+    elseif ($event->action === 'edit' && $event->id && self::isApi4ManagedType($event->entity)) {
+      CRM_Core_DAO::executeQuery('UPDATE civicrm_managed SET entity_modified_date = CURRENT_TIMESTAMP WHERE entity_type = %1 AND entity_id = %2', [
+        1 => [$event->entity, 'String'],
+        2 => [$event->id, 'Integer'],
+      ]);
+    }
   }
 
   /**
index 91ce2a7b7acc901b5afefa487c8b3df082968843..3e0789e97dfd27c870062036903d4eb64dc99bd4 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Core/Managed.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:c8697305b613f6ca6854638025df2fd7)
+ * (GenCodeChecksum:ca11b419bcdf2bce26609d9488527023)
  */
 
 /**
@@ -72,6 +72,13 @@ class CRM_Core_DAO_Managed extends CRM_Core_DAO {
    */
   public $cleanup;
 
+  /**
+   * When the managed entity was changed from its original settings.
+   *
+   * @var timestamp
+   */
+  public $entity_modified_date;
+
   /**
    * Class constructor.
    */
@@ -107,7 +114,7 @@ class CRM_Core_DAO_Managed extends CRM_Core_DAO {
           'where' => 'civicrm_managed.id',
           'table_name' => 'civicrm_managed',
           'entity' => 'Managed',
-          'bao' => 'CRM_Core_DAO_Managed',
+          'bao' => 'CRM_Core_BAO_Managed',
           'localizable' => 0,
           'html' => [
             'type' => 'Number',
@@ -126,7 +133,7 @@ class CRM_Core_DAO_Managed extends CRM_Core_DAO {
           'where' => 'civicrm_managed.module',
           'table_name' => 'civicrm_managed',
           'entity' => 'Managed',
-          'bao' => 'CRM_Core_DAO_Managed',
+          'bao' => 'CRM_Core_BAO_Managed',
           'localizable' => 0,
           'add' => '4.2',
         ],
@@ -140,7 +147,7 @@ class CRM_Core_DAO_Managed extends CRM_Core_DAO {
           'where' => 'civicrm_managed.name',
           'table_name' => 'civicrm_managed',
           'entity' => 'Managed',
-          'bao' => 'CRM_Core_DAO_Managed',
+          'bao' => 'CRM_Core_BAO_Managed',
           'localizable' => 0,
           'add' => '4.2',
         ],
@@ -155,7 +162,7 @@ class CRM_Core_DAO_Managed extends CRM_Core_DAO {
           'where' => 'civicrm_managed.entity_type',
           'table_name' => 'civicrm_managed',
           'entity' => 'Managed',
-          'bao' => 'CRM_Core_DAO_Managed',
+          'bao' => 'CRM_Core_BAO_Managed',
           'localizable' => 0,
           'add' => '4.2',
         ],
@@ -168,7 +175,7 @@ class CRM_Core_DAO_Managed extends CRM_Core_DAO {
           'where' => 'civicrm_managed.entity_id',
           'table_name' => 'civicrm_managed',
           'entity' => 'Managed',
-          'bao' => 'CRM_Core_DAO_Managed',
+          'bao' => 'CRM_Core_BAO_Managed',
           'localizable' => 0,
           'add' => '4.2',
         ],
@@ -182,7 +189,7 @@ class CRM_Core_DAO_Managed extends CRM_Core_DAO {
           'where' => 'civicrm_managed.cleanup',
           'table_name' => 'civicrm_managed',
           'entity' => 'Managed',
-          'bao' => 'CRM_Core_DAO_Managed',
+          'bao' => 'CRM_Core_BAO_Managed',
           'localizable' => 0,
           'html' => [
             'type' => 'Select',
@@ -192,6 +199,20 @@ class CRM_Core_DAO_Managed extends CRM_Core_DAO {
           ],
           'add' => '4.5',
         ],
+        'entity_modified_date' => [
+          'name' => 'entity_modified_date',
+          'type' => CRM_Utils_Type::T_TIMESTAMP,
+          'title' => ts('Entity Modified Date'),
+          'description' => ts('When the managed entity was changed from its original settings.'),
+          'required' => FALSE,
+          'where' => 'civicrm_managed.entity_modified_date',
+          'default' => NULL,
+          'table_name' => 'civicrm_managed',
+          'entity' => 'Managed',
+          'bao' => 'CRM_Core_BAO_Managed',
+          'localizable' => 0,
+          'add' => '5.45',
+        ],
       ];
       CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
     }
index 2978393b351a6f86c7de074a8216d2fb80e25bf9..80f79e639a1e228780942ebe82cf026ab4118d18 100644 (file)
@@ -336,7 +336,7 @@ class CRM_Core_ManagedEntities {
    *   Entity specification (per hook_civicrm_managedEntities).
    */
   protected function updateExistingEntity($dao, $todo) {
-    $policy = CRM_Utils_Array::value('update', $todo, 'always');
+    $policy = $todo['update'] ?? 'always';
     $doUpdate = ($policy === 'always');
 
     if ($doUpdate && $todo['params']['version'] == 3) {
@@ -374,8 +374,10 @@ class CRM_Core_ManagedEntities {
       civicrm_api4($dao->entity_type, 'update', $params);
     }
 
-    if (isset($todo['cleanup'])) {
-      $dao->cleanup = $todo['cleanup'];
+    if (isset($todo['cleanup']) || $doUpdate) {
+      $dao->cleanup = $todo['cleanup'] ?? NULL;
+      // Reset the `entity_modified_date` timestamp if reverting record.
+      $dao->entity_modified_date = $doUpdate ? 'null' : NULL;
       $dao->update();
     }
   }
@@ -401,6 +403,9 @@ class CRM_Core_ManagedEntities {
       if ($result['is_error']) {
         $this->onApiError($dao->entity_type, 'create', $params, $result);
       }
+      // Reset the `entity_modified_date` timestamp to indicate that the entity has not been modified by the user.
+      $dao->entity_modified_date = 'null';
+      $dao->update();
     }
   }
 
index 1398d93703e03fb7ec34affcd883803c9c3d93f4..90bf9181dc472c4cd361feddf00ddc61509a25fe 100644 (file)
@@ -49,27 +49,16 @@ class CRM_Upgrade_Incremental_php_FiveFortyFive extends CRM_Upgrade_Incremental_
     // }
   }
 
-  /*
-   * Important! All upgrade functions MUST add a 'runSql' task.
-   * Uncomment and use the following template for a new upgrade version
-   * (change the x in the function name):
+  /**
+   * Upgrade function.
+   *
+   * @param string $rev
    */
-
-  //  /**
-  //   * Upgrade function.
-  //   *
-  //   * @param string $rev
-  //   */
-  //  public function upgrade_5_0_x($rev): void {
-  //    $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
-  //    $this->addTask('Do the foo change', 'taskFoo', ...);
-  //    // Additional tasks here...
-  //    // Note: do not use ts() in the addTask description because it adds unnecessary strings to transifex.
-  //    // The above is an exception because 'Upgrade DB to %1: SQL' is generic & reusable.
-  //  }
-
-  // public static function taskFoo(CRM_Queue_TaskContext $ctx, ...): bool {
-  //   return TRUE;
-  // }
+  public function upgrade_5_45_alpha1($rev): void {
+    $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
+    $this->addTask('Add entity_modified_date column to civicrm_managed', 'addColumn',
+      'civicrm_managed', 'entity_modified_date', "timestamp NULL DEFAULT NULL COMMENT 'When the managed entity was changed from its original settings.'"
+    );
+  }
 
 }
index c7ae06b979e760818ca4dd8c299209b32ebc8b36..175cadf6fe186fb37174a97ed43c2038a13ddaa8 100644 (file)
@@ -53,6 +53,16 @@ class ManagedEntitySpecProvider implements Generic\SpecProviderInterface {
       ->setOptionsCallback(['CRM_Core_PseudoConstant', 'getExtensions'])
       ->setSqlRenderer([__CLASS__, 'renderBaseModule']);
     $spec->addFieldSpec($field);
+
+    $field = (new FieldSpec('local_modified_date', $spec->getEntity(), 'Timestamp'))
+      ->setLabel(ts('Locally Modified'))
+      ->setTitle(ts('Locally modified'))
+      ->setColumnName('id')
+      ->setDescription(ts('When the managed entity was changed from its original settings'))
+      ->setType('Extra')
+      ->setReadonly(TRUE)
+      ->setSqlRenderer([__CLASS__, 'renderLocalModifiedDate']);
+    $spec->addFieldSpec($field);
   }
 
   /**
@@ -91,4 +101,15 @@ class ManagedEntitySpecProvider implements Generic\SpecProviderInterface {
     return "(SELECT `civicrm_managed`.`module` FROM `civicrm_managed` WHERE `civicrm_managed`.`entity_id` = $id AND `civicrm_managed`.`entity_type` = '$entity' LIMIT 1)";
   }
 
+  /**
+   * Get sql snippet for local_modified_date
+   * @param array $field
+   * return string
+   */
+  public static function renderLocalModifiedDate(array $field): string {
+    $id = $field['sql_name'];
+    $entity = $field['entity'];
+    return "(SELECT `civicrm_managed`.`entity_modified_date` FROM `civicrm_managed` WHERE `civicrm_managed`.`entity_id` = $id AND `civicrm_managed`.`entity_type` = '$entity' LIMIT 1)";
+  }
+
 }
index f959940f8ff51e4f107c4c8173b6c3a2a78df22b..bc86da428dd795f5800945ddb1d101ec55cda49f 100644 (file)
@@ -70,22 +70,28 @@ class ManagedEntityTest extends UnitTestCase implements TransactionalInterface,
 
     $search = SavedSearch::get(FALSE)
       ->addWhere('name', '=', 'TestManagedSavedSearch')
+      ->addSelect('description', 'local_modified_date')
       ->execute()->single();
     $this->assertEquals('Original state', $search['description']);
+    $this->assertNull($search['local_modified_date']);
 
     SavedSearch::update(FALSE)
       ->addValue('id', $search['id'])
       ->addValue('description', 'Altered state')
       ->execute();
 
+    $time = $this->getCurrentTimestamp();
     $search = SavedSearch::get(FALSE)
       ->addWhere('name', '=', 'TestManagedSavedSearch')
-      ->addSelect('description', 'has_base', 'base_module')
+      ->addSelect('description', 'has_base', 'base_module', 'local_modified_date')
       ->execute()->single();
     $this->assertEquals('Altered state', $search['description']);
     // Check calculated fields
     $this->assertTrue($search['has_base']);
     $this->assertEquals('civicrm', $search['base_module']);
+    // local_modified_date should reflect the update just made
+    $this->assertGreaterThanOrEqual($time, $search['local_modified_date']);
+    $this->assertLessThanOrEqual($this->getCurrentTimestamp(), $search['local_modified_date']);
 
     SavedSearch::revert(FALSE)
       ->addWhere('name', '=', 'TestManagedSavedSearch')
@@ -94,7 +100,7 @@ class ManagedEntityTest extends UnitTestCase implements TransactionalInterface,
     // Entity should be revered to original state
     $result = SavedSearch::get(FALSE)
       ->addWhere('name', '=', 'TestManagedSavedSearch')
-      ->addSelect('description', 'has_base', 'base_module')
+      ->addSelect('description', 'has_base', 'base_module', 'local_modified_date')
       ->setDebug(TRUE)
       ->execute();
     $search = $result->single();
@@ -102,6 +108,8 @@ class ManagedEntityTest extends UnitTestCase implements TransactionalInterface,
     // Check calculated fields
     $this->assertTrue($search['has_base']);
     $this->assertEquals('civicrm', $search['base_module']);
+    // local_modified_date should be reset by the revert action
+    $this->assertNull($search['local_modified_date']);
 
     // Check calculated fields for a non-managed entity - they should be empty
     $newName = uniqid(__FUNCTION__);
@@ -111,12 +119,13 @@ class ManagedEntityTest extends UnitTestCase implements TransactionalInterface,
       ->execute();
     $search = SavedSearch::get(FALSE)
       ->addWhere('name', '=', $newName)
-      ->addSelect('label', 'has_base', 'base_module')
+      ->addSelect('label', 'has_base', 'base_module', 'local_modified_date')
       ->execute()->single();
     $this->assertEquals('Whatever', $search['label']);
     // Check calculated fields
     $this->assertEquals(NULL, $search['base_module']);
     $this->assertFalse($search['has_base']);
+    $this->assertNull($search['local_modified_date']);
   }
 
   /**
@@ -143,4 +152,8 @@ class ManagedEntityTest extends UnitTestCase implements TransactionalInterface,
     ];
   }
 
+  private function getCurrentTimestamp() {
+    return \CRM_Core_DAO::singleValueQuery('SELECT CURRENT_TIMESTAMP');
+  }
+
 }
index b00d1aa042ad1b830dce1eb10b8d5b8b1cad4f03..ee884dfee695315cdfac1b5d6422358f0d3ca9a5 100644 (file)
     </html>
     <add>4.5</add>
   </field>
+  <field>
+    <name>entity_modified_date</name>
+    <type>timestamp</type>
+    <comment>When the managed entity was changed from its original settings.</comment>
+    <required>false</required>
+    <default>NULL</default>
+    <add>5.45</add>
+  </field>
   <index>
     <name>UI_managed_entity</name>
     <fieldName>entity_type</fieldName>