From 28ed3089cc3894e5ff5599b4d773bbe13b2fea3d Mon Sep 17 00:00:00 2001 From: Jaap Jansma <jaap.jansma@civicoop.org> Date: Fri, 11 Oct 2019 12:30:46 +0200 Subject: [PATCH] Added date field --- CHANGELOG.md | 1 + CRM/Dataprocessor/BAO/DataProcessor.php | 2 +- .../DataFlow/AbstractDataFlow.php | 16 +- .../CombinedDataFlow/CombinedSqlDataFlow.php | 4 +- Civi/DataProcessor/DataFlow/SqlDataFlow.php | 4 +- .../DataFlow/SqlTableDataFlow.php | 15 +- .../DataFlow/Utils/Aggregator.php | 24 +- .../DataSpecification/FieldSpecification.php | 21 ++ Civi/DataProcessor/Factory.php | 1 + .../DateFieldOutputHandler.php | 212 ++++++++++++++++++ .../OutputHandlerAggregate.php | 11 + .../RawFieldOutputHandler.php | 12 + .../Configuration/DateFieldOutputHandler.tpl | 23 ++ 13 files changed, 314 insertions(+), 32 deletions(-) create mode 100644 Civi/DataProcessor/FieldOutputHandler/DateFieldOutputHandler.php create mode 100644 templates/CRM/Dataprocessor/Form/Field/Configuration/DateFieldOutputHandler.tpl diff --git a/CHANGELOG.md b/CHANGELOG.md index 48fde9a1..c1f7c166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * Refactored aggregation functionality and added aggregation function field. * Fixed issue with updating navigation after editing an output. * Added option to expand criteria forms on search forms. +* Added a Date field. # Version 1.0.7 diff --git a/CRM/Dataprocessor/BAO/DataProcessor.php b/CRM/Dataprocessor/BAO/DataProcessor.php index e8d0768c..31b4e6bb 100644 --- a/CRM/Dataprocessor/BAO/DataProcessor.php +++ b/CRM/Dataprocessor/BAO/DataProcessor.php @@ -99,7 +99,7 @@ class CRM_Dataprocessor_BAO_DataProcessor extends CRM_Dataprocessor_DAO_DataProc $outputHandler->initialize($field['name'], $field['title'], $field['configuration']); $dataProcessorClass->addOutputFieldHandlers($outputHandler); if ($outputHandler instanceof OutputHandlerAggregate && $outputHandler->isAggregateField()) { - $dataProcessorClass->getDataFlow()->addAggregateField($outputHandler->getAggregateFieldSpec()); + $dataProcessorClass->getDataFlow()->addAggregateOutputHandler($outputHandler); } } catch (\Exception $e) { CRM_Core_Session::setStatus($e->getMessage(), E::ts("Invalid field"), 'error'); diff --git a/Civi/DataProcessor/DataFlow/AbstractDataFlow.php b/Civi/DataProcessor/DataFlow/AbstractDataFlow.php index 4fa85a1f..19cd4f0a 100644 --- a/Civi/DataProcessor/DataFlow/AbstractDataFlow.php +++ b/Civi/DataProcessor/DataFlow/AbstractDataFlow.php @@ -12,6 +12,7 @@ use \Civi\DataProcessor\DataSpecification\DataSpecification; use Civi\DataProcessor\DataSpecification\FieldSpecification; use \Civi\DataProcessor\FieldOutputHandler\AbstractFieldOutputHandler; use Civi\DataProcessor\DataFlow\Sort\SortSpecification; +use Civi\DataProcessor\FieldOutputHandler\OutputHandlerAggregate; abstract class AbstractDataFlow { @@ -46,9 +47,9 @@ abstract class AbstractDataFlow { protected $dataSpecification; /** - * @var FieldSpecification[] + * @var \Civi\DataProcessor\FieldOutputHandler\OutputHandlerAggregate[] */ - protected $aggregateFields = array(); + protected $aggregateOutputHandlers = array(); /** * @var SortSpecification[] @@ -240,8 +241,11 @@ abstract class AbstractDataFlow { return array(); } - public function addAggregateField(FieldSpecification $aggregateField) { - $this->aggregateFields[] = $aggregateField; + /** + * @param \Civi\DataProcessor\DataFlow\OutputHandlerAggregate $aggregateOutputHandler + */ + public function addAggregateOutputHandler(OutputHandlerAggregate $aggregateOutputHandler) { + $this->aggregateOutputHandlers[] = $aggregateOutputHandler; } /** @@ -283,8 +287,8 @@ abstract class AbstractDataFlow { * @return array(); */ protected function aggregate($records, $fieldNameprefix="") { - if (count($this->aggregateFields)) { - $aggregator = new Aggregator($records, $this->aggregateFields, $this->dataSpecification); + if (count($this->aggregateOutputHandlers)) { + $aggregator = new Aggregator($records, $this->aggregateOutputHandlers, $this->dataSpecification); $records = $aggregator->aggregateRecords($fieldNameprefix); } return $records; diff --git a/Civi/DataProcessor/DataFlow/CombinedDataFlow/CombinedSqlDataFlow.php b/Civi/DataProcessor/DataFlow/CombinedDataFlow/CombinedSqlDataFlow.php index ac408dd9..defe3703 100644 --- a/Civi/DataProcessor/DataFlow/CombinedDataFlow/CombinedSqlDataFlow.php +++ b/Civi/DataProcessor/DataFlow/CombinedDataFlow/CombinedSqlDataFlow.php @@ -133,8 +133,8 @@ class CombinedSqlDataFlow extends SqlDataFlow implements MultipleSourceDataFlows */ public function getFieldsForGroupByStatement() { $fields = array(); - foreach($this->aggregateFields as $field) { - $fields[] = "`{$this->primary_table_alias}`.`{$field->name}`"; + foreach($this->aggregateOutputHandlers as $outputHandler) { + $fields[] = $outputHandler->getAggregateFieldSpec()->getSqlGroupByStatement($this->getName()); } foreach($this->sourceDataFlowDescriptions as $sourceDataFlowDescription) { $fields = array_merge($fields, $sourceDataFlowDescription->getDataFlow()->getFieldsForGroupByStatement()); diff --git a/Civi/DataProcessor/DataFlow/SqlDataFlow.php b/Civi/DataProcessor/DataFlow/SqlDataFlow.php index 4f518ba7..4458e572 100644 --- a/Civi/DataProcessor/DataFlow/SqlDataFlow.php +++ b/Civi/DataProcessor/DataFlow/SqlDataFlow.php @@ -45,8 +45,8 @@ abstract class SqlDataFlow extends AbstractDataFlow { */ public function getFieldsForGroupByStatement() { $fields = array(); - foreach($this->aggregateFields as $field) { - $fields[] = $field->getSqlGroupByStatement($this->getName()); + foreach($this->aggregateOutputHandlers as $outputHandler) { + $fields[] = $outputHandler->getAggregateFieldSpec()->getSqlGroupByStatement($this->getName()); } return $fields; } diff --git a/Civi/DataProcessor/DataFlow/SqlTableDataFlow.php b/Civi/DataProcessor/DataFlow/SqlTableDataFlow.php index c8e7bcc7..84cd1ca1 100644 --- a/Civi/DataProcessor/DataFlow/SqlTableDataFlow.php +++ b/Civi/DataProcessor/DataFlow/SqlTableDataFlow.php @@ -72,19 +72,6 @@ class SqlTableDataFlow extends SqlDataFlow { return $fields; } - /** - * Returns an array with the fields for in the group by statement in the sql query. - * - * @return string[] - */ - public function getFieldsForGroupByStatement() { - $fields = array(); - foreach($this->aggregateFields as $field) { - $fields[] = "`{$this->table_alias}`.`{$field->name}`"; - } - return $fields; - } - /** * @return string */ @@ -99,4 +86,4 @@ class SqlTableDataFlow extends SqlDataFlow { return $this->table_alias; } -} \ No newline at end of file +} diff --git a/Civi/DataProcessor/DataFlow/Utils/Aggregator.php b/Civi/DataProcessor/DataFlow/Utils/Aggregator.php index 941ace68..a3bd595a 100644 --- a/Civi/DataProcessor/DataFlow/Utils/Aggregator.php +++ b/Civi/DataProcessor/DataFlow/Utils/Aggregator.php @@ -7,7 +7,9 @@ namespace Civi\DataProcessor\DataFlow\Utils; use Civi\DataProcessor\DataSpecification\Aggregatable; +use Civi\DataProcessor\DataSpecification\DataSpecification; use Civi\DataProcessor\DataSpecification\FieldSpecification; +use Civi\DataProcessor\FieldOutputHandler\OutputHandlerAggregate; class Aggregator { @@ -17,18 +19,25 @@ class Aggregator { protected $records; /** - * @var FieldSpecification[] + * @var OutputHandlerAggregate[] */ - protected $aggregateFields = array(); + protected $aggregateOutputHandlers = array(); /** * @var \Civi\DataProcessor\DataSpecification\DataSpecification */ protected $dataSpecification = array(); - public function __construct($records, $aggregateFields, $dataSpecification) { + /** + * Aggregator constructor. + * + * @param $records + * @param OutputHandlerAggregate[] $aggregateOutputHandlers + * @param \Civi\DataProcessor\DataSpecification\DataSpecification $dataSpecification + */ + public function __construct($records, $aggregateOutputHandlers, DataSpecification $dataSpecification) { $this->records = $records; - $this->aggregateFields = $aggregateFields; + $this->aggregateOutputHandlers = $aggregateOutputHandlers; $this->dataSpecification = $dataSpecification; } @@ -57,10 +66,11 @@ class Aggregator { protected function getAggregationKeyFromRecord($record, $fieldNameprefix="") { $key = ''; - foreach($this->aggregateFields as $field) { - $alias = $field->alias; + foreach($this->aggregateOutputHandlers as $outputHandler) { + $alias = $outputHandler->getAggregateFieldSpec()->alias; if (isset($record[$fieldNameprefix.$alias])) { - $key .= $record[$fieldNameprefix.$alias].'_'; + $value = $outputHandler->formatAggregationValue($record[$fieldNameprefix.$alias]); + $key .= $value.'_'; } else { $key .= 'null_'; } diff --git a/Civi/DataProcessor/DataSpecification/FieldSpecification.php b/Civi/DataProcessor/DataSpecification/FieldSpecification.php index 8f539c92..ee90fb61 100644 --- a/Civi/DataProcessor/DataSpecification/FieldSpecification.php +++ b/Civi/DataProcessor/DataSpecification/FieldSpecification.php @@ -33,6 +33,11 @@ class FieldSpecification implements SqlFieldSpecification { */ public $options = null; + /** + * @var null|String + */ + protected $sqlValueFormatFunction = null; + public function __construct($name, $type, $title, $options=null, $alias=null) { if (empty($alias)) { $this->alias = $name; @@ -49,6 +54,13 @@ class FieldSpecification implements SqlFieldSpecification { return $this->options; } + /** + * @param $function + */ + public function setMySqlFunction($function) { + $this->sqlValueFormatFunction = $function; + } + /** * Returns the select statement for this field. * E.g. COUNT(civicrm_contact.id) AS contact_id_count @@ -57,6 +69,9 @@ class FieldSpecification implements SqlFieldSpecification { * @return string */ public function getSqlSelectStatement($table_alias) { + if ($this->sqlValueFormatFunction) { + return "{$this->sqlValueFormatFunction} (`{$table_alias}`.`{$this->name}`) AS `{$this->alias}`"; + } return "`{$table_alias}`.`{$this->name}` AS `{$this->alias}`"; } @@ -68,6 +83,9 @@ class FieldSpecification implements SqlFieldSpecification { * @return string */ public function getSqlColumnName($table_alias) { + if ($this->sqlValueFormatFunction) { + return "{$this->sqlValueFormatFunction} (`{$table_alias}`.`{$this->name}`)"; + } return "`{$table_alias}`.`{$this->name}`"; } @@ -80,6 +98,9 @@ class FieldSpecification implements SqlFieldSpecification { * @return String */ public function getSqlGroupByStatement($table_alias) { + if ($this->sqlValueFormatFunction) { + return "{$this->sqlValueFormatFunction} (`{$table_alias}`.`{$this->name}`)"; + } return "`{$table_alias}`.`{$this->name}`"; } diff --git a/Civi/DataProcessor/Factory.php b/Civi/DataProcessor/Factory.php index b7c5d689..3292f2b4 100644 --- a/Civi/DataProcessor/Factory.php +++ b/Civi/DataProcessor/Factory.php @@ -140,6 +140,7 @@ class Factory { $this->addjoinType('simple_join', 'Civi\DataProcessor\DataFlow\MultipleDataFlows\SimpleJoin', E::ts('Select fields to join on')); $this->addjoinType('simple_non_required_join', 'Civi\DataProcessor\DataFlow\MultipleDataFlows\SimpleNonRequiredJoin', E::ts('Select fields to join on (not required)')); $this->addOutputHandler('raw', 'Civi\DataProcessor\FieldOutputHandler\RawFieldOutputHandler', E::ts('Raw field value')); + $this->addOutputHandler('date', 'Civi\DataProcessor\FieldOutputHandler\DateFieldOutputHandler', E::ts('Date field value')); $this->addOutputHandler('contact_link', 'Civi\DataProcessor\FieldOutputHandler\ContactLinkFieldOutputHandler', E::ts('Link to view contact')); $this->addOutputHandler('file_field', 'Civi\DataProcessor\FieldOutputHandler\FileFieldOutputHandler', E::ts('File download link')); $this->addOutputHandler('option_label', 'Civi\DataProcessor\FieldOutputHandler\OptionFieldOutputHandler', E::ts('Option label')); diff --git a/Civi/DataProcessor/FieldOutputHandler/DateFieldOutputHandler.php b/Civi/DataProcessor/FieldOutputHandler/DateFieldOutputHandler.php new file mode 100644 index 00000000..564a9b36 --- /dev/null +++ b/Civi/DataProcessor/FieldOutputHandler/DateFieldOutputHandler.php @@ -0,0 +1,212 @@ +<?php +/** + * @author Jaap Jansma <jaap.jansma@civicoop.org> + * @license AGPL-3.0 + */ + +namespace Civi\DataProcessor\FieldOutputHandler; + +use Civi\DataProcessor\Exception\DataSourceNotFoundException; +use Civi\DataProcessor\Exception\FieldNotFoundException; +use CRM_Dataprocessor_ExtensionUtil as E; +use Civi\DataProcessor\Source\SourceInterface; +use Civi\DataProcessor\DataSpecification\FieldSpecification; + +class DateFieldOutputHandler extends AbstractSimpleFieldOutputHandler implements OutputHandlerAggregate { + + /** + * @var bool + */ + protected $isAggregateField = false; + + protected $function = null; + + protected $format = null; + + /** + * 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; + $this->format = isset($configuration['format']) ? $configuration['format'] : false; + $function = isset($configuration['function']) ? $configuration['function'] : false; + $availableFunctions = $this->getFunctions(); + if (isset($availableFunctions[$function])) { + $this->function = $function; + $this->inputFieldSpec->setMySqlFunction($availableFunctions[$function]['sql']); + } + } + + /** + * 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); + + $dateFunctions = array(); + foreach($this->getFunctions() as $key => $function) { + $dateFunctions[$key] = $function['title']; + } + $form->add('text', 'format', E::ts('Format'), array( + 'style' => 'min-width:250px', + 'class' => 'huge', + )); + $form->add('select', 'function', E::ts('Date function'), $dateFunctions, false, array( + 'style' => 'min-width:250px', + 'class' => 'crm-select2 huge', + 'placeholder' => E::ts('- select -'), + )); + $form->add('checkbox', 'is_aggregate', E::ts('Aggregate on this field')); + if (isset($field['configuration'])) { + $configuration = $field['configuration']; + $defaults = array(); + if (isset($configuration['function'])) { + $defaults['function'] = $configuration['function']; + } + if (isset($configuration['format'])) { + $defaults['format'] = $configuration['format']; + } + if (isset($configuration['is_aggregate'])) { + $defaults['is_aggregate'] = $configuration['is_aggregate']; + } + $form->setDefaults($defaults); + } + } + + /** + * 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/DateFieldOutputHandler.tpl"; + } + + + /** + * 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; + $configuration['function'] = isset($submittedValues['function']) ? $submittedValues['function'] : false; + $configuration['format'] = isset($submittedValues['format']) ? $submittedValues['format'] : false; + return $configuration; + } + + /** + * Returns the formatted value + * + * @param $rawRecord + * @param $formattedRecord + * + * @return \Civi\DataProcessor\FieldOutputHandler\FieldOutput + */ + public function formatField($rawRecord, $formattedRecord) { + $formattedValue = $rawRecord[$this->inputFieldSpec->alias]; + if ($this->format) { + $date = new \DateTime($formattedValue); + $formattedValue = $date->format($this->format); + } + $output = new FieldOutput($rawRecord[$this->inputFieldSpec->alias]); + $output->formattedValue = $formattedValue; + return $output; + } + + /** + * @return \Civi\DataProcessor\DataSpecification\FieldSpecification + */ + public function getAggregateFieldSpec() { + return $this->inputFieldSpec; + } + + /** + * @return bool + */ + public function isAggregateField() { + return $this->isAggregateField; + } + + /** + * Callback function for determining whether this field could be handled by this output handler. + * + * @param \Civi\DataProcessor\DataSpecification\FieldSpecification $field + * @return bool + */ + public function isFieldValid(FieldSpecification $field) { + switch ($field->type) { + case 'Timestamp': + case 'Date': + return TRUE; + break; + } + return FALSE; + } + + /** + * 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) { + $availableFunctions = $this->getFunctions(); + if ($this->function && isset($availableFunctions[$this->function]) && $value) { + $date = new \DateTime($value); + $value = $date->format($availableFunctions[$this->function]['php_date_format']); + if (isset($availableFunctions[$this->function]['php_add'])) { + $value = $value + $availableFunctions[$this->function]['php_add']; + } + } + return $value; + } + + protected function getFunctions() { + return array( + 'dayofmonth' => array( + 'sql' => 'DAYOFMONTH', + 'php_date_format' => 'j', + 'title' => E::ts('Day of month'), + ), + 'dayofweek' => array( + 'sql' => 'DAYOFWEEK', + 'php_date_format' => 'N', + 'php_add' => +1, // +1 as the php date format return 0 for sunday and 6 for saturday. So add one + 'title' => E::ts('Day of week (1 = Sunday, 7 = Saturday)'), + ), + 'week' => array( + 'sql' => 'WEEK', + 'php_date_format' => 'W', + 'title' => E::ts('Weeknumber'), + ), + 'month' => array( + 'sql' => 'MONTH', + 'php_date_format' => 'n', + 'title' => E::ts('Month'), + ), + 'year' => array( + 'sql' => 'YEAR', + 'php_date_format' => 'Y', + 'title' => E::ts('Year'), + ), + ); + } + + +} diff --git a/Civi/DataProcessor/FieldOutputHandler/OutputHandlerAggregate.php b/Civi/DataProcessor/FieldOutputHandler/OutputHandlerAggregate.php index 187f723d..7abc605c 100644 --- a/Civi/DataProcessor/FieldOutputHandler/OutputHandlerAggregate.php +++ b/Civi/DataProcessor/FieldOutputHandler/OutputHandlerAggregate.php @@ -18,4 +18,15 @@ interface OutputHandlerAggregate { */ public function isAggregateField(); + /** + * 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); + } diff --git a/Civi/DataProcessor/FieldOutputHandler/RawFieldOutputHandler.php b/Civi/DataProcessor/FieldOutputHandler/RawFieldOutputHandler.php index 6347b7dd..c5e58656 100644 --- a/Civi/DataProcessor/FieldOutputHandler/RawFieldOutputHandler.php +++ b/Civi/DataProcessor/FieldOutputHandler/RawFieldOutputHandler.php @@ -89,5 +89,17 @@ class RawFieldOutputHandler extends AbstractSimpleFieldOutputHandler implements return $this->isAggregateField; } + /** + * 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; + } + } diff --git a/templates/CRM/Dataprocessor/Form/Field/Configuration/DateFieldOutputHandler.tpl b/templates/CRM/Dataprocessor/Form/Field/Configuration/DateFieldOutputHandler.tpl new file mode 100644 index 00000000..b97dc79c --- /dev/null +++ b/templates/CRM/Dataprocessor/Form/Field/Configuration/DateFieldOutputHandler.tpl @@ -0,0 +1,23 @@ +{crmScope extensionKey='dataprocessor'} + {include file="CRM/Dataprocessor/Form/Field/Configuration/SimpleFieldOutputHandler.tpl"} + <div class="crm-section"> + <div class="label">{$form.format.label}</div> + <div class="content"> + {$form.format.html} + <p class="description"> + {ts 1='https://www.php.net/manual/en/function.date.php#format'}See <a href="%1">DateTime format on php.net</a> for help.{/ts} + </p> + </div> + <div class="clear"></div> + </div> + <div class="crm-section"> + <div class="label">{$form.function.label}</div> + <div class="content">{$form.function.html}</div> + <div class="clear"></div> + </div> + <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} -- GitLab