From 747156dd69084845ae5e3fe46cd8e12dc43baec3 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 26 Jul 2021 22:20:35 -0700 Subject: [PATCH 01/12] WorkflowMessageExample - Add API for searching examples --- .../Action/WorkflowMessageExample/Get.php | 65 +++++++ Civi/Api4/WorkflowMessageExample.php | 93 ++++++++++ Civi/WorkflowMessage/Examples.php | 172 ++++++++++++++++++ .../GenericWorkflowMessage/alex.ex.php | 79 ++++++++ .../v4/Entity/WorkflowMessageExampleTest.php | 59 ++++++ 5 files changed, 468 insertions(+) create mode 100644 Civi/Api4/Action/WorkflowMessageExample/Get.php create mode 100644 Civi/Api4/WorkflowMessageExample.php create mode 100644 Civi/WorkflowMessage/Examples.php create mode 100644 Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php create mode 100644 tests/phpunit/api/v4/Entity/WorkflowMessageExampleTest.php diff --git a/Civi/Api4/Action/WorkflowMessageExample/Get.php b/Civi/Api4/Action/WorkflowMessageExample/Get.php new file mode 100644 index 000000000000..c084fc706931 --- /dev/null +++ b/Civi/Api4/Action/WorkflowMessageExample/Get.php @@ -0,0 +1,65 @@ +select !== [] && !in_array('name', $this->select)) { + $this->select[] = 'name'; + } + parent::_run($result); + } + + protected function getRecords() { + $this->_scanner = new Examples(); + $all = $this->_scanner->findAll(); + foreach ($all as &$example) { + $example['tags'] = !empty($example['tags']) ? \CRM_Utils_Array::implodePadded($example['tags']) : ''; + } + return $all; + } + + protected function selectArray($values) { + $result = parent::selectArray($values); + + $heavyFields = array_intersect(['data', 'asserts'], $this->select ?: []); + if (!empty($heavyFields)) { + foreach ($result as &$item) { + $heavy = $this->_scanner->getHeavy($item['name']); + $item = array_merge($item, \CRM_Utils_Array::subset($heavy, $heavyFields)); + } + } + + return $result; + } + +} diff --git a/Civi/Api4/WorkflowMessageExample.php b/Civi/Api4/WorkflowMessageExample.php new file mode 100644 index 000000000000..6b05bc44e10d --- /dev/null +++ b/Civi/Api4/WorkflowMessageExample.php @@ -0,0 +1,93 @@ +setCheckPermissions($checkPermissions); + } + + /** + * @param bool $checkPermissions + * @return Generic\BasicGetFieldsAction + */ + public static function getFields($checkPermissions = TRUE) { + return (new Generic\BasicGetFieldsAction(__CLASS__, __FUNCTION__, function () { + return [ + [ + 'name' => 'name', + 'title' => 'Example Name', + 'data_type' => 'String', + ], + [ + 'name' => 'title', + 'title' => 'Example Title', + 'data_type' => 'String', + ], + [ + 'name' => 'workflow', + 'title' => 'Workflow Name', + 'data_type' => 'String', + ], + [ + 'name' => 'file', + 'title' => 'File Path', + 'data_type' => 'String', + 'description' => 'If the example is loaded from a file, this is the location.', + ], + [ + 'name' => 'tags', + 'title' => 'Tags', + 'data_type' => 'String', + 'serialize' => \CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND, + ], + [ + 'name' => 'data', + 'title' => 'Example data', + 'data_type' => 'String', + 'serialize' => \CRM_Core_DAO::SERIALIZE_JSON, + ], + [ + 'name' => 'asserts', + 'title' => 'Test assertions', + 'data_type' => 'String', + 'serialize' => \CRM_Core_DAO::SERIALIZE_JSON, + ], + ]; + }))->setCheckPermissions($checkPermissions); + } + + /** + * @return array + */ + public static function permissions() { + return [ + // FIXME: Perhaps use 'edit message templates' or similar? + "meta" => ["access CiviCRM"], + "default" => ["administer CiviCRM"], + ]; + } + +} diff --git a/Civi/WorkflowMessage/Examples.php b/Civi/WorkflowMessage/Examples.php new file mode 100644 index 000000000000..52ec6dae4237 --- /dev/null +++ b/Civi/WorkflowMessage/Examples.php @@ -0,0 +1,172 @@ +cache = $cache ?: \Civi::cache('short' /* long */); + $this->cacheKey = \CRM_Utils_String::munge(__CLASS__); + } + + /** + * Get a list of all examples, including basic metadata (name, title, workflow). + * + * @return array + * Ex: ['my_example' => ['title' => ..., 'workflow' => ..., 'tags' => ...]] + * @throws \ReflectionException + */ + public function findAll(): array { + $all = $this->cache->get($this->cacheKey); + if ($all === NULL) { + $all = []; + $wfClasses = Invasive::call([WorkflowMessage::class, 'getWorkflowNameClassMap']); + foreach ($wfClasses as $workflow => $class) { + try { + $classFile = (new \ReflectionClass($class))->getFileName(); + } + catch (\ReflectionException $e) { + throw new \RuntimeException("Failed to locate workflow class ($class)", 0, $e); + } + $classDir = preg_replace('/\.php$/', '', $classFile); + if (is_dir($classDir)) { + $all = array_merge($all, $this->scanDir($classDir, $workflow)); + } + } + } + return $all; + } + + /** + * @param string $dir + * @param string $workflow + * @return array + * Ex: ['my_example' => ['title' => ..., 'workflow' => ..., 'tags' => ...]] + */ + protected function scanDir($dir, $workflow) { + $all = []; + $files = (array) glob($dir . "/*.ex.php"); + foreach ($files as $file) { + $name = $workflow . '.' . preg_replace('/\.ex.php/', '', basename($file)); + $scanRecord = [ + 'name' => $name, + 'title' => $name, + 'workflow' => $workflow, + 'tags' => [], + 'file' => $file, + // ^^ relativize? + ]; + $rawRecord = $this->loadFile($file); + $all[$name] = array_merge($scanRecord, \CRM_Utils_Array::subset($rawRecord, ['name', 'title', 'workflow', 'tags'])); + } + return $all; + } + + /** + * Load an example data file (based on its file path). + * + * @param string $_exFile + * Loadable PHP filename. + * @return array + * The raw/unevaluated dataset. + */ + public function loadFile($_exFile): array { + // Isolate variables. + // If you need export values, use something like `extract($_tplVars);` + return require $_exFile; + } + + /** + * Get example data (based on its symbolic name). + * + * @param string|string[] $nameOrPath + * Ex: "foo" -> load all the data from example "foo" + * Ex: "foo.b.a.r" -> load the example "foo" and pull out the data from $foo['b']['a']['r'] + * Ex: ["foo","b","a","r"] - Same as above. But there is no ambiguity with nested dots. + * @return array + */ + public function get($nameOrPath) { + $path = is_array($nameOrPath) ? $nameOrPath : explode('.', $nameOrPath); + $exampleName = array_shift($path) . '.' . array_shift($path); + return \CRM_Utils_Array::pathGet($this->getHeavy($exampleName), $path); + } + + /** + * Get one of the "heavy" properties. + * + * @param string $name + * @return array + * @throws \ReflectionException + */ + public function getHeavy(string $name): array { + if (isset($this->heavyCache[$name])) { + return $this->heavyCache[$name]; + + } + $all = $this->findAll(); + if (!isset($all[$name])) { + throw new \RuntimeException("Cannot load example ($name)"); + } + $heavyRecord = $all[$name]; + $loaded = $this->loadFile($all[$name]['file']); + foreach (['data', 'asserts'] as $heavyField) { + if (isset($loaded[$heavyField])) { + $heavyRecord[$heavyField] = $loaded[$heavyField] instanceof \Closure + ? call_user_func($loaded[$heavyField], $this) + : $loaded[$heavyField]; + } + } + + $this->heavyCache[$name] = $heavyRecord; + return $this->heavyCache[$name]; + } + + /** + * Get an example and merge/extend it with more data. + * + * @param string|string[] $nameOrPath + * Ex: "foo" -> load all the data from example "foo" + * Ex: "foo.b.a.r" -> load the example "foo" and pull out the data from $foo['b']['a']['r'] + * Ex: ["foo","b","a","r"] - Same as above. But there is no ambiguity with nested dots. + * @param array $overrides + * Data to add. + * @return array + * The result of merging the original example with the $overrides. + */ + public function extend($nameOrPath, $overrides = []) { + $data = $this->get($nameOrPath); + \CRM_Utils_Array::extend($data, $overrides); + return $data; + } + +} diff --git a/Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php b/Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php new file mode 100644 index 000000000000..a2ccd3216782 --- /dev/null +++ b/Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php @@ -0,0 +1,79 @@ + [], + 'data' => function(\Civi\WorkflowMessage\Examples $examples) { + return [ + 'modelProps' => [ + 'contact' => [ + 'contact_id' => '100', + 'contact_type' => 'Individual', + 'contact_sub_type' => NULL, + 'sort_name' => 'D\u00edaz, Alex', + 'display_name' => 'Dr. Alex D\u00edaz', + 'do_not_email' => '1', + 'do_not_phone' => '1', + 'do_not_mail' => '0', + 'do_not_sms' => '0', + 'do_not_trade' => '0', + 'is_opt_out' => '0', + 'legal_identifier' => NULL, + 'external_identifier' => NULL, + 'nick_name' => NULL, + 'legal_name' => NULL, + 'image_URL' => NULL, + 'preferred_communication_method' => NULL, + 'preferred_language' => NULL, + 'preferred_mail_format' => 'Both', + 'first_name' => 'Alex', + 'middle_name' => '', + 'last_name' => 'D\u00edaz', + 'prefix_id' => '4', + 'suffix_id' => NULL, + 'formal_title' => NULL, + 'communication_style_id' => NULL, + 'job_title' => NULL, + 'gender_id' => '1', + 'birth_date' => '1994-04-21', + 'is_deceased' => '0', + 'deceased_date' => NULL, + 'household_name' => NULL, + 'organization_name' => NULL, + 'sic_code' => NULL, + 'contact_is_deleted' => '0', + 'current_employer' => NULL, + 'address_id' => NULL, + 'street_address' => NULL, + 'supplemental_address_1' => NULL, + 'supplemental_address_2' => NULL, + 'supplemental_address_3' => NULL, + 'city' => NULL, + 'postal_code_suffix' => NULL, + 'postal_code' => NULL, + 'geo_code_1' => NULL, + 'geo_code_2' => NULL, + 'state_province_id' => NULL, + 'country_id' => NULL, + 'phone_id' => '7', + 'phone_type_id' => '1', + 'phone' => '293-6934', + 'email_id' => '7', + 'email' => 'daz.alex67@testing.net', + 'on_hold' => '0', + 'im_id' => NULL, + 'provider_id' => NULL, + 'im' => NULL, + 'worldregion_id' => NULL, + 'world_region' => NULL, + 'languages' => NULL, + 'individual_prefix' => 'Dr.', + 'individual_suffix' => NULL, + 'communication_style' => NULL, + 'gender' => 'Female', + 'state_province_name' => NULL, + 'state_province' => NULL, + 'country' => NULL, + ], + ], + ]; + }, +]; diff --git a/tests/phpunit/api/v4/Entity/WorkflowMessageExampleTest.php b/tests/phpunit/api/v4/Entity/WorkflowMessageExampleTest.php new file mode 100644 index 000000000000..39659455d358 --- /dev/null +++ b/tests/phpunit/api/v4/Entity/WorkflowMessageExampleTest.php @@ -0,0 +1,59 @@ +getPath('[civicrm.root]/Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php'); + $workflow = 'generic'; + $name = 'generic.alex'; + + $this->assertTrue(file_exists($file), "Expect find canary file ($file)"); + + $get = \Civi\Api4\WorkflowMessageExample::get() + ->addWhere('name', '=', $name) + ->execute() + ->single(); + $this->assertEquals($workflow, $get['workflow']); + $this->assertTrue(!isset($get['data'])); + $this->assertTrue(!isset($get['asserts'])); + + $get = \Civi\Api4\WorkflowMessageExample::get() + ->addWhere('name', '=', $name) + ->addSelect('workflow', 'data') + ->execute() + ->single(); + $this->assertEquals($workflow, $get['workflow']); + $this->assertEquals(100, $get['data']['modelProps']['contact']['contact_id']); + } + +} From 4f22d37ee370018e214e9fca03fd7f7dd26d517e Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 5 Jul 2021 23:32:29 -0700 Subject: [PATCH 02/12] CaseActivity - Define class-model and unit-test for workflow message. Add base test-trait. --- CRM/Case/WorkflowMessage/CaseActivity.php | 111 ++++++++++++++++++ .../CaseActivity/adhoc_1.ex.php | 32 +++++ .../CaseActivity/class_1.ex.php | 33 ++++++ Civi/Test/WorkflowMessageTestTrait.php | 82 +++++++++++++ Civi/Token/TokenCompatSubscriber.php | 2 +- .../Case/WorkflowMessage/CaseActivityTest.php | 79 +++++++++++++ .../CRM/Core/BAO/MessageTemplateTest.php | 45 ++++--- 7 files changed, 360 insertions(+), 24 deletions(-) create mode 100644 CRM/Case/WorkflowMessage/CaseActivity.php create mode 100644 CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php create mode 100644 CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php create mode 100644 Civi/Test/WorkflowMessageTestTrait.php create mode 100644 tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php diff --git a/CRM/Case/WorkflowMessage/CaseActivity.php b/CRM/Case/WorkflowMessage/CaseActivity.php new file mode 100644 index 000000000000..59380ba3c131 --- /dev/null +++ b/CRM/Case/WorkflowMessage/CaseActivity.php @@ -0,0 +1,111 @@ + 123, 'display_name' => 'Bob Roberts', role => 'FIXME'] + * + * @var array|null + * @scope tokenContext, tplParams + * @required + */ + public $contact; + + /** + * @var int + * @scope tplParams as client_id + * @required + */ + public $clientId; + + /** + * @var string + * @scope tplParams + * @required + */ + public $activitySubject; + + /** + * @var string + * @scope tplParams + * @required + */ + public $activityTypeName; + + /** + * Unique ID for this activity. Unique and difficult to guess. + * + * @var string + * @scope tplParams + * @required + */ + public $idHash; + + /** + * @var bool + * @scope tplParams + * @required + */ + public $isCaseActivity; + + /** + * @var string + * @scope tplParams + */ + public $editActURL; + + /** + * @var string + * @scope tplParams + */ + public $viewActURL; + + /** + * @var string + * @scope tplParams + */ + public $manageCaseURL; + + /** + * List of conventional activity fields. + * + * Example: [['label' => ..., 'category' => ..., 'type' => ..., 'value' => ...]] + * + * @var array + * @scope tplParams as activity.fields + * @required + */ + public $activityFields; + + /** + * List of custom activity fields, grouped by CustomGroup. + * + * Example: ['My Custom Stuff' => [['label' => ..., 'category' => ..., 'type' => ..., 'value' => ...]]] + * + * @var array + * @scope tplParams as activity.customGroups + */ + public $activityCustomGroups = []; + +} diff --git a/CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php b/CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php new file mode 100644 index 000000000000..47c9900e4d3d --- /dev/null +++ b/CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php @@ -0,0 +1,32 @@ + ts('Case Activity (Adhoc-style example)'), + 'tags' => [], + 'data' => function (\Civi\WorkflowMessage\Examples $examples) { + $contact = $examples->extend('generic.alex.data.modelProps.contact', [ + 'role' => 'myrole', + ]); + return [ + 'tokenContext' => [ + 'contact' => $contact, + ], + 'tplParams' => [ + 'contact' => $contact, + 'isCaseActivity' => 1, + 'client_id' => 101, + 'activityTypeName' => 'Follow up', + 'activitySubject' => 'Test 123', + 'idHash' => 'abcdefg', + 'activity' => [ + 'fields' => [ + [ + 'label' => 'Case ID', + 'type' => 'String', + 'value' => '1234', + ], + ], + ], + ], + ]; + }, +]; diff --git a/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php b/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php new file mode 100644 index 000000000000..65f413ecf1b5 --- /dev/null +++ b/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php @@ -0,0 +1,33 @@ + ts('Case Activity (Class-style example)'), + 'tags' => ['phpunit', 'preview'], + 'data' => function (\Civi\WorkflowMessage\Examples $examples) { + return $examples->extend('generic.alex.data', [ + 'modelProps' => [ + 'contact' => [ + 'role' => 'myrole', + ], + 'isCaseActivity' => 1, + 'clientId' => 101, + 'activityTypeName' => 'Follow up', + 'activityFields' => [ + [ + 'label' => 'Case ID', + 'type' => 'String', + 'value' => '1234', + ], + ], + 'activitySubject' => 'Test 123', + 'activityCustomGroups' => [], + 'idHash' => 'abcdefg', + ], + ]); + }, + 'asserts' => [ + 'default' => [ + ['for' => 'subject', 'regex' => '/\[case #abcdefg\] Test 123/'], + ['for' => 'text', 'regex' => '/Your Case Role\(s\) : myrole/'], + ], + ], +]; diff --git a/Civi/Test/WorkflowMessageTestTrait.php b/Civi/Test/WorkflowMessageTestTrait.php new file mode 100644 index 000000000000..395721bedeb1 --- /dev/null +++ b/Civi/Test/WorkflowMessageTestTrait.php @@ -0,0 +1,82 @@ +getWorkflowClass(); + return $class::WORKFLOW; + } + + /** + * @return \Civi\Api4\Generic\AbstractGetAction + * @throws \API_Exception + */ + protected function findExamples(): \Civi\Api4\Generic\AbstractGetAction { + return \Civi\Api4\WorkflowMessageExample::get(0) + ->setSelect(['name', 'title', 'workflow', 'tags', 'data', 'asserts']) + ->addWhere('workflow', '=', $this->getWorkflowName()) + ->addWhere('tags', 'CONTAINS', 'phpunit'); + } + + /** + * @param array $exampleProps + * @param string $exampleName + * @throws \Civi\WorkflowMessage\Exception\WorkflowMessageException + */ + protected function assertConstructorEquivalence(array $exampleProps, $exampleName = ''): void { + $class = $this->getWorkflowClass(); + $instances = []; + $instances["factory_$exampleName"] = WorkflowMessage::create($this->getWorkflowName(), $exampleProps); + $instances["class_$exampleName"] = new $class($exampleProps); + + /** @var \Civi\WorkflowMessage\WorkflowMessageInterface $refInstance */ + /** @var \Civi\WorkflowMessage\WorkflowMessageInterface $cmpInstance */ + + $refName = $refInstance = NULL; + $comparisons = 0; + foreach ($instances as $cmpName => $cmpInstance) { + if ($refName === NULL) { + $refName = $cmpName; + $refInstance = $cmpInstance; + continue; + } + + $this->assertSameWorkflowMessage($refInstance, $cmpInstance, "Compare $refName vs $cmpName: "); + $comparisons++; + } + $this->assertEquals(1, $comparisons); + } + + /** + * @param \Civi\WorkflowMessage\WorkflowMessageInterface $refInstance + * @param \Civi\WorkflowMessage\WorkflowMessageInterface $cmpInstance + * @param string|null $prefix + */ + protected function assertSameWorkflowMessage(\Civi\WorkflowMessage\WorkflowMessageInterface $refInstance, \Civi\WorkflowMessage\WorkflowMessageInterface $cmpInstance, ?string $prefix = NULL): void { + if ($prefix === NULL) { + $prefix = sprintf('[%s] ', $this->getWorkflowName()); + } + $this->assertEquals($refInstance->export('tplParams'), $cmpInstance->export('tplParams'), "{$prefix}Should have same export(tplParams)"); + $this->assertEquals($refInstance->export('tokenContext'), $cmpInstance->export('tokenContext'), "{$prefix}should have same export(tokenContext)"); + $this->assertEquals($refInstance->export('envelope'), $cmpInstance->export('envelope'), "{$prefix}Should have same export(envelope)"); + $refExportAll = WorkflowMessage::exportAll($refInstance); + $cmpExportAll = WorkflowMessage::exportAll($cmpInstance); + $this->assertEquals($refExportAll, $cmpExportAll, "{$prefix}Should have same exportAll()"); + } + +} diff --git a/Civi/Token/TokenCompatSubscriber.php b/Civi/Token/TokenCompatSubscriber.php index 417450ef93b3..8e9abec17287 100644 --- a/Civi/Token/TokenCompatSubscriber.php +++ b/Civi/Token/TokenCompatSubscriber.php @@ -303,7 +303,7 @@ public function onRender(TokenRenderEvent $e) { $e->string = \CRM_Utils_Token::replaceDomainTokens($e->string, $domain, $isHtml, $e->message['tokens'], $useSmarty); if (!empty($e->context['contact'])) { - \CRM_Utils_Token::replaceGreetingTokens($e->string, $e->context['contact'], $e->context['contact']['contact_id'], NULL, $useSmarty); + \CRM_Utils_Token::replaceGreetingTokens($e->string, $e->context['contact'], $e->context['contact']['contact_id'] ?? $e->context['contactId'], NULL, $useSmarty); } if ($useSmarty) { diff --git a/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php new file mode 100644 index 000000000000..5a21f5be7c83 --- /dev/null +++ b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php @@ -0,0 +1,79 @@ +setSelect(['name', 'data']) + ->addWhere('name', 'IN', ['case_activity.adhoc_1', 'case_activity.class_1']) + ->execute() + ->indexBy('name') + ->column('data'); + $byAdhoc = Civi\WorkflowMessage\WorkflowMessage::create('case_activity', $examples['case_activity.adhoc_1']); + $byClass = new CRM_Case_WorkflowMessage_CaseActivity($examples['case_activity.class_1']); + $this->assertSameWorkflowMessage($byClass, $byAdhoc, 'Compare byClass and byAdhoc: '); + } + + /** + * Ensure that various methods of constructing a WorkflowMessage all produce similar results. + * + * To see this, we take all the example data and use it with diff constructors. + */ + public function testConstructorEquivalence() { + $examples = $this->findExamples()->execute()->indexBy('name')->column('data'); + $this->assertTrue(count($examples) >= 1, 'Must have at least one example data-set'); + foreach ($examples as $example) { + $this->assertConstructorEquivalence($example); + } + } + + /** + * Basic canary test fetching a specific example. + * + * @throws \API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + public function testExampleGet() { + $file = \Civi::paths()->getPath('[civicrm.root]/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php'); + $workflow = 'case_activity'; + $name = 'case_activity.class_1'; + + $this->assertTrue(file_exists($file), "Expect find canary file ($file)"); + + $get = \Civi\Api4\WorkflowMessageExample::get() + ->addWhere('name', '=', $name) + ->execute() + ->single(); + $this->assertEquals($workflow, $get['workflow']); + $this->assertTrue(!isset($get['data'])); + $this->assertTrue(!isset($get['asserts'])); + + $get = \Civi\Api4\WorkflowMessageExample::get() + ->addWhere('name', '=', $name) + ->addSelect('workflow', 'data') + ->execute() + ->single(); + $this->assertEquals($workflow, $get['workflow']); + $this->assertEquals(100, $get['data']['modelProps']['contact']['contact_id']); + $this->assertEquals('myrole', $get['data']['modelProps']['contact']['role']); + } + +} diff --git a/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php b/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php index b288a7ca98ec..f3ae76088940 100644 --- a/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php +++ b/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php @@ -195,38 +195,37 @@ public function testCaseActivityCopyTemplate():void { $client_id = $this->individualCreate(); $contact_id = $this->individualCreate(); - $tplParams = [ - 'isCaseActivity' => 1, - 'client_id' => $client_id, - // activityTypeName means label here not name, but it's ok because label is desired here (dev/core#1116-ok-label) - 'activityTypeName' => 'Follow up', - 'activity' => [ - 'fields' => [ + $msg = \Civi\WorkflowMessage\WorkflowMessage::create('case_activity', [ + 'modelProps' => [ + 'contactId' => $contact_id, + 'contact' => ['role' => 'Sand grain counter'], + 'isCaseActivity' => 1, + 'clientId' => $client_id, + // activityTypeName means label here not name, but it's ok because label is desired here (dev/core#1116-ok-label) + 'activityTypeName' => 'Follow up', + 'activityFields' => [ [ 'label' => 'Case ID', 'type' => 'String', 'value' => '1234', ], ], + 'activitySubject' => 'Test 123', + 'idHash' => substr(sha1(CIVICRM_SITE_KEY . '1234'), 0, 7), ], - 'activitySubject' => 'Test 123', - 'idHash' => substr(sha1(CIVICRM_SITE_KEY . '1234'), 0, 7), - 'contact' => ['role' => 'Sand grain counter'], - ]; + ]); - [, $subject, $message] = CRM_Core_BAO_MessageTemplate::sendTemplate( - [ - 'valueName' => 'case_activity', - 'contactId' => $contact_id, - 'tplParams' => $tplParams, - 'from' => 'admin@example.com', - 'toName' => 'Demo', - 'toEmail' => 'admin@example.com', - 'attachments' => NULL, - ] - ); + $this->assertEquals([], \Civi\Test\Invasive::get([$msg, '_extras'])); + + [, $subject, $message] = $msg->sendTemplate([ + 'valueName' => 'case_activity', + 'from' => 'admin@example.com', + 'toName' => 'Demo', + 'toEmail' => 'admin@example.com', + 'attachments' => NULL, + ]); - $this->assertEquals('[case #' . $tplParams['idHash'] . '] Test 123', $subject); + $this->assertEquals('[case #' . $msg->getIdHash() . '] Test 123', $subject); $this->assertStringContainsString('Your Case Role(s) : Sand grain counter', $message); $this->assertStringContainsString('Case ID : 1234', $message); } From ebd92dafbecd02b9fa3431b403ae04fc7cf88cf6 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 13 Jul 2021 17:21:52 -0700 Subject: [PATCH 03/12] Add WorkflowMessage.get and WorkflowMessage.render APIs --- CRM/Core/Permission.php | 4 + Civi/Api4/Action/WorkflowMessage/Render.php | 168 ++++++++++++++++++ Civi/Api4/WorkflowMessage.php | 120 +++++++++++++ Civi/WorkflowMessage/WorkflowMessage.php | 35 ++++ .../api/v4/Entity/WorkflowMessageTest.php | 94 ++++++++++ 5 files changed, 421 insertions(+) create mode 100644 Civi/Api4/Action/WorkflowMessage/Render.php create mode 100644 Civi/Api4/WorkflowMessage.php create mode 100644 tests/phpunit/api/v4/Entity/WorkflowMessageTest.php diff --git a/CRM/Core/Permission.php b/CRM/Core/Permission.php index 5166b65466ae..2ad9e9977e06 100644 --- a/CRM/Core/Permission.php +++ b/CRM/Core/Permission.php @@ -841,6 +841,10 @@ public static function getCorePermissions() { $prefix . ts('administer payment processors'), ts('Add, Update, or Disable Payment Processors'), ], + 'render templates' => [ + $prefix . ts('render templates'), + ts('Render open-ended template content. (Additional constraints may apply to autoloaded records and specific notations.)'), + ], 'edit message templates' => [ $prefix . ts('edit message templates'), ], diff --git a/Civi/Api4/Action/WorkflowMessage/Render.php b/Civi/Api4/Action/WorkflowMessage/Render.php new file mode 100644 index 000000000000..86611072b9f7 --- /dev/null +++ b/Civi/Api4/Action/WorkflowMessage/Render.php @@ -0,0 +1,168 @@ +Hello {contact.first_name}!

`) + */ + protected $messageTemplate; + + /** + * @var \Civi\WorkflowMessage\WorkflowMessageInterface + */ + protected $_model; + + public function _run(\Civi\Api4\Generic\Result $result) { + $this->validateValues(); + + $r = \CRM_Core_BAO_MessageTemplate::renderTemplate([ + 'model' => $this->_model, + 'messageTemplate' => $this->getMessageTemplate(), + 'messageTemplateId' => $this->getMessageTemplateId(), + ]); + + $result[] = \CRM_Utils_Array::subset($r, ['subject', 'html', 'text']); + } + + /** + * The token-processor supports a range of context parameters. We enforce different security rules for each kind of input. + * + * Broadly, this distinguishes between a few values: + * - Autoloaded data (e.g. 'contactId', 'activityId'). We need to ensure that the specific records are visible and extant. + * - Inputted data (e.g. 'contact'). We merely ensure that these are type-correct. + * - Prohibited/extended options, e.g. 'smarty' + */ + protected function validateValues() { + $rows = [$this->getValues()]; + $e = new ValidateValuesEvent($this, $rows, new \CRM_Utils_LazyArray(function () use ($rows) { + return array_map( + function ($row) { + return ['old' => NULL, 'new' => $row]; + }, + $rows + ); + })); + $this->onValidateValues($e); + \Civi::dispatcher()->dispatch('civi.api4.validate', $e); + if (!empty($e->errors)) { + throw $e->toException(); + } + } + + protected function onValidateValues(ValidateValuesEvent $e) { + $errorWeightMap = \CRM_Core_Error_Log::getMap(); + $errorWeight = $errorWeightMap[$this->getErrorLevel()]; + + if (count($e->records) !== 1) { + throw new \CRM_Core_Exception("Expected exactly one record to validate"); + } + foreach ($e->records as $recordKey => $record) { + /** @var \Civi\WorkflowMessage\WorkflowMessageInterface $w */ + $w = $this->_model = WorkflowMessage::create($this->getWorkflow(), [ + 'modelProps' => $record, + ]); + $fields = $w->getFields(); + + $unknowns = array_diff(array_keys($record), array_keys($fields)); + foreach ($unknowns as $fieldName) { + $e->addError($recordKey, $fieldName, 'unknown_field', ts('Unknown field (%1). Templates may only be executed with supported fields.', [ + 1 => $fieldName, + ])); + } + + // Merge intrinsic validations + foreach ($w->validate() as $issue) { + if ($errorWeightMap[$issue['severity']] < $errorWeight) { + $e->addError($recordKey, $issue['fields'], $issue['name'], $issue['message']); + } + } + + // Add checks which don't fit in WFM::validate + foreach ($fields as $fieldName => $fieldSpec) { + $fieldValue = $record[$fieldName] ?? NULL; + if ($fieldSpec->getFkEntity() && !empty($fieldValue)) { + if (!empty($params['check_permissions']) && !\Civi\Api4\Utils\CoreUtil::checkAccessDelegated($fieldSpec->getFkEntity(), 'get', ['id' => $fieldValue], CRM_Core_Session::getLoggedInContactID() ?: 0)) { + $e->addError($recordKey, $fieldName, 'nonexistent_id', ts('Referenced record does not exist or is not visible (%1).', [ + 1 => $this->getWorkflow() . '::' . $fieldName, + ])); + } + } + } + } + } + + public function fields() { + return []; + // We don't currently get the name of the workflow. But if we do... + //$item = \Civi\WorkflowMessage\WorkflowMessage::create($this->workflow); + ///** @var \Civi\WorkflowMessage\FieldSpec[] $fields */ + //$fields = $item->getFields(); + //$array = []; + //foreach ($fields as $name => $field) { + // $array[$name] = $field->toArray(); + //} + //return $array; + } + +} diff --git a/Civi/Api4/WorkflowMessage.php b/Civi/Api4/WorkflowMessage.php new file mode 100644 index 000000000000..565bff795311 --- /dev/null +++ b/Civi/Api4/WorkflowMessage.php @@ -0,0 +1,120 @@ +`MessageTemplate.workflow_name`). + * The WorkflowMessage defines the _contract_ or _processing_ of the + * message, and the MessageTemplate defines the _literal prose_. The prose + * would change frequently (eg for different deployments, locales, timeframes, + * and other whims), but contract would change conservatively (eg with a + * code-update and with some attention to backward-compatibility/change-management). + * + * @searchable none + * @since 5.43 + * @package Civi\Api4 + */ +class WorkflowMessage extends Generic\AbstractEntity { + + /** + * @param bool $checkPermissions + * @return Generic\BasicGetAction + */ + public static function get($checkPermissions = TRUE) { + return (new Generic\BasicGetAction(__CLASS__, __FUNCTION__, function ($get) { + return \Civi\WorkflowMessage\WorkflowMessage::getWorkflowSpecs(); + }))->setCheckPermissions($checkPermissions); + } + + /** + * @param bool $checkPermissions + * + * @return \Civi\Api4\Action\WorkflowMessage\Render + */ + public static function render($checkPermissions = TRUE) { + return (new Action\WorkflowMessage\Render(__CLASS__, __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + + /** + * @param bool $checkPermissions + * @return Generic\BasicGetFieldsAction + */ + public static function getFields($checkPermissions = TRUE) { + return (new Generic\BasicGetFieldsAction(__CLASS__, __FUNCTION__, function() { + return [ + [ + 'name' => 'name', + 'title' => 'Name', + 'data_type' => 'String', + ], + [ + 'name' => 'group', + 'title' => 'Group', + 'data_type' => 'String', + ], + [ + 'name' => 'class', + 'title' => 'Class', + 'data_type' => 'String', + ], + [ + 'name' => 'description', + 'title' => 'Description', + 'data_type' => 'String', + ], + ]; + }))->setCheckPermissions($checkPermissions); + } + + public static function permissions() { + return [ + 'meta' => ['access CiviCRM'], + 'default' => ['administer CiviCRM'], + 'render' => [ + // nested array = OR + [ + 'edit message templates', + 'edit user-driven message templates', + 'edit system workflow message templates', + 'render templates', + ], + ], + ]; + } + + /** + * @inheritDoc + */ + public static function getInfo() { + $info = parent::getInfo(); + $info['primary_key'] = ['name']; + return $info; + } + +} diff --git a/Civi/WorkflowMessage/WorkflowMessage.php b/Civi/WorkflowMessage/WorkflowMessage.php index 3647286ef558..5ad47eaed474 100644 --- a/Civi/WorkflowMessage/WorkflowMessage.php +++ b/Civi/WorkflowMessage/WorkflowMessage.php @@ -12,6 +12,7 @@ namespace Civi\WorkflowMessage; +use Civi\Api4\Utils\ReflectionUtils; use Civi\WorkflowMessage\Exception\WorkflowMessageException; /** @@ -170,4 +171,38 @@ public static function getWorkflowNameClassMap() { return $map; } + /** + * Get general description of available workflow-messages. + * + * @return array + * Array(string $workflowName => string $className). + * Ex: ["case_activity" => ["name" => "case_activity", "group" => "msg_workflow_case"] + * @internal + */ + public static function getWorkflowSpecs() { + $compute = function() { + $keys = ['name', 'group', 'class', 'description', 'comment']; + $list = []; + foreach (self::getWorkflowNameClassMap() as $name => $class) { + $specs = [ + 'name' => $name, + 'group' => \CRM_Utils_Constant::value($class . '::GROUP'), + 'class' => $class, + ]; + $list[$name] = \CRM_Utils_Array::subset( + array_merge(ReflectionUtils::getCodeDocs(new \ReflectionClass($class)), $specs), + $keys); + } + return $list; + }; + + $cache = \Civi::cache('long'); + $cacheKey = 'WorkflowMessage-' . __FUNCTION__; + $list = $cache->get($cacheKey); + if ($list === NULL) { + $cache->set($cacheKey, $list = $compute()); + } + return $list; + } + } diff --git a/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php b/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php new file mode 100644 index 000000000000..056dc396bb54 --- /dev/null +++ b/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php @@ -0,0 +1,94 @@ +addWhere('name', 'LIKE', 'case%') + ->execute() + ->indexBy('name'); + $this->assertTrue(isset($result['case_activity'])); + } + + public function testRenderDefaultTemplate() { + $ex = \Civi\Api4\WorkflowMessageExample::get(0) + ->addWhere('name', '=', 'case_activity.class_1') + ->addSelect('data', 'workflow') + ->addChain('render', WorkflowMessage::render() + ->setWorkflow('$workflow') + ->setValues('$data.modelProps')) + ->execute() + ->single(); + $result = $ex['render'][0]; + $this->assertRegExp('/Case ID : 1234/', $result['text']); + } + + public function testRenderCustomTemplate() { + $ex = \Civi\Api4\WorkflowMessageExample::get(0) + ->addWhere('name', '=', 'case_activity.class_1') + ->addSelect('data') + ->execute() + ->single(); + $result = \Civi\Api4\WorkflowMessage::render(0) + ->setWorkflow('case_activity') + ->setValues($ex['data']['modelProps']) + ->setMessageTemplate([ + 'msg_text' => 'The role is {$contact.role}.', + ]) + ->execute() + ->single(); + $this->assertRegExp('/The role is myrole./', $result['text']); + } + + public function testRenderExamples() { + $examples = \Civi\Api4\WorkflowMessageExample::get(0) + ->addWhere('tags', 'CONTAINS', 'phpunit') + ->addSelect('name', 'workflow', 'data', 'asserts') + ->execute(); + $this->assertTrue($examples->rowCount >= 1); + foreach ($examples as $example) { + $this->assertTrue(!empty($example['data']['modelProps']), sprintf("Example (%s) is tagged phpunit. It should have modelProps.", $example['name'])); + $this->assertTrue(!empty($example['asserts']['default']), sprintf("Example (%s) is tagged phpunit. It should have assertions.", $example['name'])); + $result = \Civi\Api4\WorkflowMessage::render(0) + ->setWorkflow($example['workflow']) + ->setValues($example['data']['modelProps']) + ->execute() + ->single(); + foreach ($example['asserts']['default'] as $num => $assert) { + $msg = sprintf('Check assertion(%s) on example (%s)', $num, $example['name']); + if (isset($assert['regex'])) { + $this->assertRegExp($assert['regex'], $result[$assert['for']], $msg); + } + else { + $this->fail('Unrecognized assertion: ' . json_encode($assert)); + } + } + } + } + +} From 8f73531e89d16793044fd4530b8cac784a59f76e Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 29 Jul 2021 00:27:05 -0700 Subject: [PATCH 04/12] Add WorkflowMessage.getTemplateFields API --- .../WorkflowMessage/GetTemplateFields.php | 69 +++++++++++++++++++ Civi/Api4/WorkflowMessage.php | 9 +++ 2 files changed, 78 insertions(+) create mode 100644 Civi/Api4/Action/WorkflowMessage/GetTemplateFields.php diff --git a/Civi/Api4/Action/WorkflowMessage/GetTemplateFields.php b/Civi/Api4/Action/WorkflowMessage/GetTemplateFields.php new file mode 100644 index 000000000000..bef1735e03d5 --- /dev/null +++ b/Civi/Api4/Action/WorkflowMessage/GetTemplateFields.php @@ -0,0 +1,69 @@ +workflow); + /** @var \Civi\WorkflowMessage\FieldSpec[] $fields */ + $fields = $item->getFields(); + $array = []; + $genericExamples = [ + 'string[]' => ['example-string1', 'example-string2...'], + 'string' => 'example-string', + 'int[]' => [1, 2, 3], + 'int' => 123, + 'double[]' => [1.23, 4.56], + 'double' => 1.23, + 'array' => [], + ]; + + switch ($this->format) { + case 'metadata': + foreach ($fields as $name => $field) { + $array[$name] = $field->toArray(); + } + return $array; + + case 'example': + foreach ($fields as $name => $field) { + $array[$name] = NULL; + foreach (array_intersect(array_keys($genericExamples), $field->getType()) as $ex) { + $array[$name] = $genericExamples[$ex]; + } + } + ksort($array); + return [$array]; + + default: + throw new \RuntimeException("Unrecognized format"); + } + } + +} diff --git a/Civi/Api4/WorkflowMessage.php b/Civi/Api4/WorkflowMessage.php index 565bff795311..a206214bb09d 100644 --- a/Civi/Api4/WorkflowMessage.php +++ b/Civi/Api4/WorkflowMessage.php @@ -61,6 +61,15 @@ public static function render($checkPermissions = TRUE) { ->setCheckPermissions($checkPermissions); } + /** + * @param bool $checkPermissions + * @return Generic\BasicGetFieldsAction + */ + public static function getTemplateFields($checkPermissions = TRUE) { + return (new Action\WorkflowMessage\GetTemplateFields(__CLASS__, __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + /** * @param bool $checkPermissions * @return Generic\BasicGetFieldsAction From 8cbb82e5ee2921372705a0e33044972bc74f5e63 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 1 Sep 2021 15:24:45 -0700 Subject: [PATCH 05/12] WorkflowMessage - Track 'support' level for each message --- CRM/Case/WorkflowMessage/CaseActivity.php | 1 + Civi/Api4/WorkflowMessage.php | 10 ++++++++++ Civi/WorkflowMessage/GenericWorkflowMessage.php | 4 ++++ Civi/WorkflowMessage/WorkflowMessage.php | 2 +- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CRM/Case/WorkflowMessage/CaseActivity.php b/CRM/Case/WorkflowMessage/CaseActivity.php index 59380ba3c131..166f2a57f251 100644 --- a/CRM/Case/WorkflowMessage/CaseActivity.php +++ b/CRM/Case/WorkflowMessage/CaseActivity.php @@ -15,6 +15,7 @@ * the configuration/add-ons) additional copies may be sent. * * @see CRM_Case_BAO_Case::sendActivityCopy + * @support template-only */ class CRM_Case_WorkflowMessage_CaseActivity extends Civi\WorkflowMessage\GenericWorkflowMessage { diff --git a/Civi/Api4/WorkflowMessage.php b/Civi/Api4/WorkflowMessage.php index a206214bb09d..5369e84df1b4 100644 --- a/Civi/Api4/WorkflowMessage.php +++ b/Civi/Api4/WorkflowMessage.php @@ -97,6 +97,16 @@ public static function getFields($checkPermissions = TRUE) { 'title' => 'Description', 'data_type' => 'String', ], + [ + 'name' => 'support', + 'title' => 'Support Level', + 'options' => [ + 'experimental' => ts('Experimental: Message may change substantively with no special communication or facilitation.'), + 'template-only' => ts('Template Support: Changes affecting the content of the message-template will get active support/facilitation.'), + 'full' => ts('Full Support: All changes affecting message-templates or message-senders will get active support/facilitation.'), + ], + 'data_type' => 'String', + ], ]; }))->setCheckPermissions($checkPermissions); } diff --git a/Civi/WorkflowMessage/GenericWorkflowMessage.php b/Civi/WorkflowMessage/GenericWorkflowMessage.php index 862c3a8d42cf..0c072a485e10 100644 --- a/Civi/WorkflowMessage/GenericWorkflowMessage.php +++ b/Civi/WorkflowMessage/GenericWorkflowMessage.php @@ -24,6 +24,10 @@ * @method int|null getContactId() * @method $this setContact(array|null $contact) * @method array|null getContact() + * + * @support template-only + * GenericWorkflowMessage should aim for "full" support, but it's prudent to keep + * it flexible for the first few months. Consider updating to "full" after Dec 2021. */ class GenericWorkflowMessage implements WorkflowMessageInterface { diff --git a/Civi/WorkflowMessage/WorkflowMessage.php b/Civi/WorkflowMessage/WorkflowMessage.php index 5ad47eaed474..32593418bbdd 100644 --- a/Civi/WorkflowMessage/WorkflowMessage.php +++ b/Civi/WorkflowMessage/WorkflowMessage.php @@ -181,7 +181,7 @@ public static function getWorkflowNameClassMap() { */ public static function getWorkflowSpecs() { $compute = function() { - $keys = ['name', 'group', 'class', 'description', 'comment']; + $keys = ['name', 'group', 'class', 'description', 'comment', 'support']; $list = []; foreach (self::getWorkflowNameClassMap() as $name => $class) { $specs = [ From e43a9841d2f89565768c1d14d9df69750caf7a5c Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 1 Sep 2021 15:25:25 -0700 Subject: [PATCH 06/12] (NFC) CaseActivity Msg - Clarify `contactId` vs `clientId` --- CRM/Case/WorkflowMessage/CaseActivity.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CRM/Case/WorkflowMessage/CaseActivity.php b/CRM/Case/WorkflowMessage/CaseActivity.php index 166f2a57f251..738f57cd26c0 100644 --- a/CRM/Case/WorkflowMessage/CaseActivity.php +++ b/CRM/Case/WorkflowMessage/CaseActivity.php @@ -23,20 +23,29 @@ class CRM_Case_WorkflowMessage_CaseActivity extends Civi\WorkflowMessage\Generic const WORKFLOW = 'case_activity'; /** - * The recipient. + * The recipient of the notification. The `{contact.*}` tokens will reference this person. * * Example: ['contact_id' => 123, 'display_name' => 'Bob Roberts', role => 'FIXME'] * * @var array|null * @scope tokenContext, tplParams + * @fkEntity Contact * @required */ public $contact; /** + * The primary contact associated with this case (eg `civicrm_case_contact.contact_id`). + * + * Existing callers are inconsistent about setting this parameter. + * + * By default, CiviCRM allows one client on any given case, and this should reflect + * that contact. However, some systems may enable multiple clients per case. + * This field may not make sense in the long-term. + * * @var int * @scope tplParams as client_id - * @required + * @fkEntity Contact */ public $clientId; From ad6ea1b1e1993f316a528fbc891ef71ca718d97e Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 8 Sep 2021 21:13:05 -0700 Subject: [PATCH 07/12] (REF) WorkflowMessageExample - Set data_type=Array. Remove serialization bits. --- Civi/Api4/Action/WorkflowMessageExample/Get.php | 8 ++------ Civi/Api4/WorkflowMessageExample.php | 7 +++++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Civi/Api4/Action/WorkflowMessageExample/Get.php b/Civi/Api4/Action/WorkflowMessageExample/Get.php index c084fc706931..64cbc1f5700e 100644 --- a/Civi/Api4/Action/WorkflowMessageExample/Get.php +++ b/Civi/Api4/Action/WorkflowMessageExample/Get.php @@ -33,6 +33,7 @@ class Get extends BasicGetAction { private $_scanner; public function _run(Result $result) { + $this->_scanner = new Examples(); if ($this->select !== [] && !in_array('name', $this->select)) { $this->select[] = 'name'; } @@ -40,12 +41,7 @@ public function _run(Result $result) { } protected function getRecords() { - $this->_scanner = new Examples(); - $all = $this->_scanner->findAll(); - foreach ($all as &$example) { - $example['tags'] = !empty($example['tags']) ? \CRM_Utils_Array::implodePadded($example['tags']) : ''; - } - return $all; + return $this->_scanner->findAll(); } protected function selectArray($values) { diff --git a/Civi/Api4/WorkflowMessageExample.php b/Civi/Api4/WorkflowMessageExample.php index 6b05bc44e10d..c672e6d91c05 100644 --- a/Civi/Api4/WorkflowMessageExample.php +++ b/Civi/Api4/WorkflowMessageExample.php @@ -60,8 +60,11 @@ public static function getFields($checkPermissions = TRUE) { [ 'name' => 'tags', 'title' => 'Tags', - 'data_type' => 'String', - 'serialize' => \CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND, + 'data_type' => 'Array', + 'options' => [ + 'preview' => ts('Preview: Display as an example in the "Preview" dialog'), + 'phpunit' => ts('PHPUnit: Run basic sniff tests in PHPUnit using this example'), + ], ], [ 'name' => 'data', From bee4821d961619ea731514fd665c2b3d07ed9f20 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 9 Sep 2021 20:51:29 -0700 Subject: [PATCH 08/12] (REF) Rename WorkflowMessageExample => ExampleData --- .../Action/{WorkflowMessageExample => ExampleData}/Get.php | 2 +- Civi/Api4/{WorkflowMessageExample.php => ExampleData.php} | 4 ++-- Civi/Test/WorkflowMessageTestTrait.php | 2 +- tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php | 6 +++--- .../{WorkflowMessageExampleTest.php => ExampleDataTest.php} | 6 +++--- tests/phpunit/api/v4/Entity/WorkflowMessageTest.php | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) rename Civi/Api4/Action/{WorkflowMessageExample => ExampleData}/Get.php (97%) rename Civi/Api4/{WorkflowMessageExample.php => ExampleData.php} (94%) rename tests/phpunit/api/v4/Entity/{WorkflowMessageExampleTest.php => ExampleDataTest.php} (90%) diff --git a/Civi/Api4/Action/WorkflowMessageExample/Get.php b/Civi/Api4/Action/ExampleData/Get.php similarity index 97% rename from Civi/Api4/Action/WorkflowMessageExample/Get.php rename to Civi/Api4/Action/ExampleData/Get.php index 64cbc1f5700e..628154867584 100644 --- a/Civi/Api4/Action/WorkflowMessageExample/Get.php +++ b/Civi/Api4/Action/ExampleData/Get.php @@ -10,7 +10,7 @@ +--------------------------------------------------------------------+ */ -namespace Civi\Api4\Action\WorkflowMessageExample; +namespace Civi\Api4\Action\ExampleData; use Civi\Api4\Generic\BasicGetAction; use Civi\Api4\Generic\Result; diff --git a/Civi/Api4/WorkflowMessageExample.php b/Civi/Api4/ExampleData.php similarity index 94% rename from Civi/Api4/WorkflowMessageExample.php rename to Civi/Api4/ExampleData.php index c672e6d91c05..987581195b57 100644 --- a/Civi/Api4/WorkflowMessageExample.php +++ b/Civi/Api4/ExampleData.php @@ -18,14 +18,14 @@ * @since 5.43 * @package Civi\Api4 */ -class WorkflowMessageExample extends \Civi\Api4\Generic\AbstractEntity { +class ExampleData extends \Civi\Api4\Generic\AbstractEntity { /** * @param bool $checkPermissions * @return Generic\AbstractGetAction */ public static function get($checkPermissions = TRUE) { - return (new Action\WorkflowMessageExample\Get(__CLASS__, __FILE__)) + return (new Action\ExampleData\Get(__CLASS__, __FILE__)) ->setCheckPermissions($checkPermissions); } diff --git a/Civi/Test/WorkflowMessageTestTrait.php b/Civi/Test/WorkflowMessageTestTrait.php index 395721bedeb1..71f3fe2968a0 100644 --- a/Civi/Test/WorkflowMessageTestTrait.php +++ b/Civi/Test/WorkflowMessageTestTrait.php @@ -27,7 +27,7 @@ public function getWorkflowName(): string { * @throws \API_Exception */ protected function findExamples(): \Civi\Api4\Generic\AbstractGetAction { - return \Civi\Api4\WorkflowMessageExample::get(0) + return \Civi\Api4\ExampleData::get(0) ->setSelect(['name', 'title', 'workflow', 'tags', 'data', 'asserts']) ->addWhere('workflow', '=', $this->getWorkflowName()) ->addWhere('tags', 'CONTAINS', 'phpunit'); diff --git a/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php index 5a21f5be7c83..3ca0fa32f413 100644 --- a/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php +++ b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php @@ -21,7 +21,7 @@ public function getWorkflowClass(): string { } public function testAdhocClassEquiv() { - $examples = \Civi\Api4\WorkflowMessageExample::get(0) + $examples = \Civi\Api4\ExampleData::get(0) ->setSelect(['name', 'data']) ->addWhere('name', 'IN', ['case_activity.adhoc_1', 'case_activity.class_1']) ->execute() @@ -58,7 +58,7 @@ public function testExampleGet() { $this->assertTrue(file_exists($file), "Expect find canary file ($file)"); - $get = \Civi\Api4\WorkflowMessageExample::get() + $get = \Civi\Api4\ExampleData::get() ->addWhere('name', '=', $name) ->execute() ->single(); @@ -66,7 +66,7 @@ public function testExampleGet() { $this->assertTrue(!isset($get['data'])); $this->assertTrue(!isset($get['asserts'])); - $get = \Civi\Api4\WorkflowMessageExample::get() + $get = \Civi\Api4\ExampleData::get() ->addWhere('name', '=', $name) ->addSelect('workflow', 'data') ->execute() diff --git a/tests/phpunit/api/v4/Entity/WorkflowMessageExampleTest.php b/tests/phpunit/api/v4/Entity/ExampleDataTest.php similarity index 90% rename from tests/phpunit/api/v4/Entity/WorkflowMessageExampleTest.php rename to tests/phpunit/api/v4/Entity/ExampleDataTest.php index 39659455d358..540bf3311fdd 100644 --- a/tests/phpunit/api/v4/Entity/WorkflowMessageExampleTest.php +++ b/tests/phpunit/api/v4/Entity/ExampleDataTest.php @@ -24,7 +24,7 @@ /** * @group headless */ -class WorkflowMessageExampleTest extends UnitTestCase { +class ExampleDataTest extends UnitTestCase { /** * Basic canary test fetching a specific example. @@ -39,7 +39,7 @@ public function testGet() { $this->assertTrue(file_exists($file), "Expect find canary file ($file)"); - $get = \Civi\Api4\WorkflowMessageExample::get() + $get = \Civi\Api4\ExampleData::get() ->addWhere('name', '=', $name) ->execute() ->single(); @@ -47,7 +47,7 @@ public function testGet() { $this->assertTrue(!isset($get['data'])); $this->assertTrue(!isset($get['asserts'])); - $get = \Civi\Api4\WorkflowMessageExample::get() + $get = \Civi\Api4\ExampleData::get() ->addWhere('name', '=', $name) ->addSelect('workflow', 'data') ->execute() diff --git a/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php b/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php index 056dc396bb54..0707c13ddacc 100644 --- a/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php +++ b/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php @@ -36,7 +36,7 @@ public function testGet() { } public function testRenderDefaultTemplate() { - $ex = \Civi\Api4\WorkflowMessageExample::get(0) + $ex = \Civi\Api4\ExampleData::get(0) ->addWhere('name', '=', 'case_activity.class_1') ->addSelect('data', 'workflow') ->addChain('render', WorkflowMessage::render() @@ -49,7 +49,7 @@ public function testRenderDefaultTemplate() { } public function testRenderCustomTemplate() { - $ex = \Civi\Api4\WorkflowMessageExample::get(0) + $ex = \Civi\Api4\ExampleData::get(0) ->addWhere('name', '=', 'case_activity.class_1') ->addSelect('data') ->execute() @@ -66,7 +66,7 @@ public function testRenderCustomTemplate() { } public function testRenderExamples() { - $examples = \Civi\Api4\WorkflowMessageExample::get(0) + $examples = \Civi\Api4\ExampleData::get(0) ->addWhere('tags', 'CONTAINS', 'phpunit') ->addSelect('name', 'workflow', 'data', 'asserts') ->execute(); From a37f134c0039de2fc076d570010e14cddadd3043 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 9 Sep 2021 21:17:32 -0700 Subject: [PATCH 09/12] ExampleData - Declare the PK field to be 'name'. Add 'type=>Extra' for heavy fields. --- Civi/Api4/ExampleData.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Civi/Api4/ExampleData.php b/Civi/Api4/ExampleData.php index 987581195b57..c53f430b1b9b 100644 --- a/Civi/Api4/ExampleData.php +++ b/Civi/Api4/ExampleData.php @@ -67,12 +67,14 @@ public static function getFields($checkPermissions = TRUE) { ], ], [ + 'type' => 'Extra', 'name' => 'data', 'title' => 'Example data', 'data_type' => 'String', 'serialize' => \CRM_Core_DAO::SERIALIZE_JSON, ], [ + 'type' => 'Extra', 'name' => 'asserts', 'title' => 'Test assertions', 'data_type' => 'String', @@ -93,4 +95,13 @@ public static function permissions() { ]; } + /** + * @inheritDoc + */ + public static function getInfo() { + $info = parent::getInfo(); + $info['primary_key'] = ['name']; + return $info; + } + } From 2cc4b0c077cc269b84fe21759473056118b7bab2 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 13 Sep 2021 17:05:24 -0700 Subject: [PATCH 10/12] (REF-1) Convert Civi/WorkflowMessage/Examples.php to Civi/Test/ExampleDataLoader.php --- Civi/Api4/Action/ExampleData/Get.php | 17 +- Civi/Test.php | 23 +++ Civi/Test/ExampleDataInterface.php | 32 ++++ Civi/Test/ExampleDataLoader.php | 135 ++++++++++++++ Civi/WorkflowMessage/Examples.php | 172 ------------------ .../phpunit/api/v4/Entity/ExampleDataTest.php | 21 ++- 6 files changed, 211 insertions(+), 189 deletions(-) create mode 100644 Civi/Test/ExampleDataInterface.php create mode 100644 Civi/Test/ExampleDataLoader.php delete mode 100644 Civi/WorkflowMessage/Examples.php diff --git a/Civi/Api4/Action/ExampleData/Get.php b/Civi/Api4/Action/ExampleData/Get.php index 628154867584..9f91c82221f5 100644 --- a/Civi/Api4/Action/ExampleData/Get.php +++ b/Civi/Api4/Action/ExampleData/Get.php @@ -14,7 +14,7 @@ use Civi\Api4\Generic\BasicGetAction; use Civi\Api4\Generic\Result; -use Civi\WorkflowMessage\Examples; +use Civi\Test\ExampleDataLoader; /** * Get a list of example data-sets. @@ -27,13 +27,7 @@ */ class Get extends BasicGetAction { - /** - * @var \Civi\WorkflowMessage\Examples - */ - private $_scanner; - public function _run(Result $result) { - $this->_scanner = new Examples(); if ($this->select !== [] && !in_array('name', $this->select)) { $this->select[] = 'name'; } @@ -41,16 +35,19 @@ public function _run(Result $result) { } protected function getRecords() { - return $this->_scanner->findAll(); + return \Civi\Test::examples()->getMetas(); } protected function selectArray($values) { $result = parent::selectArray($values); - $heavyFields = array_intersect(['data', 'asserts'], $this->select ?: []); + $heavyFields = array_intersect( + explode(',', ExampleDataLoader::HEAVY_FIELDS), + $this->select ?: [] + ); if (!empty($heavyFields)) { foreach ($result as &$item) { - $heavy = $this->_scanner->getHeavy($item['name']); + $heavy = \Civi\Test::examples()->getFull($item['name']); $item = array_merge($item, \CRM_Utils_Array::subset($heavy, $heavyFields)); } } diff --git a/Civi/Test.php b/Civi/Test.php index cf9c841ab798..ff498544235d 100644 --- a/Civi/Test.php +++ b/Civi/Test.php @@ -182,6 +182,29 @@ public static function data() { return self::$singletons['data']; } + /** + * @return \Civi\Test\ExampleDataLoader + */ + public static function examples(): \Civi\Test\ExampleDataLoader { + if (!isset(self::$singletons['examples'])) { + self::$singletons['examples'] = new \Civi\Test\ExampleDataLoader(); + } + return self::$singletons['examples']; + } + + /** + * @param string $name + * Symbolic name of the data-set. + * @return array + */ + public static function example(string $name): array { + $result = static::examples()->getFull($name); + if ($result === NULL) { + throw new \CRM_Core_Exception("Failed to load example data-set: $name"); + } + return $result; + } + /** * Prepare and execute a batch of SQL statements. * diff --git a/Civi/Test/ExampleDataInterface.php b/Civi/Test/ExampleDataInterface.php new file mode 100644 index 000000000000..1389b05cdd3d --- /dev/null +++ b/Civi/Test/ExampleDataInterface.php @@ -0,0 +1,32 @@ +build($example);` + * - They are not generated by '$ex->getExamples();' + * - They are returned by `$this->getFull()` + * - They are not returned by `$this->getMeta()`. + */ + const HEAVY_FIELDS = 'data,asserts'; + + /** + * @var array|null + */ + private $metas; + + /** + * Get a list of all examples, including basic metadata (name, title, workflow). + * + * @return array + * Ex: ['my_example' => ['title' => ..., 'workflow' => ..., 'tags' => ...]] + * @throws \ReflectionException + */ + public function getMetas(): array { + if ($this->metas === NULL) { + // $cache = new \CRM_Utils_Cache_NoCache([]); + $cache = \CRM_Utils_Constant::value('CIVICRM_TEST') ? new \CRM_Utils_Cache_NoCache([]) : \Civi::cache('long'); + $cacheKey = \CRM_Utils_String::munge(__CLASS__); + $this->metas = $cache->get($cacheKey); + if ($this->metas === NULL) { + $this->metas = $this->findMetas(); + $cache->set($cacheKey, $this->metas); + } + } + return $this->metas; + } + + public function getMeta(string $name): ?array { + $all = $this->getMetas(); + return $all[$name] ?? NULL; + } + + /** + * @param string $name + * + * @return array|null + */ + public function getFull(string $name): ?array { + $example = $this->getMeta($name); + if ($example === NULL) { + return NULL; + } + + if ($example['file']) { + include_once $example['file']; + } + $obj = new $example['class'](); + $obj->build($example); + return $example; + } + + /** + * Get a list of all examples, including basic metadata (name, title, workflow). + * + * @return array + * Ex: ['my_example' => ['title' => ..., 'workflow' => ..., 'tags' => ...]] + * @throws \ReflectionException + */ + protected function findMetas(): array { + $classes = array_merge( + // This scope of search is decidedly narrow - it should probably be expanded. + $this->scanExampleClasses(\Civi::paths()->getPath('[civicrm.root]/'), 'CRM/*/WorkflowMessage', '_'), + $this->scanExampleClasses(\Civi::paths()->getPath('[civicrm.root]/'), 'Civi/*/WorkflowMessage', '\\'), + $this->scanExampleClasses(\Civi::paths()->getPath('[civicrm.root]/'), 'Civi/WorkflowMessage', '\\'), + $this->scanExampleClasses(\Civi::paths()->getPath('[civicrm.root]/tests/phpunit/'), 'CRM/*/WorkflowMessage', '_'), + $this->scanExampleClasses(\Civi::paths()->getPath('[civicrm.root]/tests/phpunit/'), 'Civi/*/WorkflowMessage', '\\') + ); + + $all = []; + foreach ($classes as $file => $class) { + require_once $file; + $obj = new $class(); + $offset = 0; + foreach ($obj->getExamples() as $example) { + $example['file'] = $file; + $example['class'] = $class; + if (!isset($example['name'])) { + $example['name'] = $example['class'] . '#' . $offset; + } + $all[$example['name']] = $example; + $offset++; + } + } + + return $all; + } + + /** + * @param $classRoot + * Ex: Civi root dir. + * @param $classDir + * Folder to search (within the parent). + * @param $classDelim + * Namespace separator, eg underscore or backslash. + * @return array + * Array(string $relativeFileName => string $className). + */ + private function scanExampleClasses($classRoot, $classDir, $classDelim): array { + $r = []; + $exDirs = (array) glob($classRoot . $classDir); + foreach ($exDirs as $exDir) { + foreach (\CRM_Utils_File::findFiles($exDir, '*.ex.php') as $file) { + $file = str_replace(DIRECTORY_SEPARATOR, '/', $file); + $file = \CRM_Utils_File::relativize($file, \CRM_Utils_File::addTrailingSlash($classRoot, '/')); + $class = str_replace('/', $classDelim, preg_replace('/\.ex\.php$/', '', $file)); + $r[$file] = $class; + } + } + return $r; + } + +} diff --git a/Civi/WorkflowMessage/Examples.php b/Civi/WorkflowMessage/Examples.php deleted file mode 100644 index 52ec6dae4237..000000000000 --- a/Civi/WorkflowMessage/Examples.php +++ /dev/null @@ -1,172 +0,0 @@ -cache = $cache ?: \Civi::cache('short' /* long */); - $this->cacheKey = \CRM_Utils_String::munge(__CLASS__); - } - - /** - * Get a list of all examples, including basic metadata (name, title, workflow). - * - * @return array - * Ex: ['my_example' => ['title' => ..., 'workflow' => ..., 'tags' => ...]] - * @throws \ReflectionException - */ - public function findAll(): array { - $all = $this->cache->get($this->cacheKey); - if ($all === NULL) { - $all = []; - $wfClasses = Invasive::call([WorkflowMessage::class, 'getWorkflowNameClassMap']); - foreach ($wfClasses as $workflow => $class) { - try { - $classFile = (new \ReflectionClass($class))->getFileName(); - } - catch (\ReflectionException $e) { - throw new \RuntimeException("Failed to locate workflow class ($class)", 0, $e); - } - $classDir = preg_replace('/\.php$/', '', $classFile); - if (is_dir($classDir)) { - $all = array_merge($all, $this->scanDir($classDir, $workflow)); - } - } - } - return $all; - } - - /** - * @param string $dir - * @param string $workflow - * @return array - * Ex: ['my_example' => ['title' => ..., 'workflow' => ..., 'tags' => ...]] - */ - protected function scanDir($dir, $workflow) { - $all = []; - $files = (array) glob($dir . "/*.ex.php"); - foreach ($files as $file) { - $name = $workflow . '.' . preg_replace('/\.ex.php/', '', basename($file)); - $scanRecord = [ - 'name' => $name, - 'title' => $name, - 'workflow' => $workflow, - 'tags' => [], - 'file' => $file, - // ^^ relativize? - ]; - $rawRecord = $this->loadFile($file); - $all[$name] = array_merge($scanRecord, \CRM_Utils_Array::subset($rawRecord, ['name', 'title', 'workflow', 'tags'])); - } - return $all; - } - - /** - * Load an example data file (based on its file path). - * - * @param string $_exFile - * Loadable PHP filename. - * @return array - * The raw/unevaluated dataset. - */ - public function loadFile($_exFile): array { - // Isolate variables. - // If you need export values, use something like `extract($_tplVars);` - return require $_exFile; - } - - /** - * Get example data (based on its symbolic name). - * - * @param string|string[] $nameOrPath - * Ex: "foo" -> load all the data from example "foo" - * Ex: "foo.b.a.r" -> load the example "foo" and pull out the data from $foo['b']['a']['r'] - * Ex: ["foo","b","a","r"] - Same as above. But there is no ambiguity with nested dots. - * @return array - */ - public function get($nameOrPath) { - $path = is_array($nameOrPath) ? $nameOrPath : explode('.', $nameOrPath); - $exampleName = array_shift($path) . '.' . array_shift($path); - return \CRM_Utils_Array::pathGet($this->getHeavy($exampleName), $path); - } - - /** - * Get one of the "heavy" properties. - * - * @param string $name - * @return array - * @throws \ReflectionException - */ - public function getHeavy(string $name): array { - if (isset($this->heavyCache[$name])) { - return $this->heavyCache[$name]; - - } - $all = $this->findAll(); - if (!isset($all[$name])) { - throw new \RuntimeException("Cannot load example ($name)"); - } - $heavyRecord = $all[$name]; - $loaded = $this->loadFile($all[$name]['file']); - foreach (['data', 'asserts'] as $heavyField) { - if (isset($loaded[$heavyField])) { - $heavyRecord[$heavyField] = $loaded[$heavyField] instanceof \Closure - ? call_user_func($loaded[$heavyField], $this) - : $loaded[$heavyField]; - } - } - - $this->heavyCache[$name] = $heavyRecord; - return $this->heavyCache[$name]; - } - - /** - * Get an example and merge/extend it with more data. - * - * @param string|string[] $nameOrPath - * Ex: "foo" -> load all the data from example "foo" - * Ex: "foo.b.a.r" -> load the example "foo" and pull out the data from $foo['b']['a']['r'] - * Ex: ["foo","b","a","r"] - Same as above. But there is no ambiguity with nested dots. - * @param array $overrides - * Data to add. - * @return array - * The result of merging the original example with the $overrides. - */ - public function extend($nameOrPath, $overrides = []) { - $data = $this->get($nameOrPath); - \CRM_Utils_Array::extend($data, $overrides); - return $data; - } - -} diff --git a/tests/phpunit/api/v4/Entity/ExampleDataTest.php b/tests/phpunit/api/v4/Entity/ExampleDataTest.php index 540bf3311fdd..88ace338ffb6 100644 --- a/tests/phpunit/api/v4/Entity/ExampleDataTest.php +++ b/tests/phpunit/api/v4/Entity/ExampleDataTest.php @@ -33,9 +33,8 @@ class ExampleDataTest extends UnitTestCase { * @throws \Civi\API\Exception\UnauthorizedException */ public function testGet() { - $file = \Civi::paths()->getPath('[civicrm.root]/Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php'); - $workflow = 'generic'; - $name = 'generic.alex'; + $file = \Civi::paths()->getPath('[civicrm.root]/Civi/WorkflowMessage/GenericWorkflowMessage/Alex.ex.php'); + $name = 'workflow/generic/alex'; $this->assertTrue(file_exists($file), "Expect find canary file ($file)"); @@ -43,16 +42,24 @@ public function testGet() { ->addWhere('name', '=', $name) ->execute() ->single(); - $this->assertEquals($workflow, $get['workflow']); - $this->assertTrue(!isset($get['data'])); - $this->assertTrue(!isset($get['asserts'])); + $this->assertEquals($name, $get['name']); + $this->assertTrue(!isset($get['data']), 'Default "get" should not return "data"'); + $this->assertTrue(!isset($get['asserts']), 'Default "get" should not return "asserts"'); + + $get = \Civi\Api4\ExampleData::get() + ->addWhere('name', 'LIKE', 'workflow/generic/%') + ->execute(); + $this->assertTrue($get->count() > 0); + foreach ($get as $gotten) { + $this->assertStringStartsWith('workflow/generic/', $gotten['name']); + } $get = \Civi\Api4\ExampleData::get() ->addWhere('name', '=', $name) ->addSelect('workflow', 'data') ->execute() ->single(); - $this->assertEquals($workflow, $get['workflow']); + $this->assertEquals($name, $get['name']); $this->assertEquals(100, $get['data']['modelProps']['contact']['contact_id']); } From 8e3b2970429e5d38428361ca298bf53713f6aeb8 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 13 Sep 2021 17:06:06 -0700 Subject: [PATCH 11/12] (REF-2) Convert WorkflowMessage examples to use ExampleData classes --- .../CaseActivity/CaseAdhocExample.ex.php | 49 ++++++++++++ .../CaseActivity/CaseModelExample.ex.php | 49 ++++++++++++ .../CaseActivity/adhoc_1.ex.php | 32 -------- .../CaseActivity/class_1.ex.php | 33 -------- Civi/Test/WorkflowMessageTestTrait.php | 4 +- .../{alex.ex.php => Alex.ex.php} | 29 +++++-- .../WorkflowMessageExample.php | 79 +++++++++++++++++++ .../Case/WorkflowMessage/CaseActivityTest.php | 17 ++-- .../phpunit/api/v4/Entity/ExampleDataTest.php | 2 +- .../api/v4/Entity/WorkflowMessageTest.php | 12 +-- 10 files changed, 217 insertions(+), 89 deletions(-) create mode 100644 CRM/Case/WorkflowMessage/CaseActivity/CaseAdhocExample.ex.php create mode 100644 CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php delete mode 100644 CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php delete mode 100644 CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php rename Civi/WorkflowMessage/GenericWorkflowMessage/{alex.ex.php => Alex.ex.php} (85%) create mode 100644 Civi/WorkflowMessage/WorkflowMessageExample.php diff --git a/CRM/Case/WorkflowMessage/CaseActivity/CaseAdhocExample.ex.php b/CRM/Case/WorkflowMessage/CaseActivity/CaseAdhocExample.ex.php new file mode 100644 index 000000000000..e05b2590ccf6 --- /dev/null +++ b/CRM/Case/WorkflowMessage/CaseActivity/CaseAdhocExample.ex.php @@ -0,0 +1,49 @@ + "workflow/{$this->wfName}/{$this->exName}", + 'title' => ts('Case Activity (Adhoc-style example)'), + 'tags' => [], + ]; + } + + /** + * @inheritDoc + */ + public function build(array &$example): void { + $alex = \Civi\Test::example('workflow/generic/Alex'); + $contact = $this->extend($alex['data']['modelProps']['contact'], [ + 'role' => 'myrole', + ]); + $example['data'] = [ + 'workflow' => $this->wfName, + 'tokenContext' => [ + 'contact' => $contact, + ], + 'tplParams' => [ + 'contact' => $contact, + 'isCaseActivity' => 1, + 'client_id' => 101, + 'activityTypeName' => 'Follow up', + 'activitySubject' => 'Test 123', + 'idHash' => 'abcdefg', + 'activity' => [ + 'fields' => [ + [ + 'label' => 'Case ID', + 'type' => 'String', + 'value' => '1234', + ], + ], + ], + ], + ]; + } + +} diff --git a/CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php b/CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php new file mode 100644 index 000000000000..b661afa0772e --- /dev/null +++ b/CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php @@ -0,0 +1,49 @@ + "workflow/{$this->wfName}/{$this->exName}", + 'title' => ts('Case Activity (Class-style example)'), + 'tags' => ['phpunit', 'preview'], + ]; + } + + /** + * @inheritDoc + */ + public function build(array &$example): void { + $alex = \Civi\Test::example('workflow/generic/Alex'); + $example['data'] = $this->extend($alex['data'], [ + 'workflow' => $this->wfName, + 'modelProps' => [ + 'contact' => [ + 'role' => 'myrole', + ], + 'isCaseActivity' => 1, + 'clientId' => 101, + 'activityTypeName' => 'Follow up', + 'activityFields' => [ + [ + 'label' => 'Case ID', + 'type' => 'String', + 'value' => '1234', + ], + ], + 'activitySubject' => 'Test 123', + 'activityCustomGroups' => [], + 'idHash' => 'abcdefg', + ], + ]); + $example['asserts'] = [ + 'default' => [ + ['for' => 'subject', 'regex' => '/\[case #abcdefg\] Test 123/'], + ['for' => 'text', 'regex' => '/Your Case Role\(s\) : myrole/'], + ], + ]; + } + +} diff --git a/CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php b/CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php deleted file mode 100644 index 47c9900e4d3d..000000000000 --- a/CRM/Case/WorkflowMessage/CaseActivity/adhoc_1.ex.php +++ /dev/null @@ -1,32 +0,0 @@ - ts('Case Activity (Adhoc-style example)'), - 'tags' => [], - 'data' => function (\Civi\WorkflowMessage\Examples $examples) { - $contact = $examples->extend('generic.alex.data.modelProps.contact', [ - 'role' => 'myrole', - ]); - return [ - 'tokenContext' => [ - 'contact' => $contact, - ], - 'tplParams' => [ - 'contact' => $contact, - 'isCaseActivity' => 1, - 'client_id' => 101, - 'activityTypeName' => 'Follow up', - 'activitySubject' => 'Test 123', - 'idHash' => 'abcdefg', - 'activity' => [ - 'fields' => [ - [ - 'label' => 'Case ID', - 'type' => 'String', - 'value' => '1234', - ], - ], - ], - ], - ]; - }, -]; diff --git a/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php b/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php deleted file mode 100644 index 65f413ecf1b5..000000000000 --- a/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php +++ /dev/null @@ -1,33 +0,0 @@ - ts('Case Activity (Class-style example)'), - 'tags' => ['phpunit', 'preview'], - 'data' => function (\Civi\WorkflowMessage\Examples $examples) { - return $examples->extend('generic.alex.data', [ - 'modelProps' => [ - 'contact' => [ - 'role' => 'myrole', - ], - 'isCaseActivity' => 1, - 'clientId' => 101, - 'activityTypeName' => 'Follow up', - 'activityFields' => [ - [ - 'label' => 'Case ID', - 'type' => 'String', - 'value' => '1234', - ], - ], - 'activitySubject' => 'Test 123', - 'activityCustomGroups' => [], - 'idHash' => 'abcdefg', - ], - ]); - }, - 'asserts' => [ - 'default' => [ - ['for' => 'subject', 'regex' => '/\[case #abcdefg\] Test 123/'], - ['for' => 'text', 'regex' => '/Your Case Role\(s\) : myrole/'], - ], - ], -]; diff --git a/Civi/Test/WorkflowMessageTestTrait.php b/Civi/Test/WorkflowMessageTestTrait.php index 71f3fe2968a0..1c4bd7780510 100644 --- a/Civi/Test/WorkflowMessageTestTrait.php +++ b/Civi/Test/WorkflowMessageTestTrait.php @@ -28,8 +28,8 @@ public function getWorkflowName(): string { */ protected function findExamples(): \Civi\Api4\Generic\AbstractGetAction { return \Civi\Api4\ExampleData::get(0) - ->setSelect(['name', 'title', 'workflow', 'tags', 'data', 'asserts']) - ->addWhere('workflow', '=', $this->getWorkflowName()) + ->setSelect(['name', 'title', 'tags', 'data', 'asserts']) + ->addWhere('name', 'LIKE', 'workflow/' . $this->getWorkflowName() . '/%') ->addWhere('tags', 'CONTAINS', 'phpunit'); } diff --git a/Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php b/Civi/WorkflowMessage/GenericWorkflowMessage/Alex.ex.php similarity index 85% rename from Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php rename to Civi/WorkflowMessage/GenericWorkflowMessage/Alex.ex.php index a2ccd3216782..5a7ba7d43762 100644 --- a/Civi/WorkflowMessage/GenericWorkflowMessage/alex.ex.php +++ b/Civi/WorkflowMessage/GenericWorkflowMessage/Alex.ex.php @@ -1,8 +1,24 @@ [], - 'data' => function(\Civi\WorkflowMessage\Examples $examples) { - return [ + +namespace Civi\WorkflowMessage\GenericWorkflowMessage; + +class Alex extends \Civi\WorkflowMessage\WorkflowMessageExample { + + /** + * @inheritDoc + */ + public function getExamples(): iterable { + yield [ + 'name' => "workflow/{$this->wfName}/{$this->exName}", + 'tags' => [], + ]; + } + + /** + * @inheritDoc + */ + public function build(array &$example): void { + $example['data'] = [ 'modelProps' => [ 'contact' => [ 'contact_id' => '100', @@ -75,5 +91,6 @@ ], ], ]; - }, -]; + } + +} diff --git a/Civi/WorkflowMessage/WorkflowMessageExample.php b/Civi/WorkflowMessage/WorkflowMessageExample.php new file mode 100644 index 000000000000..899c0a91653b --- /dev/null +++ b/Civi/WorkflowMessage/WorkflowMessageExample.php @@ -0,0 +1,79 @@ +wfClass = $m[1]; + $this->wfName = array_search($m[1], \Civi\WorkflowMessage\WorkflowMessage::getWorkflowNameClassMap()); + $this->exName = $m[2]; + } + + /** + * Get an example, merge/extend it with more data, and return the extended + * variant. + * + * @param array $base + * Baseline data to build upon. + * @param array $overrides + * Additional data to recursively add. + * + * @return array + * The result of merging the original example with the $overrides. + */ + public function extend($base, $overrides = []) { + \CRM_Utils_Array::extend($base, $overrides); + return $base; + } + +} diff --git a/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php index 3ca0fa32f413..c9ec0c824e74 100644 --- a/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php +++ b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php @@ -23,12 +23,12 @@ public function getWorkflowClass(): string { public function testAdhocClassEquiv() { $examples = \Civi\Api4\ExampleData::get(0) ->setSelect(['name', 'data']) - ->addWhere('name', 'IN', ['case_activity.adhoc_1', 'case_activity.class_1']) + ->addWhere('name', 'IN', ['workflow/case_activity/CaseAdhocExample', 'workflow/case_activity/CaseModelExample']) ->execute() ->indexBy('name') ->column('data'); - $byAdhoc = Civi\WorkflowMessage\WorkflowMessage::create('case_activity', $examples['case_activity.adhoc_1']); - $byClass = new CRM_Case_WorkflowMessage_CaseActivity($examples['case_activity.class_1']); + $byAdhoc = Civi\WorkflowMessage\WorkflowMessage::create('case_activity', $examples['workflow/case_activity/CaseAdhocExample']); + $byClass = new CRM_Case_WorkflowMessage_CaseActivity($examples['workflow/case_activity/CaseModelExample']); $this->assertSameWorkflowMessage($byClass, $byAdhoc, 'Compare byClass and byAdhoc: '); } @@ -52,9 +52,8 @@ public function testConstructorEquivalence() { * @throws \Civi\API\Exception\UnauthorizedException */ public function testExampleGet() { - $file = \Civi::paths()->getPath('[civicrm.root]/CRM/Case/WorkflowMessage/CaseActivity/class_1.ex.php'); - $workflow = 'case_activity'; - $name = 'case_activity.class_1'; + $file = \Civi::paths()->getPath('[civicrm.root]/CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php'); + $name = 'workflow/case_activity/CaseModelExample'; $this->assertTrue(file_exists($file), "Expect find canary file ($file)"); @@ -62,16 +61,16 @@ public function testExampleGet() { ->addWhere('name', '=', $name) ->execute() ->single(); - $this->assertEquals($workflow, $get['workflow']); + $this->assertEquals($name, $get['name']); $this->assertTrue(!isset($get['data'])); $this->assertTrue(!isset($get['asserts'])); $get = \Civi\Api4\ExampleData::get() ->addWhere('name', '=', $name) - ->addSelect('workflow', 'data') + ->addSelect('data') ->execute() ->single(); - $this->assertEquals($workflow, $get['workflow']); + $this->assertEquals($name, $get['name']); $this->assertEquals(100, $get['data']['modelProps']['contact']['contact_id']); $this->assertEquals('myrole', $get['data']['modelProps']['contact']['role']); } diff --git a/tests/phpunit/api/v4/Entity/ExampleDataTest.php b/tests/phpunit/api/v4/Entity/ExampleDataTest.php index 88ace338ffb6..8f71f57240fe 100644 --- a/tests/phpunit/api/v4/Entity/ExampleDataTest.php +++ b/tests/phpunit/api/v4/Entity/ExampleDataTest.php @@ -34,7 +34,7 @@ class ExampleDataTest extends UnitTestCase { */ public function testGet() { $file = \Civi::paths()->getPath('[civicrm.root]/Civi/WorkflowMessage/GenericWorkflowMessage/Alex.ex.php'); - $name = 'workflow/generic/alex'; + $name = 'workflow/generic/Alex'; $this->assertTrue(file_exists($file), "Expect find canary file ($file)"); diff --git a/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php b/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php index 0707c13ddacc..43cef7085b02 100644 --- a/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php +++ b/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php @@ -37,10 +37,10 @@ public function testGet() { public function testRenderDefaultTemplate() { $ex = \Civi\Api4\ExampleData::get(0) - ->addWhere('name', '=', 'case_activity.class_1') - ->addSelect('data', 'workflow') + ->addWhere('name', '=', 'workflow/case_activity/CaseModelExample') + ->addSelect('data') ->addChain('render', WorkflowMessage::render() - ->setWorkflow('$workflow') + ->setWorkflow('$data.workflow') ->setValues('$data.modelProps')) ->execute() ->single(); @@ -50,7 +50,7 @@ public function testRenderDefaultTemplate() { public function testRenderCustomTemplate() { $ex = \Civi\Api4\ExampleData::get(0) - ->addWhere('name', '=', 'case_activity.class_1') + ->addWhere('name', '=', 'workflow/case_activity/CaseModelExample') ->addSelect('data') ->execute() ->single(); @@ -68,14 +68,14 @@ public function testRenderCustomTemplate() { public function testRenderExamples() { $examples = \Civi\Api4\ExampleData::get(0) ->addWhere('tags', 'CONTAINS', 'phpunit') - ->addSelect('name', 'workflow', 'data', 'asserts') + ->addSelect('name', 'data', 'asserts') ->execute(); $this->assertTrue($examples->rowCount >= 1); foreach ($examples as $example) { $this->assertTrue(!empty($example['data']['modelProps']), sprintf("Example (%s) is tagged phpunit. It should have modelProps.", $example['name'])); $this->assertTrue(!empty($example['asserts']['default']), sprintf("Example (%s) is tagged phpunit. It should have assertions.", $example['name'])); $result = \Civi\Api4\WorkflowMessage::render(0) - ->setWorkflow($example['workflow']) + ->setWorkflow($example['data']['workflow']) ->setValues($example['data']['modelProps']) ->execute() ->single(); From 2548cc10e9591e8f6620d9452d674ea064887ed3 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 13 Sep 2021 23:40:51 -0700 Subject: [PATCH 12/12] CaseActivity - Temporarily move to tests/phpunit This is mostly to circumvent near-term questions on reviewing CaseActivity while still allowing it as an example-case. --- .../phpunit/CRM}/Case/WorkflowMessage/CaseActivity.php | 0 .../Case/WorkflowMessage/CaseActivity/CaseAdhocExample.ex.php | 0 .../Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php | 0 tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename {CRM => tests/phpunit/CRM}/Case/WorkflowMessage/CaseActivity.php (100%) rename {CRM => tests/phpunit/CRM}/Case/WorkflowMessage/CaseActivity/CaseAdhocExample.ex.php (100%) rename {CRM => tests/phpunit/CRM}/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php (100%) diff --git a/CRM/Case/WorkflowMessage/CaseActivity.php b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivity.php similarity index 100% rename from CRM/Case/WorkflowMessage/CaseActivity.php rename to tests/phpunit/CRM/Case/WorkflowMessage/CaseActivity.php diff --git a/CRM/Case/WorkflowMessage/CaseActivity/CaseAdhocExample.ex.php b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivity/CaseAdhocExample.ex.php similarity index 100% rename from CRM/Case/WorkflowMessage/CaseActivity/CaseAdhocExample.ex.php rename to tests/phpunit/CRM/Case/WorkflowMessage/CaseActivity/CaseAdhocExample.ex.php diff --git a/CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php similarity index 100% rename from CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php rename to tests/phpunit/CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php diff --git a/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php index c9ec0c824e74..8d29866cf640 100644 --- a/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php +++ b/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivityTest.php @@ -52,7 +52,7 @@ public function testConstructorEquivalence() { * @throws \Civi\API\Exception\UnauthorizedException */ public function testExampleGet() { - $file = \Civi::paths()->getPath('[civicrm.root]/CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php'); + $file = \Civi::paths()->getPath('[civicrm.root]/tests/phpunit/CRM/Case/WorkflowMessage/CaseActivity/CaseModelExample.ex.php'); $name = 'workflow/case_activity/CaseModelExample'; $this->assertTrue(file_exists($file), "Expect find canary file ($file)");