From 47f375b67ca2feb120f02e1475a5f49e028120ac Mon Sep 17 00:00:00 2001
From: Jaap Jansma <jaap.jansma@civicoop.org>
Date: Wed, 26 Apr 2023 13:08:57 +0200
Subject: [PATCH] Added functionality to have a hard limit on a selection

---
 CHANGELOG.md                                  |   1 +
 CRM/Contact/DataProcessorContactSearch.php    |  11 ++
 .../Selector/DataProcessorContactSearch.php   |   1 +
 .../Form/Output/AbstractUIOutputForm.php      | 117 ++++++++++++++++++
 .../AbstractOutputExport.php                  |  15 ++-
 CRM/DataprocessorSearch/ActivitySearch.php    |  10 ++
 CRM/DataprocessorSearch/CaseSearch.php        |  10 ++
 .../ContributionSearch.php                    |  10 ++
 .../Form/AbstractSearch.php                   |  77 ++++++------
 CRM/DataprocessorSearch/MembershipSearch.php  |  10 ++
 CRM/DataprocessorSearch/ParticipantSearch.php |  10 ++
 CRM/DataprocessorSearch/Search.php            |  10 ++
 .../DataProcessorContactSearch.tpl            |  22 +++-
 .../Form/Output/UIOutput/CriteriaForm.tpl     |  17 ++-
 .../OutputConfiguration/ActivitySearch.tpl    |  21 ++++
 .../Form/OutputConfiguration/CaseSearch.tpl   |  21 ++++
 .../ContributionSearch.tpl                    |  21 ++++
 .../OutputConfiguration/MembershipSearch.tpl  |  21 ++++
 .../OutputConfiguration/ParticipantSearch.tpl |  21 ++++
 .../Form/OutputConfiguration/Search.tpl       |  21 ++++
 20 files changed, 402 insertions(+), 45 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ed1a360e..0f7d2683 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 * Added field output handler: Arthmetic Operations.
 * Added field output handler for date range segments based on months.
 * Fixed issue with permission to view contact. Also works now without a user record
+* Added functionality to have a hard limit on a selection
 
 # Version 1.66
 
diff --git a/CRM/Contact/DataProcessorContactSearch.php b/CRM/Contact/DataProcessorContactSearch.php
index 4bd7c327..36906804 100644
--- a/CRM/Contact/DataProcessorContactSearch.php
+++ b/CRM/Contact/DataProcessorContactSearch.php
@@ -60,6 +60,9 @@ class CRM_Contact_DataProcessorContactSearch implements UIFormOutputInterface {
 
     $form->add('checkbox', 'expanded_search', E::ts('Expand criteria form initially?'));
 
+    $form->add('checkbox', 'enable_hard_limit', E::ts('Enable Hard Limit?'));
+    $form->add('text', 'default_hard_limit', E::ts('Default Hard Limit'), array('class' => 'huge'), false);
+
     $form->addYesNo('link_to_view_contact', E::ts('Add link to view contact?'));
 
     // navigation field
@@ -106,6 +109,12 @@ class CRM_Contact_DataProcessorContactSearch implements UIFormOutputInterface {
         else {
           $defaults['link_to_view_contact'] = TRUE;
         }
+        if (isset($output['configuration']['enable_hard_limit'])) {
+          $defaults['enable_hard_limit'] = $output['configuration']['enable_hard_limit'];
+        }
+        if (isset($output['configuration']['default_hard_limit'])) {
+          $defaults['default_hard_limit'] = $output['configuration']['default_hard_limit'];
+        }
       }
     }
     if (!isset($defaults['permission'])) {
@@ -144,6 +153,8 @@ class CRM_Contact_DataProcessorContactSearch implements UIFormOutputInterface {
     $configuration['help_text'] = $submittedValues['help_text'];
     $configuration['expanded_search'] = isset($submittedValues['expanded_search']) ? $submittedValues['expanded_search'] : false;
     $configuration['link_to_view_contact'] = $submittedValues['link_to_view_contact'];
+    $configuration['enable_hard_limit'] = isset($submittedValues['enable_hard_limit']) ? $submittedValues['enable_hard_limit'] : false;
+    $configuration['default_hard_limit'] = $submittedValues['default_hard_limit'];
     return $configuration;
   }
 
diff --git a/CRM/Contact/Selector/DataProcessorContactSearch.php b/CRM/Contact/Selector/DataProcessorContactSearch.php
index 52a2a33e..86240431 100644
--- a/CRM/Contact/Selector/DataProcessorContactSearch.php
+++ b/CRM/Contact/Selector/DataProcessorContactSearch.php
@@ -44,6 +44,7 @@ class CRM_Contact_Selector_DataProcessorContactSearch {
       }
       $this->dataProcessorClass->getDataFlow()->addSort($sortField['name'], $sortDirection);
     }
+    CRM_Dataprocessor_Form_Output_AbstractUIOutputForm::applyLimit($this->dataProcessorClass, $formValues);
   }
 
   /**
diff --git a/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php b/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php
index fd3885eb..88ab305d 100644
--- a/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php
+++ b/CRM/Dataprocessor/Form/Output/AbstractUIOutputForm.php
@@ -4,6 +4,7 @@
  * @license AGPL-3.0
  */
 
+use Civi\DataProcessor\DataFlow\InvalidFlowException;
 use Civi\DataProcessor\FilterHandler\AbstractFilterHandler;
 use Civi\DataProcessor\Output\UIOutputInterface;
 use Civi\DataProcessor\ProcessorType\AbstractProcessorType;
@@ -199,6 +200,122 @@ abstract class CRM_Dataprocessor_Form_Output_AbstractUIOutputForm extends CRM_Co
     return $errors;
   }
 
+  /**
+   * Returns whether hard limit is enabled.
+   *
+   * @return bool
+   */
+  protected function isHardLimitEnabled(): bool {
+    if (isset($this->dataProcessorOutput['configuration']) && isset($this->dataProcessorOutput['configuration']['enable_hard_limit']) && $this->dataProcessorOutput['configuration']['enable_hard_limit']) {
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Returns the default hard limit
+   *
+   * @return string
+   */
+  protected function getDefaultHardLimit(): string {
+    if (isset($this->dataProcessorOutput['configuration']) && isset($this->dataProcessorOutput['configuration']['default_hard_limit'])) {
+      return $this->dataProcessorOutput['configuration']['default_hard_limit'];
+    }
+    return '';
+  }
+
+  /**
+   * Returns the default row limit.
+   *
+   * @return int
+   */
+  protected static function getDefaultLimit(): int {
+    return CRM_Utils_Pager::ROWCOUNT;
+  }
+
+  /**
+   * Returns the count of the current data processor.
+   * Taking into account a hard limit (if set).
+   *
+   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessor
+   * @param array|NULL $submittedValues
+   *
+   * @return int
+   */
+  public static function getCount(AbstractProcessorType $dataProcessor, array $submittedValues = null): int {
+    $count = 0;
+    try {
+      $count = $dataProcessor->getDataFlow()->recordCount();
+      $hardLimit = static::getHardLimit($submittedValues);
+      if ($hardLimit && $count > $hardLimit) {
+        $count = $hardLimit;
+      }
+    } catch (InvalidFlowException $e) {
+    }
+    return $count;
+  }
+
+  /**
+   * Apply the offset and limit
+   *
+   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessor
+   * @param array|null $submittedValues
+   *
+   * @return void
+   */
+  public static function applyLimit(AbstractProcessorType $dataProcessor, array $submittedValues = null) {
+    $hardLimit = static::getHardLimit($submittedValues);
+    $limit = static::getDefaultLimit();
+    if (isset($submittedValues['crmRowCount']) && $submittedValues['crmRowCount']) {
+      $limit = $submittedValues['crmRowCount'];
+    }
+    $pageId = isset($submittedValues['crmPID']) ?? 1;
+    $offset = static::getOffset($pageId, $limit, $hardLimit);
+    if ($hardLimit && $limit > $hardLimit) {
+      $limit = $hardLimit;
+    }
+    try {
+      $dataProcessor->getDataFlow()->setOffset($offset);
+      $dataProcessor->getDataFlow()->setLimit($limit);
+    } catch (InvalidFlowException $e) {
+    }
+  }
+
+  /**
+   * Returns the hard limit values
+   *
+   * @param array|null $submittedValues
+   * @return int|null
+   */
+  protected static function getHardLimit(array $submittedValues=null):? int {
+    if (isset($submittedValues['hard_limit']) && is_numeric($submittedValues['hard_limit'])) {
+      return (int) $submittedValues['hard_limit'];
+    }
+    return null;
+  }
+
+  /**
+   * Returns the offset for this page
+   *
+   * @param int $pageId
+   * @param int $limit
+   * @param int|NULL $hardLimit
+   *
+   * @return int
+   */
+  public static function getOffset(int $pageId, int $limit, int $hardLimit = null): int {
+    if ($pageId) {
+      $offset = ($pageId - 1) * $limit;
+    } else {
+      $offset = 0;
+    }
+    if ($hardLimit && $offset > $hardLimit) {
+      $pageId = $hardLimit / $limit;
+      $offset = ($pageId -1) * $limit;
+    }
+    return $offset;
+  }
+
   /**
    * Apply the filters to the database processor
    */
diff --git a/CRM/DataprocessorOutputExport/AbstractOutputExport.php b/CRM/DataprocessorOutputExport/AbstractOutputExport.php
index 7bd700f0..b735d256 100644
--- a/CRM/DataprocessorOutputExport/AbstractOutputExport.php
+++ b/CRM/DataprocessorOutputExport/AbstractOutputExport.php
@@ -205,15 +205,22 @@ abstract class CRM_DataprocessorOutputExport_AbstractOutputExport implements Exp
   public function downloadExport(AbstractProcessorType $dataProcessorClass, $dataProcessor, $outputBAO, $formValues, $sortFieldName = null, $sortDirection = 'ASC', $idField=null, $selectedIds=array()) {
     try {
       static::addSelectedIdsFilterToDataProcessor($dataProcessorClass, $idField, $selectedIds);
-      if ($dataProcessorClass->getDataFlow()->recordCount() > $this->getMaxDirectDownload()) {
+      $count = CRM_Dataprocessor_Form_Output_AbstractUIOutputForm::getCount($dataProcessorClass, $formValues);
+      if ($count > $this->getMaxDirectDownload()) {
         $this->startBatchJob($dataProcessorClass, $dataProcessor, $outputBAO, $formValues, $sortFieldName, $sortDirection, $idField, $selectedIds);
       } else {
+        $dataProcessorClass->getDataFlow()->setOffset(0);
+        $dataProcessorClass->getDataFlow()->setLimit($count);
         $this->doDirectDownload($dataProcessorClass, $dataProcessor, $outputBAO, $sortFieldName, $sortDirection, $idField, $selectedIds, $formValues);
       }
     } catch (InvalidFlowException $e) {
     }
   }
 
+  public static function getHardLimit(array $formValues):? int {
+    return 2;
+  }
+
   /**
    * Get the download name of the export file.
    *
@@ -304,11 +311,7 @@ abstract class CRM_DataprocessorOutputExport_AbstractOutputExport implements Exp
     //now add this task to the queue
     $queue->createItem($task);
 
-    $count = 0;
-    try {
-      $count = $dataProcessorClass->getDataFlow()->recordCount();
-    } catch (InvalidFlowException $e) {
-    }
+    $count = CRM_Dataprocessor_Form_Output_AbstractUIOutputForm::getCount($dataProcessorClass, $formValues);
     $recordsPerJob = $this->getJobSize();
     for($i=0; $i < $count; $i = $i + $recordsPerJob) {
       $title = E::ts('Exporting records %1/%2', array(
diff --git a/CRM/DataprocessorSearch/ActivitySearch.php b/CRM/DataprocessorSearch/ActivitySearch.php
index f03a4858..4bee8bd0 100644
--- a/CRM/DataprocessorSearch/ActivitySearch.php
+++ b/CRM/DataprocessorSearch/ActivitySearch.php
@@ -59,6 +59,8 @@ class CRM_DataprocessorSearch_ActivitySearch implements UIFormOutputInterface {
     $form->add('wysiwyg', 'help_text', E::ts('Help text for this search'), array('rows' => 6, 'cols' => 80));
     $form->add('text', 'no_result_text', E::ts('No result text'), array('class' => 'huge'), false);
     $form->add('checkbox', 'expanded_search', E::ts('Expand criteria form initially'));
+    $form->add('checkbox', 'enable_hard_limit', E::ts('Enable Hard Limit?'));
+    $form->add('text', 'default_hard_limit', E::ts('Default Hard Limit'), array('class' => 'huge'), false);
 
     // navigation field
     $navigationOptions = $navigation->getNavigationOptions();
@@ -97,6 +99,12 @@ class CRM_DataprocessorSearch_ActivitySearch implements UIFormOutputInterface {
         if (isset($output['configuration']['expanded_search'])) {
           $defaults['expanded_search'] = $output['configuration']['expanded_search'];
         }
+        if (isset($output['configuration']['enable_hard_limit'])) {
+          $defaults['enable_hard_limit'] = $output['configuration']['enable_hard_limit'];
+        }
+        if (isset($output['configuration']['default_hard_limit'])) {
+          $defaults['default_hard_limit'] = $output['configuration']['default_hard_limit'];
+        }
       }
     }
     if (!isset($defaults['permission'])) {
@@ -133,6 +141,8 @@ class CRM_DataprocessorSearch_ActivitySearch implements UIFormOutputInterface {
     $configuration['hidden_fields'] = $submittedValues['hidden_fields'];
     $configuration['help_text'] = $submittedValues['help_text'];
     $configuration['expanded_search'] = isset($submittedValues['expanded_search']) ? $submittedValues['expanded_search'] : false;
+    $configuration['enable_hard_limit'] = isset($submittedValues['enable_hard_limit']) ? $submittedValues['enable_hard_limit'] : false;
+    $configuration['default_hard_limit'] = $submittedValues['default_hard_limit'];
     return $configuration;
   }
 
diff --git a/CRM/DataprocessorSearch/CaseSearch.php b/CRM/DataprocessorSearch/CaseSearch.php
index 26fb5190..ecbb892d 100644
--- a/CRM/DataprocessorSearch/CaseSearch.php
+++ b/CRM/DataprocessorSearch/CaseSearch.php
@@ -65,6 +65,8 @@ class CRM_DataprocessorSearch_CaseSearch implements UIFormOutputInterface {
     $form->add('text', 'no_result_text', E::ts('No result text'), array('class' => 'huge'), false);
     $form->add('checkbox', 'expanded_search', E::ts('Expand criteria form initially'));
     $form->add('checkbox', 'show_manage_case', E::ts('Show manage case column'));
+    $form->add('checkbox', 'enable_hard_limit', E::ts('Enable Hard Limit?'));
+    $form->add('text', 'default_hard_limit', E::ts('Default Hard Limit'), array('class' => 'huge'), false);
 
     // navigation field
     $navigationOptions = $navigation->getNavigationOptions();
@@ -109,6 +111,12 @@ class CRM_DataprocessorSearch_CaseSearch implements UIFormOutputInterface {
         if (isset($output['configuration']['show_manage_case'])) {
           $defaults['show_manage_case'] = $output['configuration']['show_manage_case'];
         }
+        if (isset($output['configuration']['enable_hard_limit'])) {
+          $defaults['enable_hard_limit'] = $output['configuration']['enable_hard_limit'];
+        }
+        if (isset($output['configuration']['default_hard_limit'])) {
+          $defaults['default_hard_limit'] = $output['configuration']['default_hard_limit'];
+        }
       }
     }
     if (!isset($defaults['permission'])) {
@@ -147,6 +155,8 @@ class CRM_DataprocessorSearch_CaseSearch implements UIFormOutputInterface {
     $configuration['help_text'] = $submittedValues['help_text'];
     $configuration['expanded_search'] = isset($submittedValues['expanded_search']) ? $submittedValues['expanded_search'] : false;
     $configuration['show_manage_case'] = isset($submittedValues['show_manage_case']) ? $submittedValues['show_manage_case'] : false;
+    $configuration['enable_hard_limit'] = isset($submittedValues['enable_hard_limit']) ? $submittedValues['enable_hard_limit'] : false;
+    $configuration['default_hard_limit'] = $submittedValues['default_hard_limit'];
     return $configuration;
   }
 
diff --git a/CRM/DataprocessorSearch/ContributionSearch.php b/CRM/DataprocessorSearch/ContributionSearch.php
index c77a785d..aa054ba9 100644
--- a/CRM/DataprocessorSearch/ContributionSearch.php
+++ b/CRM/DataprocessorSearch/ContributionSearch.php
@@ -59,6 +59,8 @@ class CRM_DataprocessorSearch_ContributionSearch implements UIFormOutputInterfac
     $form->add('wysiwyg', 'help_text', E::ts('Help text for this search'), array('rows' => 6, 'cols' => 80));
     $form->add('text', 'no_result_text', E::ts('No result text'), array('class' => 'huge'), false);
     $form->add('checkbox', 'expanded_search', E::ts('Expand criteria form initially'));
+    $form->add('checkbox', 'enable_hard_limit', E::ts('Enable Hard Limit?'));
+    $form->add('text', 'default_hard_limit', E::ts('Default Hard Limit'), array('class' => 'huge'), false);
 
     // navigation field
     $navigationOptions = $navigation->getNavigationOptions();
@@ -97,6 +99,12 @@ class CRM_DataprocessorSearch_ContributionSearch implements UIFormOutputInterfac
         if (isset($output['configuration']['expanded_search'])) {
           $defaults['expanded_search'] = $output['configuration']['expanded_search'];
         }
+        if (isset($output['configuration']['enable_hard_limit'])) {
+          $defaults['enable_hard_limit'] = $output['configuration']['enable_hard_limit'];
+        }
+        if (isset($output['configuration']['default_hard_limit'])) {
+          $defaults['default_hard_limit'] = $output['configuration']['default_hard_limit'];
+        }
       }
     }
     if (!isset($defaults['permission'])) {
@@ -133,6 +141,8 @@ class CRM_DataprocessorSearch_ContributionSearch implements UIFormOutputInterfac
     $configuration['no_result_text'] = $submittedValues['no_result_text'];
     $configuration['help_text'] = $submittedValues['help_text'];
     $configuration['expanded_search'] = isset($submittedValues['expanded_search']) ? $submittedValues['expanded_search'] : false;
+    $configuration['enable_hard_limit'] = isset($submittedValues['enable_hard_limit']) ? $submittedValues['enable_hard_limit'] : false;
+    $configuration['default_hard_limit'] = $submittedValues['default_hard_limit'];
     return $configuration;
   }
 
diff --git a/CRM/DataprocessorSearch/Form/AbstractSearch.php b/CRM/DataprocessorSearch/Form/AbstractSearch.php
index 78de8b18..3408e08f 100644
--- a/CRM/DataprocessorSearch/Form/AbstractSearch.php
+++ b/CRM/DataprocessorSearch/Form/AbstractSearch.php
@@ -118,6 +118,18 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
     return false;
   }
 
+  /**
+   * Returns the key for the (prevnext) cache
+   * @return string
+   */
+  protected function getCacheKey(): string {
+    $qfKeyParam = CRM_Utils_Array::value('qfKey', $this->_formValues);
+    if (empty($qfKeyParam) && $this->controller->_key) {
+      $qfKeyParam = $this->controller->_key;
+    }
+    return "civicrm search " . $qfKeyParam;
+  }
+
   /**
    * Returns whether the ID field is Visible
    *
@@ -219,6 +231,10 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
       $this->_formValues = $this->controller->exportValues($this->_name);
     }
 
+    if ($this->isHardLimitEnabled() && !isset($this->_formValues['hard_limit'])) {
+      $this->_formValues['hard_limit'] = $this->getDefaultHardLimit();
+    }
+
     $this->_searchButtonName = $this->getButtonName('refresh');
     $this->_actionButtonName = $this->getButtonName('next', 'action');
     $this->_done = FALSE;
@@ -240,9 +256,8 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
         $this->sort->initSortID($this->_formValues[CRM_Utils_Sort::SORT_ID]);
       }
       $this->assign_by_ref('sort', $this->sort);
-      $limit = CRM_Utils_Request::retrieveValue('crmRowCount', 'Positive', $this->getDefaultLimit());
       $pageId = CRM_Utils_Request::retrieveValue('crmPID', 'Positive', 1);
-      $this->buildRows($pageId, $limit);
+      $this->buildRows($pageId);
       $this->addExportOutputs();
     }
 
@@ -265,22 +280,12 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
     return false;
   }
 
-  /**
-   * Returns the default row limit.
-   *
-   * @return int
-   */
-  protected function getDefaultLimit(): int {
-    return CRM_Utils_Pager::ROWCOUNT;
-  }
-
   /**
    * Retrieve the records from the data processor
    *
    * @param $pageId
-   * @param $limit
    */
-  protected function buildRows($pageId, $limit) {
+  protected function buildRows($pageId) {
     $rows = [];
     $ids = array();
     $prevnextData = array();
@@ -288,12 +293,8 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
 
     $id_field = $this->getIdFieldName();
     $this->assign('id_field', $id_field);
-
-    $offset = ($pageId - 1) * $limit;
     try {
-      $this->dataProcessorClass->getDataFlow()->setLimit($limit);
-      $this->dataProcessorClass->getDataFlow()->setOffset($offset);
-      self::applyFilters($this->dataProcessorClass, $this->_formValues);
+      static::applyFilters($this->dataProcessorClass, $this->_formValues);
 
       // Set the sort
       if ($this->sort->getCurrentSortID() > 1) {
@@ -310,11 +311,12 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
       } elseif ($id_field) {
         $this->dataProcessorClass->getDataFlow()->addSort($id_field, 'ASC');
       }
+      static::applyLimit($this->dataProcessorClass, $this->_formValues);
 
       $this->alterDataProcessor($this->dataProcessorClass);
       $this->dataProcessorClass->postInitialize();
       $pagerParams = $this->getPagerParams();
-      $pagerParams['total'] = $this->dataProcessorClass->getDataFlow()->recordCount();
+      $pagerParams['total'] = static::getCount($this->dataProcessorClass, $this->_formValues);
       $pagerParams['pageID'] = $pageId;
       $this->pager = new CRM_Utils_Pager($pagerParams);
       $this->assign('pager', $this->pager);
@@ -354,7 +356,7 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
           $prevnextData[] = array(
             'entity_id1' => $row['id'],
             'entity_table' => $this->getEntityTable(),
-            'data' => json_encode($record),
+            'data' => '', // json_encode($record),
           );
         }
         $ids[] = $row['id'];
@@ -381,8 +383,7 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
     }
 
     if ($this->usePrevNextCache()) {
-      $cacheKey = "civicrm search {$this->controller->_key}";
-      CRM_DataprocessorSearch_Utils_PrevNextCache::fillWithArray($cacheKey, $prevnextData);
+      CRM_DataprocessorSearch_Utils_PrevNextCache::fillWithArray($this->getCacheKey(), $prevnextData);
     } else {
       $this->retrieveEntityIds();
     }
@@ -437,7 +438,7 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
     $params['total'] = 0;
     $params['status'] =E::ts('%%StatusMessage%%');
     $params['csvString'] = NULL;
-    $params['rowCount'] =  $this->getDefaultLimit();
+    $params['rowCount'] =  static::getDefaultLimit();
     $params['buttonTop'] = 'PagerTopButton';
     $params['buttonBottom'] = 'PagerBottomButton';
     return $params;
@@ -493,6 +494,12 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
     $this->assign_by_ref('selectedIds', $selectedIds);
     $this->add('hidden', 'context');
     $this->add('hidden', CRM_Utils_Sort::SORT_ID);
+
+    $this->assign('hard_limit_enabled', $this->isHardLimitEnabled());
+    if ($this->isHardLimitEnabled()) {
+      $this->add('text', 'hard_limit', E::ts('Hard Limit'));
+      $this->setDefaults(['hard_limit' => $this->getDefaultHardLimit()]);
+    }
   }
 
   public function setDefaultValues(): array {
@@ -526,8 +533,7 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
       ($this->_force && !$crmPID)
     ) {
       //reset the cache table for new search
-      $cacheKey = "civicrm search {$this->controller->_key}";
-      CRM_DataprocessorSearch_Utils_PrevNextCache::deleteItem(NULL, $cacheKey);
+      CRM_DataprocessorSearch_Utils_PrevNextCache::deleteItem(NULL, $this->getCacheKey());
     }
 
     if (!empty($_POST)) {
@@ -581,19 +587,12 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
       return $this->entityIDs;
     }
     $this->entityIDs = [];
-    $qfKeyParam = CRM_Utils_Array::value('qfKey', $this->_formValues);
-    if (empty($qfKeyParam) && $this->controller->_key) {
-      $qfKeyParam = $this->controller->_key;
-    }
     // We use ajax to handle selections only if the search results component_mode is set to "contacts"
     if ($this->usePrevNextCache()) {
       $this->addClass('crm-ajax-selection-form');
-      if ($qfKeyParam) {
-        $qfKeyParam = "civicrm search $qfKeyParam";
-        $selectedIdsArr = CRM_DataprocessorSearch_Utils_PrevNextCache::getSelection($qfKeyParam);
-        if (isset($selectedIdsArr[$qfKeyParam]) && is_array($selectedIdsArr[$qfKeyParam])) {
-          $this->entityIDs = array_keys($selectedIdsArr[$qfKeyParam]);
-        }
+      $selectedIdsArr = CRM_DataprocessorSearch_Utils_PrevNextCache::getSelection($this->getCacheKey());
+      if (isset($selectedIdsArr[$this->getCacheKey()]) && is_array($selectedIdsArr[$this->getCacheKey()])) {
+        $this->entityIDs = array_keys($selectedIdsArr[$this->getCacheKey()]);
       }
     } else {
       if (isset($this->_formValues['radio_ts']) && $this->_formValues['radio_ts'] == 'ts_sel') {
@@ -604,7 +603,11 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
         }
       } elseif (!$skipSelectAll && (!isset($this->_formValues['radio_ts']) || $this->_formValues['radio_ts'] == 'ts_all')) {
         try {
-          $this->dataProcessorClass->getDataFlow()->setLimit(FALSE);
+          $limit = FALSE;
+          if ($this->getHardLimit()) {
+            $limit = $this->getHardLimit();
+          }
+          $this->dataProcessorClass->getDataFlow()->setLimit($limit);
           $this->dataProcessorClass->getDataFlow()->setOffset(0);
           $id_field = $this->getIdFieldName();
           while ($record = $this->dataProcessorClass->getDataFlow()
@@ -630,7 +633,7 @@ abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataproce
     // We use ajax to handle selections only if the search results component_mode is set to "contacts"
     if ($this->allRecordsSelected()) {
       try {
-        return $this->dataProcessorClass->getDataFlow()->recordCount();
+        return static::getCount($this->dataProcessorClass, $this->_formValues);
       } catch (InvalidFlowException $e) {
         // Do nothing
       }
diff --git a/CRM/DataprocessorSearch/MembershipSearch.php b/CRM/DataprocessorSearch/MembershipSearch.php
index 4b63d1a0..c48766b0 100644
--- a/CRM/DataprocessorSearch/MembershipSearch.php
+++ b/CRM/DataprocessorSearch/MembershipSearch.php
@@ -59,6 +59,8 @@ class CRM_DataprocessorSearch_MembershipSearch implements UIFormOutputInterface
     $form->add('wysiwyg', 'help_text', E::ts('Help text for this search'), array('rows' => 6, 'cols' => 80));
     $form->add('text', 'no_result_text', E::ts('No result text'), array('class' => 'huge'), false);
     $form->add('checkbox', 'expanded_search', E::ts('Expand criteria form initially'));
+    $form->add('checkbox', 'enable_hard_limit', E::ts('Enable Hard Limit?'));
+    $form->add('text', 'default_hard_limit', E::ts('Default Hard Limit'), array('class' => 'huge'), false);
 
     // navigation field
     $navigationOptions = $navigation->getNavigationOptions();
@@ -97,6 +99,12 @@ class CRM_DataprocessorSearch_MembershipSearch implements UIFormOutputInterface
         if (isset($output['configuration']['expanded_search'])) {
           $defaults['expanded_search'] = $output['configuration']['expanded_search'];
         }
+        if (isset($output['configuration']['enable_hard_limit'])) {
+          $defaults['enable_hard_limit'] = $output['configuration']['enable_hard_limit'];
+        }
+        if (isset($output['configuration']['default_hard_limit'])) {
+          $defaults['default_hard_limit'] = $output['configuration']['default_hard_limit'];
+        }
       }
     }
     if (!isset($defaults['permission'])) {
@@ -133,6 +141,8 @@ class CRM_DataprocessorSearch_MembershipSearch implements UIFormOutputInterface
     $configuration['hidden_fields'] = $submittedValues['hidden_fields'];
     $configuration['help_text'] = $submittedValues['help_text'];
     $configuration['expanded_search'] = isset($submittedValues['expanded_search']) ? $submittedValues['expanded_search'] : false;
+    $configuration['enable_hard_limit'] = isset($submittedValues['enable_hard_limit']) ? $submittedValues['enable_hard_limit'] : false;
+    $configuration['default_hard_limit'] = $submittedValues['default_hard_limit'];
     return $configuration;
   }
 
diff --git a/CRM/DataprocessorSearch/ParticipantSearch.php b/CRM/DataprocessorSearch/ParticipantSearch.php
index 4bfdcef9..628e29a1 100644
--- a/CRM/DataprocessorSearch/ParticipantSearch.php
+++ b/CRM/DataprocessorSearch/ParticipantSearch.php
@@ -59,6 +59,8 @@ class CRM_DataprocessorSearch_ParticipantSearch implements UIFormOutputInterface
     $form->add('wysiwyg', 'help_text', E::ts('Help text for this search'), array('rows' => 6, 'cols' => 80));
     $form->add('text', 'no_result_text', E::ts('No result text'), array('class' => 'huge'), false);
     $form->add('checkbox', 'expanded_search', E::ts('Expand criteria form initially'));
+    $form->add('checkbox', 'enable_hard_limit', E::ts('Enable Hard Limit?'));
+    $form->add('text', 'default_hard_limit', E::ts('Default Hard Limit'), array('class' => 'huge'), false);
 
     // navigation field
     $navigationOptions = $navigation->getNavigationOptions();
@@ -97,6 +99,12 @@ class CRM_DataprocessorSearch_ParticipantSearch implements UIFormOutputInterface
         if (isset($output['configuration']['expanded_search'])) {
           $defaults['expanded_search'] = $output['configuration']['expanded_search'];
         }
+        if (isset($output['configuration']['enable_hard_limit'])) {
+          $defaults['enable_hard_limit'] = $output['configuration']['enable_hard_limit'];
+        }
+        if (isset($output['configuration']['default_hard_limit'])) {
+          $defaults['default_hard_limit'] = $output['configuration']['default_hard_limit'];
+        }
       }
     }
     if (!isset($defaults['permission'])) {
@@ -133,6 +141,8 @@ class CRM_DataprocessorSearch_ParticipantSearch implements UIFormOutputInterface
     $configuration['hidden_fields'] = $submittedValues['hidden_fields'];
     $configuration['help_text'] = $submittedValues['help_text'];
     $configuration['expanded_search'] = isset($submittedValues['expanded_search']) ? $submittedValues['expanded_search'] : false;
+    $configuration['enable_hard_limit'] = isset($submittedValues['enable_hard_limit']) ? $submittedValues['enable_hard_limit'] : false;
+    $configuration['default_hard_limit'] = $submittedValues['default_hard_limit'];
     return $configuration;
   }
 
diff --git a/CRM/DataprocessorSearch/Search.php b/CRM/DataprocessorSearch/Search.php
index 27f39f9b..f2e8f267 100644
--- a/CRM/DataprocessorSearch/Search.php
+++ b/CRM/DataprocessorSearch/Search.php
@@ -62,6 +62,8 @@ class CRM_DataprocessorSearch_Search implements UIFormOutputInterface {
     $form->add('checkbox', 'expanded_search', E::ts('Expand criteria form initially'));
     $form->add('checkbox', 'expose_aggregate', E::ts('Expose aggregate options'));
     $form->add('checkbox', 'expose_hidden_fields', E::ts('Expose hidden field options'));
+    $form->add('checkbox', 'enable_hard_limit', E::ts('Enable Hard Limit?'));
+    $form->add('text', 'default_hard_limit', E::ts('Default Hard Limit'), array('class' => 'huge'), false);
 
     // navigation field
     $navigationOptions = $navigation->getNavigationOptions();
@@ -106,6 +108,12 @@ class CRM_DataprocessorSearch_Search implements UIFormOutputInterface {
         if (isset($output['configuration']['expose_hidden_fields'])) {
           $defaults['expose_hidden_fields'] = $output['configuration']['expose_hidden_fields'];
         }
+        if (isset($output['configuration']['enable_hard_limit'])) {
+          $defaults['enable_hard_limit'] = $output['configuration']['enable_hard_limit'];
+        }
+        if (isset($output['configuration']['default_hard_limit'])) {
+          $defaults['default_hard_limit'] = $output['configuration']['default_hard_limit'];
+        }
       }
     }
     if (!isset($defaults['permission'])) {
@@ -143,6 +151,8 @@ class CRM_DataprocessorSearch_Search implements UIFormOutputInterface {
     $configuration['expanded_search'] = isset($submittedValues['expanded_search']) ? $submittedValues['expanded_search'] : false;
     $configuration['expose_aggregate'] = isset($submittedValues['expose_aggregate']) ? $submittedValues['expose_aggregate'] : false;
     $configuration['expose_hidden_fields'] = isset($submittedValues['expose_hidden_fields']) ? $submittedValues['expose_hidden_fields'] : false;
+    $configuration['enable_hard_limit'] = isset($submittedValues['enable_hard_limit']) ? $submittedValues['enable_hard_limit'] : false;
+    $configuration['default_hard_limit'] = $submittedValues['default_hard_limit'];
     return $configuration;
   }
 
diff --git a/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSearch.tpl b/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSearch.tpl
index 45c0c497..0ef5ba2a 100644
--- a/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSearch.tpl
+++ b/templates/CRM/Contact/Form/OutputConfiguration/DataProcessorContactSearch.tpl
@@ -54,5 +54,25 @@
       <div class="content">{$form.no_result_text.html}</div>
       <div class="clear"></div>
     </div>
-
+  <div class="crm-section">
+    <div class="label">{$form.enable_hard_limit.label}</div>
+    <div class="content">{$form.enable_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section" id="section_default_hard_limit">
+    <div class="label">{$form.default_hard_limit.label}</div>
+    <div class="content">{$form.default_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
 {/crmScope}
+{literal}
+<script type="text/javascript">
+  cj("#enable_hard_limit").click(function() {
+    if (this.checked) {
+      cj('#section_default_hard_limit').show();
+    } else {
+      cj('#section_default_hard_limit').hide();
+    }
+  }).click();
+</script>
+{/literal}
diff --git a/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl b/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl
index 7c8776a3..d7c9ab6e 100644
--- a/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl
+++ b/templates/CRM/Dataprocessor/Form/Output/UIOutput/CriteriaForm.tpl
@@ -1,5 +1,5 @@
 {crmScope extensionKey='dataprocessor'}
-{if $has_exposed_filters}
+{if $has_exposed_filters || $hard_limit_enabled}
   <div class="crm-form-block crm-search-form-block">
     <div id="searchForm" class="form-item">
       <div class="crm-accordion-wrapper crm-advanced_search_form-accordion {if (!empty($dataprocessor_rows))}collapsed{/if}">
@@ -8,6 +8,7 @@
           </div>
           <!-- /.crm-accordion-header -->
           <div class="crm-accordion-body">
+              {if $has_exposed_filters}
               {foreach from=$filters item=filterCollection}
                   {if $filterCollection.title}
                     <div class="crm-accordion-wrapper {$filterCollection.class}">
@@ -38,6 +39,20 @@
                   </div>
                   {/if}
               {/foreach}
+              {/if}
+              {if $hard_limit_enabled}
+                <div class="crm-accordion-wrapper">
+                  <div class="crm-accordion-header">{ts}Limit results to a maximum number of records.{/ts}</div>
+                  <div class="crm-accordion-body">
+                    <table>
+                      <tr>
+                        <td class="label">{$form.hard_limit.label}</td>
+                        <td>{$form.hard_limit.html}</td>
+                      </tr>
+                    </table>
+                  </div>
+                </div>
+              {/if}
                   <div class="crm-submit-buttons">{include file="CRM/common/formButtons.tpl" location="botton"}</div>
               </div>
           </div>
diff --git a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ActivitySearch.tpl b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ActivitySearch.tpl
index 75a77ac7..48889342 100644
--- a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ActivitySearch.tpl
+++ b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ActivitySearch.tpl
@@ -49,4 +49,25 @@
       <div class="content">{$form.no_result_text.html}</div>
       <div class="clear"></div>
     </div>
+  <div class="crm-section">
+    <div class="label">{$form.enable_hard_limit.label}</div>
+    <div class="content">{$form.enable_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section" id="section_default_hard_limit">
+    <div class="label">{$form.default_hard_limit.label}</div>
+    <div class="content">{$form.default_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
 {/crmScope}
+{literal}
+  <script type="text/javascript">
+    cj("#enable_hard_limit").click(function() {
+      if (this.checked) {
+        cj('#section_default_hard_limit').show();
+      } else {
+        cj('#section_default_hard_limit').hide();
+      }
+    }).click();
+  </script>
+{/literal}
diff --git a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/CaseSearch.tpl b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/CaseSearch.tpl
index d97b2625..54d1df4c 100644
--- a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/CaseSearch.tpl
+++ b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/CaseSearch.tpl
@@ -59,4 +59,25 @@
       <div class="content">{$form.no_result_text.html}</div>
       <div class="clear"></div>
     </div>
+  <div class="crm-section">
+    <div class="label">{$form.enable_hard_limit.label}</div>
+    <div class="content">{$form.enable_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section" id="section_default_hard_limit">
+    <div class="label">{$form.default_hard_limit.label}</div>
+    <div class="content">{$form.default_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
 {/crmScope}
+{literal}
+  <script type="text/javascript">
+    cj("#enable_hard_limit").click(function() {
+      if (this.checked) {
+        cj('#section_default_hard_limit').show();
+      } else {
+        cj('#section_default_hard_limit').hide();
+      }
+    }).click();
+  </script>
+{/literal}
diff --git a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ContributionSearch.tpl b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ContributionSearch.tpl
index 400045fb..0fbc2323 100644
--- a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ContributionSearch.tpl
+++ b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ContributionSearch.tpl
@@ -49,4 +49,25 @@
       <div class="content">{$form.no_result_text.html}</div>
       <div class="clear"></div>
     </div>
+  <div class="crm-section">
+    <div class="label">{$form.enable_hard_limit.label}</div>
+    <div class="content">{$form.enable_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section" id="section_default_hard_limit">
+    <div class="label">{$form.default_hard_limit.label}</div>
+    <div class="content">{$form.default_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
 {/crmScope}
+{literal}
+  <script type="text/javascript">
+    cj("#enable_hard_limit").click(function() {
+      if (this.checked) {
+        cj('#section_default_hard_limit').show();
+      } else {
+        cj('#section_default_hard_limit').hide();
+      }
+    }).click();
+  </script>
+{/literal}
diff --git a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/MembershipSearch.tpl b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/MembershipSearch.tpl
index c3603719..f3122a3f 100644
--- a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/MembershipSearch.tpl
+++ b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/MembershipSearch.tpl
@@ -49,4 +49,25 @@
       <div class="content">{$form.no_result_text.html}</div>
       <div class="clear"></div>
     </div>
+  <div class="crm-section">
+    <div class="label">{$form.enable_hard_limit.label}</div>
+    <div class="content">{$form.enable_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section" id="section_default_hard_limit">
+    <div class="label">{$form.default_hard_limit.label}</div>
+    <div class="content">{$form.default_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
 {/crmScope}
+{literal}
+  <script type="text/javascript">
+    cj("#enable_hard_limit").click(function() {
+      if (this.checked) {
+        cj('#section_default_hard_limit').show();
+      } else {
+        cj('#section_default_hard_limit').hide();
+      }
+    }).click();
+  </script>
+{/literal}
diff --git a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ParticipantSearch.tpl b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ParticipantSearch.tpl
index b6b741c2..cdc792c2 100644
--- a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ParticipantSearch.tpl
+++ b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/ParticipantSearch.tpl
@@ -49,4 +49,25 @@
       <div class="content">{$form.no_result_text.html}</div>
       <div class="clear"></div>
     </div>
+  <div class="crm-section">
+    <div class="label">{$form.enable_hard_limit.label}</div>
+    <div class="content">{$form.enable_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section" id="section_default_hard_limit">
+    <div class="label">{$form.default_hard_limit.label}</div>
+    <div class="content">{$form.default_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
 {/crmScope}
+{literal}
+  <script type="text/javascript">
+    cj("#enable_hard_limit").click(function() {
+      if (this.checked) {
+        cj('#section_default_hard_limit').show();
+      } else {
+        cj('#section_default_hard_limit').hide();
+      }
+    }).click();
+  </script>
+{/literal}
diff --git a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/Search.tpl b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/Search.tpl
index 519e7f27..653944b5 100644
--- a/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/Search.tpl
+++ b/templates/CRM/DataprocessorSearch/Form/OutputConfiguration/Search.tpl
@@ -59,4 +59,25 @@
     <div class="content">{$form.no_result_text.html}</div>
     <div class="clear"></div>
   </div>
+  <div class="crm-section">
+    <div class="label">{$form.enable_hard_limit.label}</div>
+    <div class="content">{$form.enable_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
+  <div class="crm-section" id="section_default_hard_limit">
+    <div class="label">{$form.default_hard_limit.label}</div>
+    <div class="content">{$form.default_hard_limit.html}</div>
+    <div class="clear"></div>
+  </div>
 {/crmScope}
+{literal}
+  <script type="text/javascript">
+    cj("#enable_hard_limit").click(function() {
+      if (this.checked) {
+        cj('#section_default_hard_limit').show();
+      } else {
+        cj('#section_default_hard_limit').hide();
+      }
+    }).click();
+  </script>
+{/literal}
-- 
GitLab