APIv4 - Enable exporting profiles with custom fields
authorColeman Watts <coleman@civicrm.org>
Fri, 17 Jun 2022 01:18:03 +0000 (21:18 -0400)
committerColeman Watts <coleman@civicrm.org>
Fri, 17 Jun 2022 16:36:16 +0000 (12:36 -0400)
Before: Impossible to export a profile with custom fields as a managed entity because of the dependence on custom ids as the fieldname.

After: Uses APIv4-style names when exporting custom fields for profiles, which makes them portable.

CRM/Core/BAO/UFField.php
CRM/Core/DAO/UFField.php
Civi/Api4/Generic/ExportAction.php
tests/phpunit/api/v4/Custom/CustomProfileFieldTest.php [new file with mode: 0644]
tests/phpunit/api/v4/Custom/ExportCustomGroupTest.php
xml/schema/Core/UFField.xml

index 0bc9d2c2e237d45f8384b522bf03eeb6ad9875ba..9818e7b231862a5377b13998a832f3dd57b1dbbe 100644 (file)
@@ -1040,6 +1040,33 @@ SELECT  id
     return CRM_Utils_Array::collect('title', $fields);
   }
 
+  /**
+   * Get pseudoconstant list for `field_name`
+   *
+   * Includes APIv4-style names for custom fields for portability.
+   *
+   * @return array
+   */
+  public static function getAvailableFieldOptions() {
+    $fields = self::getAvailableFieldsFlat();
+    $fields['formatting'] = ['title' => ts('Formatting')];
+    $options = [];
+    foreach ($fields as $fieldName => $field) {
+      $option = [
+        'id' => $fieldName,
+        'name' => $fieldName,
+        'label' => $field['title'],
+      ];
+      if (!empty($field['custom_group_id']) && !empty($field['id'])) {
+        $groupName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $field['custom_group_id']);
+        $fieldName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', $field['id']);
+        $option['name'] = "$groupName.$fieldName";
+      }
+      $options[] = $option;
+    }
+    return $options;
+  }
+
   /**
    * Determine whether the given field_name is valid.
    *
index 8f4b833061c5b692a6892de16bdde909026c41e4..ac2ddc2252cc2ead81c1c3153a324c8736bfc681 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Core/UFField.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:4d8ac7c59b6f2301cc22fe7966e8fc91)
+ * (GenCodeChecksum:350622e76d9fc3367d201bb3a9c91f18)
  */
 
 /**
@@ -297,7 +297,7 @@ class CRM_Core_DAO_UFField extends CRM_Core_DAO {
           'bao' => 'CRM_Core_BAO_UFField',
           'localizable' => 0,
           'pseudoconstant' => [
-            'callback' => 'CRM_Core_BAO_UFField::getAvailableFieldTitles',
+            'callback' => 'CRM_Core_BAO_UFField::getAvailableFieldOptions',
           ],
           'add' => '1.1',
         ],
index aed9e322a42bb27b9dbb1278bb35078612c82fbc..87cd3c5048e96befd289d59d892a091852a7bb7f 100644 (file)
@@ -81,21 +81,19 @@ class ExportAction extends AbstractAction {
     foreach ($allFields as $field) {
       // Use implicit join syntax but only if the fk entity has a `name` field
       if (!empty($field['fk_entity']) && array_key_exists('name', $this->getFieldsForExport($field['fk_entity']))) {
+        $select[] = $field['name'];
         $select[] = $field['name'] . '.name';
         $pseudofields[$field['name'] . '.name'] = $field['name'];
       }
       // Use pseudoconstant syntax if appropriate
       elseif ($this->shouldUsePseudoconstant($entityType, $field)) {
+        $select[] = $field['name'];
         $select[] = $field['name'] . ':name';
         $pseudofields[$field['name'] . ':name'] = $field['name'];
       }
       elseif (empty($field['fk_entity'])) {
         $select[] = $field['name'];
       }
-      // Needed for exporting the option group for a custom field
-      if ($entityType === 'CustomField' && ($field['fk_entity'] ?? NULL) === 'OptionGroup') {
-        $select[] = $field['name'];
-      }
     }
     $record = civicrm_api4($entityType, 'get', [
       'checkPermissions' => $this->checkPermissions,
@@ -107,13 +105,6 @@ class ExportAction extends AbstractAction {
     }
     // The get api always returns ID, but it should not be included in an export
     unset($record['id']);
-    // Null fields should not use joins/pseudoconstants
-    foreach ($pseudofields as $alias => $fieldName) {
-      if (is_null($record[$alias])) {
-        unset($record[$alias]);
-        $record[$fieldName] = NULL;
-      }
-    }
     // Should references be limited to the current domain?
     $limitRefsByDomain = $entityType === 'OptionGroup' && \CRM_Core_OptionGroup::isDomainOptionGroup($record['name']) ? \CRM_Core_BAO_Domain::getDomain()->id : FALSE;
     foreach ($allFields as $fieldName => $field) {
@@ -144,7 +135,15 @@ class ExportAction extends AbstractAction {
       ) {
         $this->exportRecord('OptionGroup', $record['option_group_id'], $result);
       }
-      unset($record['option_group_id']);
+    }
+    // Don't use joins/pseudoconstants if null or if it has the same value as the original
+    foreach ($pseudofields as $alias => $fieldName) {
+      if (!isset($record[$alias]) || $record[$alias] == ($record[$fieldName] ?? NULL)) {
+        unset($record[$alias]);
+      }
+      else {
+        unset($record[$fieldName]);
+      }
     }
     $result[] = [
       'name' => $name,
@@ -221,6 +220,10 @@ class ExportAction extends AbstractAction {
       return FALSE;
     }
     $daoName = CoreUtil::getInfoItem($entityType, 'dao');
+    // Exception for Profile.field_name
+    if ($entityType === 'UFField' && $field['name'] === 'field_name') {
+      return TRUE;
+    }
     // Options generated by a callback function tend to be stable,
     // and the :name property may not be reliable. Use plain value.
     if ($daoName && !empty($daoName::getSupportedFields()[$field['name']]['pseudoconstant']['callback'])) {
diff --git a/tests/phpunit/api/v4/Custom/CustomProfileFieldTest.php b/tests/phpunit/api/v4/Custom/CustomProfileFieldTest.php
new file mode 100644 (file)
index 0000000..ce7eaf9
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
+
+namespace api\v4\Custom;
+
+use Civi\Api4\CustomGroup;
+use Civi\Api4\UFGroup;
+
+/**
+ * @group headless
+ */
+class CustomProfileFieldTest extends CustomTestBase {
+
+  public function testExportProfileWithCustomFields() {
+    $customGroup = CustomGroup::create(FALSE)
+      ->addValue('title', 'ProfileGroup')
+      ->addValue('extends', 'Individual')
+      ->execute()
+      ->first();
+
+    $custom1 = $this->createTestRecord('CustomField', [
+      'label' => 'F1',
+      'custom_group_id' => $customGroup['id'],
+    ]);
+    $custom2 = $this->createTestRecord('CustomField', [
+      'label' => 'F2',
+      'custom_group_id' => $customGroup['id'],
+    ]);
+
+    $profile = $this->createTestRecord('UFGroup');
+    $field0 = $this->createTestRecord('UFField', [
+      'uf_group_id' => $profile['id'],
+      'field_name' => 'first_name',
+    ]);
+    $field1 = $this->createTestRecord('UFField', [
+      'uf_group_id' => $profile['id'],
+      'field_name' => 'custom_' . $custom1['id'],
+    ]);
+    $field2 = $this->createTestRecord('UFField', [
+      'uf_group_id' => $profile['id'],
+      'field_name:name' => 'ProfileGroup.F2',
+    ]);
+
+    $export = UFGroup::export(FALSE)
+      ->setId($profile['id'])
+      ->execute();
+
+    $this->assertCount(4, $export);
+
+    $this->assertEquals('UFGroup', $export[0]['entity']);
+    // First Name field should not use pseudoconstant
+    $this->assertEquals('first_name', $export[1]['params']['values']['field_name']);
+    $this->assertArrayNotHasKey('field_name:name', $export[1]['params']['values']);
+    $this->assertEquals('First Name', $export[1]['params']['values']['label']);
+
+    // Custom fields should use pseudoconstants
+    $this->assertEquals('ProfileGroup.F1', $export[2]['params']['values']['field_name:name']);
+    $this->assertArrayNotHasKey('field_name', $export[2]['params']['values']);
+    $this->assertEquals('F1', $export[2]['params']['values']['label']);
+
+    // Custom fields should use pseudoconstants
+    $this->assertEquals('ProfileGroup.F2', $export[3]['params']['values']['field_name:name']);
+    $this->assertArrayNotHasKey('field_name', $export[3]['params']['values']);
+    $this->assertEquals('F2', $export[3]['params']['values']['label']);
+  }
+
+}
index e12a887b5498591bf2e78d4c5a23bb1bf3717d55..16638a498bb86a8a4081bcdc869f33ef02c81a38 100644 (file)
@@ -65,8 +65,9 @@ class ExportCustomGroupTest extends CustomTestBase {
     $this->assertEquals($export[5]['params']['values']['option_group_id.name'], $export[1]['params']['values']['name']);
     // Should be only name, not id
     $this->assertArrayNotHasKey('option_group_id', $export[5]['params']['values']);
+    // Field with no options
+    $this->assertNull($export[6]['params']['values']['option_group_id']);
     $this->assertArrayNotHasKey('option_group_id.name', $export[6]['params']['values']);
-    $this->assertArrayNotHasKey('option_group_id', $export[6]['params']['values']);
     $this->assertArrayNotHasKey('option_values', $export[6]['params']['values']);
   }
 
index 0a2aa118ee5d17b3fb651b2cd1b9ff59a0db9269..5620cd66fddd3bef2a79f03623f8a233e4befe7b 100644 (file)
@@ -54,7 +54,7 @@
     <required>true</required>
     <comment>Name for CiviCRM field which is being exposed for sharing.</comment>
     <pseudoconstant>
-      <callback>CRM_Core_BAO_UFField::getAvailableFieldTitles</callback>
+      <callback>CRM_Core_BAO_UFField::getAvailableFieldOptions</callback>
     </pseudoconstant>
     <add>1.1</add>
   </field>