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']);