Skip to content

Commit

Permalink
Merge pull request #26357 from colemanw/groupRefresh
Browse files Browse the repository at this point in the history
APIv4 - Add Group.cache_expired calculated field and Group::refresh action
  • Loading branch information
colemanw authored May 30, 2023
2 parents 5106461 + e9c28bf commit 538ff4c
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 7 deletions.
13 changes: 8 additions & 5 deletions CRM/Contact/BAO/GroupContactCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,11 +336,12 @@ public static function removeContact($cid, $groupId = NULL) {
/**
* 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) {
Expand All @@ -361,6 +362,7 @@ public static function load($group, $force = FALSE) {
self::releaseGroupLocks([$groupID]);
$groupContactsTempTable->drop();
}
return in_array($groupID, $lockedGroups);
}

/**
Expand Down Expand Up @@ -482,14 +484,15 @@ public static function getCacheInvalidDateTime(): string {
}

/**
* 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'],
]);
}

Expand Down
35 changes: 35 additions & 0 deletions Civi/Api4/Action/Group/Refresh.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
}

}
11 changes: 11 additions & 0 deletions Civi/Api4/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@
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
*
Expand All @@ -34,6 +44,7 @@ public static function permissions():array {
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;
}

Expand Down
24 changes: 23 additions & 1 deletion Civi/Api4/Service/Spec/Provider/GroupGetSpecProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ public function modifySpec(RequestSpec $spec): void {
->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);
}

/**
Expand All @@ -44,7 +53,7 @@ public function modifySpec(RequestSpec $spec): void {
* @return bool
*/
public function applies($entity, $action): bool {
return $entity === 'Group' && $action === 'get';
return $entity === 'Group' && in_array($action, ['get', 'refresh'], TRUE);
}

/**
Expand All @@ -60,4 +69,17 @@ public static function countContacts(array $field): string {
)";
}

/**
* 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)";
}

}
2 changes: 1 addition & 1 deletion Civi/Api4/Utils/CoreUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public static function checkAccessRecord(\Civi\Api4\Generic\AbstractAction $apiR
// 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();
}

Expand Down
59 changes: 59 additions & 0 deletions tests/phpunit/api/v4/Entity/GroupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,65 @@
*/
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 = [
Expand Down

0 comments on commit 538ff4c

Please sign in to comment.