From 116710735ac7445caf2536dc58e8848a71a8e469 Mon Sep 17 00:00:00 2001
From: Jaap Jansma <jaap.jansma@civicoop.org>
Date: Tue, 11 Feb 2020 17:16:46 +0100
Subject: [PATCH] Added exposure of aggregation on search/report

---
 CHANGELOG.md                                  |   1 +
 .../Form/Output/AbstractUIOutputForm.php      |  12 +-
 .../Form/AbstractSearch.php                   |   2 +
 CRM/DataprocessorSearch/Form/Search.php       |  84 ++++++++++++
 CRM/DataprocessorSearch/Search.php            |   5 +
 .../DataFlow/AbstractDataFlow.php             |  12 ++
 .../DateFieldOutputHandler.php                |  32 +++++
 .../OptionFieldOutputHandler.php              | 128 +++++++++++++++++-
 .../OutputHandlerAggregate.php                |  15 ++
 .../RawFieldOutputHandler.php                 |  32 +++++
 .../OptionFieldOutputHandler.tpl              |   8 ++
 .../Form/Output/UIOutput/CriteriaForm.tpl     |   4 +
 .../Form/Criteria/AggregateCriteria.tpl       |   7 +
 .../Form/OutputConfiguration/Search.tpl       |   5 +
 14 files changed, 345 insertions(+), 2 deletions(-)
 create mode 100644 templates/CRM/Dataprocessor/Form/Field/Configuration/OptionFieldOutputHandler.tpl
 create mode 100644 templates/CRM/DataprocessorSearch/Form/Criteria/AggregateCriteria.tpl

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fec9a57b..6be0d376 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 * Change Group Filter so that it also works with smart groups
 * Fixed bug with date filter
 * Added date group by function to date output field handler.
+* Added exposure of Aggregation on the Search/Report output.
 
 # Version 1.1.0
 
diff --git a/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php b/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php
index 768af4d7..bea25b7f 100644
--- a/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php
+++ b/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php
@@ -66,7 +66,7 @@ abstract class CRM_Dataprocessor_Form_Output_AbstractUIOutputForm extends CRM_Co
       $dataProcessorName = $this->getDataProcessorName();
       $sql = "
         SELECT civicrm_data_processor.id as data_processor_id,  civicrm_data_processor_output.id AS output_id
-        FROM civicrm_data_processor 
+        FROM civicrm_data_processor
         INNER JOIN civicrm_data_processor_output ON civicrm_data_processor.id = civicrm_data_processor_output.data_processor_id
         WHERE is_active = 1 AND civicrm_data_processor.name = %1 AND civicrm_data_processor_output.type = %2
       ";
@@ -181,6 +181,7 @@ abstract class CRM_Dataprocessor_Form_Output_AbstractUIOutputForm extends CRM_Co
         $filterElements[$fieldSpec->alias]['template'] = $filterHandler->getTemplateFileName();
       }
       $this->assign('filters', $filterElements);
+      $this->assign('additional_criteria_template', $this->getAdditionalCriteriaTemplate());
     }
   }
 
@@ -193,4 +194,13 @@ abstract class CRM_Dataprocessor_Form_Output_AbstractUIOutputForm extends CRM_Co
   protected function getCriteriaElementSize() {
     return 'full';
   }
+
+  /**
+   * Returns the name of the additional criteria template.
+   *
+   * @return false|String
+   */
+  protected function getAdditionalCriteriaTemplate() {
+    return false;
+  }
 }
diff --git a/CRM/DataprocessorSearch/Form/AbstractSearch.php b/CRM/DataprocessorSearch/Form/AbstractSearch.php
index ea7509c6..29dd4bbf 100644
--- a/CRM/DataprocessorSearch/Form/AbstractSearch.php
+++ b/CRM/DataprocessorSearch/Form/AbstractSearch.php
@@ -228,6 +228,8 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
       $sortFieldName = $sortField['name'];
     }
 
+    $this->alterDataProcessor($this->dataProcessorClass);
+
     $output = civicrm_api3("DataProcessorOutput", "getsingle", array('id' => $export_id));
     $outputClass = $factory->getOutputByName($output['type']);
     if ($outputClass instanceof \Civi\DataProcessor\Output\ExportOutputInterface) {
diff --git a/CRM/DataprocessorSearch/Form/Search.php b/CRM/DataprocessorSearch/Form/Search.php
index f398ba30..1234d22a 100644
--- a/CRM/DataprocessorSearch/Form/Search.php
+++ b/CRM/DataprocessorSearch/Form/Search.php
@@ -90,4 +90,88 @@ class CRM_DataprocessorSearch_Form_Search extends CRM_DataprocessorSearch_Form_A
     return $this->_taskList;
   }
 
+  /**
+   * Build the criteria form
+   */
+  protected function buildCriteriaForm() {
+    parent::buildCriteriaForm();
+    $this->buildAggregateForm();
+  }
+
+  /**
+   * Returns the name of the additional criteria template.
+   *
+   * @return false|String
+   */
+  protected function getAdditionalCriteriaTemplate() {
+    if (isset($this->dataProcessorOutput['configuration']['expose_aggregate']) && $this->dataProcessorOutput['configuration']['expose_aggregate']) {
+      return "CRM/DataprocessorSearch/Form/Criteria/AggregateCriteria.tpl";
+    }
+    return false;
+  }
+
+
+  /**
+   * Build the aggregate form
+   */
+  protected function buildAggregateForm() {
+    if (!isset($this->dataProcessorOutput['configuration']['expose_aggregate']) || !$this->dataProcessorOutput['configuration']['expose_aggregate']) {
+      return;
+    }
+    $size = $this->getCriteriaElementSize();
+
+    $sizeClass = 'huge';
+    $minWidth = 'min-width: 250px;';
+    if ($size =='compact') {
+      $sizeClass = 'medium';
+      $minWidth = '';
+    }
+
+    $aggregateFields = array();
+    $defaults = array();
+    foreach ($this->dataProcessorClass->getDataFlow()->getOutputFieldHandlers() as $outputFieldHandler) {
+      if ($outputFieldHandler instanceof \Civi\DataProcessor\FieldOutputHandler\OutputHandlerAggregate) {
+        $aggregateFields[$outputFieldHandler->getAggregateFieldSpec()->alias] = $outputFieldHandler->getOutputFieldSpecification()->title;
+        if ($outputFieldHandler->isAggregateField()) {
+          $defaults[] = $outputFieldHandler->getAggregateFieldSpec()->alias;
+        }
+      }
+    }
+
+    $this->add('select', "aggregateFields", '', $aggregateFields, false, [
+      'style' => $minWidth,
+      'class' => 'crm-select2 '.$sizeClass,
+      'multiple' => TRUE,
+      'placeholder' => E::ts('- Select -'),
+    ]);
+
+    $this->setDefaults(['aggregateFields' => $defaults]);
+  }
+
+  /**
+   * Alter the data processor.
+   *
+   * Use this function in child classes to add for example additional filters.
+   *
+   * E.g. The contact summary tab uses this to add additional filtering on the contact id of
+   * the displayed contact.
+   *
+   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass
+   */
+  protected function alterDataProcessor(\Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass) {
+    if (isset($this->dataProcessorOutput['configuration']['expose_aggregate']) && $this->dataProcessorOutput['configuration']['expose_aggregate']) {
+      $aggregateFields = $this->_formValues['aggregateFields'];
+      foreach ($this->dataProcessorClass->getDataFlow()->getOutputFieldHandlers() as $outputFieldHandler) {
+        if ($outputFieldHandler instanceof \Civi\DataProcessor\FieldOutputHandler\OutputHandlerAggregate) {
+          $alias = $outputFieldHandler->getAggregateFieldSpec()->alias;
+          if (in_array($alias, $aggregateFields) && !$outputFieldHandler->isAggregateField()) {
+            $outputFieldHandler->enableAggregation();
+          } elseif (!in_array($alias, $aggregateFields) && $outputFieldHandler->isAggregateField()) {
+            $outputFieldHandler->disableAggregation();
+          }
+        }
+      }
+    }
+  }
+
 }
diff --git a/CRM/DataprocessorSearch/Search.php b/CRM/DataprocessorSearch/Search.php
index 625fca7d..d46c1b35 100644
--- a/CRM/DataprocessorSearch/Search.php
+++ b/CRM/DataprocessorSearch/Search.php
@@ -58,6 +58,7 @@ class CRM_DataprocessorSearch_Search implements UIOutputInterface {
 
     $form->add('wysiwyg', 'help_text', E::ts('Help text for this search'), array('rows' => 6, 'cols' => 80));
     $form->add('checkbox', 'expanded_search', E::ts('Expand criteria form initially'));
+    $form->add('checkbox', 'expose_aggregate', E::ts('Expose aggregate options'));
 
     // navigation field
     $navigationOptions = $navigation->getNavigationOptions();
@@ -91,6 +92,9 @@ class CRM_DataprocessorSearch_Search implements UIOutputInterface {
         if (isset($output['configuration']['expanded_search'])) {
           $defaults['expanded_search'] = $output['configuration']['expanded_search'];
         }
+        if (isset($output['configuration']['expose_aggregate'])) {
+          $defaults['expose_aggregate'] = $output['configuration']['expose_aggregate'];
+        }
       }
     }
     if (!isset($defaults['permission'])) {
@@ -124,6 +128,7 @@ class CRM_DataprocessorSearch_Search implements UIOutputInterface {
     $configuration['hidden_fields'] = $submittedValues['hidden_fields'];
     $configuration['help_text'] = $submittedValues['help_text'];
     $configuration['expanded_search'] = isset($submittedValues['expanded_search']) ? $submittedValues['expanded_search'] : false;
+    $configuration['expose_aggregate'] = isset($submittedValues['expose_aggregate']) ? $submittedValues['expose_aggregate'] : false;
     return $configuration;
   }
 
diff --git a/Civi/DataProcessor/DataFlow/AbstractDataFlow.php b/Civi/DataProcessor/DataFlow/AbstractDataFlow.php
index 9cf44820..59a30247 100644
--- a/Civi/DataProcessor/DataFlow/AbstractDataFlow.php
+++ b/Civi/DataProcessor/DataFlow/AbstractDataFlow.php
@@ -251,6 +251,18 @@ abstract class AbstractDataFlow {
     $this->aggregateOutputHandlers[] = $aggregateOutputHandler;
   }
 
+  /**
+   * @param \Civi\DataProcessor\DataFlow\OutputHandlerAggregate $aggregateOutputHandler
+   */
+  public function removeAggregateOutputHandler(OutputHandlerAggregate $aggregateOutputHandler) {
+    foreach($this->aggregateOutputHandlers as $key => $item) {
+      if ($item->getAggregateFieldSpec()->alias == $aggregateOutputHandler->getAggregateFieldSpec()->alias) {
+        unset($this->aggregateOutputHandlers[$key]);
+        break;
+      }
+    }
+  }
+
   /**
    * Adds a field for sorting
    *
diff --git a/Civi/DataProcessor/FieldOutputHandler/DateFieldOutputHandler.php b/Civi/DataProcessor/FieldOutputHandler/DateFieldOutputHandler.php
index 5d2c3584..77e170a5 100644
--- a/Civi/DataProcessor/FieldOutputHandler/DateFieldOutputHandler.php
+++ b/Civi/DataProcessor/FieldOutputHandler/DateFieldOutputHandler.php
@@ -183,6 +183,38 @@ class DateFieldOutputHandler extends AbstractSimpleFieldOutputHandler implements
     return $value;
   }
 
+  /**
+   * Enable aggregation for this field.
+   *
+   * @return void
+   */
+  public function enableAggregation() {
+    try {
+      $dataFlow = $this->dataSource->ensureField($this->getAggregateFieldSpec());
+      if ($dataFlow) {
+        $dataFlow->addAggregateOutputHandler($this);
+      }
+    } catch (\Exception $e) {
+      // Do nothing.
+    }
+  }
+
+  /**
+   * Disable aggregation for this field.
+   *
+   * @return void
+   */
+  public function disableAggregation() {
+    try {
+      $dataFlow = $this->dataSource->ensureField($this->getAggregateFieldSpec());
+      if ($dataFlow) {
+        $dataFlow->removeAggregateOutputHandler($this);
+      }
+    } catch (\Exception $e) {
+      // Do nothing.
+    }
+  }
+
   protected function getFunctions() {
     return array(
       'date' => array(
diff --git a/Civi/DataProcessor/FieldOutputHandler/OptionFieldOutputHandler.php b/Civi/DataProcessor/FieldOutputHandler/OptionFieldOutputHandler.php
index 6c748c55..7f6ca5b7 100644
--- a/Civi/DataProcessor/FieldOutputHandler/OptionFieldOutputHandler.php
+++ b/Civi/DataProcessor/FieldOutputHandler/OptionFieldOutputHandler.php
@@ -11,7 +11,32 @@ use Civi\DataProcessor\Source\SourceInterface;
 use Civi\DataProcessor\DataSpecification\FieldSpecification;
 use Civi\DataProcessor\FieldOutputHandler\FieldOutput;
 
-class OptionFieldOutputHandler extends AbstractSimpleFieldOutputHandler {
+class OptionFieldOutputHandler extends AbstractSimpleFieldOutputHandler implements OutputHandlerAggregate {
+
+  /**
+   * @var bool
+   */
+  protected $isAggregateField = false;
+
+  /**
+   * Initialize the processor
+   *
+   * @param String $alias
+   * @param String $title
+   * @param array $configuration
+   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $processorType
+   */
+  public function initialize($alias, $title, $configuration) {
+    parent::initialize($alias, $title, $configuration);
+    $this->isAggregateField = isset($configuration['is_aggregate']) ? $configuration['is_aggregate'] : false;
+
+    if ($this->isAggregateField) {
+      $dataFlow = $this->dataSource->ensureField($this->getAggregateFieldSpec());
+      if ($dataFlow) {
+        $dataFlow->addAggregateOutputHandler($this);
+      }
+    }
+  }
 
 
   /**
@@ -63,5 +88,106 @@ class OptionFieldOutputHandler extends AbstractSimpleFieldOutputHandler {
     return false;
   }
 
+  /**
+   * @return \Civi\DataProcessor\DataSpecification\FieldSpecification
+   */
+  public function getAggregateFieldSpec() {
+    return $this->inputFieldSpec;
+  }
+
+  /**
+   * @return bool
+   */
+  public function isAggregateField() {
+    return $this->isAggregateField;
+  }
+
+  /**
+   * Enable aggregation for this field.
+   *
+   * @return void
+   */
+  public function enableAggregation() {
+    try {
+      $dataFlow = $this->dataSource->ensureField($this->getAggregateFieldSpec());
+      if ($dataFlow) {
+        $dataFlow->addAggregateOutputHandler($this);
+      }
+    } catch (\Exception $e) {
+      // Do nothing.
+    }
+  }
+
+  /**
+   * Disable aggregation for this field.
+   *
+   * @return void
+   */
+  public function disableAggregation() {
+    try {
+      $dataFlow = $this->dataSource->ensureField($this->getAggregateFieldSpec());
+      if ($dataFlow) {
+        $dataFlow->removeAggregateOutputHandler($this);
+      }
+    } catch (\Exception $e) {
+      // Do nothing.
+    }
+  }
+
+  /**
+   * Returns the value. And if needed a formatting could be applied.
+   * E.g. when the value is a date field and you want to aggregate on the month
+   * you can then return the month here.
+   *
+   * @param $value
+   *
+   * @return mixed
+   */
+  public function formatAggregationValue($value) {
+    return $value;
+  }
+
+  /**
+   * When this handler has additional configuration you can add
+   * the fields on the form with this function.
+   *
+   * @param \CRM_Core_Form $form
+   * @param array $field
+   */
+  public function buildConfigurationForm(\CRM_Core_Form $form, $field=array()) {
+    parent::buildConfigurationForm($form, $field);
+    $form->add('checkbox', 'is_aggregate', E::ts('Aggregate on this field'));
+    if (isset($field['configuration'])) {
+      $configuration = $field['configuration'];
+      $defaults = array();
+      if (isset($configuration['is_aggregate'])) {
+        $defaults['is_aggregate'] = $configuration['is_aggregate'];
+      }
+      $form->setDefaults($defaults);
+    }
+  }
+
+  /**
+   * Process the submitted values and create a configuration array
+   *
+   * @param $submittedValues
+   * @return array
+   */
+  public function processConfiguration($submittedValues) {
+    $configuration = parent::processConfiguration($submittedValues);
+    $configuration['is_aggregate'] = isset($submittedValues['is_aggregate']) ? $submittedValues['is_aggregate'] : false;
+    return $configuration;
+  }
+
+  /**
+   * When this handler has configuration specify the template file name
+   * for the configuration form.
+   *
+   * @return false|string
+   */
+  public function getConfigurationTemplateFileName() {
+    return "CRM/Dataprocessor/Form/Field/Configuration/OptionFieldOutputHandler.tpl";
+  }
+
 
 }
diff --git a/Civi/DataProcessor/FieldOutputHandler/OutputHandlerAggregate.php b/Civi/DataProcessor/FieldOutputHandler/OutputHandlerAggregate.php
index 7abc605c..c419d446 100644
--- a/Civi/DataProcessor/FieldOutputHandler/OutputHandlerAggregate.php
+++ b/Civi/DataProcessor/FieldOutputHandler/OutputHandlerAggregate.php
@@ -18,6 +18,21 @@ interface OutputHandlerAggregate {
    */
   public function isAggregateField();
 
+  /**
+   * Enable aggregation for this field.
+   *
+   * @return void
+   */
+  public function enableAggregation();
+
+  /**
+   * Disable aggregation for this field.
+   *
+   * @return void
+   */
+  public function disableAggregation();
+
+
   /**
    * Returns the value. And if needed a formatting could be applied.
    * E.g. when the value is a date field and you want to aggregate on the month
diff --git a/Civi/DataProcessor/FieldOutputHandler/RawFieldOutputHandler.php b/Civi/DataProcessor/FieldOutputHandler/RawFieldOutputHandler.php
index b3f6e668..076eed82 100644
--- a/Civi/DataProcessor/FieldOutputHandler/RawFieldOutputHandler.php
+++ b/Civi/DataProcessor/FieldOutputHandler/RawFieldOutputHandler.php
@@ -39,6 +39,38 @@ class RawFieldOutputHandler extends AbstractSimpleFieldOutputHandler implements
     }
   }
 
+  /**
+   * Enable aggregation for this field.
+   *
+   * @return void
+   */
+  public function enableAggregation() {
+    try {
+      $dataFlow = $this->dataSource->ensureField($this->getAggregateFieldSpec());
+      if ($dataFlow) {
+        $dataFlow->addAggregateOutputHandler($this);
+      }
+    } catch (\Exception $e) {
+      // Do nothing.
+    }
+  }
+
+  /**
+   * Disable aggregation for this field.
+   *
+   * @return void
+   */
+  public function disableAggregation() {
+    try {
+      $dataFlow = $this->dataSource->ensureField($this->getAggregateFieldSpec());
+      if ($dataFlow) {
+        $dataFlow->removeAggregateOutputHandler($this);
+      }
+    } catch (\Exception $e) {
+      // Do nothing.
+    }
+  }
+
   /**
    * When this handler has additional configuration you can add
    * the fields on the form with this function.
diff --git a/templates/CRM/Dataprocessor/Form/Field/Configuration/OptionFieldOutputHandler.tpl b/templates/CRM/Dataprocessor/Form/Field/Configuration/OptionFieldOutputHandler.tpl
new file mode 100644
index 00000000..fa7ddec4
--- /dev/null
+++ b/templates/CRM/Dataprocessor/Form/Field/Configuration/OptionFieldOutputHandler.tpl
@@ -0,0 +1,8 @@
+{crmScope extensionKey='dataprocessor'}
+  {include file="CRM/Dataprocessor/Form/Field/Configuration/SimpleFieldOutputHandler.tpl"}
+  <div class="crm-section">
+    <div class="label">{$form.is_aggregate.label}</div>
+    <div class="content">{$form.is_aggregate.html}</div>
+    <div class="clear"></div>
+  </div>
+{/crmScope}
diff --git a/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl b/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl
index a6836a62..54eb7eb7 100644
--- a/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl
+++ b/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl
@@ -17,7 +17,11 @@
                       {foreach from=$filters key=filterName item=filter}
                           {include file=$filter.template filterName=$filter.alias filter=$filter.filter}
                       {/foreach}
+                      {if $additional_criteria_template}
+                        {include file=$additional_criteria_template}
+                      {/if}
                   </table>
+
                   <div class="crm-submit-buttons">{include file="CRM/common/formButtons.tpl" location="botton"}</div>
               </div>
           </div>
diff --git a/templates/CRM/DataprocessorSearch/Form/Criteria/AggregateCriteria.tpl b/templates/CRM/DataprocessorSearch/Form/Criteria/AggregateCriteria.tpl
new file mode 100644
index 00000000..9b5fbb3e
--- /dev/null
+++ b/templates/CRM/DataprocessorSearch/Form/Criteria/AggregateCriteria.tpl
@@ -0,0 +1,7 @@
+{crmScope extensionKey='dataprocessor'}
+<tr>
+  <td class="label">{ts}Aggregate{/ts}</td>
+  <td>&nbsp;</td>
+  <td>{$form.aggregateFields.html}</td>
+</tr>
+{/crmScope}
diff --git a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/Search.tpl b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/Search.tpl
index 8237e188..4c22ac38 100644
--- a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/Search.tpl
+++ b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/Search.tpl
@@ -29,6 +29,11 @@
       <div class="content">{$form.expanded_search.html}</div>
       <div class="clear"></div>
     </div>
+  <div class="crm-section">
+    <div class="label">{$form.expose_aggregate.label}</div>
+    <div class="content">{$form.expose_aggregate.html}</div>
+    <div class="clear"></div>
+  </div>
     <div class="crm-section">
         <div class="label">{$form.help_text.label}</div>
         <div class="content">{$form.help_text.html}</div>
-- 
GitLab