From 9b3cb77dca4db85658e62b2b1b7f2d24958a9714 Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Tue, 27 Jul 2021 19:40:13 +1200 Subject: [PATCH] dev/core#2650 Add support for names & labels for token pseudoconstants This is per https://lab.civicrm.org/dev/core/-/issues/2650 - it - ensures that the 4 existing functions that deal with tokens handle tokens for the name and label for the (only) field that has had this treatment so far -contribution_status_id Hence both CRM_Core_SelecetValues::contributionTokens and CRM_Contribute_Tokens->tokenNames are tested to ensure they both return the same keys and labels for {contribution.contribution_status_id} {contribution.contribution_status_id:name} {contribution.contribution_status_id:label} And both rendering CRM_Contribute_Tokens via scheduled reminders and using CRM_Contribute_BAO_Contribution::replaceContributionTokens are tested to ensure they render them the same. In the context of this PR no existing tokens are altered or removed & there is only addition. However, the next step would be to remove the following token from {contribution.status}. Since there is no UI availability of this token it is likely unused - but that step would entail an upgrade script to remove it from the saved scheduled reminders. With those parts in place it should be possible to reconcile the remaining tokens, lock that parity in with tests and move on to exposing the contribution tokens to message templates. It would be nice to fully remove CRM_Contribute_BAO_Contribution::replaceContributionTokens or make it a wrapper for - however, I fear that might be quite challenging due to the way it's used with group bys & some pretty intense hackery. --- CRM/Contribute/BAO/Contribution.php | 14 +++- CRM/Contribute/Tokens.php | 76 ++++++++++++++++++- CRM/Core/SelectValues.php | 4 +- CRM/Utils/Token.php | 9 ++- .../Contribute/ActionMapping/ByTypeTest.php | 52 +++++++++++-- .../CRM/Contribute/BAO/ContributionTest.php | 19 +++-- 6 files changed, 150 insertions(+), 24 deletions(-) diff --git a/CRM/Contribute/BAO/Contribution.php b/CRM/Contribute/BAO/Contribution.php index 76e6e6e7f0..a3259036da 100644 --- a/CRM/Contribute/BAO/Contribution.php +++ b/CRM/Contribute/BAO/Contribution.php @@ -5191,11 +5191,17 @@ LIMIT 1;"; } $result = civicrm_api3('Contribution', 'get', ['id' => $id]); // lab.c.o mail#46 - show labels, not values, for custom fields with option values. + foreach ($result['values'][$id] as $fieldName => $fieldValue) { + if (strpos($fieldName, 'custom_') === 0 && array_search($fieldName, $messageToken['contribution']) !== FALSE) { + $result['values'][$id][$fieldName] = CRM_Core_BAO_CustomField::displayValue($result['values'][$id][$fieldName], $fieldName); + } + } if (!empty($messageToken['contribution'])) { - foreach ($result['values'][$id] as $fieldName => $fieldValue) { - if (strpos($fieldName, 'custom_') === 0 && array_search($fieldName, $messageToken['contribution']) !== FALSE) { - $result['values'][$id][$fieldName] = CRM_Core_BAO_CustomField::displayValue($result['values'][$id][$fieldName], $fieldName); - } + $processor = new CRM_Contribute_Tokens(); + $pseudoFields = array_keys($processor->getPseudoTokens()); + foreach ($pseudoFields as $pseudoField) { + $split = explode(':', $pseudoField); + $result['values'][$id][$pseudoField] = $processor->getPseudoValue($split[0], $split[1], $result['values'][$id][$split[0]]); } } return $result; diff --git a/CRM/Contribute/Tokens.php b/CRM/Contribute/Tokens.php index a8ddf76aaf..6ff9a0c637 100644 --- a/CRM/Contribute/Tokens.php +++ b/CRM/Contribute/Tokens.php @@ -25,6 +25,27 @@ use Civi\Token\TokenRow; */ class CRM_Contribute_Tokens extends AbstractTokenSubscriber { + /** + * @return string + */ + private function getEntityName(): string { + return 'contribution'; + } + + /** + * Get the relevant bao name. + */ + public function getBAOName(): string { + return CRM_Core_DAO_AllCoreTables::getFullName(ucfirst($this->getEntityName())); + } + + /** + * Metadata about the entity fields. + * + * @var array + */ + protected $entityFieldMetadata = []; + /** * Get a list of tokens whose name and title match the DB fields. * @return array @@ -78,12 +99,31 @@ class CRM_Contribute_Tokens extends AbstractTokenSubscriber { return ['contribution_status_id' => ts('Contribution Status ID')]; } + /** + * Get pseudoTokens - it tokens that reflect the name or label of a pseudoconstant. + * + * @internal - this function will likely be made protected soon. + * + * @return array + */ + public function getPseudoTokens(): array { + $return = []; + foreach (array_keys($this->getBasicTokens()) as $fieldName) { + if (!empty($this->entityFieldMetadata[$fieldName]['pseudoconstant'])) { + $return[$fieldName . ':label'] = $this->entityFieldMetadata[$fieldName]['html']['label']; + $return[$fieldName . ':name'] = ts('Machine name') . ': ' . $this->entityFieldMetadata[$fieldName]['html']['label']; + } + } + return $return; + } + /** * Class constructor. */ public function __construct() { + $this->entityFieldMetadata = CRM_Contribute_DAO_Contribution::fields(); $tokens = CRM_Utils_Array::subset( - CRM_Utils_Array::collect('title', CRM_Contribute_DAO_Contribution::fields()), + CRM_Utils_Array::collect('title', $this->entityFieldMetadata), $this->getPassthruTokens() ); $tokens['id'] = ts('Contribution ID'); @@ -94,7 +134,7 @@ class CRM_Contribute_Tokens extends AbstractTokenSubscriber { // {contribution.contribution_status_id:label} $tokens['status'] = ts('Contribution Status'); $tokens['type'] = ts('Financial Type'); - $tokens = array_merge($tokens, CRM_Utils_Token::getCustomFieldTokens('Contribution')); + $tokens = array_merge($tokens, $this->getPseudoTokens(), CRM_Utils_Token::getCustomFieldTokens('Contribution')); parent::__construct('contribution', $tokens); } @@ -124,8 +164,12 @@ class CRM_Contribute_Tokens extends AbstractTokenSubscriber { foreach ($this->getPassthruTokens() as $token) { $e->query->select("e." . $fields[$token]['name'] . " AS contrib_{$token}"); } + foreach (array_keys($this->getPseudoTokens()) as $token) { + $split = explode(':', $token); + $e->query->select("e." . $fields[$split[0]]['name'] . " AS contrib_{$split[0]}"); + } foreach ($this->getAliasTokens() as $alias => $orig) { - $e->query->select("e." . $fields[$orig]['name'] . " AS contrib_{$alias}"); + $e->query->select('e.' . $fields[$orig]['name'] . " AS contrib_{$alias}"); } } @@ -147,6 +191,10 @@ class CRM_Contribute_Tokens extends AbstractTokenSubscriber { elseif ($cfID = \CRM_Core_BAO_CustomField::getKeyID($field)) { $row->customToken($entity, $cfID, $actionSearchResult->entity_id); } + elseif (array_key_exists($field, $this->getPseudoTokens())) { + $split = explode(':', $field); + $row->tokens($entity, $field, $this->getPseudoValue($split[0], $split[1], $actionSearchResult->{"contrib_$split[0]"} ?? NULL)); + } elseif (in_array($field, array_keys($this->getBasicTokens()))) { // For now we just ensure that the label fields do not override the // id field here. @@ -158,4 +206,26 @@ class CRM_Contribute_Tokens extends AbstractTokenSubscriber { } } + /** + * Get the value for the relevant pseudo field. + * + * @param string $realField e.g contribution_status_id + * @param string $pseudoKey e.g name + * @param int|string $fieldValue e.g 1 + * + * @return string + * Eg. 'Completed' in the example above. + * + * @internal function will likely be protected soon. + */ + public function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string { + if ($pseudoKey === 'name') { + $fieldValue = (string) CRM_Core_PseudoConstant::getName($this->getBAOName(), $realField, $fieldValue); + } + if ($pseudoKey === 'label') { + $fieldValue = (string) CRM_Core_PseudoConstant::getLabel($this->getBAOName(), $realField, $fieldValue); + } + return (string) $fieldValue; + } + } diff --git a/CRM/Core/SelectValues.php b/CRM/Core/SelectValues.php index 327efe9069..361ed4cd62 100644 --- a/CRM/Core/SelectValues.php +++ b/CRM/Core/SelectValues.php @@ -582,7 +582,9 @@ class CRM_Core_SelectValues { '{contribution.amount_level}' => ts('Amount Level'), //'{contribution.contribution_recur_id}' => ts('Contribution Recurring ID'), //'{contribution.honor_contact_id}' => ts('Honor Contact ID'), - '{contribution.contribution_status_id}' => ts('Contribution Status'), + '{contribution.contribution_status_id}' => ts('Contribution Status ID'), + '{contribution.contribution_status_id:label}' => ts('Contribution Status'), + '{contribution.contribution_status_id:name}' => ts('Machine name') . ': ' . ts('Contribution Status'), //'{contribution.honor_type_id}' => ts('Honor Type ID'), //'{contribution.address_id}' => ts('Address ID'), '{contribution.check_number}' => ts('Check Number'), diff --git a/CRM/Utils/Token.php b/CRM/Utils/Token.php index ce10fce61c..8d065c4021 100644 --- a/CRM/Utils/Token.php +++ b/CRM/Utils/Token.php @@ -191,7 +191,7 @@ class CRM_Utils_Token { * regular expression sutiable for using in preg_replace */ private static function tokenRegex($token_type) { - return '/(? [], 'financial_type' => [], 'payment_instrument' => []], - self::getCustomFieldTokens('Contribution') + self::getCustomFieldTokens('Contribution'), + $processor->getPseudoTokens() ); foreach ($tokens as $token) { if (!empty($token['name'])) { diff --git a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php index 0fe9af14f5..a2d1bc7c08 100644 --- a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php +++ b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php @@ -151,7 +151,7 @@ class CRM_Contribute_ActionMapping_ByTypeTest extends \Civi\ActionSchedule\Abstr * Create a contribution record for Alice with type "Member Dues". */ public function addAliceDues() { - $this->callAPISuccess('Contribution', 'create', [ + $this->ids['Contribution']['alice'] = $this->callAPISuccess('Contribution', 'create', [ 'contact_id' => $this->contacts['alice']['id'], 'receive_date' => date('Ymd', strtotime($this->targetDate)), 'total_amount' => '100', @@ -168,7 +168,7 @@ class CRM_Contribute_ActionMapping_ByTypeTest extends \Civi\ActionSchedule\Abstr 'soft_credit_type_id' => 3, ], ], - ]); + ])['id']; } /** @@ -237,7 +237,19 @@ class CRM_Contribute_ActionMapping_ByTypeTest extends \Civi\ActionSchedule\Abstr $this->schedule->body_text = 'Hello, {contact.first_name}. @{contribution.status} (via body_text)'; } - public function testTokenRendering() { + /** + * Test that reconciled tokens are rendered the same via multiple code paths. + * + * We expect that the list of tokens from the processor class === the selectValues function. + * - once this is verified to be true selectValues can call the processor function internally. + * + * We also expect that rendering action action schedules will do the same as the + * legacy processor function. Once this is true we can expose the listener on the + * token processor for contribution and call it internally from the legacy code. + * + * @throws \CiviCRM_API3_Exception + */ + public function testTokenRendering(): void { $this->targetDate = '20150201000107'; \CRM_Utils_Time::setTime('2015-02-01 00:00:00'); $this->addAliceDues(); @@ -248,15 +260,43 @@ class CRM_Contribute_ActionMapping_ByTypeTest extends \Civi\ActionSchedule\Abstr first name = {contact.first_name} receive_date = {contribution.receive_date} contribution status id = {contribution.contribution_status_id} - legacy style status = {contribution.status}'; + legacy style status = {contribution.status} + new style status = {contribution.contribution_status_id:name}'; $this->schedule->save(); $this->callAPISuccess('job', 'send_reminder', []); - $this->mut->checkMailLog([ + $expected = [ 'first name = Alice', 'receive_date = February 1st, 2015 12:00 AM', 'contribution status id = 1', + 'new style status = Completed', 'legacy style status = Completed', - ]); + ]; + $this->mut->checkMailLog($expected); + + $messageToken = CRM_Utils_Token::getTokens($this->schedule->body_text); + + $contributionDetails = CRM_Contribute_BAO_Contribution::replaceContributionTokens( + [$this->ids['Contribution']['alice']], + $this->schedule->body_text, + $messageToken, + $this->schedule->body_text, + $this->schedule->body_text, + $messageToken, + TRUE + ); + $expected = [ + 'receive_date = February 1st, 2015 12:00 AM', + 'new style status = Completed', + 'contribution status id = 1', + ]; + foreach ($expected as $string) { + $this->assertStringContainsString($string, $contributionDetails[$this->contacts['alice']['id']]['html']); + } + $tokens = ['contribution_status_id', 'contribution_status_id:name', 'contribution_status_id:label']; + $processor = new CRM_Contribute_Tokens(); + foreach ($tokens as $token) { + $this->assertEquals(CRM_Core_SelectValues::contributionTokens()['{contribution.' . $token . '}'], $processor->tokenNames[$token]); + } } } diff --git a/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php b/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php index 17686476ac..292d7df34e 100644 --- a/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php +++ b/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php @@ -1304,9 +1304,12 @@ WHERE eft.entity_id = %1 AND ft.to_financial_account_id <> %2"; /** * Test for replaceContributionTokens. - * This function tests whether the contribution tokens are replaced with values from contribution. + * This function tests whether the contribution tokens are replaced with + * values from contribution. + * + * @throws \CiviCRM_API3_Exception */ - public function testReplaceContributionTokens() { + public function testReplaceContributionTokens(): void { $customGroup = $this->customGroupCreate(['extends' => 'Contribution', 'title' => 'contribution stuff']); $customField = $this->customFieldOptionValueCreate($customGroup, 'myCustomField'); $contactId1 = $this->individualCreate(); @@ -1339,11 +1342,12 @@ WHERE eft.entity_id = %1 AND ft.to_financial_account_id <> %2"; $ids = [$contribution1, $contribution2]; $subject = "This is a test for contribution ID: {contribution.contribution_id}"; - $text = "Contribution Amount: {contribution.total_amount}"; + $text = 'Contribution Amount: {contribution.total_amount}'; $html = "

Contribution Source: {contribution.contribution_source}


Contribution Invoice ID: {contribution.invoice_id}


Contribution Receive Date: {contribution.receive_date}


-

Contribution Custom Field: {contribution.custom_{$customField['id']}}


"; +

Contribution Custom Field: {contribution.custom_{$customField['id']}}


+ {contribution.contribution_status_id:name}"; $subjectToken = CRM_Utils_Token::getTokens($subject); $messageToken = CRM_Utils_Token::getTokens($text); @@ -1359,11 +1363,12 @@ WHERE eft.entity_id = %1 AND ft.to_financial_account_id <> %2"; TRUE ); - $this->assertEquals("Contribution Amount: € 100.00", $contributionDetails[$contactId1]['text'], "The text does not match"); - $this->assertEquals("

Contribution Source: ABC


+ $this->assertEquals('Contribution Amount: € 100.00', $contributionDetails[$contactId1]['text'], "The text does not match"); + $this->assertEquals('

Contribution Source: ABC


Contribution Invoice ID: 12345


Contribution Receive Date: May 11th, 2015 12:00 AM


-

Contribution Custom Field: Label2


", $contributionDetails[$contactId2]['html'], "The html does not match"); +

Contribution Custom Field: Label2


+ Completed', $contributionDetails[$contactId2]['html'], 'The html does not match'); } /** -- 2.25.1