Merge pull request #24187 from colemanw/removeCiviAuction
authorEileen McNaughton <emcnaughton@wikimedia.org>
Tue, 9 Aug 2022 07:55:08 +0000 (19:55 +1200)
committerGitHub <noreply@github.com>
Tue, 9 Aug 2022 07:55:08 +0000 (19:55 +1200)
Remove reference to CiviAuction

34 files changed:
CRM/Api4/Page/AJAX.php
CRM/Api4/Permission.php
CRM/Contact/DAO/SavedSearch.php
CRM/Contribute/Import/Parser/Contribution.php
CRM/Core/BAO/LabelFormat.php
CRM/Core/BAO/PdfFormat.php
CRM/Core/Payment/Elavon.php
CRM/Core/SelectValues.php
CRM/Event/Import/Parser/Participant.php
CRM/Member/Import/Parser/Membership.php
CRM/Upgrade/Incremental/php/FiveFiftyThree.php
CRM/Utils/DeprecatedUtils.php
CRM/Utils/Recent.php
CRM/Utils/System.php
CRM/Utils/System/DrupalBase.php
CRM/Utils/System/UnitTests.php
Civi/Api4/Action/Contact/ContactSaveTrait.php
Civi/Api4/Event/Subscriber/ContactSchemaMapSubscriber.php [new file with mode: 0644]
Civi/Api4/Generic/Traits/DAOActionTrait.php
Civi/Api4/Query/Api4SelectQuery.php
Civi/Api4/Service/Schema/Joinable/Joinable.php
Civi/Api4/Service/Spec/Provider/ContactGetSpecProvider.php
ang/api4Explorer/Explorer.js
ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php
ext/afform/core/Civi/Api4/Action/Afform/Submit.php
ext/civigrant/managed/OptionGroup_recent_items_providers_OptionValue_Grant.mgd.php [new file with mode: 0644]
ext/search_kit/Civi/Search/Admin.php
tests/phpunit/CRM/Contribute/Import/Parser/ContributionTest.php
tests/phpunit/CRM/Contribute/Import/Parser/data/contributions_match_external_id.csv [new file with mode: 0644]
tests/phpunit/api/v4/Action/ContactGetTest.php
tests/phpunit/api/v4/Action/NullValueTest.php
tests/phpunit/api/v4/Entity/ContactJoinTest.php
xml/schema/Contact/SavedSearch.xml
xml/templates/civicrm_data.tpl

index 4816ea9c97d9261c355bf6d2e3e9304ddc2d6d3a..0aa40e94d9cdea315bd901f0dd25d5be63ce5b68 100644 (file)
@@ -58,8 +58,9 @@ class CRM_Api4_Page_AJAX extends CRM_Core_Page {
       CRM_Utils_System::civiExit();
     }
     try {
-      // Call multiple
+      // Two call formats. Which one was used? Note: CRM_Api4_Permission::check() and CRM_Api4_Page_AJAX::run() should have matching conditionals.
       if (empty($this->urlPath[3])) {
+        // Received multi-call format
         $calls = CRM_Utils_Request::retrieve('calls', 'String', CRM_Core_DAO::$_nullObject, TRUE, NULL, 'POST');
         $calls = json_decode($calls, TRUE);
         $response = [];
@@ -67,8 +68,8 @@ class CRM_Api4_Page_AJAX extends CRM_Core_Page {
           $response[$index] = call_user_func_array([$this, 'execute'], $call);
         }
       }
-      // Call single
       else {
+        // Received single-call format
         $entity = $this->urlPath[3];
         $action = $this->urlPath[4];
         $params = CRM_Utils_Request::retrieve('params', 'String');
index c179ad57c023ce094545c3806935ba0f93b5df2e..e476627656063aa82f0b3b935cbe8ab2454bdbb2 100644 (file)
 class CRM_Api4_Permission {
 
   public static function check() {
-    $config = CRM_Core_Config::singleton();
-    $urlPath = explode('/', $_GET[$config->userFrameworkURLVar]);
-    $permissions = [
+    $urlPath = explode('/', CRM_Utils_System::currentPath());
+    $defaultPermissions = [
       ['access CiviCRM', 'access AJAX API'],
     ];
+    // Two call formats. Which one was used? Note: CRM_Api4_Permission::check() and CRM_Api4_Page_AJAX::run() should have matching conditionals.
     if (!empty($urlPath[3])) {
+      // Received single-call format
       $entity = $urlPath[3];
       $action = $urlPath[4];
+      $permissions = $defaultPermissions;
       CRM_Utils_Hook::alterApiRoutePermissions($permissions, $entity, $action);
+      return CRM_Core_Permission::check($permissions);
+    }
+    else {
+      // Received multi-call format
+      $calls = CRM_Utils_Request::retrieve('calls', 'String', CRM_Core_DAO::$_nullObject, TRUE, NULL, 'POST');
+      $calls = json_decode($calls, TRUE);
+      foreach ($calls as $call) {
+        $permissions = $defaultPermissions;
+        CRM_Utils_Hook::alterApiRoutePermissions($permissions, $call[0], $call[1]);
+        if (!CRM_Core_Permission::check($permissions)) {
+          return FALSE;
+        }
+      }
+      return TRUE;
     }
-    return CRM_Core_Permission::check($permissions);
   }
 
 }
index 1d52252db88b8d29a6971800a61b8cf36bcc1a06..2a7ff17ee31754861cec0573170520b00336f8f1 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from xml/schema/CRM/Contact/SavedSearch.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:1b324e028960b1c8fd94ecdc973a6fdc)
+ * (GenCodeChecksum:463e44ca73e5b034502fcc1605f5a9c2)
  */
 
 /**
@@ -30,6 +30,13 @@ class CRM_Contact_DAO_SavedSearch extends CRM_Core_DAO {
    */
   public static $_icon = 'fa-search-plus';
 
+  /**
+   * Field to show when displaying a record.
+   *
+   * @var string
+   */
+  public static $_labelField = 'label';
+
   /**
    * Should CiviCRM log any modifications to this table in the civicrm_log table.
    *
index e244fe0fc36dee8609fff99dec1dc025929c0939..9c4a4bbd249d23e866c604cdf9fe98eddee8910b 100644 (file)
@@ -417,7 +417,7 @@ class CRM_Contribute_Import_Parser_Contribution extends CRM_Import_Parser {
         $error = $this->checkContactDuplicate($paramValues);
 
         if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
-          $matchedIDs = explode(',', $error['error_message']['params'][0]);
+          $matchedIDs = (array) $error['error_message']['params'];
           if (count($matchedIDs) > 1) {
             throw new CRM_Core_Exception('Multiple matching contact records detected for this row. The contribution was not imported', CRM_Import_Parser::ERROR);
           }
@@ -776,7 +776,7 @@ class CRM_Contribute_Import_Parser_Contribution extends CRM_Import_Parser {
               $error = $this->checkContactDuplicate($params);
 
               if (isset($error['error_message']['params'][0])) {
-                $matchedIDs = explode(',', $error['error_message']['params'][0]);
+                $matchedIDs = (array) $error['error_message']['params'];
 
                 // check if only one contact is found
                 if (count($matchedIDs) > 1) {
index 8678560c9cf30a693035e0d4ea34b5872fda3168..4d8e462dabad5c20a50ad9460e9498aa26bacd56 100644 (file)
@@ -203,13 +203,8 @@ class CRM_Core_BAO_LabelFormat extends CRM_Core_DAO_OptionValue {
    * @return array
    *   array of measurement units
    */
-  public static function getUnits() {
-    return [
-      'in' => ts('Inches'),
-      'cm' => ts('Centimeters'),
-      'mm' => ts('Millimeters'),
-      'pt' => ts('Points'),
-    ];
+  public static function getUnits(): array {
+    return CRM_Core_SelectValues::getLayoutUnits();
   }
 
   /**
index a082ace7afe12a9547b18291373154070c1bf1f2..cfe2ebfb0116d7cbe1209e4ccc8fc0c7798832dc 100644 (file)
@@ -113,13 +113,8 @@ class CRM_Core_BAO_PdfFormat extends CRM_Core_DAO_OptionValue {
    * @return array
    *   array of measurement units
    */
-  public static function getUnits() {
-    return [
-      'in' => ts('Inches'),
-      'cm' => ts('Centimeters'),
-      'mm' => ts('Millimeters'),
-      'pt' => ts('Points'),
-    ];
+  public static function getUnits(): array {
+    return CRM_Core_SelectValues::getLayoutUnits();
   }
 
   /**
index fbc74d43446b4104f58801ef3406a937a4ec41da..32f6039b61da15bb4bf7c8cd5b3e5d03b5c17167 100644 (file)
@@ -41,6 +41,25 @@ class CRM_Core_Payment_Elavon extends CRM_Core_Payment {
     $this->_paymentProcessor = $paymentProcessor;
   }
 
+  /**
+   * @var GuzzleHttp\Client
+   */
+  protected $guzzleClient;
+
+  /**
+   * @return \GuzzleHttp\Client
+   */
+  public function getGuzzleClient(): \GuzzleHttp\Client {
+    return $this->guzzleClient ?? new \GuzzleHttp\Client();
+  }
+
+  /**
+   * @param \GuzzleHttp\Client $guzzleClient
+   */
+  public function setGuzzleClient(\GuzzleHttp\Client $guzzleClient) {
+    $this->guzzleClient = $guzzleClient;
+  }
+
   /**
    * Map fields to parameters.
    *
@@ -142,71 +161,29 @@ class CRM_Core_Payment_Elavon extends CRM_Core_Payment {
     // Send to the payment processor using cURL
 
     $chHost = $host . '?xmldata=' . $xml;
-
-    $ch = curl_init($chHost);
-    if (!$ch) {
-      throw new PaymentProcessorException('Could not initiate connection to payment gateway', 9004);
-    }
-
-    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, Civi::settings()->get('verifySSL') ? 2 : 0);
-    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, Civi::settings()->get('verifySSL'));
-    // return the result on success, FALSE on failure
-    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
-    curl_setopt($ch, CURLOPT_TIMEOUT, 36000);
-    // set this for debugging -look for output in apache error log
-    //curl_setopt ($ch,CURLOPT_VERBOSE,1 );
-    // ensures any Location headers are followed
+    $curlParams = [
+      CURLOPT_RETURNTRANSFER => TRUE,
+      CURLOPT_TIMEOUT => 36000,
+      CURLOPT_SSL_VERIFYHOST => Civi::settings()->get('verifySSL') ? 2 : 0,
+      CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
+    ];
     if (ini_get('open_basedir') == '') {
-      curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
-    }
-
-    // Send the data out over the wire
-    $responseData = curl_exec($ch);
-
-    // See if we had a curl error - if so tell 'em and bail out
-    // NOTE: curl_error does not return a logical value (see its documentation), but
-    // a string, which is empty when there was no error.
-    if ((curl_errno($ch) > 0) || (strlen(curl_error($ch)) > 0)) {
-      curl_close($ch);
-      $errorNum = curl_errno($ch);
-      $errorDesc = curl_error($ch);
-
-      // Paranoia - in the unlikley event that 'curl' errno fails
-      if ($errorNum == 0) {
-        $errorNum = 9005;
-      }
-
-      // Paranoia - in the unlikley event that 'curl' error fails
-      if (strlen($errorDesc) == 0) {
-        $errorDesc = 'Connection to payment gateway failed';
-      }
-      throw new PaymentProcessorException('Curl error - ' . $errorDesc . ' Try this link for more information http://curl.haxx.se/docs/sslcerts.html', $errorNum);
-    }
-
-    // If null data returned - tell 'em and bail out
-    // NOTE: You will not necessarily get a string back, if the request failed for
-    // any reason, the return value will be the boolean false.
-    if (($responseData === FALSE) || (strlen($responseData) == 0)) {
-      curl_close($ch);
-      throw new PaymentProcessorException('Error: Connection to payment gateway failed - no data returned.', 9006);
+      $curlParams[CURLOPT_FOLLOWLOCATION] = 1;
     }
+    $responseData = $this->getGuzzleClient()->post($chHost, [
+      'curl' => $curlParams,
+    ])->getBody();
 
     // If gateway returned no data - tell 'em and bail out
     if (empty($responseData)) {
-      curl_close($ch);
       throw new PaymentProcessorException('Error: No data returned from payment gateway.', 9007);
     }
 
-    // Success so far - close the curl and check the data
-    curl_close($ch);
-
     // Payment successfully sent to gateway - process the response now
-
     $processorResponse = $this->decodeXMLresponse($responseData);
     // success in test mode returns response "APPROVED"
     // test mode always returns trxn_id = 0
     // fix for CRM-2566
-
     if ($processorResponse['errorCode']) {
       throw new PaymentProcessorException("Error: [" . $processorResponse['errorCode'] . " " . $processorResponse['errorName'] . " " . $processorResponse['errorMessage'] . '] - from payment processor', 9010);
     }
index 2943c62f1ceb974d7e7fa17fc550f199811b390e..e5a837f9340075bc13d84a998ebddf5ce9800ea3 100644 (file)
@@ -769,6 +769,22 @@ class CRM_Core_SelectValues {
     ];
   }
 
+  /**
+   * Get measurement units recognized by the TCPDF package used to create PDF labels.
+   *
+   * @return array
+   *   array of measurement units
+   */
+  public static function getLayoutUnits(): array {
+    return [
+      'in' => ts('Inches'),
+      'cm' => ts('Centimeters'),
+      'mm' => ts('Millimeters'),
+      'pt' => ts('Points'),
+      'px' => ts('Pixels'),
+    ];
+  }
+
   /**
    * Extension types.
    *
index 8afe963e82de27ee09de6a99411de77b08bb86fd..de5f53ef520591a129ad4a1246b1514b31abe836 100644 (file)
@@ -236,7 +236,7 @@ class CRM_Event_Import_Parser_Participant extends CRM_Import_Parser {
         $error = $this->checkContactDuplicate($formatValues);
 
         if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
-          $matchedIDs = explode(',', $error['error_message']['params'][0]);
+          $matchedIDs = (array) $error['error_message']['params'];
           if (count($matchedIDs) >= 1) {
             foreach ($matchedIDs as $contactId) {
               $formatted['contact_id'] = $contactId;
index 45db615fcdf5209b14cf72e8735165636639c2bf..5c6dbc9b22ee484cbdb809afc9ac7c0374929cf6 100644 (file)
@@ -215,7 +215,7 @@ class CRM_Member_Import_Parser_Membership extends CRM_Import_Parser {
         $error = $this->checkContactDuplicate($formatValues);
 
         if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
-          $matchedIDs = explode(',', $error['error_message']['params'][0]);
+          $matchedIDs = (array) $error['error_message']['params'];
           if (count($matchedIDs) > 1) {
             throw new CRM_Core_Exception('Multiple matching contact records detected for this row. The membership was not imported', CRM_Import_Parser::ERROR);
           }
index 1d735abcd13d8cd7c4eb2e4c5a1e491f45dcf8b2..91460a0b458858432ac8ca99047f4be541ee6a98 100644 (file)
@@ -58,6 +58,7 @@ class CRM_Upgrade_Incremental_php_FiveFiftyThree extends CRM_Upgrade_Incremental
     $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
     $this->addTask('Replace %A specifier in date settings.', 'replacePercentA');
     $this->addTask('Add invoice pdf format', 'addInvoicePDFFormat');
+    $this->addTask('Add Recent Items Providers', 'addRecentItemsProviders');
   }
 
   /**
@@ -106,4 +107,37 @@ class CRM_Upgrade_Incremental_php_FiveFiftyThree extends CRM_Upgrade_Incremental
     return $usages;
   }
 
+  /**
+   * dev/core#3783 Add Recent Items Providers.
+   * @return bool
+   */
+  public static function addRecentItemsProviders() {
+    CRM_Core_BAO_OptionGroup::ensureOptionGroupExists([
+      'name' => 'recent_items_providers',
+      'title' => ts('Recent Items Providers'),
+      'is_reserved' => 0,
+    ]);
+    $values = [
+      'Contact' => ['label' => ts('Contacts')],
+      'Relationship' => ['label' => ts('Relationships')],
+      'Activity' => ['label' => ts('Activities')],
+      'Note' => ['label' => ts('Notes')],
+      'Group' => ['label' => ts('Groups')],
+      'Case' => ['label' => ts('Cases')],
+      'Contribution' => ['label' => ts('Contributions')],
+      'Participant' => ['label' => ts('Participants')],
+      'Membership' => ['label' => ts('Memberships')],
+      'Pledge' => ['label' => ts('Pledges')],
+      'Event' => ['label' => ts('Events')],
+      'Campaign' => ['label' => ts('Campaigns')],
+    ];
+    foreach ($values as $name => $value) {
+      CRM_Core_BAO_OptionValue::ensureOptionValueExists($value + [
+        'name' => $name,
+        'option_group_id' => 'recent_items_providers',
+      ]);
+    }
+    return TRUE;
+  }
+
 }
index 4bfe2c71f435a3dd38be709d637e198234a07d8d..7b9cb3f67da2b381d806f7f4bccab3fba32684b5 100644 (file)
@@ -48,7 +48,7 @@ function _civicrm_api3_deprecated_duplicate_formatted_contact($params) {
         'is_error' => 1,
         'error_message' => [
           'code' => CRM_Core_Error::DUPLICATE_CONTACT,
-          'params' => $contact->id,
+          'params' => [$contact->id],
           'level' => 'Fatal',
           'message' => "Found matching contacts: $contact->id",
         ],
index f09ce294bbbb795e667bb1f15f1d5d8731948ea1..d364387132377ca311dac09d75c000e66ad40bc8 100644 (file)
@@ -14,6 +14,7 @@
  * @copyright CiviCRM LLC https://civicrm.org/licensing
  */
 
+use Civi\Api4\OptionValue;
 use Civi\Api4\Utils\CoreUtil;
 
 /**
@@ -340,28 +341,16 @@ class CRM_Utils_Recent {
   /**
    * Gets the list of available providers to civi's recent items stack
    *
-   * TODO: Make this an option group so extensions can extend it.
-   *
    * @return array
    */
   public static function getProviders() {
-    $providers = [
-      'Contact' => ts('Contacts'),
-      'Relationship' => ts('Relationships'),
-      'Activity' => ts('Activities'),
-      'Note' => ts('Notes'),
-      'Group' => ts('Groups'),
-      'Case' => ts('Cases'),
-      'Contribution' => ts('Contributions'),
-      'Participant' => ts('Participants'),
-      'Grant' => ts('Grants'),
-      'Membership' => ts('Memberships'),
-      'Pledge' => ts('Pledges'),
-      'Event' => ts('Events'),
-      'Campaign' => ts('Campaigns'),
-    ];
-
-    return $providers;
+    return OptionValue::get(FALSE)
+      ->addWhere('option_group_id:name', '=', 'recent_items_providers')
+      ->addWhere('is_active', '=', TRUE)
+      ->addOrderBy('weight', 'ASC')
+      ->execute()
+      ->indexBy('value')
+      ->column('label');
   }
 
 }
index 43148fb6a228ef8dd39a040e50ac456ee50cb6bb..b980182b80b753bb8f960fd88cde399a0dee2e2e 100644 (file)
@@ -244,23 +244,27 @@ class CRM_Utils_System {
    *   An HTML string containing a link to the given path.
    */
   public static function url(
-    $path = NULL,
-    $query = NULL,
+    $path = '',
+    $query = '',
     $absolute = FALSE,
     $fragment = NULL,
     $htmlize = TRUE,
     $frontend = FALSE,
     $forceBackend = FALSE
   ) {
+    // handle legacy null params
+    $path = $path ?? '';
+    $query = $query ?? '';
+
     $query = self::makeQueryString($query);
 
     // Legacy handling for when the system passes around html escaped strings
-    if (strstr(($query ?? ''), '&amp;')) {
+    if (strstr($query, '&amp;')) {
       $query = html_entity_decode($query);
     }
 
     // Extract fragment from path or query if munged together
-    if ($query && strstr(($query ?? ''), '#')) {
+    if ($query && strstr($query, '#')) {
       list($path, $fragment) = explode('#', $query);
     }
     if ($path && strstr($path, '#')) {
index b25d6fc82384476d3ac55dbc0808d053e4e5d581..f5b916bf09fcaecda8478563837a1fff4ebb3b6b 100644 (file)
@@ -171,8 +171,8 @@ abstract class CRM_Utils_System_DrupalBase extends CRM_Utils_System_Base {
     $separator = '&';
 
     if (!$config->cleanURL) {
-      if (isset($path)) {
-        if (isset($query)) {
+      if ($path !== NULL && $path !== '' && $path !== FALSE) {
+        if ($query !== NULL && $query !== '' && $query !== FALSE) {
           return $base . $script . '?q=' . $path . $separator . $query . $fragment;
         }
         else {
@@ -180,7 +180,7 @@ abstract class CRM_Utils_System_DrupalBase extends CRM_Utils_System_Base {
         }
       }
       else {
-        if (isset($query)) {
+        if ($query !== NULL && $query !== '' && $query !== FALSE) {
           return $base . $script . '?' . $query . $fragment;
         }
         else {
@@ -189,8 +189,8 @@ abstract class CRM_Utils_System_DrupalBase extends CRM_Utils_System_Base {
       }
     }
     else {
-      if (isset($path)) {
-        if (isset($query)) {
+      if ($path !== NULL && $path !== '' && $path !== FALSE) {
+        if ($query !== NULL && $query !== '' && $query !== FALSE) {
           return $base . $path . '?' . $query . $fragment;
         }
         else {
@@ -198,7 +198,7 @@ abstract class CRM_Utils_System_DrupalBase extends CRM_Utils_System_Base {
         }
       }
       else {
-        if (isset($query)) {
+        if ($query !== NULL && $query !== '' && $query !== FALSE) {
           return $base . $script . '?' . $query . $fragment;
         }
         else {
@@ -657,7 +657,7 @@ abstract class CRM_Utils_System_DrupalBase extends CRM_Utils_System_Base {
    * @return bool
    */
   public function isFrontEndPage() {
-    $path = CRM_Utils_System::currentPath();
+    $path = CRM_Utils_System::currentPath() ?? '';
 
     // Get the menu for above URL.
     $item = CRM_Core_Menu::get($path);
index 547b93f98dbf0446030a139e9416dd3376c9d9ff..868367da48571d9aa87ba5f7444ac1c0fa636ebf 100644 (file)
@@ -103,8 +103,8 @@ class CRM_Utils_System_UnitTests extends CRM_Utils_System_Base {
     $separator = ($htmlize && $frontend) ? '&amp;' : '&';
 
     if (!$config->cleanURL) {
-      if (isset($path)) {
-        if (isset($query)) {
+      if ($path !== NULL && $path !== '' && $path !== FALSE) {
+        if ($query !== NULL && $query !== '' && $query !== FALSE) {
           return $base . $script . '?q=' . $path . $separator . $query . $fragment;
         }
         else {
@@ -112,7 +112,7 @@ class CRM_Utils_System_UnitTests extends CRM_Utils_System_Base {
         }
       }
       else {
-        if (isset($query)) {
+        if ($query !== NULL && $query !== '' && $query !== FALSE) {
           return $base . $script . '?' . $query . $fragment;
         }
         else {
index 67a9cca550738dbdf0f92aaa69040499c21acdf3..3fcc920a254b5a80556be5ec9deb38a7844e425e 100644 (file)
@@ -12,6 +12,9 @@
 
 namespace Civi\Api4\Action\Contact;
 
+use Civi\Api4\Utils\CoreUtil;
+use Civi\Api4\Utils\FormattingUtil;
+
 /**
  * Code shared by Contact create/update/save actions
  */
@@ -40,7 +43,52 @@ trait ContactSaveTrait {
         }
       }
     }
-    return parent::write($items);
+    $saved = parent::write($items);
+    foreach ($items as $index => $item) {
+      self::saveLocations($item, $saved[$index]);
+    }
+    return $saved;
+  }
+
+  /**
+   * @param array $params
+   * @param \CRM_Contact_DAO_Contact $contact
+   */
+  protected function saveLocations(array $params, $contact) {
+    foreach (['Address', 'Email', 'Phone', 'IM'] as $entity) {
+      foreach (['primary', 'billing'] as $type) {
+        $prefix = strtolower($entity) . '_' . $type . '.';
+        $item = FormattingUtil::filterByPrefix($params, $prefix . '*', '*');
+        // Not allowed to update by id or alter primary or billing flags
+        unset($item['id'], $item['is_primary'], $item['is_billing']);
+        if ($item) {
+          $labelField = CoreUtil::getInfoItem($entity, 'label_field');
+          // If NULL was given for the main field (e.g. `email`) then delete the record
+          if ($labelField && array_key_exists($labelField, $item) && is_null($item[$labelField])) {
+            civicrm_api4($entity, 'delete', [
+              'checkPermissions' => FALSE,
+              'where' => [
+                ['contact_id', '=', $contact->id],
+                ["is_$type", '=', TRUE],
+              ],
+            ]);
+          }
+          else {
+            $item['contact_id'] = $contact->id;
+            $item["is_$type"] = TRUE;
+            $saved = civicrm_api4($entity, 'save', [
+              'checkPermissions' => FALSE,
+              'records' => [$item],
+              'match' => ['contact_id', "is_$type"],
+            ])->first();
+            foreach ($saved as $key => $value) {
+              $key = $prefix . $key;
+              $contact->$key = $value;
+            }
+          }
+        }
+      }
+    }
   }
 
 }
diff --git a/Civi/Api4/Event/Subscriber/ContactSchemaMapSubscriber.php b/Civi/Api4/Event/Subscriber/ContactSchemaMapSubscriber.php
new file mode 100644 (file)
index 0000000..6177ebe
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+namespace Civi\Api4\Event\Subscriber;
+
+use Civi\Api4\Event\Events;
+use Civi\Api4\Event\SchemaMapBuildEvent;
+use Civi\Api4\Service\Schema\Joinable\Joinable;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class ContactSchemaMapSubscriber implements EventSubscriberInterface {
+
+  /**
+   * @return array
+   */
+  public static function getSubscribedEvents() {
+    return [
+      Events::SCHEMA_MAP_BUILD => 'onSchemaBuild',
+    ];
+  }
+
+  /**
+   * @param \Civi\Api4\Event\SchemaMapBuildEvent $event
+   */
+  public function onSchemaBuild(SchemaMapBuildEvent $event) {
+    $schema = $event->getSchemaMap();
+    $table = $schema->getTableByName('civicrm_contact');
+
+    // Add links to primary & billing email, address, phone & im
+    foreach (['email', 'address', 'phone', 'im'] as $ent) {
+      foreach (['primary', 'billing'] as $type) {
+        $link = new Joinable("civicrm_$ent", 'contact_id', "{$ent}_$type");
+        $link->setBaseTable('civicrm_contact');
+        $link->setJoinType(Joinable::JOIN_TYPE_ONE_TO_ONE);
+        $link->addCondition("`{target_table}`.`is_$type` = 1");
+        $table->addTableLink('id', $link);
+      }
+    }
+  }
+
+}
index d8e511e70009b176a46a098149d9be03c5bace12..94e0d7af21eecc39bd64f38a0fab88a6da490f1b 100644 (file)
@@ -52,13 +52,15 @@ trait DAOActionTrait {
    * @return array
    */
   public function baoToArray($bao, $input) {
-    $allFields = array_column($bao->fields(), 'name');
+    $entityFields = array_column($bao->fields(), 'name');
+    $inputFields = array_map(function($key) {
+      return explode(':', $key)[0];
+    }, array_keys($input));
+    $combinedFields = array_unique(array_merge($entityFields, $inputFields));
     if (!empty($this->reload)) {
-      $inputFields = $allFields;
       $bao->find(TRUE);
     }
     else {
-      $inputFields = array_keys($input);
       // Convert 'null' input to true null
       foreach ($inputFields as $key) {
         if (($bao->$key ?? NULL) === 'null') {
@@ -67,8 +69,8 @@ trait DAOActionTrait {
       }
     }
     $values = [];
-    foreach ($allFields as $field) {
-      if (isset($bao->$field) || in_array($field, $inputFields)) {
+    foreach ($combinedFields as $field) {
+      if (isset($bao->$field) || in_array($field, $inputFields) || (!empty($this->reload) && in_array($field, $entityFields))) {
         $values[$field] = $bao->$field ?? NULL;
       }
     }
index 71ab0d209221513f73f528964847ed9495d96b76..0ccce5e8b8d67c9ac9f8f7c52e338613bb7756e4 100644 (file)
@@ -799,7 +799,10 @@ class Api4SelectQuery {
     // If we're not explicitly referencing the ID (or some other FK field) of the joinEntity, search for a default
     if (!$explicitFK) {
       foreach ($this->apiFieldSpec as $name => $field) {
-        if (is_array($field) && $field['entity'] !== $joinEntity && $field['fk_entity'] === $joinEntity) {
+        if (!is_array($field) || $field['type'] !== 'Field') {
+          continue;
+        }
+        if ($field['entity'] !== $joinEntity && $field['fk_entity'] === $joinEntity) {
           $conditions[] = $this->treeWalkClauses([$name, '=', "$alias.id"], 'ON');
         }
         elseif (strpos($name, "$alias.") === 0 && substr_count($name, '.') === 1 && $field['fk_entity'] === $this->getEntity()) {
index 076707504e1aaf41d662d626ed01125cf45fe077..95b14cb95c233f675cdeeb55fa580c1fd2d0ae73 100644 (file)
@@ -51,7 +51,7 @@ class Joinable {
   protected $alias;
 
   /**
-   * @var array
+   * @var string[]
    */
   protected $conditions = [];
 
@@ -103,15 +103,18 @@ class Joinable {
    * @return array
    */
   public function getConditionsForJoin(string $baseTableAlias, string $tableAlias) {
-    $baseCondition = sprintf(
+    $conditions = [];
+    $conditions[] = sprintf(
       '`%s`.`%s` =  `%s`.`%s`',
       $baseTableAlias,
       $this->baseColumn,
       $tableAlias,
       $this->targetColumn
     );
-
-    return array_merge([$baseCondition], $this->conditions);
+    foreach ($this->conditions as $condition) {
+      $conditions[] = str_replace(['{base_table}', '{target_table}'], [$baseTableAlias, $tableAlias], $condition);
+    }
+    return $conditions;
   }
 
   /**
@@ -190,11 +193,11 @@ class Joinable {
   }
 
   /**
-   * @param $condition
+   * @param string $condition
    *
    * @return $this
    */
-  public function addCondition($condition) {
+  public function addCondition(string $condition) {
     $this->conditions[] = $condition;
 
     return $this;
@@ -208,7 +211,7 @@ class Joinable {
   }
 
   /**
-   * @param array $conditions
+   * @param string[] $conditions
    *
    * @return $this
    */
index 5c24e90b029b23107813b6bd2915251cb6ba83a4..d3c9dab9219e45c63fe8391b1fc46ddb4a719e3b 100644 (file)
@@ -49,6 +49,64 @@ class ContactGetSpecProvider implements Generic\SpecProviderInterface {
         ->setSqlRenderer([__CLASS__, 'calculateAge']);
       $spec->addFieldSpec($field);
     }
+
+    // Address, Email, Phone, IM
+    $entities = [
+      'Address' => [
+        'primary' => [
+          'title' => ts('Primary Address ID'),
+          'label' => ts('Primary Address'),
+        ],
+        'billing' => [
+          'title' => ts('Billing Address ID'),
+          'label' => ts('Billing Address'),
+        ],
+      ],
+      'Email' => [
+        'primary' => [
+          'title' => ts('Primary Email ID'),
+          'label' => ts('Primary Email'),
+        ],
+        'billing' => [
+          'title' => ts('Billing Email ID'),
+          'label' => ts('Billing Email'),
+        ],
+      ],
+      'Phone' => [
+        'primary' => [
+          'title' => ts('Primary Phone ID'),
+          'label' => ts('Primary Phone'),
+        ],
+        'billing' => [
+          'title' => ts('Billing Phone ID'),
+          'label' => ts('Billing Phone'),
+        ],
+      ],
+      'IM' => [
+        'primary' => [
+          'title' => ts('Primary IM ID'),
+          'label' => ts('Primary IM'),
+        ],
+        'billing' => [
+          'title' => ts('Billing IM ID'),
+          'label' => ts('Billing IM'),
+        ],
+      ],
+    ];
+    foreach ($entities as $entity => $types) {
+      foreach ($types as $type => $info) {
+        $name = strtolower($entity) . '_' . $type;
+        $field = new FieldSpec($name, 'Contact', 'String');
+        $field->setLabel($info['label'])
+          ->setTitle($info['title'])
+          ->setColumnName('id')
+          ->setType('Extra')
+          ->setFkEntity($entity)
+          ->setSqlRenderer([__CLASS__, 'getLocationFieldSql']);
+        $spec->addFieldSpec($field);
+      }
+    }
+
   }
 
   /**
@@ -119,4 +177,16 @@ class ContactGetSpecProvider implements Generic\SpecProviderInterface {
     return "TIMESTAMPDIFF(YEAR, {$field['sql_name']}, CURDATE())";
   }
 
+  /**
+   * Generate SQL for address/email/phone/im id field
+   * @param array $field
+   * @param \Civi\Api4\Query\Api4SelectQuery $query
+   * @return string
+   */
+  public static function getLocationFieldSql(array $field, Api4SelectQuery $query) {
+    $prefix = empty($field['explicit_join']) ? '' : $field['explicit_join'] . '.';
+    $idField = $query->getField($prefix . $field['name'] . '.id');
+    return $idField['sql_name'];
+  }
+
 }
index 4d299cf9ecabbdbbfb674284c54ee87a8b107b5d..d1581210972f00336242f218f9982c2c8e86fe86 100644 (file)
       var fieldInfo = _.cloneDeep(_.findWhere(getEntity().actions, {name: action}).fields);
       fieldList.length = 0;
       if (addPseudoconstant) {
-        addPseudoconstants(fieldInfo, addPseudoconstant);
+        addPseudoconstants(fieldInfo);
       }
       if (addWriteJoins) {
         addWriteJoinFields(fieldInfo);
             });
           }
           if (addPseudoconstant) {
-            addPseudoconstants(joinFields, addPseudoconstant);
+            addPseudoconstants(joinFields);
           }
           fieldList.push({
             text: join.entity + ' AS ' + join.alias,
           var linkFields = _.cloneDeep(entityFields(field.fk_entity)),
             wildCard = addWildcard ? [{id: field.name + '.*', text: field.name + '.*', 'description': 'All core ' + field.fk_entity + ' fields'}] : [];
           if (addPseudoconstant) {
-            addPseudoconstants(linkFields, addPseudoconstant);
+            addPseudoconstants(linkFields);
           }
           fieldList.push({
             text: field.name,
     }
 
     // Note: this function transforms a raw list a-la getFields; not a select2-formatted list
-    function addPseudoconstants(fieldList, toAdd) {
+    function addPseudoconstants(fieldList) {
       var optionFields = _.filter(fieldList, 'options');
       _.each(optionFields, function(field) {
         var pos = _.findIndex(fieldList, {name: field.name}) + 1;
-        _.each(toAdd, function(suffix) {
+        _.each(field.suffixes, function(suffix) {
           var newField = _.cloneDeep(field);
           newField.name += ':' + suffix;
           fieldList.splice(pos, 0, newField);
     $scope.fieldList = function(param) {
       return function() {
         var fields = [];
-        getFieldList(fields, $scope.action === 'getFields' ? ($scope.params.action || 'get') : $scope.action, ['name'], true);
+        getFieldList(fields, $scope.action === 'getFields' ? ($scope.params.action || 'get') : $scope.action, true, true);
         // Disable fields that are already in use
         _.each($scope.params[param] || [], function(val) {
           var usedField = val[0].replace(/[:.]name/, '');
     this.buildFieldList = function() {
       var actionInfo = _.findWhere(actions, {id: $scope.action});
       getFieldList($scope.fields, $scope.action);
-      getFieldList($scope.fieldsAndJoins, $scope.action, ['name']);
+      getFieldList($scope.fieldsAndJoins, $scope.action, true);
       getFieldList($scope.fieldsAndJoinsAndFunctions, $scope.action);
-      getFieldList($scope.fieldsAndJoinsAndFunctionsWithSuffixes, $scope.action, ['name', 'label']);
-      getFieldList($scope.fieldsAndJoinsAndFunctionsAndWildcards, $scope.action, ['name', 'label']);
+      getFieldList($scope.fieldsAndJoinsAndFunctionsWithSuffixes, $scope.action, true);
+      getFieldList($scope.fieldsAndJoinsAndFunctionsAndWildcards, $scope.action, true);
       if (_.contains(['get', 'update', 'delete', 'replace'], $scope.action)) {
         addJoins($scope.fieldsAndJoins);
         // SQL functions are supported if HAVING is
           $scope.fieldsAndJoinsAndFunctionsAndWildcards.push(functions);
         }
         addJoins($scope.fieldsAndJoinsAndFunctions, true);
-        addJoins($scope.fieldsAndJoinsAndFunctionsWithSuffixes, false, ['name', 'label']);
-        addJoins($scope.fieldsAndJoinsAndFunctionsAndWildcards, true, ['name', 'label']);
+        addJoins($scope.fieldsAndJoinsAndFunctionsWithSuffixes, false, true);
+        addJoins($scope.fieldsAndJoinsAndFunctionsAndWildcards, true, true);
       }
       // Custom fields are supported if HAVING is
       if (actionInfo.params.having) {
index 3fcde4e9c4cb9483425fbe02c53d1c83316ade14..870254d1c161c35664f301220719c459012082bc 100644 (file)
@@ -103,7 +103,7 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction {
         $data = ['fields' => $result[$id]];
         foreach ($entity['joins'] ?? [] as $joinEntity => $join) {
           $data['joins'][$joinEntity] = (array) $api4($joinEntity, 'get', [
-            'where' => self::getJoinWhereClause($entity['type'], $joinEntity, $id),
+            'where' => self::getJoinWhereClause($this->_formDataModel, $entity['name'], $joinEntity, $id),
             'limit' => !empty($join['af-repeat']) ? $join['max'] ?? 0 : 1,
             'select' => array_keys($join['fields']),
             'orderBy' => self::getEntityField($joinEntity, 'is_primary') ? ['is_primary' => 'DESC'] : [],
@@ -139,22 +139,32 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction {
   abstract protected function processForm();
 
   /**
-   * @param $mainEntityName
-   * @param $joinEntityName
-   * @param $mainEntityId
+   * @param \Civi\Afform\FormDataModel $formDataModel
+   * @param string $mainEntityName
+   * @param string $joinEntityType
+   * @param int|string $mainEntityId
    * @return array
    * @throws \API_Exception
    */
-  protected static function getJoinWhereClause($mainEntityName, $joinEntityName, $mainEntityId) {
+  protected static function getJoinWhereClause(FormDataModel $formDataModel, string $mainEntityName, string $joinEntityType, $mainEntityId) {
+    $entity = $formDataModel->getEntity($mainEntityName);
+    $mainEntityType = $entity['type'];
     $params = [];
-    if (self::getEntityField($joinEntityName, 'entity_id')) {
+
+    // Add data as clauses e.g. `is_primary: true`
+    foreach ($entity['joins'][$joinEntityType]['data'] ?? [] as $key => $val) {
+      $params[] = [$key, '=', $val];
+    }
+
+    // Figure out the FK field between the join entity and the main entity
+    if (self::getEntityField($joinEntityType, 'entity_id')) {
       $params[] = ['entity_id', '=', $mainEntityId];
-      if (self::getEntityField($joinEntityName, 'entity_table')) {
-        $params[] = ['entity_table', '=', CoreUtil::getTableName($mainEntityName)];
+      if (self::getEntityField($joinEntityType, 'entity_table')) {
+        $params[] = ['entity_table', '=', CoreUtil::getTableName($mainEntityType)];
       }
     }
     else {
-      $mainEntityField = \CRM_Core_DAO_AllCoreTables::convertEntityNameToLower($mainEntityName) . '_id';
+      $mainEntityField = \CRM_Core_DAO_AllCoreTables::convertEntityNameToLower($mainEntityType) . '_id';
       $params[] = [$mainEntityField, '=', $mainEntityId];
     }
     return $params;
index 3e453c52ea1fd680a711fccca5aa63fcad87af9b..0120e046ae307a839cd45eb5ecde7ca5d6ecf105 100644 (file)
@@ -242,7 +242,7 @@ class Submit extends AbstractProcessor {
         $result = civicrm_api4($joinEntityName, 'replace', [
           // Disable permission checks because the main entity has already been vetted
           'checkPermissions' => FALSE,
-          'where' => self::getJoinWhereClause($event->getEntityType(), $joinEntityName, $entityId),
+          'where' => self::getJoinWhereClause($event->getFormDataModel(), $event->getEntityName(), $joinEntityName, $entityId),
           'records' => $values,
         ], ['id']);
         $indexedResult = array_combine(array_keys($values), (array) $result);
@@ -254,7 +254,7 @@ class Submit extends AbstractProcessor {
           civicrm_api4($joinEntityName, 'delete', [
             // Disable permission checks because the main entity has already been vetted
             'checkPermissions' => FALSE,
-            'where' => self::getJoinWhereClause($event->getEntityType(), $joinEntityName, $entityId),
+            'where' => self::getJoinWhereClause($event->getFormDataModel(), $event->getEntityName(), $joinEntityName, $entityId),
           ]);
         }
         catch (\API_Exception $e) {
diff --git a/ext/civigrant/managed/OptionGroup_recent_items_providers_OptionValue_Grant.mgd.php b/ext/civigrant/managed/OptionGroup_recent_items_providers_OptionValue_Grant.mgd.php
new file mode 100644 (file)
index 0000000..d0359dd
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+use CRM_Grant_ExtensionUtil as E;
+
+// Prevent errors during upgrades from < 5.53 that don't yet have the option group
+$optionGroup = \Civi\Api4\OptionGroup::get(FALSE)
+  ->addWhere('name', '=', 'recent_items_providers')
+  ->selectRowCount()
+  ->execute();
+if (!$optionGroup->count()) {
+  return [];
+}
+
+return [
+  [
+    'name' => 'OptionGroup_recent_items_providers_OptionValue_Grant',
+    'entity' => 'OptionValue',
+    'cleanup' => 'always',
+    'update' => 'always',
+    'params' => [
+      'version' => 4,
+      'values' => [
+        'option_group_id.name' => 'recent_items_providers',
+        'label' => E::ts('Grants'),
+        'value' => 'Grant',
+        'name' => 'Grants',
+        'grouping' => NULL,
+        'filter' => 0,
+        'is_default' => FALSE,
+        'description' => NULL,
+        'is_optgroup' => FALSE,
+        'is_reserved' => FALSE,
+        'is_active' => TRUE,
+        'icon' => NULL,
+        'color' => NULL,
+        'component_id' => NULL,
+        'domain_id' => NULL,
+        'visibility_id' => NULL,
+      ],
+    ],
+  ],
+];
index 5eb3d4601a7743e63e432fe627ef4cec4f04dbbc..8d0d2e2271f8c75da8f2254ecc0b345692e74aa6 100644 (file)
@@ -192,6 +192,19 @@ class Admin {
             array_splice($entity['fields'], $index, 0, [$newField]);
           }
         }
+        // Useful address fields (see ContactSchemaMapSubscriber)
+        if ($entity['name'] === 'Contact') {
+          $addressFields = ['city', 'state_province_id', 'country_id'];
+          foreach ($addressFields as $fieldName) {
+            foreach (['primary', 'billing'] as $type) {
+              $newField = \CRM_Utils_Array::findAll($schema['Address']['fields'], ['name' => $fieldName])[0];
+              $newField['name'] = "address_$type.$fieldName";
+              $arg = [1 => $newField['label']];
+              $newField['label'] = $type === 'primary' ? ts('Address (primary) %1', $arg) : ts('Address (billing) %1', $arg);
+              $entity['fields'][] = $newField;
+            }
+          }
+        }
       }
     }
     return array_values($schema);
index 0a94114c5b4068bf9fe69e85faef6569bfbe503c..e1463439fabdc93a7d4379381c8dc9b392d258aa 100644 (file)
@@ -277,6 +277,49 @@ class CRM_Contribute_Import_Parser_ContributionTest extends CiviUnitTestCase {
     $this->assertEquals('No matching Contact found for (mum@example.com )', $row['_status_message']);
   }
 
+  public function testImportWithMatchByExternalIdentifier() :void {
+    CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_contact AUTO_INCREMENT = 1000000");
+
+    $contactRubyParams = [
+      'first_name' => 'Ruby',
+      'external_identifier' => 'ruby',
+      'contact_type' => 'Individual',
+    ];
+    $contactSapphireParams = [
+      'first_name' => 'Sapphire',
+      'external_identifier' => 'sapphire',
+      'contact_type' => 'Individual',
+    ];
+    $contactRubyId = $this->individualCreate($contactRubyParams);
+    $contactSapphireId = $this->individualCreate($contactSapphireParams);
+
+    // make sure we're testing dev/core#3784
+    self::assertEquals(1, substr($contactRubyId, 0, 1));
+    self::assertEquals(1, substr($contactSapphireId, 0, 1));
+
+    $mapping = [
+      ['name' => 'external_identifier'],
+      ['name' => 'total_amount'],
+      ['name' => 'receive_date'],
+      ['name' => 'financial_type_id'],
+    ];
+    $this->importCSV('contributions_match_external_id.csv', $mapping);
+
+    $contributionsOfRuby = Contribution::get()
+      ->addWhere('contact_id', '=', $contactRubyId)->execute();
+    $contributionsOfSapphire = Contribution::get()
+      ->addWhere('contact_id', '=', $contactSapphireId)->execute();
+
+    $this->assertCount(1, $contributionsOfRuby, 'Wrong number of contributions imported');
+    $this->assertCount(1, $contributionsOfSapphire, 'Wrong number of contributions imported');
+    $this->assertEquals(22222, $contributionsOfRuby->first()['total_amount']);
+    $this->assertEquals(5, $contributionsOfSapphire->first()['total_amount']);
+
+    $dataSource = new CRM_Import_DataSource_CSV($this->userJobID);
+    $this->assertEquals(0, $dataSource->getRowCount([CRM_Import_Parser::ERROR]));
+    $this->assertEquals(2, $dataSource->getRowCount([CRM_Import_Parser::VALID]));
+  }
+
   /**
    * Run the import parser.
    *
diff --git a/tests/phpunit/CRM/Contribute/Import/Parser/data/contributions_match_external_id.csv b/tests/phpunit/CRM/Contribute/Import/Parser/data/contributions_match_external_id.csv
new file mode 100644 (file)
index 0000000..d70db33
--- /dev/null
@@ -0,0 +1,3 @@
+External Identifier,Total Amount,Receive Date,Financial Type
+sapphire,5,2005-05-05,Donation
+ruby,22222,2022-02-22,Donation
index 6e4a72298586a0fb78980d1a6c0414053990359d..b341ba762b3e416aec0ba81e8d3dbe2355f38e3c 100644 (file)
@@ -369,4 +369,37 @@ class ContactGetTest extends Api4TestBase implements TransactionalInterface {
 
   }
 
+  public function testGetWithPrimaryEmailPhoneIMAddress() {
+    $lastName = uniqid(__FUNCTION__);
+    $email = uniqid() . '@example.com';
+    $phone = uniqid('phone');
+    $im = uniqid('im');
+    $c1 = $this->createTestRecord('Contact', ['last_name' => $lastName]);
+    $c2 = $this->createTestRecord('Contact', ['last_name' => $lastName]);
+    $c3 = $this->createTestRecord('Contact', ['last_name' => $lastName]);
+
+    $this->createTestRecord('Email', ['email' => $email, 'contact_id' => $c1['id']]);
+    $this->createTestRecord('Email', ['email' => 'not@primary.com', 'contact_id' => $c1['id']]);
+    $this->createTestRecord('Phone', ['phone' => $phone, 'contact_id' => $c1['id']]);
+    $this->createTestRecord('IM', ['name' => $im, 'contact_id' => $c2['id']]);
+    $this->createTestRecord('Address', ['city' => 'Somewhere', 'street_address' => '123 Street', 'contact_id' => $c2['id']]);
+
+    $results = Contact::get(FALSE)
+      ->addSelect('id', 'email_primary.email', 'phone_primary.phone', 'im_primary.name', 'address_primary.*')
+      ->addWhere('last_name', '=', $lastName)
+      ->addOrderBy('id')
+      ->execute();
+
+    $this->assertEquals($email, $results[0]['email_primary.email']);
+    $this->assertEquals($phone, $results[0]['phone_primary.phone']);
+    $this->assertEquals($im, $results[1]['im_primary.name']);
+    $this->assertEquals('Somewhere', $results[1]['address_primary.city']);
+    $this->assertEquals('123 Street', $results[1]['address_primary.street_address']);
+    $this->assertNull($results[0]['im_primary.name']);
+    $this->assertNull($results[2]['email_primary.email']);
+    $this->assertNull($results[2]['phone_primary.phone']);
+    $this->assertNull($results[2]['im_primary.name']);
+    $this->assertNull($results[2]['address_primary.city']);
+  }
+
 }
index 04ea355c47bea2d4b1d3884ca52d8925f770a25c..8dd0449f5afac78486495483e1f0457b49bd4464 100644 (file)
@@ -29,10 +29,10 @@ use Civi\Test\TransactionalInterface;
  */
 class NullValueTest extends Api4TestBase implements TransactionalInterface {
 
-  public function setUpHeadless() {
+  public function setUp(): void {
     $format = '{contact.first_name}{ }{contact.last_name}';
     \Civi::settings()->set('display_name_format', $format);
-    return parent::setUpHeadless();
+    parent::setUp();
   }
 
   public function testStringNull() {
index 40fadf38e0af0f2a6452e1538fa20e1da98a1dba..9f7d026e12976e9af2fd9fe98e8018ea5887af78 100644 (file)
 
 namespace api\v4\Entity;
 
+use Civi\Api4\Address;
 use Civi\Api4\Contact;
+use Civi\Api4\Email;
 use Civi\Api4\OptionValue;
 use api\v4\Api4TestBase;
+use Civi\Api4\Phone;
 
 /**
  * @group headless
@@ -101,4 +104,56 @@ class ContactJoinTest extends Api4TestBase {
     $this->assertEquals($labels, $fetchedContact['preferred_communication_method:label']);
   }
 
+  public function testCreateWithPrimaryAndBilling() {
+    $contact = $this->createTestRecord('Contact', [
+      'email_primary.email' => 'a@test.com',
+      'email_billing.email' => 'b@test.com',
+      'address_billing.city' => 'Hello',
+      'address_billing.state_province_id:abbr' => 'AK',
+      'address_billing.country_id:abbr' => 'USA',
+    ]);
+    $addr = Address::get(FALSE)
+      ->addWhere('contact_id', '=', $contact['id'])
+      ->execute();
+    $this->assertCount(1, $addr);
+    $this->assertEquals('Hello', $contact['address_billing.city']);
+    $this->assertEquals(1228, $contact['address_billing.country_id']);
+    $emails = Email::get(FALSE)
+      ->addWhere('contact_id', '=', $contact['id'])
+      ->execute();
+    $this->assertCount(2, $emails);
+    $this->assertEquals('a@test.com', $contact['email_primary.email']);
+    $this->assertEquals('b@test.com', $contact['email_billing.email']);
+  }
+
+  public function testUpdateDeletePrimaryAndBilling() {
+    $contact = $this->createTestRecord('Contact', [
+      'phone_primary.phone' => '12345',
+      'phone_billing.phone' => '54321',
+    ]);
+    Contact::update(FALSE)
+      ->addValue('id', $contact['id'])
+      // Delete primary phone, update billing phone
+      ->addValue('phone_primary.phone', NULL)
+      ->addValue('phone_billing.phone', 99999)
+      ->execute();
+    $phone = Phone::get(FALSE)
+      ->addWhere('contact_id', '=', $contact['id'])
+      ->execute()
+      ->single();
+    $this->assertEquals('99999', $phone['phone']);
+    $this->assertTrue($phone['is_billing']);
+    // Contact only has one phone now, so it should be auto-set to primary
+    $this->assertTrue($phone['is_primary']);
+
+    $get = Contact::get(FALSE)
+      ->addWhere('id', '=', $contact['id'])
+      ->addSelect('phone_primary.*')
+      ->addSelect('phone_billing.*')
+      ->execute()->single();
+    $this->assertEquals('99999', $get['phone_primary.phone']);
+    $this->assertEquals('99999', $get['phone_billing.phone']);
+    $this->assertEquals($get['phone_primary.id'], $get['phone_billing.id']);
+  }
+
 }
index b3a7ee515f757477bc8a8509b3e697c7007ce745..4eafe74051dbe737737b96da9958869a5714edcc 100644 (file)
@@ -6,6 +6,7 @@
   <name>civicrm_saved_search</name>
   <comment>Users can save their complex SQL queries and use them later.</comment>
   <icon>fa-search-plus</icon>
+  <labelField>label</labelField>
   <add>1.1</add>
   <field>
     <name>id</name>
index dc537763df1338a392889026efa8895af0194cb3..13edefd796c0665dbc6bf99464551d0d953dace2 100644 (file)
@@ -1789,3 +1789,23 @@ INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_act
 INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_active) VALUES ('module', 'ckeditor4', 'CKEditor4', 'CKEditor4', 'ckeditor4', 1);
 INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_active) VALUES ('module', 'legacycustomsearches', 'Custom search framework', 'Custom search framework', 'legacycustomsearches', 1);
 INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_active) VALUES ('module', 'org.civicrm.flexmailer', 'FlexMailer', 'FlexMailer', 'flexmailer', 1);
+
+-- dev/core#3783 Recent Items providers
+INSERT INTO civicrm_option_group (`name`, `title`, `is_reserved`, `is_active`) VALUES ('recent_items_providers', {localize}'{ts escape="sql"}Recent Items Providers{/ts}'{/localize}, 1, 1);
+
+SELECT @option_group_id_recent := max(id) from civicrm_option_group where name = 'recent_items_providers';
+
+INSERT INTO civicrm_option_value (`option_group_id`, {localize field='label'}label{/localize}, `value`, `name`, `grouping`, `filter`, `is_default`, `weight`, {localize field='description'}description{/localize}, `is_optgroup`, `is_reserved`, `is_active`, `component_id`, `visibility_id`)
+ VALUES
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Contacts{/ts}'{/localize}, 'Contact', 'Contacts', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Relationships{/ts}'{/localize}, 'Relationship', 'Relationships', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Activities{/ts}'{/localize}, 'Activity', 'Activities', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Notes{/ts}'{/localize}, 'Note', 'Notes', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Groups{/ts}'{/localize}, 'Group', 'Groups', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Cases{/ts}'{/localize}, 'Case', 'Cases', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Contributions{/ts}'{/localize}, 'Contribution', 'Contributions', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Participants{/ts}'{/localize}, 'Participant', 'Participants', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Memberships{/ts}'{/localize}, 'Membership', 'Memberships', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Pledges{/ts}'{/localize}, 'Pledge', 'Pledges', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Events{/ts}'{/localize}, 'Event', 'Events', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+    (@option_group_id_recent, {localize}'{ts escape="sql"}Campaigns{/ts}'{/localize}, 'Campaign', 'Campaigns', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL);