From 3706e67d0df6fa406b2b3e0fcfaffa3ecbec6020 Mon Sep 17 00:00:00 2001 From: Eileen McNaughton <emcnaughton@wikimedia.org> 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 | 6 ++ CRM/Contribute/Tokens.php | 76 ++++++++++++++++++- CRM/Core/SelectValues.php | 4 +- CRM/Utils/Token.php | 9 ++- .../Contribute/ActionMapping/ByTypeTest.php | 48 +++++++++++- .../CRM/Contribute/BAO/ContributionTest.php | 10 ++- 6 files changed, 138 insertions(+), 15 deletions(-) diff --git a/CRM/Contribute/BAO/Contribution.php b/CRM/Contribute/BAO/Contribution.php index c57b02e185ab..285a66582494 100644 --- a/CRM/Contribute/BAO/Contribution.php +++ b/CRM/Contribute/BAO/Contribution.php @@ -5192,6 +5192,12 @@ public static function getContributionTokenValues($id, $messageToken) { $result = civicrm_api3('Contribution', 'get', ['id' => $id]); // lab.c.o mail#46 - show labels, not values, for custom fields with option values. if (!empty($messageToken['contribution'])) { + $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]]); + } 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); diff --git a/CRM/Contribute/Tokens.php b/CRM/Contribute/Tokens.php index a8ddf76aafcb..6ff9a0c63731 100644 --- a/CRM/Contribute/Tokens.php +++ b/CRM/Contribute/Tokens.php @@ -25,6 +25,27 @@ */ 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 @@ protected function getBasicTokens(): array { 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 @@ public function __construct() { // {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 @@ public function alterActionScheduleQuery(MailingQueryEvent $e): void { 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 @@ public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) 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 @@ public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) } } + /** + * 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 327efe906908..361ed4cd6213 100644 --- a/CRM/Core/SelectValues.php +++ b/CRM/Core/SelectValues.php @@ -582,7 +582,9 @@ public static function contributionTokens() { '{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 ce10fce61cb5..8d065c4021f1 100644 --- a/CRM/Utils/Token.php +++ b/CRM/Utils/Token.php @@ -191,7 +191,7 @@ public static function token_replace($type, $var, $value, &$str, $escapeSmarty = * regular expression sutiable for using in preg_replace */ private static function tokenRegex($token_type) { - return '/(?<!\{|\\\\)\{' . $token_type . '\.([\w]+(\-[\w\s]+)?)\}(?!\})/'; + return '/(?<!\{|\\\\)\{' . $token_type . '\.([\w]+:?\w*(\-[\w\s]+)?)\}(?!\})/'; } /** @@ -1102,7 +1102,7 @@ public static function &replaceComponentTokens(&$str, $contact, $components, $es public static function getTokens($string) { $matches = []; $tokens = []; - preg_match_all('/(?<!\{|\\\\)\{(\w+\.\w+)\}(?!\})/', + preg_match_all('/(?<!\{|\\\\)\{(\w+\.\w+:?\w*)\}(?!\})/', $string, $matches, PREG_PATTERN_ORDER @@ -1558,10 +1558,13 @@ public static function getUserTokenReplacement($token, $escapeSmarty = FALSE) { protected static function _buildContributionTokens() { $key = 'contribution'; + if (self::$_tokens[$key] == NULL) { + $processor = new CRM_Contribute_Tokens(); $tokens = array_merge(CRM_Contribute_BAO_Contribution::exportableFields('All'), ['campaign' => [], '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 0fe9af14f5d3..539c6099abf4 100644 --- a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php +++ b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php @@ -237,7 +237,19 @@ public function useHelloFirstNameStatus() { $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 @@ public function testTokenRendering() { 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 17686476ac24..a9b28df165b9 100644 --- a/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php +++ b/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php @@ -1339,11 +1339,12 @@ public function testReplaceContributionTokens() { $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 = "<p>Contribution Source: {contribution.contribution_source}</p></br> <p>Contribution Invoice ID: {contribution.invoice_id}</p></br> <p>Contribution Receive Date: {contribution.receive_date}</p></br> - <p>Contribution Custom Field: {contribution.custom_{$customField['id']}}</p></br>"; + <p>Contribution Custom Field: {contribution.custom_{$customField['id']}}</p></br> + {contribution.contribution_status_id:name}"; $subjectToken = CRM_Utils_Token::getTokens($subject); $messageToken = CRM_Utils_Token::getTokens($text); @@ -1360,10 +1361,11 @@ public function testReplaceContributionTokens() { ); $this->assertEquals("Contribution Amount: € 100.00", $contributionDetails[$contactId1]['text'], "The text does not match"); - $this->assertEquals("<p>Contribution Source: ABC</p></br> + $this->assertEquals('<p>Contribution Source: ABC</p></br> <p>Contribution Invoice ID: 12345</p></br> <p>Contribution Receive Date: May 11th, 2015 12:00 AM</p></br> - <p>Contribution Custom Field: Label2</p></br>", $contributionDetails[$contactId2]['html'], "The html does not match"); + <p>Contribution Custom Field: Label2</p></br> + Completed', $contributionDetails[$contactId2]['html'], 'The html does not match'); } /**