Merge pull request #15542 from demeritcowboy/upgrade-convert-autoassignee-2
authorEileen McNaughton <emcnaughton@wikimedia.org>
Fri, 18 Oct 2019 23:23:21 +0000 (12:23 +1300)
committerGitHub <noreply@github.com>
Fri, 18 Oct 2019 23:23:21 +0000 (12:23 +1300)
Upgrade script to flip autoassignees using bidirectional relationship in older civicase configs

CRM/Upgrade/Incremental/php/FiveTwenty.php
tests/phpunit/CRM/Upgrade/Incremental/php/FiveTwentyTest.php [new file with mode: 0644]

index 9485ff8813f8a635c8be9e9cc10bb5f5da3bf6ee..ac9ccbfc683d1b0e4d63410a971389258157584b 100644 (file)
  * Upgrade logic for FiveTwenty */
 class CRM_Upgrade_Incremental_php_FiveTwenty extends CRM_Upgrade_Incremental_Base {
 
+  /**
+   * @var $relationshipTypes array
+   *   api call result keyed on relationship_type.id
+   */
+  protected static $relationshipTypes;
+
   /**
    * Compute any messages which should be displayed beforeupgrade.
    *
@@ -83,7 +89,101 @@ class CRM_Upgrade_Incremental_php_FiveTwenty extends CRM_Upgrade_Incremental_Bas
       "tinyint(4) DEFAULT '0' COMMENT 'Shows this is a template for recurring contributions.'", FALSE, '5.20.alpha1');
     $this->addTask('Add order_reference field to civicrm_financial_trxn', 'addColumn', 'civicrm_financial_trxn', 'order_reference',
       "varchar(255) COMMENT 'Payment Processor external order reference'", FALSE, '5.20.alpha1');
+    $config = CRM_Core_Config::singleton();
+    if (in_array('CiviCase', $config->enableComponents)) {
+      $this->addTask('Change direction of autoassignees in case type xml', 'changeCaseTypeAutoassignee');
+    }
     $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
   }
 
+  /**
+   * Change direction of activity autoassignees in case type xml for
+   * bidirectional relationship types if they point the other way. This is
+   * mostly a visual issue on the case type edit screen and doesn't affect
+   * normal operation, but could lead to confusion and a future mixup.
+   * (dev/core#1046)
+   * ONLY for ones using database storage - don't want to "fork" case types
+   * that aren't currently forked.
+   *
+   * Earlier iterations of this used the api and array manipulation
+   * and then another iteration used SimpleXML manipulation, but both
+   * suffered from weirdnesses in how conversion back and forth worked.
+   *
+   * Here we use SQL and a regex. The thing we're changing is pretty
+   * well-defined and unique:
+   * <default_assignee_relationship>N_b_a</default_assignee_relationship>
+   *
+   * @return bool
+   */
+  public static function changeCaseTypeAutoassignee() {
+    self::$relationshipTypes = civicrm_api3('RelationshipType', 'get', [
+      'options' => ['limit' => 0],
+    ])['values'];
+
+    // Get all case types definitions that are using db storage
+    $dao = CRM_Core_DAO::executeQuery("SELECT id, definition FROM civicrm_case_type WHERE definition IS NOT NULL AND definition <> ''");
+    while ($dao->fetch()) {
+      self::processCaseTypeAutoassignee($dao->id, $dao->definition);
+    }
+    return TRUE;
+  }
+
+  /**
+   * Process a single case type
+   *
+   * @param $caseTypeId int
+   * @param $definition string
+   *   xml string
+   */
+  public static function processCaseTypeAutoassignee($caseTypeId, $definition) {
+    $isDirty = FALSE;
+    // find the autoassignees
+    preg_match_all('/<default_assignee_relationship>(.*?)<\/default_assignee_relationship>/', $definition, $matches);
+    // $matches[1][n] has the text inside the xml tag, e.g. 2_a_b
+    foreach ($matches[1] as $index => $match) {
+      if (empty($match)) {
+        continue;
+      }
+      // parse out existing id and direction
+      list($relationshipTypeId, $direction1) = explode('_', $match);
+      // we only care about ones that are b_a
+      if ($direction1 === 'b') {
+        // we only care about bidirectional
+        if (self::isBidirectionalRelationship($relationshipTypeId)) {
+          // flip it to be a_b
+          // $matches[0][n] has the whole match including the xml tag
+          $definition = str_replace($matches[0][$index], "<default_assignee_relationship>{$relationshipTypeId}_a_b</default_assignee_relationship>", $definition);
+          $isDirty = TRUE;
+        }
+      }
+    }
+
+    if ($isDirty) {
+      $sqlParams = [
+        1 => [$definition, 'String'],
+        2 => [$caseTypeId, 'Integer'],
+      ];
+      CRM_Core_DAO::executeQuery("UPDATE civicrm_case_type SET definition = %1 WHERE id = %2", $sqlParams);
+      //echo "UPDATE civicrm_case_type SET definition = '" . CRM_Core_DAO::escapeString($sqlParams[1][0]) . "' WHERE id = {$sqlParams[2][0]}\n";
+    }
+  }
+
+  /**
+   * Check if this is bidirectional, based on label. In the situation where
+   * we're using this we don't care too much about the edge case where name
+   * might not also be bidirectional.
+   *
+   * @param $relationshipTypeId int
+   *
+   * @return bool
+   */
+  private static function isBidirectionalRelationship($relationshipTypeId) {
+    if (isset(self::$relationshipTypes[$relationshipTypeId])) {
+      if (self::$relationshipTypes[$relationshipTypeId]['label_a_b'] === self::$relationshipTypes[$relationshipTypeId]['label_b_a']) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
 }
diff --git a/tests/phpunit/CRM/Upgrade/Incremental/php/FiveTwentyTest.php b/tests/phpunit/CRM/Upgrade/Incremental/php/FiveTwentyTest.php
new file mode 100644 (file)
index 0000000..5cbf00f
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+require_once 'CiviTest/CiviCaseTestCase.php';
+
+/**
+ * Class CRM_Upgrade_Incremental_php_FiveTwentyTest
+ * @group headless
+ */
+class CRM_Upgrade_Incremental_php_FiveTwentyTest extends CiviCaseTestCase {
+
+  public function setUp() {
+    parent::setUp();
+  }
+
+  /**
+   * Test that the upgrade task changes the direction but only
+   * for bidirectional relationship types that are b_a.
+   */
+  public function testChangeCaseTypeAutoassignee() {
+
+    // We don't know what the ids are for the relationship types since it
+    // seems to depend what ran before us, so retrieve them first and go by
+    // name.
+    // Also spouse might not exist.
+
+    $result = $this->callAPISuccess('RelationshipType', 'get', ['limit' => 0])['values'];
+    // Get list of ids keyed on name.
+    $relationshipTypeNames = array_column($result, 'id', 'name_b_a');
+
+    // Create spouse if none.
+    if (!isset($relationshipTypeNames['Spouse of'])) {
+      $spouseId = $this->relationshipTypeCreate([
+        'name_a_b' => 'Spouse of',
+        'name_b_a' => 'Spouse of',
+        'label_a_b' => 'Spouse of',
+        'label_b_a' => 'Spouse of',
+        'contact_type_a' => 'Individual',
+        'contact_type_b' => 'Individual',
+      ]);
+      $relationshipTypeNames['Spouse of'] = $spouseId;
+    }
+    // Maybe unnecessary but why not. Slightly different than an undefined
+    // index later when it doesn't exist at all.
+    $this->assertGreaterThan(0, $relationshipTypeNames['Spouse of']);
+
+    /**
+     * Set up xml.
+     * In this sample case type for autoassignees we have:
+     * - a_b unidirectional: Case Coordinator is
+     * - b_a unidirectional: Benefits Specialist
+     * - Bidirectional the way it's stored in 5.16+: Spouse of
+     * - config entry pre-5.16, where it's a bidirectional relationship stored as b_a: Spouse of
+     *
+     * Also just for extra some non-ascii chars in here to show they
+     * don't get borked.
+     */
+    $newCaseTypeXml = <<<ENDXML
+<?xml version="1.0" encoding="utf-8" ?>
+
+<CaseType>
+<name>test_type</name>
+<ActivityTypes>
+<ActivityType>
+<name>Open Case</name>
+<max_instances>1</max_instances>
+</ActivityType>
+<ActivityType>
+<name>Email</name>
+</ActivityType>
+<ActivityType>
+<name>Follow up</name>
+</ActivityType>
+<ActivityType>
+<name>Meeting</name>
+</ActivityType>
+<ActivityType>
+<name>Phone Call</name>
+</ActivityType>
+<ActivityType>
+<name>давид</name>
+</ActivityTypes>
+<ActivitySets>
+<ActivitySet>
+<name>standard_timeline</name>
+<label>Standard Timeline</label>
+<timeline>true</timeline>
+<ActivityTypes>
+<ActivityType>
+<name>Open Case</name>
+<status>Completed</status>
+<label>Open Case</label>
+<default_assignee_type>1</default_assignee_type>
+</ActivityType>
+</ActivityTypes>
+</ActivitySet>
+<ActivitySet>
+<name>timeline_1</name>
+<label>AnotherTimeline</label>
+<timeline>true</timeline>
+<ActivityTypes>
+<ActivityType>
+<name>Follow up</name>
+<label>Follow up</label>
+<status>Scheduled</status>
+<reference_activity>Open Case</reference_activity>
+<reference_offset>7</reference_offset>
+<reference_select>newest</reference_select>
+<default_assignee_type>2</default_assignee_type>
+<default_assignee_relationship>{$relationshipTypeNames['Senior Services Coordinator']}_b_a</default_assignee_relationship>
+<default_assignee_contact></default_assignee_contact>
+</ActivityType>
+<ActivityType>
+<name>Follow up</name>
+<label>Follow up</label>
+<status>Scheduled</status>
+<reference_activity>Open Case</reference_activity>
+<reference_offset>14</reference_offset>
+<reference_select>newest</reference_select>
+<default_assignee_type>2</default_assignee_type>
+<default_assignee_relationship>{$relationshipTypeNames['Benefits Specialist']}_a_b</default_assignee_relationship>
+<default_assignee_contact></default_assignee_contact>
+</ActivityType>
+<ActivityType>
+<name>Follow up</name>
+<label>Follow up</label>
+<status>Scheduled</status>
+<reference_activity>Open Case</reference_activity>
+<reference_offset>21</reference_offset>
+<reference_select>newest</reference_select>
+<default_assignee_type>2</default_assignee_type>
+<default_assignee_relationship>{$relationshipTypeNames['Spouse of']}_a_b</default_assignee_relationship>
+<default_assignee_contact></default_assignee_contact>
+</ActivityType>
+<ActivityType>
+<name>Follow up</name>
+<label>Follow up</label>
+<status>Scheduled</status>
+<reference_activity>Open Case</reference_activity>
+<reference_offset>28</reference_offset>
+<reference_select>newest</reference_select>
+<default_assignee_type>2</default_assignee_type>
+<default_assignee_relationship>{$relationshipTypeNames['Spouse of']}_b_a</default_assignee_relationship>
+<default_assignee_contact></default_assignee_contact>
+</ActivityType>
+</ActivityTypes>
+</ActivitySet>
+</ActivitySets>
+<CaseRoles>
+<RelationshipType>
+<name>Senior Services Coordinator</name>
+<creator>1</creator>
+<manager>1</manager>
+</RelationshipType>
+<RelationshipType>
+<name>Spouse of</name>
+</RelationshipType>
+<RelationshipType>
+<name>Benefits Specialist is</name>
+</RelationshipType>
+</CaseRoles>
+<RestrictActivityAsgmtToCmsUser>0</RestrictActivityAsgmtToCmsUser>
+</CaseType>
+ENDXML;
+
+    $dao = new CRM_Case_DAO_CaseType();
+    $dao->name = 'test_type';
+    $dao->title = 'Test Type';
+    $dao->is_active = 1;
+    $dao->definition = $newCaseTypeXml;
+    $dao->insert();
+
+    $caseTypeId = $dao->id;
+
+    // run the task
+    $upgrader = new CRM_Upgrade_Incremental_php_FiveTwenty();
+    $upgrader->changeCaseTypeAutoassignee();
+
+    // Check if the case type is what we expect. It should be identical except
+    // the b_a spouse one should get converted to the a_b direction.
+    $expectedCaseTypeXml = str_replace(
+      "<default_assignee_relationship>{$relationshipTypeNames['Spouse of']}_b_a",
+      "<default_assignee_relationship>{$relationshipTypeNames['Spouse of']}_a_b",
+      $newCaseTypeXml
+    );
+
+    //echo $expectedCaseTypeXml;
+
+    // Get the updated case type and check.
+    $dao = CRM_Core_DAO::executeQuery("SELECT definition FROM civicrm_case_type WHERE id = {$caseTypeId}");
+    $dao->fetch();
+
+    $this->assertEquals($expectedCaseTypeXml, $dao->definition);
+  }
+
+}