Commit 8e998f78 authored by colemanw's avatar colemanw Committed by GitHub
Browse files

Merge pull request #10618 from colemanw/CRM-20830

CRM-20830 - Improve handling of overdue activities
parents 869538fb d544ffcd
......@@ -36,6 +36,14 @@
*/
class CRM_Activity_BAO_Activity extends CRM_Activity_DAO_Activity {
/**
* Activity status types
*/
const
INCOMPLETE = 0,
COMPLETED = 1,
CANCELLED = 2;
/**
* Static field for all the activity information that we can potentially export.
*
......@@ -2436,6 +2444,45 @@ AND cl.modified_id = c.id
return $result;
}
/**
* Return list of activity statuses of a given type.
*
* Note: activity status options use the "grouping" field to distinguish status types.
* Types are defined in class constants INCOMPLETE, COMPLETED, CANCELLED
*
* @param int $type
*
* @return array
*/
public static function getStatusesByType($type) {
if (!isset(Civi::$statics[__CLASS__][__FUNCTION__])) {
$statuses = civicrm_api3('OptionValue', 'get', array(
'option_group_id' => 'activity_status',
'return' => array('value', 'name', 'filter'),
'options' => array('limit' => 0),
));
Civi::$statics[__CLASS__][__FUNCTION__] = $statuses['values'];
}
$ret = array();
foreach (Civi::$statics[__CLASS__][__FUNCTION__] as $status) {
if ($status['filter'] == $type) {
$ret[$status['value']] = $status['name'];
}
}
return $ret;
}
/**
* Check if activity is overdue.
*
* @param array $activity
*
* @return bool
*/
public static function isOverdue($activity) {
return array_key_exists($activity['status_id'], self::getStatusesByType(self::INCOMPLETE)) && CRM_Utils_Date::overdue($activity['activity_date_time']);
}
/**
* Get the exportable fields for Activities.
*
......@@ -2802,9 +2849,7 @@ INNER JOIN civicrm_option_group grp ON ( grp.id = val.option_group_id AND grp.n
$activity['DT_RowId'] = $activityId;
// Add class to this row if overdue.
$activity['DT_RowClass'] = "crm-entity status-id-{$values['status_id']}";
if (CRM_Utils_Date::overdue(CRM_Utils_Array::value('activity_date_time', $values))
&& CRM_Utils_Array::value('status_id', $values) == 1
) {
if (self::isOverdue($values)) {
$activity['DT_RowClass'] .= ' status-overdue';
}
else {
......
......@@ -222,17 +222,22 @@ class CRM_Admin_Form_Options extends CRM_Admin_Form {
);
}
$required = FALSE;
if ($this->_gName == 'custom_search') {
$required = TRUE;
}
elseif ($this->_gName == 'redaction_rule' || $this->_gName == 'engagement_index') {
if ($this->_gName == 'redaction_rule') {
$this->add('checkbox',
'filter',
ts('Regular Expression?')
);
}
if ($this->_gName == 'activity_status') {
$this->add('select',
'filter',
ts('Status Type'),
array(
CRM_Activity_BAO_Activity::INCOMPLETE => ts('Incomplete'),
CRM_Activity_BAO_Activity::COMPLETED => ts('Completed'),
CRM_Activity_BAO_Activity::CANCELLED => ts('Cancelled'),
)
);
}
if ($this->_gName == 'redaction_rule') {
$this->add('checkbox',
'filter',
ts('Regular Expression?')
);
}
if ($this->_gName == 'participant_listing') {
$this->add('text',
......@@ -246,7 +251,7 @@ class CRM_Admin_Form_Options extends CRM_Admin_Form {
$this->add('wysiwyg', 'description',
ts('Description'),
array('rows' => 4, 'cols' => 80),
$required
$this->_gName == 'custom_search'
);
}
......
......@@ -13,3 +13,14 @@ VALUES
-- CRM-20387
UPDATE `civicrm_contribution` SET `invoice_number` = `invoice_id` WHERE `invoice_id` LIKE CONCAT('%', `id`);
-- CRM-20830
UPDATE `civicrm_option_value`
SET filter = 1
WHERE option_group_id = (SELECT id FROM civicrm_option_group WHERE name = 'activity_status')
AND name = 'Completed';
UPDATE `civicrm_option_value`
SET filter = 2
WHERE option_group_id = (SELECT id FROM civicrm_option_group WHERE name = 'activity_status')
AND name IN ('Cancelled', 'Unreachable', 'Not Required', 'No-show');
......@@ -232,7 +232,7 @@ function _civicrm_api3_activity_get_spec(&$params) {
$params['tag_id'] = array(
'title' => 'Tags',
'description' => 'Find activities with specified tags.',
'type' => 1,
'type' => CRM_Utils_Type::T_INT,
'FKClassName' => 'CRM_Core_DAO_Tag',
'FKApiName' => 'Tag',
'supports_joins' => TRUE,
......@@ -240,45 +240,50 @@ function _civicrm_api3_activity_get_spec(&$params) {
$params['file_id'] = array(
'title' => 'Attached Files',
'description' => 'Find activities with attached files.',
'type' => 1,
'type' => CRM_Utils_Type::T_INT,
'FKClassName' => 'CRM_Core_DAO_File',
'FKApiName' => 'File',
);
$params['case_id'] = array(
'title' => 'Cases',
'description' => 'Find activities within specified cases.',
'type' => 1,
'type' => CRM_Utils_Type::T_INT,
'FKClassName' => 'CRM_Case_DAO_Case',
'FKApiName' => 'Case',
);
$params['contact_id'] = array(
'title' => 'Activity Contact ID',
'description' => 'Find activities involving this contact (as target, source, OR assignee).',
'type' => 1,
'type' => CRM_Utils_Type::T_INT,
'FKClassName' => 'CRM_Contact_DAO_Contact',
'FKApiName' => 'Contact',
);
$params['target_contact_id'] = array(
'title' => 'Target Contact ID',
'description' => 'Find activities with specified target contact.',
'type' => 1,
'type' => CRM_Utils_Type::T_INT,
'FKClassName' => 'CRM_Contact_DAO_Contact',
'FKApiName' => 'Contact',
);
$params['source_contact_id'] = array(
'title' => 'Source Contact ID',
'description' => 'Find activities with specified source contact.',
'type' => 1,
'type' => CRM_Utils_Type::T_INT,
'FKClassName' => 'CRM_Contact_DAO_Contact',
'FKApiName' => 'Contact',
);
$params['assignee_contact_id'] = array(
'title' => 'Assignee Contact ID',
'description' => 'Find activities with specified assignee contact.',
'type' => 1,
'type' => CRM_Utils_Type::T_INT,
'FKClassName' => 'CRM_Contact_DAO_Contact',
'FKApiName' => 'Contact',
);
$params['is_overdue'] = array(
'title' => 'Is Activity Overdue',
'description' => 'Incomplete activities with a past date.',
'type' => CRM_Utils_Type::T_BOOLEAN,
);
}
/**
......@@ -295,16 +300,9 @@ function _civicrm_api3_activity_get_spec(&$params) {
* @throws \Civi\API\Exception\UnauthorizedException
*/
function civicrm_api3_activity_get($params) {
$options = _civicrm_api3_get_options_from_params($params, FALSE, 'Activity', 'get');
$sql = CRM_Utils_SQL_Select::fragment();
$recordTypes = civicrm_api3('ActivityContact', 'getoptions', array('field' => 'record_type_id'));
$recordTypes = $recordTypes['values'];
$activityContactOptions = array(
'contact_id' => NULL,
'target_contact_id' => array_search('Activity Targets', $recordTypes),
'source_contact_id' => array_search('Activity Source', $recordTypes),
'assignee_contact_id' => array_search('Activity Assignees', $recordTypes),
);
if (empty($params['target_contact_id']) && empty($params['source_contact_id'])
&& empty($params['assignee_contact_id']) &&
!empty($params['check_permissions']) && !CRM_Core_Permission::check('view all activities')
......@@ -315,6 +313,67 @@ function civicrm_api3_activity_get($params) {
//$params['contact_id'] = array('IS NOT NULL' => TRUE);
}
_civicrm_api3_activity_get_extraFilters($params, $sql);
// Handle is_overdue sort
if (!empty($options['sort'])) {
$sort = explode(', ', $options['sort']);
foreach ($sort as $index => &$sortString) {
// Get sort field and direction
list($sortField, $dir) = array_pad(explode(' ', $sortString), 2, 'ASC');
if ($sortField == 'is_overdue') {
$incomplete = implode(',', array_keys(CRM_Activity_BAO_Activity::getStatusesByType(CRM_Activity_BAO_Activity::INCOMPLETE)));
$sql->orderBy("IF((a.activity_date_time >= NOW() OR a.status_id NOT IN ($incomplete)), 0, 1) $dir", NULL, $index);
// Replace the sort with a placeholder which will be ignored by sql
$sortString = '(1)';
}
}
$params['options']['sort'] = implode(', ', $sort);
}
// Ensure there's enough data for calculating is_overdue
if (!empty($options['return']['is_overdue']) && (empty($options['return']['status_id']) || empty($options['return']['activity_date_time']))) {
$options['return']['status_id'] = $options['return']['activity_date_time'] = 1;
$params['return'] = array_keys($options['return']);
}
$activities = _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params, FALSE, 'Activity', $sql);
if (!empty($params['check_permissions']) && !CRM_Core_Permission::check('view all activities')) {
// @todo get this to work at the query level - see contact_id join above.
foreach ($activities as $activity) {
if (!CRM_Activity_BAO_Activity::checkPermission($activity['id'], CRM_Core_Action::VIEW)) {
unset($activities[$activity['id']]);
}
}
}
if ($options['is_count']) {
return civicrm_api3_create_success($activities, $params, 'Activity', 'get');
}
$activities = _civicrm_api3_activity_get_formatResult($params, $activities, $options);
//legacy custom data get - so previous formatted response is still returned too
return civicrm_api3_create_success($activities, $params, 'Activity', 'get');
}
/**
* Support filters beyond what basic_get can do.
*
* @param array $params
* @param CRM_Utils_SQL_Select $sql
* @throws \CiviCRM_API3_Exception
* @throws \Exception
*/
function _civicrm_api3_activity_get_extraFilters(&$params, &$sql) {
// Filter by activity contacts
$recordTypes = civicrm_api3('ActivityContact', 'getoptions', array('field' => 'record_type_id'));
$recordTypes = $recordTypes['values'];
$activityContactOptions = array(
'contact_id' => NULL,
'target_contact_id' => array_search('Activity Targets', $recordTypes),
'source_contact_id' => array_search('Activity Source', $recordTypes),
'assignee_contact_id' => array_search('Activity Assignees', $recordTypes),
);
foreach ($activityContactOptions as $activityContactName => $activityContactValue) {
if (!empty($params[$activityContactName])) {
if (!is_array($params[$activityContactName])) {
......@@ -328,6 +387,19 @@ function civicrm_api3_activity_get($params) {
}
}
// Handle is_overdue filter
// Boolean calculated field - does not support operators
if (isset($params['is_overdue'])) {
$incomplete = implode(',', array_keys(CRM_Activity_BAO_Activity::getStatusesByType(CRM_Activity_BAO_Activity::INCOMPLETE)));
if ($params['is_overdue']) {
$sql->where('a.activity_date_time < NOW()');
$sql->where("a.status_id IN ($incomplete)");
}
else {
$sql->where("(a.activity_date_time >= NOW() OR a.status_id NOT IN ($incomplete))");
}
}
// Define how to handle filters on some related entities.
// Subqueries are nice in (a) avoiding duplicates and (b) when the result
// list is expected to be bite-sized. Joins are nice (a) with larger
......@@ -372,23 +444,6 @@ function civicrm_api3_activity_get($params) {
}
}
}
$activities = _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params, FALSE, 'Activity', $sql);
if (!empty($params['check_permissions']) && !CRM_Core_Permission::check('view all activities')) {
// @todo get this to work at the query level - see contact_id join above.
foreach ($activities as $activity) {
if (!CRM_Activity_BAO_Activity::checkPermission($activity['id'], CRM_Core_Action::VIEW)) {
unset($activities[$activity['id']]);
}
}
}
$options = _civicrm_api3_get_options_from_params($params, FALSE, 'Activity', 'get');
if ($options['is_count']) {
return civicrm_api3_create_success($activities, $params, 'Activity', 'get');
}
$activities = _civicrm_api3_activity_get_formatResult($params, $activities, $options);
//legacy custom data get - so previous formatted response is still returned too
return civicrm_api3_create_success($activities, $params, 'Activity', 'get');
}
/**
......@@ -495,6 +550,12 @@ function _civicrm_api3_activity_get_formatResult($params, $activities, $options)
}
break;
case 'is_overdue':
foreach ($activities as $key => $activityArray) {
$activities[$key]['is_overdue'] = (int) CRM_Activity_BAO_Activity::isOverdue($activityArray);
}
break;
default:
if (substr($n, 0, 6) == 'custom') {
$returnProperties[$n] = $v;
......
This diff is collapsed.
......@@ -1374,6 +1374,9 @@ class api_v3_ActivityTest extends CiviUnitTestCase {
}
}
/**
* Test or operator in api params
*/
public function testGetWithOr() {
$acts = array(
'test or 1' => 'orOperator',
......@@ -1403,4 +1406,45 @@ class api_v3_ActivityTest extends CiviUnitTestCase {
$this->assertEquals(3, $result['count']);
}
/**
* Test handling of is_overdue calculated field
*/
public function testGetOverdue() {
$overdueAct = $this->callAPISuccess('Activity', 'create', array(
'activity_date_time' => 'now - 1 week',
'status_id' => 'Scheduled',
) + $this->_params
);
$completedAct = $this->callAPISuccess('Activity', 'create', array(
'activity_date_time' => 'now - 1 week',
'status_id' => 'Completed',
) + $this->_params
);
$ids = array($overdueAct['id'], $completedAct['id']);
// Test sorting
$completedFirst = $this->callAPISuccess('Activity', 'get', array(
'id' => array('IN' => $ids),
'options' => array('sort' => 'is_overdue ASC'),
));
$this->assertEquals(array_reverse($ids), array_keys($completedFirst['values']));
$overdueFirst = $this->callAPISuccess('Activity', 'get', array(
'id' => array('IN' => $ids),
'options' => array('sort' => 'is_overdue DESC'),
'return' => 'is_overdue',
));
$this->assertEquals($ids, array_keys($overdueFirst['values']));
// Test return value
$this->assertEquals(1, $overdueFirst['values'][$overdueAct['id']]['is_overdue']);
$this->assertEquals(0, $overdueFirst['values'][$completedAct['id']]['is_overdue']);
// Test filtering
$onlyOverdue = $this->callAPISuccess('Activity', 'get', array(
'id' => array('IN' => $ids),
'is_overdue' => 1,
));
$this->assertEquals(array($overdueAct['id']), array_keys($onlyOverdue['values']));
}
}
......@@ -639,14 +639,14 @@ VALUES
(@option_group_id_report, {localize}'{ts escape="sql"}Recurring Contributions Summary{/ts}'{/localize}, 'contribute/recursummary', 'CRM_Report_Form_Contribute_RecurSummary', NULL, 0, NULL, 49, {localize}'{ts escape="sql"}Provides simple summary for each payment instrument for which there are recurring contributions (e.g. Credit Card, Standing Order, Direct Debit, etc., NULL), showing within a given date range.{/ts}'{/localize}, 0, 0, 1, @contributeCompId, NULL, NULL),
(@option_group_id_report, {localize}'{ts escape="sql"}Deferred Revenue Details{/ts}'{/localize}, 'contribute/deferredrevenue', 'CRM_Report_Form_Contribute_DeferredRevenue', NULL, 0, NULL, 50, {localize}'{ts escape="sql"}Deferred Revenue Details Report{/ts}'{/localize}, 0, 0, 1, @contributeCompId, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Scheduled{/ts}', 1, 'Scheduled', NULL, 0, 1, 1, NULL, 0, 1, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Completed{/ts}', 2, 'Completed', NULL, 0, NULL, 2, NULL, 0, 1, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Cancelled{/ts}', 3, 'Cancelled', NULL, 0, NULL, 3, NULL, 0, 1, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Scheduled{/ts}', 1, 'Scheduled', NULL, 0, 1, 1, NULL, 0, 1, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Completed{/ts}', 2, 'Completed', NULL, 1, NULL, 2, NULL, 0, 1, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Cancelled{/ts}', 3, 'Cancelled', NULL, 2, NULL, 3, NULL, 0, 1, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Left Message{/ts}', 4, 'Left Message', NULL, 0, NULL, 4, NULL, 0, 0, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Unreachable{/ts}', 5, 'Unreachable', NULL, 0, NULL, 5, NULL, 0, 0, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Not Required{/ts}', 6, 'Not Required', NULL, 0, NULL, 6, NULL, 0, 0, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Available{/ts}', 7, 'Available', NULL, 0, NULL, 7, NULL, 0, 0, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}No-show{/ts}', 8, 'No_show', NULL, 0, NULL, 8, NULL, 0, 0, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Unreachable{/ts}', 5, 'Unreachable', NULL, 2, NULL, 5, NULL, 0, 0, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Not Required{/ts}', 6, 'Not Required', NULL, 2, NULL, 6, NULL, 0, 0, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}Available{/ts}', 7, 'Available', NULL, 0, NULL, 7, NULL, 0, 0, 1, NULL, NULL, NULL),
(@option_group_id_acs, '{ts escape="sql"}No-show{/ts}', 8, 'No_show', NULL, 2, NULL, 8, NULL, 0, 0, 1, NULL, NULL, NULL),
(@option_group_id_cas, '{ts escape="sql"}Ongoing{/ts}' , 1, 'Open' , 'Opened', 0, 1, 1, NULL, 0, 1, 1, NULL, NULL, NULL),
(@option_group_id_cas, '{ts escape="sql"}Resolved{/ts}', 2, 'Closed', 'Closed', 0, NULL, 2, NULL, 0, 1, 1, NULL, NULL, NULL),
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment