dev/core#107 Refactor selection of default assignee by relationship type
authorRené Olivo <minet.ws@gmail.com>
Tue, 22 May 2018 20:51:23 +0000 (16:51 -0400)
committerRené Olivo <minet.ws@gmail.com>
Wed, 4 Jul 2018 10:45:00 +0000 (06:45 -0400)
CRM/Case/XMLProcessor/Process.php
ang/crmCaseType.js
ang/crmCaseType/timelineTable.html
tests/karma/unit/crmCaseTypeSpec.js
tests/phpunit/CRM/Case/XMLProcessor/ProcessTest.php

index 9bd6e0b555bc2c8a3fc2b51db2b41fda8f382d83..1929c38fb7293bcb328c3cb6df8239d7b670167c 100644 (file)
@@ -640,29 +640,65 @@ AND        a.is_deleted = 0
    * @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.
index d244ba2aa0ee56b9df5f93263e79d6ef981ec07b..12500df6154ca3eab8d5f03e7d4cc6e2cd042ed1 100644 (file)
       $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;
index fd24b03bfb3b96d5754971faa5e80f7c1b0fb4f6..4d044f1b9d32b88568c82e23b13fbf4c06e1f518 100644 (file)
@@ -76,7 +76,7 @@ Required vars: activitySet
           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
         ></select>
       </p>
index eb13d20f6b49f71e27653f9748a2556efd7da37b..3369579c19bdf3d99aa5563871c2f33e6d7e00a0 100644 (file)
@@ -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;
index 52413195309f63d32a79955912a86a7301420c43..2d30c0bc82f2af71c45f5cdcf512701b48839a2a 100644 (file)
@@ -11,29 +11,31 @@ class CRM_Case_XMLProcessor_ProcessTest extends CiviCaseTestCase {
     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->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 @@ class CRM_Case_XMLProcessor_ProcessTest extends CiviCaseTestCase {
   /**
    * 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 = '<activity-type><name>Open Case</name></activity-type>';
+    $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 @@ class CRM_Case_XMLProcessor_ProcessTest extends CiviCaseTestCase {
    */
   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 @@ class CRM_Case_XMLProcessor_ProcessTest extends CiviCaseTestCase {
     $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 @@ class CRM_Case_XMLProcessor_ProcessTest extends CiviCaseTestCase {
   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 @@ class CRM_Case_XMLProcessor_ProcessTest extends CiviCaseTestCase {
   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 @@ class CRM_Case_XMLProcessor_ProcessTest extends CiviCaseTestCase {
    * 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 @@ class CRM_Case_XMLProcessor_ProcessTest extends CiviCaseTestCase {
   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']);