dev/core#107 Automatically add default assignees when creating new cases
authorRené Olivo <minet.ws@gmail.com>
Thu, 15 Mar 2018 16:00:05 +0000 (12:00 -0400)
committerRené Olivo <minet.ws@gmail.com>
Wed, 4 Jul 2018 10:45:00 +0000 (06:45 -0400)
This feature allows users to define default assignees for each activity in a case type.
When creating a new case contacts are assigned to activities automatically by following
one of these rules:

* By relationship to case client
* The user creating the case
* A specific contact
* None (default)

CRM/Case/XMLProcessor/Process.php
CRM/Core/BAO/OptionValue.php
CRM/Upgrade/Incremental/php/FiveFour.php
ang/crmCaseType.js
ang/crmCaseType/timelineTable.html
tests/karma/unit/crmCaseTypeSpec.js
tests/phpunit/CRM/Case/XMLProcessor/ProcessTest.php [new file with mode: 0644]
tests/phpunit/CRM/Core/BAO/OptionValueTest.php

index f125355ec9367e5095d043fabb6206c193f1139c..9bd6e0b555bc2c8a3fc2b51db2b41fda8f382d83 100644 (file)
@@ -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 @@ class CRM_Case_XMLProcessor_Process extends CRM_Case_XMLProcessor {
 
   /**
    * @param SimpleXMLElement $caseTypeXML
+   *
    * @return array<string> symbolic activity-type names
    */
   public function getDeclaredActivityTypes($caseTypeXML) {
@@ -342,6 +345,7 @@ class CRM_Case_XMLProcessor_Process extends CRM_Case_XMLProcessor {
 
   /**
    * @param SimpleXMLElement $caseTypeXML
+   *
    * @return array<string> symbolic relationship-type names
    */
   public function getDeclaredRelationshipTypes($caseTypeXML) {
@@ -474,6 +478,8 @@ AND        a.is_deleted = 0
       );
     }
 
+    $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,119 @@ AND        a.is_deleted = 0
     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) {
+    if (!isset($activityTypeXML->default_assignee_relationship)) {
+      return NULL;
+    }
+
+    $targetContactId = is_array($activityParams['target_contact_id'])
+      ? CRM_Utils_Array::first($activityParams['target_contact_id'])
+      : $activityParams['target_contact_id'];
+
+    $relationships = civicrm_api3('Relationship', 'get', [
+      'contact_id_b' => $targetContactId,
+      'relationship_type_id.name_b_a' => (string) $activityTypeXML->default_assignee_relationship,
+      'is_active' => 1,
+      'sequential' => 1,
+    ]);
+
+    if ($relationships['count']) {
+      return $relationships['values'][0]['contact_id_a'];
+    }
+    else {
+      return NULL;
+    }
+  }
+
+  /**
+   * 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 +736,7 @@ AND        a.is_deleted = 0
 
   /**
    * @param string $caseType
+   *
    * @return array<\Civi\CCase\CaseChangeListener>
    */
   public function getListeners($caseType) {
@@ -662,6 +782,7 @@ AND        a.is_deleted = 0
    * @param string $settingKey
    * @param string $xmlTag
    * @param mixed $default
+   *
    * @return int
    */
   private function getBoolSetting($settingKey, $xmlTag, $default = 0) {
index 2e621cf073ccafcb09c2e2c3fd48dc911db761d1..cfb4b45dce48838c9d9b53720e6db4b26fd80fc7 100644 (file)
@@ -547,16 +547,23 @@ class CRM_Core_BAO_OptionValue extends CRM_Core_DAO_OptionValue {
    * 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']);
   }
 
 }
index 16d3e73e9623a2b925fa2e31f5112a0eaede8929..9c4082c0d7ce2da1fc3283035f706a6f178b7393 100644 (file)
@@ -73,6 +73,42 @@ class CRM_Upgrade_Incremental_php_FiveFour extends CRM_Upgrade_Incremental_Base
       'civicrm_uf_group', 'add_cancel_button', "tinyint DEFAULT '1' COMMENT 'Should a Cancel button be included in this Profile form.'");
     $this->addTask('Add location_id if missing to group_contact table (affects some older installs CRM-20711)', 'addColumn',
       'civicrm_group_contact', 'location_id', "int(10) unsigned DEFAULT NULL COMMENT 'Optional location to associate with this membership'");
+    $this->addTask('dev/core#107 - Add Activity\'s default assignee options', 'addActivityDefaultAssigneeOptions');
+  }
+
+  /**
+   * This task adds the default assignee option values that can be selected when
+   * creating or editing a new workflow's activity.
+   *
+   * @return bool
+   */
+  public static function addActivityDefaultAssigneeOptions() {
+    // Add option group for activity default assignees:
+    CRM_Core_BAO_OptionGroup::ensureOptionGroupExists(array(
+      'name' => 'activity_default_assignee',
+      'title' => ts('Activity default assignee'),
+      'is_reserved' => 1,
+    ));
+
+    // Add option values for activity default assignees:
+    $options = array(
+      array('name' => 'NONE', 'label' => ts('None'), 'is_default' => 1),
+      array('name' => 'BY_RELATIONSHIP', 'label' => ts('By relationship to case client')),
+      array('name' => 'SPECIFIC_CONTACT', 'label' => ts('Specific contact')),
+      array('name' => 'USER_CREATING_THE_CASE', 'label' => ts('User creating the case')),
+    );
+
+    foreach ($options as $option) {
+      CRM_Core_BAO_OptionValue::ensureOptionValueExists(array(
+        'option_group_id' => 'activity_default_assignee',
+        'name' => $option['name'],
+        'label' => $option['label'],
+        'is_default' => CRM_Utils_Array::value('is_default', $option, 0),
+        'is_active' => TRUE,
+      ));
+    }
+
+    return TRUE;
   }
 
   /*
index 0f74d410f8c90850daa6f0c40d6a49d8453445d7..d244ba2aa0ee56b9df5f93263e79d6ef981ec07b 100644 (file)
                 limit: 0
               }
             }];
+            reqs.defaultAssigneeTypes = ['OptionValue', 'get', {
+              option_group_id: 'activity_default_assignee',
+              sequential: 1,
+              options: {
+                limit: 0
+              }
+            }];
             reqs.relTypes = ['RelationshipType', 'get', {
               sequential: 1,
               options: {
   });
 
   crmCaseType.controller('CaseTypeCtrl', function($scope, crmApi, apiCalls) {
-    // CRM_Case_XMLProcessor::REL_TYPE_CNAME
-    var REL_TYPE_CNAME = CRM.crmCaseType.REL_TYPE_CNAME,
-
-    ts = $scope.ts = CRM.ts(null);
+    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};
+      });
+      // stores the default assignee values indexed by their option name:
+      $scope.defaultAssigneeTypeValues = _.chain($scope.defaultAssigneeTypes)
+        .indexBy('name').mapValues('value').value();
+    }
 
-    $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};
+    /// initializes the case type object
+    function initCaseType() {
+      var isNewCaseType = !apiCalls.caseType;
 
-    $scope.workflows = {
-      'timeline': 'Timeline',
-      'sequence': 'Sequence'
-    };
+      if (isNewCaseType) {
+        $scope.caseType = _.cloneDeep(newCaseTypeTemplate);
+      } else {
+        $scope.caseType = 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;
+    /// 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.caseType.definition.timelineActivityTypes = $scope.caseType.definition.timelineActivityTypes || [];
+    /// initializes the selected statuses
+    function initSelectedStatuses() {
+      $scope.selectedStatuses = {};
 
-    $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;
-    });
+      _.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 = {};
           status: 'Scheduled',
           reference_activity: 'Open Case',
           reference_offset: '1',
-          reference_select: 'newest'
+          reference_select: 'newest',
+          default_assignee_type: $scope.defaultAssigneeTypeValues.NONE
       };
       activitySet.activityTypes.push(activity);
       if(typeof activitySet.timeline !== "undefined" && activitySet.timeline == "1") {
       }
     };
 
+    /// 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');
index 0dba5c98bda64b6dfda5dec7f5a8777ad46b71e7..fd24b03bfb3b96d5754971faa5e80f7c1b0fb4f6 100644 (file)
@@ -11,6 +11,7 @@ Required vars: activitySet
     <th>{{ts('Reference')}}</th>
     <th>{{ts('Offset')}}</th>
     <th>{{ts('Select')}}</th>
+    <th>{{ts('Default assignee')}}</th>
     <th></th>
   </tr>
   </thead>
@@ -21,13 +22,13 @@ Required vars: activitySet
       <i class="crm-i fa-arrows grip-n-drag"></i>
     </td>
     <td>
-      <i class="crm-i {{ activityTypes[activity.name].icon }}"></i>
-      {{ activity.label }}
+      <i class="crm-i {{activityTypes[activity.name].icon}}"></i>
+      {{activity.label}}
     </td>
     <td>
       <select
         ui-jq="select2"
-        ui-options="{dropdownAutoWidth : true}"
+        ui-options="{dropdownAutoWidth: true}"
         ng-model="activity.status"
         ng-options="actStatus.name as actStatus.label for actStatus in activityStatuses|orderBy:'label'"
         >
@@ -37,7 +38,7 @@ Required vars: activitySet
     <td>
       <select
         ui-jq="select2"
-        ui-options="{dropdownAutoWidth : true}"
+        ui-options="{dropdownAutoWidth: true}"
         ng-model="activity.reference_activity"
         ng-options="activityType.name as activityType.label for activityType in caseType.definition.timelineActivityTypes"
         >
@@ -55,12 +56,41 @@ Required vars: activitySet
     <td>
       <select
         ui-jq="select2"
-        ui-options="{dropdownAutoWidth : true}"
+        ui-options="{dropdownAutoWidth: true}"
         ng-model="activity.reference_select"
         ng-options="key as value for (key,value) in {newest: ts('Newest'), oldest: ts('Oldest')}"
         >
       </select>
     </td>
+    <td>
+      <select
+        ui-jq="select2"
+        ui-options="{dropdownAutoWidth: true}"
+        ng-model="activity.default_assignee_type"
+        ng-options="option.value as option.label for option in defaultAssigneeTypes"
+        ng-change="clearActivityDefaultAssigneeValues(activity)"
+      ></select>
+
+      <p ng-if="activity.default_assignee_type === defaultAssigneeTypeValues.BY_RELATIONSHIP">
+        <select
+          ui-jq="select2"
+          ui-options="{dropdownAutoWidth: true}"
+          ng-model="activity.default_assignee_relationship"
+          ng-options="option.id as option.text for option in relationshipTypeOptions"
+          required
+        ></select>
+      </p>
+
+      <p ng-if="activity.default_assignee_type === defaultAssigneeTypeValues.SPECIFIC_CONTACT">
+        <input
+          type="text"
+          ng-model="activity.default_assignee_contact"
+          placeholder="- Select contact -"
+          crm-entityref="{ entity: 'Contact' }"
+          data-create-links="true"
+          required />
+      </p>
+    </td>
     <td>
       <a class="crm-hover-button"
          crm-icon="fa-trash"
@@ -74,7 +104,7 @@ Required vars: activitySet
 
   <tfoot>
   <tr class="addRow">
-    <td colspan="6">
+    <td colspan="8">
       <span crm-add-name=""
            crm-options="activityTypeOptions"
            crm-var="newActivity"
index 9279d2017d762ed753cc6b952da69f22c78bc586..eb13d20f6b49f71e27653f9748a2556efd7da37b 100644 (file)
@@ -11,6 +11,7 @@ describe('crmCaseType', function() {
   var apiCalls;
   var ctrl;
   var compile;
+  var defaultAssigneeDefaultValue;
   var scope;
 
   beforeEach(function() {
@@ -231,8 +232,65 @@ describe('crmCaseType', function() {
               }
             ]
           }
+        },
+        defaultAssigneeTypes: {
+          values: [
+              {
+                "id": "1174",
+                "option_group_id": "152",
+                "label": "None",
+                "value": "1",
+                "name": "NONE",
+                "filter": "0",
+                "is_default": "1",
+                "weight": "1",
+                "is_optgroup": "0",
+                "is_reserved": "0",
+                "is_active": "1"
+              },
+              {
+                "id": "1175",
+                "option_group_id": "152",
+                "label": "By relationship to workflow client",
+                "value": "2",
+                "name": "BY_RELATIONSHIP",
+                "filter": "0",
+                "is_default": "0",
+                "weight": "2",
+                "is_optgroup": "0",
+                "is_reserved": "0",
+                "is_active": "1"
+              },
+              {
+                "id": "1176",
+                "option_group_id": "152",
+                "label": "Specific contact",
+                "value": "3",
+                "name": "SPECIFIC_CONTACT",
+                "filter": "0",
+                "is_default": "0",
+                "weight": "3",
+                "is_optgroup": "0",
+                "is_reserved": "0",
+                "is_active": "1"
+              },
+              {
+                "id": "1177",
+                "option_group_id": "152",
+                "label": "User creating the workflow",
+                "value": "4",
+                "name": "USER_CREATING_THE_CASE",
+                "filter": "0",
+                "is_default": "0",
+                "weight": "4",
+                "is_optgroup": "0",
+                "is_reserved": "0",
+                "is_active": "1"
+              }
+          ]
         }
       };
+      defaultAssigneeDefaultValue = _.find(apiCalls.defaultAssigneeTypes.values, { is_default: '1' });
       scope = $rootScope.$new();
       ctrl = $controller('CaseTypeCtrl', {$scope: scope, apiCalls: apiCalls});
     });
@@ -245,6 +303,17 @@ describe('crmCaseType', function() {
       expect(scope.activityTypes['ADC referral']).toEqualData(apiCalls.actTypes.values[0]);
     });
 
+    it('should store the default assignee types', function() {
+      expect(scope.defaultAssigneeTypes).toBe(apiCalls.defaultAssigneeTypes.values);
+    });
+
+    it('should store the default assignee types values indexed by name', function() {
+      var defaultAssigneeTypeValues = _.chain(apiCalls.defaultAssigneeTypes.values)
+        .indexBy('name').mapValues('value').value();
+
+      expect(scope.defaultAssigneeTypeValues).toEqual(defaultAssigneeTypeValues);
+    });
+
     it('addActivitySet should add an activitySet to the case type', function() {
       scope.addActivitySet('timeline');
       var activitySets = scope.caseType.definition.activitySets;
@@ -263,6 +332,75 @@ describe('crmCaseType', function() {
       expect(newSet.timeline).toBe('1');
       expect(newSet.label).toBe('Timeline #2');
     });
+
+    describe('when clearing the activity\'s default assignee type values', function() {
+      var activity;
+
+      beforeEach(function() {
+        activity = {
+          default_assignee_relationship: 1,
+          default_assignee_contact: 2
+        };
+
+        scope.clearActivityDefaultAssigneeValues(activity);
+      });
+
+      it('clears the default assignee relationship for the activity', function() {
+        expect(activity.default_assignee_relationship).toBe(null);
+      });
+
+      it('clears the default assignee contact for the activity', function() {
+        expect(activity.default_assignee_contact).toBe(null);
+      });
+    });
+
+    describe('when adding a new activity to a set', function() {
+      var activitySet;
+
+      beforeEach(function() {
+        activitySet = { activityTypes: [] };
+        scope.activityTypes = { comment: { label: 'Add a new comment' } };
+
+        scope.addActivity(activitySet, 'comment');
+      });
+
+      it('adds a new Comment activity to the set', function() {
+        expect(activitySet.activityTypes[0]).toEqual({
+          name: 'comment',
+          label: scope.activityTypes.comment.label,
+          status: 'Scheduled',
+          reference_activity: 'Open Case',
+          reference_offset: '1',
+          reference_select: 'newest',
+          default_assignee_type: defaultAssigneeDefaultValue.value
+        });
+      });
+    });
+
+    describe('when creating a new workflow', function() {
+      beforeEach(inject(function ($controller) {
+        apiCalls.caseType = null;
+
+        ctrl = $controller('CaseTypeCtrl', {$scope: scope, apiCalls: apiCalls});
+      }));
+
+      it('sets default values for the case type title, name, and active status', function() {
+        expect(scope.caseType).toEqual(jasmine.objectContaining({
+          title: '',
+          name: '',
+          is_active: '1'
+        }));
+      });
+
+      it('adds an Open Case activty to the default activty set', function() {
+        expect(scope.caseType.definition.activitySets[0].activityTypes).toEqual([{
+          name: 'Open Case',
+          label: 'Open Case',
+          status: 'Completed',
+          default_assignee_type: defaultAssigneeDefaultValue.value
+        }]);
+      });
+    });
   });
 
   describe('crmAddName', function () {
diff --git a/tests/phpunit/CRM/Case/XMLProcessor/ProcessTest.php b/tests/phpunit/CRM/Case/XMLProcessor/ProcessTest.php
new file mode 100644 (file)
index 0000000..5241319
--- /dev/null
@@ -0,0 +1,173 @@
+<?php
+require_once 'CiviTest/CiviCaseTestCase.php';
+
+/**
+ * Class CRM_Case_PseudoConstantTest
+ * @group headless
+ */
+class CRM_Case_XMLProcessor_ProcessTest extends CiviCaseTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->defaultAssigneeOptionsValues = [];
+    $this->assigneeContactId = $this->individualCreate();
+    $this->targetContactId = $this->individualCreate();
+
+    $this->setUpDefaultAssigneeOptions();
+    $this->setUpRelationship();
+
+    $activityTypeXml = '<activity-type><name>Open Case</name></activity-type>';
+    $this->activityTypeXml = new SimpleXMLElement($activityTypeXml);
+    $this->params = [
+      'activity_date_time' => date('Ymd'),
+      'caseID' => $this->caseTypeId,
+      'clientID' => $this->targetContactId,
+      'creatorID' => $this->_loggedInUser,
+    ];
+
+    $this->process = new CRM_Case_XMLProcessor_Process();
+  }
+
+  /**
+   * 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() {
+    $options = [
+      'NONE', 'BY_RELATIONSHIP', 'SPECIFIC_CONTACT', 'USER_CREATING_THE_CASE'
+    ];
+
+    CRM_Core_BAO_OptionGroup::ensureOptionGroupExists([
+      'name' => 'activity_default_assignee'
+    ]);
+
+    foreach ($options as $option) {
+      $optionValue = CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+        'option_group_id' => 'activity_default_assignee',
+        'name' => $option,
+        'label' => $option
+      ]);
+
+      $this->defaultAssigneeOptionsValues[$option] = $optionValue['value'];
+    }
+  }
+
+  /**
+   * 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
+    ]);
+  }
+
+  /**
+   * Tests the creation of activities with default assignee by relationship.
+   */
+  public function testCreateActivityWithDefaultContactByRelationship() {
+    $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
+    $this->activityTypeXml->default_assignee_relationship = $this->assignedRelationshipType;
+
+    $this->process->createActivity($this->activityTypeXml, $this->params);
+    $this->assertActivityAssignedToContactExists($this->assigneeContactId);
+  }
+
+  /**
+   * Tests the creation of activities with default assignee by relationship,
+   * but the target contact doesn't have any relationship of the selected type.
+   */
+  public function testCreateActivityWithDefaultContactByRelationButTheresNoRelationship() {
+    $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['BY_RELATIONSHIP'];
+    $this->activityTypeXml->default_assignee_relationship = $this->unassignedRelationshipType;
+
+    $this->process->createActivity($this->activityTypeXml, $this->params);
+    $this->assertActivityAssignedToContactExists(NULL);
+  }
+
+  /**
+   * Tests the creation of activities with default assignee set to a specific contact.
+   */
+  public function testCreateActivityAssignedToSpecificContact() {
+    $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['SPECIFIC_CONTACT'];
+    $this->activityTypeXml->default_assignee_contact = $this->assigneeContactId;
+
+    $this->process->createActivity($this->activityTypeXml, $this->params);
+    $this->assertActivityAssignedToContactExists($this->assigneeContactId);
+  }
+
+  /**
+   * Tests the creation of activities with default assignee set to a specific contact,
+   * but the contact does not exist.
+   */
+  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->assertActivityAssignedToContactExists(NULL);
+  }
+
+  /**
+   * Tests the creation of activities with the default assignee being the one
+   * creating the case's activity.
+   */
+  public function testCreateActivityAssignedToUserCreatingTheCase() {
+    $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['USER_CREATING_THE_CASE'];
+
+    $this->process->createActivity($this->activityTypeXml, $this->params);
+    $this->assertActivityAssignedToContactExists($this->_loggedInUser);
+  }
+
+  /**
+   * Tests the creation of activities when the default assignee is set to NONE.
+   */
+  public function testCreateActivityAssignedNoUser() {
+    $this->activityTypeXml->default_assignee_type = $this->defaultAssigneeOptionsValues['NONE'];
+
+    $this->process->createActivity($this->activityTypeXml, $this->params);
+    $this->assertActivityAssignedToContactExists(NULL);
+  }
+
+  /**
+   * Tests the creation of activities when the default assignee is set to NONE.
+   */
+  public function testCreateActivityWithNoDefaultAssigneeOption() {
+    $this->process->createActivity($this->activityTypeXml, $this->params);
+    $this->assertActivityAssignedToContactExists(NULL);
+  }
+
+  /**
+   * Asserts that an activity was created where the assignee was the one related
+   * to the target contact.
+   *
+   * @param int|null $assigneeContactId the ID of the expected assigned contact or NULL if expected to be empty.
+   */
+  protected function assertActivityAssignedToContactExists($assigneeContactId) {
+    $expectedContact = $assigneeContactId === NULL ? [] : [$assigneeContactId];
+    $result = $this->callAPISuccess('Activity', 'get', [
+      'target_contact_id' => $this->targetContactId,
+      'return' => ['assignee_contact_id']
+    ]);
+    $activity = CRM_Utils_Array::first($result['values']);
+
+    $this->assertNotNull($activity, 'Target contact has no activities assigned to them');
+    $this->assertEquals($expectedContact, $activity['assignee_contact_id'], 'Activity is not assigned to expected contact');
+  }
+
+}
index 23fbb3616994cfeaf5102100f43b0e1be572a1d2..c85e37be6959ab045bcefc72fa02a8ad35381c6f 100644 (file)
@@ -72,12 +72,15 @@ class CRM_Core_BAO_OptionValueTest extends CiviUnitTestCase {
    * decision to disable it & leaving it in that state.
    */
   public function testEnsureOptionValueExistsDisabled() {
-    CRM_Core_BAO_OptionValue::ensureOptionValueExists(array('name' => 'Crashed', 'option_group_id' => 'contribution_status', 'is_active' => 0));
+    $optionValue = CRM_Core_BAO_OptionValue::ensureOptionValueExists(array('name' => 'Crashed', 'option_group_id' => 'contribution_status', 'is_active' => 0));
     $value = $this->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'));
+    $this->assertEquals($value['id'], $optionValue['id']);
+
+    $optionValue = 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']);
+    $this->assertEquals($value['id'], $optionValue['id']);
   }
 
 }