From 4f54a1843794d9b8a12c61df2b2db8f4c29f6a7b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ren=C3=A9=20Olivo?=
Date: Tue, 22 May 2018 16:51:23 -0400
Subject: [PATCH] dev/core#107 Refactor selection of default assignee by
relationship type
---
CRM/Case/XMLProcessor/Process.php | 50 +++++-
ang/crmCaseType.js | 23 +++
ang/crmCaseType/timelineTable.html | 2 +-
tests/karma/unit/crmCaseTypeSpec.js | 32 ++++
.../CRM/Case/XMLProcessor/ProcessTest.php | 167 +++++++++++++-----
5 files changed, 217 insertions(+), 57 deletions(-)
diff --git a/CRM/Case/XMLProcessor/Process.php b/CRM/Case/XMLProcessor/Process.php
index 9bd6e0b555bc..1929c38fb729 100644
--- a/CRM/Case/XMLProcessor/Process.php
+++ b/CRM/Case/XMLProcessor/Process.php
@@ -640,29 +640,65 @@ protected function getDefaultAssigneeOptionValues() {
* @return int|null the ID of the default assignee contact or null if none.
*/
protected function getDefaultAssigneeByRelationship($activityParams, $activityTypeXML) {
- if (!isset($activityTypeXML->default_assignee_relationship)) {
+ $isDefaultRelationshipDefined = isset($activityTypeXML->default_assignee_relationship)
+ && preg_match('/\d+_[ab]_[ab]/', $activityTypeXML->default_assignee_relationship);
+
+ if (!$isDefaultRelationshipDefined) {
return NULL;
}
$targetContactId = is_array($activityParams['target_contact_id'])
? CRM_Utils_Array::first($activityParams['target_contact_id'])
: $activityParams['target_contact_id'];
+ list($relTypeId, $a, $b) = explode('_', $activityTypeXML->default_assignee_relationship);
- $relationships = civicrm_api3('Relationship', 'get', [
- 'contact_id_b' => $targetContactId,
- 'relationship_type_id.name_b_a' => (string) $activityTypeXML->default_assignee_relationship,
+ $params = [
+ 'relationship_type_id' => $relTypeId,
+ "contact_id_$b" => $targetContactId,
'is_active' => 1,
- 'sequential' => 1,
- ]);
+ ];
+
+ if ($this->isBidirectionalRelationshipType($relTypeId)) {
+ $params["contact_id_$a"] = $targetContactId;
+ $params['options']['or'] = [['contact_id_a', 'contact_id_b']];
+ }
+
+ $relationships = civicrm_api3('Relationship', 'get', $params);
if ($relationships['count']) {
- return $relationships['values'][0]['contact_id_a'];
+ $relationship = CRM_Utils_Array::first($relationships['values']);
+
+ // returns the contact id on the other side of the relationship:
+ return (int) $relationship['contact_id_a'] === (int) $targetContactId
+ ? $relationship['contact_id_b']
+ : $relationship['contact_id_a'];
}
else {
return NULL;
}
}
+ /**
+ * Determines if the given relationship type is bidirectional or not by
+ * comparing their labels.
+ *
+ * @return bool
+ */
+ protected function isBidirectionalRelationshipType($relationshipTypeId) {
+ $relationshipTypeResult = civicrm_api3('RelationshipType', 'get', [
+ 'id' => $relationshipTypeId,
+ 'options' => ['limit' => 1]
+ ]);
+
+ if ($relationshipTypeResult['count'] === 0) {
+ return FALSE;
+ }
+
+ $relationshipType = CRM_Utils_Array::first($relationshipTypeResult['values']);
+
+ return $relationshipType['label_b_a'] === $relationshipType['label_a_b'];
+ }
+
/**
* Returns the activity's default assignee for a specific contact if the contact exists,
* otherwise returns null.
diff --git a/ang/crmCaseType.js b/ang/crmCaseType.js
index f7667f809690..22aaa5d35f2d 100644
--- a/ang/crmCaseType.js
+++ b/ang/crmCaseType.js
@@ -264,11 +264,34 @@
$scope.relationshipTypeOptions = _.map(apiCalls.relTypes.values, function(type) {
return {id: type[REL_TYPE_CNAME], text: type.label_b_a};
});
+ $scope.defaultRelationshipTypeOptions = getDefaultRelationshipTypeOptions();
// stores the default assignee values indexed by their option name:
$scope.defaultAssigneeTypeValues = _.chain($scope.defaultAssigneeTypes)
.indexBy('name').mapValues('value').value();
}
+ /// Returns the default relationship type options. If the relationship is
+ /// bidirectional (Ex: Spouse of) it adds a single option otherwise it adds
+ /// two options representing the relationship type directions
+ /// (Ex: Employee of, Employer is)
+ function getDefaultRelationshipTypeOptions() {
+ return _.transform(apiCalls.relTypes.values, function(result, relType) {
+ var isBidirectionalRelationship = relType.label_a_b === relType.label_b_a;
+
+ result.push({
+ label: relType.label_b_a,
+ value: relType.id + '_b_a'
+ });
+
+ if (!isBidirectionalRelationship) {
+ result.push({
+ label: relType.label_a_b,
+ value: relType.id + '_a_b'
+ });
+ }
+ }, []);
+ }
+
/// initializes the case type object
function initCaseType() {
var isNewCaseType = !apiCalls.caseType;
diff --git a/ang/crmCaseType/timelineTable.html b/ang/crmCaseType/timelineTable.html
index 70c7c343cc84..a67787ae99bb 100644
--- a/ang/crmCaseType/timelineTable.html
+++ b/ang/crmCaseType/timelineTable.html
@@ -76,7 +76,7 @@
ui-jq="select2"
ui-options="{dropdownAutoWidth: true}"
ng-model="activity.default_assignee_relationship"
- ng-options="option.id as option.text for option in relationshipTypeOptions"
+ ng-options="option.value as option.label for option in defaultRelationshipTypeOptions"
required
>
diff --git a/tests/karma/unit/crmCaseTypeSpec.js b/tests/karma/unit/crmCaseTypeSpec.js
index eb13d20f6b49..3369579c19bd 100644
--- a/tests/karma/unit/crmCaseTypeSpec.js
+++ b/tests/karma/unit/crmCaseTypeSpec.js
@@ -188,6 +188,18 @@ describe('crmCaseType', function() {
"contact_type_b": "Individual",
"is_reserved": "0",
"is_active": "1"
+ },
+ {
+ "id": "2",
+ "name_a_b": "Spouse of",
+ "label_a_b": "Spouse of",
+ "name_b_a": "Spouse of",
+ "label_b_a": "Spouse of",
+ "description": "Spousal relationship.",
+ "contact_type_a": "Individual",
+ "contact_type_b": "Individual",
+ "is_reserved": "0",
+ "is_active": "1"
}
]
},
@@ -314,6 +326,26 @@ describe('crmCaseType', function() {
expect(scope.defaultAssigneeTypeValues).toEqual(defaultAssigneeTypeValues);
});
+ it('should store the default assignee relationship type options', function() {
+ var defaultRelationshipTypeOptions = _.transform(apiCalls.relTypes.values, function(result, relType) {
+ var isBidirectionalRelationship = relType.label_a_b === relType.label_b_a;
+
+ result.push({
+ label: relType.label_b_a,
+ value: relType.id + '_b_a'
+ });
+
+ if (!isBidirectionalRelationship) {
+ result.push({
+ label: relType.label_a_b,
+ value: relType.id + '_a_b'
+ });
+ }
+ }, []);
+
+ expect(scope.defaultRelationshipTypeOptions).toEqual(defaultRelationshipTypeOptions);
+ });
+
it('addActivitySet should add an activitySet to the case type', function() {
scope.addActivitySet('timeline');
var activitySets = scope.caseType.definition.activitySets;
diff --git a/tests/phpunit/CRM/Case/XMLProcessor/ProcessTest.php b/tests/phpunit/CRM/Case/XMLProcessor/ProcessTest.php
index 52413195309f..2d30c0bc82f2 100644
--- a/tests/phpunit/CRM/Case/XMLProcessor/ProcessTest.php
+++ b/tests/phpunit/CRM/Case/XMLProcessor/ProcessTest.php
@@ -11,29 +11,31 @@ public function setUp() {
parent::setUp();
$this->defaultAssigneeOptionsValues = [];
- $this->assigneeContactId = $this->individualCreate();
- $this->targetContactId = $this->individualCreate();
- $this->setUpDefaultAssigneeOptions();
- $this->setUpRelationship();
-
- $activityTypeXml = 'Open Case';
- $this->activityTypeXml = new SimpleXMLElement($activityTypeXml);
- $this->params = [
- 'activity_date_time' => date('Ymd'),
- 'caseID' => $this->caseTypeId,
- 'clientID' => $this->targetContactId,
- 'creatorID' => $this->_loggedInUser,
- ];
+ $this->setupContacts();
+ $this->setupDefaultAssigneeOptions();
+ $this->setupRelationships();
+ $this->setupActivityDefinitions();
$this->process = new CRM_Case_XMLProcessor_Process();
}
+ /**
+ * Creates sample contacts.
+ */
+ protected function setUpContacts() {
+ $this->contacts = [
+ 'ana' => $this->individualCreate(),
+ 'beto' => $this->individualCreate(),
+ 'carlos' => $this->individualCreate(),
+ ];
+ }
+
/**
* Adds the default assignee group and options to the test database.
* It also stores the IDs of the options in an index.
*/
- protected function setUpDefaultAssigneeOptions() {
+ protected function setupDefaultAssigneeOptions() {
$options = [
'NONE', 'BY_RELATIONSHIP', 'SPECIFIC_CONTACT', 'USER_CREATING_THE_CASE'
];
@@ -56,47 +58,114 @@ protected function setUpDefaultAssigneeOptions() {
/**
* Adds a relationship between the activity's target contact and default assignee.
*/
- protected function setUpRelationship() {
- $this->assignedRelationshipType = 'Instructor of';
- $this->unassignedRelationshipType = 'Employer of';
-
- $assignedRelationshipTypeId = $this->relationshipTypeCreate([
- 'contact_type_a' => 'Individual',
- 'contact_type_b' => 'Individual',
- 'name_a_b' => 'Pupil of',
- 'name_b_a' => $this->assignedRelationshipType,
- ]);
- $this->relationshipTypeCreate([
- 'name_a_b' => 'Employee of',
- 'name_b_a' => $this->unassignedRelationshipType,
- ]);
- $this->callAPISuccess('Relationship', 'create', [
- 'contact_id_a' => $this->assigneeContactId,
- 'contact_id_b' => $this->targetContactId,
- 'relationship_type_id' => $assignedRelationshipTypeId
- ]);
+ protected function setupRelationships() {
+ $this->relationships = [
+ 'ana_is_pupil_of_beto' => [
+ 'type_id' => NULL,
+ 'name_a_b' => 'Pupil of',
+ 'name_b_a' => 'Instructor',
+ 'contact_id_a' => $this->contacts['ana'],
+ 'contact_id_b' => $this->contacts['beto']
+ ],
+ 'ana_is_spouse_of_carlos' => [
+ 'type_id' => NULL,
+ 'name_a_b' => 'Spouse of',
+ 'name_b_a' => 'Spouse of',
+ 'contact_id_a' => $this->contacts['ana'],
+ 'contact_id_b' => $this->contacts['carlos']
+ ],
+ 'unassigned_employee' => [
+ 'type_id' => NULL,
+ 'name_a_b' => 'Employee of',
+ 'name_b_a' => 'Employer'
+ ],
+ ];
+
+ foreach ($this->relationships as $name => &$relationship) {
+ $relationship['type_id'] = $this->relationshipTypeCreate([
+ 'contact_type_a' => 'Individual',
+ 'contact_type_b' => 'Individual',
+ 'name_a_b' => $relationship['name_a_b'],
+ 'label_a_b' => $relationship['name_a_b'],
+ 'name_b_a' => $relationship['name_b_a'],
+ 'label_b_a' => $relationship['name_b_a']
+ ]);
+
+ if (isset($relationship['contact_id_a'])) {
+ $this->callAPISuccess('Relationship', 'create', [
+ 'contact_id_a' => $relationship['contact_id_a'],
+ 'contact_id_b' => $relationship['contact_id_b'],
+ 'relationship_type_id' => $relationship['type_id'],
+ ]);
+ }
+ }
}
/**
- * Tests the creation of activities with default assignee by relationship.
+ * Defines the the activity parameters and XML definitions. These can be used
+ * to create the activity.
+ */
+ protected function setupActivityDefinitions() {
+ $activityTypeXml = 'Open Case';
+ $this->activityTypeXml = new SimpleXMLElement($activityTypeXml);
+ $this->activityParams = [
+ 'activity_date_time' => date('Ymd'),
+ 'caseID' => $this->caseTypeId,
+ 'clientID' => $this->contacts['ana'],
+ 'creatorID' => $this->_loggedInUser,
+ ];
+ }
+
+ /**
+ * Tests the creation of activities where the default assignee should be the
+ * target contact's instructor. Beto is the instructor for Ana.
*/
public function testCreateActivityWithDefaultContactByRelationship() {
+ $relationship = $this->relationships['ana_is_pupil_of_beto'];
+ $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
+ $this->activityTypeXml->default_assignee_relationship = "{$relationship['type_id']}_b_a";
+
+ $this->process->createActivity($this->activityTypeXml, $this->activityParams);
+ $this->assertActivityAssignedToContactExists($this->contacts['beto']);
+ }
+
+ /**
+ * Tests when the default assignee relationship exists, but in the other direction only.
+ * Ana is a pupil, but has no pupils related to her.
+ */
+ public function testCreateActivityWithDefaultContactByRelationshipMissing() {
+ $relationship = $this->relationships['ana_is_pupil_of_beto'];
+ $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
+ $this->activityTypeXml->default_assignee_relationship = "{$relationship['type_id']}_a_b";
+
+ $this->process->createActivity($this->activityTypeXml, $this->activityParams);
+ $this->assertActivityAssignedToContactExists(NULL);
+ }
+
+ /**
+ * Tests when the the default assignee relationship exists and is a bidirectional
+ * relationship. Ana and Carlos are spouses.
+ */
+ public function testCreateActivityWithDefaultContactByRelationshipBidirectional() {
+ $relationship = $this->relationships['ana_is_spouse_of_carlos'];
+ $this->activityParams['clientID'] = $this->contacts['carlos'];
$this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
- $this->activityTypeXml->default_assignee_relationship = $this->assignedRelationshipType;
+ $this->activityTypeXml->default_assignee_relationship = "{$relationship['type_id']}_a_b";
- $this->process->createActivity($this->activityTypeXml, $this->params);
- $this->assertActivityAssignedToContactExists($this->assigneeContactId);
+ $this->process->createActivity($this->activityTypeXml, $this->activityParams);
+ $this->assertActivityAssignedToContactExists($this->contacts['ana']);
}
/**
- * Tests the creation of activities with default assignee by relationship,
- * but the target contact doesn't have any relationship of the selected type.
+ * Tests when the default assignee relationship does not exist. Ana is not an
+ * employee for anyone.
*/
public function testCreateActivityWithDefaultContactByRelationButTheresNoRelationship() {
+ $relationship = $this->relationships['unassigned_employee'];
$this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
- $this->activityTypeXml->default_assignee_relationship = $this->unassignedRelationshipType;
+ $this->activityTypeXml->default_assignee_relationship = "{$relationship['type_id']}_b_a";
- $this->process->createActivity($this->activityTypeXml, $this->params);
+ $this->process->createActivity($this->activityTypeXml, $this->activityParams);
$this->assertActivityAssignedToContactExists(NULL);
}
@@ -105,10 +174,10 @@ public function testCreateActivityWithDefaultContactByRelationButTheresNoRelatio
*/
public function testCreateActivityAssignedToSpecificContact() {
$this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['SPECIFIC_CONTACT'];
- $this->activityTypeXml->default_assignee_contact = $this->assigneeContactId;
+ $this->activityTypeXml->default_assignee_contact = $this->contacts['carlos'];
- $this->process->createActivity($this->activityTypeXml, $this->params);
- $this->assertActivityAssignedToContactExists($this->assigneeContactId);
+ $this->process->createActivity($this->activityTypeXml, $this->activityParams);
+ $this->assertActivityAssignedToContactExists($this->contacts['carlos']);
}
/**
@@ -119,7 +188,7 @@ public function testCreateActivityAssignedToNonExistantSpecificContact() {
$this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['SPECIFIC_CONTACT'];
$this->activityTypeXml->default_assignee_contact = 987456321;
- $this->process->createActivity($this->activityTypeXml, $this->params);
+ $this->process->createActivity($this->activityTypeXml, $this->activityParams);
$this->assertActivityAssignedToContactExists(NULL);
}
@@ -130,7 +199,7 @@ public function testCreateActivityAssignedToNonExistantSpecificContact() {
public function testCreateActivityAssignedToUserCreatingTheCase() {
$this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['USER_CREATING_THE_CASE'];
- $this->process->createActivity($this->activityTypeXml, $this->params);
+ $this->process->createActivity($this->activityTypeXml, $this->activityParams);
$this->assertActivityAssignedToContactExists($this->_loggedInUser);
}
@@ -140,7 +209,7 @@ public function testCreateActivityAssignedToUserCreatingTheCase() {
public function testCreateActivityAssignedNoUser() {
$this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['NONE'];
- $this->process->createActivity($this->activityTypeXml, $this->params);
+ $this->process->createActivity($this->activityTypeXml, $this->activityParams);
$this->assertActivityAssignedToContactExists(NULL);
}
@@ -148,7 +217,7 @@ public function testCreateActivityAssignedNoUser() {
* Tests the creation of activities when the default assignee is set to NONE.
*/
public function testCreateActivityWithNoDefaultAssigneeOption() {
- $this->process->createActivity($this->activityTypeXml, $this->params);
+ $this->process->createActivity($this->activityTypeXml, $this->activityParams);
$this->assertActivityAssignedToContactExists(NULL);
}
@@ -161,7 +230,7 @@ public function testCreateActivityWithNoDefaultAssigneeOption() {
protected function assertActivityAssignedToContactExists($assigneeContactId) {
$expectedContact = $assigneeContactId === NULL ? [] : [$assigneeContactId];
$result = $this->callAPISuccess('Activity', 'get', [
- 'target_contact_id' => $this->targetContactId,
+ 'target_contact_id' => $this->activityParams['clientID'],
'return' => ['assignee_contact_id']
]);
$activity = CRM_Utils_Array::first($result['values']);