Afform - Support repeatable relationships
authorColeman Watts <coleman@civicrm.org>
Wed, 14 Dec 2022 22:27:15 +0000 (17:27 -0500)
committerColeman Watts <coleman@civicrm.org>
Sun, 2 Apr 2023 20:39:26 +0000 (16:39 -0400)
ext/afform/core/Civi/Api4/Action/Afform/Submit.php
ext/afform/mock/tests/phpunit/api/v4/AfformRelationshipUsageTest.php

index 1e886b126aeac2bf2a481eddf2f0230c89e057b6..44dc9a5d9daf45a26125fcb5ef76fd839928a058 100644 (file)
@@ -344,52 +344,55 @@ class Submit extends AbstractProcessor {
     // Prevent processGenericEntity
     $event->stopPropagation();
     $api4 = $event->getSecureApi4();
-    $relationship = $event->records[0]['fields'] ?? [];
-    if (empty($relationship['contact_id_a']) || empty($relationship['contact_id_b']) || empty($relationship['relationship_type_id'])) {
-      return;
-    }
-    $relationshipType = RelationshipType::get(FALSE)
-      ->addWhere('id', '=', $relationship['relationship_type_id'])
-      ->execute()->single();
-    $isReciprocal = $relationshipType['label_a_b'] == $relationshipType['label_b_a'];
-    $isActive = !isset($relationship['is_active']) || !empty($relationship['is_active']);
-    // Each contact id could be multivalued (e.g. using `af-repeat`)
-    foreach ((array) $relationship['contact_id_a'] as $contact_id_a) {
-      foreach ((array) $relationship['contact_id_b'] as $contact_id_b) {
-        $params = $relationship;
-        $params['contact_id_a'] = $contact_id_a;
-        $params['contact_id_b'] = $contact_id_b;
-        // Check for existing relationships (if allowed)
-        if (!empty($event->getEntity()['actions']['update'])) {
-          $where = [
-            ['is_active', '=', $isActive],
-            ['relationship_type_id', '=', $relationship['relationship_type_id']],
-          ];
-          // Reciprocal relationship types need an extra check
-          if ($isReciprocal) {
-            $where[] = [
-              'OR', [
-                ['AND', [['contact_id_a', '=', $contact_id_a], ['contact_id_b', '=', $contact_id_b]]],
-                ['AND', [['contact_id_a', '=', $contact_id_b], ['contact_id_b', '=', $contact_id_a]]],
-              ],
+    // Iterate through multiple relationships (if using af-repeat)
+    foreach ($event->records as $relationship) {
+      $relationship = $relationship['fields'] ?? [];
+      if (empty($relationship['contact_id_a']) || empty($relationship['contact_id_b']) || empty($relationship['relationship_type_id'])) {
+        return;
+      }
+      $relationshipType = RelationshipType::get(FALSE)
+        ->addWhere('id', '=', $relationship['relationship_type_id'])
+        ->execute()->single();
+      $isReciprocal = $relationshipType['label_a_b'] == $relationshipType['label_b_a'];
+      $isActive = !isset($relationship['is_active']) || !empty($relationship['is_active']);
+      // Each contact id could be multivalued (e.g. using `af-repeat`)
+      foreach ((array) $relationship['contact_id_a'] as $contact_id_a) {
+        foreach ((array) $relationship['contact_id_b'] as $contact_id_b) {
+          $params = $relationship;
+          $params['contact_id_a'] = $contact_id_a;
+          $params['contact_id_b'] = $contact_id_b;
+          // Check for existing relationships (if allowed)
+          if (!empty($event->getEntity()['actions']['update'])) {
+            $where = [
+              ['is_active', '=', $isActive],
+              ['relationship_type_id', '=', $relationship['relationship_type_id']],
             ];
+            // Reciprocal relationship types need an extra check
+            if ($isReciprocal) {
+              $where[] = [
+                'OR', [
+                  ['AND', [['contact_id_a', '=', $contact_id_a], ['contact_id_b', '=', $contact_id_b]]],
+                  ['AND', [['contact_id_a', '=', $contact_id_b], ['contact_id_b', '=', $contact_id_a]]],
+                ],
+              ];
+            }
+            else {
+              $where[] = ['contact_id_a', '=', $contact_id_a];
+              $where[] = ['contact_id_b', '=', $contact_id_b];
+            }
+            $existing = $api4('Relationship', 'get', ['where' => $where])->first();
+            if ($existing) {
+              $params['id'] = $existing['id'];
+              unset($params['contact_id_a'], $params['contact_id_b']);
+              // If this is a flipped reciprocal relationship, also flip the permissions
+              $params['is_permission_a_b'] = $relationship['is_permission_b_a'] ?? NULL;
+              $params['is_permission_b_a'] = $relationship['is_permission_a_b'] ?? NULL;
+            }
           }
-          else {
-            $where[] = ['contact_id_a', '=', $contact_id_a];
-            $where[] = ['contact_id_b', '=', $contact_id_b];
-          }
-          $existing = $api4('Relationship', 'get', ['where' => $where])->first();
-          if ($existing) {
-            $params['id'] = $existing['id'];
-            unset($params['contact_id_a'], $params['contact_id_b']);
-            // If this is a flipped reciprocal relationship, also flip the permissions
-            $params['is_permission_a_b'] = $relationship['is_permission_b_a'] ?? NULL;
-            $params['is_permission_b_a'] = $relationship['is_permission_a_b'] ?? NULL;
-          }
+          $api4('Relationship', 'save', [
+            'records' => [$params],
+          ]);
         }
-        $api4('Relationship', 'save', [
-          'records' => [$params],
-        ]);
       }
     }
   }
index a31a7b66cf31bc298d34857242ecfbac16745cb0..bb12c0e8570580eff5d3c2e00d5c45782736c042 100644 (file)
@@ -12,7 +12,7 @@ class api_v4_AfformRelationshipUsageTest extends api_v4_AfformUsageTestCase {
   /**
    * Tests creating a relationship between multiple contacts
    */
-  public function testCreateContactsWithRelationships(): void {
+  public function testCreateContactsWithPresetRelationships(): void {
     $layout = <<<EOHTML
 <af-form ctrl="afform">
   <af-entity data="{contact_type: 'Individual', source: 'Test Rel'}" type="Contact" name="Individual1" label="Individual 1" actions="{create: true, update: true}" security="RBAC" />
@@ -53,13 +53,90 @@ EOHTML;
 
     $saved = Relationship::get(FALSE)
       ->addWhere('contact_id_b.last_name', '=', $lastName)
-      ->addSelect('contact_id_a.first_name', 'is_active')
+      ->addSelect('contact_id_a.first_name', 'is_active', 'relationship_type_id')
       ->addOrderBy('contact_id_a.first_name')
       ->execute();
 
     $this->assertCount(2, $saved);
     $this->assertEquals('Firsty2', $saved[0]['contact_id_a.first_name']);
     $this->assertEquals('Firsty3', $saved[1]['contact_id_a.first_name']);
+    $this->assertEquals(1, $saved[0]['relationship_type_id']);
+    $this->assertEquals(1, $saved[1]['relationship_type_id']);
+  }
+
+  /**
+   * Tests creating multiple relationships using af-repeat
+   */
+  public function testCreateContactsWithMultipleRelationships(): void {
+    $layout = <<<EOHTML
+<af-form ctrl="afform">
+  <af-entity data="{contact_type: 'Individual', source: 'Test Rel'}" type="Contact" name="Individual1" label="Individual 1" actions="{create: true, update: true}" security="RBAC" />
+  <af-entity security="FBAC" type="Relationship" name="Relationship1" label="Relationship 1" actions="{create: true, update: true}" data="{contact_id_b: ['Individual1'], contact_id_a: ['Org1']}" />
+  <af-entity data="{contact_type: 'Organization', source: 'Test Rel'}" type="Contact" name="Org1" label="Org" actions="{create: true, update: true}" security="RBAC" />
+  <fieldset af-fieldset="Individual1" class="af-container" af-title="Individual 1">
+    <afblock-name-individual></afblock-name-individual>
+  </fieldset>
+  <fieldset af-fieldset="Relationship1" class="af-container" af-repeat="Add" min="1">
+    <af-field name="relationship_type_id"></af-field>
+  </fieldset>
+  <fieldset af-fieldset="Org1" class="af-container" af-title="Org 1">
+    <afblock-name-organization></afblock-name-organization>
+  </fieldset>
+  <button class="af-button btn btn-primary" crm-icon="fa-check" ng-click="afform.submit()">Submit</button>
+</af-form>
+EOHTML;
+
+    $this->useValues([
+      'layout' => $layout,
+      'permission' => CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION,
+    ]);
+
+    $types = [
+      uniqid(__FUNCTION__),
+      uniqid(__FUNCTION__),
+    ];
+    $typeIds = [];
+
+    foreach ($types as $type) {
+      $typeIds[] = \Civi\Api4\RelationshipType::create(FALSE)
+        ->addValue('contact_type_a', 'Organization')
+        ->addValue('contact_type_b', 'Individual')
+        ->addValue('name_a_b', $type)
+        ->addValue('name_b_a', "$type of")
+        ->execute()->first()['id'];
+    }
+
+    $lastName = uniqid(__FUNCTION__);
+
+    $submission = [
+      'Individual1' => [
+        ['fields' => ['first_name' => 'Firsty', 'last_name' => $lastName]],
+      ],
+      'Org1' => [
+        ['fields' => ['organization_name' => "Hello $lastName"]],
+      ],
+      'Relationship1' => [
+        ['fields' => ['relationship_type_id' => $typeIds[0]]],
+        ['fields' => ['relationship_type_id' => $typeIds[1]]],
+      ],
+    ];
+
+    Civi\Api4\Afform::submit()
+      ->setName($this->formName)
+      ->setValues($submission)
+      ->execute();
+
+    $saved = Relationship::get(FALSE)
+      ->addWhere('contact_id_b.last_name', '=', $lastName)
+      ->addSelect('contact_id_a.organization_name', 'is_active', 'relationship_type_id')
+      ->addOrderBy('relationship_type_id')
+      ->execute();
+
+    $this->assertEquals("Hello $lastName", $saved[0]['contact_id_a.organization_name']);
+    $this->assertEquals($typeIds[0], $saved[0]['relationship_type_id']);
+    $this->assertEquals("Hello $lastName", $saved[1]['contact_id_a.organization_name']);
+    $this->assertEquals($typeIds[1], $saved[1]['relationship_type_id']);
+    $this->assertCount(2, $saved);
   }
 
   public function testPrefillContactsByRelationship(): void {