From 0713541fdfc0e05ff5a4117375a664fd32c49757 Mon Sep 17 00:00:00 2001
From: Jaap Jansma <jaap.jansma@civicoop.org>
Date: Tue, 29 Oct 2019 17:03:10 +0100
Subject: [PATCH] Output a data processor as a contribution search.

---
 CHANGELOG.md                                  |   1 +
 .../Form/Output/AbstractUIOutputForm.php      |  22 +--
 .../ContributionSearch.php                    | 185 ++++++++++++++++++
 .../Controller/ContributionSearch.php         |  78 ++++++++
 .../Form/ContributionSearch.php               | 110 +++++++++++
 .../StateMachine/ContributionSearch.php       |  92 +++++++++
 Civi/DataProcessor/Factory.php                |   1 +
 .../Form/ContributionSearch.tpl               |   1 +
 .../ContributionSearch.tpl                    |  32 +++
 9 files changed, 507 insertions(+), 15 deletions(-)
 create mode 100644 CRM/DataprocessorSearch/ContributionSearch.php
 create mode 100644 CRM/DataprocessorSearch/Controller/ContributionSearch.php
 create mode 100644 CRM/DataprocessorSearch/Form/ContributionSearch.php
 create mode 100644 CRM/DataprocessorSearch/StateMachine/ContributionSearch.php
 create mode 100644 templates/CRM/DataprocessorSearch/Form/ContributionSearch.tpl
 create mode 100644 templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ContributionSearch.tpl

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d684c62d..667d2b8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 * Allow to limit ContactFilter to only show contacts from specific groups.
 * Output a data processor as a dashboard.
 * Output a data processor as a tab on the contact summary screen.
+* Output a data processor as a contribution search.
 * Added field outputs for simple calculations (substract and total).
 * Added escaped output to search screens.
 * Replaced the value separator in the raw field with a comma.
diff --git a/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php b/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php
index 9871ae66..768af4d7 100644
--- a/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php
+++ b/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php
@@ -55,26 +55,13 @@ abstract class CRM_Dataprocessor_Form_Output_AbstractUIOutputForm extends CRM_Co
     $this->assign('has_exposed_filters', $this->hasExposedFilters());
   }
 
-  /**
-   * Check whether the user has access to the output.
-   *
-   * @return bool
-   */
-  protected function checkPermission() {
-    if (isset($this->dataProcessorOutput['permission']) && $this->dataProcessorOutput['permission']) {
-      if (!CRM_Core_Permission::check(array($this->dataProcessorOutput['permission']))) {
-        return false;
-      }
-    }
-    return true;
-  }
-
   /**
    * Retrieve the data processor and the output configuration
    *
    * @throws \Exception
    */
   protected function loadDataProcessor() {
+    $factory = dataprocessor_get_factory();
     if (!$this->dataProcessorId) {
       $dataProcessorName = $this->getDataProcessorName();
       $sql = "
@@ -97,7 +84,12 @@ abstract class CRM_Dataprocessor_Form_Output_AbstractUIOutputForm extends CRM_Co
       $this->dataProcessorOutput = civicrm_api3('DataProcessorOutput', 'getsingle', array('id' => $dao->output_id));
       $this->assign('output', $this->dataProcessorOutput);
 
-      if (!$this->checkPermission()) {
+      $outputClass = $factory->getOutputByName($this->dataProcessorOutput['type']);
+      if (!$outputClass instanceof \Civi\DataProcessor\Output\UIOutputInterface) {
+        throw new \Exception('Invalid output');
+      }
+
+      if (!$outputClass->checkUIPermission($this->dataProcessorOutput, $this->dataProcessor)) {
         CRM_Utils_System::permissionDenied();
         CRM_Utils_System::civiExit();
       } elseif (!$this->isConfigurationValid()) {
diff --git a/CRM/DataprocessorSearch/ContributionSearch.php b/CRM/DataprocessorSearch/ContributionSearch.php
new file mode 100644
index 00000000..e12ab5f5
--- /dev/null
+++ b/CRM/DataprocessorSearch/ContributionSearch.php
@@ -0,0 +1,185 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+use Civi\DataProcessor\Output\UIOutputInterface;
+
+use CRM_Dataprocessor_ExtensionUtil as E;
+
+class CRM_DataprocessorSearch_ContributionSearch implements UIOutputInterface {
+
+  /**
+   * Returns true when this filter has additional configuration
+   *
+   * @return bool
+   */
+  public function hasConfiguration() {
+    return true;
+  }
+
+  /**
+   * When this filter type has additional configuration you can add
+   * the fields on the form with this function.
+   *
+   * @param \CRM_Core_Form $form
+   * @param array $filter
+   */
+  public function buildConfigurationForm(\CRM_Core_Form $form, $output=array()) {
+
+    $navigation = CRM_Dataprocessor_Utils_Navigation::singleton();
+    $dataProcessor = civicrm_api3('DataProcessor', 'getsingle', array('id' => $output['data_processor_id']));
+    $dataProcessorClass = \CRM_Dataprocessor_BAO_DataProcessor::dataProcessorToClass($dataProcessor);
+    $fields = array();
+    foreach($dataProcessorClass->getDataFlow()->getOutputFieldHandlers() as $outputFieldHandler) {
+      $field = $outputFieldHandler->getOutputFieldSpecification();
+      $fields[$field->alias] = $field->title;
+    }
+
+    $form->add('select','permission', E::ts('Permission'), \CRM_Core_Permission::basicPermissions(), true, array(
+      'style' => 'min-width:250px',
+      'class' => 'crm-select2 huge',
+      'placeholder' => E::ts('- select -'),
+    ));
+    $form->add('select', 'contribution_id_field', E::ts('Contribution ID field'), $fields, true, array(
+      'style' => 'min-width:250px',
+      'class' => 'crm-select2 huge',
+      'placeholder' => E::ts('- select -'),
+    ));
+    $form->add('select', 'hide_id_field', E::ts('Show Contribution ID field'), array(0=>'Contribution ID is Visible', 1=> 'Contribution ID is hidden'));
+
+    $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'));
+
+    // navigation field
+    $navigationOptions = $navigation->getNavigationOptions();
+    if (isset($output['configuration']['navigation_id'])) {
+      $navigationPath = $navigation->getNavigationPathById($output['configuration']['navigation_id']);
+      unset($navigationOptions[$navigationPath]);
+    }
+    $form->add('select', 'navigation_parent_path', ts('Parent Menu'), array('' => ts('- select -')) + $navigationOptions, true);
+
+    $defaults = array();
+    if ($output) {
+      if (isset($output['permission'])) {
+        $defaults['permission'] = $output['permission'];
+      }
+      if (isset($output['configuration']) && is_array($output['configuration'])) {
+        if (isset($output['configuration']['contribution_id_field'])) {
+          $defaults['contribution_id_field'] = $output['configuration']['contribution_id_field'];
+        }
+        if (isset($output['configuration']['navigation_id'])) {
+          $defaults['navigation_parent_path'] = $navigation->getNavigationParentPathById($output['configuration']['navigation_id']);
+        }
+        if (isset($output['configuration']['hide_id_field'])) {
+          $defaults['hide_id_field'] = $output['configuration']['hide_id_field'];
+        }
+        if (isset($output['configuration']['help_text'])) {
+          $defaults['help_text'] = $output['configuration']['help_text'];
+        }
+        if (isset($output['configuration']['expanded_search'])) {
+          $defaults['expanded_search'] = $output['configuration']['expanded_search'];
+        }
+      }
+    }
+    if (!isset($defaults['permission'])) {
+      $defaults['permission'] = 'access CiviCRM';
+    }
+    $form->setDefaults($defaults);
+  }
+
+  /**
+   * When this filter type has configuration specify the template file name
+   * for the configuration form.
+   *
+   * @return false|string
+   */
+  public function getConfigurationTemplateFileName() {
+    return "CRM/DataprocessorSearch/Form/OutputConfiguration/ContributionSearch.tpl";
+  }
+
+
+  /**
+   * Process the submitted values and create a configuration array
+   *
+   * @param $submittedValues
+   * @param array $output
+   * @return array
+   */
+  public function processConfiguration($submittedValues, &$output) {
+    $output['permission'] = $submittedValues['permission'];
+    $configuration['contribution_id_field'] = $submittedValues['contribution_id_field'];
+    $configuration['navigation_parent_path'] = $submittedValues['navigation_parent_path'];
+    $configuration['hide_id_field'] = $submittedValues['hide_id_field'];
+    $configuration['help_text'] = $submittedValues['help_text'];
+    $configuration['expanded_search'] = isset($submittedValues['expanded_search']) ? $submittedValues['expanded_search'] : false;
+    return $configuration;
+  }
+
+  /**
+   * This function is called prior to removing an output
+   *
+   * @param array $output
+   * @return void
+   */
+  public function deleteOutput($output) {
+    // Do nothing
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string
+   */
+  public function getUrlToUi($output, $dataProcessor) {
+    return "civicrm/dataprocessor_contribution_search/{$dataProcessor['name']}";
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string
+   */
+  public function getTitleForUiLink($output, $dataProcessor) {
+    return $dataProcessor['title'];
+  }
+
+  /**
+   * Returns the url for the page/form this output will show to the user
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return string|false
+   */
+  public function getIconForUiLink($output, $dataProcessor) {
+    return false;
+  }
+
+  /**
+   * Returns the callback for the UI.
+   *
+   * @return string
+   */
+  public function getCallbackForUi() {
+    return 'CRM_DataprocessorSearch_Controller_ContributionSearch';
+  }
+
+  /**
+   * Checks whether the current user has access to this output
+   *
+   * @param array $output
+   * @param array $dataProcessor
+   * @return bool
+   */
+  public function checkUIPermission($output, $dataProcessor) {
+    return CRM_Core_Permission::check(array(
+      $output['permission']
+    ));
+  }
+
+}
diff --git a/CRM/DataprocessorSearch/Controller/ContributionSearch.php b/CRM/DataprocessorSearch/Controller/ContributionSearch.php
new file mode 100644
index 00000000..ca1427e0
--- /dev/null
+++ b/CRM/DataprocessorSearch/Controller/ContributionSearch.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+/**
+ * This class is used by the Search functionality.
+ *
+ *  - the search controller is used for building/processing multiform
+ *    searches.
+ *
+ * Typically the first form will display the search criteria and it's results
+ *
+ * The second form is used to process search results with the associated actions.
+ */
+class CRM_DataprocessorSearch_Controller_ContributionSearch extends CRM_Core_Controller {
+
+  /**
+   * Class constructor.
+   *
+   * @param string $title
+   * @param bool $modal
+   * @param int|mixed|null $action
+   */
+  public function __construct($title = NULL, $modal = TRUE, $action = CRM_Core_Action::NONE) {
+    parent::__construct($title, $modal);
+
+    $this->_stateMachine = new CRM_DataprocessorSearch_StateMachine_ContributionSearch($this, $action);
+
+    // create and instantiate the pages
+    $this->addPages($this->_stateMachine, $action);
+
+    // add all the actions
+    $this->addActions();
+  }
+
+  /**
+   * Process the request, overrides the default QFC run method
+   * This routine actually checks if the QFC is modal and if it
+   * is the first invalid page, if so it call the requested action
+   * if not, it calls the display action on the first invalid page
+   * avoids the issue of users hitting the back button and getting
+   * a broken page
+   *
+   * This run is basically a composition of the original run and the
+   * jump action
+   *
+   * @return mixed
+   */
+  public function run() {
+
+    $actionName = $this->getActionName();
+    list($pageName, $action) = $actionName;
+    // Hack to replace to userContext for redirecting after a Task has been completed.
+    // We want the redirect
+    if (!$this->_pages[$pageName] instanceof CRM_DataprocessorSearch_Form_ContributionSearch) {
+      $session = CRM_Core_Session::singleton();
+      $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $this);
+      $urlPath = CRM_Utils_System::getUrlPath();
+      $urlParams = 'force=1';
+      if ($qfKey) {
+        $urlParams .= "&qfKey=$qfKey";
+      }
+      $this->setDestination(CRM_Utils_System::url($urlPath, $urlParams));
+    }
+
+    return parent::run();
+  }
+
+  /**
+   * @return mixed
+   */
+  public function selectorName() {
+    return $this->get('selectorName');
+  }
+
+}
diff --git a/CRM/DataprocessorSearch/Form/ContributionSearch.php b/CRM/DataprocessorSearch/Form/ContributionSearch.php
new file mode 100644
index 00000000..ee93085c
--- /dev/null
+++ b/CRM/DataprocessorSearch/Form/ContributionSearch.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+use CRM_Dataprocessor_ExtensionUtil as E;
+
+class CRM_DataprocessorSearch_Form_ContributionSearch extends CRM_DataprocessorSearch_Form_AbstractSearch {
+
+  /**
+   * Returns the name of the default Entity
+   *
+   * @return string
+   */
+  public function getDefaultEntity() {
+    return 'Contribution';
+  }
+
+  /**
+   * Returns the url for view of the record action
+   *
+   * @param $row
+   *
+   * @return false|string
+   */
+  protected function link($row) {
+    return CRM_Utils_System::url('civicrm/contact/view/contribution', 'reset=1&id='.$row['id'].'&cid='.$row['id'].'&action=view');
+  }
+
+  /**
+   * Returns the link text for view of the record action
+   *
+   * @param $row
+   *
+   * @return false|string
+   */
+  protected function linkText($row) {
+    return E::ts('View contribution');
+  }
+
+  /**
+   * Checks whether the output has a valid configuration
+   *
+   * @return bool
+   */
+  protected function isConfigurationValid() {
+    if (!isset($this->dataProcessorOutput['configuration']['contribution_id_field'])) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Return the data processor ID
+   *
+   * @return String
+   */
+  protected function getDataProcessorName() {
+    $dataProcessorName = str_replace('civicrm/dataprocessor_contribution_search/', '', CRM_Utils_System::getUrlPath());
+    return $dataProcessorName;
+  }
+
+  /**
+   * Returns the name of the output for this search
+   *
+   * @return string
+   */
+  protected function getOutputName() {
+    return 'contribution_search';
+  }
+
+  /**
+   * Returns the name of the ID field in the dataset.
+   *
+   * @return string
+   */
+  protected function getIdFieldName() {
+    return $this->dataProcessorOutput['configuration']['contribution_id_field'];
+  }
+
+  /**
+   * @return string
+   */
+  protected function getEntityTable() {
+    return 'civicrm_contribution';
+  }
+
+  /**
+   * Returns whether we want to use the prevnext cache.
+   * @return bool
+   */
+  protected function usePrevNextCache() {
+    return true;
+  }
+
+  /**
+   * Builds the list of tasks or actions that a searcher can perform on a result set.
+   *
+   * @return array
+   */
+  public function buildTaskList() {
+    if (!$this->_taskList) {
+      $taskParams['softCreditFiltering'] = FALSE;
+      $this->_taskList = CRM_Contribute_Task::permissionedTaskTitles(CRM_Core_Permission::getPermission(), $taskParams);
+    }
+    return $this->_taskList;
+  }
+
+}
diff --git a/CRM/DataprocessorSearch/StateMachine/ContributionSearch.php b/CRM/DataprocessorSearch/StateMachine/ContributionSearch.php
new file mode 100644
index 00000000..b8defeb6
--- /dev/null
+++ b/CRM/DataprocessorSearch/StateMachine/ContributionSearch.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * @author Jaap Jansma <jaap.jansma@civicoop.org>
+ * @license AGPL-3.0
+ */
+
+class CRM_DataprocessorSearch_StateMachine_ContributionSearch extends CRM_Core_StateMachine {
+
+  /**
+   * The task that the wizard is currently processing
+   *
+   * @var string
+   */
+  protected $_task;
+
+  /**
+   * Class constructor.
+   *
+   * @param object $controller
+   * @param \const|int $action
+   */
+  public function __construct($controller, $action = CRM_Core_Action::NONE) {
+    parent::__construct($controller, $action);
+
+    $this->_pages = array();
+    $this->_pages['Basic'] = array(
+      'className' => 'CRM_DataprocessorSearch_Form_ContributionSearch',
+    );
+    list($task, $result) = $this->taskName($controller);
+    $this->_task = $task;
+    if (is_array($task)) {
+      foreach ($task as $t) {
+        $this->_pages[$t] = NULL;
+      }
+    }
+    else {
+      $this->_pages[$task] = NULL;
+    }
+
+    if ($result) {
+      $this->_pages['CRM_Contribute_Form_Task_Result'] = NULL;
+    }
+
+    $this->addSequentialPages($this->_pages, $action);
+  }
+
+  /**
+   * Determine the form name based on the action. This allows us
+   * to avoid using  conditional state machine, much more efficient
+   * and simpler
+   *
+   * @param CRM_Core_Controller $controller
+   *   The controller object.
+   *
+   * @return array
+   *   the name of the form that will handle the task
+   */
+  public function taskName($controller) {
+    // total hack, check POST vars and then session to determine stuff
+    $value = CRM_Utils_Array::value('task', $_POST);
+    if (!isset($value)) {
+      $value = $controller->get('task');
+    }
+    $this->_controller->set('task', $value);
+
+    return CRM_Contribute_Task::getTask($value);
+  }
+
+  /**
+   * Return the form name of the task.
+   *
+   * @return string
+   */
+  public function getTaskFormName() {
+    if (is_array($this->_task)) {
+      // return first page
+      return CRM_Utils_String::getClassName($this->_task[0]);
+    }
+    else {
+      return CRM_Utils_String::getClassName($this->_task);
+    }
+  }
+
+  /**
+   * Since this is a state machine for search and we want to come back to the same state
+   * we dont want to issue a reset of the state session when we are done processing a task
+   */
+  public function shouldReset() {
+    return FALSE;
+  }
+
+}
diff --git a/Civi/DataProcessor/Factory.php b/Civi/DataProcessor/Factory.php
index 3292f2b4..cef2312c 100644
--- a/Civi/DataProcessor/Factory.php
+++ b/Civi/DataProcessor/Factory.php
@@ -125,6 +125,7 @@ class Factory {
     $this->addOutput('contact_search', 'CRM_Contact_DataProcessorContactSearch', E::ts('Contact Search'));
     $this->addOutput('activity_search', 'CRM_DataprocessorSearch_ActivitySearch', E::ts('Activity Search'));
     $this->addOutput('case_search', 'CRM_DataprocessorSearch_CaseSearch', E::ts('Case Search'));
+    $this->addOutput('contribution_search', 'CRM_DataprocessorSearch_ContributionSearch', E::ts('Contribution Search'));
     $this->addOutput('participant_search', 'CRM_DataprocessorSearch_ParticipantSearch', E::ts('Participant Search'));
     $this->addOutput('export_csv', 'CRM_DataprocessorOutputExport_CSV', E::ts('CSV Export'));
     $this->addFilter('simple_sql_filter', 'Civi\DataProcessor\FilterHandler\SimpleSqlFilter', E::ts('Field filter'));
diff --git a/templates/CRM/DataprocessorSearch/Form/ContributionSearch.tpl b/templates/CRM/DataprocessorSearch/Form/ContributionSearch.tpl
new file mode 100644
index 00000000..004d808e
--- /dev/null
+++ b/templates/CRM/DataprocessorSearch/Form/ContributionSearch.tpl
@@ -0,0 +1 @@
+{include file="CRM/DataprocessorSearch/Form/Search.tpl"}
\ No newline at end of file
diff --git a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ContributionSearch.tpl b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ContributionSearch.tpl
new file mode 100644
index 00000000..cf52f6b9
--- /dev/null
+++ b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ContributionSearch.tpl
@@ -0,0 +1,32 @@
+{crmScope extensionKey='dataprocessor'}
+    <div class="crm-section">
+        <div class="label">{$form.navigation_parent_path.label}</div>
+        <div class="content">{$form.navigation_parent_path.html}</div>
+        <div class="clear"></div>
+    </div>
+    <div class="crm-section">
+        <div class="label">{$form.permission.label}</div>
+        <div class="content">{$form.permission.html}</div>
+        <div class="clear"></div>
+    </div>
+    <div class="crm-section">
+        <div class="label">{$form.contribution_id_field.label}</div>
+        <div class="content">{$form.contribution_id_field.html}</div>
+        <div class="clear"></div>
+    </div>
+    <div class="crm-section">
+        <div class="label">{$form.hide_id_field.label}</div>
+        <div class="content">{$form.hide_id_field.html}</div>
+        <div class="clear"></div>
+    </div>
+    <div class="crm-section">
+      <div class="label">{$form.expanded_search.label}</div>
+      <div class="content">{$form.expanded_search.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>
+        <div class="clear"></div>
+    </div>
+{/crmScope}
-- 
GitLab