Skip to content

Commit

Permalink
APIv4 - Add SortableEntity and ManagedEntity traits to Navigation men…
Browse files Browse the repository at this point in the history
…u entity

Excludes 'weight' from managed entity calculations for references,
adds unit tests for the interaction of managed entities and sortable entities
  • Loading branch information
colemanw committed Nov 28, 2021
1 parent f74fbb4 commit 02ea6e1
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 22 deletions.
2 changes: 1 addition & 1 deletion CRM/Core/Module.php
Expand Up @@ -48,7 +48,7 @@ public function __construct($name, $is_active) {
* @param bool $fresh
* Force new results?
*
* @return array
* @return CRM_Core_Module[]
*/
public static function getAll($fresh = FALSE) {
static $result;
Expand Down
13 changes: 9 additions & 4 deletions CRM/Utils/Weight.php
Expand Up @@ -305,10 +305,15 @@ public static function &query(
// invalid field specified. abort.
throw new CRM_Core_Exception("Invalid field '$fieldName' for $daoName");
}
$fieldNum++;
$whereConditions[] = "$fieldName = %$fieldNum";
$fieldType = $fields[$fieldName]['type'];
$params[$fieldNum] = [$value, CRM_Utils_Type::typeToString($fieldType)];
if (CRM_Utils_System::isNull($value)) {
$whereConditions[] = "$fieldName IS NULL";
}
else {
$fieldNum++;
$whereConditions[] = "$fieldName = %$fieldNum";
$fieldType = $fields[$fieldName]['type'];
$params[$fieldNum] = [$value, CRM_Utils_Type::typeToString($fieldType)];
}
}
}
$where = implode(' AND ', $whereConditions);
Expand Down
48 changes: 38 additions & 10 deletions Civi/Api4/Generic/ExportAction.php
Expand Up @@ -69,14 +69,15 @@ public function _run(Result $result) {
* @param int $entityId
* @param \Civi\Api4\Generic\Result $result
* @param string $parentName
* @param array $excludeFields
*/
private function exportRecord(string $entityType, int $entityId, Result $result, $parentName = NULL) {
private function exportRecord(string $entityType, int $entityId, Result $result, $parentName = NULL, $excludeFields = []) {
if (isset($this->exportedEntities[$entityType][$entityId])) {
throw new \API_Exception("Circular reference detected: attempted to export $entityType id $entityId multiple times.");
}
$this->exportedEntities[$entityType][$entityId] = TRUE;
$select = $pseudofields = [];
$allFields = $this->getFieldsForExport($entityType, TRUE);
$allFields = $this->getFieldsForExport($entityType, TRUE, $excludeFields);
foreach ($allFields as $field) {
// Use implicit join syntax but only if the fk entity has a `name` field
if (!empty($field['fk_entity']) && array_key_exists('name', $this->getFieldsForExport($field['fk_entity']))) {
Expand Down Expand Up @@ -135,12 +136,34 @@ private function exportRecord(string $entityType, int $entityId, Result $result,
/** @var \CRM_Core_DAO $dao */
$dao = new $daoName();
$dao->id = $entityId;
// Collect references into arrays keyed by entity type
$references = [];
foreach ($dao->findReferences() as $reference) {
$refEntity = $reference::fields()['id']['entity'] ?? '';
$refEntity = \CRM_Utils_Array::first($reference::fields())['entity'] ?? '';
$references[$refEntity][] = $reference;
}
foreach ($references as $refEntity => $records) {
$refApiType = CoreUtil::getInfoItem($refEntity, 'type') ?? [];
// Reference must be a ManagedEntity
if (in_array('ManagedEntity', $refApiType, TRUE)) {
$this->exportRecord($refEntity, $reference->id, $result, $name . '_');
if (!in_array('ManagedEntity', $refApiType, TRUE)) {
continue;
}
$exclude = [];
// For sortable entities, order by weight and exclude weight from the export (it will be auto-managed)
if (in_array('SortableEntity', $refApiType, TRUE)) {
$exclude[] = $weightCol = CoreUtil::getInfoItem($refEntity, 'order_by');
usort($records, function($a, $b) use ($weightCol) {
if (!isset($a->$weightCol)) {
$a->find(TRUE);
}
if (!isset($b->$weightCol)) {
$b->find(TRUE);
}
return $a->$weightCol < $b->$weightCol ? -1 : 1;
});
}
foreach ($records as $record) {
$this->exportRecord($refEntity, $record->id, $result, $name . '_', $exclude);
}
}
}
Expand Down Expand Up @@ -170,16 +193,21 @@ private function shouldUsePseudoconstant(array $field) {
/**
* @param $entityType
* @param bool $loadOptions
* @param array $excludeFields
* @return array
*/
private function getFieldsForExport($entityType, $loadOptions = FALSE): array {
private function getFieldsForExport($entityType, $loadOptions = FALSE, $excludeFields = []): array {
$conditions = [
['type', 'IN', ['Field', 'Custom']],
['readonly', '!=', TRUE],
];
if ($excludeFields) {
$conditions[] = ['name', 'NOT IN', $excludeFields];
}
try {
return (array) civicrm_api4($entityType, 'getFields', [
'action' => 'create',
'where' => [
['type', 'IN', ['Field', 'Custom']],
['readonly', '!=', TRUE],
],
'where' => $conditions,
'loadOptions' => $loadOptions,
'checkPermissions' => $this->checkPermissions,
])->indexBy('name');
Expand Down
9 changes: 3 additions & 6 deletions Civi/Api4/Generic/Traits/DAOActionTrait.php
Expand Up @@ -115,6 +115,7 @@ protected function writeObjects(&$items) {
'CustomField' => 'writeRecords',
'EntityTag' => 'add',
'GroupContact' => 'add',
'Navigation' => 'writeRecords',
];
$method = $functionNames[$this->getEntityName()] ?? NULL;
if (!isset($method)) {
Expand Down Expand Up @@ -324,14 +325,10 @@ protected function updateWeight(array &$record) {
$oldWeight = empty($record[$idField]) ? NULL : \CRM_Core_DAO::getFieldValue($daoName, $record[$idField], $weightField);

// FIXME: Need a more metadata-ish approach. For now here's a hardcoded list of the fields sortable entities use for grouping.
$guesses = ['option_group_id', 'price_set_id', 'price_field_id', 'premiums_id', 'uf_group_id', 'custom_group_id', 'domain_id'];
$guesses = ['option_group_id', 'price_set_id', 'price_field_id', 'premiums_id', 'uf_group_id', 'custom_group_id', 'parent_id', 'domain_id'];
$filters = [];
foreach (array_intersect($guesses, array_keys($daoFields)) as $filter) {
$value = $record[$filter] ?? (empty($record[$idField]) ? NULL : \CRM_Core_DAO::getFieldValue($daoName, $record[$idField], $filter));
// Ignore the db-formatted string 'null' and empty strings as well as NULL values
if (!\CRM_Utils_System::isNull($value)) {
$filters[$filter] = $value;
}
$filters[$filter] = $record[$filter] ?? (empty($record[$idField]) ? NULL : \CRM_Core_DAO::getFieldValue($daoName, $record[$idField], $filter));
}
// Supply default weight for new record
if (!isset($record[$weightField]) && empty($record[$idField])) {
Expand Down
5 changes: 4 additions & 1 deletion Civi/Api4/Navigation.php
Expand Up @@ -11,12 +11,15 @@
namespace Civi\Api4;

/**
* Navigation entity.
* Navigation menu items.
*
* @searchable none
* @orderBy weight
* @since 5.19
* @package Civi\Api4
*/
class Navigation extends Generic\DAOEntity {
use Generic\Traits\SortableEntity;
use Generic\Traits\ManagedEntity;

}
169 changes: 169 additions & 0 deletions tests/phpunit/api/v4/Entity/ManagedEntityTest.php
Expand Up @@ -19,6 +19,7 @@
namespace api\v4\Entity;

use api\v4\UnitTestCase;
use Civi\Api4\Navigation;
use Civi\Api4\OptionGroup;
use Civi\Api4\OptionValue;
use Civi\Api4\SavedSearch;
Expand Down Expand Up @@ -363,6 +364,173 @@ public function testExportOptionGroupWithDomain() {
$this->assertStringStartsWith('OptionGroup_from_email_address_OptionValue_', $result['export'][1]['name']);
}

public function testManagedNavigationWeights() {
$this->_managedEntities = [
[
'module' => 'unit.test.fake.ext',
'name' => 'Navigation_Test_Parent',
'entity' => 'Navigation',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'label' => 'Test Parent',
'name' => 'Test_Parent',
'url' => NULL,
'icon' => 'crm-i test',
'permission' => 'access CiviCRM',
'permission_operator' => '',
'is_active' => TRUE,
'weight' => 50,
'parent_id' => NULL,
'has_separator' => NULL,
'domain_id' => 'current_domain',
],
],
],
[
'module' => 'unit.test.fake.ext',
'name' => 'Navigation_Test_Child_1',
'entity' => 'Navigation',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'label' => 'Test Child 1',
'name' => 'Test_Child_1',
'url' => 'civicrm/test1?reset=1',
'icon' => NULL,
'permission' => 'access CiviCRM',
'permission_operator' => '',
'parent_id.name' => 'Test_Parent',
'is_active' => TRUE,
'has_separator' => NULL,
'domain_id' => 'current_domain',
],
],
],
[
'module' => 'unit.test.fake.ext',
'name' => 'Navigation_Test_Child_2',
'entity' => 'Navigation',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'label' => 'Test Child 2',
'name' => 'Test_Child_2',
'url' => 'civicrm/test2?reset=1',
'icon' => NULL,
'permission' => 'access CiviCRM',
'permission_operator' => '',
'parent_id.name' => 'Test_Parent',
'is_active' => TRUE,
'has_separator' => NULL,
'domain_id' => 'current_domain',
],
],
],
[
'module' => 'unit.test.fake.ext',
'name' => 'Navigation_Test_Child_3',
'entity' => 'Navigation',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'label' => 'Test Child 3',
'name' => 'Test_Child_3',
'url' => 'civicrm/test3?reset=1',
'icon' => NULL,
'permission' => 'access CiviCRM',
'permission_operator' => '',
'parent_id.name' => 'Test_Parent',
'is_active' => TRUE,
'has_separator' => NULL,
'domain_id' => 'current_domain',
],
],
],
];

// Refresh managed entities with module active
$allModules = [
new \CRM_Core_Module('unit.test.fake.ext', TRUE),
];
(new \CRM_Core_ManagedEntities($allModules))->reconcile();

$nav = Navigation::get(FALSE)
->addWhere('name', '=', 'Test_Parent')
->addChain('export', Navigation::export()->setId('$id'))
->execute()->first();

$this->assertCount(4, $nav['export']);
$this->assertEquals(TRUE, $nav['is_active']);

$this->assertEquals(50, $nav['export'][0]['params']['values']['weight']);
$this->assertEquals('Navigation_Test_Parent_Navigation_Test_Child_1', $nav['export'][1]['name']);
$this->assertEquals('Navigation_Test_Parent_Navigation_Test_Child_2', $nav['export'][2]['name']);
$this->assertEquals('Navigation_Test_Parent_Navigation_Test_Child_3', $nav['export'][3]['name']);
// Weight should not be included in export of children, leaving it to be auto-managed
$this->assertArrayNotHasKey('weight', $nav['export'][1]['params']['values']);

// Children should have been assigned correct auto-weights
$children = Navigation::get(FALSE)
->addWhere('parent_id.name', '=', 'Test_Parent')
->addOrderBy('weight')
->execute();
foreach ([1, 2, 3] as $index => $weight) {
$this->assertEquals($weight, $children[$index]['weight']);
$this->assertEquals(TRUE, $children[$index]['is_active']);
}

// Refresh managed entities with module disabled
$allModules = [
new \CRM_Core_Module('unit.test.fake.ext', FALSE),
];
(new \CRM_Core_ManagedEntities($allModules))->reconcile();

// Children's weight should have been unaffected, but they should be disabled
$children = Navigation::get(FALSE)
->addWhere('parent_id.name', '=', 'Test_Parent')
->addOrderBy('weight')
->execute();
foreach ([1, 2, 3] as $index => $weight) {
$this->assertEquals($weight, $children[$index]['weight']);
$this->assertEquals(FALSE, $children[$index]['is_active']);
}

$nav = Navigation::get(FALSE)
->addWhere('name', '=', 'Test_Parent')
->execute()->first();
$this->assertEquals(FALSE, $nav['is_active']);

// Refresh managed entities with module active
$allModules = [
new \CRM_Core_Module('unit.test.fake.ext', TRUE),
];
(new \CRM_Core_ManagedEntities($allModules))->reconcile();

// Children's weight should have been unaffected, but they should be enabled
$children = Navigation::get(FALSE)
->addWhere('parent_id.name', '=', 'Test_Parent')
->addOrderBy('weight')
->execute();
foreach ([1, 2, 3] as $index => $weight) {
$this->assertEquals($weight, $children[$index]['weight']);
$this->assertEquals(TRUE, $children[$index]['is_active']);
}
// Parent should also be re-enabled
$nav = Navigation::get(FALSE)
->addWhere('name', '=', 'Test_Parent')
->execute()->first();
$this->assertEquals(TRUE, $nav['is_active']);
}

/**
* @dataProvider sampleEntityTypes
* @param string $entityName
Expand All @@ -389,6 +557,7 @@ public function sampleEntityTypes() {
'CustomField' => TRUE,
'CustomGroup' => TRUE,
'MembershipType' => TRUE,
'Navigation' => TRUE,
'OptionGroup' => TRUE,
'OptionValue' => TRUE,
'SavedSearch' => TRUE,
Expand Down

0 comments on commit 02ea6e1

Please sign in to comment.