Skip to content
Snippets Groups Projects
Unverified Commit f4aed099 authored by Eileen McNaughton's avatar Eileen McNaughton Committed by GitHub
Browse files

Merge pull request #14086 from agh1/caseactqueryfix

Case activity query performs poorly on large databases
parents c99fd2bc 8cf4fbdb
No related branches found
No related tags found
No related merge requests found
......@@ -429,121 +429,60 @@ WHERE cc.contact_id = %1 AND civicrm_case_type.name = '{$caseType}'";
'civicrm_phone.phone as phone',
'civicrm_contact.contact_type as contact_type',
'civicrm_contact.contact_sub_type as contact_sub_type',
't_act.activity_type_id',
'c_type.title as case_type',
't_act.activity_type_id as activity_type_id',
'civicrm_case.case_type_id as case_type_id',
'cov_status.label as case_status',
'cov_status.label as case_status_name',
't_act.status_id',
'civicrm_case.status_id as case_status_id',
't_act.status_id as status_id',
'civicrm_case.start_date as case_start_date',
'case_relation_type.label_b_a as case_role',
't_act.activity_date_time as activity_date_time',
't_act.id as activity_id',
);
if ($type == 'upcoming') {
$selectClauses = array_merge($selectClauses, array(
't_act.desired_date as case_scheduled_activity_date',
't_act.id as case_scheduled_activity_id',
't_act.act_type_name as case_scheduled_activity_type_name',
't_act.act_type AS case_scheduled_activity_type',
));
}
elseif ($type == 'recent') {
$selectClauses = array_merge($selectClauses, array(
't_act.desired_date as case_recent_activity_date',
't_act.id as case_recent_activity_id',
't_act.act_type_name as case_recent_activity_type_name',
't_act.act_type AS case_recent_activity_type',
));
}
elseif ($type == 'any') {
$selectClauses = array_merge($selectClauses, array(
't_act.desired_date as case_activity_date',
't_act.id as case_activity_id',
't_act.act_type_name as case_activity_type_name',
't_act.act_type AS case_activity_type',
));
}
$query = CRM_Contact_BAO_Query::appendAnyValueToSelect($selectClauses, 'case_id');
$query .= " FROM civicrm_case
INNER JOIN civicrm_case_contact ON civicrm_case.id = civicrm_case_contact.case_id
INNER JOIN civicrm_contact ON civicrm_case_contact.contact_id = civicrm_contact.id ";
if ($type == 'upcoming') {
// This gets the earliest activity per case that's scheduled within 14 days from now.
// Note we have an inner select to get the min activity id in order to remove duplicates in case there are two with the same datetime.
// In this case we don't really care which one, so min(id) works.
// optimized in CRM-11837
$query .= " INNER JOIN
(
SELECT case_id, act.id, activity_date_time AS desired_date, activity_type_id, status_id, aov.name AS act_type_name, aov.label AS act_type
FROM (
SELECT *
FROM (
SELECT *
FROM civicrm_view_case_activity_upcoming
ORDER BY activity_date_time ASC, id ASC
) AS upcomingOrdered
) AS act
LEFT JOIN civicrm_option_group aog ON aog.name='activity_type'
LEFT JOIN civicrm_option_value aov ON ( aov.option_group_id = aog.id AND aov.value = act.activity_type_id )
) AS t_act
";
}
elseif ($type == 'recent') {
// Similarly, the most recent activity in the past 14 days, and exclude scheduled.
//improve query performance - CRM-10598
$query .= " INNER JOIN
(
SELECT case_id, act.id, activity_date_time AS desired_date, activity_type_id, status_id, aov.name AS act_type_name, aov.label AS act_type
FROM (
SELECT *
FROM (
SELECT *
FROM civicrm_view_case_activity_recent
ORDER BY activity_date_time DESC, id ASC
) AS recentOrdered
) AS act
LEFT JOIN civicrm_option_group aog ON aog.name='activity_type'
LEFT JOIN civicrm_option_value aov ON ( aov.option_group_id = aog.id AND aov.value = act.activity_type_id )
) AS t_act ";
}
elseif ($type == 'any') {
$query .= " LEFT JOIN
(
SELECT ca4.case_id, act4.id AS id, act4.activity_date_time AS desired_date, act4.activity_type_id, act4.status_id, aov.name AS act_type_name, aov.label AS act_type
FROM civicrm_activity act4
LEFT JOIN civicrm_case_activity ca4
ON ca4.activity_id = act4.id
AND act4.is_current_revision = 1
LEFT JOIN civicrm_option_group aog
ON aog.name='activity_type'
LEFT JOIN civicrm_option_value aov
ON aov.option_group_id = aog.id
AND aov.value = act4.activity_type_id
) AS t_act";
}
$query .= "
ON t_act.case_id = civicrm_case.id
LEFT JOIN civicrm_phone ON (civicrm_phone.contact_id = civicrm_contact.id AND civicrm_phone.is_primary=1)
LEFT JOIN civicrm_relationship case_relationship
ON ( case_relationship.contact_id_a = civicrm_case_contact.contact_id AND case_relationship.contact_id_b = {$userID} AND case_relationship.is_active AND case_relationship.case_id = civicrm_case.id )
LEFT JOIN civicrm_relationship_type case_relation_type
ON ( case_relation_type.id = case_relationship.relationship_type_id
AND case_relation_type.id = case_relationship.relationship_type_id )
LEFT JOIN civicrm_case_type c_type
ON civicrm_case.case_type_id = c_type.id
LEFT JOIN civicrm_option_group cog_status
ON cog_status.name = 'case_status'
LEFT JOIN civicrm_option_value cov_status
ON ( civicrm_case.status_id = cov_status.value
AND cog_status.id = cov_status.option_group_id )
";
$query .= <<<HERESQL
FROM civicrm_case
INNER JOIN civicrm_case_contact ON civicrm_case.id = civicrm_case_contact.case_id
INNER JOIN civicrm_contact ON civicrm_case_contact.contact_id = civicrm_contact.id
HERESQL;
switch ($type) {
case 'upcoming':
case 'recent':
// civicrm_view_case_activity_upcoming and
// civicrm_view_case_activity_recent are views that show the next
// scheduled and most recent not-scheduled activity on each case,
// respectively.
$query .= <<<HERESQL
INNER JOIN civicrm_view_case_activity_$type t_act
ON t_act.case_id = civicrm_case.id
HERESQL;
break;
case 'any':
$query .= <<<HERESQL
LEFT JOIN civicrm_case_activity ca4
ON civicrm_case.id = ca4.case_id
LEFT JOIN civicrm_activity t_act
ON t_act.id = ca4.activity_id
AND t_act.is_current_revision = 1
HERESQL;
}
$query .= <<<HERESQL
LEFT JOIN civicrm_phone
ON civicrm_phone.contact_id = civicrm_contact.id
AND civicrm_phone.is_primary = 1
LEFT JOIN civicrm_relationship case_relationship
ON case_relationship.contact_id_a = civicrm_case_contact.contact_id
AND case_relationship.contact_id_b = {$userID}
AND case_relationship.is_active
AND case_relationship.case_id = civicrm_case.id
LEFT JOIN civicrm_relationship_type case_relation_type
ON case_relation_type.id = case_relationship.relationship_type_id
AND case_relation_type.id = case_relationship.relationship_type_id
HERESQL;
if ($condition) {
// CRM-8749 backwards compatibility - callers of this function expect to start $condition with "AND"
......@@ -551,20 +490,7 @@ LEFT JOIN civicrm_option_group aog ON aog.name='activity_type'
}
$query .= " GROUP BY case_id ";
if ($order) {
$query .= $order;
}
else {
if ($type == 'upcoming') {
$query .= " ORDER BY case_scheduled_activity_date ASC ";
}
elseif ($type == 'recent') {
$query .= " ORDER BY case_recent_activity_date ASC ";
}
elseif ($type == 'any') {
$query .= " ORDER BY case_activity_date ASC ";
}
}
$query .= ($order) ?: ' ORDER BY activity_date_time ASC';
if ($limit) {
$query .= $limit;
......@@ -601,20 +527,6 @@ LEFT JOIN civicrm_option_group aog ON aog.name='activity_type'
$type = CRM_Utils_Array::value('type', $params, 'upcoming');
$userID = CRM_Core_Session::singleton()->get('userID');
$caseActivityTypeColumn = 'case_activity_type_name';
$caseActivityDateColumn = 'case_activity_date';
$caseActivityIDColumn = 'case_activity_id';
if ($type == 'upcoming') {
$caseActivityDateColumn = 'case_scheduled_activity_date';
$caseActivityTypeColumn = 'case_scheduled_activity_type';
$caseActivityIDColumn = 'case_scheduled_activity_id';
}
elseif ($type == 'recent') {
$caseActivityDateColumn = 'case_recent_activity_date';
$caseActivityTypeColumn = 'case_recent_activity_type';
$caseActivityIDColumn = 'case_recent_activity_id';
}
// validate access for all cases.
if ($allCases && !CRM_Core_Permission::check('access all cases and activities')) {
$allCases = FALSE;
......@@ -654,7 +566,7 @@ LEFT JOIN civicrm_option_group aog ON aog.name='activity_type'
$order = NULL;
if (!empty($params['sortBy'])) {
if (strstr($params['sortBy'], 'date ')) {
$params['sortBy'] = str_replace('date', $caseActivityDateColumn, $params['sortBy']);
$params['sortBy'] = str_replace('date', 'activity_date_time', $params['sortBy']);
}
$order = "ORDER BY " . $params['sortBy'];
}
......@@ -662,8 +574,6 @@ LEFT JOIN civicrm_option_group aog ON aog.name='activity_type'
$query = self::getCaseActivityQuery($type, $userID, $condition, $limit, $order);
$result = CRM_Core_DAO::executeQuery($query);
$caseStatus = CRM_Core_OptionGroup::values('case_status', FALSE, FALSE, FALSE, " AND v.name = 'Urgent' ");
// we're going to use the usual actions, so doesn't make sense to duplicate definitions
$actions = CRM_Case_Selector_Search::links();
......@@ -679,7 +589,12 @@ LEFT JOIN civicrm_option_group aog ON aog.name='activity_type'
}
$mask = CRM_Core_Action::mask($permissions);
// Pseudoconstants to populate labels
$caseStatuses = CRM_Case_PseudoConstant::caseStatus('label', FALSE);
$caseTypes = CRM_Case_PseudoConstant::caseType('name');
$caseTypeTitles = CRM_Case_PseudoConstant::caseType('title', FALSE);
$activityTypeLabels = CRM_Activity_BAO_Activity::buildOptions('activity_type_id');
foreach ($result->fetchAll() as $case) {
$key = $case['case_id'];
$casesList[$key] = array();
......@@ -701,30 +616,31 @@ LEFT JOIN civicrm_option_group aog ON aog.name='activity_type'
$case['case_id']
);
$casesList[$key]['subject'] = $case['case_subject'];
$casesList[$key]['case_status'] = in_array($case['case_status'], $caseStatus) ? sprintf('<strong>%s</strong>', strtoupper($case['case_status'])) : $case['case_status'];
$casesList[$key]['case_type'] = $case['case_type'];
$casesList[$key]['case_status'] = CRM_Utils_Array::value($case['case_status_id'], $caseStatuses);
if ($case['case_status_id'] == CRM_Case_PseudoConstant::getKey('CRM_Case_BAO_Case', 'case_status_id', 'Urgent')) {
$casesList[$key]['case_status'] = sprintf('<strong>%s</strong>', strtoupper($casesList[$key]['case_status']));
}
$casesList[$key]['case_type'] = CRM_Utils_Array::value($case['case_type_id'], $caseTypeTitles);
$casesList[$key]['case_role'] = CRM_Utils_Array::value('case_role', $case, '---');
$casesList[$key]['manager'] = self::getCaseManagerContact($caseTypes[$case['case_type_id']], $case['case_id']);
$casesList[$key]['date'] = $case[$caseActivityTypeColumn];
if (($actId = CRM_Utils_Array::value('case_scheduled_activity_id', $case)) ||
($actId = CRM_Utils_Array::value('case_recent_activity_id', $case))
) {
$casesList[$key]['date'] = CRM_Utils_Array::value($case['activity_type_id'], $activityTypeLabels);
if ($actId = CRM_Utils_Array::value('activity_id', $case)) {
if (self::checkPermission($actId, 'view', $case['activity_type_id'], $userID)) {
if ($type == 'recent') {
$casesList[$key]['date'] = sprintf('<a class="action-item crm-hover-button" href="%s" title="%s">%s</a>',
CRM_Utils_System::url('civicrm/case/activity/view', array('reset' => 1, 'cid' => $case['contact_id'], 'aid' => $case[$caseActivityIDColumn])),
CRM_Utils_System::url('civicrm/case/activity/view', array('reset' => 1, 'cid' => $case['contact_id'], 'aid' => $case['activity_id'])),
ts('View activity'),
$case[$caseActivityTypeColumn]
CRM_Utils_Array::value($case['activity_type_id'], $activityTypeLabels)
);
}
else {
$status = CRM_Utils_Date::overdue($case[$caseActivityDateColumn]) ? 'status-overdue' : 'status-scheduled';
$status = CRM_Utils_Date::overdue($case['activity_date_time']) ? 'status-overdue' : 'status-scheduled';
$casesList[$key]['date'] = sprintf('<a class="crm-popup %s" href="%s" title="%s">%s</a> &nbsp;&nbsp;',
$status,
CRM_Utils_System::url('civicrm/case/activity/view', array('reset' => 1, 'cid' => $case['contact_id'], 'aid' => $case[$caseActivityIDColumn])),
CRM_Utils_System::url('civicrm/case/activity/view', array('reset' => 1, 'cid' => $case['contact_id'], 'aid' => $case['activity_id'])),
ts('View activity'),
$case[$caseActivityTypeColumn]
CRM_Utils_Array::value($case['activity_type_id'], $activityTypeLabels)
);
}
}
......@@ -735,7 +651,7 @@ LEFT JOIN civicrm_option_group aog ON aog.name='activity_type'
);
}
}
$casesList[$key]['date'] .= "<br/>" . CRM_Utils_Date::customFormat($case[$caseActivityDateColumn]);
$casesList[$key]['date'] .= "<br/>" . CRM_Utils_Date::customFormat($case['activity_date_time']);
$casesList[$key]['links'] = CRM_Core_Action::formLink($actions['primaryActions'], $mask,
array(
'id' => $case['case_id'],
......@@ -1627,18 +1543,19 @@ SELECT case_status.label AS case_status, status_id, civicrm_case_type.title AS c
AND civicrm_case.is_deleted = {$cases['case_deleted']}";
$query = self::getCaseActivityQuery($type, $userID, $condition);
$activityTypes = CRM_Activity_BAO_Activity::buildOptions('activity_type_id');
$res = CRM_Core_DAO::executeQuery($query);
$activityInfo = array();
while ($res->fetch()) {
if ($type == 'upcoming') {
$activityInfo[$res->case_id]['date'] = $res->case_scheduled_activity_date;
$activityInfo[$res->case_id]['type'] = $res->case_scheduled_activity_type;
$activityInfo[$res->case_id]['date'] = $res->activity_date_time;
$activityInfo[$res->case_id]['type'] = CRM_Utils_Array::value($res->activity_type_id, $activityTypes);
}
else {
$activityInfo[$res->case_id]['date'] = $res->case_recent_activity_date;
$activityInfo[$res->case_id]['type'] = $res->case_recent_activity_type;
$activityInfo[$res->case_id]['date'] = $res->activity_date_time;
$activityInfo[$res->case_id]['type'] = CRM_Utils_Array::value($res->activity_type_id, $activityTypes);
}
}
......@@ -3055,8 +2972,8 @@ WHERE id IN (' . implode(',', $copiedActivityIds) . ')';
AS SELECT ca.case_id, a.id, a.activity_date_time, a.status_id, a.activity_type_id
FROM civicrm_case_activity ca
INNER JOIN civicrm_activity a ON ca.activity_id=a.id
WHERE a.activity_date_time =
(SELECT b.activity_date_time FROM civicrm_case_activity bca
WHERE a.id =
(SELECT b.id FROM civicrm_case_activity bca
INNER JOIN civicrm_activity b ON bca.activity_id=b.id
WHERE b.activity_date_time <= DATE_ADD( NOW(), INTERVAL 14 DAY )
AND b.is_current_revision = 1 AND b.is_deleted=0 AND b.status_id = $scheduled_id
......@@ -3068,8 +2985,8 @@ WHERE id IN (' . implode(',', $copiedActivityIds) . ')';
AS SELECT ca.case_id, a.id, a.activity_date_time, a.status_id, a.activity_type_id
FROM civicrm_case_activity ca
INNER JOIN civicrm_activity a ON ca.activity_id=a.id
WHERE a.activity_date_time =
(SELECT b.activity_date_time FROM civicrm_case_activity bca
WHERE a.id =
(SELECT b.id FROM civicrm_case_activity bca
INNER JOIN civicrm_activity b ON bca.activity_id=b.id
WHERE b.activity_date_time >= DATE_SUB( NOW(), INTERVAL 14 DAY )
AND b.is_current_revision = 1 AND b.is_deleted=0 AND b.status_id <> $scheduled_id
......
......@@ -31,6 +31,28 @@ class CRM_Case_BAO_CaseTest extends CiviUnitTestCase {
CRM_Core_BAO_ConfigSetting::enableComponent('CiviCase');
}
/**
* Make sure that the latest case activity works accurately.
*/
public function testCaseActivity() {
$userID = $this->createLoggedInUser();
$addTimeline = civicrm_api3('Case', 'addtimeline', [
'case_id' => 1,
'timeline' => "standard_timeline",
]);
$query = CRM_Case_BAO_Case::getCaseActivityQuery('recent', $userID, ' civicrm_case.id IN( 1 )');
$res = CRM_Core_DAO::executeQuery($query);
$openCaseType = CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Open Case');
while ($res->fetch()) {
$message = 'Failed asserting that the case activity query has a activity_type_id property:';
$this->assertObjectHasAttribute('activity_type_id', $res, $message . PHP_EOL . print_r($res, TRUE));
$message = 'Failed asserting that the latest activity from Case ID 1 was "Open Case":';
$this->assertEquals($openCaseType, $res->activity_type_id, $message . PHP_EOL . print_r($res, TRUE));
}
}
protected function tearDown() {
parent::tearDown();
$this->quickCleanup($this->tablesToTruncate, TRUE);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment