From 0501e0eac68d304debeb723da96a96165f272d61 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Mon, 4 Apr 2022 21:24:16 -0400 Subject: [PATCH] APIv4 - Add API for RecentItem This exposes an api entity for the Recent Items stored in the user session. In the process, it simplifies the process of adding a recent item, performing most of the lookups automatically. --- CRM/Activity/BAO/Activity.php | 21 +++ CRM/Contact/BAO/Contact.php | 28 ++- CRM/Contact/Form/Contact.php | 2 +- CRM/Contact/Page/View.php | 5 +- CRM/Core/DAO.php | 21 +++ CRM/Utils/Recent.php | 167 +++++++++++++----- Civi/Api4/Generic/BasicEntity.php | 2 +- Civi/Api4/RecentItem.php | 86 +++++++++ .../phpunit/api/v4/Action/RecentItemsTest.php | 27 ++- .../phpunit/api/v4/Entity/ConformanceTest.php | 45 +++-- .../phpunit/api/v4/Entity/RecentItemTest.php | 95 ++++++++++ .../Service/TestCreationParameterProvider.php | 5 +- 12 files changed, 421 insertions(+), 83 deletions(-) create mode 100644 Civi/Api4/RecentItem.php create mode 100644 tests/phpunit/api/v4/Entity/RecentItemTest.php diff --git a/CRM/Activity/BAO/Activity.php b/CRM/Activity/BAO/Activity.php index d106c6f5cf..a6f4977010 100644 --- a/CRM/Activity/BAO/Activity.php +++ b/CRM/Activity/BAO/Activity.php @@ -2797,4 +2797,25 @@ INNER JOIN civicrm_option_group grp ON (grp.id = option_group_id AND grp.name = ]; } + /** + * Get icon for a particular activity (based on type). + * + * Example: `CRM_Activity_BAO_Activity::getIcon('Activity', 123)` + * + * @param string $entityName + * Always "Activity". + * @param int $entityId + * Id of the activity. + * @throws CRM_Core_Exception + */ + public static function getEntityIcon(string $entityName, int $entityId) { + $field = Civi\Api4\Activity::getFields(FALSE) + ->addWhere('name', '=', 'activity_type_id') + ->setLoadOptions(['id', 'label', 'icon']) + ->execute()->single(); + $activityTypes = array_column($field['options'], NULL, 'id'); + $activityType = CRM_Core_DAO::getFieldValue(parent::class, $entityId, 'activity_type_id'); + return $activityTypes[$activityType]['icon'] ?? self::$_icon; + } + } diff --git a/CRM/Contact/BAO/Contact.php b/CRM/Contact/BAO/Contact.php index f89c33b142..82103ea677 100644 --- a/CRM/Contact/BAO/Contact.php +++ b/CRM/Contact/BAO/Contact.php @@ -742,7 +742,7 @@ WHERE civicrm_contact.id = " . CRM_Utils_Type::escape($id, 'Integer'); 'id' => $contact->id, 'is_deleted' => 1, ]; - CRM_Utils_Hook::pre('update', $contact->contact_type, $contact->id, $updateParams); + CRM_Utils_Hook::pre('edit', $contact->contact_type, $contact->id, $updateParams); $params = [1 => [$contact->id, 'Integer']]; $query = 'DELETE FROM civicrm_uf_match WHERE contact_id = %1'; @@ -752,7 +752,7 @@ WHERE civicrm_contact.id = " . CRM_Utils_Type::escape($id, 'Integer'); $contact->save(); CRM_Core_BAO_Log::register($contact->id, 'civicrm_contact', $contact->id); - CRM_Utils_Hook::post('update', $contact->contact_type, $contact->id, $contact); + CRM_Utils_Hook::post('edit', $contact->contact_type, $contact->id, $contact); return TRUE; } @@ -3736,4 +3736,28 @@ LEFT JOIN civicrm_address ON ( civicrm_address.contact_id = civicrm_contact.id ) return CRM_Contact_BAO_Contact_Permission::allow($record['id'], $actionType, $userID); } + /** + * Get icon for a particular contact. + * + * Example: `CRM_Contact_BAO_Contact::getIcon('Contact', 123)` + * + * @param string $entityName + * Always "Contact". + * @param int $entityId + * Id of the contact. + * @throws CRM_Core_Exception + */ + public static function getEntityIcon(string $entityName, int $entityId) { + $contactTypes = CRM_Contact_BAO_ContactType::getAllContactTypes(); + $subTypes = CRM_Utils_Array::explodePadded(CRM_Core_DAO::getFieldValue(parent::class, $entityId, 'contact_sub_type')); + foreach ((array) $subTypes as $subType) { + if (!empty($contactTypes[$subType]['icon'])) { + return $contactTypes[$subType]['icon']; + } + } + // If no sub-type icon, lookup contact type + $contactType = CRM_Core_DAO::getFieldValue(parent::class, $entityId, 'contact_type'); + return $contactTypes[$contactType]['icon'] ?? self::$_icon; + } + } diff --git a/CRM/Contact/Form/Contact.php b/CRM/Contact/Form/Contact.php index 76aa4b196a..8beb4d3e11 100644 --- a/CRM/Contact/Form/Contact.php +++ b/CRM/Contact/Form/Contact.php @@ -1046,7 +1046,7 @@ class CRM_Contact_Form_Contact extends CRM_Core_Form { CRM_Utils_Recent::add($contact->display_name, CRM_Utils_System::url('civicrm/contact/view', 'reset=1&cid=' . $contact->id), $contact->id, - $this->_contactType, + 'Contact', $contact->id, $contact->display_name, $recentOther diff --git a/CRM/Contact/Page/View.php b/CRM/Contact/Page/View.php index bbbc950dde..180a528c9c 100644 --- a/CRM/Contact/Page/View.php +++ b/CRM/Contact/Page/View.php @@ -169,8 +169,7 @@ class CRM_Contact_Page_View extends CRM_Core_Page { $recentOther = [ 'imageUrl' => $contactImageUrl, - 'subtype' => $contactSubtype, - 'isDeleted' => $isDeleted, + 'is_deleted' => $isDeleted, ]; if (CRM_Contact_BAO_Contact_Permission::allow($this->_contactId, CRM_Core_Permission::EDIT)) { @@ -186,7 +185,7 @@ class CRM_Contact_Page_View extends CRM_Core_Page { CRM_Utils_Recent::add($displayName, CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$this->_contactId}"), $this->_contactId, - $contactType, + 'Contact', $this->_contactId, $displayName, $recentOther diff --git a/CRM/Core/DAO.php b/CRM/Core/DAO.php index 31c43e0b78..4e954132f8 100644 --- a/CRM/Core/DAO.php +++ b/CRM/Core/DAO.php @@ -3291,6 +3291,27 @@ SELECT contact_id return static::$_paths ?? []; } + /** + * Overridable function to get icon for a particular entity. + * + * Example: `CRM_Contact_BAO_Contact::getIcon('Contact', 123)` + * + * @param string $entityName + * Short name of the entity. This may seem redundant because the entity name can usually be inferred + * from the BAO class being called, but not always. Some virtual entities share a BAO class. + * @param int $entityId + * Id of the entity. + * @throws CRM_Core_Exception + */ + public static function getEntityIcon(string $entityName, int $entityId) { + if (static::class === 'CRM_Core_DAO' || static::class !== CRM_Core_DAO_AllCoreTables::getBAOClassName(static::class)) { + throw new CRM_Core_Exception('CRM_Core_DAO::getIcon must be called on a BAO class e.g. CRM_Contact_BAO_Contact::getIcon("Contact", 123).'); + } + // By default, just return the icon representing this entity. If there's more complex lookup to do, + // the BAO for this entity should override this method. + return static::$_icon; + } + /** * When creating a record without a supplied name, * create a unique, clean name derived from the label. diff --git a/CRM/Utils/Recent.php b/CRM/Utils/Recent.php index fc98cc37fe..a2a22ebb41 100644 --- a/CRM/Utils/Recent.php +++ b/CRM/Utils/Recent.php @@ -14,6 +14,8 @@ * @copyright CiviCRM LLC https://civicrm.org/licensing */ +use Civi\Api4\Utils\CoreUtil; + /** * Recent items utility class. */ @@ -74,6 +76,20 @@ class CRM_Utils_Recent { return self::$_recent; } + /** + * Create function used by the API - supplies defaults + * + * @param array $params + */ + public static function create(array $params) { + $params['title'] = $params['title'] ?? self::getTitle($params['entity_type'], $params['entity_id']); + $params['view_url'] = $params['view_url'] ?? self::getUrl($params['entity_type'], $params['entity_id'], 'view'); + $params['edit_url'] = $params['edit_url'] ?? self::getUrl($params['entity_type'], $params['entity_id'], 'update'); + $params['delete_url'] = $params['delete_url'] ?? (empty($params['is_deleted']) ? self::getUrl($params['entity_type'], $params['entity_id'], 'delete') : NULL); + self::add($params['title'], $params['view_url'], $params['entity_id'], $params['entity_type'], $params['contact_id'] ?? NULL, NULL, $params); + return $params; + } + /** * Add an item to the recent stack. * @@ -81,29 +97,33 @@ class CRM_Utils_Recent { * The title to display. * @param string $url * The link for the above title. - * @param string $id + * @param string $entityId * Object id. - * @param string $type + * @param string $entityType * @param int $contactId + * Deprecated, probably unused param * @param string $contactName + * Deprecated, probably unused param * @param array $others */ public static function add( $title, $url, - $id, - $type, + $entityId, + $entityType, $contactId, $contactName, $others = [] ) { + $entityType = self::normalizeEntityType($entityType); + // Abort if this entity type is not supported - if (!self::isProviderEnabled($type)) { + if (!self::isProviderEnabled($entityType)) { return; } // Ensure item is not already present in list - self::removeItems(['id' => $id, 'type' => $type]); + self::removeItems(['entity_id' => $entityId, 'entity_type' => $entityType]); if (!is_array($others)) { $others = []; @@ -112,17 +132,28 @@ class CRM_Utils_Recent { array_unshift(self::$_recent, [ 'title' => $title, + // TODO: deprecate & remove "url" in favor of "view_url" 'url' => $url, - 'id' => $id, - 'type' => $type, + 'view_url' => $url, + // TODO: deprecate & remove "id" in favor of "entity_id" + 'id' => $entityId, + 'entity_id' => (int) $entityId, + // TODO: deprecate & remove "type" in favor of "entity_type" + 'type' => $entityType, + 'entity_type' => $entityType, + // Deprecated param 'contact_id' => $contactId, + // Param appears to be unused 'contactName' => $contactName, 'subtype' => $others['subtype'] ?? NULL, - 'isDeleted' => $others['isDeleted'] ?? FALSE, + // TODO: deprecate & remove "isDeleted" in favor of "is_deleted" + 'isDeleted' => $others['is_deleted'] ?? $others['isDeleted'] ?? FALSE, + 'is_deleted' => (bool) ($others['is_deleted'] ?? $others['isDeleted'] ?? FALSE), + // imageUrl is deprecated 'image_url' => $others['imageUrl'] ?? NULL, - 'edit_url' => $others['editUrl'] ?? NULL, - 'delete_url' => $others['deleteUrl'] ?? NULL, - 'icon' => $others['icon'] ?? self::getIcon($type, $others['subtype'] ?? NULL), + 'edit_url' => $others['edit_url'] ?? $others['editUrl'] ?? NULL, + 'delete_url' => $others['delete_url'] ?? $others['deleteUrl'] ?? NULL, + 'icon' => $others['icon'] ?? self::getIcon($entityType, $entityId), ] ); @@ -138,27 +169,61 @@ class CRM_Utils_Recent { } /** - * @param $type - * @param $subType + * Get default title for this item, based on the entity's `label_field` + * + * @param string $entityType + * @param int $entityId + * @return string|null + */ + private static function getTitle($entityType, $entityId) { + $labelField = CoreUtil::getInfoItem($entityType, 'label_field'); + $title = NULL; + if ($labelField) { + $record = civicrm_api4($entityType, 'get', [ + 'where' => [['id', '=', $entityId]], + 'select' => [$labelField], + ], 0); + $title = $record[$labelField] ?? NULL; + } + return $title ?? (CoreUtil::getInfoItem($entityType, 'label_field')); + } + + /** + * Get a link to view/update/delete a given entity. + * + * @param string $entityType + * @param int $entityId + * @param string $action + * Either 'view', 'update', or 'delete' * @return string|null */ - private static function getIcon($type, $subType) { - $icon = NULL; - $contactTypes = CRM_Contact_BAO_ContactType::getAllContactTypes(); - if (!empty($contactTypes[$type])) { - // Pick icon from contact sub-type first if available, then contact type - $subTypesAndType = array_merge((array) CRM_Utils_Array::explodePadded($subType), [$type]); - foreach ($subTypesAndType as $contactType) { - $icon = $icon ?? $contactTypes[$contactType]['icon'] ?? NULL; + private static function getUrl($entityType, $entityId, $action) { + if ($action !== 'view') { + $check = civicrm_api4($entityType, 'checkAccess', [ + 'action' => $action, + 'values' => ['id' => $entityId], + ], 0); + if (empty($check['access'])) { + return NULL; } - // If no contact type icon, proceed to lookup icon from dao - $type = 'Contact'; } - if (!$icon) { - $daoClass = CRM_Core_DAO_AllCoreTables::getFullName($type); - if ($daoClass) { - $icon = $daoClass::$_icon; - } + $paths = (array) CoreUtil::getInfoItem($entityType, 'paths'); + if (!empty($paths[$action])) { + return CRM_Utils_System::url(str_replace('[id]', $entityId, $paths[$action])); + } + return NULL; + } + + /** + * @param $entityType + * @param $entityId + * @return string|null + */ + private static function getIcon($entityType, $entityId) { + $icon = NULL; + $daoClass = CRM_Core_DAO_AllCoreTables::getFullName($entityType); + if ($daoClass) { + $icon = CRM_Core_DAO_AllCoreTables::getBAOClassName($daoClass)::getEntityIcon($entityType, $entityId); } return $icon ?: 'fa-gear'; } @@ -168,11 +233,23 @@ class CRM_Utils_Recent { * @param \Civi\Core\Event\PostEvent $event */ public static function on_hook_civicrm_post(\Civi\Core\Event\PostEvent $event) { - if ($event->action === 'delete' && $event->id && CRM_Core_Session::getLoggedInContactID()) { - // Is this an entity that might be in the recent items list? - $providersPermitted = Civi::settings()->get('recentItemsProviders') ?: array_keys(self::getProviders()); - if (in_array($event->entity, $providersPermitted)) { - self::del(['id' => $event->id, 'type' => $event->entity]); + if ($event->id && CRM_Core_Session::getLoggedInContactID()) { + $entityType = self::normalizeEntityType($event->entity); + if ($event->action === 'delete') { + // Is this an entity that might be in the recent items list? + $providersPermitted = Civi::settings()->get('recentItemsProviders') ?: array_keys(self::getProviders()); + if (in_array($entityType, $providersPermitted)) { + self::del(['entity_id' => $event->id, 'entity_type' => $entityType]); + } + } + elseif ($event->action === 'edit') { + if (isset($event->object->is_deleted)) { + \Civi\Api4\RecentItem::update() + ->addWhere('entity_type', '=', $entityType) + ->addWhere('entity_id', '=', $event->id) + ->addValue('is_deleted', (bool) $event->object->is_deleted) + ->execute(); + } } } } @@ -186,7 +263,7 @@ class CRM_Utils_Recent { self::$_recent = array_filter(self::$_recent, function($item) use ($props) { foreach ($props as $key => $val) { - if (isset($item[$key]) && $item[$key] != $val) { + if (($item[$key] ?? NULL) != $val) { return TRUE; } } @@ -226,12 +303,6 @@ class CRM_Utils_Recent { * @return bool */ public static function isProviderEnabled($providerName) { - - // Join contact types to providerName 'Contact' - $contactTypes = CRM_Contact_BAO_ContactType::contactTypes(TRUE); - if (in_array($providerName, $contactTypes)) { - $providerName = 'Contact'; - } $allowed = TRUE; // Use core setting recentItemsProviders if configured @@ -243,9 +314,23 @@ class CRM_Utils_Recent { return $allowed; } + /** + * @param string $entityType + * @return string + */ + private static function normalizeEntityType($entityType) { + // Change Individual/Organization/Household to 'Contact' + if (in_array($entityType, CRM_Contact_BAO_ContactType::basicTypes(TRUE), TRUE)) { + return 'Contact'; + } + return $entityType; + } + /** * Gets the list of available providers to civi's recent items stack * + * TODO: Make this an option group so extensions can extend it. + * * @return array */ public static function getProviders() { diff --git a/Civi/Api4/Generic/BasicEntity.php b/Civi/Api4/Generic/BasicEntity.php index 48c649b661..066ef04e7a 100644 --- a/Civi/Api4/Generic/BasicEntity.php +++ b/Civi/Api4/Generic/BasicEntity.php @@ -34,7 +34,7 @@ abstract class BasicEntity extends AbstractEntity { /** * Unique identifier for this entity. * - * @var string + * @var string|string[] */ protected static $idField = 'id'; diff --git a/Civi/Api4/RecentItem.php b/Civi/Api4/RecentItem.php new file mode 100644 index 0000000000..e8b0da1aba --- /dev/null +++ b/Civi/Api4/RecentItem.php @@ -0,0 +1,86 @@ + 'entity_id', + 'data_type' => 'Integer', + 'required' => TRUE, + ], + [ + 'name' => 'entity_type', + 'title' => 'Entity Type', + 'options' => \CRM_Utils_Recent::getProviders(), + 'required' => TRUE, + ], + [ + 'name' => 'title', + ], + [ + 'name' => 'is_deleted', + 'data_type' => 'Boolean', + ], + [ + 'name' => 'icon', + ], + [ + 'name' => 'view_url', + 'title' => 'View URL', + ], + [ + 'name' => 'edit_url', + 'title' => 'Edit URL', + ], + [ + 'name' => 'delete_url', + 'title' => 'Delete URL', + ], + ]; + }))->setCheckPermissions($checkPermissions); + } + + /** + * @return array + */ + public static function permissions() { + return [ + 'default' => ['access CiviCRM'], + ]; + } + +} diff --git a/tests/phpunit/api/v4/Action/RecentItemsTest.php b/tests/phpunit/api/v4/Action/RecentItemsTest.php index 4c81a618e1..259e186d58 100644 --- a/tests/phpunit/api/v4/Action/RecentItemsTest.php +++ b/tests/phpunit/api/v4/Action/RecentItemsTest.php @@ -21,6 +21,7 @@ namespace api\v4\Action; use api\v4\UnitTestCase; use Civi\Api4\Activity; +use Civi\Api4\RecentItem; /** * @group headless @@ -36,20 +37,23 @@ class RecentItemsTest extends UnitTestCase { ->addValue('subject', 'Hello recent!') ->execute()->first()['id']; $this->assertEquals(1, $this->getRecentItemCount(['type' => 'Activity', 'id' => $aid1])); - $this->assertStringContainsString('Hello recent!', \CRM_Utils_Recent::get()[0]['title']); + $recentItem = RecentItem::get(FALSE)->execute()->first(); + $this->assertStringContainsString('Hello recent!', $recentItem['title']); + $this->assertStringContainsString("id=$aid1", $recentItem['view_url']); + $this->assertEquals('fa-slideshare', $recentItem['icon']); $aid2 = Activity::create(FALSE) ->addValue('activity_type_id:name', 'Meeting') ->addValue('source_contact_id', $cid) ->addValue('subject', 'Goodbye recent!') ->execute()->first()['id']; - $this->assertEquals(1, $this->getRecentItemCount(['type' => 'Activity', 'id' => $aid2])); - $this->assertStringContainsString('Goodbye recent!', \CRM_Utils_Recent::get()[0]['title']); + $this->assertEquals(1, $this->getRecentItemCount(['type' => 'Activity', 'entity_id' => $aid2])); + $this->assertStringContainsString('Goodbye recent!', RecentItem::get(FALSE)->execute()[0]['title']); Activity::delete(FALSE)->addWhere('id', '=', $aid1)->execute(); - $this->assertEquals(0, $this->getRecentItemCount(['type' => 'Activity', 'id' => $aid1])); - $this->assertEquals(1, $this->getRecentItemCount(['type' => 'Activity', 'id' => $aid2])); + $this->assertEquals(0, $this->getRecentItemCount(['entity_type' => 'Activity', 'entity_id' => $aid1])); + $this->assertEquals(1, $this->getRecentItemCount(['entity_type' => 'Activity', 'entity_id' => $aid2])); } /** @@ -57,16 +61,11 @@ class RecentItemsTest extends UnitTestCase { * @return int */ private function getRecentItemCount($props) { - $count = 0; - foreach (\CRM_Utils_Recent::get() as $item) { - foreach ($props as $key => $val) { - if (($item[$key] ?? NULL) != $val) { - continue 2; - } - } - ++$count; + $recent = RecentItem::get(FALSE); + foreach ($props as $key => $val) { + $recent->addWhere($key, '=', $val); } - return $count; + return $recent->execute()->count(); } } diff --git a/tests/phpunit/api/v4/Entity/ConformanceTest.php b/tests/phpunit/api/v4/Entity/ConformanceTest.php index e6f0e56454..ae58ea64a1 100644 --- a/tests/phpunit/api/v4/Entity/ConformanceTest.php +++ b/tests/phpunit/api/v4/Entity/ConformanceTest.php @@ -210,11 +210,12 @@ class ConformanceTest extends UnitTestCase implements HookInterface { ->execute() ->indexBy('name'); - $errMsg = sprintf('%s is missing required ID field', $entity); - $subset = ['data_type' => 'Integer']; + $idField = CoreUtil::getIdFieldName($entity); - $this->assertArrayHasKey('data_type', $fields['id'], $errMsg); - $this->assertEquals('Integer', $fields['id']['data_type']); + $errMsg = sprintf('%s getfields is missing primary key field', $entity); + + $this->assertArrayHasKey($idField, $fields, $errMsg); + $this->assertEquals('Integer', $fields[$idField]['data_type']); // Ensure that the getFields (FieldSpec) format is generally consistent. foreach ($fields as $field) { @@ -272,8 +273,10 @@ class ConformanceTest extends UnitTestCase implements HookInterface { ->execute() ->first(); - $this->assertArrayHasKey('id', $createResult, "create missing ID"); - $id = $createResult['id']; + $idField = CoreUtil::getIdFieldName($entity); + + $this->assertArrayHasKey($idField, $createResult, "create missing ID"); + $id = $createResult[$idField]; $this->assertGreaterThanOrEqual(1, $id, "$entity ID not positive"); if (!$isReadOnly) { $this->assertEquals(1, $this->checkAccessCounts["{$entity}::create"]); @@ -342,7 +345,8 @@ class ConformanceTest extends UnitTestCase implements HookInterface { ->execute(); $errMsg = sprintf('Failed to fetch a %s after creation', $entity); - $this->assertEquals($id, $getResult->first()['id'], $errMsg); + $idField = CoreUtil::getIdFieldName($entity); + $this->assertEquals($id, $getResult->first()[$idField], $errMsg); $this->assertEquals(1, $getResult->count(), $errMsg); } @@ -361,7 +365,8 @@ class ConformanceTest extends UnitTestCase implements HookInterface { ->execute(); $errMsg = sprintf('Failed to fetch a %s after creation', $entity); - $this->assertEquals($id, $getResult->first()['id'], $errMsg); + $idField = CoreUtil::getIdFieldName($entity); + $this->assertEquals($id, $getResult->first()[$idField], $errMsg); $this->assertEquals(1, $getResult->count(), $errMsg); $this->resetCheckAccess(); } @@ -372,8 +377,9 @@ class ConformanceTest extends UnitTestCase implements HookInterface { * @param string $entity */ protected function checkGetCount($entityClass, $id, $entity): void { + $idField = CoreUtil::getIdFieldName($entity); $getResult = $entityClass::get(FALSE) - ->addWhere('id', '=', $id) + ->addWhere($idField, '=', $id) ->selectRowCount() ->execute(); $errMsg = sprintf('%s getCount failed', $entity); @@ -428,9 +434,10 @@ class ConformanceTest extends UnitTestCase implements HookInterface { $this->assertEquals(0, $this->checkAccessCounts["{$entity}::delete"]); $isReadOnly = $this->isReadOnly($entityClass); + $idField = CoreUtil::getIdFieldName($entity); $deleteAction = $entityClass::delete() ->setCheckPermissions(!$isReadOnly) - ->addWhere('id', '=', $id); + ->addWhere($idField, '=', $id); if (property_exists($deleteAction, 'useTrash')) { $deleteAction->setUseTrash(FALSE); @@ -440,15 +447,17 @@ class ConformanceTest extends UnitTestCase implements HookInterface { $deleteResult = $deleteAction->execute(); }); - // We should have emitted an event. - $hookEntity = ($entity === 'Contact') ? 'Individual' : $entity; /* ooph */ - $this->assertContains("pre.{$hookEntity}.delete", $log, "$entity should emit hook_civicrm_pre() for deletions"); - $this->assertContains("post.{$hookEntity}.delete", $log, "$entity should emit hook_civicrm_post() for deletions"); + if (in_array('DAOEntity', CoreUtil::getInfoItem($entity, 'type'))) { + // We should have emitted an event. + $hookEntity = ($entity === 'Contact') ? 'Individual' : $entity;/* ooph */ + $this->assertContains("pre.{$hookEntity}.delete", $log, "$entity should emit hook_civicrm_pre() for deletions"); + $this->assertContains("post.{$hookEntity}.delete", $log, "$entity should emit hook_civicrm_post() for deletions"); - // should get back an array of deleted id - $this->assertEquals([['id' => $id]], (array) $deleteResult); - if (!$isReadOnly) { - $this->assertEquals(1, $this->checkAccessCounts["{$entity}::delete"]); + // should get back an array of deleted id + $this->assertEquals([['id' => $id]], (array) $deleteResult); + if (!$isReadOnly) { + $this->assertEquals(1, $this->checkAccessCounts["{$entity}::delete"]); + } } $this->resetCheckAccess(); } diff --git a/tests/phpunit/api/v4/Entity/RecentItemTest.php b/tests/phpunit/api/v4/Entity/RecentItemTest.php new file mode 100644 index 0000000000..fd146a15f9 --- /dev/null +++ b/tests/phpunit/api/v4/Entity/RecentItemTest.php @@ -0,0 +1,95 @@ +addValue('first_name', 'Hello') + ->execute()->single()['id']; + + $this->createLoggedInUser(); + + RecentItem::create(FALSE) + ->addValue('entity_type', 'Contact') + ->addValue('entity_id', $cid) + ->execute(); + + $item = RecentItem::get(FALSE) + ->addWhere('entity_type', '=', 'Contact') + ->addWhere('entity_id', '=', $cid) + ->execute()->single(); + + $this->assertEquals('Hello', $item['title']); + $this->assertEquals('fa-user', $item['icon']); + $this->assertEquals(\CRM_Utils_System::url('civicrm/contact/view?reset=1&cid=' . $cid), $item['view_url']); + + RecentItem::delete(FALSE) + ->addWhere('entity_type', '=', 'Contact') + ->addWhere('entity_id', '=', $cid) + ->execute(); + + $this->assertCount(0, RecentItem::get(FALSE) + ->addWhere('entity_type', '=', 'Contact') + ->addWhere('entity_id', '=', $cid) + ->execute()); + + RecentItem::create(FALSE) + ->addValue('entity_type', 'Contact') + ->addValue('entity_id', $cid) + ->execute(); + + $this->assertCount(1, RecentItem::get(FALSE) + ->addWhere('entity_type', '=', 'Contact') + ->addWhere('entity_id', '=', $cid) + ->execute()); + + // Move contact to trash + Contact::delete(FALSE)->addWhere('id', '=', $cid)->execute(); + $item = RecentItem::get(FALSE) + ->addWhere('entity_type', '=', 'Contact') + ->addWhere('entity_id', '=', $cid) + ->execute()->single(); + $this->assertEquals('Hello', $item['title']); + $this->assertTrue($item['is_deleted']); + + // Delete contact + Contact::delete(FALSE)->setUseTrash(FALSE)->addWhere('id', '=', $cid)->execute(); + + $this->assertCount(0, RecentItem::get(FALSE) + ->addWhere('entity_type', '=', 'Contact') + ->addWhere('entity_id', '=', $cid) + ->execute()); + } + +} diff --git a/tests/phpunit/api/v4/Service/TestCreationParameterProvider.php b/tests/phpunit/api/v4/Service/TestCreationParameterProvider.php index 4db5a44423..7d016aacc9 100644 --- a/tests/phpunit/api/v4/Service/TestCreationParameterProvider.php +++ b/tests/phpunit/api/v4/Service/TestCreationParameterProvider.php @@ -51,6 +51,7 @@ class TestCreationParameterProvider { 'loadOptions' => TRUE, 'where' => [ ['OR', [['required', '=', TRUE], ['required_if', 'IS NOT EMPTY']]], + ['readonly', 'IS EMPTY'], ], ], 'name'); @@ -76,8 +77,6 @@ class TestCreationParameterProvider { $requiredParams = array_merge($requiredParams, $overrides[$entity]); } - unset($requiredParams['id']); - return $requiredParams; } @@ -106,7 +105,7 @@ class TestCreationParameterProvider { } if ($field['name'] === 'entity_id') { // What could possibly go wrong with this? - switch ($field['table_name']) { + switch ($field['table_name'] ?? NULL) { case 'civicrm_financial_item': return $this->getFkID(FinancialItemCreationSpecProvider::DEFAULT_ENTITY); -- 2.25.1