Skip to content

Commit

Permalink
dev/core#2650 Add support for names & labels for token pseudoconstants
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
eileenmcnaughton committed Jul 27, 2021
1 parent d38a96e commit 046a056
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 13 deletions.
6 changes: 6 additions & 0 deletions CRM/Contribute/BAO/Contribution.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
76 changes: 73 additions & 3 deletions CRM/Contribute/Tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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');
Expand All @@ -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);
}

Expand Down Expand Up @@ -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}");
}
}

Expand All @@ -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.
Expand All @@ -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;
}

}
4 changes: 3 additions & 1 deletion CRM/Core/SelectValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
9 changes: 6 additions & 3 deletions CRM/Utils/Token.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]+)?)\}(?!\})/';
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'])) {
Expand Down
48 changes: 44 additions & 4 deletions tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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]);
}
}

}
6 changes: 4 additions & 2 deletions tests/phpunit/CRM/Contribute/BAO/ContributionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1339,11 +1339,13 @@ 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);
Expand Down

0 comments on commit 046a056

Please sign in to comment.