diff --git a/CHANGELOG.md b/CHANGELOG.md index 7117eb68f9f5594582cf185acc18068112df743c..8f93db120d39c2b933802bfed5c06f534c3fa1a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Version 1.73 (not yet released) +* Added filter: Contact has number of activities in period * Added filter: Most recent event date # Version 1.72 diff --git a/CRM/Dataprocessor/Utils/Form.php b/CRM/Dataprocessor/Utils/Form.php index 15f1e59891143ea8ce3d1518a4023f479262e457..5228fcbee86601f042aa820fd72213b6fd06ece3 100644 --- a/CRM/Dataprocessor/Utils/Form.php +++ b/CRM/Dataprocessor/Utils/Form.php @@ -28,8 +28,9 @@ class CRM_Dataprocessor_Utils_Form { * @param array $additionalOptions * @param string $to string to append to the to field. * @param string $from string to append to the from field. + * @param string $additionalClass */ - public static function addDatePickerRange($form, $fieldName, $label, $isDateTime = FALSE, $required = FALSE, $fromLabel = 'From', $toLabel = 'To', $additionalOptions = [], $to = '_high', $from = '_low') { + public static function addDatePickerRange($form, $fieldName, $label, $isDateTime = FALSE, $required = FALSE, $fromLabel = 'From', $toLabel = 'To', $additionalOptions = [], $to = '_high', $from = '_low', $additionalClass='') { $options = [ '' => ts('- any -'), 0 => ts('Choose Date Range'), @@ -40,13 +41,12 @@ class CRM_Dataprocessor_Utils_Form { $options[$key] = $optionLabel; } } - $form->add('select', "{$fieldName}_relative", $label, $options, $required, - ['class' => 'crm-select2'] + ['class' => 'crm-select2 ' . $additionalClass] ); $attributes = ['formatType' => 'searchDate']; $extra = ['time' => $isDateTime]; diff --git a/Civi/DataProcessor/Factory.php b/Civi/DataProcessor/Factory.php index 96bc3160bdfcbd3579e63f1267105a9a831dbddc..20cc9d029d079d94070d13c85dc8a0f1fe76a094 100644 --- a/Civi/DataProcessor/Factory.php +++ b/Civi/DataProcessor/Factory.php @@ -177,6 +177,7 @@ class Factory { $this->addFilter('contact_has_active_recurring_contribution', new Definition('Civi\DataProcessor\FilterHandler\ContactHasActiveRecurringContributions'), E::ts('Contact has active recurring contributions')); $this->addFilter('contact_has_relationship', new Definition('Civi\DataProcessor\FilterHandler\ContactHasRelationship'), E::ts('Contact has a relationship')); $this->addFilter('contact_has_activity_in_period_filter', new Definition('Civi\DataProcessor\FilterHandler\ContactHasActivityInPeriodFilter'), E::ts('Contact has activity in period')); + $this->addFilter('contact_has_number_activities_in_period_filter', new Definition('Civi\DataProcessor\FilterHandler\ContactHasNumberOfActivitiesInPeriodFilter'), E::ts('Contact has number of activities in period')); $this->addFilter('worldregion_filter', new Definition('Civi\DataProcessor\FilterHandler\WorldRegionFilter'), E::ts('World Region Filter')); $this->addFilter('contact_type_filter', new Definition('Civi\DataProcessor\FilterHandler\ContactTypeFilter'), E::ts('Contact Type filter')); $this->addFilter('permission_to_view_contact', new Definition('Civi\DataProcessor\FilterHandler\PermissionToViewContactFilter'), E::ts('Permission to view contact')); diff --git a/Civi/DataProcessor/FilterHandler/AbstractFieldInPeriodFilter.php b/Civi/DataProcessor/FilterHandler/AbstractFieldInPeriodFilter.php index d8e15a145dd6868992694dc032c3e0e5e431e0a5..2d1de2d8a7df371b367355841b5efd12de2b533e 100644 --- a/Civi/DataProcessor/FilterHandler/AbstractFieldInPeriodFilter.php +++ b/Civi/DataProcessor/FilterHandler/AbstractFieldInPeriodFilter.php @@ -60,7 +60,8 @@ abstract class AbstractFieldInPeriodFilter extends AbstractFieldFilterHandler { } catch (CRM_Core_Exception $e) { } - CRM_Dataprocessor_Utils_Form::addDatePickerRange($form, $alias, $title, FALSE, FALSE, E::ts('From'), E::ts('To')); + CRM_Dataprocessor_Utils_Form::addDatePickerRange($form, $alias, $title, FALSE, FALSE, E::ts('From'), E::ts('To'), [], '_high', '_low', $sizeClass); + if (isset($defaultFilterValue['value'])) { $defaults[$alias.'_value'] = $defaultFilterValue['value']; } diff --git a/Civi/DataProcessor/FilterHandler/ContactHasActivityInPeriodFilter.php b/Civi/DataProcessor/FilterHandler/ContactHasActivityInPeriodFilter.php index 0f494cf92df6538ec3c3a2e5c318c46a64143ea9..c9453043de86591c1644721cc5b1bf9a893f96ec 100644 --- a/Civi/DataProcessor/FilterHandler/ContactHasActivityInPeriodFilter.php +++ b/Civi/DataProcessor/FilterHandler/ContactHasActivityInPeriodFilter.php @@ -310,32 +310,49 @@ class ContactHasActivityInPeriodFilter extends AbstractFieldInPeriodFilter { $tableAlias = $this->getTableAlias($dataFlow); $fieldName = $this->inputFieldSpecification->getName(); $fieldAlias = $this->inputFieldSpecification->alias; - $sqlStatement = "`$tableAlias`.`$fieldName` {$filterParams['op']} ( - SELECT `contact_id` + + $select = "SELECT `contact_id`"; + $from = " FROM `civicrm_activity` `activity_$fieldAlias` INNER JOIN `civicrm_activity_contact` `activity_contact_$fieldAlias` ON `activity_$fieldAlias`.`id` = `activity_contact_$fieldAlias`.`activity_id` - WHERE 1"; + "; + $where = "WHERE 1"; if (isset($filterParams['status_ids']) && is_array($filterParams['status_ids']) && count($filterParams['status_ids'])) { - $sqlStatement .= " AND `activity_$fieldAlias}`.`status_id` IN (" . implode(",", $filterParams['status_ids']) . ")"; + $where .= " AND `activity_$fieldAlias`.`status_id` IN (" . implode(",", $filterParams['status_ids']) . ")"; } if (isset($filterParams['type_ids']) && is_array($filterParams['type_ids']) && count($filterParams['type_ids'])) { - $sqlStatement .= " AND `activity_$fieldAlias`.`activity_type_id` IN (" . implode(",", $filterParams['type_ids']) . ")"; + $where .= " AND `activity_$fieldAlias`.`activity_type_id` IN (" . implode(",", $filterParams['type_ids']) . ")"; } if (isset($filterParams['record_type_ids']) && is_array($filterParams['record_type_ids']) && count($filterParams['record_type_ids'])) { - $sqlStatement .= " AND `activity_contact_$fieldAlias`.`record_type_id` IN (" . implode(",", $filterParams['record_type_ids']) . ")"; + $where .= " AND `activity_contact_$fieldAlias`.`record_type_id` IN (" . implode(",", $filterParams['record_type_ids']) . ")"; } if (isset($filterParams['campaign_ids']) && is_array($filterParams['campaign_ids']) && count($filterParams['campaign_ids'])) { - $sqlStatement .= " AND `activity_$fieldAlias`.`campaign_id` IN (" . implode(",", $filterParams['campaign_ids']) . ")"; + $where .= " AND `activity_$fieldAlias`.`campaign_id` IN (" . implode(",", $filterParams['campaign_ids']) . ")"; } - $sqlStatement .= " AND " . $this->getDateSqlStatement('activity_'.$fieldAlias, 'activity_date_time', $filterParams); - $sqlStatement .= ")"; - $this->whereClause = new SqlDataFlow\PureSqlStatementClause($sqlStatement, FALSE); + $where .= " AND " . $this->getDateSqlStatement('activity_'.$fieldAlias, 'activity_date_time', $filterParams); + + $subQueryStatement = $this->getSubQueryStatement($select, $from, $where, $filterParams); + $this->whereClause = new SqlDataFlow\PureSqlStatementClause("`$tableAlias`.`$fieldName` {$filterParams['op']} ($subQueryStatement)", FALSE); $this->filterCollection->addWhere($this->whereClause); } } catch (Exception $e) { } } - + /** + * Returns the subquery statement. + * + * This function can be overridden in child classes to alter the subquery. + * + * @param string $select + * @param string $from + * @param string $where + * @param array $filterParams + * + * @return string + */ + protected function getSubQueryStatement(string $select, string $from, string $where, array $filterParams): string { + return trim($select) . " " . trim($from) . " " . trim($where); + } } diff --git a/Civi/DataProcessor/FilterHandler/ContactHasNumberOfActivitiesInPeriodFilter.php b/Civi/DataProcessor/FilterHandler/ContactHasNumberOfActivitiesInPeriodFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..1276e3e4efab9e84c76766535a03c3852f837571 --- /dev/null +++ b/Civi/DataProcessor/FilterHandler/ContactHasNumberOfActivitiesInPeriodFilter.php @@ -0,0 +1,187 @@ +<?php +/** + * Copyright (C) 2023 Jaap Jansma (jaap.jansma@civicoop.org) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +namespace Civi\DataProcessor\FilterHandler; + +use Civi\DataProcessor\DataSpecification\FieldSpecification; +use CRM_Core_Exception; +use CRM_Core_Form; +use CRM_Dataprocessor_ExtensionUtil as E; +use CRM_Utils_Type; + +class ContactHasNumberOfActivitiesInPeriodFilter extends ContactHasActivityInPeriodFilter { + + protected function getOperatorOptions(FieldSpecification $fieldSpec): array { + return array( + 'IN' => E::ts('Has number of activities in period'), + 'NOT IN' => E::ts('Does not have this number of activities in period'), + ); + } + + /** + * Add addition filter fields to the API specs. + * + * @param array $specs + * + * @return array + */ + public function enhanceApi3FieldSpec(array $specs): array { + $specs = parent::enhanceApi3FieldSpec($specs); + $fieldSpec = $this->getFieldSpecification(); + $alias = $fieldSpec->alias; + $specs["{$alias}_min"] = [ + 'name' => "{$alias}_min", + 'title' => $fieldSpec->title .': '.E::ts('Min number of activities'), + 'api.filter' => TRUE, + 'api.return' => FALSE, + 'type' => CRM_Utils_Type::T_INT, + ]; + $specs["{$alias}_max"] = [ + 'name' => "{$alias}_max", + 'title' => $fieldSpec->title .': '.E::ts('Max number of activities'), + 'api.filter' => TRUE, + 'api.return' => FALSE, + 'type' => CRM_Utils_Type::T_INT, + ]; + return $specs; + } + + /** + * Add the elements to the filter form. + * + * @param \CRM_Core_Form $form + * @param array $defaultFilterValue + * @param string $size + * Possible values: full or compact + * @return array + * Return variables belonging to this filter. + */ + public function addToFilterForm(CRM_Core_Form $form, $defaultFilterValue, $size='full'): array { + $filter = parent::addToFilterForm($form, $defaultFilterValue, $size); + $fieldSpec = $this->getFieldSpecification(); + $alias = $fieldSpec->alias; + + try { + $form->add('text', "{$alias}_min", E::ts('Min'), [ + 'class' => 'four', + ]); + $form->add('text', "{$alias}_max", E::ts('Max'), [ + 'class' => 'four', + ]); + } catch (CRM_Core_Exception $e) { + } + + $defaults = []; + if (isset($defaultFilterValue['min'])) { + $defaults[$alias.'_min'] = $defaultFilterValue['min']; + } + if (isset($defaultFilterValue['max'])) { + $defaults[$alias.'_max'] = $defaultFilterValue['max']; + } + if (count($defaults)) { + $form->setDefaults($defaults); + } + return $filter; + } + + /** + * File name of the template to add this filter to the criteria form. + * + * @return string + */ + public function getTemplateFileName(): string { + return "CRM/Dataprocessor/Form/Filter/ContactHasNumberOfActivitiesInPeriodFilter.tpl"; + } + + /** + * Process the submitted values to a filter value + * Which could then be processed by applyFilter function + * + * @param $submittedValues + * @return array + */ + public function processSubmittedValues($submittedValues): array { + $return = parent::processSubmittedValues($submittedValues); + $filterSpec = $this->getFieldSpecification(); + $alias = $filterSpec->alias; + if (isset($submittedValues[$alias.'_min'])) { + $return['min'] = $submittedValues[$alias . '_min']; + } + if (isset($submittedValues[$alias.'_max'])) { + $return['max'] = $submittedValues[$alias . '_max']; + } + return $return; + } + + /** + * Extend the filter params from the submitted values. + * + * @param array $submittedValues + * @param array $filterParams + * + * @return array + */ + public function extendFilterParamsFromSubmittedValues(array $submittedValues, array $filterParams): array { + $filterParams = parent::extendFilterParamsFromSubmittedValues($submittedValues, $filterParams); + if (isset($submittedValues['min']) && is_numeric($submittedValues['min']) && strlen($submittedValues['min'])) { + $filterParams['min'] = $submittedValues['min']; + } + if (isset($submittedValues['max']) && is_numeric($submittedValues['max']) && strlen($submittedValues['max'])) { + $filterParams['max'] = $submittedValues['max']; + } + return $filterParams; + } + + /** + * Returns the subquery statement. + * + * This function can be overridden in child classes to alter the subquery. + * + * @param string $select + * @param string $from + * @param string $where + * @param array $filterParams + * + * @return string + */ + protected function getSubQueryStatement(string $select, string $from, string $where, array $filterParams): string { + $groupBy = "GROUP BY `contact_id`"; + $havingClauses = []; + $having = ""; + + $fieldAlias = $this->inputFieldSpecification->alias; + if (isset($filterParams['min']) && strlen($filterParams['min']) && is_numeric($filterParams['min'])) { + try { + $havingClauses[] = "COUNT(`activity_$fieldAlias`.`id`) >= " . CRM_Utils_Type::escape($filterParams['min'], "Positive"); + } catch (CRM_Core_Exception $e) { + } + } + if (isset($filterParams['max']) && strlen($filterParams['max']) && is_numeric($filterParams['max'])) { + try { + $havingClauses[] = "COUNT(`activity_$fieldAlias`.`id`) <= " . CRM_Utils_Type::escape($filterParams['max'], "Positive"); + } catch (CRM_Core_Exception $e) { + } + } + if (count($havingClauses)) { + $having = "HAVING " . implode(" AND ", $havingClauses); + } + + return trim(trim($select) . " " . trim($from) . " " . trim($where) . " ".$groupBy . " " . $having); + } + +} diff --git a/templates/CRM/Dataprocessor/Form/Filter/ContactHasNumberOfActivitiesInPeriodFilter.tpl b/templates/CRM/Dataprocessor/Form/Filter/ContactHasNumberOfActivitiesInPeriodFilter.tpl new file mode 100644 index 0000000000000000000000000000000000000000..c1103a00352207b787ca235e822d9f49e2f445a3 --- /dev/null +++ b/templates/CRM/Dataprocessor/Form/Filter/ContactHasNumberOfActivitiesInPeriodFilter.tpl @@ -0,0 +1,29 @@ +{crmScope extensionKey='dataprocessor'} +{assign var=fieldOp value=$filter.alias|cat:"_op"} +{assign var=filterMin value=$filter.alias|cat:"_min"} +{assign var=filterMax value=$filter.alias|cat:"_max"} +{assign var=filterStatusIds value=$filter.alias|cat:"_status_ids"} +{assign var=filterActivityTypeIds value=$filter.alias|cat:"_type_ids"} +{assign var=filterRecordTypeIds value=$filter.alias|cat:"_record_type_ids"} +{assign var=filterCampaignIds value=$filter.alias|cat:"_campaign_ids"} + +<tr> + <td class="label">{$filter.title}</td> + <td> + {if $form.$fieldOp.html} + <span class="filter-processor-element filter-{$filter.alias}">{$form.$fieldOp.html}</span> + <span class="filter-processor-show-close filter-{$filter.alias}"> </span> + {/if} + </td> + <td> + {include file="CRM/Dataprocessor/Form/Filter/DateRange.tpl" fieldName=$filter.alias from='_low' to='_high'} + <p> + {ts}Number of activities: {/ts}<br /><span id="{$filterMin}_max_cell">{$form.$filterMin.label} {$form.$filterMin.html} {$form.$filterMax.label} {$form.$filterMax.html}</span> <br /> + {ts}With status: {/ts}<br /><span id="{$filterStatusIds}_cell">{$form.$filterStatusIds.html}</span> <br /> + {ts}With activity type: {/ts}<br /><span id="{$filterActivityTypeIds}_cell">{$form.$filterActivityTypeIds.html}</span> <br /> + {ts}With contact record type: {/ts}<br /><span id="{$filterRecordTypeIds}_cell">{$form.$filterRecordTypeIds.html}</span> <br /> + {ts}With campaign: {/ts}<br /><span id="{$filterCampaignIds}_cell">{$form.$filterCampaignIds.html}</span> + </p> + </td> +</tr> +{/crmScope}