From 70dd8c1619526d4842c7752fabc7d88c5a9a73f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Olivo?= Date: Thu, 15 Mar 2018 12:00:05 -0400 Subject: [PATCH] PCHR-3935: Add default assignee configuration for Case Types Included in 5.4.0 Core PR: https://github.com/civicrm/civicrm-core/pull/11998 --- CRM/Case/XMLProcessor/Process.php | 157 ++++++++++++++++ CRM/Core/BAO/OptionValue.php | 15 +- ang/crmCaseType.js | 152 ++++++++++++---- ang/crmCaseType/timelineTable.html | 42 ++++- tests/karma/unit/crmCaseTypeSpec.js | 170 ++++++++++++++++++ .../phpunit/CRM/Core/BAO/OptionValueTest.php | 1 + 6 files changed, 490 insertions(+), 47 deletions(-) diff --git a/CRM/Case/XMLProcessor/Process.php b/CRM/Case/XMLProcessor/Process.php index f125355ec936..1929c38fb729 100644 --- a/CRM/Case/XMLProcessor/Process.php +++ b/CRM/Case/XMLProcessor/Process.php @@ -31,6 +31,8 @@ * @copyright CiviCRM LLC (c) 2004-2018 */ class CRM_Case_XMLProcessor_Process extends CRM_Case_XMLProcessor { + protected $defaultAssigneeOptionsValues = []; + /** * Run. * @@ -314,6 +316,7 @@ public function activityTypes($activityTypesXML, $maxInst = FALSE, $isLabel = FA /** * @param SimpleXMLElement $caseTypeXML + * * @return array symbolic activity-type names */ public function getDeclaredActivityTypes($caseTypeXML) { @@ -342,6 +345,7 @@ public function getDeclaredActivityTypes($caseTypeXML) { /** * @param SimpleXMLElement $caseTypeXML + * * @return array symbolic relationship-type names */ public function getDeclaredRelationshipTypes($caseTypeXML) { @@ -474,6 +478,8 @@ public function createActivity($activityTypeXML, &$params) { ); } + $activityParams['assignee_contact_id'] = $this->getDefaultAssigneeForActivity($activityParams, $activityTypeXML); + //parsing date to default preference format $params['activity_date_time'] = CRM_Utils_Date::processDate($params['activity_date_time']); @@ -568,6 +574,155 @@ public function createActivity($activityTypeXML, &$params) { return TRUE; } + /** + * Return the default assignee contact for the activity. + * + * @param array $activityParams + * @param object $activityTypeXML + * + * @return int|null the ID of the default assignee contact or null if none. + */ + protected function getDefaultAssigneeForActivity($activityParams, $activityTypeXML) { + if (!isset($activityTypeXML->default_assignee_type)) { + return NULL; + } + + $defaultAssigneeOptionsValues = $this->getDefaultAssigneeOptionValues(); + + switch ($activityTypeXML->default_assignee_type) { + case $defaultAssigneeOptionsValues['BY_RELATIONSHIP']: + return $this->getDefaultAssigneeByRelationship($activityParams, $activityTypeXML); + + break; + case $defaultAssigneeOptionsValues['SPECIFIC_CONTACT']: + return $this->getDefaultAssigneeBySpecificContact($activityTypeXML); + + break; + case $defaultAssigneeOptionsValues['USER_CREATING_THE_CASE']: + return $activityParams['source_contact_id']; + + break; + case $defaultAssigneeOptionsValues['NONE']: + default: + return NULL; + } + } + + /** + * Fetches and caches the activity's default assignee options. + * + * @return array + */ + protected function getDefaultAssigneeOptionValues() { + if (!empty($this->defaultAssigneeOptionsValues)) { + return $this->defaultAssigneeOptionsValues; + } + + $defaultAssigneeOptions = civicrm_api3('OptionValue', 'get', [ + 'option_group_id' => 'activity_default_assignee', + 'options' => [ 'limit' => 0 ] + ]); + + foreach ($defaultAssigneeOptions['values'] as $option) { + $this->defaultAssigneeOptionsValues[$option['name']] = $option['value']; + } + + return $this->defaultAssigneeOptionsValues; + } + + /** + * Returns the default assignee for the activity by searching for the target's + * contact relationship type defined in the activity's details. + * + * @param array $activityParams + * @param object $activityTypeXML + * + * @return int|null the ID of the default assignee contact or null if none. + */ + protected function getDefaultAssigneeByRelationship($activityParams, $activityTypeXML) { + $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); + + $params = [ + 'relationship_type_id' => $relTypeId, + "contact_id_$b" => $targetContactId, + 'is_active' => 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']) { + $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. + * + * @param object $activityTypeXML + * + * @return int|null + */ + protected function getDefaultAssigneeBySpecificContact($activityTypeXML) { + if (!$activityTypeXML->default_assignee_contact) { + return NULL; + } + + $contact = civicrm_api3('Contact', 'get', [ + 'id' => $activityTypeXML->default_assignee_contact + ]); + + if ($contact['count'] == 1) { + return $activityTypeXML->default_assignee_contact; + } + + return NULL; + } + /** * @param $activitySetsXML * @@ -617,6 +772,7 @@ public function getCaseManagerRoleId($caseType) { /** * @param string $caseType + * * @return array<\Civi\CCase\CaseChangeListener> */ public function getListeners($caseType) { @@ -662,6 +818,7 @@ public function getNaturalActivityTypeSort() { * @param string $settingKey * @param string $xmlTag * @param mixed $default + * * @return int */ private function getBoolSetting($settingKey, $xmlTag, $default = 0) { diff --git a/CRM/Core/BAO/OptionValue.php b/CRM/Core/BAO/OptionValue.php index 2e621cf073cc..cfb4b45dce48 100644 --- a/CRM/Core/BAO/OptionValue.php +++ b/CRM/Core/BAO/OptionValue.php @@ -547,16 +547,23 @@ public static function getOptionValuesAssocArrayFromName($optionGroupName) { * that an option value exists, without hitting an error if it already exists. * * This is sympathetic to sites who might pre-add it. + * + * @param array $params the option value attributes. + * @return array the option value attributes. */ public static function ensureOptionValueExists($params) { - $existingValues = civicrm_api3('OptionValue', 'get', array( + $result = civicrm_api3('OptionValue', 'get', array( 'option_group_id' => $params['option_group_id'], 'name' => $params['name'], - 'return' => 'id', + 'return' => ['id', 'value'], + 'sequential' => 1, )); - if (!$existingValues['count']) { - civicrm_api3('OptionValue', 'create', $params); + + if (!$result['count']) { + $result = civicrm_api3('OptionValue', 'create', $params); } + + return CRM_Utils_Array::first($result['values']); } } diff --git a/ang/crmCaseType.js b/ang/crmCaseType.js index ee9efb960306..141af3460dfe 100644 --- a/ang/crmCaseType.js +++ b/ang/crmCaseType.js @@ -67,6 +67,13 @@ limit: 0 } }]; + reqs.defaultAssigneeTypes = ['OptionValue', 'get', { + option_group_id: 'activity_default_assignee', + sequential: 1, + options: { + limit: 0 + } + }]; reqs.relTypes = ['RelationshipType', 'get', { sequential: 1, options: { @@ -230,41 +237,101 @@ }); crmCaseType.controller('CaseTypeCtrl', function($scope, crmApi, apiCalls) { - // CRM_Case_XMLProcessor::REL_TYPE_CNAME - var REL_TYPE_CNAME = CRM.crmCaseType.REL_TYPE_CNAME, + var REL_TYPE_CNAME, defaultAssigneeDefaultValue, ts; + + (function init () { + // CRM_Case_XMLProcessor::REL_TYPE_CNAME + REL_TYPE_CNAME = CRM.crmCaseType.REL_TYPE_CNAME; + + ts = $scope.ts = CRM.ts(null); + $scope.locks = { caseTypeName: true, activitySetName: true }; + $scope.workflows = { timeline: 'Timeline', sequence: 'Sequence' }; + defaultAssigneeDefaultValue = _.find(apiCalls.defaultAssigneeTypes.values, { is_default: '1' }) || {}; + + storeApiCallsResults(); + initCaseType(); + initCaseTypeDefinition(); + initSelectedStatuses(); + })(); + + /// Stores the api calls results in the $scope object + function storeApiCallsResults() { + $scope.activityStatuses = apiCalls.actStatuses.values; + $scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name'); + $scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name'); + $scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption); + $scope.defaultAssigneeTypes = apiCalls.defaultAssigneeTypes.values; + $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(); + } - ts = $scope.ts = CRM.ts(null); + /// 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' + }); - $scope.activityStatuses = apiCalls.actStatuses.values; - $scope.caseStatuses = _.indexBy(apiCalls.caseStatuses.values, 'name'); - $scope.activityTypes = _.indexBy(apiCalls.actTypes.values, 'name'); - $scope.activityTypeOptions = _.map(apiCalls.actTypes.values, formatActivityTypeOption); - $scope.relationshipTypeOptions = _.map(apiCalls.relTypes.values, function(type) { - return {id: type[REL_TYPE_CNAME], text: type.label_b_a}; - }); - $scope.locks = {caseTypeName: true, activitySetName: true}; + if (!isBidirectionalRelationship) { + result.push({ + label: relType.label_a_b, + value: relType.id + '_a_b' + }); + } + }, []); + } - $scope.workflows = { - 'timeline': 'Timeline', - 'sequence': 'Sequence' - }; + /// initializes the case type object + function initCaseType() { + var isNewCaseType = !apiCalls.caseType; - $scope.caseType = apiCalls.caseType ? apiCalls.caseType : _.cloneDeep(newCaseTypeTemplate); - $scope.caseType.definition = $scope.caseType.definition || []; - $scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || []; - $scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || []; - _.each($scope.caseType.definition.activitySets, function (set) { - _.each(set.activityTypes, function (type, name) { - type.label = $scope.activityTypes[type.name].label; + if (isNewCaseType) { + $scope.caseType = _.cloneDeep(newCaseTypeTemplate); + } else { + $scope.caseType = apiCalls.caseType; + } + } + + /// initializes the case type definition object + function initCaseTypeDefinition() { + $scope.caseType.definition = $scope.caseType.definition || []; + $scope.caseType.definition.activityTypes = $scope.caseType.definition.activityTypes || []; + $scope.caseType.definition.activitySets = $scope.caseType.definition.activitySets || []; + $scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || []; + $scope.caseType.definition.statuses = $scope.caseType.definition.statuses || []; + $scope.caseType.definition.timelineActivityTypes = $scope.caseType.definition.timelineActivityTypes || []; + + _.each($scope.caseType.definition.activitySets, function (set) { + _.each(set.activityTypes, function (type, name) { + var isDefaultAssigneeTypeUndefined = _.isUndefined(type.default_assignee_type); + type.label = $scope.activityTypes[type.name].label; + + if (isDefaultAssigneeTypeUndefined) { + type.default_assignee_type = defaultAssigneeDefaultValue.value; + } + }); }); - }); - $scope.caseType.definition.caseRoles = $scope.caseType.definition.caseRoles || []; - $scope.caseType.definition.statuses = $scope.caseType.definition.statuses || []; + } - $scope.selectedStatuses = {}; - _.each(apiCalls.caseStatuses.values, function (status) { - $scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1; - }); + /// initializes the selected statuses + function initSelectedStatuses() { + $scope.selectedStatuses = {}; + + _.each(apiCalls.caseStatuses.values, function (status) { + $scope.selectedStatuses[status.name] = !$scope.caseType.definition.statuses.length || $scope.caseType.definition.statuses.indexOf(status.name) > -1; + }); + } $scope.addActivitySet = function(workflow) { var activitySet = {}; @@ -288,14 +355,19 @@ } function addActivityToSet(activitySet, activityTypeName) { - activitySet.activityTypes.push({ - name: activityTypeName, - label: $scope.activityTypes[activityTypeName].label, - status: 'Scheduled', - reference_activity: 'Open Case', - reference_offset: '1', - reference_select: 'newest' - }); + var activity = { + name: activityTypeName, + label: $scope.activityTypes[activityTypeName].label, + status: 'Scheduled', + reference_activity: 'Open Case', + reference_offset: '1', + reference_select: 'newest', + default_assignee_type: $scope.defaultAssigneeTypeValues.NONE + }; + activitySet.activityTypes.push(activity); + if(typeof activitySet.timeline !== "undefined" && activitySet.timeline == "1") { + $scope.caseType.definition.timelineActivityTypes.push(activity); + } } function createActivity(name, callback) { @@ -334,6 +406,12 @@ } }; + /// Clears the activity's default assignee values for relationship and contact + $scope.clearActivityDefaultAssigneeValues = function(activity) { + activity.default_assignee_relationship = null; + activity.default_assignee_contact = null; + }; + /// Add a new role $scope.addRole = function(roles, roleName) { var names = _.pluck($scope.caseType.definition.caseRoles, 'name'); diff --git a/ang/crmCaseType/timelineTable.html b/ang/crmCaseType/timelineTable.html index bc38d3ddd574..a67787ae99bb 100644 --- a/ang/crmCaseType/timelineTable.html +++ b/ang/crmCaseType/timelineTable.html @@ -11,6 +11,7 @@ {{ts('Reference')}} {{ts('Offset')}} {{ts('Select')}} + {{ts('Default assignee')}} @@ -21,13 +22,13 @@ - - {{ activity.label }} + + {{activity.label}} @@ -55,12 +56,41 @@ + + + +

+ +

+ +

+ +

+ - + callAPISuccessGetSingle('OptionValue', array('name' => 'Crashed', 'option_group_id' => 'contribution_status')); $this->assertEquals(0, $value['is_active']); CRM_Core_BAO_OptionValue::ensureOptionValueExists(array('name' => 'Crashed', 'option_group_id' => 'contribution_status')); + $value = $this->callAPISuccessGetSingle('OptionValue', array('name' => 'Crashed', 'option_group_id' => 'contribution_status')); $this->assertEquals(0, $value['is_active']); }