api4 Activity: add targets & assignees as read-write virtual multi-value fields
authorNoah Miller <nm@lemnisc.us>
Thu, 25 May 2023 00:46:38 +0000 (17:46 -0700)
committerNoah Miller <nm@lemnisc.us>
Thu, 25 May 2023 00:46:38 +0000 (17:46 -0700)
Civi/Api4/Service/Spec/Provider/ActivitySpecProvider.php
tests/phpunit/CRM/Activity/Form/Task/PDFTest.php
tests/phpunit/api/v4/Entity/ActivityTest.php

index 73e9ebd9e90704417b25fb436a8324f4835655ee..5eb148517d55aeeeedf29fe27a8b3bce08684e21 100644 (file)
@@ -12,6 +12,7 @@
 
 namespace Civi\Api4\Service\Spec\Provider;
 
+use Civi\Api4\Query\Api4SelectQuery;
 use Civi\Api4\Service\Spec\FieldSpec;
 use Civi\Api4\Service\Spec\RequestSpec;
 
@@ -45,7 +46,9 @@ class ActivitySpecProvider extends \Civi\Core\Service\AutoService implements Gen
       $spec->getFieldByName('activity_type_id')
         ->setDefaultValue(NULL)
         ->setRequired($action === 'create');
+    }
 
+    if (in_array($action, ['get', 'create', 'update'], TRUE)) {
       $field = new FieldSpec('source_contact_id', 'Activity', 'Integer');
       $field->setTitle(ts('Source Contact'));
       $field->setLabel(ts('Added by'));
@@ -53,24 +56,29 @@ class ActivitySpecProvider extends \Civi\Core\Service\AutoService implements Gen
       $field->setRequired($action === 'create');
       $field->setFkEntity('Contact');
       $field->setInputType('EntityRef');
+      $field->setSqlRenderer([__CLASS__, 'renderSqlForActivityContactIds']);
       $spec->addFieldSpec($field);
 
       $field = new FieldSpec('target_contact_id', 'Activity', 'Array');
       $field->setTitle(ts('Target Contacts'));
-      $field->setLabel(ts('With Contact(s)'));
-      $field->setDescription(ts('Contact(s) involved in this activity.'));
+      $field->setLabel(ts('With Contacts'));
+      $field->setDescription(ts('Contacts involved in this activity.'));
       $field->setFkEntity('Contact');
       $field->setInputType('EntityRef');
       $field->setInputAttrs(['multiple' => TRUE]);
+      $field->setSqlRenderer([__CLASS__, 'renderSqlForActivityContactIds']);
+      $field->addOutputFormatter([__CLASS__, 'formatOutputForMultipleActivityContactIds']);
       $spec->addFieldSpec($field);
 
       $field = new FieldSpec('assignee_contact_id', 'Activity', 'Array');
       $field->setTitle(ts('Assignee Contacts'));
       $field->setLabel(ts('Assigned to'));
-      $field->setDescription(ts('Contact(s) assigned to this activity.'));
+      $field->setDescription(ts('Contacts assigned to this activity.'));
       $field->setFkEntity('Contact');
       $field->setInputType('EntityRef');
       $field->setInputAttrs(['multiple' => TRUE]);
+      $field->setSqlRenderer([__CLASS__, 'renderSqlForActivityContactIds']);
+      $field->addOutputFormatter([__CLASS__, 'formatOutputForMultipleActivityContactIds']);
       $spec->addFieldSpec($field);
     }
   }
@@ -82,4 +90,26 @@ class ActivitySpecProvider extends \Civi\Core\Service\AutoService implements Gen
     return $entity === 'Activity';
   }
 
+  public static function renderSqlForActivityContactIds(array $field, Api4SelectQuery $query): string {
+    $contactLinkTypes = [
+      'source_contact_id' => 'Activity Source',
+      'target_contact_id' => 'Activity Targets',
+      'assignee_contact_id' => 'Activity Assignees',
+    ];
+    $recordTypeId = \CRM_Core_PseudoConstant::getKey(
+        'CRM_Activity_BAO_ActivityContact',
+        'record_type_id',
+        $contactLinkTypes[$field['name']]);
+    return '(SELECT GROUP_CONCAT(`civicrm_activity_contact`.`contact_id`) '
+          . 'FROM `civicrm_activity_contact` '
+          . 'WHERE `civicrm_activity_contact`.`activity_id` = `a`.`id` '
+          . 'AND record_type_id = ' . $recordTypeId . ')';
+  }
+
+  public static function formatOutputForMultipleActivityContactIds(
+    ?string &$value, array $row, array $field
+  ): void {
+    $value = explode(',', $value ?? '');
+  }
+
 }
index 3f0056dd0abb395e6be48596efb9cec0948ca950..bebdcbf741b51c33593611fc56d0f9cc0c8d11d1 100644 (file)
@@ -23,11 +23,13 @@ class CRM_Activity_Form_Task_PDFTest extends CiviUnitTestCase {
    */
   public function testCreateDocumentBasicTokens(): void {
     CRM_Core_BAO_ConfigSetting::enableComponent('CiviCase');
-    $case = $this->createCase($this->individualCreate());
+    $sourceContactId = $this->individualCreate();
+    $case = $this->createCase($sourceContactId);
 
     $activity = $this->activityCreate([
       'campaign_id' => $this->campaignCreate(),
       'case_id' => $case->id,
+      'source_contact_id' => $sourceContactId,
     ]);
     $data = [
       ['Subject: {activity.subject}', 'Subject: Discussion on warm beer'],
@@ -44,6 +46,9 @@ class CRM_Activity_Form_Task_PDFTest extends CiviUnitTestCase {
       ['(legacy) Activity ID: {activity.activity_id}', '(legacy) Activity ID: ' . $activity['id']],
       ['Activity ID: {activity.id}', 'Activity ID: ' . $activity['id']],
       ['(APIv4 virtual field) Case ID: {activity.case_id}', '(APIv4 virtual field) Case ID: ' . $case->id],
+      ['(APIv4 virtual field) Source Contact ID: {activity.source_contact_id}', '(APIv4 virtual field) Source Contact ID: ' . $sourceContactId],
+      ['(APIv4 virtual field) Target Contact IDs: {activity.target_contact_id}', '(APIv4 virtual field) Target Contact IDs: ' . $activity['target_contact_id']],
+      ['(APIv4 virtual field) Assignee Contact IDs: {activity.assignee_contact_id}', '(APIv4 virtual field) Assignee Contact IDs: ' . $activity['assignee_contact_id']],
     ];
     $tokenProcessor = new TokenProcessor(Civi::dispatcher(), ['schema' => ['activityId']]);
 
@@ -81,6 +86,9 @@ class CRM_Activity_Form_Task_PDFTest extends CiviUnitTestCase {
       '{activity.status_id:label}' => 'Activity Status',
       '{activity.campaign_id:label}' => 'Campaign',
       '{activity.case_id}' => 'Case ID',
+      '{activity.source_contact_id}' => 'Source Contact',
+      '{activity.target_contact_id}' => 'Target Contacts',
+      '{activity.assignee_contact_id}' => 'Assignee Contacts',
     ];
   }
 
index c0a952ba3ddda5f8b7e22a425a78201caec6ff73..2c504d849bdd3174899774299ddb11024409bbe2 100644 (file)
@@ -100,4 +100,39 @@ class ActivityTest extends Api4TestBase implements TransactionalInterface {
     $this->assertEquals($expectedActivityContacts, $activityContacts, "ActivityContacts not as expected after update.");
   }
 
+  public function testActivityCreateAndGetVirtualFields() {
+    $meetingActivityTypeID = \Civi\Api4\OptionValue::get()
+      ->addSelect('value')
+      ->addWhere('option_group_id:name', '=', 'activity_type')
+      ->execute()->first()['value'];
+
+    $sourceContactId = \CRM_Core_BAO_Domain::getDomain()->contact_id;
+    $c1 = Contact::create(FALSE)->addValue('first_name', '1')->execute()->first()['id'];
+    $c2 = Contact::create(FALSE)->addValue('first_name', '2')->execute()->first()['id'];
+    $c3 = Contact::create(FALSE)->addValue('first_name', '3')->execute()->first()['id'];
+    $c4 = Contact::create(FALSE)->addValue('first_name', '4')->execute()->first()['id'];
+
+    $targetContactIds = [$c1, $c2];
+    $assigneeContactIds = [$c3, $c4];
+
+    // Test that we can write to and read from the virtual fields.
+    $activityID = Activity::create(FALSE)
+      ->setValues([
+        'target_contact_id'   => $targetContactIds,
+        'assignee_contact_id' => $assigneeContactIds,
+        'activity_type_id'    => $meetingActivityTypeID,
+        'source_contact_id'   => $sourceContactId,
+        'subject'             => 'test activity',
+      ])->execute()->first()['id'];
+
+    $activity = Activity::get(FALSE)
+      ->addSelect('id', 'source_contact_id', 'target_contact_id', 'assignee_contact_id')
+      ->addWhere('id', '=', $activityID)
+      ->execute()->first();
+
+    $this->assertEquals($sourceContactId, $activity['source_contact_id']);
+    $this->assertEquals($targetContactIds, $activity['target_contact_id']);
+    $this->assertEquals($assigneeContactIds, $activity['assignee_contact_id']);
+  }
+
 }