CRM-14727 - Implement Civi\CCase\SequenceListener
authorTim Otten <totten@civicrm.org>
Tue, 27 May 2014 05:40:29 +0000 (22:40 -0700)
committerTim Otten <totten@civicrm.org>
Wed, 28 May 2014 04:00:11 +0000 (21:00 -0700)
Civi/CCase/SequenceListener.php [new file with mode: 0644]
Civi/Core/Container.php
tests/phpunit/Civi/CCase/HousingSupportWithSequence.xml [new file with mode: 0644]
tests/phpunit/Civi/CCase/SequenceListenerTest.php [new file with mode: 0644]

diff --git a/Civi/CCase/SequenceListener.php b/Civi/CCase/SequenceListener.php
new file mode 100644 (file)
index 0000000..5eaa32d
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+namespace Civi\CCase;
+
+/**
+ * The sequence-listener looks for CiviCase XML tags with "<sequence>". If
+ * a change is made to any record in case-type which uses "<sequence>", then
+ * it attempts to add the next step in the sequence.
+ */
+class SequenceListener implements CaseChangeListener {
+
+  /**
+   * @var SequenceListener
+   */
+  private static $singleton;
+
+  /**
+   * @param bool $reset whether to forcibly rebuild the entire container
+   * @return \Symfony\Component\DependencyInjection\TaggedContainerInterface
+   */
+  public static function singleton($reset = FALSE) {
+    if ($reset || self::$singleton === NULL) {
+      self::$singleton = new SequenceListener();
+    }
+    return self::$singleton;
+  }
+
+  public static function onCaseChange_static(\Civi\CCase\Event\CaseChangeEvent $event) {
+    self::singleton()->onCaseChange($event);
+  }
+
+  private $isActive = array();
+
+  public function onCaseChange(\Civi\CCase\Event\CaseChangeEvent $event) {
+    /** @var \Civi\CCase\Analyzer $analyzer */
+    $analyzer = $event->analyzer;
+
+    if (isset($this->isActive[$analyzer->getCaseId()])) {
+      return;
+    }
+    $this->isActive[$analyzer->getCaseId()] = 1;
+
+    $activitySetXML = $this->getSequenceXml($analyzer->getXml());
+    if (!$activitySetXML) {
+      return;
+    }
+
+    $actTypes = array_flip(\CRM_Core_PseudoConstant::activityType(TRUE, TRUE, FALSE, 'name'));
+    $actStatuses = array_flip(\CRM_Core_PseudoConstant::activityStatus('name'));
+
+    $actIndex = $analyzer->getActivityIndex(array('activity_type_id', 'status_id'));
+
+    foreach ($activitySetXML->ActivityTypes->ActivityType as $actTypeXML) {
+      $actTypeId = $actTypes[(string) $actTypeXML->name];
+      if (empty($actIndex[$actTypeId])) {
+        // Haven't tried this step yet!
+        $this->createActivity($analyzer, $actTypeXML);
+        unset($this->isActive[$analyzer->getCaseId()]);
+        return;
+      }
+      elseif (empty($actIndex[$actTypeId][$actStatuses['Completed']])) {
+        // Haven't gotten past this step yet!
+        unset($this->isActive[$analyzer->getCaseId()]);
+        return;
+      }
+    }
+
+    // OK, the activity has completed every step in the sequence!
+    civicrm_api3('Case', 'create', array(
+      'id'  => $analyzer->getCaseId(),
+      'status_id' => 'Closed',
+    ));
+    $analyzer->flush();
+
+    // Wrap-up
+    unset($this->isActive[$analyzer->getCaseId()]);
+  }
+
+  /**
+   * Find the ActivitySet which defines the pipeline.
+   *
+   * @param \SimpleXMLElement $xml
+   * @return \SimpleXMLElement|NULL
+   */
+  public function getSequenceXml($xml) {
+    if ($xml->ActivitySets && $xml->ActivitySets->ActivitySet) {
+      foreach ($xml->ActivitySets->ActivitySet as $activitySetXML) {
+        $seq = (string) $activitySetXML->sequence;
+        if ($seq && strtolower($seq) == 'true') {
+          if ($activitySetXML->ActivityTypes && $activitySetXML->ActivityTypes->ActivityType) {
+            return $activitySetXML;
+          }
+          else {
+            return NULL;
+          }
+        }
+      }
+    }
+    return NULL;
+  }
+
+  /**
+   * @param Analyzer $analyzer the case being analyzed -- to which we want to add an activity
+   * @param \SimpleXMLElement $actXML the <ActivityType> tag which describes the new activity
+   */
+  public function createActivity(Analyzer $analyzer, \SimpleXMLElement $actXML) {
+    $params = array(
+      'activity_type_id' => (string) $actXML->name,
+      'status_id' => 'Scheduled',
+      'activity_date_time' => \CRM_Utils_Time::getTime('YmdHis'),
+      'case_id' => $analyzer->getCaseId(),
+    );
+    $r = civicrm_api3('Activity', 'create', $params);
+    $analyzer->flush();
+  }
+}
\ No newline at end of file
index 3c4c846e23ae33c94a01c7a8b9d563314deec9dc..faecd4b407eb47456bd03f09e4f2a08ab8ffb276 100644 (file)
@@ -92,6 +92,7 @@ class Container {
     $dispatcher->addListener('hook_civicrm_post::Activity', array('\Civi\CCase\Events', 'fireCaseChange'));
     $dispatcher->addListener('hook_civicrm_post::Case', array('\Civi\CCase\Events', 'fireCaseChange'));
     $dispatcher->addListener('hook_civicrm_caseChange', array('\Civi\CCase\Events', 'delegateToXmlListeners'));
+    $dispatcher->addListener('hook_civicrm_caseChange', array('\Civi\CCase\SequenceListener', 'onCaseChange_static'));
     return $dispatcher;
   }
 
diff --git a/tests/phpunit/Civi/CCase/HousingSupportWithSequence.xml b/tests/phpunit/Civi/CCase/HousingSupportWithSequence.xml
new file mode 100644 (file)
index 0000000..419d351
--- /dev/null
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="iso-8859-1" ?>
+
+<CaseType>
+  <name>Housing Support</name>
+  <ActivityTypes>
+    <ActivityType>
+      <name>Open Case</name>
+      <max_instances>1</max_instances>
+    </ActivityType>
+    <ActivityType>
+      <name>Medical evaluation</name>
+    </ActivityType>
+    <ActivityType>
+      <name>Mental health evaluation</name>
+    </ActivityType>
+    <ActivityType>
+      <name>Secure temporary housing</name>
+    </ActivityType>
+    <ActivityType>
+      <name>Income and benefits stabilization</name>
+    </ActivityType>
+    <ActivityType>
+      <name>Long-term housing plan</name>
+    </ActivityType>
+    <ActivityType>
+      <name>Follow up</name>
+    </ActivityType>
+  <ActivityType>
+      <name>Change Case Type</name>
+    </ActivityType>
+    <ActivityType>
+      <name>Change Case Status</name>
+    </ActivityType>
+    <ActivityType>
+      <name>Change Case Start Date</name>
+    </ActivityType>
+    <ActivityType>
+      <name>Link Cases</name>
+    </ActivityType>
+  </ActivityTypes>
+  <ActivitySets>
+    <ActivitySet>
+      <name>standard_timeline</name>
+      <label>Standard Timeline</label>
+      <timeline>true</timeline>
+      <ActivityTypes>
+        <ActivityType>
+          <name>Open Case</name>
+          <status>Completed</status>
+        </ActivityType>
+      </ActivityTypes>
+    </ActivitySet>
+    <ActivitySet>
+      <name>my_sequence</name>
+      <label>My Sequence</label>
+      <sequence>true</sequence>
+      <ActivityTypes>
+        <ActivityType>
+        <name>Medical evaluation</name>
+        </ActivityType>
+        <ActivityType>
+          <name>Mental health evaluation</name>
+        </ActivityType>
+        <ActivityType>
+          <name>Secure temporary housing</name>
+        </ActivityType>
+      </ActivityTypes>
+    </ActivitySet>
+  </ActivitySets>
+  <CaseRoles>
+    <RelationshipType>
+        <name>Homeless Services Coordinator</name>
+        <creator>1</creator>
+        <manager>1</manager>
+    </RelationshipType>
+    <RelationshipType>
+        <name>Health Services Coordinator</name>
+    </RelationshipType>
+    <RelationshipType>
+        <name>Benefits Specialist</name>
+    </RelationshipType>
+ </CaseRoles>
+</CaseType>
diff --git a/tests/phpunit/Civi/CCase/SequenceListenerTest.php b/tests/phpunit/Civi/CCase/SequenceListenerTest.php
new file mode 100644 (file)
index 0000000..eed57bb
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+namespace Civi\CCase;
+
+require_once 'CiviTest/CiviCaseTestCase.php';
+
+class SequenceListenerTest extends \CiviCaseTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->_params = array(
+      'case_type' => 'Housing Support', // FIXME: $this->caseType,
+      'subject' => 'Test case',
+      'contact_id' => 17,
+    );
+  }
+
+  public function testSequence() {
+    $actStatuses = array_flip(\CRM_Core_PseudoConstant::activityStatus('name'));
+    $caseStatuses = array_flip(\CRM_Case_PseudoConstant::caseStatus('name'));
+
+    // Create case; schedule first activity
+    \CRM_Utils_Time::setTime('2013-11-30 01:00:00');
+    $case = $this->callAPISuccess('case', 'create', $this->_params);
+
+    $analyzer = new \Civi\CCase\Analyzer($case['id']);
+    $this->assertEquals($caseStatuses['Open'], $analyzer->getCase()['status_id']);
+    $this->assertEquals('2013-11-30 01:00:00', $analyzer->getSingleActivity('Medical evaluation')['activity_date_time']);
+    $this->assertEquals($actStatuses['Scheduled'], $analyzer->getSingleActivity('Medical evaluation')['status_id']);
+    $this->assertFalse($analyzer->hasActivity('Mental health evaluation'));
+    $this->assertFalse($analyzer->hasActivity('Secure temporary housing'));
+
+    // Complete first activity; schedule second
+    \CRM_Utils_Time::setTime('2013-11-30 02:00:00');
+    $this->callApiSuccess('Activity', 'create', array(
+      'id' => $analyzer->getSingleActivity('Medical evaluation')['id'],
+      'status_id' => $actStatuses['Completed'],
+    ));
+    $analyzer->flush();
+    $this->assertEquals($caseStatuses['Open'], $analyzer->getCase()['status_id']);
+    $this->assertEquals('2013-11-30 01:00:00', $analyzer->getSingleActivity('Medical evaluation')['activity_date_time']);
+    $this->assertEquals($actStatuses['Completed'], $analyzer->getSingleActivity('Medical evaluation')['status_id']);
+    $this->assertEquals('2013-11-30 02:00:00', $analyzer->getSingleActivity('Mental health evaluation')['activity_date_time']);
+    $this->assertEquals($actStatuses['Scheduled'], $analyzer->getSingleActivity('Mental health evaluation')['status_id']);
+    $this->assertFalse($analyzer->hasActivity('Secure temporary housing'));
+
+    // Complete second activity; schedule third
+    \CRM_Utils_Time::setTime('2013-11-30 03:00:00');
+    $this->callApiSuccess('Activity', 'create', array(
+      'id' => $analyzer->getSingleActivity('Mental health evaluation')['id'],
+      'status_id' => $actStatuses['Completed'],
+    ));
+    $analyzer->flush();
+    $this->assertEquals($caseStatuses['Open'], $analyzer->getCase()['status_id']);
+    $this->assertEquals('2013-11-30 01:00:00', $analyzer->getSingleActivity('Medical evaluation')['activity_date_time']);
+    $this->assertEquals($actStatuses['Completed'], $analyzer->getSingleActivity('Medical evaluation')['status_id']);
+    $this->assertEquals('2013-11-30 02:00:00', $analyzer->getSingleActivity('Mental health evaluation')['activity_date_time']);
+    $this->assertEquals($actStatuses['Completed'], $analyzer->getSingleActivity('Mental health evaluation')['status_id']);
+    $this->assertEquals('2013-11-30 03:00:00', $analyzer->getSingleActivity('Secure temporary housing')['activity_date_time']);
+    $this->assertEquals($actStatuses['Scheduled'], $analyzer->getSingleActivity('Secure temporary housing')['status_id']);
+
+    // Complete third activity; close case
+    \CRM_Utils_Time::setTime('2013-11-30 04:00:00');
+    $this->callApiSuccess('Activity', 'create', array(
+      'id' => $analyzer->getSingleActivity('Secure temporary housing')['id'],
+      'status_id' => $actStatuses['Completed'],
+    ));
+    $analyzer->flush();
+    $this->assertEquals('2013-11-30 01:00:00', $analyzer->getSingleActivity('Medical evaluation')['activity_date_time']);
+    $this->assertEquals($actStatuses['Completed'], $analyzer->getSingleActivity('Medical evaluation')['status_id']);
+    $this->assertEquals('2013-11-30 02:00:00', $analyzer->getSingleActivity('Mental health evaluation')['activity_date_time']);
+    $this->assertEquals($actStatuses['Completed'], $analyzer->getSingleActivity('Mental health evaluation')['status_id']);
+    $this->assertEquals('2013-11-30 03:00:00', $analyzer->getSingleActivity('Secure temporary housing')['activity_date_time']);
+    $this->assertEquals($actStatuses['Completed'], $analyzer->getSingleActivity('Secure temporary housing')['status_id']);
+    $this->assertEquals($caseStatuses['Closed'], $analyzer->getCase()['status_id']);
+  }
+
+  /**
+   * @param $caseTypes
+   * @see \CRM_Utils_Hook::caseTypes
+   */
+  function hook_caseTypes(&$caseTypes) {
+    $caseTypes[$this->caseType] = array(
+      'module' => 'org.civicrm.hrcase',
+      'name' => $this->caseType,
+      'file' => __DIR__ . '/HousingSupportWithSequence.xml',
+    );
+  }
+
+  function assertApproxTime($expected, $actual, $tolerance = 1) {
+    $diff = abs(strtotime($expected) - strtotime($actual));
+    $this->assertTrue($diff <= $tolerance, sprintf("Check approx time equality. expected=[%s] actual=[%s] tolerance=[%s]",
+      $expected, $actual, $tolerance
+    ));
+  }
+}
\ No newline at end of file