Fix mishandling of renamed membership status labels on membership import
authoreileen <emcnaughton@wikimedia.org>
Wed, 31 Jul 2019 22:04:07 +0000 (10:04 +1200)
committereileen <emcnaughton@wikimedia.org>
Thu, 1 Aug 2019 01:53:18 +0000 (13:53 +1200)
CRM/Contribute/Import/Parser/Contribution.php
CRM/Core/PseudoConstant.php
CRM/Import/Parser.php
CRM/Member/Import/Parser/Membership.php
CRM/Member/PseudoConstant.php
tests/phpunit/CRM/Member/Import/Parser/MembershipTest.php

index b6c801431f57115bf89d3670ebbff7b0c8c56725..82eb9ef4217242f80e3a8b36f8ab10a6009c5161 100644 (file)
@@ -1011,18 +1011,7 @@ class CRM_Contribute_Import_Parser_Contribution extends CRM_Contribute_Import_Pa
           ) {
             $realField = $fields[$key]['is_pseudofield_for'] ?? $key;
             $realFieldSpec = $fields[$realField];
-            /* @var \CRM_Core_DAO $bao */
-            $bao = $realFieldSpec['bao'];
-            // Get names & labels - we will try to match name first but if not available then see if
-            // we have a label that can be converted to a name.
-            // For historical reasons use validate as context - ie disabled name matches ARE permitted per prior to change.
-            $nameOptions = $bao::buildOptions($realField, 'validate');
-            if (!isset($nameOptions[$value])) {
-              $labelOptions = array_flip($bao::buildOptions($realField, 'match'));
-              if (isset($labelOptions[$params[$key]])) {
-                $values[$key] = $labelOptions[$params[$key]];
-              }
-            }
+            $values[$key] = $this->parsePseudoConstantField($value, $realFieldSpec);
           }
           break;
       }
index df29c7f3f5154c47aa08fc3310906e04af617b2b..96a6a864206a61999e990ee0764bf0d2f69cef15 100644 (file)
@@ -300,8 +300,8 @@ class CRM_Core_PseudoConstant {
         $cacheKey = $daoName . $fieldName . serialize($params);
 
         // Retrieve cached options
-        if (isset(self::$cache[$cacheKey]) && empty($params['fresh'])) {
-          $output = self::$cache[$cacheKey];
+        if (isset(\Civi::$statics[__CLASS__][$cacheKey]) && empty($params['fresh'])) {
+          $output = \Civi::$statics[__CLASS__][$cacheKey];
         }
         else {
           $daoName = CRM_Core_DAO_AllCoreTables::getClassForTable($pseudoconstant['table']);
@@ -386,7 +386,7 @@ class CRM_Core_PseudoConstant {
             }
           }
           CRM_Utils_Hook::fieldOptions($entity, $fieldName, $output, $params);
-          self::$cache[$cacheKey] = $output;
+          \Civi::$statics[__CLASS__][$cacheKey] = $output;
         }
         return $flip ? array_flip($output) : $output;
       }
index cf01fdd054821daa1bdca2506520ea979d334f2e..6fab5498b9c4fa4762164f930e0fe0a88704aaee 100644 (file)
@@ -520,4 +520,29 @@ abstract class CRM_Import_Parser {
     return $error;
   }
 
+  /**
+   * Parse a field which could be represented by a label or name value rather than the DB value.
+   *
+   * We will try to match name first but if not available then see if we have a label that can be converted to a name.
+   *
+   * @param string|int|null $submittedValue
+   * @param array $fieldSpec
+   *   Metadata for the field
+   *
+   * @return mixed
+   */
+  protected function parsePseudoConstantField($submittedValue, $fieldSpec) {
+    /* @var \CRM_Core_DAO $bao */
+    $bao = $fieldSpec['bao'];
+    // For historical reasons use validate as context - ie disabled name matches ARE permitted.
+    $nameOptions = $bao::buildOptions($fieldSpec['name'], 'validate');
+    if (!isset($nameOptions[$submittedValue])) {
+      $labelOptions = array_flip($bao::buildOptions($fieldSpec['name'], 'match'));
+      if (isset($labelOptions[$submittedValue])) {
+        return array_search($labelOptions[$submittedValue], $nameOptions, TRUE);
+      }
+    }
+    return '';
+  }
+
 }
index 2501ba9d0157245dd0c109523ce610657f7bb926..2cea95d33e141499a52f37f4019e7e2355343ce2 100644 (file)
@@ -48,6 +48,13 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
   private $_membershipTypeIndex;
   private $_membershipStatusIndex;
 
+  /**
+   * Array of metadata for all available fields.
+   *
+   * @var array
+   */
+  protected $fieldMetadata = [];
+
   /**
    * Array of successfully imported membership id's
    *
@@ -59,12 +66,10 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
    * Class constructor.
    *
    * @param $mapperKeys
-   * @param null $mapperLocType
-   * @param null $mapperPhoneType
    */
-  public function __construct(&$mapperKeys, $mapperLocType = NULL, $mapperPhoneType = NULL) {
+  public function __construct($mapperKeys) {
     parent::__construct();
-    $this->_mapperKeys = &$mapperKeys;
+    $this->_mapperKeys = $mapperKeys;
   }
 
   /**
@@ -73,9 +78,10 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
    * @return void
    */
   public function init() {
-    $fields = CRM_Member_BAO_Membership::importableFields($this->_contactType, FALSE);
+    $this->fieldMetadata = CRM_Member_BAO_Membership::importableFields($this->_contactType, FALSE);
 
-    foreach ($fields as $name => $field) {
+    foreach ($this->fieldMetadata as $name => $field) {
+      // @todo - we don't really need to do all this.... fieldMetadata is just fine to use as is.
       $field['type'] = CRM_Utils_Array::value('type', $field, CRM_Utils_Type::T_INT);
       $field['dataPattern'] = CRM_Utils_Array::value('dataPattern', $field, '//');
       $field['headerPattern'] = CRM_Utils_Array::value('headerPattern', $field, '//');
@@ -223,6 +229,7 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
             break;
 
           case 'membership_type_id':
+            // @todo - squish into membership status - can use same lines here too.
             $membershipTypes = CRM_Member_PseudoConstant::membershipType();
             if (!CRM_Utils_Array::crmInArray($val, $membershipTypes) &&
               !array_key_exists($val, $membershipTypes)
@@ -232,7 +239,7 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
             break;
 
           case 'status_id':
-            if (!CRM_Utils_Array::crmInArray($val, CRM_Member_PseudoConstant::membershipStatus())) {
+            if (!empty($val) && !$this->parsePseudoConstantField($val, $this->fieldMetadata[$key])) {
               CRM_Contact_Import_Parser_Contact::addToErrorMsg('Membership Status', $errorMessage);
             }
             break;
@@ -324,10 +331,8 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
               break;
 
             case 'status_id':
-              if (!is_numeric($val)) {
-                unset($params['status_id']);
-                $params['membership_status'] = $val;
-              }
+              // @todo - we can do this based on the presence of 'pseudoconstant' in the metadata rather than field specific.
+              $params[$key] = $this->parsePseudoConstantField($val, $this->fieldMetadata[$key]);
               break;
 
             case 'is_override':
@@ -347,12 +352,6 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
       }
       //date-Format part ends
 
-      static $indieFields = NULL;
-      if ($indieFields == NULL) {
-        $tempIndieFields = CRM_Member_DAO_Membership::import();
-        $indieFields = $tempIndieFields;
-      }
-
       $formatValues = [];
       foreach ($params as $key => $field) {
         if ($field == NULL || $field === '') {
@@ -721,30 +720,6 @@ class CRM_Member_Import_Parser_Membership extends CRM_Member_Import_Parser {
           $values['membership_type_id'] = $membershipTypeId;
           break;
 
-        case 'status_id':
-          if (!CRM_Utils_Array::value($value, CRM_Member_PseudoConstant::membershipStatus())) {
-            throw new Exception('Invalid Membership Status Id');
-          }
-          $values[$key] = $value;
-          break;
-
-        case 'membership_status':
-          $membershipStatusId = CRM_Utils_Array::key(ucfirst($value),
-            CRM_Member_PseudoConstant::membershipStatus()
-          );
-          if ($membershipStatusId) {
-            if (!empty($values['status_id']) &&
-              $membershipStatusId != $values['status_id']
-            ) {
-              throw new Exception('Mismatched membership Status and Membership Status Id');
-            }
-          }
-          else {
-            throw new Exception('Invalid Membership Status');
-          }
-          $values['status_id'] = $membershipStatusId;
-          break;
-
         default:
           break;
       }
index 417de01f0f779282a68383510577e57f1730710f..dedf38aaaf0da074d9d5a418a903ee4e0333335e 100644 (file)
@@ -132,6 +132,9 @@ class CRM_Member_PseudoConstant extends CRM_Core_PseudoConstant {
     if (isset(self::$$name)) {
       self::$$name = NULL;
     }
+    // The preferred source of membership pseudoconstants is in fact the Core class.
+    // which buildOptions accesses - better flush that too.
+    CRM_Core_PseudoConstant::flush();
   }
 
 }
index 4585ab9e771405bd45ee5f42763e13711fd26b04..06c2c2c738595e188b5fb77d30befe18ab9f4154 100644 (file)
@@ -85,6 +85,8 @@ class CRM_Member_Import_Parser_MembershipTest extends CiviUnitTestCase {
   /**
    * Tears down the fixture, for example, closes a network connection.
    * This method is called after a test is executed.
+   *
+   * @throws \CRM_Core_Exception
    */
   public function tearDown() {
     $tablesToTruncate = [
@@ -92,12 +94,12 @@ class CRM_Member_Import_Parser_MembershipTest extends CiviUnitTestCase {
       'civicrm_membership_log',
       'civicrm_contribution',
       'civicrm_membership_payment',
+      'civicrm_contact',
     ];
-    $this->quickCleanup($tablesToTruncate);
+    $this->quickCleanup($tablesToTruncate, TRUE);
     $this->relationshipTypeDelete($this->_relationshipTypeId);
     $this->membershipTypeDelete(['id' => $this->_membershipTypeID]);
     $this->membershipStatusDelete($this->_mebershipStatusID);
-    $this->contactDelete($this->_orgContactID);
   }
 
   /**
@@ -171,19 +173,20 @@ class CRM_Member_Import_Parser_MembershipTest extends CiviUnitTestCase {
     $this->assertContains('Required parameter missing: Status', $importValues);
   }
 
+  /**
+   * Test that the passed in status is respected.
+   *
+   * @throws \CRM_Core_Exception
+   */
   public function testImportOverriddenMembershipWithStatus() {
     $this->individualCreate(['email' => 'anthony_anderson3@civicrm.org']);
-
-    $fieldMapper = [
-      'mapper[0][0]' => 'email',
-      'mapper[1][0]' => 'membership_type_id',
-      'mapper[2][0]' => 'membership_start_date',
-      'mapper[3][0]' => 'is_override',
-      'mapper[4][0]' => 'status_id',
-    ];
-    $membershipImporter = new CRM_Member_Import_Parser_Membership($fieldMapper);
-    $membershipImporter->init();
-    $membershipImporter->_contactType = 'Individual';
+    $membershipImporter = $this->createImportObject([
+      'email',
+      'membership_type_id',
+      'membership_start_date',
+      'is_override',
+      'status_id',
+    ]);
 
     $importValues = [
       'anthony_anderson3@civicrm.org',
@@ -254,4 +257,64 @@ class CRM_Member_Import_Parser_MembershipTest extends CiviUnitTestCase {
     $this->assertContains('Required parameter missing: Status', $importValues);
   }
 
+  /**
+   * Test that memberships can still be imported if the status is renamed.
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public function testImportMembershipWithRenamedStatus() {
+    $this->individualCreate(['email' => 'anthony_anderson3@civicrm.org']);
+
+    $this->callAPISuccess('MembershipStatus', 'get', [
+      'name' => 'New',
+      'api.MembershipStatus.create' => [
+        'label' => 'New-renamed',
+      ],
+    ]);
+    $membershipImporter = $this->createImportObject([
+      'email',
+      'membership_type_id',
+      'membership_start_date',
+      'is_override',
+      'status_id',
+    ]);
+
+    $importValues = [
+      'anthony_anderson3@civicrm.org',
+      $this->_membershipTypeID,
+      date('Y-m-d'),
+      TRUE,
+      'New-renamed',
+    ];
+
+    $importResponse = $membershipImporter->import(CRM_Import_Parser::DUPLICATE_UPDATE, $importValues);
+    $this->assertEquals(CRM_Import_Parser::VALID, $importResponse);
+    $createdStatusID = $this->callAPISuccessGetValue('Membership', ['return' => 'status_id']);
+    $this->assertEquals(CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'New'), $createdStatusID);
+    $this->callAPISuccess('MembershipStatus', 'get', [
+      'name' => 'New',
+      'api.MembershipStatus.create' => [
+        'label' => 'New',
+      ],
+    ]);
+  }
+
+  /**
+   * Create an import object.
+   *
+   * @param array $fields
+   *
+   * @return \CRM_Member_Import_Parser_Membership
+   */
+  protected function createImportObject(array $fields): \CRM_Member_Import_Parser_Membership {
+    $fieldMapper = [];
+    foreach ($fields as $index => $field) {
+      $fieldMapper['mapper[' . $index . '][0]'] = $field;
+    }
+    $membershipImporter = new CRM_Member_Import_Parser_Membership($fieldMapper);
+    $membershipImporter->init();
+    $membershipImporter->_contactType = 'Individual';
+    return $membershipImporter;
+  }
+
 }