Afform - Fix custom field handling and add tests
authorColeman Watts <coleman@civicrm.org>
Wed, 2 Jun 2021 15:15:02 +0000 (11:15 -0400)
committerColeman Watts <coleman@civicrm.org>
Wed, 2 Jun 2021 15:52:54 +0000 (11:52 -0400)
This ensures custom fields are handled properly by Afform,
including multi-record custom field groups & their autogenerated blocks,
and contact reference fields.

CRM/Custom/Form/Group.php
Civi/Api4/Service/Spec/SpecFormatter.php
ext/afform/core/CRM/Afform/AfformScanner.php
ext/afform/core/Civi/Api4/Action/Afform/Submit.php
ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php [moved from ext/afform/mock/tests/phpunit/api/v4/AfformUsageTest.php with 91% similarity]
ext/afform/mock/tests/phpunit/api/v4/AfformCustomFieldUsageTest.php [new file with mode: 0644]
ext/afform/mock/tests/phpunit/api/v4/AfformUsageTestCase.php [new file with mode: 0644]
tests/phpunit/api/v4/Utils/CoreUtilTest.php

index 9bf4be5e6fdef9f0633580b30039a526f156ee9b..ebb516f7993977a80333708617752681cb548999 100644 (file)
@@ -318,7 +318,7 @@ class CRM_Custom_Form_Group extends CRM_Core_Form {
     // $min_multiple = $this->add('text', 'min_multiple', ts('Minimum number of multiple records'), $attributes['min_multiple'] );
     // $this->addRule('min_multiple', ts('is a numeric field') , 'numeric');
 
-    $max_multiple = $this->add('text', 'max_multiple', ts('Maximum number of multiple records'), $attributes['max_multiple']);
+    $max_multiple = $this->add('number', 'max_multiple', ts('Maximum number of multiple records'), $attributes['max_multiple']);
     $this->addRule('max_multiple', ts('is a numeric field'), 'numeric');
 
     //allow to edit settings if custom set is empty CRM-5258
index 4a4fc9838d82b17f8de230c3040f48aa3efe222a..bcff7945b1ae8d663be894b6a1e72778a57f907b 100644 (file)
@@ -248,6 +248,9 @@ class SpecFormatter {
       'Link' => 'Url',
     ];
     $inputType = $map[$inputType] ?? $inputType;
+    if ($dataTypeName === 'ContactReference') {
+      $inputType = 'EntityRef';
+    }
     if (in_array($inputType, ['Select', 'EntityRef'], TRUE) && !empty($data['serialize'])) {
       $inputAttrs['multiple'] = TRUE;
     }
index f4e4bd86340411b605156da3980ba9115997b07e..c7e30aa4d88e76e7b9d81a0735ad4cc96aa91a21 100644 (file)
@@ -164,7 +164,7 @@ class CRM_Afform_AfformScanner {
   public function addComputedFields(&$record) {
     $name = $record['name'];
     // Ex: $allPaths['viewIndividual'][0] == '/var/www/foo/afform/view-individual'].
-    $allPaths = $this->findFilePaths()[$name];
+    $allPaths = $this->findFilePaths()[$name] ?? [];
     // $activeLayoutPath = $this->findFilePath($name, self::LAYOUT_FILE);
     // $activeMetaPath = $this->findFilePath($name, self::METADATA_FILE);
     $localLayoutPath = $this->createSiteLocalPath($name, self::LAYOUT_FILE);
index b00753dffb4e24204e10068b425652eb87591c3c..17818184be837ec7a35282e2dbd0a33e004e31c6 100644 (file)
@@ -28,6 +28,16 @@ class Submit extends AbstractProcessor {
       foreach ($this->values[$entityName] ?? [] as $values) {
         // Only accept values from fields on the form
         $values['fields'] = array_intersect_key($values['fields'] ?? [], $entity['fields']);
+        // Only accept joins set on the form
+        $values['joins'] = array_intersect_key($values['joins'] ?? [], $entity['joins']);
+        foreach ($values['joins'] as $joinEntity => &$joinValues) {
+          // Enforce the limit set by join[max]
+          $joinValues = array_slice($joinValues, 0, $entity['joins'][$joinEntity]['max'] ?? NULL);
+          // Only accept values from join fields on the form
+          foreach ($joinValues as $index => $vals) {
+            $joinValues[$index] = array_intersect_key($vals, $entity['joins'][$joinEntity]['fields']);
+          }
+        }
         $entityValues[$entityName][] = $values;
       }
       // Predetermined values override submitted values
similarity index 91%
rename from ext/afform/mock/tests/phpunit/api/v4/AfformUsageTest.php
rename to ext/afform/mock/tests/phpunit/api/v4/AfformContactUsageTest.php
index fd8506608da3ec7c03f4de85a334f22206ff8e1e..45222a343d6495d44cf1f1862513558629918db5 100644 (file)
@@ -5,13 +5,7 @@
  *
  * @group headless
  */
-class api_v4_AfformUsageTest extends api_v4_AfformTestCase {
-  use \Civi\Test\Api3TestTrait;
-  use \Civi\Test\ContactTestTrait;
-
-  protected static $layouts = [];
-
-  protected $formName;
+class api_v4_AfformContactUsageTest extends api_v4_AfformUsageTestCase {
 
   public static function setUpBeforeClass(): void {
     parent::setUpBeforeClass();
@@ -63,21 +57,6 @@ EOHTML;
 EOHTML;
   }
 
-  public function setUp(): void {
-    parent::setUp();
-    Civi\Api4\Afform::revert(FALSE)
-      ->addWhere('type', '=', 'block')
-      ->execute();
-    $this->formName = 'mock' . rand(0, 100000);
-  }
-
-  public function tearDown(): void {
-    Civi\Api4\Afform::revert(FALSE)
-      ->addWhere('name', '=', $this->formName)
-      ->execute();
-    parent::tearDown();
-  }
-
   public function testAboutMeAllowed(): void {
     $this->useValues([
       'layout' => self::$layouts['aboutMe'],
@@ -312,16 +291,4 @@ EOHTML;
     $this->assertEquals($orgEmail, $contact['org.display_name']);
   }
 
-  protected function useValues($values) {
-    $defaults = [
-      'title' => 'My form',
-      'name' => $this->formName,
-    ];
-    $full = array_merge($defaults, $values);
-    Civi\Api4\Afform::create(FALSE)
-      ->setLayoutFormat('html')
-      ->setValues($full)
-      ->execute();
-  }
-
 }
diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformCustomFieldUsageTest.php b/ext/afform/mock/tests/phpunit/api/v4/AfformCustomFieldUsageTest.php
new file mode 100644 (file)
index 0000000..4249d39
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * Test case for Afform.prefill and Afform.submit.
+ *
+ * @group headless
+ */
+class api_v4_AfformCustomFieldUsageTest extends api_v4_AfformUsageTestCase {
+
+  public static function setUpBeforeClass(): void {
+    parent::setUpBeforeClass();
+
+    self::$layouts['customMulti'] = <<<EOHTML
+<af-form ctrl="afform">
+  <af-entity data="{contact_type: 'Individual'}" type="Contact" name="Individual1" label="Individual 1" actions="{create: true, update: true}" security="FBAC" />
+  <fieldset af-fieldset="Individual1">
+    <legend class="af-text">Individual 1</legend>
+    <afblock-name-individual></afblock-name-individual>
+    <div af-join="Custom_MyThings" af-repeat="Add" max="2">
+      <afjoin-custom-my-things></afjoin-custom-my-things>
+    </div>
+  </fieldset>
+  <button class="af-button btn-primary" crm-icon="fa-check" ng-click="afform.submit()">Submit</button>
+</af-form>
+EOHTML;
+  }
+
+  /**
+   * Checks that by creating a multi-record field group,
+   * Afform has automatically generated a block to go with it,
+   * which can be submitted multiple times
+   */
+  public function testMultiRecordCustomBlock(): void {
+    $customGroup = \Civi\Api4\CustomGroup::create(FALSE)
+      ->addValue('name', 'MyThings')
+      ->addValue('title', 'My Things')
+      ->addValue('style', 'Tab with table')
+      ->addValue('extends', 'Contact')
+      ->addValue('is_multiple', TRUE)
+      ->addValue('max_multiple', 2)
+      ->addChain('fields', \Civi\Api4\CustomField::save()
+        ->addDefault('custom_group_id', '$id')
+        ->setRecords([
+          ['name' => 'my_text', 'label' => 'My Text', 'data_type' => 'String', 'html_type' => 'Text'],
+          ['name' => 'my_friend', 'label' => 'My Friend', 'data_type' => 'ContactReference', 'html_type' => 'Autocomplete-Select'],
+        ])
+      )
+      ->execute();
+
+    // Creating a custom group should automatically create an afform block
+    $block = \Civi\Api4\Afform::get()
+      ->addWhere('name', '=', 'afjoinCustom_MyThings')
+      ->setLayoutFormat('shallow')
+      ->setFormatWhitespace(TRUE)
+      ->execute()->first();
+    $this->assertEquals(2, $block['repeat']);
+    $this->assertEquals('my_text', $block['layout'][0]['name']);
+    $this->assertEquals('my_friend', $block['layout'][1]['name']);
+
+    $cid1 = $this->individualCreate([], 1);
+    $cid2 = $this->individualCreate([], 2);
+
+    $this->useValues([
+      'layout' => self::$layouts['customMulti'],
+      'permission' => CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION,
+    ]);
+    $firstName = uniqid(__FUNCTION__);
+    $values = [
+      'Individual1' => [
+        [
+          'fields' => [
+            'first_name' => $firstName,
+            'last_name' => 'tester',
+          ],
+          'joins' => [
+            'Custom_MyThings' => [
+              ['my_text' => 'One', 'my_friend' => $cid1],
+              ['my_text' => 'Two', 'my_friend' => $cid2],
+              ['my_text' => 'Not allowed', 'my_friend' => $cid2],
+            ],
+          ],
+        ],
+      ],
+    ];
+    Civi\Api4\Afform::submit()
+      ->setName($this->formName)
+      ->setValues($values)
+      ->execute();
+    $contact = \Civi\Api4\Contact::get(FALSE)
+      ->addWhere('first_name', '=', $firstName)
+      ->addJoin('Custom_MyThings AS Custom_MyThings', 'LEFT', ['id', '=', 'Custom_MyThings.entity_id'])
+      ->addSelect('Custom_MyThings.my_text', 'Custom_MyThings.my_friend')
+      ->addOrderBy('Custom_MyThings.id')
+      ->execute();
+    $this->assertEquals('One', $contact[0]['Custom_MyThings.my_text']);
+    $this->assertEquals($cid1, $contact[0]['Custom_MyThings.my_friend']);
+    $this->assertEquals('Two', $contact[1]['Custom_MyThings.my_text']);
+    $this->assertEquals($cid2, $contact[1]['Custom_MyThings.my_friend']);
+    $this->assertTrue(empty($contact[2]));
+  }
+
+}
diff --git a/ext/afform/mock/tests/phpunit/api/v4/AfformUsageTestCase.php b/ext/afform/mock/tests/phpunit/api/v4/AfformUsageTestCase.php
new file mode 100644 (file)
index 0000000..9e121c5
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Test case for Afform.prefill and Afform.submit.
+ *
+ * @group headless
+ */
+abstract class api_v4_AfformUsageTestCase extends api_v4_AfformTestCase {
+  use \Civi\Test\Api3TestTrait;
+  use \Civi\Test\ContactTestTrait;
+
+  protected static $layouts = [];
+
+  protected $formName;
+
+  public function setUp(): void {
+    parent::setUp();
+    Civi\Api4\Afform::revert(FALSE)
+      ->addWhere('type', '=', 'block')
+      ->execute();
+    $this->formName = 'mock' . rand(0, 100000);
+  }
+
+  public function tearDown(): void {
+    Civi\Api4\Afform::revert(FALSE)
+      ->addWhere('name', '=', $this->formName)
+      ->execute();
+    parent::tearDown();
+  }
+
+  protected function useValues($values) {
+    $defaults = [
+      'title' => 'My form',
+      'name' => $this->formName,
+    ];
+    $full = array_merge($defaults, $values);
+    Civi\Api4\Afform::create(FALSE)
+      ->setLayoutFormat('html')
+      ->setValues($full)
+      ->execute();
+  }
+
+}
index 2f81dd15fd484997bd117717896525d05e201333..31b419d81693c9aeb12b9d0e6cfda04b78579601 100644 (file)
@@ -48,6 +48,7 @@ class CoreUtilTest extends UnitTestCase {
       ->execute()->first();
 
     $this->assertEquals('Custom_' . $multiGroup['name'], CoreUtil::getApiNameFromTableName($multiGroup['table_name']));
+    $this->assertEquals($multiGroup['table_name'], CoreUtil::getTableName('Custom_' . $multiGroup['name']));
   }
 
   public function testGetApiClass() {