APIv4 - Add Group.cache_expired calculated field and Group::refresh action
authorcolemanw <coleman@civicrm.org>
Fri, 26 May 2023 21:55:51 +0000 (17:55 -0400)
committercolemanw <coleman@civicrm.org>
Fri, 26 May 2023 21:55:51 +0000 (17:55 -0400)
CRM/Contact/BAO/GroupContactCache.php
Civi/Api4/Action/Group/Refresh.php [new file with mode: 0644]
Civi/Api4/Group.php
Civi/Api4/Service/Spec/Provider/GroupGetSpecProvider.php
Civi/Api4/Utils/CoreUtil.php
tests/phpunit/api/v4/Entity/GroupTest.php

index 6c451f8bc6c1f5d7ba5420eeb20c6ec74a4856d8..8f88bf9548a6db9b48b789739486e082a0b9fe14 100644 (file)
@@ -336,11 +336,12 @@ WHERE  id IN ( $groupIDs )
   /**
    * Load the smart group cache for a saved search.
    *
-   * @param object $group
+   * @param CRM_Core_DAO $group
    *   The smart group that needs to be loaded.
    * @param bool $force
    *   deprecated parameter = Should we force a search through.
    *
+   * return bool
    * @throws \CRM_Core_Exception
    */
   public static function load($group, $force = FALSE) {
@@ -361,6 +362,7 @@ WHERE  id IN ( $groupIDs )
       self::releaseGroupLocks([$groupID]);
       $groupContactsTempTable->drop();
     }
+    return in_array($groupID, $lockedGroups);
   }
 
   /**
@@ -482,14 +484,15 @@ ORDER BY   gc.contact_id, g.children
   }
 
   /**
-   * Invalidates the smart group cache for a particular group
-   * @param int $groupID - Group to invalidate
+   * Invalidates the smart group cache for one or more groups
+   * @param int|int[] $groupID - Group to invalidate
    */
   public static function invalidateGroupContactCache($groupID): void {
+    $groupIDs = implode(',', (array) $groupID);
     CRM_Core_DAO::executeQuery('UPDATE civicrm_group
       SET cache_date = NULL
-      WHERE id = %1 AND (saved_search_id IS NOT NULL OR children IS NOT NULL)', [
-        1 => [$groupID, 'Positive'],
+      WHERE id IN (%1) AND (saved_search_id IS NOT NULL OR children IS NOT NULL)', [
+        1 => [$groupIDs, 'CommaSeparatedIntegers'],
       ]);
   }
 
diff --git a/Civi/Api4/Action/Group/Refresh.php b/Civi/Api4/Action/Group/Refresh.php
new file mode 100644 (file)
index 0000000..ce4262b
--- /dev/null
@@ -0,0 +1,35 @@
+<?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       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\Group;
+
+use Civi\Api4\Generic\Result;
+
+/**
+ * @inheritDoc
+ */
+class Refresh extends \Civi\Api4\Generic\BasicBatchAction {
+
+  protected function processBatch(Result $result, array $items) {
+    if ($items) {
+      \CRM_Contact_BAO_GroupContactCache::invalidateGroupContactCache(array_column($items, 'id'));
+    }
+    foreach ($items as $item) {
+      $group = new \CRM_Contact_DAO_Group();
+      $group->id = $item['id'];
+      if (\CRM_Contact_BAO_GroupContactCache::load($group)) {
+        $result[] = $item;
+      }
+    }
+  }
+
+}
index 67a34edeef798f3617d81a0bb2aff938ed6a5640..0a5d2e0ca6063bcafcbf08a3a099192009a2223e 100644 (file)
@@ -22,6 +22,16 @@ namespace Civi\Api4;
 class Group extends Generic\DAOEntity {
   use Generic\Traits\ManagedEntity;
 
+  /**
+   * @param bool $checkPermissions
+   * @return \Civi\Api4\Action\Group\Refresh
+   * @throws \CRM_Core_Exception
+   */
+  public static function refresh(bool $checkPermissions = TRUE): Action\Group\Refresh {
+    return (new Action\Group\Refresh(__CLASS__, __FUNCTION__))
+      ->setCheckPermissions($checkPermissions);
+  }
+
   /**
    * Provides more-open permissions that will be further restricted by checkAccess
    *
@@ -34,6 +44,7 @@ class Group extends Generic\DAOEntity {
     return [
       // Create permission depends on the group type (see CRM_Contact_BAO_Group::_checkAccess).
       'create' => ['access CiviCRM', ['edit groups', 'access CiviMail', 'create mailings']],
+      'refresh' => ['access CiviCRM'],
     ] + $permissions;
   }
 
index 969f5c5e3197ad6f1dac2ece710c75abfc47fded..2aeb05c9b7b7ba241254fa80cc7ce8a6f8daeeb7 100644 (file)
@@ -35,6 +35,15 @@ class GroupGetSpecProvider extends \Civi\Core\Service\AutoService implements Gen
       ->setReadonly(TRUE)
       ->setSqlRenderer([__CLASS__, 'countContacts']);
     $spec->addFieldSpec($field);
+
+    // Calculated field to check smart group cache status
+    $field = new FieldSpec('cache_expired', 'Group', 'Boolean');
+    $field->setLabel(ts('Cache Expired'))
+      ->setDescription(ts('Is the smart group cache expired'))
+      ->setColumnName('cache_date')
+      ->setReadonly(TRUE)
+      ->setSqlRenderer([__CLASS__, 'getCacheExpiredSQL']);
+    $spec->addFieldSpec($field);
   }
 
   /**
@@ -44,7 +53,7 @@ class GroupGetSpecProvider extends \Civi\Core\Service\AutoService implements Gen
    * @return bool
    */
   public function applies($entity, $action): bool {
-    return $entity === 'Group' && $action === 'get';
+    return $entity === 'Group' && in_array($action, ['get', 'refresh'], TRUE);
   }
 
   /**
@@ -60,4 +69,17 @@ class GroupGetSpecProvider extends \Civi\Core\Service\AutoService implements Gen
     )";
   }
 
+  /**
+   * Generate SQL for checking cache expiration for smart groups and parent groups
+   *
+   * @return string
+   */
+  public static function getCacheExpiredSQL(array $field): string {
+    $smartGroupCacheTimeoutDateTime = \CRM_Contact_BAO_GroupContactCache::getCacheInvalidDateTime();
+    $cacheDate = $field['sql_name'];
+    $savedSearchId = substr_replace($field['sql_name'], 'saved_search_id', -11, -1);
+    $children = substr_replace($field['sql_name'], 'children', -11, -1);
+    return "IF(($savedSearchId IS NULL AND $children IS NULL) OR $cacheDate > $smartGroupCacheTimeoutDateTime, 0, 1)";
+  }
+
 }
index 24c4fb62b1c3fb2165f1d905fca8fbf33f29e131..d5185ae847790b2112cd0602b30b24d09455ce33 100644 (file)
@@ -188,7 +188,7 @@ class CoreUtil {
     // For get actions, just run a get and ACLs will be applied to the query.
     // It's a cheap trick and not as efficient as not running the query at all,
     // but BAO::checkAccess doesn't consistently check permissions for the "get" action.
-    if (is_a($apiRequest, '\Civi\Api4\Generic\DAOGetAction')) {
+    if (is_a($apiRequest, '\Civi\Api4\Generic\AbstractGetAction')) {
       return (bool) $apiRequest->addSelect('id')->addWhere('id', '=', $record['id'])->execute()->count();
     }
 
index bfe4ab1c49b8dfd551b7d608f8cfc92ce8bdb37e..4870e0c90ea380aea7d0f753403d9cd5d8f2388d 100644 (file)
@@ -28,6 +28,65 @@ use Civi\Api4\Group;
  */
 class GroupTest extends Api4TestBase {
 
+  public function testSmartGroupCache(): void {
+    \Civi::settings()->set('smartGroupCacheTimeout', 5);
+    $savedSearch = $this->createTestRecord('SavedSearch', [
+      'api_entity' => 'Contact',
+      'api_params' => [
+        'version' => 4,
+        'select' => ['id'],
+        'where' => [],
+      ],
+    ]);
+    $smartGroup = $this->createTestRecord('Group', [
+      'saved_search_id' => $savedSearch['id'],
+    ]);
+    $parentGroup = $this->createTestRecord('Group');
+    $childGroup = $this->createTestRecord('Group', [
+      'parents' => [$parentGroup['id']],
+    ]);
+    $groupIds = [$smartGroup['id'], $parentGroup['id'], $childGroup['id']];
+
+    $get = Group::get(FALSE)->addWhere('id', 'IN', $groupIds)
+      ->addSelect('id', 'cache_date', 'cache_expired')
+      ->execute()->indexBy('id');
+    // Static (non-parent) groups should always have a null cache_date and expired should always be false.
+    $this->assertNull($get[$childGroup['id']]['cache_date']);
+    $this->assertFalse($get[$childGroup['id']]['cache_expired']);
+    // The others will start off with no cache date
+    $this->assertNull($get[$parentGroup['id']]['cache_date']);
+    $this->assertTrue($get[$parentGroup['id']]['cache_expired']);
+    $this->assertNull($get[$smartGroup['id']]['cache_date']);
+    $this->assertTrue($get[$smartGroup['id']]['cache_expired']);
+
+    $refresh = Group::refresh(FALSE)
+      ->addWhere('id', 'IN', $groupIds)
+      ->execute();
+    $this->assertCount(2, $refresh);
+
+    $get = Group::get(FALSE)->addWhere('id', 'IN', $groupIds)
+      ->addSelect('id', 'cache_date', 'cache_expired')
+      ->execute()->indexBy('id');
+    $this->assertNull($get[$childGroup['id']]['cache_date']);
+    $this->assertFalse($get[$childGroup['id']]['cache_expired']);
+    $this->assertNotNull($get[$smartGroup['id']]['cache_date']);
+    $this->assertFalse($get[$smartGroup['id']]['cache_expired']);
+
+    // Pretend the group was refreshed 6 minutes ago
+    $lastRefresh = date('YmdHis', strtotime("-6 minutes"));
+    \CRM_Core_DAO::executeQuery("UPDATE civicrm_group SET cache_date = $lastRefresh WHERE id = %1", [
+      1 => [$smartGroup['id'], 'Integer'],
+    ]);
+
+    $get = Group::get(FALSE)->addWhere('id', 'IN', $groupIds)
+      ->addSelect('id', 'cache_date', 'cache_expired')
+      ->execute()->indexBy('id');
+    $this->assertNull($get[$childGroup['id']]['cache_date']);
+    $this->assertFalse($get[$childGroup['id']]['cache_expired']);
+    $this->assertNotNull($get[$smartGroup['id']]['cache_date']);
+    $this->assertTrue($get[$smartGroup['id']]['cache_expired']);
+  }
+
   public function testCreate() {
     $this->createLoggedInUser();
     \CRM_Core_Config::singleton()->userPermissionClass->permissions = [