* 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.
*
"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;
+ }
+
}
--- /dev/null
+<?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);
+ }
+
+}