Fixes for Activity import
[civicrm-core.git] / CRM / Activity / Import / Parser / Activity.php
index 5e95f929eeb393f43ac5ce37c3c1e97b823d38e1..cd186dbfb3186d8893e10824a6e83a0352974543 100644 (file)
@@ -23,8 +23,6 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
 
   protected $_mapperKeys;
 
-  private $_contactIdIndex;
-
   /**
    * Array of successfully imported activity id's
    *
@@ -32,33 +30,12 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
    */
   protected $_newActivity;
 
-  protected $_fileName;
-
-  /**
-   * Imported file size.
-   * @var int
-   */
-  protected $_fileSize;
-
-  /**
-   * Separator being used.
-   * @var string
-   */
-  protected $_separator;
-
   /**
    * Total number of lines in file.
    * @var int
    */
   protected $_lineCount;
 
-  /**
-   * Whether the file has a column header or not.
-   *
-   * @var bool
-   */
-  protected $_haveColumnHeader;
-
   /**
    * Class constructor.
    *
@@ -73,24 +50,9 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
    * The initializer code, called before the processing.
    */
   public function init() {
-    $activityContact = CRM_Activity_BAO_ActivityContact::import();
-    $activityTarget['target_contact_id'] = $activityContact['contact_id'];
-    $fields = array_merge(CRM_Activity_BAO_Activity::importableFields(),
-      $activityTarget
-    );
-
-    $fields = array_merge($fields, [
-      'source_contact_id' => [
-        'title' => ts('Source Contact'),
-        'headerPattern' => '/Source.Contact?/i',
-      ],
-      'activity_label' => [
-        'title' => ts('Activity Type Label'),
-        'headerPattern' => '/(activity.)?type label?/i',
-      ],
-    ]);
-
-    foreach ($fields as $name => $field) {
+    $this->setFieldMetadata();
+
+    foreach ($this->importableFieldsMetadata as $name => $field) {
       $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, '//');
@@ -103,20 +65,6 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
     $this->_newActivity = [];
 
     $this->setActiveFields($this->_mapperKeys);
-
-    // FIXME: we should do this in one place together with Form/MapField.php
-    $this->_contactIdIndex = -1;
-
-    $index = 0;
-    foreach ($this->_mapperKeys as $key) {
-      switch ($key) {
-        case 'target_contact_id':
-        case 'external_identifier':
-          $this->_contactIdIndex = $index;
-          break;
-      }
-      $index++;
-    }
   }
 
   /**
@@ -143,137 +91,126 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
   /**
    * Handle the values in import mode.
    *
-   * @param int $onDuplicate
-   *   The code for what action to take on duplicates.
    * @param array $values
    *   The array of values belonging to this line.
-   *
-   * @return int
-   *   CRM_Import_Parser::VALID for success or
-   *   CRM_Import_Parser::ERROR for error.
-   *
-   * @throws \CRM_Core_Exception
    */
-  public function import($onDuplicate, &$values) {
+  public function import($values) {
+    $rowNumber = (int) ($values[array_key_last($values)]);
     // First make sure this is a valid line
     try {
-      $this->validateValues($values);
-    }
-    catch (CRM_Core_Exception $e) {
-      return $this->addError($values, [$e->getMessage()]);
-    }
-    $params = $this->getApiReadyParams($values);
-    // For date-Formats.
-    $session = CRM_Core_Session::singleton();
-    $dateType = $session->get('dateTypes');
+      $params = $this->getMappedRow($values);
 
-    $customFields = CRM_Core_BAO_CustomField::getFields('Activity');
+      if (empty($params['external_identifier']) && empty($params['target_contact_id'])) {
 
-    foreach ($params as $key => $val) {
-      if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
-        if (!empty($customFields[$customFieldID]) && $customFields[$customFieldID]['data_type'] == 'Date') {
-          CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $params, $dateType, $key);
-        }
-        elseif (!empty($customFields[$customFieldID]) && $customFields[$customFieldID]['data_type'] == 'Boolean') {
-          $params[$key] = CRM_Utils_String::strtoboolstr($val);
-        }
-      }
-      elseif ($key === 'activity_date_time') {
-        $params[$key] = CRM_Utils_Date::formatDate($val, $dateType);
-      }
-      elseif ($key === 'activity_subject') {
-        $params['subject'] = $val;
-      }
-    }
+        // Retrieve contact id using contact dedupe rule.
+        // Since we are supporting only individual's activity import.
+        $params['contact_type'] = 'Individual';
+        $params['version'] = 3;
+        $matchedIDs = CRM_Contact_BAO_Contact::getDuplicateContacts($params, 'Individual');
 
-    if ($this->_contactIdIndex < 0) {
+        if (!empty($matchedIDs)) {
+          if (count($matchedIDs) > 1) {
+            throw new CRM_Core_Exception('Multiple matching contact records detected for this row. The activity was not imported');
+          }
+          $cid = $matchedIDs[0];
+          $params['target_contact_id'] = $cid;
+          $params['version'] = 3;
+          $newActivity = civicrm_api('activity', 'create', $params);
+          if (!empty($newActivity['is_error'])) {
+            throw new CRM_Core_Exception($newActivity['error_message']);
+          }
 
-      // Retrieve contact id using contact dedupe rule.
-      // Since we are supporting only individual's activity import.
-      $params['contact_type'] = 'Individual';
-      $params['version'] = 3;
-      $error = _civicrm_api3_deprecated_duplicate_formatted_contact($params);
+          $this->_newActivity[] = $newActivity['id'];
+          $this->setImportStatus($rowNumber, 'IMPORTED', '', $newActivity['id']);
+          return;
 
-      if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
-        $matchedIDs = explode(',', $error['error_message']['params'][0]);
-        if (count($matchedIDs) > 1) {
-          array_unshift($values, 'Multiple matching contact records detected for this row. The activity was not imported');
-          return CRM_Import_Parser::ERROR;
         }
-        $cid = $matchedIDs[0];
-        $params['target_contact_id'] = $cid;
-        $params['version'] = 3;
-        $newActivity = civicrm_api('activity', 'create', $params);
-        if (!empty($newActivity['is_error'])) {
-          array_unshift($values, $newActivity['error_message']);
-          return CRM_Import_Parser::ERROR;
+        // Using new Dedupe rule.
+        $ruleParams = [
+          'contact_type' => 'Individual',
+          'used' => 'Unsupervised',
+        ];
+        $fieldsArray = CRM_Dedupe_BAO_DedupeRule::dedupeRuleFields($ruleParams);
+
+        $disp = NULL;
+        foreach ($fieldsArray as $value) {
+          if (array_key_exists(trim($value), $params)) {
+            $paramValue = $params[trim($value)];
+            if (is_array($paramValue)) {
+              $disp .= $params[trim($value)][0][trim($value)] . " ";
+            }
+            else {
+              $disp .= $params[trim($value)] . " ";
+            }
+          }
         }
 
-        $this->_newActivity[] = $newActivity['id'];
-        return CRM_Import_Parser::VALID;
-
-      }
-      // Using new Dedupe rule.
-      $ruleParams = [
-        'contact_type' => 'Individual',
-        'used' => 'Unsupervised',
-      ];
-      $fieldsArray = CRM_Dedupe_BAO_DedupeRule::dedupeRuleFields($ruleParams);
-
-      $disp = NULL;
-      foreach ($fieldsArray as $value) {
-        if (array_key_exists(trim($value), $params)) {
-          $paramValue = $params[trim($value)];
-          if (is_array($paramValue)) {
-            $disp .= $params[trim($value)][0][trim($value)] . " ";
+        if (!empty($params['external_identifier'])) {
+          if ($disp) {
+            $disp .= "AND {$params['external_identifier']}";
           }
           else {
-            $disp .= $params[trim($value)] . " ";
+            $disp = $params['external_identifier'];
           }
         }
-      }
 
+        throw new CRM_Core_Exception('No matching Contact found for (' . $disp . ')');
+      }
       if (!empty($params['external_identifier'])) {
-        if ($disp) {
-          $disp .= "AND {$params['external_identifier']}";
+        $targetContactId = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact',
+          $params['external_identifier'], 'id', 'external_identifier'
+        );
+
+        if (!empty($params['target_contact_id']) &&
+          $params['target_contact_id'] != $targetContactId
+        ) {
+          throw new CRM_Core_Exception('Mismatch of External ID:' . $params['external_identifier'] . ' and Contact Id:' . $params['target_contact_id']);
+        }
+        if ($targetContactId) {
+          $params['target_contact_id'] = $targetContactId;
         }
         else {
-          $disp = $params['external_identifier'];
+          throw new CRM_Core_Exception('No Matching Contact for External ID:' . $params['external_identifier']);
         }
       }
 
-      array_unshift($values, 'No matching Contact found for (' . $disp . ')');
-      return CRM_Import_Parser::ERROR;
+      $params['version'] = 3;
+      $newActivity = civicrm_api('activity', 'create', $params);
+      if (!empty($newActivity['is_error'])) {
+        throw new CRM_Core_Exception($newActivity['error_message']);
+      }
     }
-    if (!empty($params['external_identifier'])) {
-      $targetContactId = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact',
-        $params['external_identifier'], 'id', 'external_identifier'
-      );
+    catch (CRM_Core_Exception $e) {
+      $this->setImportStatus($rowNumber, 'ERROR', $e->getMessage());
+      return;
+    }
+    $this->_newActivity[] = $newActivity['id'];
+    $this->setImportStatus($rowNumber, 'IMPORTED', '', $newActivity['id']);
+  }
 
-      if (!empty($params['target_contact_id']) &&
-        $params['target_contact_id'] != $targetContactId
-      ) {
-        array_unshift($values, 'Mismatch of External ID:' . $params['external_identifier'] . ' and Contact Id:' . $params['target_contact_id']);
-        return CRM_Import_Parser::ERROR;
-      }
-      if ($targetContactId) {
-        $params['target_contact_id'] = $targetContactId;
+  /**
+   * Get the row from the csv mapped to our parameters.
+   *
+   * @param array $values
+   *
+   * @return array
+   * @throws \API_Exception
+   */
+  public function getMappedRow(array $values): array {
+    $params = [];
+    foreach ($this->getFieldMappings() as $i => $mappedField) {
+      if ($mappedField['name'] === 'do_not_import') {
+        continue;
       }
-      else {
-        array_unshift($values, 'No Matching Contact for External ID:' . $params['external_identifier']);
-        return CRM_Import_Parser::ERROR;
+      if ($mappedField['name']) {
+        $fieldName = $this->getFieldMetadata($mappedField['name'])['name'];
+        if (in_array($mappedField['name'], ['target_contact_id', 'source_contact_id'])) {
+          $fieldName = $mappedField['name'];
+        }
+        $params[$fieldName] = $this->getTransformedFieldValue($mappedField['name'], $values[$i]);
       }
     }
-
-    $params['version'] = 3;
-    $newActivity = civicrm_api('activity', 'create', $params);
-    if (!empty($newActivity['is_error'])) {
-      array_unshift($values, $newActivity['error_message']);
-      return CRM_Import_Parser::ERROR;
-    }
-
-    $this->_newActivity[] = $newActivity['id'];
-    return CRM_Import_Parser::VALID;
+    return $params;
   }
 
   /**
@@ -292,6 +229,13 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
     return $row[$this->getFieldIndex($fieldName)] ?? NULL;
   }
 
+  /**
+   * @return array
+   */
+  protected function getRequiredFields(): array {
+    return [['activity_type_id' => ts('Activity Type'), 'activity_date_time' => ts('Activity Date')]];
+  }
+
   /**
    * Get the index for the given field.
    *
@@ -375,47 +319,12 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
     $errorMessage = NULL;
     // Checking error in custom data.
     $params['contact_type'] = 'Activity';
-    CRM_Contact_Import_Parser_Contact::isErrorInCustomData($params, $errorMessage);
+    $this->isErrorInCustomData($params, $errorMessage);
     if ($errorMessage) {
       throw new CRM_Core_Exception('Invalid value for field(s) : ' . $errorMessage);
     }
   }
 
-  /**
-   * @param array $values
-   *
-   * @throws \CRM_Core_Exception
-   */
-  protected function validateValues(array $values): void {
-    // Check required fields if this is not an update.
-    if (!$this->getFieldValue($values, 'activity_id')) {
-      if (!$this->getFieldValue($values, 'activity_label')
-        && !$this->getFieldValue($values, 'activity_type_id')) {
-        throw new CRM_Core_Exception(ts('Missing required fields: Activity type label or Activity type ID'));
-      }
-      if (!$this->getFieldValue($values, 'activity_date_time')) {
-        throw new CRM_Core_Exception(ts('Missing required fields'));
-      }
-    }
-
-    $this->validateActivityTypeIDAndLabel($values);
-    if ($this->getFieldValue($values, 'activity_date_time')
-      && !$this->isValidDate($this->getFieldValue($values, 'activity_date_time'))) {
-      throw new CRM_Core_Exception(ts('Invalid Activity Date'));
-    }
-
-    if ($this->getFieldValue($values, 'activity_engagement_level')
-      && !CRM_Utils_Rule::positiveInteger($this->getFieldValue($values, 'activity_engagement_level'))) {
-      throw new CRM_Core_Exception(ts('Activity Engagement Index'));
-    }
-
-    $targetContactID = $this->getFieldValue($values, 'target_contact_id');
-    if ($targetContactID && !$this->isValidContactID($targetContactID)) {
-      throw new CRM_Core_Exception("Invalid Contact ID: There is no contact record with contact_id = " . CRM_Utils_Type::escape($targetContactID, 'String'));
-    }
-    $this->validateCustomFields($values);
-  }
-
   /**
    * Get array of parameters formatted for the api from the submitted values.
    *
@@ -445,9 +354,6 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
    * @param int $onDuplicate
    * @param int $statusID
    * @param int $totalRowCount
-   *
-   * @return mixed
-   * @throws Exception
    */
   public function run(
     array $fileName,
@@ -459,29 +365,14 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
           $statusID = NULL,
           $totalRowCount = NULL
   ) {
-
-    $fileName = $fileName['name'];
-
     $this->init();
 
-    $this->_haveColumnHeader = $skipColumnHeader;
-
-    $this->_separator = $separator;
-
-    $fd = fopen($fileName, "r");
-    if (!$fd) {
-      return FALSE;
-    }
-
     $this->_lineCount = 0;
     $this->_invalidRowCount = $this->_validCount = 0;
     $this->_totalCount = 0;
 
     $this->_errors = [];
     $this->_warnings = [];
-
-    $this->_fileSize = number_format(filesize($fileName) / 1024.0, 2);
-
     if ($mode == self::MODE_MAPFIELD) {
       $this->_rows = [];
     }
@@ -493,33 +384,11 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
       $startTimestamp = $currTimestamp = $prevTimestamp = time();
     }
 
-    while (!feof($fd)) {
+    $dataSource = $this->getDataSourceObject();
+    $dataSource->setStatuses(['new']);
+    while ($row = $dataSource->getRow()) {
       $this->_lineCount++;
-
-      $values = fgetcsv($fd, 8192, $separator);
-      if (!$values) {
-        continue;
-      }
-
-      self::encloseScrub($values);
-
-      // skip column header if we're not in mapfield mode
-      if ($mode != self::MODE_MAPFIELD && $skipColumnHeader) {
-        $skipColumnHeader = FALSE;
-        continue;
-      }
-
-      // Trim whitespace around the values.
-
-      $empty = TRUE;
-      foreach ($values as $k => $v) {
-        $values[$k] = trim($v, " \t\r\n");
-      }
-
-      if (CRM_Utils_System::isNull($values)) {
-        continue;
-      }
-
+      $values = array_values($row);
       $this->_totalCount++;
 
       if ($mode == self::MODE_MAPFIELD) {
@@ -530,60 +399,11 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
         $returnCode = $this->summary($values);
       }
       elseif ($mode == self::MODE_IMPORT) {
-        $returnCode = $this->import($onDuplicate, $values);
+        $this->import($values);
         if ($statusID && (($this->_lineCount % 50) == 0)) {
           $prevTimestamp = $this->progressImport($statusID, FALSE, $startTimestamp, $prevTimestamp, $totalRowCount);
         }
       }
-      else {
-        $returnCode = self::ERROR;
-      }
-
-      // note that a line could be valid but still produce a warning
-      if ($returnCode & self::VALID) {
-        $this->_validCount++;
-        if ($mode == self::MODE_MAPFIELD) {
-          $this->_rows[] = $values;
-          $this->_activeFieldCount = max($this->_activeFieldCount, count($values));
-        }
-      }
-
-      if ($returnCode & self::ERROR) {
-        $this->_invalidRowCount++;
-        $recordNumber = $this->_lineCount;
-        if ($this->_haveColumnHeader) {
-          $recordNumber--;
-        }
-        array_unshift($values, $recordNumber);
-        $this->_errors[] = $values;
-      }
-
-      // if we are done processing the maxNumber of lines, break
-      if ($this->_maxLinesToProcess > 0 && $this->_validCount >= $this->_maxLinesToProcess) {
-        break;
-      }
-    }
-
-    fclose($fd);
-
-    if ($mode == self::MODE_PREVIEW || $mode == self::MODE_IMPORT) {
-      $customHeaders = $mapper;
-
-      $customfields = CRM_Core_BAO_CustomField::getFields('Activity');
-      foreach ($customHeaders as $key => $value) {
-        if ($id = CRM_Core_BAO_CustomField::getKeyID($value)) {
-          $customHeaders[$key] = $customfields[$id][0];
-        }
-      }
-      if ($this->_invalidRowCount) {
-        // removed view url for invlaid contacts
-        $headers = array_merge(
-          [ts('Line Number'), ts('Reason')],
-          $customHeaders
-        );
-        $this->_errorFileName = self::errorFileName(self::ERROR);
-        self::exportCSV($this->_errorFileName, $headers, $this->_errors);
-      }
     }
   }
 
@@ -632,28 +452,7 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
    *
    * @param CRM_Core_Session $store
    */
-  public function set($store) {
-    $store->set('fileSize', $this->_fileSize);
-    $store->set('lineCount', $this->_lineCount);
-    $store->set('separator', $this->_separator);
-    $store->set('fields', $this->getSelectValues());
-
-    $store->set('headerPatterns', $this->getHeaderPatterns());
-    $store->set('dataPatterns', $this->getDataPatterns());
-    $store->set('columnCount', $this->_activeFieldCount);
-
-    $store->set('totalRowCount', $this->_totalCount);
-    $store->set('validRowCount', $this->_validCount);
-    $store->set('invalidRowCount', $this->_invalidRowCount);
-
-    if ($this->_invalidRowCount) {
-      $store->set('errorsFileName', $this->_errorFileName);
-    }
-
-    if (isset($this->_rows) && !empty($this->_rows)) {
-      $store->set('dataValues', $this->_rows);
-    }
-  }
+  public function set($store) {}
 
   /**
    * Export data to a CSV file.
@@ -682,4 +481,25 @@ class CRM_Activity_Import_Parser_Activity extends CRM_Import_Parser {
     fclose($fd);
   }
 
+  /**
+   * Ensure metadata is loaded.
+   */
+  protected function setFieldMetadata(): void {
+    if (empty($this->importableFieldsMetadata)) {
+      $activityContact = CRM_Activity_BAO_ActivityContact::import();
+      $activityTarget['target_contact_id'] = $activityContact['contact_id'];
+      $fields = array_merge(CRM_Activity_BAO_Activity::importableFields(),
+        $activityTarget
+      );
+
+      $fields = array_merge($fields, [
+        'source_contact_id' => [
+          'title' => ts('Source Contact'),
+          'headerPattern' => '/Source.Contact?/i',
+        ],
+      ]);
+      $this->importableFieldsMetadata = $fields;
+    }
+  }
+
 }