From 8dbb6d39b23696683b3922867785f4562177fa51 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Mon, 6 Mar 2023 13:43:31 -0500 Subject: [PATCH] SearchKit - Add select options for MONTH date filter --- CRM/Api4/Page/Api4Explorer.php | 27 +------- Civi/Api4/Generic/BasicGetFieldsAction.php | 32 +-------- Civi/Api4/Query/SqlFunction.php | 22 +++++- Civi/Api4/Query/SqlFunctionEXTRACT.php | 2 +- Civi/Api4/Query/SqlFunctionMONTH.php | 20 ++++++ Civi/Api4/Utils/CoreUtil.php | 67 +++++++++++++++++++ ext/search_kit/Civi/Search/Admin.php | 3 +- ext/search_kit/Civi/Search/Meta.php | 6 ++ .../phpunit/api/v4/Action/SqlFunctionTest.php | 6 +- 9 files changed, 124 insertions(+), 61 deletions(-) diff --git a/CRM/Api4/Page/Api4Explorer.php b/CRM/Api4/Page/Api4Explorer.php index 167db8777a4b..0b03bc972580 100644 --- a/CRM/Api4/Page/Api4Explorer.php +++ b/CRM/Api4/Page/Api4Explorer.php @@ -28,7 +28,7 @@ public function run() { 'basePath' => Civi::resources()->getUrl('civicrm'), 'schema' => (array) \Civi\Api4\Entity::get()->setChain(['fields' => ['$name', 'getFields']])->execute(), 'docs' => \Civi\Api4\Utils\ReflectionUtils::parseDocBlock($apiDoc->getDocComment()), - 'functions' => self::getSqlFunctions(), + 'functions' => CoreUtil::getSqlFunctions(), 'authxEnabled' => $extensions->isActiveModule('authx'), 'restUrl' => rtrim(CRM_Utils_System::url('civicrm/ajax/api4/CRMAPI4ENTITY/CRMAPI4ACTION', NULL, TRUE, NULL, FALSE, TRUE), '/'), ]; @@ -46,29 +46,4 @@ public function run() { parent::run(); } - /** - * Gets info about all available sql functions - * @return array - */ - public static function getSqlFunctions() { - $fns = []; - foreach (glob(Civi::paths()->getPath('[civicrm.root]/Civi/Api4/Query/SqlFunction*.php')) as $file) { - $matches = []; - if (preg_match('/(SqlFunction[A-Z_]+)\.php$/', $file, $matches)) { - $className = '\Civi\Api4\Query\\' . $matches[1]; - if (is_subclass_of($className, '\Civi\Api4\Query\SqlFunction')) { - $fns[] = [ - 'name' => $className::getName(), - 'title' => $className::getTitle(), - 'description' => $className::getDescription(), - 'params' => $className::getParams(), - 'category' => $className::getCategory(), - 'dataType' => $className::getDataType(), - ]; - } - } - } - return $fns; - } - } diff --git a/Civi/Api4/Generic/BasicGetFieldsAction.php b/Civi/Api4/Generic/BasicGetFieldsAction.php index d114fad93f2c..0b10f4f6a163 100644 --- a/Civi/Api4/Generic/BasicGetFieldsAction.php +++ b/Civi/Api4/Generic/BasicGetFieldsAction.php @@ -175,37 +175,7 @@ private function formatOptionList(&$field) { throw new \CRM_Core_Exception('Unsupported pseudoconstant type for field "' . $field['name'] . '"'); } } - if (!$field['options'] || !is_array($field['options'])) { - return; - } - - $formatted = []; - $first = reset($field['options']); - // Flat array requested - if ($this->loadOptions === TRUE) { - // Convert non-associative to flat array - if (is_array($first) && isset($first['id'])) { - foreach ($field['options'] as $option) { - $formatted[$option['id']] = $option['label'] ?? $option['name'] ?? $option['id']; - } - $field['options'] = $formatted; - } - } - // Non-associative array of multiple properties requested - else { - foreach ($field['options'] as $id => $option) { - // Transform a flat list - if (!is_array($option)) { - $option = [ - 'id' => $id, - 'name' => $id, - 'label' => $option, - ]; - } - $formatted[] = array_intersect_key($option, array_flip($this->loadOptions)); - } - $field['options'] = $formatted; - } + $field['options'] = CoreUtil::formatOptionList($field['options'], $this->loadOptions); } /** diff --git a/Civi/Api4/Query/SqlFunction.php b/Civi/Api4/Query/SqlFunction.php index 6b094eff3dc2..7b00b6f2c95c 100644 --- a/Civi/Api4/Query/SqlFunction.php +++ b/Civi/Api4/Query/SqlFunction.php @@ -12,7 +12,14 @@ namespace Civi\Api4\Query; /** - * Base class for all Sql functions. + * Base class for all APIv4 Sql function definitions. + * + * SqlFunction classes don't actually process data, SQL itself does the real work. + * The role of each SqlFunction class is to: + * + * 1. Whitelist the SQL function for use by APIv4 (it doesn't allow any that don't have a SQLFunction class). + * 2. Document what the function does and what arguments it accepts. + * 3. Tell APIv4 how to treat the inputs and how to format the outputs. * * @package Civi\Api4\Query */ @@ -199,6 +206,19 @@ public static function getCategory(): string { return static::$category; } + /** + * For functions which output a finite set of values, + * this allows the API to treat it as pseudoconstant options. + * + * e.g. MONTH() only returns integers 1-12, which can be formatted like + * [1 => January, 2 => February, etc.] + * + * @return array|null + */ + public static function getOptions(): ?array { + return NULL; + } + /** * All functions return 'SqlFunction' as their type. * diff --git a/Civi/Api4/Query/SqlFunctionEXTRACT.php b/Civi/Api4/Query/SqlFunctionEXTRACT.php index ff555ec46bf3..fb8611744020 100644 --- a/Civi/Api4/Query/SqlFunctionEXTRACT.php +++ b/Civi/Api4/Query/SqlFunctionEXTRACT.php @@ -60,7 +60,7 @@ public static function getTitle(): string { * @return string */ public static function getDescription(): string { - return ts('The numeric month (1-12) of a date.'); + return ts('Extract part(s) of a date (e.g. the day, year, etc.)'); } } diff --git a/Civi/Api4/Query/SqlFunctionMONTH.php b/Civi/Api4/Query/SqlFunctionMONTH.php index 4f903347a6e9..e99678e1ede0 100644 --- a/Civi/Api4/Query/SqlFunctionMONTH.php +++ b/Civi/Api4/Query/SqlFunctionMONTH.php @@ -43,4 +43,24 @@ public static function getDescription(): string { return ts('The numeric month (1-12) of a date.'); } + /** + * @return array + */ + public static function getOptions(): ?array { + return [ + 1 => ts('January'), + 2 => ts('February'), + 3 => ts('March'), + 4 => ts('April'), + 5 => ts('May'), + 6 => ts('June'), + 7 => ts('July'), + 8 => ts('August'), + 9 => ts('September'), + 10 => ts('October'), + 11 => ts('November'), + 12 => ts('December'), + ]; + } + } diff --git a/Civi/Api4/Utils/CoreUtil.php b/Civi/Api4/Utils/CoreUtil.php index a50543de89fd..965e330ec237 100644 --- a/Civi/Api4/Utils/CoreUtil.php +++ b/Civi/Api4/Utils/CoreUtil.php @@ -305,4 +305,71 @@ public static function getOptionValueFields($optionGroup, $key = 'name'): array return explode(',', $fields); } + /** + * Transforms a raw option list (which could be either a flat or non-associative array) + * into an APIv4-compatible format. + * + * @param array|bool $options + * @param array|bool $format + * @return array|bool + */ + public static function formatOptionList($options, $format) { + if (!$options || !is_array($options)) { + return $options ?? FALSE; + } + + $formatted = []; + $first = reset($options); + // Flat array requested + if ($format === TRUE) { + // Convert non-associative to flat array + if (is_array($first) && isset($first['id'])) { + foreach ($options as $option) { + $formatted[$option['id']] = $option['label'] ?? $option['name'] ?? $option['id']; + } + return $formatted; + } + return $options; + } + // Non-associative array of multiple properties requested + foreach ($options as $id => $option) { + // Transform a flat list + if (!is_array($option)) { + $option = [ + 'id' => $id, + 'name' => $id, + 'label' => $option, + ]; + } + $formatted[] = array_intersect_key($option, array_flip($format)); + } + return $formatted; + } + + /** + * Gets info about all available sql functions + * @return array + */ + public static function getSqlFunctions(): array { + $fns = []; + foreach (glob(\Civi::paths()->getPath('[civicrm.root]/Civi/Api4/Query/SqlFunction*.php')) as $file) { + $matches = []; + if (preg_match('/(SqlFunction[A-Z_]+)\.php$/', $file, $matches)) { + $className = '\Civi\Api4\Query\\' . $matches[1]; + if (is_subclass_of($className, '\Civi\Api4\Query\SqlFunction')) { + $fns[] = [ + 'name' => $className::getName(), + 'title' => $className::getTitle(), + 'description' => $className::getDescription(), + 'params' => $className::getParams(), + 'category' => $className::getCategory(), + 'dataType' => $className::getDataType(), + 'options' => CoreUtil::formatOptionList($className::getOptions(), ['id', 'name', 'label']), + ]; + } + } + } + return $fns; + } + } diff --git a/ext/search_kit/Civi/Search/Admin.php b/ext/search_kit/Civi/Search/Admin.php index ee85568afd35..15263f1d018c 100644 --- a/ext/search_kit/Civi/Search/Admin.php +++ b/ext/search_kit/Civi/Search/Admin.php @@ -458,7 +458,7 @@ private static function getJoinDefaults(string $alias, ...$entities):array { * @return array */ private static function getSqlFunctions():array { - $functions = \CRM_Api4_Page_Api4Explorer::getSqlFunctions(); + $functions = CoreUtil::getSqlFunctions(); // Add faux function "e" for SqlEquations $functions[] = [ 'name' => 'e', @@ -466,6 +466,7 @@ private static function getSqlFunctions():array { 'description' => ts('Add, subtract, multiply, divide'), 'category' => SqlFunction::CATEGORY_MATH, 'data_type' => 'Number', + 'options' => FALSE, 'params' => [ [ 'label' => ts('Value'), diff --git a/ext/search_kit/Civi/Search/Meta.php b/ext/search_kit/Civi/Search/Meta.php index 826fb2348937..40251f5010db 100644 --- a/ext/search_kit/Civi/Search/Meta.php +++ b/ext/search_kit/Civi/Search/Meta.php @@ -70,12 +70,18 @@ public static function getCalcFields($apiEntity, $apiParams): array { $dataType = $field['data_type'] ?? 'String'; $inputType = $field['input_type'] ?? $dataTypeToInputType[$dataType] ?? 'Text'; } + $options = FALSE; + if ($expr->getType() === 'SqlFunction' && $expr::getOptions()) { + $inputType = 'Select'; + $options = CoreUtil::formatOptionList($expr::getOptions(), ['id', 'label']); + } $calcFields[] = [ 'name' => $expr->getAlias(), 'label' => $label, 'input_type' => $inputType, 'data_type' => $dataType, + 'options' => $options, ]; } } diff --git a/tests/phpunit/api/v4/Action/SqlFunctionTest.php b/tests/phpunit/api/v4/Action/SqlFunctionTest.php index 1cbb7384ff7d..3304c92639f5 100644 --- a/tests/phpunit/api/v4/Action/SqlFunctionTest.php +++ b/tests/phpunit/api/v4/Action/SqlFunctionTest.php @@ -23,6 +23,7 @@ use Civi\Api4\Activity; use Civi\Api4\Contact; use Civi\Api4\Contribution; +use Civi\Api4\Utils\CoreUtil; use Civi\Test\TransactionalInterface; /** @@ -31,12 +32,15 @@ class SqlFunctionTest extends Api4TestBase implements TransactionalInterface { public function testGetFunctions() { - $functions = array_column(\CRM_Api4_Page_Api4Explorer::getSqlFunctions(), NULL, 'name'); + $functions = array_column(CoreUtil::getSqlFunctions(), NULL, 'name'); $this->assertArrayHasKey('SUM', $functions); $this->assertArrayNotHasKey('', $functions); $this->assertArrayNotHasKey('SqlFunction', $functions); $this->assertEquals(1, $functions['MAX']['params'][0]['min_expr']); $this->assertEquals(1, $functions['MAX']['params'][0]['max_expr']); + $this->assertFalse($functions['YEAR']['options']); + $this->assertEquals(1, $functions['MONTH']['options'][0]['id']); + $this->assertEquals(12, $functions['MONTH']['options'][11]['id']); } public function testGroupAggregates() {