Skip to content

Commit

Permalink
SearchKit - Add select options for MONTH date filter
Browse files Browse the repository at this point in the history
  • Loading branch information
colemanw committed Mar 7, 2023
1 parent 716a8dc commit 8dbb6d3
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 61 deletions.
27 changes: 1 addition & 26 deletions CRM/Api4/Page/Api4Explorer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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), '/'),
];
Expand All @@ -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;
}

}
32 changes: 1 addition & 31 deletions Civi/Api4/Generic/BasicGetFieldsAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
22 changes: 21 additions & 1 deletion Civi/Api4/Query/SqlFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
*
Expand Down
2 changes: 1 addition & 1 deletion Civi/Api4/Query/SqlFunctionEXTRACT.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.)');
}

}
20 changes: 20 additions & 0 deletions Civi/Api4/Query/SqlFunctionMONTH.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
];
}

}
67 changes: 67 additions & 0 deletions Civi/Api4/Utils/CoreUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
3 changes: 2 additions & 1 deletion ext/search_kit/Civi/Search/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -458,14 +458,15 @@ 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',
'title' => ts('Arithmetic'),
'description' => ts('Add, subtract, multiply, divide'),
'category' => SqlFunction::CATEGORY_MATH,
'data_type' => 'Number',
'options' => FALSE,
'params' => [
[
'label' => ts('Value'),
Expand Down
6 changes: 6 additions & 0 deletions ext/search_kit/Civi/Search/Meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
];
}
}
Expand Down
6 changes: 5 additions & 1 deletion tests/phpunit/api/v4/Action/SqlFunctionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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() {
Expand Down

0 comments on commit 8dbb6d3

Please sign in to comment.