dev/core#2814 Add support for preferred syntax for contact tokens
authorEileen McNaughton <emcnaughton@wikimedia.org>
Wed, 22 Sep 2021 12:00:29 +0000 (00:00 +1200)
committerEileen McNaughton <emcnaughton@wikimedia.org>
Wed, 22 Sep 2021 12:20:19 +0000 (00:20 +1200)
This maintains support for all currently advertised tokens. But it
switches the advertisement to our preferred style (matching the db field,
specifically denoting if the label is desired.) Tests for both styles are added.

This is a step towards switching to apiv4. An important difference with v4 is
that we can distinquish between 'not fetched' and 'empty' - saving us
from extra queries when it is not clear - which will in turn
allow us to switch greeting
processing over & still support the performance tweaks via
that method

Civi/Token/TokenCompatSubscriber.php
tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php

index 92e20b780680f0d3c9ad2a39cca84cf1a9a4bb96..41929112c8f75c64f5c54710605ce62ed30f5e48 100644 (file)
@@ -158,28 +158,28 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
       'display_name' => 'Display Name',
       'nick_name' => 'Nickname',
       'image_URL' => 'Image Url',
-      'preferred_communication_method' => 'Preferred Communication Method',
-      'preferred_language' => 'Preferred Language',
-      'preferred_mail_format' => 'Preferred Mail Format',
+      'preferred_communication_method:label' => 'Preferred Communication Method',
+      'preferred_language:label' => 'Preferred Language',
+      'preferred_mail_format:label' => 'Preferred Mail Format',
       'hash' => 'Contact Hash',
-      'contact_source' => 'Contact Source',
+      'source' => 'Contact Source',
       'first_name' => 'First Name',
       'middle_name' => 'Middle Name',
       'last_name' => 'Last Name',
-      'individual_prefix' => 'Individual Prefix',
-      'individual_suffix' => 'Individual Suffix',
+      'prefix_id:label' => 'Individual Prefix',
+      'suffix_id:label' => 'Individual Suffix',
       'formal_title' => 'Formal Title',
-      'communication_style' => 'Communication Style',
+      'communication_style_id:label' => 'Communication Style',
       'job_title' => 'Job Title',
-      'gender' => 'Gender ID',
+      'gender_id:label' => 'Gender ID',
       'birth_date' => 'Birth Date',
       'current_employer_id' => 'Current Employer ID',
-      'contact_is_deleted' => 'Contact is in Trash',
+      'is_deleted:label' => 'Contact is in Trash',
       'created_date' => 'Created Date',
       'modified_date' => 'Modified Date',
-      'addressee' => 'Addressee',
-      'email_greeting' => 'Email Greeting',
-      'postal_greeting' => 'Postal Greeting',
+      'addressee_display' => 'Addressee',
+      'email_greeting_display' => 'Email Greeting',
+      'postal_greeting_display' => 'Postal Greeting',
       'current_employer' => 'Current Employer',
       'location_type' => 'Location Type',
       'address_id' => 'Address ID',
@@ -216,7 +216,7 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
       'world_region' => 'World Region',
       'url' => 'Website',
       'checksum' => 'Checksum',
-      'contact_id' => 'Internal Contact ID',
+      'id' => 'Internal Contact ID',
     ];
   }
 
@@ -348,12 +348,41 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
         }
         else {
           $row->format('text/html')
-            ->tokens('contact', $token, $row->context['contact'][$token] ?? '');
+            ->tokens('contact', $token, $this->getFieldValue($row, $token));
         }
       }
     }
   }
 
+  /**
+   * Get the field value.
+   *
+   * @param \Civi\Token\TokenRow $row
+   * @param string $field
+   * @return string|int
+   */
+  protected function getFieldValue(TokenRow $row, string $field) {
+    $entityName = 'contact';
+    if (isset($this->getDeprecatedTokens()[$field])) {
+      // Check the non-deprecated location first, fall back to deprecated
+      // this is important for the greetings because - they are weird in the query object.
+      $possibilities = [$this->getDeprecatedTokens()[$field], $field];
+    }
+    else {
+      $possibilities = [$field];
+      if (in_array($field, $this->getDeprecatedTokens(), TRUE)) {
+        $possibilities[] = array_search($field, $this->getDeprecatedTokens(), TRUE);
+      }
+    }
+
+    foreach ($possibilities as $possibility) {
+      if (isset($row->context[$entityName][$possibility])) {
+        return $row->context[$entityName][$possibility];
+      }
+    }
+    return '';
+  }
+
   /**
    * Is the given field a boolean field.
    *
@@ -458,18 +487,15 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
    */
   protected function getContact(int $contactId, array $requiredFields, bool $getAll = FALSE): array {
     $returnProperties = array_fill_keys($requiredFields, 1);
-    $mappedFields = [
-      'email_greeting' => 'email_greeting_display',
-      'postal_greeting' => 'postal_greeting_display',
-      'addressee' => 'addressee_display',
-    ];
+    $mappedFields = array_flip($this->getDeprecatedTokens());
+
     if (!empty($returnProperties['checksum'])) {
       $returnProperties['hash'] = 1;
     }
 
-    foreach ($mappedFields as $tokenName => $realName) {
+    foreach ($mappedFields as $tokenName => $api3Name) {
       if (in_array($tokenName, $requiredFields, TRUE)) {
-        $returnProperties[$realName] = 1;
+        $returnProperties[$api3Name] = 1;
       }
     }
     if ($getAll) {
@@ -483,8 +509,14 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
     [$contact] = \CRM_Contact_BAO_Query::apiQuery($params, $returnProperties ?? NULL);
     //CRM-4524
     $contact = reset($contact);
-    foreach ($mappedFields as $tokenName => $realName) {
-      $contact[$tokenName] = $contact[$realName] ?? '';
+    foreach ($mappedFields as $tokenName => $apiv3Name) {
+      // it would be set already with the right value for a greeting token
+      // the query object returns the db value for email_greeting_display
+      // and a numeric value for email_greeting if you put email_greeting
+      // in the return properties.
+      if (!isset($contact[$tokenName])) {
+        $contact[$tokenName] = $contact[$apiv3Name] ?? '';
+      }
     }
 
     //update value of custom field token
@@ -562,4 +594,34 @@ class TokenCompatSubscriber implements EventSubscriberInterface {
     ];
   }
 
+  /**
+   * These tokens still work but we don't advertise them.
+   *
+   * We can remove from the following places
+   * - scheduled reminders
+   * - add to 'blocked' on pdf letter & email
+   *
+   * & then at some point start issuing warnings for them
+   * but contact tokens are pretty central so it might be
+   * a bit drawn out.
+   *
+   * @return string[]
+   *   Keys are deprecated tokens and values are their replacements.
+   */
+  protected function getDeprecatedTokens(): array {
+    return [
+      'individual_prefix' => 'prefix_id:label',
+      'individual_suffix' => 'suffix_id:label',
+      'gender' => 'gender_id:label',
+      'communication_style' => 'communication_style_id:label',
+      'preferred_communication_method' => 'preferred_communication_method:label',
+      'email_greeting' => 'email_greeting_display',
+      'postal_greeting' => 'postal_greeting_display',
+      'addressee' => 'addressee_display',
+      'contact_id' => 'id',
+      'contact_source' => 'source',
+      'contact_is_deleted' => 'is_deleted:label',
+    ];
+  }
+
 }
index ed303f27341fbef855367105c481ad6838aec3fc..a315860c3c7fed238b15f0a47bc2a750a77deed3 100644 (file)
@@ -2,6 +2,7 @@
 
 use Civi\Api4\Address;
 use Civi\Api4\Contact;
+use Civi\Api4\MessageTemplate;
 use Civi\Token\TokenProcessor;
 
 /**
@@ -110,7 +111,7 @@ class CRM_Core_BAO_MessageTemplateTest extends CiviUnitTestCase {
     CRM_Core_Transaction::create(TRUE)->run(function(CRM_Core_Transaction $tx) {
       $tx->rollback();
 
-      \Civi\Api4\MessageTemplate::update()
+      MessageTemplate::update()
         ->addWhere('workflow_name', '=', 'case_activity')
         ->addWhere('is_reserved', '=', 0)
         ->setValues([
@@ -147,7 +148,7 @@ class CRM_Core_BAO_MessageTemplateTest extends CiviUnitTestCase {
     CRM_Core_Transaction::create(TRUE)->run(function(CRM_Core_Transaction $tx) {
       $tx->rollback();
 
-      \Civi\Api4\MessageTemplate::update()
+      MessageTemplate::update()
         ->addWhere('workflow_name', '=', 'case_activity')
         ->addWhere('is_reserved', '=', 0)
         ->setValues([
@@ -327,12 +328,6 @@ London, 90210
     $advertisedTokens = CRM_Core_SelectValues::contactTokens();
     $this->assertEquals($this->getAdvertisedTokens(), $advertisedTokens);
 
-    // Compare with our token data.
-    unset($advertisedTokens['{important_stuff.favourite_emoticon}']);
-    foreach (array_keys($advertisedTokens) as $token) {
-      $this->assertArrayKeyExists(substr($token, 9, -1), $tokenData);
-    }
-
     CRM_Core_Smarty::singleton()->assign('pre_assigned_smarty', 'wee');
     // This string contains the 4 types of possible replaces just to be sure they
     // work in combination.
@@ -386,6 +381,36 @@ emo
     CRM_Utils_Time::resetTime();
   }
 
+  /**
+   * Test that old contact tokens still work, as we add new-style support.
+   */
+  public function testLegacyTokens(): void {
+    $contactID = $this->individualCreate(['gender_id' => 'Female', 'communication_style' => 1, 'preferred_communication_method' => 'Phone']);
+    $mappings = [
+      ['old' => '{contact.individual_prefix}', 'new' => '{contact.prefix_id:label}', 'output' => 'Mr.'],
+      ['old' => '{contact.individual_suffix}', 'new' => '{contact.suffix_id:label}', 'output' => 'II'],
+      ['old' => '{contact.gender}', 'new' => '{contact.gender_id:label}', 'output' => 'Female'],
+      ['old' => '{contact.communication_style}', 'new' => '{contact.communication_style_id:label}', 'output' => 'Formal'],
+      ['old' => '{contact.preferred_communication_method}', 'new' => '{contact.preferred_communication_method:label}', 'output' => 'Phone'],
+      ['old' => '{contact.contact_id}', 'new' => '{contact.id}', 'output' => $contactID],
+      ['old' => '{contact.email_greeting}', 'new' => '{contact.email_greeting_display}', 'output' => 'Dear Anthony'],
+      ['old' => '{contact.postal_greeting}', 'new' => '{contact.postal_greeting_display}', 'output' => 'Dear Anthony'],
+      ['old' => '{contact.addressee}', 'new' => '{contact.addressee_display}', 'output' => 'Mr. Anthony J. Anderson II'],
+    ];
+
+    foreach ($mappings as $mapping) {
+      foreach (['old', 'new'] as $type) {
+        $messageContent = CRM_Core_BAO_MessageTemplate::renderTemplate([
+          'contactId' => $contactID,
+          'messageTemplate' => [
+            'msg_text' => $mapping[$type],
+          ],
+        ])['text'];
+        $this->assertEquals($mapping['output'], $messageContent, 'could not resolve ' . $mapping[$type]);
+      }
+    }
+  }
+
   /**
    * Implement token values hook.
    *
@@ -485,28 +510,28 @@ emo
       '{contact.display_name}' => 'Display Name',
       '{contact.nick_name}' => 'Nickname',
       '{contact.image_URL}' => 'Image Url',
-      '{contact.preferred_communication_method}' => 'Preferred Communication Method',
-      '{contact.preferred_language}' => 'Preferred Language',
-      '{contact.preferred_mail_format}' => 'Preferred Mail Format',
+      '{contact.preferred_communication_method:label}' => 'Preferred Communication Method',
+      '{contact.preferred_language:label}' => 'Preferred Language',
+      '{contact.preferred_mail_format:label}' => 'Preferred Mail Format',
       '{contact.hash}' => 'Contact Hash',
-      '{contact.contact_source}' => 'Contact Source',
+      '{contact.source}' => 'Contact Source',
       '{contact.first_name}' => 'First Name',
       '{contact.middle_name}' => 'Middle Name',
       '{contact.last_name}' => 'Last Name',
-      '{contact.individual_prefix}' => 'Individual Prefix',
-      '{contact.individual_suffix}' => 'Individual Suffix',
+      '{contact.prefix_id:label}' => 'Individual Prefix',
+      '{contact.suffix_id:label}' => 'Individual Suffix',
       '{contact.formal_title}' => 'Formal Title',
-      '{contact.communication_style}' => 'Communication Style',
+      '{contact.communication_style_id:label}' => 'Communication Style',
       '{contact.job_title}' => 'Job Title',
-      '{contact.gender}' => 'Gender ID',
+      '{contact.gender_id:label}' => 'Gender ID',
       '{contact.birth_date}' => 'Birth Date',
       '{contact.current_employer_id}' => 'Current Employer ID',
-      '{contact.contact_is_deleted}' => 'Contact is in Trash',
+      '{contact.is_deleted:label}' => 'Contact is in Trash',
       '{contact.created_date}' => 'Created Date',
       '{contact.modified_date}' => 'Modified Date',
-      '{contact.addressee}' => 'Addressee',
-      '{contact.email_greeting}' => 'Email Greeting',
-      '{contact.postal_greeting}' => 'Postal Greeting',
+      '{contact.addressee_display}' => 'Addressee',
+      '{contact.email_greeting_display}' => 'Email Greeting',
+      '{contact.postal_greeting_display}' => 'Postal Greeting',
       '{contact.current_employer}' => 'Current Employer',
       '{contact.location_type}' => 'Location Type',
       '{contact.address_id}' => 'Address ID',
@@ -556,7 +581,7 @@ emo
       '{contact.custom_12}' => 'Yes No :: Custom Group',
       '{contact.custom_3}' => 'Test Date :: Custom Group',
       '{contact.checksum}' => 'Checksum',
-      '{contact.contact_id}' => 'Internal Contact ID',
+      '{contact.id}' => 'Internal Contact ID',
       '{important_stuff.favourite_emoticon}' => 'Best coolest emoticon',
     ];
   }