From 69e13f9b242901e272e8bf7e05a4f6dc9c57d01c Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sat, 6 Nov 2021 16:33:20 -0400 Subject: [PATCH] ManagedEntity - Add timestamp field to track user modifications 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 | 7 ++++ CRM/Core/DAO/Managed.php | 35 +++++++++++++++---- CRM/Core/ManagedEntities.php | 11 ++++-- CRM/Upgrade/Incremental/php/FiveFortyFive.php | 31 ++++++---------- .../Provider/ManagedEntitySpecProvider.php | 21 +++++++++++ .../api/v4/Entity/ManagedEntityTest.php | 19 ++++++++-- xml/schema/Core/Managed.xml | 8 +++++ 7 files changed, 98 insertions(+), 34 deletions(-) diff --git a/CRM/Core/BAO/Managed.php b/CRM/Core/BAO/Managed.php index fcbb8b0960..ae64d877bb 100644 --- a/CRM/Core/BAO/Managed.php +++ b/CRM/Core/BAO/Managed.php @@ -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'], + ]); + } } /** diff --git a/CRM/Core/DAO/Managed.php b/CRM/Core/DAO/Managed.php index 91ce2a7b7a..3e0789e97d 100644 --- a/CRM/Core/DAO/Managed.php +++ b/CRM/Core/DAO/Managed.php @@ -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']); } diff --git a/CRM/Core/ManagedEntities.php b/CRM/Core/ManagedEntities.php index 2978393b35..80f79e639a 100644 --- a/CRM/Core/ManagedEntities.php +++ b/CRM/Core/ManagedEntities.php @@ -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(); } } diff --git a/CRM/Upgrade/Incremental/php/FiveFortyFive.php b/CRM/Upgrade/Incremental/php/FiveFortyFive.php index 1398d93703..90bf9181dc 100644 --- a/CRM/Upgrade/Incremental/php/FiveFortyFive.php +++ b/CRM/Upgrade/Incremental/php/FiveFortyFive.php @@ -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.'" + ); + } } diff --git a/Civi/Api4/Service/Spec/Provider/ManagedEntitySpecProvider.php b/Civi/Api4/Service/Spec/Provider/ManagedEntitySpecProvider.php index c7ae06b979..175cadf6fe 100644 --- a/Civi/Api4/Service/Spec/Provider/ManagedEntitySpecProvider.php +++ b/Civi/Api4/Service/Spec/Provider/ManagedEntitySpecProvider.php @@ -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)"; + } + } diff --git a/tests/phpunit/api/v4/Entity/ManagedEntityTest.php b/tests/phpunit/api/v4/Entity/ManagedEntityTest.php index f959940f8f..bc86da428d 100644 --- a/tests/phpunit/api/v4/Entity/ManagedEntityTest.php +++ b/tests/phpunit/api/v4/Entity/ManagedEntityTest.php @@ -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'); + } + } diff --git a/xml/schema/Core/Managed.xml b/xml/schema/Core/Managed.xml index b00d1aa042..ee884dfee6 100644 --- a/xml/schema/Core/Managed.xml +++ b/xml/schema/Core/Managed.xml @@ -76,6 +76,14 @@ 4.5 + + entity_modified_date + timestamp + When the managed entity was changed from its original settings. + false + NULL + 5.45 + UI_managed_entity entity_type -- 2.25.1