From 3c6063165619818be74b121a58dabe50f23deda6 Mon Sep 17 00:00:00 2001 From: berliner Date: Thu, 6 Mar 2025 17:10:47 +0100 Subject: [PATCH] HPC-9947: Add new plan element for plan caseload trend charts --- README.md | 4 +- .../BaseObjectFocusCountryInterface.php | 38 ++ .../src/Entity/BaseObjectInterface.php | 2 +- .../Entity/BaseObjectMetaDataInterface.php | 2 +- .../src/Functional/BaseObjectFormTest.php | 1 + .../tests/src/Kernel/BaseObjectTest.php | 49 +- .../tests/src/Traits/BaseObjectTestTrait.php | 34 +- .../src/Plugin/Block/GHIBlockBase.php | 2 +- .../src/Plugin/Block/GlobalPage/PlanTable.php | 86 +-- .../src/Plugin/Block/Menu/SectionSwitcher.php | 156 +---- .../Block/Plan/PlanCaseloadTrendsTable.php | 286 ++++++++ .../Block/Plan/PlanClusterLogframeLinks.php | 2 +- .../Block/Plan/PlanGoverningEntitiesTable.php | 8 +- .../ConfigurationItemClusterRestrictTrait.php | 4 +- .../ghi_blocks/src/Traits/TableTrait.php | 31 + .../tests/src/Kernel/BlockKernelTestBase.php | 8 +- .../{ => Global}/DatawrapperBlockTest.php | 3 +- .../{ => Global}/DocumentLinksBlockTest.php | 3 +- .../{ => Global}/ExternalWidgetBlockTest.php | 3 +- .../{ => Global}/FeaturedOperationsTest.php | 3 +- .../src/Kernel/{ => Global}/LinkBlockTest.php | 3 +- .../{ => Global}/LinkCarouselBlockTest.php | 3 +- .../{ => Global}/SectionCollectionTest.php | 3 +- .../Plan/PlanCaseloadTrendsTableTest.php | 177 +++++ .../Plan/PlanClusterLogframeLinksTest.php | 122 ++++ .../Plan/PlanGoverningEntitiesTableTest.php | 240 +++++++ .../Kernel/Plan/PlanHeadlineFiguresTest.php | 138 ++++ .../src/Kernel/PlanBlockKernelTestBase.php | 150 ++++ .../src/Kernel/PlanClusterNodeBundleTest.php | 2 + .../tests/src/Traits/PlanClusterTestTrait.php | 6 +- .../Attachments/CaseloadAttachment.php | 2 +- .../CaseloadAttachmentInterface.php | 8 + .../ApiObjects/Attachments/DataAttachment.php | 39 +- .../Attachments/DataAttachmentInterface.php | 34 + .../Partials/PlanOverviewCaseload.php | 75 ++ .../ApiObjects/Partials/PlanOverviewPlan.php | 52 +- .../custom/ghi_plans/src/Entity/Plan.php | 33 +- .../src/Traits/AttachmentFilterTrait.php | 57 ++ .../custom/ghi_plans/translations/es.po | 23 +- .../custom/ghi_plans/translations/fr.po | 23 +- .../ghi_sections/src/SectionManager.php | 165 +++++ .../tests/src/Functional/WizardTest.php | 2 +- .../tests/src/Traits/SectionTestTrait.php | 8 +- .../PageTemplateUiTest.php | 2 +- .../hpc_api/src/ApiObjects/ApiObjectBase.php | 2 +- .../hpc_api/src/Helpers/ArrayHelper.php | 125 ++-- .../hpc_api/src/Query/EndpointQueryBase.php | 2 +- .../tests/src/Unit/ArrayHelperTest.php | 460 +++++++++++++ .../hpc_common/src/Plugin/HPCBlockBase.php | 26 +- .../tests/src/Unit/ArrayHelperTest.php | 645 ++---------------- .../tests/src/Unit/ThemeHelperTest.php | 158 ++++- 51 files changed, 2507 insertions(+), 1003 deletions(-) create mode 100644 html/modules/custom/ghi_base_objects/src/Entity/BaseObjectFocusCountryInterface.php create mode 100644 html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanCaseloadTrendsTable.php create mode 100644 html/modules/custom/ghi_blocks/src/Traits/TableTrait.php rename html/modules/custom/ghi_blocks/tests/src/Kernel/{ => Global}/DatawrapperBlockTest.php (95%) rename html/modules/custom/ghi_blocks/tests/src/Kernel/{ => Global}/DocumentLinksBlockTest.php (98%) rename html/modules/custom/ghi_blocks/tests/src/Kernel/{ => Global}/ExternalWidgetBlockTest.php (97%) rename html/modules/custom/ghi_blocks/tests/src/Kernel/{ => Global}/FeaturedOperationsTest.php (89%) rename html/modules/custom/ghi_blocks/tests/src/Kernel/{ => Global}/LinkBlockTest.php (97%) rename html/modules/custom/ghi_blocks/tests/src/Kernel/{ => Global}/LinkCarouselBlockTest.php (97%) rename html/modules/custom/ghi_blocks/tests/src/Kernel/{ => Global}/SectionCollectionTest.php (98%) create mode 100644 html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanCaseloadTrendsTableTest.php create mode 100644 html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanClusterLogframeLinksTest.php create mode 100644 html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanGoverningEntitiesTableTest.php create mode 100644 html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanHeadlineFiguresTest.php create mode 100644 html/modules/custom/ghi_blocks/tests/src/Kernel/PlanBlockKernelTestBase.php create mode 100644 html/modules/custom/ghi_plans/src/ApiObjects/Attachments/CaseloadAttachmentInterface.php create mode 100644 html/modules/custom/ghi_plans/src/ApiObjects/Attachments/DataAttachmentInterface.php create mode 100644 html/modules/custom/ghi_plans/src/ApiObjects/Partials/PlanOverviewCaseload.php create mode 100644 html/modules/custom/hpc_api/tests/src/Unit/ArrayHelperTest.php diff --git a/README.md b/README.md index 085af78c0..d25fa86af 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ place them in **_.docksal_/backups** To import a snapshot and run all the steps that a deployment would run please use this command: - fin post-deploy -i + fin deploy -i If you have multiple database snapshots available locally, running this command will let you select which one you want to import. @@ -135,7 +135,7 @@ Running this without the _-i_ argument, will just run the deployment actions on the current database without importing a snapshot. -Data migrations +DATA MIGRATIONS --------------- The data migrations can be run locally with this command: diff --git a/html/modules/custom/ghi_base_objects/src/Entity/BaseObjectFocusCountryInterface.php b/html/modules/custom/ghi_base_objects/src/Entity/BaseObjectFocusCountryInterface.php new file mode 100644 index 000000000..0c55ceec2 --- /dev/null +++ b/html/modules/custom/ghi_base_objects/src/Entity/BaseObjectFocusCountryInterface.php @@ -0,0 +1,38 @@ +createBaseObject([ 'type' => 'plan', + 'field_year' => 2025, ]); $this->drupalGet($base_object->toUrl('edit-form')->toString()); $assert_session = $this->assertSession(); diff --git a/html/modules/custom/ghi_base_objects/tests/src/Kernel/BaseObjectTest.php b/html/modules/custom/ghi_base_objects/tests/src/Kernel/BaseObjectTest.php index 19de67da9..83d40ad68 100644 --- a/html/modules/custom/ghi_base_objects/tests/src/Kernel/BaseObjectTest.php +++ b/html/modules/custom/ghi_base_objects/tests/src/Kernel/BaseObjectTest.php @@ -5,7 +5,6 @@ use Drupal\Core\Url; use Drupal\KernelTests\KernelTestBase; use Drupal\Tests\ghi_base_objects\Traits\BaseObjectTestTrait; -use Drupal\ghi_base_objects\Entity\BaseObjectType; /** * Tests the base object entity. @@ -112,15 +111,11 @@ public function testBaseObjectSourceId() { $this->assertEquals(20, $base_object->getSourceId()); $this->assertEquals('plan--20', $base_object->getUniqueIdentifier()); - $base_object_type_incomplete = BaseObjectType::create([ - 'id' => $this->randomMachineName(), - 'label' => $this->randomString(), - 'hasYear' => FALSE, - ]); + $base_object_type_incomplete = $this->createBaseObjectType(); $base_object = $this->createBaseObject([ 'type' => $base_object_type_incomplete->id(), 'name' => 'base_object_name', - 'field_original_id' => 20, + 'field_original_id' => NULL, ]); $this->assertNull($base_object->getSourceId()); } @@ -129,20 +124,12 @@ public function testBaseObjectSourceId() { * Tests base object needsYear() method. */ public function testBaseObjectNeedsYear() { - $base_object_type = $this->createBaseObjectType([ - 'hasYear' => FALSE, - ]); - $base_object = $this->createBaseObject([ - 'type' => $base_object_type->id(), - ]); + $base_object_type = $this->createBaseObjectType(); + $base_object = $this->createBaseObject(['type' => $base_object_type->id()]); $this->assertTrue($base_object->needsYear()); - $base_object_type = $this->createBaseObjectType([ - 'hasYear' => TRUE, - ]); - $base_object = $this->createBaseObject([ - 'type' => $base_object_type->id(), - ]); + $base_object_type = $this->createBaseObjectType(['field_year' => 'Year']); + $base_object = $this->createBaseObject(['type' => $base_object_type->id()]); $this->assertFalse($base_object->needsYear()); } @@ -158,4 +145,28 @@ public function testBaseObjectCreatedTime() { $this->assertEquals($timestamp, $base_object->getCreatedTime()); } + /** + * Tests base object created timestamps. + */ + public function testApiCacheTagsToInvalidate() { + $this->createBaseObjectType(['id' => 'custom_base_object_type']); + $base_object = $this->createBaseObject([ + 'type' => 'custom_base_object_type', + 'field_original_id' => 20, + ]); + $cache_tags = $base_object->getApiCacheTagsToInvalidate(); + $this->assertNotEmpty($cache_tags); + $this->assertIsArray($cache_tags); + $this->assertArrayHasKey(0, $cache_tags); + $this->assertEquals('custom_base_object_type_id:20', $cache_tags[0]); + + $base_object = $this->createBaseObject([ + 'type' => 'custom_base_object_type', + 'field_original_id' => NULL, + ]); + $cache_tags = $base_object->getApiCacheTagsToInvalidate(); + $this->assertEmpty($cache_tags); + $this->assertIsArray($cache_tags); + } + } diff --git a/html/modules/custom/ghi_base_objects/tests/src/Traits/BaseObjectTestTrait.php b/html/modules/custom/ghi_base_objects/tests/src/Traits/BaseObjectTestTrait.php index 1cb410df6..9c3de1a32 100644 --- a/html/modules/custom/ghi_base_objects/tests/src/Traits/BaseObjectTestTrait.php +++ b/html/modules/custom/ghi_base_objects/tests/src/Traits/BaseObjectTestTrait.php @@ -6,6 +6,7 @@ use Drupal\ghi_base_objects\Entity\BaseObjectInterface; use Drupal\ghi_base_objects\Entity\BaseObjectType; use Drupal\ghi_base_objects\Entity\BaseObjectTypeInterface; +use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait; /** * Provides methods to create base objects in tests. @@ -15,17 +16,21 @@ trait BaseObjectTestTrait { use FieldTestTrait; + use EntityReferenceFieldCreationTrait; /** * Create a base object type. * * @param mixed[] $values - * (optional) Additional values for the base object type entity: + * (optional) Additional key-value pairs for the base object type entity: * - id: The ID of the base object type. If none is provided, a random value * will be used. * - label: The human-readable label of the base object type. If none is * provided, a random value will be used. - * - hasYear: Whether the base object type handles years already. + * - field_year: When not empty, this will create a year field with the + * provided label. + * - field_plan: When not empty, this will create a plan field with the + * provided label. * * @return \Drupal\ghi_base_objects\Entity\BaseObjectTypeInterface * A base object type. @@ -37,14 +42,25 @@ protected function createBaseObjectType(array $values = []) { $values += [ 'id' => $this->randomMachineName(), 'label' => $this->randomString(), - 'hasYear' => FALSE, + 'hasYear' => !empty($values['field_year']), + 'field_year' => NULL, + 'field_plan' => NULL, ]; - $base_object_type = BaseObjectType::create($values); - $this->assertSame(SAVED_NEW, $base_object_type->save()); - $this->assertInstanceOf(BaseObjectTypeInterface::class, $base_object_type); - $this->createField('base_object', $base_object_type->id(), 'integer', 'field_original_id', 'Source id'); - if (!empty($values['hasYear'])) { - $this->createField('base_object', $base_object_type->id(), 'integer', 'field_year', 'Year'); + $base_object_type = BaseObjectType::load($values['id']) ?: BaseObjectType::create($values); + if ($base_object_type->isNew()) { + $this->assertSame(SAVED_NEW, $base_object_type->save()); + $this->assertInstanceOf(BaseObjectTypeInterface::class, $base_object_type); + $this->createField('base_object', $base_object_type->id(), 'integer', 'field_original_id', 'Source id'); + if (!empty($values['field_year'])) { + $this->createField('base_object', $base_object_type->id(), 'integer', 'field_year', $values['field_year']); + } + if (!empty($values['field_plan'])) { + $this->createEntityReferenceField('base_object', $base_object_type->id(), 'field_plan', $values['field_plan'], 'base_object', 'default', [ + 'target_bundles' => [ + 'plan' => 'plan', + ], + ]); + } } return $base_object_type; } diff --git a/html/modules/custom/ghi_blocks/src/Plugin/Block/GHIBlockBase.php b/html/modules/custom/ghi_blocks/src/Plugin/Block/GHIBlockBase.php index c9e743911..121771d7c 100644 --- a/html/modules/custom/ghi_blocks/src/Plugin/Block/GHIBlockBase.php +++ b/html/modules/custom/ghi_blocks/src/Plugin/Block/GHIBlockBase.php @@ -284,7 +284,7 @@ public function hasDefaultTitle() { /** * Get the default title. * - * @return string + * @return \Drupal\Core\StringTranslation\TranslatableMarkup|string * The default title if one is set in the plugin definition. */ public function getDefaultTitle() { diff --git a/html/modules/custom/ghi_blocks/src/Plugin/Block/GlobalPage/PlanTable.php b/html/modules/custom/ghi_blocks/src/Plugin/Block/GlobalPage/PlanTable.php index 804c0213c..58e1b8596 100644 --- a/html/modules/custom/ghi_blocks/src/Plugin/Block/GlobalPage/PlanTable.php +++ b/html/modules/custom/ghi_blocks/src/Plugin/Block/GlobalPage/PlanTable.php @@ -12,6 +12,7 @@ use Drupal\ghi_blocks\Traits\GlobalSettingsTrait; use Drupal\ghi_blocks\Traits\PlanFootnoteTrait; use Drupal\ghi_blocks\Traits\TableSoftLimitTrait; +use Drupal\ghi_blocks\Traits\TableTrait; use Drupal\ghi_plans\ApiObjects\Mocks\PlanOverviewPlanMock; use Drupal\ghi_plans\Traits\FtsLinkTrait; use Drupal\hpc_common\Helpers\ArrayHelper; @@ -44,6 +45,7 @@ class PlanTable extends GHIBlockBase implements HPCDownloadExcelInterface, HPCDo use GlobalPlanOverviewBlockTrait; use GlobalSettingsTrait; use PlanFootnoteTrait; + use TableTrait; use TableSoftLimitTrait; use BlockCommentTrait; use FtsLinkTrait; @@ -142,62 +144,22 @@ public function buildTableData($export = FALSE) { $header += [ 'name' => $this->t('Plans'), 'type' => $this->t('Plan type'), - 'inneed' => [ - 'data' => $this->t('People in need'), - 'data-column-type' => 'amount', - ], - 'targeted' => [ - 'data' => $this->t('People targeted'), - 'data-column-type' => 'amount', - ], - 'expected_reach' => [ - 'data' => $this->t('Estimated Reach'), - 'data-column-type' => 'amount', - ], - 'expected_reached' => [ - 'data' => $this->t('% Reached'), - 'data-column-type' => 'amount', - ], - 'latest_reach' => [ - 'data' => $this->t('People reached'), - 'data-column-type' => 'percentage', - ], - 'reached' => [ - 'data' => $this->t('% Reached'), - 'data-column-type' => 'percentage', - ], - 'requirements' => [ - 'data' => $this->t('Requirements'), - 'data-column-type' => 'currency', - ], - 'funding' => [ - 'data' => $this->t('Funding'), - 'data-column-type' => 'currency', - ], - 'coverage' => [ - 'data' => $this->t('% Funded'), - 'data-column-type' => 'percentage', - ], - 'status' => [ - 'data' => $this->t('Status'), - 'data-column-type' => 'status', - 'sortable' => FALSE, - ], + 'inneed' => $this->buildHeaderColumn($this->t('People in need'), 'amount'), + 'targeted' => $this->buildHeaderColumn($this->t('People targeted'), 'amount'), + 'expected_reach' => $this->buildHeaderColumn($this->t('Estimated Reach'), 'amount'), + 'expected_reached' => $this->buildHeaderColumn($this->t('% Reached'), 'percentage'), + 'latest_reach' => $this->buildHeaderColumn($this->t('People reached'), 'percentage'), + 'reached' => $this->buildHeaderColumn($this->t('% Reached'), 'percentage'), + 'requirements' => $this->buildHeaderColumn($this->t('Requirements'), 'currency'), + 'funding' => $this->buildHeaderColumn($this->t('Funding'), 'currency'), + 'coverage' => $this->buildHeaderColumn($this->t('% Funded'), 'percentage'), + 'status' => $this->buildHeaderColumn($this->t('Status'), 'status'), ]; if ($export) { $header['in_gho'] = $this->t('In GHO'); - $header['document'] = [ - 'data' => $this->t('Document'), - 'data-column-type' => 'document', - ]; - $header['link_ha'] = [ - 'data' => $this->t('Link to HA page'), - 'data-column-type' => 'document', - ]; - $header['link_fts'] = [ - 'data' => $this->t('Link to FTS page'), - 'data-column-type' => 'document', - ]; + $header['document'] = $this->buildHeaderColumn($this->t('Document'), 'document'); + $header['link_ha'] = $this->buildHeaderColumn($this->t('Link to HA page'), 'document'); + $header['link_fts'] = $this->buildHeaderColumn($this->t('Link to FTS page'), 'document'); } $cache_tags = []; @@ -241,19 +203,15 @@ public function buildTableData($export = FALSE) { $document_uri = $plan->getPlanDocumentUri(); // Setup the column values. - $value_in_need = $in_need ? [ + $value_in_need = [ '#theme' => 'hpc_amount', - '#amount' => $in_need, + '#amount' => $in_need ?: '-', '#decimals' => $decimals, - ] : [ - '#markup' => '-', ]; - $value_targeted = $target ? [ + $value_targeted = [ '#theme' => 'hpc_amount', - '#amount' => $target, + '#amount' => $target ?: '-', '#decimals' => $decimals, - ] : [ - '#markup' => '-', ]; $value_expected_reach = [ '#theme' => 'hpc_amount', @@ -264,12 +222,10 @@ public function buildTableData($export = FALSE) { '#theme' => 'hpc_percent', '#ratio' => $expected_reached / 100, ]; - $value_latest_reached = $latest_reached !== NULL ? [ + $value_latest_reached = [ '#theme' => 'hpc_amount', - '#amount' => $latest_reached, + '#amount' => $latest_reached ?? $this->t('Pending'), '#decimals' => $decimals, - ] : [ - '#markup' => $this->t('Pending'), ]; $value_reached = $reached_percent ? [ '#theme' => 'hpc_percent', diff --git a/html/modules/custom/ghi_blocks/src/Plugin/Block/Menu/SectionSwitcher.php b/html/modules/custom/ghi_blocks/src/Plugin/Block/Menu/SectionSwitcher.php index 2d4c39a03..8bd304dbc 100644 --- a/html/modules/custom/ghi_blocks/src/Plugin/Block/Menu/SectionSwitcher.php +++ b/html/modules/custom/ghi_blocks/src/Plugin/Block/Menu/SectionSwitcher.php @@ -5,7 +5,6 @@ use Drupal\Core\Block\BlockBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\ghi_base_objects\Traits\ShortNameTrait; -use Drupal\ghi_plans\Entity\Plan; use Drupal\ghi_sections\Entity\SectionNodeInterface; use Drupal\ghi_sections\Traits\SectionPathTrait; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -113,46 +112,15 @@ public function build() { /** * Build the section switcher options. * - * @return \Drupal\ghi_sections\Entity\SectionNodeInterface[] - * An array of section nodes to be used as options. + * @return \Drupal\ghi_sections\Entity\SectionNodeInterface[]|null + * An array of section nodes to be used as options or NULL. */ private function buildSectionSwitcherOptions() { - $section_node = $this->getSectionNode(); if (!$section_node) { return NULL; } - - $base_object = $section_node->getBaseObject(); - $sections = []; - if (!$section_node->get('field_year')->isEmpty()) { - // This is either a global section page or a section page with a base - // object that needs an additional year specified. - $args = array_filter([ - 'type' => $section_node->bundle(), - 'field_base_object' => $base_object?->id(), - ]); - $candidates = $this->entityTypeManager->getStorage($section_node->getEntityTypeId())->loadByProperties($args); - foreach ($candidates as $candidate) { - $year = $candidate->get('field_year')->value; - $sections[$year] = $candidate; - } - } - elseif ($base_object && $base_object->hasField('field_focus_country')) { - // This is a section page with no year but with a focus country field, - // e.g. a plan based section page. - $sections = $this->getSectionsByBaseObjectFocusCountry(); - } - elseif ($base_object) { - // This is a section page with no year, e.g. a plan based section page. - $sections = $this->getSectionsByBaseObjectCountryReference(); - } - - if (empty($sections)) { - return NULL; - } - - return $sections; + return $this->sectionManager->getRelatedSections($section_node); } /** @@ -180,122 +148,4 @@ private function getSectionNode() { return $section_node instanceof SectionNodeInterface ? $section_node : NULL; } - /** - * Get switcher options by a country reference on the sections base objects. - * - * @return \Drupal\ghi_sections\Entity\SectionNodeInterface[] - * An array of section nodes keyed by the base object original id - */ - private function getSectionsByBaseObjectFocusCountry() { - $options = []; - $section_node = $this->getSectionNode(); - $base_object = $section_node->getBaseObject(); - if (!$base_object || !$base_object->hasField('field_focus_country') || $base_object->get('field_focus_country')->isEmpty()) { - return $options; - } - $focus_country = $base_object->get('field_focus_country')->entity; - - // Find other object candidates that have the same focus country. - /** @var \Drupal\ghi_base_objects\Entity\BaseObjectInterface[] $base_object_candidates */ - $base_object_candidates = $this->entityTypeManager->getStorage($base_object->getEntityTypeId())->loadByProperties([ - 'type' => $base_object->bundle(), - 'field_focus_country' => $focus_country->id(), - ]); - - // If base object is a plan, thus looking for other plan base objects, - // apply filtering based on the plan type. - if ($base_object instanceof Plan) { - $base_object_candidates = array_filter($base_object_candidates, function (Plan $base_object_candidate) use ($base_object) { - // If the current base object is of type RRP, we want to retain only - // candidates that are also RRPs. If it's not an RRP, we only want - // other candiates that are not RRPs either. - return $base_object->isRrp() ? $base_object_candidate->isRrp() : !$base_object_candidate->isRrp(); - }); - } - - return $this->getSectionOptionsForBaseObjects($section_node, $base_object_candidates); - } - - /** - * Get switcher options by a country reference on the sections base objects. - * - * @return \Drupal\ghi_sections\Entity\SectionNodeInterface[] - * An array of section nodes keyed by the base object original id - */ - private function getSectionsByBaseObjectCountryReference() { - $options = []; - $section_node = $this->getSectionNode(); - $base_object = $section_node->getBaseObject(); - if (!$base_object || !$base_object->hasField('field_country') || $base_object->get('field_country')->isEmpty()) { - return $options; - } - - // Get the list of all countries associated with this object. - $country_ids = array_map(function ($country) { - return $country->id(); - }, $base_object->get('field_country')->referencedEntities()); - - // Find other object candidates that have at least one of these countries - // associated. - /** @var \Drupal\ghi_base_objects\Entity\BaseObjectInterface[] $base_object_candidates */ - $base_object_candidates = $this->entityTypeManager->getStorage($base_object->getEntityTypeId())->loadByProperties([ - 'type' => $base_object->bundle(), - 'field_country' => $country_ids, - ]); - - // Then filter out the ones that don't share the full set of countries. - $base_object_candidates = array_filter($base_object_candidates, function ($base_object_candidate) use ($country_ids) { - $candidate_country_ids = array_map(function ($country) { - return $country->id(); - }, $base_object_candidate->get('field_country')->referencedEntities()); - return empty(array_diff($country_ids, $candidate_country_ids)) && count($candidate_country_ids) == count($country_ids); - }); - if (empty($base_object_candidates)) { - return $options; - } - return $this->getSectionOptionsForBaseObjects($section_node, $base_object_candidates); - } - - /** - * Get the section options for the given base object. - * - * @param \Drupal\ghi_sections\Entity\SectionNodeInterface $section_node - * The current section node. - * @param \Drupal\ghi_base_objects\Entity\BaseObjectInterface[] $base_objects - * The base objects. - * - * @return \Drupal\ghi_sections\Entity\SectionNodeInterface[] - * An array of section nodes keyed by the base object original id. - */ - private function getSectionOptionsForBaseObjects(SectionNodeInterface $section_node, array $base_objects) { - $base_object = $section_node->getBaseObject(); - // Then load the sections associated to these objects. - /** @var \Drupal\ghi_sections\Entity\SectionNodeInterface[] $section_candidates */ - $section_candidates = $this->entityTypeManager->getStorage($section_node->getEntityTypeId())->loadByProperties([ - 'type' => $section_node->bundle(), - 'field_base_object' => array_keys($base_objects), - ]); - foreach ($section_candidates as $section_candidate) { - if (!$section_candidate->access('view')) { - continue; - } - $options[$section_candidate->getBaseObject()->getSourceId()] = $section_candidate; - } - - // Sort the options. - if ($base_object->hasField('field_year')) { - // If the base object has a year field, use that for sorting. - usort($options, function ($section_a, $section_b) { - $year_a = $section_a->getBaseObject()->get('field_year')->value; - $year_b = $section_b->getBaseObject()->get('field_year')->value; - return $year_a - $year_b; - }); - } - else { - // Otherwise just use the base objects original id as a best guess. - ksort($options); - } - return array_reverse($options); - } - } diff --git a/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanCaseloadTrendsTable.php b/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanCaseloadTrendsTable.php new file mode 100644 index 000000000..2a25b9367 --- /dev/null +++ b/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanCaseloadTrendsTable.php @@ -0,0 +1,286 @@ +sectionManager = $container->get('ghi_sections.manager'); + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getDefaultTitle() { + $title = parent::getDefaultTitle(); + $langcode = $this->getCurrentPlanObject()?->getPlanLanguage() ?? 'en'; + // @codingStandardsIgnoreStart + return $title ? $this->t((string) $title, [], ['langcode' => $langcode]) : $title; + // @codingStandardsIgnoreEnd + } + + /** + * {@inheritdoc} + */ + public function buildContent() { + $table = $this->buildTableData(); + if (empty($table)) { + return NULL; + } + return [ + '#theme' => 'table', + '#header' => $table['header'], + '#rows' => $table['rows'], + '#progress_groups' => TRUE, + '#sortable' => TRUE, + ]; + } + + /** + * Build the table data for this element. + * + * @return array|null + * An array with the keys "header" and "rows". + */ + private function buildTableData() { + $data = $this->buildSourceData(); + if (empty($data)) { + return NULL; + } + + $langcode = $this->getCurrentPlanObject()?->getPlanLanguage() ?? 'en'; + $t_options = ['langcode' => $langcode]; + $header = [ + $this->buildHeaderColumn($this->t('Year', [], $t_options), 'number'), + $this->buildHeaderColumn($this->t('People in need', [], $t_options), 'amount'), + $this->buildHeaderColumn($this->t('People targeted', [], $t_options), 'amount'), + $this->buildHeaderColumn($this->t('Requirements ($)', [], $t_options), 'currency'), + $this->buildHeaderColumn($this->t('Funding ($)', [], $t_options), 'currency'), + $this->buildHeaderColumn($this->t('% Funded', [], $t_options), 'percentage'), + ]; + $rows = []; + + foreach ($data as $item) { + $row = [ + [ + 'data' => $item['label_link'], + 'data-raw-value' => $item['label'], + 'data-column-type' => 'string', + ], + [ + 'data' => [ + '#theme' => 'hpc_amount', + '#amount' => $item['in_need'] ?: '-', + '#decimals' => 1, + ], + 'data-raw-value' => $item['in_need'], + 'data-column-type' => 'amount', + 'data-progress-group' => 'people', + ], + [ + 'data' => [ + '#theme' => 'hpc_amount', + '#amount' => $item['target'] ?: '-', + '#decimals' => 1, + ], + 'data-raw-value' => $item['target'], + 'data-column-type' => 'amount', + 'data-progress-group' => 'people', + ], + [ + 'data' => [ + '#theme' => 'hpc_currency', + '#value' => $item['current_requirements'], + ], + 'data-raw-value' => $item['current_requirements'], + 'data-column-type' => 'currency', + 'data-progress-group' => 'financial', + ], + [ + 'data' => [ + '#theme' => 'hpc_currency', + '#value' => $item['total_funding'], + ], + 'data-raw-value' => $item['total_funding'], + 'data-column-type' => 'currency', + 'data-progress-group' => 'financial', + ], + [ + 'data' => [ + '#theme' => 'hpc_percent', + '#percent' => $item['funding_coverage'], + ], + 'data-raw-value' => $item['funding_coverage'], + 'data-column-type' => 'percentage', + 'data-progress-group' => 'coverage', + ], + ]; + $rows[] = $row; + } + + if (empty($rows)) { + return NULL; + } + + return [ + 'header' => $header, + 'rows' => $rows, + ]; + } + + /** + * Build the source data for this element. + * + * @return array|null + * An array with data or NULL. + */ + private function buildSourceData() { + $related_sections = $this->getRelatedSections(); + if (empty($related_sections)) { + return NULL; + } + /** @var \Drupal\ghi_plans\Plugin\EndpointQuery\AttachmentSearchQuery $attachments_query */ + $attachments_query = $this->getQueryHandler('attachment_search'); + + /** @var \Drupal\ghi_plans\Plugin\EndpointQuery\PlanFundingSummaryQuery $funding_query */ + $funding_query = $this->getQueryHandler('plan_funding'); + + $data = []; + $years = []; + foreach ($related_sections as $section) { + /** @var \Drupal\ghi_plans\Entity\Plan $plan */ + $plan = $section->getBaseObject(); + $years[$plan->getYear()] = !empty($years[$plan->getYear()]) ? $years[$plan->getYear()] + 1 : 1; + } + + foreach ($related_sections as $section) { + /** @var \Drupal\ghi_plans\Entity\Plan $plan */ + $plan = $section->getBaseObject(); + + /** @var \Drupal\ghi_plans\ApiObjects\Attachments\CaseloadAttachment[] $caseloads */ + $caseloads = $attachments_query->getAttachmentsByObject('plan', $plan->getSourceId(), ['type' => 'caseload']); + $caseload = count($caseloads) > 1 ? $plan->getPlanCaseload($caseloads) : (!empty($caseloads) ? reset($caseloads) : NULL); + $funding_data = $funding_query->getData(['plan_id' => $plan->getSourceId()]); + + $label = $years[$plan->getYear()] > 1 ? $plan->getYear() . ' - ' . $plan->getShortName() : $plan->getYear(); + + $data[] = [ + 'label' => $label, + 'label_link' => $section->access('view') ? $section->toLink($label)->toRenderable() : ['#markup' => $label], + 'in_need' => $caseload?->getFieldByType('inNeed')?->value, + 'target' => $caseload?->getFieldByType('target')?->value, + 'current_requirements' => $funding_data['current_requirements'], + 'total_funding' => $funding_data['total_funding'], + 'funding_coverage' => $funding_data['funding_coverage'], + ]; + } + return $data; + } + + /** + * Get the related sections for this element. + * + * @return \Drupal\ghi_sections\Entity\SectionNodeInterface[] + * An array of section nodes associated to plan base objects. + */ + private function getRelatedSections() { + $section = $this->getCurrentSectionNode(); + $plan = $this->getCurrentPlanObject(); + if (!$plan || !$section) { + return []; + } + $related_sections = $this->sectionManager->getRelatedSections($section); + if (empty($related_sections)) { + return []; + } + $config = $this->getBlockConfig(); + $max_year = date('Y'); + $min_year = $plan->getYear() - $config['years'] + 1; + $plan_type_id = $plan->getPlanType()->id(); + $related_sections = array_filter($related_sections, function ($_section) use ($min_year, $max_year, $plan_type_id) { + $_base_object = $_section->getBaseObject(); + if (!$_base_object instanceof Plan || $_base_object->getPlanType()->id() != $plan_type_id) { + return FALSE; + } + return in_array($_base_object->getYear(), range($min_year, $max_year)); + }); + return $related_sections; + } + + /** + * {@inheritdoc} + */ + public function getConfigurationDefaults() { + return [ + 'years' => NULL, + ]; + } + + /** + * {@inheritdoc} + */ + public function getConfigForm(array $form, FormStateInterface $form_state) { + $years = range(self::MIN_YEARS, self::MAX_YEARS); + $form['years'] = [ + '#type' => 'select', + '#title' => $this->t('Number of years to show'), + '#options' => array_combine($years, $years), + '#default_value' => $this->getDefaultFormValueFromFormState($form_state, 'years') ?: self::DEFAULT_MAX_YEARS, + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function buildDownloadData() { + return $this->buildTableData(); + } + +} diff --git a/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanClusterLogframeLinks.php b/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanClusterLogframeLinks.php index e672b15fd..2015593a6 100644 --- a/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanClusterLogframeLinks.php +++ b/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanClusterLogframeLinks.php @@ -129,7 +129,7 @@ public function buildContent() { '#image' => Markup::create($icon), '#title' => $title_map[$plan?->getPlanClusterType() ?? Plan::CLUSTER_TYPE_CLUSTER], '#description' => $description_map[$plan?->getPlanClusterType() ?? Plan::CLUSTER_TYPE_CLUSTER], - '#link' => $link->toRenderable(), + '#link' => $link?->toRenderable(), ]; } diff --git a/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanGoverningEntitiesTable.php b/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanGoverningEntitiesTable.php index 589050a1d..657a5324b 100644 --- a/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanGoverningEntitiesTable.php +++ b/html/modules/custom/ghi_blocks/src/Plugin/Block/Plan/PlanGoverningEntitiesTable.php @@ -309,7 +309,7 @@ protected function getConfigurationDefaults() { */ public function getDefaultSubform($is_new = FALSE) { $conf = $this->getBlockConfig(); - if (!empty($conf['table']) && !empty($conf['table'])) { + if (!empty($conf['table']) && !empty($conf['table']['columns'])) { return 'table'; } return 'base'; @@ -397,8 +397,8 @@ public function displayForm(array $form, FormStateInterface $form_state) { /** * Get all governing entity objects for the current block instance. * - * @return \Drupal\ghi_plans\ApiObjects\Entities\EntityObjectInterface[] - * An array of entity objects, aka clusters. + * @return \Drupal\ghi_plans\ApiObjects\Entities\EntityObjectInterface[]|null + * An array of entity objects, aka clusters or NULL. */ private function getEntityObjects() { /** @var \Drupal\ghi_plans\Plugin\EndpointQuery\PlanEntitiesQuery $query */ @@ -426,7 +426,7 @@ private function loadBaseObjectsForEntities(array $entities) { /** * Get the first entity node for column configuration. * - * @return \Drupal\ghi_base_objects\Entity\BaseObjectInterface + * @return \Drupal\ghi_base_objects\Entity\BaseObjectInterface|null * The first entity node available. */ private function getFirstEntityObject() { diff --git a/html/modules/custom/ghi_blocks/src/Traits/ConfigurationItemClusterRestrictTrait.php b/html/modules/custom/ghi_blocks/src/Traits/ConfigurationItemClusterRestrictTrait.php index 55183d4c2..5cf0382e2 100644 --- a/html/modules/custom/ghi_blocks/src/Traits/ConfigurationItemClusterRestrictTrait.php +++ b/html/modules/custom/ghi_blocks/src/Traits/ConfigurationItemClusterRestrictTrait.php @@ -23,11 +23,11 @@ public function buildClusterRestrictFormElement(?array $default_value = NULL) { '#type' => 'cluster_restrict', '#title' => $this->t('Restrict by cluster'), '#default_value' => $default_value, - '#ajax' => [ + '#ajax' => property_exists($this, 'wrapperId') ? [ 'event' => 'change', 'callback' => [static::class, 'updateAjax'], 'wrapper' => $this->wrapperId, - ], + ] : NULL, ]; } diff --git a/html/modules/custom/ghi_blocks/src/Traits/TableTrait.php b/html/modules/custom/ghi_blocks/src/Traits/TableTrait.php new file mode 100644 index 000000000..ab6211f76 --- /dev/null +++ b/html/modules/custom/ghi_blocks/src/Traits/TableTrait.php @@ -0,0 +1,31 @@ + $label, + 'data-column-type' => $type, + 'sortable' => $sortable, + ]; + } + +} diff --git a/html/modules/custom/ghi_blocks/tests/src/Kernel/BlockKernelTestBase.php b/html/modules/custom/ghi_blocks/tests/src/Kernel/BlockKernelTestBase.php index 842863d46..637e0b2c5 100644 --- a/html/modules/custom/ghi_blocks/tests/src/Kernel/BlockKernelTestBase.php +++ b/html/modules/custom/ghi_blocks/tests/src/Kernel/BlockKernelTestBase.php @@ -23,7 +23,6 @@ abstract class BlockKernelTestBase extends KernelTestBase { 'migrate', 'hpc_api', 'ghi_form_elements', - // 'ghi_subpages', 'ghi_sections', 'ghi_blocks', 'ghi_base_objects', @@ -62,6 +61,8 @@ protected function createSectionComponent($plugin_id, $configuration, $label = ' * The plugin id. * @param array $configuration * The hpc-specific configuration. + * @param array $contexts + * An array of context objects. * @param string $label * The label. * @param bool $label_display @@ -70,8 +71,9 @@ protected function createSectionComponent($plugin_id, $configuration, $label = ' * @return \Drupal\hpc_common\Plugin\HPCPluginInterface * The block plugin. */ - protected function createBlockPlugin($plugin_id, $configuration, $label = '', $label_display = FALSE) { - return $this->createSectionComponent($plugin_id, $configuration, $label, $label_display)?->getPlugin(); + protected function createBlockPlugin($plugin_id, $configuration, array $contexts = [], $label = '', $label_display = FALSE) { + $plugin = $this->createSectionComponent($plugin_id, $configuration, $label, $label_display)?->getPlugin($contexts); + return $plugin; } /** diff --git a/html/modules/custom/ghi_blocks/tests/src/Kernel/DatawrapperBlockTest.php b/html/modules/custom/ghi_blocks/tests/src/Kernel/Global/DatawrapperBlockTest.php similarity index 95% rename from html/modules/custom/ghi_blocks/tests/src/Kernel/DatawrapperBlockTest.php rename to html/modules/custom/ghi_blocks/tests/src/Kernel/Global/DatawrapperBlockTest.php index 4dee89d81..fcadb585f 100644 --- a/html/modules/custom/ghi_blocks/tests/src/Kernel/DatawrapperBlockTest.php +++ b/html/modules/custom/ghi_blocks/tests/src/Kernel/Global/DatawrapperBlockTest.php @@ -1,9 +1,10 @@ getBlockPlugin(); + $this->assertInstanceOf(PlanCaseloadTrendsTable::class, $plugin); + $this->assertInstanceOf(OverrideDefaultTitleBlockInterface::class, $plugin); + $this->assertInstanceOf(HPCDownloadExcelInterface::class, $plugin); + $this->assertInstanceOf(HPCDownloadPNGInterface::class, $plugin); + + $this->assertEquals(5, $plugin->getBlockConfig()['years']); + $this->assertEquals('Evolution of the humanitarian response', $plugin->label()); + } + + /** + * Tests the block without context nodes. + */ + public function testBlockNoContext() { + $plugin = $this->createBlockPlugin('plan_caseload_trends_table', []); + $this->assertIsArray($this->callPrivateMethod($plugin, 'getRelatedSections')); + $this->assertEmpty($this->callPrivateMethod($plugin, 'getRelatedSections')); + $this->assertNull($this->callPrivateMethod($plugin, 'buildTableData')); + $this->assertNull($this->callPrivateMethod($plugin, 'buildSourceData')); + $this->assertNull($plugin->buildContent()); + } + + /** + * Tests the block forms. + */ + public function testBlockForms() { + $plugin = $this->getBlockPlugin(); + + $form_state = new FormState(); + $form_state->set('block', $plugin); + $form = $plugin->getConfigForm([], $form_state); + $this->assertEquals(array_combine(range(3, 10), range(3, 10)), $form['years']['#options']); + } + + /** + * Tests the retrieval of related sections. + */ + public function testGetRelatedSections() { + $plugin = $this->getBlockPlugin(); + $related_sections = $this->callPrivateMethod($plugin, 'getRelatedSections'); + $this->assertNotEmpty($related_sections); + $this->assertCount(1, $related_sections); + } + + /** + * Tests the table data. + */ + public function testBuildTableData() { + $plugin = $this->getBlockPlugin(); + $this->injectApiQueryStubs($plugin); + $table_data = $this->callPrivateMethod($plugin, 'buildTableData'); + $this->assertNotEmpty($table_data); + $this->assertCount(6, $table_data['header']); + $this->assertCount(1, $table_data['rows']); + + $requirements_cell = $table_data['rows'][0][3]; + $this->assertEquals(3000, $requirements_cell['data-raw-value']); + $this->assertEquals('currency', $requirements_cell['data-column-type']); + $this->assertEquals('financial', $requirements_cell['data-progress-group']); + + $funding_cell = $table_data['rows'][0][4]; + $this->assertEquals(1000, $funding_cell['data-raw-value']); + $this->assertEquals('currency', $funding_cell['data-column-type']); + $this->assertEquals('financial', $funding_cell['data-progress-group']); + + $coverage_cell = $table_data['rows'][0][5]; + $this->assertEquals('hpc_percent', $coverage_cell['data']['#theme']); + $this->assertEquals(0.333, $coverage_cell['data']['#percent']); + $this->assertEquals(0.333, $coverage_cell['data-raw-value']); + $this->assertEquals('percentage', $coverage_cell['data-column-type']); + $this->assertEquals('coverage', $coverage_cell['data-progress-group']); + } + + /** + * Tests the download data. + */ + public function testBuildDownloadData() { + $plugin = $this->getBlockPlugin(); + $table_data = $this->callPrivateMethod($plugin, 'buildTableData'); + $this->assertEquals($table_data, $plugin->buildDownloadData()); + } + + /** + * Tests the source data. + */ + public function testBuildSourceData() { + $plugin = $this->getBlockPlugin(); + $this->injectApiQueryStubs($plugin); + $source_data = $this->callPrivateMethod($plugin, 'buildSourceData'); + $this->assertNotEmpty($source_data); + $this->assertCount(1, $source_data); + $this->assertEquals('2025', $source_data[0]['label']); + $this->assertEquals(300, $source_data[0]['in_need']); + $this->assertEquals(100, $source_data[0]['target']); + $this->assertEquals(3000, $source_data[0]['current_requirements']); + $this->assertEquals(1000, $source_data[0]['total_funding']); + $this->assertEquals(0.333, $source_data[0]['funding_coverage']); + } + + /** + * Tests the block build. + */ + public function testBlockBuild() { + $plugin = $this->getBlockPlugin(); + $build = $plugin->buildContent(); + $this->assertNotEmpty($build); + $this->assertEquals($build['#theme'], 'table'); + $this->assertEquals($build['#progress_groups'], TRUE); + $this->assertEquals($build['#sortable'], TRUE); + $this->assertCount(6, $build['#header']); + $this->assertCount(1, $build['#rows']); + } + + /** + * Get a block plugin. + * + * @return \Drupal\ghi_blocks\Plugin\Block\Plan\PlanCaseloadTrendsTable + * The block plugin. + */ + private function getBlockPlugin() { + $configuration = [ + 'years' => 5, + ]; + $contexts = $this->getPlanSectionContexts(['field_year' => 2025]); + return $this->createBlockPlugin('plan_caseload_trends_table', $configuration, $contexts); + } + + /** + * Inject the plan entity query stub to the plugin. + * + * @param \Drupal\ghi_blocks\Plugin\Block\GHIBlockBase $plugin + * The plugin. + */ + private function injectApiQueryStubs($plugin) { + $plan_funding_query = $this->prophesize(PlanFundingSummaryQuery::class); + $plan_funding_query->getData(Argument::cetera())->willReturn([ + 'total_funding' => 1000, + 'current_requirements' => 3000, + 'funding_coverage' => 0.333, + ]); + $plugin->setQueryHandler('plan_funding', $plan_funding_query->reveal()); + + $caseload = $this->prophesize(CaseloadAttachment::class); + $caseload->getFieldByType('inNeed')->willReturn((object) ['value' => 300]); + $caseload->getFieldByType('target')->willReturn((object) ['value' => 100]); + $attachment_search_query = $this->prophesize(AttachmentSearchQuery::class); + $attachment_search_query->getAttachmentsByObject(Argument::cetera())->willReturn([$caseload->reveal()]); + $plugin->setQueryHandler('attachment_search', $attachment_search_query->reveal()); + } + +} diff --git a/html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanClusterLogframeLinksTest.php b/html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanClusterLogframeLinksTest.php new file mode 100644 index 000000000..b37529e57 --- /dev/null +++ b/html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanClusterLogframeLinksTest.php @@ -0,0 +1,122 @@ +planClusterManager = $this->container->get('ghi_plan_clusters.manager'); + + $this->createSubpageContentTypes(); + $this->createPlanClusterContentTypes(); + } + + /** + * Tests the block properties. + */ + public function testBlockProperties() { + $plugin = $this->getBlockPlugin(FALSE); + $this->assertInstanceOf(PlanClusterLogframeLinks::class, $plugin); + $this->assertInstanceOf(OverrideDefaultTitleBlockInterface::class, $plugin); + $this->assertEquals('Cluster Frameworks', $plugin->label()); + } + + /** + * Tests the buildContent method. + */ + public function testBuildContent() { + $plugin = $this->getBlockPlugin(); + $build = $plugin->buildContent(); + $this->assertNull($build); + + $section_node = $plugin->getCurrentSectionNode(); + $this->assertNotNull($section_node); + $this->assertTrue($section_node->isPublished()); + + $plan_cluster = $this->createPlanCluster($section_node); + $plan_cluster->setPublished()->save(); + $this->assertTrue($plan_cluster->isPublished()); + $this->planClusterManager->assureLogframeSubpagesForBaseNode($section_node); + $build = $plugin->buildContent(); + $this->assertNotNull($build); + } + + /** + * Tests the getRenderableEntities method. + */ + public function testGetRenderableEntities() { + $plugin = $this->getBlockPlugin(); + $this->assertNull($this->callPrivateMethod($plugin, 'getRenderableEntities')); + + $this->createContentType(['type' => 'page']); + $node = Node::create(['type' => 'page', 'title' => 'Page']); + $node->save(); + // $plan_cluster = $this->createPlanCluster($section_node); + $plugin->setContextValue('node', $node); + $this->assertNull($this->callPrivateMethod($plugin, 'getRenderableEntities')); + } + + /** + * Tests the block forms. + */ + public function testBlockForms() { + $plugin = $this->getBlockPlugin(); + + $form_state = new FormState(); + $form_state->set('block', $plugin); + + // Test the config form. + $form = $plugin->getConfigForm([], $form_state); + $this->assertEmpty($form); + } + + /** + * Get a block plugin. + * + * @return \Drupal\ghi_blocks\Plugin\Block\Plan\PlanClusterLogframeLinks + * The block plugin. + */ + private function getBlockPlugin() { + $contexts = $this->getPlanSectionContexts(); + return $this->createBlockPlugin('plan_cluster_logframe_links', [], $contexts); + } + +} diff --git a/html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanGoverningEntitiesTableTest.php b/html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanGoverningEntitiesTableTest.php new file mode 100644 index 000000000..71c996bc1 --- /dev/null +++ b/html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanGoverningEntitiesTableTest.php @@ -0,0 +1,240 @@ +createSubpageContentTypes(); + $this->createBaseObjectType([ + 'id' => 'governing_entity', + ]); + } + + /** + * Tests the block properties. + */ + public function testBlockProperties() { + $plugin = $this->getBlockPlugin(FALSE); + $this->assertInstanceOf(PlanGoverningEntitiesTable::class, $plugin); + $this->assertInstanceOf(MultiStepFormBlockInterface::class, $plugin); + $this->assertInstanceOf(OverrideDefaultTitleBlockInterface::class, $plugin); + $this->assertInstanceOf(HPCDownloadExcelInterface::class, $plugin); + $this->assertInstanceOf(HPCDownloadPNGInterface::class, $plugin); + + $allowed_item_types = $plugin->getAllowedItemTypes(); + $this->assertCount(3, $allowed_item_types); + $this->assertArrayHasKey('entity_name', $allowed_item_types); + $this->assertArrayHasKey('funding_data', $allowed_item_types); + $this->assertArrayHasKey('project_counter', $allowed_item_types); + + $this->assertEquals('Cluster overview', $plugin->label()); + + $definition = $plugin->getPluginDefinition(); + $this->assertIsArray($definition['config_forms']); + $this->assertCount(3, $definition['config_forms']); + $this->assertArrayHasKey($plugin->getDefaultSubform(), $definition['config_forms']); + $this->assertArrayHasKey($plugin->getTitleSubform(), $definition['config_forms']); + $this->assertEquals('base', $plugin->getDefaultSubform()); + $this->assertEquals('base', $plugin->getTitleSubform()); + + $plugin = $this->getBlockPlugin(); + $this->assertEquals('table', $plugin->getDefaultSubform()); + } + + /** + * Tests the buildContent method. + */ + public function testBuildContent() { + $plugin = $this->getBlockPlugin(); + $build = $plugin->buildContent(); + $this->assertNull($build); + + $cluster = $this->createBaseObject(['type' => 'governing_entity']); + $this->injectPlanEntityQueryStub($plugin, [$cluster]); + $build = $plugin->buildContent(); + $this->assertIsArray($build); + $this->assertEquals($build['#theme'], 'table'); + $this->assertEquals($build['#progress_groups'], TRUE); + $this->assertEquals($build['#sortable'], TRUE); + $this->assertEquals(0, $build['#soft_limit']); + $this->assertCount(1, $build['#header']); + $this->assertCount(1, $build['#rows']); + } + + /** + * Tests the buildTableData method. + */ + public function testBuildTableData() { + $plugin = $this->getBlockPlugin(); + $table_data = $this->callPrivateMethod($plugin, 'buildTableData'); + $this->assertNull($table_data); + + $cluster = $this->createBaseObject(['type' => 'governing_entity']); + $this->injectPlanEntityQueryStub($plugin, [$cluster]); + $table_data = $this->callPrivateMethod($plugin, 'buildTableData'); + $this->assertIsArray($table_data); + $this->assertArrayHasKey('header', $table_data); + $this->assertArrayHasKey('rows', $table_data); + $this->assertArrayHasKey(0, $table_data['rows']); + $this->assertArrayHasKey(0, $table_data['rows'][0]); + $this->assertEquals($cluster->label(), $table_data['rows'][0][0]['data-value']); + $this->assertEquals($cluster->label(), $table_data['rows'][0][0]['data-raw-value']); + $this->assertEquals($cluster->label(), $table_data['rows'][0][0]['export_value']); + $this->assertEquals('Cluster name', $table_data['rows'][0][0]['data-content']); + } + + /** + * Tests the buildDownloadData method. + */ + public function testBuildDownloadData() { + $plugin = $this->getBlockPlugin(); + $table_data = $this->callPrivateMethod($plugin, 'buildTableData'); + $this->assertEquals($table_data, $plugin->buildDownloadData()); + } + + /** + * Tests the block forms. + */ + public function testBlockForms() { + $plugin = $this->getBlockPlugin(); + + $form_state = new FormState(); + $form_state->set('block', $plugin); + + // Test the base form. + $base_form = $plugin->baseForm([], $form_state); + $this->assertArrayHasKey('include_cluster_not_reported', $base_form); + $this->assertArrayHasKey('include_shared_funding', $base_form); + $this->assertArrayHasKey('hide_target_values_for_projects', $base_form); + $this->assertArrayHasKey('cluster_restrict', $base_form); + + $table_form = $plugin->tableForm([], $form_state); + $this->assertArrayHasKey('columns', $table_form); + + $display_form = $plugin->displayForm([], $form_state); + $this->assertArrayHasKey('soft_limit', $display_form); + } + + /** + * Tests the getEntityObjects method. + */ + public function testGetEntityObjects() { + $plugin = $this->getBlockPlugin(); + $entity_objects = $this->callPrivateMethod($plugin, 'getEntityObjects'); + $this->assertNull($entity_objects); + + $this->injectPlanEntityQueryStub($plugin); + $entity_objects = $this->callPrivateMethod($plugin, 'getEntityObjects'); + $this->assertIsArray($entity_objects); + } + + /** + * Tests the loadBaseObjectsForEntities method. + */ + public function testLoadBaseObjectsForEntities() { + $plugin = $this->getBlockPlugin(); + $base_objects = $this->callPrivateMethod($plugin, 'loadBaseObjectsForEntities', [[]]); + $this->assertNull($base_objects); + + $cluster = $this->createBaseObject(['type' => 'governing_entity']); + $entity_object = (object) ['id' => $cluster->getSourceId()]; + $base_objects = $this->callPrivateMethod($plugin, 'loadBaseObjectsForEntities', [[$entity_object]]); + $this->assertIsArray($base_objects); + $this->assertArrayHasKey($cluster->getSourceId(), $base_objects); + $this->assertEquals($cluster->label(), $base_objects[$cluster->getSourceId()]->label()); + } + + /** + * Tests the getFirstEntityObject method. + */ + public function testGetFirstEntityObject() { + $plugin = $this->getBlockPlugin(); + $entity_object = $this->callPrivateMethod($plugin, 'getFirstEntityObject'); + $this->assertNull($entity_object); + + $cluster = $this->createBaseObject(['type' => 'governing_entity']); + $this->injectPlanEntityQueryStub($plugin, [$cluster]); + $entity_object = $this->callPrivateMethod($plugin, 'getFirstEntityObject'); + $this->assertInstanceOf(BaseObjectInterface::class, $entity_object); + } + + /** + * Get a block plugin. + * + * @return \Drupal\ghi_blocks\Plugin\Block\Plan\PlanGoverningEntitiesTable + * The block plugin. + */ + private function getBlockPlugin($configuration = []) { + $configuration = $configuration !== FALSE ? [ + 'table' => [ + 'columns' => [ + [ + 'id' => 0, + 'item_type' => 'entity_name', + 'config' => [ + 'label' => 'Cluster name', + ], + ], + ], + ], + ] : []; + $contexts = $this->getPlanSectionContexts(); + return $this->createBlockPlugin('plan_governing_entities_table', $configuration ?: [], $contexts); + } + + /** + * Inject the plan entity query stub to the plugin. + * + * @param \Drupal\ghi_blocks\Plugin\Block\GHIBlockBase $plugin + * The plugin. + * @param array $clusters + * An array of cluster base objects. + */ + private function injectPlanEntityQueryStub($plugin, array $clusters = []) { + $clusters = $clusters ?? [ + $this->createBaseObject(['type' => 'governing_entity']), + ]; + $plan_entities_query = $this->prophesize(PlanEntitiesQuery::class); + $plan_entities_query->getPlanEntities(Argument::cetera())->willReturn(array_map(function ($cluster) { + return (object) [ + 'id' => $cluster->getSourceId(), + 'name' => $cluster->label(), + ]; + }, $clusters)); + $plugin->setQueryHandler('entities', $plan_entities_query->reveal()); + } + +} diff --git a/html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanHeadlineFiguresTest.php b/html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanHeadlineFiguresTest.php new file mode 100644 index 000000000..b82ec25d5 --- /dev/null +++ b/html/modules/custom/ghi_blocks/tests/src/Kernel/Plan/PlanHeadlineFiguresTest.php @@ -0,0 +1,138 @@ +getBlockPlugin(FALSE); + $this->assertInstanceOf(PlanHeadlineFigures::class, $plugin); + $this->assertInstanceOf(MultiStepFormBlockInterface::class, $plugin); + $this->assertInstanceOf(ConfigurableTableBlockInterface::class, $plugin); + $this->assertInstanceOf(ConfigValidationInterface::class, $plugin); + + $allowed_item_types = $plugin->getAllowedItemTypes(); + $this->assertCount(7, $allowed_item_types); + $this->assertArrayHasKey('item_group', $allowed_item_types); + $this->assertArrayHasKey('line_break', $allowed_item_types); + $this->assertArrayHasKey('funding_data', $allowed_item_types); + $this->assertArrayHasKey('entity_counter', $allowed_item_types); + $this->assertArrayHasKey('project_counter', $allowed_item_types); + $this->assertArrayHasKey('attachment_data', $allowed_item_types); + $this->assertArrayHasKey('label_value', $allowed_item_types); + + $this->assertNull($plugin->label()); + + $definition = $plugin->getPluginDefinition(); + $this->assertIsArray($definition['config_forms']); + $this->assertCount(2, $definition['config_forms']); + $this->assertArrayHasKey($plugin->getDefaultSubform(), $definition['config_forms']); + $this->assertEquals('key_figures', $plugin->getDefaultSubform()); + } + + /** + * Tests the buildContent method. + */ + public function testBuildContent() { + $plugin = $this->getBlockPlugin(FALSE); + $build = $plugin->buildContent(); + $this->assertNull($build); + + $plugin = $this->getBlockPlugin(); + $build = $plugin->buildContent(); + $this->assertIsArray($build); + $this->assertArrayHasKey(0, $build); + $this->assertEquals('tab_container', $build[0]['#theme']); + $this->assertEquals('Population', $build[0]['#tabs'][0]['title']['#markup']); + $this->assertCount(1, $build[0]['#tabs'][0]['items']['#items']); + } + + /** + * Tests the block forms. + */ + public function testBlockForms() { + $plugin = $this->getBlockPlugin(); + + $form_state = new FormState(); + $form_state->set('block', $plugin); + + // Test the key figures form. + $key_figures_form = $plugin->keyFiguresForm([], $form_state); + $this->assertArrayHasKey('items', $key_figures_form); + + // Test the display form. + $display_form = $plugin->displayForm([], $form_state); + $this->assertArrayHasKey('comment', $display_form); + } + + /** + * Tests the config validation and fixing method. + */ + public function testConfigValidation() { + $plugin = $this->getBlockPlugin(FALSE); + $errors = $plugin->getConfigErrors(); + $this->assertIsArray($errors); + $this->assertCount(1, $errors); + $this->assertEquals('No configured items', $errors[0]); + $conf = $plugin->getBlockConfig(); + $plugin->fixConfigErrors(); + $this->assertEquals($conf, $plugin->getBlockConfig()); + + $plugin = $this->getBlockPlugin(); + $errors = $plugin->getConfigErrors(); + $this->assertIsArray($errors); + $this->assertEmpty($errors); + $conf = $plugin->getBlockConfig(); + $plugin->fixConfigErrors(); + $this->assertEquals($conf, $plugin->getBlockConfig()); + } + + /** + * Get a block plugin. + * + * @return \Drupal\ghi_blocks\Plugin\Block\Plan\PlanHeadlineFigures + * The block plugin. + */ + private function getBlockPlugin($configuration = []) { + $configuration = $configuration !== FALSE ? [ + 'key_figures' => [ + 'items' => [ + [ + 'id' => 0, + 'item_type' => 'item_group', + 'config' => [ + 'label' => 'Population', + ], + ], + [ + 'id' => 1, + 'item_type' => 'label_value', + 'config' => [ + 'label' => 'Label', + 'value' => 100, + ], + 'pid' => 0, + ], + ], + ], + ] : []; + $contexts = $this->getPlanSectionContexts(); + return $this->createBlockPlugin('plan_headline_figures', $configuration ?: [], $contexts); + } + +} diff --git a/html/modules/custom/ghi_blocks/tests/src/Kernel/PlanBlockKernelTestBase.php b/html/modules/custom/ghi_blocks/tests/src/Kernel/PlanBlockKernelTestBase.php new file mode 100644 index 000000000..634ca1126 --- /dev/null +++ b/html/modules/custom/ghi_blocks/tests/src/Kernel/PlanBlockKernelTestBase.php @@ -0,0 +1,150 @@ +installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('base_object'); + $this->installEntitySchema('path_alias'); + $this->installSchema('system', 'sequences'); + $this->installSchema('node', ['node_access']); + $this->installConfig(['system', 'node', 'field', 'pathauto']); + + $endpoint_query = $this->prophesize(EndpointQuery::class); + + $container = \Drupal::getContainer(); + $container->set('hpc_api.endpoint_query', $endpoint_query->reveal()); + \Drupal::setContainer($container); + + $this->createPlanBaseObjectType(); + + $this->createSectionType(); + $this->setUpCurrentUser([], ['access content']); + } + + /** + * Create a plan base object type. + */ + private function createPlanBaseObjectType() { + $this->createBaseObjectType([ + 'id' => 'plan', + 'label' => 'Plan', + 'field_year' => 'Year', + ]); + $this->createBaseObjectType([ + 'id' => 'country', + 'label' => 'Country', + ]); + $this->createEntityReferenceField('base_object', 'plan', 'field_country', 'Country', 'base_object', 'default', [ + 'target_bundles' => ['country'], + ]); + $this->createVocabulary(['vid' => 'plan_type']); + $this->createEntityReferenceField('base_object', 'plan', 'field_plan_type', 'Plan type', 'taxonomy_term', 'default', [ + 'target_bundles' => ['plan_type'], + ]); + $this->createField('base_object', 'plan', 'string', 'field_plan_version_argument', 'Plan version'); + } + + /** + * Create a plan base object to be used in tests. + * + * @param array $values + * Optional values for the object creation. + * + * @return \Drupal\ghi_plans\Entity\Plan + * The plan base object + */ + protected function createPlanBaseObject(array $values = []) { + $values['type'] = 'plan'; + if (empty($values['field_year'])) { + $values['field_year'] = 2024; + } + if (empty($values['field_country'])) { + $country_base_object = $this->createBaseObject([ + 'type' => 'country', + ]); + $values['field_country'] = [ + 'target_id' => $country_base_object->id(), + ]; + } + if (empty($values['field_plan_type'])) { + $plan_type = $this->createTerm(Vocabulary::load('plan_type')); + $values['field_plan_type'] = [ + 'target_id' => $plan_type->id(), + ]; + } + $plan = $this->createBaseObject($values); + return $plan; + } + + /** + * Get the necessary contexts for plan sections. + * + * @return array + * An array of context objects, keyed by the context key. + */ + protected function getPlanSectionContexts(array $plan_values = [], array $section_values = []) { + $plan = $this->createPlanBaseObject($plan_values); + $section_node = $this->createSection($section_values + [ + 'label' => 'Section node', + 'field_base_object' => $plan, + ]); + return [ + 'node' => new EntityContext(new EntityContextDefinition('node'), $section_node), + 'plan' => new EntityContext(new EntityContextDefinition('base_object'), $plan), + ]; + } + +} diff --git a/html/modules/custom/ghi_plan_clusters/tests/src/Kernel/PlanClusterNodeBundleTest.php b/html/modules/custom/ghi_plan_clusters/tests/src/Kernel/PlanClusterNodeBundleTest.php index 23553e8d6..d12ba9823 100644 --- a/html/modules/custom/ghi_plan_clusters/tests/src/Kernel/PlanClusterNodeBundleTest.php +++ b/html/modules/custom/ghi_plan_clusters/tests/src/Kernel/PlanClusterNodeBundleTest.php @@ -103,6 +103,7 @@ public function testPlanClusterSubpages() { // Create a plan object. $plan = $this->createBaseObject([ 'type' => 'plan', + 'field_year' => 2025, ]); // Create a GVE object associated to that plan. @@ -157,6 +158,7 @@ public function testTitleOverride() { // Create a plan object. $plan = $this->createBaseObject([ 'type' => 'plan', + 'field_year' => 2025, ]); // Create a GVE object associated to that plan. diff --git a/html/modules/custom/ghi_plan_clusters/tests/src/Traits/PlanClusterTestTrait.php b/html/modules/custom/ghi_plan_clusters/tests/src/Traits/PlanClusterTestTrait.php index f35f3d843..e53d07d71 100644 --- a/html/modules/custom/ghi_plan_clusters/tests/src/Traits/PlanClusterTestTrait.php +++ b/html/modules/custom/ghi_plan_clusters/tests/src/Traits/PlanClusterTestTrait.php @@ -31,6 +31,7 @@ public function createPlanCluster(?SectionNodeInterface $section = NULL) { if ($section === NULL) { $plan = $this->createBaseObject([ 'type' => 'plan', + 'field_year' => 2025, ]); $plan->save(); } @@ -74,10 +75,7 @@ private function createPlanClusterContentTypes() { $this->createBaseObjectType([ 'id' => PlanClusterManager::BASE_OBJECT_BUNDLE_GOVERNING_ENTITY, 'label' => 'Governing entity', - ]); - - $this->createEntityReferenceField('base_object', PlanClusterManager::BASE_OBJECT_BUNDLE_GOVERNING_ENTITY, 'field_plan', 'Plan', 'base_object', 'default', [ - 'target_bundles' => ['plan'], + 'field_plan' => 'Plan', ]); // Create the section bundle. diff --git a/html/modules/custom/ghi_plans/src/ApiObjects/Attachments/CaseloadAttachment.php b/html/modules/custom/ghi_plans/src/ApiObjects/Attachments/CaseloadAttachment.php index c84a9a3b1..164aa1e5c 100644 --- a/html/modules/custom/ghi_plans/src/ApiObjects/Attachments/CaseloadAttachment.php +++ b/html/modules/custom/ghi_plans/src/ApiObjects/Attachments/CaseloadAttachment.php @@ -5,6 +5,6 @@ /** * Abstraction for API data attachment objects. */ -class CaseloadAttachment extends DataAttachment { +class CaseloadAttachment extends DataAttachment implements CaseloadAttachmentInterface { } diff --git a/html/modules/custom/ghi_plans/src/ApiObjects/Attachments/CaseloadAttachmentInterface.php b/html/modules/custom/ghi_plans/src/ApiObjects/Attachments/CaseloadAttachmentInterface.php new file mode 100644 index 000000000..1eb212733 --- /dev/null +++ b/html/modules/custom/ghi_plans/src/ApiObjects/Attachments/CaseloadAttachmentInterface.php @@ -0,0 +1,8 @@ + $attachment->id, 'type' => strtolower($attachment->type), @@ -77,7 +84,7 @@ protected function map() { 'entity_id' => $attachment->objectId ?? NULL, 'plan_id' => $attachment->planId ?? NULL, ], - 'custom_id' => $attachment->attachmentVersion?->value?->customId ?? ($attachment->customReference ?? NULL), + 'custom_id' => $attachment->attachmentVersion?->value?->customId ?? ($attachment->attachmentVersion?->customReference ?? NULL), 'custom_id_prefixed_refcode' => end($references), 'composed_reference' => $attachment->composedReference ?? NULL, 'description' => $attachment->attachmentVersion?->value?->description ?? NULL, @@ -95,11 +102,10 @@ protected function map() { 'monitoring_period' => $period ?? NULL, 'fields' => $prototype->getFields(), 'field_types' => $prototype->getFieldTypes(), - 'original_fields' => array_merge( - $metric_fields, - $measurement_fields, - $calculated_fields, - ), + 'original_fields' => $all_fields, + 'original_field_types' => array_map(function ($item) { + return $item->type; + }, $all_fields ?? []), 'measurement_fields' => $measurement_fields ? array_map(function ($field) { return $field->name->en; }, $measurement_fields) : [], @@ -135,6 +141,13 @@ public function getDescription() { return $this->description; } + /** + * {@inheritdoc} + */ + public function getCustomId() { + return $this->custom_id; + } + /** * Get the type of attachment. * @@ -173,15 +186,19 @@ public function getCurrentMonitoringPeriod() { } /** - * Get the original fields. - * - * @return array - * An array of field items. + * {@inheritdoc} */ public function getOriginalFields() { return $this->original_fields; } + /** + * {@inheritdoc} + */ + public function getOriginalFieldTypes() { + return $this->original_field_types; + } + /** * Get a data field by type. * diff --git a/html/modules/custom/ghi_plans/src/ApiObjects/Attachments/DataAttachmentInterface.php b/html/modules/custom/ghi_plans/src/ApiObjects/Attachments/DataAttachmentInterface.php new file mode 100644 index 000000000..b2c02b4cb --- /dev/null +++ b/html/modules/custom/ghi_plans/src/ApiObjects/Attachments/DataAttachmentInterface.php @@ -0,0 +1,34 @@ +getRawData(); + + $calculated_fields = $data->calculatedFields ?? []; + if ($calculated_fields && !is_array($calculated_fields)) { + $calculated_fields = [ + $calculated_fields->type => $calculated_fields, + ]; + } + $fields = array_merge($data->totals, $calculated_fields); + return (object) [ + 'id' => $data->attachmentId, + 'custom_id' => $data->customReference, + 'original_fields' => $fields, + 'original_field_types' => array_map(function ($item) { + return $item->type; + }, $fields), + ]; + } + + /** + * {@inheritdoc} + */ + public function getTitle() { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getCustomId() { + return $this->custom_id; + } + + /** + * {@inheritdoc} + */ + public function getOriginalFields() { + return $this->original_fields; + } + + /** + * {@inheritdoc} + */ + public function getOriginalFieldTypes() { + return $this->original_field_types; + } + +} diff --git a/html/modules/custom/ghi_plans/src/ApiObjects/Partials/PlanOverviewPlan.php b/html/modules/custom/ghi_plans/src/ApiObjects/Partials/PlanOverviewPlan.php index 9b84a8ab3..45b02c263 100644 --- a/html/modules/custom/ghi_plans/src/ApiObjects/Partials/PlanOverviewPlan.php +++ b/html/modules/custom/ghi_plans/src/ApiObjects/Partials/PlanOverviewPlan.php @@ -5,10 +5,9 @@ use Drupal\ghi_base_objects\ApiObjects\BaseObject; use Drupal\ghi_base_objects\ApiObjects\Country; use Drupal\ghi_plans\Entity\Plan; +use Drupal\ghi_plans\Traits\AttachmentFilterTrait; use Drupal\ghi_plans\Traits\PlanReportingPeriodTrait; use Drupal\ghi_plans\Traits\PlanTypeTrait; -use Drupal\hpc_api\Query\EndpointQuery; -use Drupal\hpc_common\Helpers\ArrayHelper; /** * Abstraction class for a plan partial object. @@ -21,6 +20,7 @@ class PlanOverviewPlan extends BaseObject { use PlanReportingPeriodTrait; use PlanTypeTrait; + use AttachmentFilterTrait; /** * Map the raw data. @@ -36,6 +36,9 @@ protected function map() { 'funding' => $data->funding->totalFunding ?? 0, 'requirements' => $data->requirements ? $data->requirements->revisedRequirements : 0, 'coverage' => $data->funding->progress ?? 0, + 'caseloads' => array_map(function ($item) { + return new PlanOverviewCaseload($item); + }, $data->caseLoads ?? []), ]; } @@ -285,7 +288,7 @@ public function getRequirements() { * TRUE of the plan has caseloads, FALSE otherwise. */ private function hasCaseloads() { - return !empty($this->getRawData()->caseLoads); + return !empty($this->caseloads); } /** @@ -408,16 +411,7 @@ private function getCaseloadItemByName($name) { */ public function getPlanCaseloadFields($attachment_id = NULL) { $caseload = $this->getPlanCaseload($attachment_id); - if (!$caseload) { - return []; - } - $calculated_fields = $caseload->calculatedFields ?? []; - if ($calculated_fields && !is_array($calculated_fields)) { - $calculated_fields = [ - $calculated_fields->type => $calculated_fields, - ]; - } - return array_merge($caseload->totals, $calculated_fields); + return $caseload?->getOriginalFields() ?? []; } /** @@ -426,39 +420,11 @@ public function getPlanCaseloadFields($attachment_id = NULL) { * @param int $attachment_id * Optional argument to retrieve a specific caseload. * - * @return object|null + * @return \Drupal\ghi_plans\ApiObjects\Attachments\CaseloadAttachmentInterface|null * A caseload object or NULL. */ public function getPlanCaseload($attachment_id = NULL) { - $caseload = NULL; - - $caseloads = $this->getRawData()->caseLoads ?? []; - if (empty($caseloads)) { - return $caseload; - } - - $plan_entity = $this->getEntity(); - $selected_caseload_id = $plan_entity ? $plan_entity->getPlanCaseloadId() : NULL; - - // Try to either get the requested caseload, or the one selected in the - // base object. - $attachment_id = $attachment_id ?? $selected_caseload_id; - // We have 2 options here. Either a specific attachment has been - // requested and we use that if it is part of the available attachments. - if ($attachment_id !== NULL) { - $matching_caseloads = array_filter($caseloads, function ($caseload) use ($attachment_id) { - return $caseload->attachmentId == $attachment_id; - }); - $caseload = !empty($matching_caseloads) && is_array($matching_caseloads) ? reset($matching_caseloads) : NULL; - } - - // Or we try to deduce the suitable attachment by selecting the one with - // the lowest custom reference. - if ($caseload === NULL) { - ArrayHelper::sortObjectsByProperty($caseloads, 'customReference', EndpointQuery::SORT_ASC, SORT_STRING); - $caseload = count($caseloads) ? reset($caseloads) : NULL; - } - return $caseload; + return $this->findPlanCaseload($this->caseloads, $attachment_id ?? $this->getEntity()?->getPlanCaseloadId()); } /** diff --git a/html/modules/custom/ghi_plans/src/Entity/Plan.php b/html/modules/custom/ghi_plans/src/Entity/Plan.php index c4bbac738..73d42d747 100644 --- a/html/modules/custom/ghi_plans/src/Entity/Plan.php +++ b/html/modules/custom/ghi_plans/src/Entity/Plan.php @@ -7,17 +7,20 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\ghi_base_objects\ApiObjects\Country; use Drupal\ghi_base_objects\Entity\BaseObject; +use Drupal\ghi_base_objects\Entity\BaseObjectFocusCountryInterface; use Drupal\ghi_base_objects\Entity\BaseObjectMetaDataInterface; +use Drupal\ghi_plans\Traits\AttachmentFilterTrait; use Drupal\ghi_plans\Traits\FtsLinkTrait; use Drupal\ghi_plans\Traits\PlanTypeTrait; /** * Bundle class for plan base objects. */ -class Plan extends BaseObject implements BaseObjectMetaDataInterface { +class Plan extends BaseObject implements BaseObjectMetaDataInterface, BaseObjectFocusCountryInterface { use PlanTypeTrait; use FtsLinkTrait; + use AttachmentFilterTrait; public const CLUSTER_TYPE_CLUSTER = 'cluster'; public const CLUSTER_TYPE_SECTOR = 'sector'; @@ -68,10 +71,7 @@ public function getYear() { } /** - * Get the focus country for the plan. - * - * @return \Drupal\ghi_base_objects\Entity\BaseObjectInterface|null - * The country base object or NULL. + * {@inheritdoc} */ public function getFocusCountry() { if (!$this->hasField('field_focus_country')) { @@ -81,10 +81,7 @@ public function getFocusCountry() { } /** - * Get the focus country override for the plan. - * - * @return object|null - * A latLng object or NULL. + * {@inheritdoc} */ public function getFocusCountryOverride() { if (!$this->hasField('field_focus_country_override') || $this->get('field_focus_country_override')->isEmpty()) { @@ -97,10 +94,7 @@ public function getFocusCountryOverride() { } /** - * Get the focus country map location for the plan. - * - * @return \Drupal\ghi_base_objects\ApiObjects\Country|null - * An object describing the map location or NULL. + * {@inheritdoc} */ public function getFocusCountryMapLocation(?Country $default_country = NULL) { $focus_country = $this->getFocusCountry(); @@ -353,6 +347,19 @@ public function getPlanCaseloadId() { return $this->get('field_plan_caseload')?->attachment_id ?: NULL; } + /** + * Get the plan caseload attachment. + * + * @param array $caseloads + * The caseloads to choose from. + * + * @return object|null + * A caseload object or NULL. + */ + public function getPlanCaseload(array $caseloads) { + return $this->findPlanCaseload($caseloads, $this->getPlanCaseloadId()); + } + /** * Get the document uri. * diff --git a/html/modules/custom/ghi_plans/src/Traits/AttachmentFilterTrait.php b/html/modules/custom/ghi_plans/src/Traits/AttachmentFilterTrait.php index 23dc46a6d..80cbe9ff9 100644 --- a/html/modules/custom/ghi_plans/src/Traits/AttachmentFilterTrait.php +++ b/html/modules/custom/ghi_plans/src/Traits/AttachmentFilterTrait.php @@ -2,6 +2,8 @@ namespace Drupal\ghi_plans\Traits; +use Drupal\ghi_plans\ApiObjects\Attachments\CaseloadAttachmentInterface; +use Drupal\hpc_api\Query\EndpointQuery; use Drupal\hpc_common\Helpers\ArrayHelper; /** @@ -66,4 +68,59 @@ public function filterAttachments(array $attachments, array $filter) { return ArrayHelper::filterArray($attachments, $this->prepareAttachmentFilter($filter)); } + /** + * Find a suitable plan caseload from the given list of caseloads. + * + * This is currently called from \Drupal\ghi_plans\Entity\Plan and from + * \Drupal\ghi_plans\ApiObjects\Partials with different arguments. The former + * passes in an array of first-level DataAttachment objects, whereas the + * latter passes in an array of partial attachment data coming from the plan + * overview endpoint. This function tries to handle both. + * + * @param \Drupal\ghi_plans\ApiObjects\Attachments\CaseloadAttachmentInterface[] $caseloads + * A list of caseload attachment objects. + * @param int $attachment_id + * An attachment id. + * + * @return \Drupal\ghi_plans\ApiObjects\Attachments\CaseloadAttachmentInterface|null + * A caseload object or NULL. + */ + public function findPlanCaseload(array $caseloads, $attachment_id) { + $caseload = NULL; + + $caseloads = array_filter($caseloads, function ($_caseload) { + return $_caseload instanceof CaseloadAttachmentInterface; + }); + + if (empty($caseloads)) { + return $caseload; + } + + // We have 2 options here. Either a specific attachment has been + // requested and we use that if it is part of the available attachments. + if ($attachment_id !== NULL) { + $matching_caseloads = array_filter($caseloads, function ($caseload) use ($attachment_id) { + return $caseload->id() == $attachment_id; + }); + $caseload = !empty($matching_caseloads) ? reset($matching_caseloads) : NULL; + } + + if (!$caseload) { + // Or we try to find the real plan level caseload attachment by looking + // for the ones with PiN data. + $matching_caseloads = array_filter($caseloads, function ($_caseload) { + return in_array('inNeed', $_caseload->getOriginalFieldTypes()); + }); + $caseload = !empty($matching_caseloads) ? reset($matching_caseloads) : NULL; + } + + // Or we try to deduce the suitable attachment by selecting the one with + // the lowest custom reference. + if ($caseload === NULL) { + ArrayHelper::sortObjectsByMethod($caseloads, 'getCustomId', EndpointQuery::SORT_ASC, SORT_STRING); + $caseload = count($caseloads) ? reset($caseloads) : NULL; + } + return $caseload; + } + } diff --git a/html/modules/custom/ghi_plans/translations/es.po b/html/modules/custom/ghi_plans/translations/es.po index 4474af645..104bdcb58 100644 --- a/html/modules/custom/ghi_plans/translations/es.po +++ b/html/modules/custom/ghi_plans/translations/es.po @@ -72,4 +72,25 @@ msgid "Caseload" msgstr "Datos de Población" msgid "Indicator" -msgstr "Indicador" \ No newline at end of file +msgstr "Indicador" + +msgid "Evolution of the humanitarian response" +msgstr "Evolución de la respuesta humanitaria" + +msgid "Year" +msgstr "Año" + +msgid "People in need" +msgstr "Personas en necesidad" + +msgid "People targeted" +msgstr "Población meta" + +msgid "Requirements ($)" +msgstr "Requerimientos financieros ($)" + +msgid "Funding ($)" +msgstr "Financiación ($)" + +msgid "% Funded" +msgstr "% Financiado" \ No newline at end of file diff --git a/html/modules/custom/ghi_plans/translations/fr.po b/html/modules/custom/ghi_plans/translations/fr.po index 3f8e87b9f..bd0628cb2 100644 --- a/html/modules/custom/ghi_plans/translations/fr.po +++ b/html/modules/custom/ghi_plans/translations/fr.po @@ -72,4 +72,25 @@ msgid "Caseload" msgstr "Données de population" msgid "Indicator" -msgstr "Indicateur" \ No newline at end of file +msgstr "Indicateur" + +msgid "Evolution of the humanitarian response" +msgstr "Evolution de la réponse humanitaire" + +msgid "Year" +msgstr "Année" + +msgid "People in need" +msgstr "Personnes dans le besoin" + +msgid "People targeted" +msgstr "Personnes ciblées" + +msgid "Requirements ($)" +msgstr "Besoins financiers ($)" + +msgid "Funding ($)" +msgstr "Financement ($)" + +msgid "% Funded" +msgstr "% Financé" \ No newline at end of file diff --git a/html/modules/custom/ghi_sections/src/SectionManager.php b/html/modules/custom/ghi_sections/src/SectionManager.php index 20d658ffb..256b19bbe 100644 --- a/html/modules/custom/ghi_sections/src/SectionManager.php +++ b/html/modules/custom/ghi_sections/src/SectionManager.php @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Session\AccountProxyInterface; +use Drupal\ghi_base_objects\Entity\BaseObjectFocusCountryInterface; use Drupal\ghi_base_objects\Entity\BaseObjectInterface; use Drupal\ghi_base_objects\Traits\ShortNameTrait; use Drupal\ghi_plans\Entity\Plan; @@ -258,6 +259,170 @@ public function loadSectionsForTeam(TermInterface $term) { return $sections; } + /** + * Get related sections for the given section node. + * + * @param \Drupal\ghi_sections\Entity\SectionNodeInterface $section + * The section node for which to retrieve related sections. + * + * @return \Drupal\ghi_sections\Entity\SectionNodeInterface[] + * An array of section nodes. + */ + public function getRelatedSections(SectionNodeInterface $section) { + $base_object = $section->getBaseObject(); + $sections = []; + if ($section->hasField('field_year') && !$section->get('field_year')->isEmpty()) { + // This is either a global section page or a section page with a base + // object that needs an additional year specified. + $args = array_filter([ + 'type' => $section->bundle(), + 'field_base_object' => $base_object?->id(), + ]); + $candidates = $this->entityTypeManager->getStorage($section->getEntityTypeId())->loadByProperties($args); + foreach ($candidates as $candidate) { + $year = $candidate->get('field_year')->value; + $sections[$year] = $candidate; + } + } + elseif ($base_object && $base_object->hasField('field_focus_country')) { + // This is a section page with no year but with a focus country field, + // e.g. a plan based section page. + $sections = $this->getSectionsByBaseObjectFocusCountry($section); + } + elseif ($base_object) { + // This is a section page with no year, e.g. a plan based section page. + $sections = $this->getSectionsByBaseObjectCountryReference($section); + } + + if (empty($sections)) { + return NULL; + } + + return $sections; + } + + /** + * Get related sections by a focus country on the sections base object. + * + * @param \Drupal\ghi_sections\Entity\SectionNodeInterface $section + * The section node for which to retrieve related sections. + * + * @return \Drupal\ghi_sections\Entity\SectionNodeInterface[] + * An array of section nodes keyed by the base object original id + */ + private function getSectionsByBaseObjectFocusCountry(SectionNodeInterface $section) { + $options = []; + $base_object = $section->getBaseObject(); + if (!$base_object instanceof BaseObjectFocusCountryInterface || !$base_object->getFocusCountry()) { + return $options; + } + $focus_country = $base_object->getFocusCountry(); + + // Find other object candidates that have the same focus country. + /** @var \Drupal\ghi_base_objects\Entity\BaseObjectInterface[] $base_object_candidates */ + $base_object_candidates = $this->entityTypeManager->getStorage($base_object->getEntityTypeId())->loadByProperties([ + 'type' => $base_object->bundle(), + 'field_focus_country' => $focus_country->id(), + ]); + + // If base object is a plan, thus looking for other plan base objects, + // apply filtering based on the plan type. + if ($base_object instanceof Plan) { + $base_object_candidates = array_filter($base_object_candidates, function (Plan $base_object_candidate) use ($base_object) { + // If the current base object is of type RRP, we want to retain only + // candidates that are also RRPs. If it's not an RRP, we only want + // other candiates that are not RRPs either. + return $base_object->isRrp() ? $base_object_candidate->isRrp() : !$base_object_candidate->isRrp(); + }); + } + + return $this->getSectionOptionsForBaseObjects($section, $base_object_candidates); + } + + /** + * Get related sections by a country reference on the sections base object. + * + * @param \Drupal\ghi_sections\Entity\SectionNodeInterface $section + * The section node for which to retrieve related sections. + * + * @return \Drupal\ghi_sections\Entity\SectionNodeInterface[] + * An array of section nodes keyed by the base object original id + */ + private function getSectionsByBaseObjectCountryReference(SectionNodeInterface $section) { + $options = []; + $base_object = $section->getBaseObject(); + if (!$base_object || !$base_object->hasField('field_country') || $base_object->get('field_country')->isEmpty()) { + return $options; + } + + // Get the list of all countries associated with this object. + $country_ids = array_map(function ($country) { + return $country->id(); + }, $base_object->get('field_country')->referencedEntities()); + + // Find other object candidates that have at least one of these countries + // associated. + /** @var \Drupal\ghi_base_objects\Entity\BaseObjectInterface[] $base_object_candidates */ + $base_object_candidates = $this->entityTypeManager->getStorage($base_object->getEntityTypeId())->loadByProperties([ + 'type' => $base_object->bundle(), + 'field_country' => $country_ids, + ]); + + // Then filter out the ones that don't share the full set of countries. + $base_object_candidates = array_filter($base_object_candidates, function ($base_object_candidate) use ($country_ids) { + $candidate_country_ids = array_map(function ($country) { + return $country->id(); + }, $base_object_candidate->get('field_country')->referencedEntities()); + return empty(array_diff($country_ids, $candidate_country_ids)) && count($candidate_country_ids) == count($country_ids); + }); + if (empty($base_object_candidates)) { + return $options; + } + return $this->getSectionOptionsForBaseObjects($section, $base_object_candidates); + } + + /** + * Get the section options for the given base object. + * + * @param \Drupal\ghi_sections\Entity\SectionNodeInterface $section + * The current section node. + * @param \Drupal\ghi_base_objects\Entity\BaseObjectInterface[] $base_objects + * The base objects. + * + * @return \Drupal\ghi_sections\Entity\SectionNodeInterface[] + * An array of section nodes keyed by the base object original id. + */ + private function getSectionOptionsForBaseObjects(SectionNodeInterface $section, array $base_objects) { + $base_object = $section->getBaseObject(); + // Then load the sections associated to these objects. + /** @var \Drupal\ghi_sections\Entity\SectionNodeInterface[] $section_candidates */ + $section_candidates = $this->entityTypeManager->getStorage($section->getEntityTypeId())->loadByProperties([ + 'type' => $section->bundle(), + 'field_base_object' => array_keys($base_objects), + ]); + foreach ($section_candidates as $section_candidate) { + if (!$section_candidate->access('view')) { + continue; + } + $options[$section_candidate->getBaseObject()->getSourceId()] = $section_candidate; + } + + // Sort the options. + if ($base_object->hasField('field_year')) { + // If the base object has a year field, use that for sorting. + usort($options, function ($section_a, $section_b) { + $year_a = $section_a->getBaseObject()->get('field_year')->value; + $year_b = $section_b->getBaseObject()->get('field_year')->value; + return $year_a - $year_b; + }); + } + else { + // Otherwise just use the base objects original id as a best guess. + ksort($options); + } + return array_reverse($options); + } + /** * Set the module handler service. * diff --git a/html/modules/custom/ghi_sections/tests/src/Functional/WizardTest.php b/html/modules/custom/ghi_sections/tests/src/Functional/WizardTest.php index a54e3f331..ac3b61ff9 100644 --- a/html/modules/custom/ghi_sections/tests/src/Functional/WizardTest.php +++ b/html/modules/custom/ghi_sections/tests/src/Functional/WizardTest.php @@ -71,7 +71,7 @@ private function setupContent() { $this->createBaseObjectType([ 'id' => 'plan', 'label' => 'Plan', - 'hasYear' => TRUE, + 'field_year' => 'Year', ]); $this->drupalCreateContentType([ 'type' => 'section', diff --git a/html/modules/custom/ghi_sections/tests/src/Traits/SectionTestTrait.php b/html/modules/custom/ghi_sections/tests/src/Traits/SectionTestTrait.php index b09232469..2b1ff8f07 100644 --- a/html/modules/custom/ghi_sections/tests/src/Traits/SectionTestTrait.php +++ b/html/modules/custom/ghi_sections/tests/src/Traits/SectionTestTrait.php @@ -12,6 +12,7 @@ use Drupal\ghi_sections\Entity\Section; use Drupal\ghi_sections\Entity\SectionNodeInterface; use Drupal\ghi_sections\Menu\SectionMenuStorage; +use Drupal\node\Entity\NodeType; use Drupal\taxonomy\Entity\Vocabulary; /** @@ -37,10 +38,14 @@ trait SectionTestTrait { * The created node type. */ public function createSectionType() { + if ($section_type = NodeType::load(self::SECTION_BUNDLE)) { + return $section_type; + } + $this->createBaseObjectType([ 'id' => 'plan', 'label' => 'Plan', - 'hasYear' => TRUE, + 'field_year' => 'Year', ]); // Create a team. @@ -113,6 +118,7 @@ public function createSection(array $values = []) { if (empty($values['field_base_object'])) { $base_object = $this->createBaseObject([ 'type' => 'plan', + 'field_year' => 2025, ]); $values['field_base_object'] = [ 'target_id' => $base_object->id(), diff --git a/html/modules/custom/ghi_templates/tests/src/FunctionalJavascript/PageTemplateUiTest.php b/html/modules/custom/ghi_templates/tests/src/FunctionalJavascript/PageTemplateUiTest.php index 1cb3c72b1..09335fd2b 100644 --- a/html/modules/custom/ghi_templates/tests/src/FunctionalJavascript/PageTemplateUiTest.php +++ b/html/modules/custom/ghi_templates/tests/src/FunctionalJavascript/PageTemplateUiTest.php @@ -37,7 +37,7 @@ protected function setUp(): void { $this->createBaseObjectType([ 'id' => 'plan', 'label' => 'Plan', - 'hasYear' => TRUE, + 'field_year' => 'Year', ]); $this->createLayoutBuilderContentType('section'); $handler_settings = [ diff --git a/html/modules/custom/hpc_api/src/ApiObjects/ApiObjectBase.php b/html/modules/custom/hpc_api/src/ApiObjects/ApiObjectBase.php index 825fd9b65..6cc7f846e 100644 --- a/html/modules/custom/hpc_api/src/ApiObjects/ApiObjectBase.php +++ b/html/modules/custom/hpc_api/src/ApiObjects/ApiObjectBase.php @@ -47,7 +47,7 @@ public function __construct($data) { * {@inheritdoc} */ public function id() { - return (int) $this->data->id; + return (int) $this->id ?? $this->data->id; } /** diff --git a/html/modules/custom/hpc_api/src/Helpers/ArrayHelper.php b/html/modules/custom/hpc_api/src/Helpers/ArrayHelper.php index f4caecb67..a99a15d4b 100644 --- a/html/modules/custom/hpc_api/src/Helpers/ArrayHelper.php +++ b/html/modules/custom/hpc_api/src/Helpers/ArrayHelper.php @@ -156,15 +156,11 @@ public static function sortArray(array &$data, $order, $sort, $sort_type = SORT_ * The sort direction. */ public static function sortArrayByNumericKey(array &$data, $order, $sort) { - uasort($data, function ($a, $b) use ($order, $sort) { - $a_value = !empty($a[$order]) ? $a[$order] : 0; - $b_value = !empty($b[$order]) ? $b[$order] : 0; - if ($sort == EndpointQuery::SORT_ASC) { - return $a_value - $b_value; - } - if ($sort == EndpointQuery::SORT_DESC) { - return $b_value - $a_value; - } + $sort_factor = $sort == EndpointQuery::SORT_DESC ? -1 : 1; + uasort($data, function ($a, $b) use ($order, $sort_factor) { + $a_value = $a[$order] ?? 0; + $b_value = $b[$order] ?? 0; + return $sort_factor * ($a_value - $b_value); }); } @@ -179,18 +175,11 @@ public static function sortArrayByNumericKey(array &$data, $order, $sort) { * The sort direction. */ public static function sortArrayByStringKey(array &$data, $order, $sort = EndpointQuery::SORT_ASC) { - uasort($data, function ($a, $b) use ($order, $sort) { - if (empty($a[$order]) || empty($b[$order])) { - $a_value = empty($a[$order]) ? 0 : 1; - $b_value = empty($b[$order]) ? 0 : 1; - return $sort == EndpointQuery::SORT_ASC ? $a_value - $b_value : $b_value - $a_value; - } - if ($sort == EndpointQuery::SORT_ASC) { - return strcasecmp($a[$order], $b[$order]); - } - if ($sort == EndpointQuery::SORT_DESC) { - return strcasecmp($b[$order], $a[$order]); - } + $sort_factor = $sort == EndpointQuery::SORT_DESC ? -1 : 1; + uasort($data, function ($a, $b) use ($order, $sort_factor) { + $a_value = $a[$order] ?? ''; + $b_value = $b[$order] ?? ''; + return $sort_factor * strcasecmp($a_value, $b_value); }); } @@ -207,18 +196,14 @@ public static function sortArrayByStringKey(array &$data, $order, $sort = Endpoi * The total value for the progress calculation. */ public static function sortArrayByProgress(array &$data, $order, $sort, $total) { - uasort($data, function ($a, $b) use ($sort, $order, $total) { + $sort_factor = $sort == EndpointQuery::SORT_DESC ? -1 : 1; + uasort($data, function ($a, $b) use ($sort_factor, $order, $total) { $a_funding = !empty($a[$order]) ? $a[$order] : 0; $b_funding = !empty($b[$order]) ? $b[$order] : 0; $a_value = $a_funding > 0 ? $a_funding / $total : 0; $b_value = $b_funding > 0 ? $b_funding / $total : 0; - if ($sort == EndpointQuery::SORT_ASC) { - return $a_value <=> $b_value; - } - if ($sort == EndpointQuery::SORT_DESC) { - return $a_value <=> $b_value; - } + return $sort_factor * ($a_value <=> $b_value); }); } @@ -244,34 +229,30 @@ public static function sortArrayByProgress(array &$data, $order, $sort, $total) * The sort direction. */ public static function sortArrayByCompositeArrayKey(array &$data, $order, $sort) { - uasort($data, function ($a, $b) use ($order, $sort) { - // Quick return checks. - if (empty($a[$order]) || empty($b[$order])) { - return $sort == EndpointQuery::SORT_ASC ? empty($a[$order]) > empty($b[$order]) : empty($a[$order]) < empty($b[$order]); - } - + $sort_factor = $sort == EndpointQuery::SORT_DESC ? -1 : 1; + uasort($data, function ($a, $b) use ($order, $sort_factor) { // Now prepare the data. The sort key can potentially contain multiple // entries. What we do is this: // 1. Sort the values inside the data property // 2. Use the first item inside the data property for further sorting. // // Step 1: Sorting inside the properties. - $a_item = $a[$order]; - $b_item = $b[$order]; - uasort($a_item, function ($a, $b) use ($sort) { + $a_item = $a[$order] ?? ''; + $b_item = $b[$order] ?? ''; + uasort($a_item, function ($a, $b) use ($sort_factor) { [, $a_value] = explode(':', $a); [, $b_value] = explode(':', $b); - return $sort == EndpointQuery::SORT_ASC ? strnatcasecmp($a_value, $b_value) : strnatcasecmp($b_value, $a_value); + return $sort_factor * strnatcasecmp($a_value, $b_value); }); - uasort($b_item, function ($a, $b) use ($sort) { + uasort($b_item, function ($a, $b) use ($sort_factor) { [, $a_value] = explode(':', $a); [, $b_value] = explode(':', $b); - return $sort == EndpointQuery::SORT_ASC ? strnatcasecmp($a_value, $b_value) : strnatcasecmp($b_value, $a_value); + return $sort_factor * strnatcasecmp($a_value, $b_value); }); // Step 2: Now we have prepared values to use for the actual sorting. [, $a_value] = explode(':', $a_item[0]); [, $b_value] = explode(':', $b_item[0]); - return $sort == EndpointQuery::SORT_ASC ? strnatcasecmp($a_value, $b_value) : strnatcasecmp($b_value, $a_value); + return $sort_factor * strnatcasecmp($a_value, $b_value); }); } @@ -292,7 +273,8 @@ public static function sortArrayByCompositeArrayKey(array &$data, $order, $sort) * The property to use for sorting. */ public static function sortArrayByObjectListProperty(array &$data, $order, $sort, $object_list = 'fields', $search_property = 'name', $value_property = 'value') { - usort($data, function ($a, $b) use ($order, $sort, $object_list, $search_property, $value_property) { + $sort_factor = $sort == EndpointQuery::SORT_DESC ? -1 : 1; + uasort($data, function ($a, $b) use ($order, $sort_factor, $object_list, $search_property, $value_property) { if (!empty($a[$object_list]) && !empty($b[$object_list])) { $x = ''; $y = ''; @@ -307,15 +289,7 @@ public static function sortArrayByObjectListProperty(array &$data, $order, $sort $y = $b[$object_list][$index]->$value_property; } } - if (empty($x) || empty($y)) { - return $sort == EndpointQuery::SORT_ASC ? empty($x) > empty($y) : empty($x) < empty($y); - } - if ($sort == EndpointQuery::SORT_ASC) { - return strnatcasecmp($x, $y); - } - if ($sort == EndpointQuery::SORT_DESC) { - return strnatcasecmp($y, $x); - } + return $sort_factor * strnatcasecmp($x, $y); } }); } @@ -421,28 +395,47 @@ public static function sumObjectsByProperty(array $array, $property) { return $sum; } + /** + * Sort an array of objects by the given callback function. + * + * @param array $array + * An array of objects. + * @param string $method + * The name of the method that should be used for sorting. + * @param string $sort + * The sort direction. + * @param int $sort_type + * The sort direction, either SORT_NUMERIC or SORT_STRING. + */ + public static function sortObjectsByMethod(array &$array, string $method, $sort = EndpointQuery::SORT_ASC, $sort_type = SORT_NUMERIC) { + $sort_factor = $sort == EndpointQuery::SORT_DESC ? -1 : 1; + uasort($array, function ($a, $b) use ($method, $sort_factor, $sort_type) { + $default = $sort_type == SORT_NUMERIC ? 0 : ''; + $a_value = method_exists($a, $method) ? (call_user_func([$a, $method]) ?? $default) : $default; + $b_value = method_exists($b, $method) ? (call_user_func([$b, $method]) ?? $default) : $default; + return $sort_factor * ($sort_type == SORT_NUMERIC ? $a_value - $b_value : strnatcasecmp($a_value, $b_value)); + }); + } + /** * Sort an array of objects by the given callback function. * * @param array $array * An array of objects. * @param callable $callback - * The object property that should be sorted by. + * The callback that should be used for sorting. * @param string $sort * The sort direction. * @param int $sort_type * The sort direction, either SORT_NUMERIC or SORT_STRING. */ public static function sortObjectsByCallback(array &$array, callable $callback, $sort = EndpointQuery::SORT_ASC, $sort_type = SORT_NUMERIC) { - uasort($array, function ($a, $b) use ($callback, $sort, $sort_type) { - $a_value = $callback($a) ?? NULL; - $b_value = $callback($b) ?? NULL; - if ($sort == EndpointQuery::SORT_ASC) { - return $sort_type == SORT_NUMERIC ? $a_value - $b_value : strnatcasecmp($a_value, $b_value); - } - if ($sort == EndpointQuery::SORT_DESC) { - return $sort_type == SORT_NUMERIC ? $a_value - $b_value : strnatcasecmp($b_value, $a_value); - } + $sort_factor = $sort == EndpointQuery::SORT_DESC ? -1 : 1; + uasort($array, function ($a, $b) use ($callback, $sort_factor, $sort_type) { + $default = $sort_type == SORT_NUMERIC ? 0 : ''; + $a_value = $callback($a) ?? $default; + $b_value = $callback($b) ?? $default; + return $sort_factor * ($sort_type == SORT_NUMERIC ? $a_value - $b_value : strnatcasecmp($a_value, $b_value)); }); } @@ -484,15 +477,11 @@ public static function sortObjectsByProperty(array &$array, $property, $sort = E * The sort direction. */ public static function sortObjectsByNumericProperty(array &$array, $property, $sort = EndpointQuery::SORT_ASC) { - uasort($array, function ($a, $b) use ($property, $sort) { + $sort_factor = $sort == EndpointQuery::SORT_DESC ? -1 : 1; + uasort($array, function ($a, $b) use ($property, $sort_factor) { $a_value = method_exists($a, $property) ? ($a->$property() ?? 0) : (!empty($a->$property) ? $a->$property : 0); $b_value = method_exists($b, $property) ? ($b->$property() ?? 0) : (!empty($b->$property) ? $b->$property : 0); - if ($sort == EndpointQuery::SORT_ASC) { - return $a_value <=> $b_value; - } - if ($sort == EndpointQuery::SORT_DESC) { - return $a_value <=> $b_value; - } + return $sort_factor * ($a_value - $b_value); }); } diff --git a/html/modules/custom/hpc_api/src/Query/EndpointQueryBase.php b/html/modules/custom/hpc_api/src/Query/EndpointQueryBase.php index b2fe7bc4f..4a3a3d4fa 100644 --- a/html/modules/custom/hpc_api/src/Query/EndpointQueryBase.php +++ b/html/modules/custom/hpc_api/src/Query/EndpointQueryBase.php @@ -210,7 +210,7 @@ public function setCacheTags($cache_tags = []) { */ public function getCacheTags() { $cache_tags = $this->cacheTags; - $placeholders = $this->getPlaceholders(); + $placeholders = $this->getPlaceholders() ?? []; foreach ($placeholders as $key => $value) { Cache::mergeTags($cache_tags, [$key . ':' . $value]); } diff --git a/html/modules/custom/hpc_api/tests/src/Unit/ArrayHelperTest.php b/html/modules/custom/hpc_api/tests/src/Unit/ArrayHelperTest.php new file mode 100644 index 000000000..8da908e74 --- /dev/null +++ b/html/modules/custom/hpc_api/tests/src/Unit/ArrayHelperTest.php @@ -0,0 +1,460 @@ + 1, + 'name' => 'India', + 'population' => '1.3 bn', + ]; + array_push($array, $object1); + // Object 2. + $object2 = (object) [ + 'id' => 2, + 'name' => 'Germany', + 'continent' => 'Europe', + ]; + array_push($array, $object2); + // Object 3. + $object3 = (object) [ + 'id' => 3, + 'name' => 'France', + 'population' => '40 mn', + ]; + array_push($array, $object3); + + // Expected output when filter of population is applied. + $outputWithPopulationFilter[0] = $object1; + $outputWithPopulationFilter[2] = $object3; + // Expected output when filter of continent is applied. + $outputWithContinentFilter[1] = $object2; + + return [ + [$array, ['population'], $outputWithPopulationFilter], + [$array, ['continent'], $outputWithContinentFilter], + ]; + } + + /** + * Test filter array by property. + * + * @group ArrayHelper + * @dataProvider filterArrayByPropertiesDataProvider + */ + public function testFilterArrayByProperties($array, $properties, $result) { + $this->assertEquals($result, ArrayHelper::filterArrayByProperties($array, $properties)); + } + + /** + * Data provider for filterArrayBySearchArray. + */ + public function filterArrayBySearchArrayDataProvider() { + // Prepare a mock array. + $array = [ + 0 => [ + 'field' => 'flow_property_simple', + 'property' => 'id', + ], + 1 => [ + 'field' => 'flow_property_directional', + 'object_type' => 'organizations', + 'direction' => 'source', + ], + 2 => [ + 'field' => 'flow_property_simple', + 'property' => 'description', + ], + 3 => [ + 'field' => 'flow_property_simple', + 'property' => 'amountUSD', + ], + 4 => [ + 'field' => 'flow_property_directional', + 'object_type' => 'plans', + 'direction' => 'destination', + ], + 5 => [ + 'field' => 'flow_property_directional', + 'object_type' => 'locations', + 'direction' => 'destination', + ], + ]; + + return [ + [$array, ['field' => 'flow_property_simple', 'property' => 'amountUSD'], [3]], + [$array, ['direction' => 'destination'], [4, 5]], + ]; + } + + /** + * Test filter array by search array. + * + * @group ArrayHelper + * @dataProvider filterArrayBySearchArrayDataProvider + */ + public function testFilterArrayBySearchArray($data, $search_array, $result_order) { + $result = ArrayHelper::filterArrayBySearchArray($data, $search_array); + $expected = array_combine($result_order, array_map(function ($key) use ($data) { + return $data[$key]; + }, $result_order)); + $this->assertSame($expected, $result); + } + + /** + * Data provider for sortArray. + */ + public function sortArrayDataProvider() { + $array = [ + 'apple' => [ + 'total' => 200, + 'name' => 'Apple', + ], + 'strawberry' => [ + 'total' => 500, + 'name' => 'Strawberry', + ], + 'orange' => [ + 'total' => 250, + 'name' => 'Orange', + ], + ]; + + return [ + [$array, 'total', EndpointQuery::SORT_ASC, SORT_NUMERIC, ['apple', 'orange', 'strawberry']], + [$array, 'total', EndpointQuery::SORT_DESC, SORT_NUMERIC, ['strawberry', 'orange', 'apple']], + [$array, 'name', EndpointQuery::SORT_ASC, SORT_STRING, ['apple', 'orange', 'strawberry']], + [$array, 'name', EndpointQuery::SORT_DESC, SORT_STRING, ['strawberry', 'orange', 'apple']], + ]; + } + + /** + * Test sort array. + * + * @group ArrayHelper + * @dataProvider sortArrayDataProvider + */ + public function testSortArray($data, $order, $sort, $sort_type, $result_order) { + ArrayHelper::sortArray($data, $order, $sort, $sort_type); + $expected = array_combine($result_order, array_map(function ($key) use ($data) { + return $data[$key]; + }, $result_order)); + $this->assertSame($expected, $data); + } + + /** + * Data provider for sortArrayByProgress. + */ + public function sortArrayByProgressDataProvider() { + $array = [ + 0 => ['total' => 200, 'name' => 'Apple'], + 1 => ['total' => 500, 'name' => 'Strawberry'], + 2 => ['total' => 250, 'name' => 'Orange'], + ]; + + return [ + [$array, 'total', EndpointQuery::SORT_ASC, 100, [0, 2, 1]], + [$array, 'total', EndpointQuery::SORT_DESC, 100, [1, 2, 0]], + ]; + } + + /** + * Test sort array by progress. + * + * @group ArrayHelper + * @dataProvider sortArrayByProgressDataProvider + */ + public function testSortArrayByProgress($data, $order, $sort, $total, $result_order) { + ArrayHelper::sortArrayByProgress($data, $order, $sort, $total); + $expected = array_combine($result_order, array_map(function ($key) use ($data) { + return $data[$key]; + }, $result_order)); + $this->assertSame($expected, $data); + } + + /** + * Data provider for sortArrayByCompositeArrayKey. + */ + public function sortArrayByCompositeArrayKeyDataProvider() { + $array = [ + 0 => [ + 'name' => 'WASH Emergency Rapid Response to Conflict Affected Populations', + 'organizations' => ['2178:Norwegian Refugee Council'], + 'code' => 'NGA-18/WS/122391/5834', + ], + 1 => [ + 'name' => 'Emergency shelter and Camp Management to support displaced population', + 'organizations' => ['3244:INTERSOS Humanitarian Aid Organization'], + 'code' => 'NGA-18/CCCM/120179/5660', + ], + 2 => [ + 'name' => 'Provision of Humanitarian Air Services in Nigeria', + 'organizations' => ['3049:World Food Programme'], + 'code' => 'NGA-18/LOG/122420/561', + ], + 3 => [ + 'name' => 'Provision of safe and equitable access to inclusive education', + 'organizations' => ['2915:United Nations Children Fund'], + 'code' => 'NGA-18/E/122686/124', + ], + ]; + + return [ + [$array, 'organizations', EndpointQuery::SORT_ASC, [1, 0, 3, 2]], + [$array, 'organizations', EndpointQuery::SORT_DESC, [2, 3, 0, 1]], + ]; + } + + /** + * Test sort array by composite array key. + * + * @group ArrayHelper + * @dataProvider sortArrayByCompositeArrayKeyDataProvider + */ + public function testSortArrayByCompositeArrayKey($data, $order, $sort, $result_order) { + ArrayHelper::sortArrayByCompositeArrayKey($data, $order, $sort); + $expected = array_combine($result_order, array_map(function ($key) use ($data) { + return $data[$key]; + }, $result_order)); + $this->assertSame($expected, $data); + } + + /** + * Data provider for sortArrayByObjectListProperty. + */ + public function sortArrayByObjectListPropertyDataProvider() { + $array = [ + 0 => [ + 'name' => 'WASH Emergency Rapid Response to Conflict Affected Populations', + 'fields' => [ + (object) ['name' => 'Gender Marker', 'value' => 'Mark females'], + (object) ['name' => 'Project Priority', 'value' => 'High'], + ], + ], + 1 => [ + 'name' => 'Emergency shelter and Camp Management to support displaced population', + 'fields' => [ + (object) ['name' => 'Gender Marker', 'value' => 'Highlight Males'], + (object) ['name' => 'Project Priority', 'value' => 'Low'], + ], + ], + 2 => [ + 'name' => 'Provision of Humanitarian Air Services in Nigeria', + 'fields' => [ + (object) ['name' => 'Gender Marker', 'value' => 'Not specified'], + (object) ['name' => 'Project Priority', 'value' => 'High'], + ], + ], + ]; + + return [ + [$array, 'Gender Marker', EndpointQuery::SORT_ASC, [1, 0, 2]], + [$array, 'Gender Marker', EndpointQuery::SORT_DESC, [2, 0, 1]], + ]; + } + + /** + * Test sort array by object list property. + * + * @group ArrayHelper + * @dataProvider sortArrayByObjectListPropertyDataProvider + */ + public function testSortArrayByObjectListProperty($data, $order, $sort, $result_order) { + ArrayHelper::sortArrayByObjectListProperty($data, $order, $sort); + $expected = array_combine($result_order, array_map(function ($key) use ($data) { + return $data[$key]; + }, $result_order)); + $this->assertSame($expected, $data); + } + + /** + * Data provider for findFirstItemByProperties. + */ + public function findFirstItemByPropertiesDataProvider() { + $array = [ + 0 => ['name' => 'Bill', 'surname' => 'Gates', 'country' => 'USA'], + 1 => ['name' => 'Abdul', 'surname' => 'Kalam', 'country' => 'India'], + 2 => ['name' => 'Abdul', 'surname' => 'Hafiz', 'country' => 'Pakistan'], + ]; + return [ + [$array, ['name' => 'Bill'], $array[0]], + [$array, ['name' => 'Abdul'], $array[1]], + [$array, ['name' => 'Abdul', 'country' => 'Pakistan'], $array[2]], + ]; + } + + /** + * Test find first by property. + * + * @group ArrayHelper + * @dataProvider findFirstItemByPropertiesDataProvider + */ + public function testFindFirstItemByProperties($data, $parameters, $result) { + $this->assertEquals($result, ArrayHelper::findFirstItemByProperties($data, $parameters)); + } + + /** + * Data provider for extendAssociativeArray. + */ + public function extendAssociativeArrayDataProvider() { + $array = [ + 'name' => 'Bill', + 'surname' => 'Gates', + 'country' => 'USA', + ]; + + // Result for 1st data set. + $result_1 = [ + 'name' => 'Bill', + 'surname' => 'Gates', + 'country' => 'USA', + 'company' => 'Microsoft', + ]; + + // Result for 2nd data set. + $result_2 = [ + 'name' => 'Bill', + 'surname' => 'Gates', + 'job' => 'CEO', + 'country' => 'USA', + ]; + + return [ + [$array, 'company', 'Microsoft', NULL, $result_1], + [$array, 'job', 'CEO', 2, $result_2], + ]; + } + + /** + * Test extending an associative array. + * + * @group ArrayHelper + * @dataProvider extendAssociativeArrayDataProvider + */ + public function testExtendAssociativeArray($data, $key, $value, $pos, $result) { + ArrayHelper::extendAssociativeArray($data, $key, $value, $pos); + $this->assertEquals($result, $data); + } + + /** + * Data provider for sumObjectsByProperty. + */ + public function sumObjectsByPropertyDataProvider() { + $array = [ + (object) ['item' => 'mobile', 'cost' => 1500], + (object) ['item' => 'tshirt', 'cost' => 200], + (object) ['item' => 'laptop', 'cost' => 2000], + ]; + + return [ + [$array, 'cost', 3700], + [[], 'cost', 0], + ]; + } + + /** + * Test sum objects by property. + * + * @group ArrayHelper + * @dataProvider sumObjectsByPropertyDataProvider + */ + public function testSumObjectsByProperty($data, $property, $result) { + $this->assertEquals($result, ArrayHelper::sumObjectsByProperty($data, $property)); + } + + /** + * Data provider for sortObjectsByProperty. + */ + public function sortObjectsByPropertyDataProvider() { + $array = [ + 0 => (object) ['item' => 'mobile', 'cost' => 1500], + 1 => (object) ['item' => 'tshirt', 'cost' => 200], + 2 => (object) ['item' => 'laptop', 'cost' => 2000], + ]; + + return [ + [$array, 'cost', EndpointQuery::SORT_ASC, SORT_NUMERIC, [1, 0, 2]], + [$array, 'cost', EndpointQuery::SORT_DESC, SORT_NUMERIC, [2, 0, 1]], + [$array, 'item', EndpointQuery::SORT_ASC, SORT_STRING, [2, 0, 1]], + [$array, 'item', EndpointQuery::SORT_DESC, SORT_STRING, [1, 0, 2]], + ]; + } + + /** + * Test sort objects by property. + * + * @group ArrayHelper + * @dataProvider sortObjectsByPropertyDataProvider + */ + public function testSortObjectsByProperty($data, $property, $sort, $sort_type, $result_order) { + ArrayHelper::sortObjectsByProperty($data, $property, $sort, $sort_type); + $expected = array_combine($result_order, array_map(function ($key) use ($data) { + return $data[$key]; + }, $result_order)); + $this->assertSame($expected, $data); + } + + /** + * Data provider for sortObjectsByMethod. + */ + public function sortObjectsByMethodDataProvider() { + $class = function ($item, $cost) { + // @codingStandardsIgnoreStart + return new class ($item, $cost) { + private $item; + private $cost; + public function __construct($item, $cost) { + $this->item = $item; + $this->cost = $cost; + } + public function getCost() { return $this->cost; } + public function getItem() { return $this->item; } + }; + // @codingStandardsIgnoreEnd + }; + + $array = [ + 0 => $class('mobile', 1500), + 1 => $class('tshirt', 200), + 2 => $class('laptop', 2000), + ]; + + return [ + [$array, 'getCost', EndpointQuery::SORT_ASC, SORT_NUMERIC, [1, 0, 2]], + [$array, 'getCost', EndpointQuery::SORT_DESC, SORT_NUMERIC, [2, 0, 1]], + [$array, 'getItem', EndpointQuery::SORT_ASC, SORT_STRING, [2, 0, 1]], + [$array, 'getItem', EndpointQuery::SORT_DESC, SORT_STRING, [1, 0, 2]], + ]; + } + + /** + * Test sort objects by property. + * + * @group ArrayHelper + * @dataProvider sortObjectsByMethodDataProvider + */ + public function testSortObjectsByMethod($data, $method, $sort, $sort_type, $result_order) { + ArrayHelper::sortObjectsByMethod($data, $method, $sort, $sort_type); + $expected = array_combine($result_order, array_map(function ($key) use ($data) { + return $data[$key]; + }, $result_order)); + $this->assertSame($expected, $data); + } + +} diff --git a/html/modules/custom/hpc_common/src/Plugin/HPCBlockBase.php b/html/modules/custom/hpc_common/src/Plugin/HPCBlockBase.php index a9b377d0a..86de8bfb4 100644 --- a/html/modules/custom/hpc_common/src/Plugin/HPCBlockBase.php +++ b/html/modules/custom/hpc_common/src/Plugin/HPCBlockBase.php @@ -7,6 +7,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\hpc_api\Query\EndpointQueryPluginInterface; use Drupal\hpc_common\Helpers\ContextHelper; use Drupal\hpc_common\Helpers\RequestHelper; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; @@ -119,6 +120,13 @@ abstract class HPCBlockBase extends BlockBase implements HPCPluginInterface, Con */ protected $fileSystem; + /** + * Stores instantiated query handlers. + * + * @var \Drupal\hpc_api\Query\EndpointQueryPluginInterface[] + */ + protected $queryHandlers = []; + /** * {@inheritdoc} */ @@ -617,7 +625,9 @@ protected function baseConfigurationDefaults() { * The query handler class. */ protected function getQueryHandler($source_key = 'data') { - + if (!empty($this->queryHandlers[$source_key])) { + return $this->queryHandlers[$source_key]; + } $configuration = $this->getPluginDefinition(); if (empty($configuration['data_sources'])) { return NULL; @@ -658,10 +668,22 @@ protected function getQueryHandler($source_key = 'data') { } } } - + $this->queryHandlers[$source_key] = $query_handler; return $query_handler; } + /** + * Set a query handler plugin to be used for the given source key. + * + * @param string $source_key + * The source key for which the given query plugin should be used. + * @param \Drupal\hpc_api\Query\EndpointQueryPluginInterface $query_handler + * The query plugin. + */ + public function setQueryHandler($source_key, EndpointQueryPluginInterface $query_handler) { + $this->queryHandlers[$source_key] = $query_handler; + } + /** * Get data for this block. * diff --git a/html/modules/custom/hpc_common/tests/src/Unit/ArrayHelperTest.php b/html/modules/custom/hpc_common/tests/src/Unit/ArrayHelperTest.php index 1f29a9e11..9f78ce7f1 100644 --- a/html/modules/custom/hpc_common/tests/src/Unit/ArrayHelperTest.php +++ b/html/modules/custom/hpc_common/tests/src/Unit/ArrayHelperTest.php @@ -3,7 +3,6 @@ namespace Drupal\Tests\hpc_common\Unit; use Drupal\Tests\UnitTestCase; -use Drupal\hpc_api\Query\EndpointQuery; use Drupal\hpc_common\Helpers\ArrayHelper; /** @@ -12,627 +11,125 @@ class ArrayHelperTest extends UnitTestCase { /** - * The array helper class. - * - * @var \Drupal\hpc_common\Helpers\ArrayHelper - */ - protected $arrayHelper; - - /** - * {@inheritdoc} - */ - protected function setUp(): void { - parent::setUp(); - $this->arrayHelper = new ArrayHelper(); - } - - /** - * {@inheritdoc} - */ - protected function tearDown(): void { - parent::tearDown(); - unset($this->arrayHelper); - } - - /** - * Data provider for filterArrayByProperties. - */ - public function filterArrayByPropertiesDataProvider() { - $array = []; - $outputWithPopulationFilter = []; - $outputWithContinentFilter = []; - // Object 1. - $object1 = (object) [ - 'id' => 1, - 'name' => 'India', - 'population' => '1.3 bn', - ]; - array_push($array, $object1); - // Object 2. - $object2 = (object) [ - 'id' => 2, - 'name' => 'Germany', - 'continent' => 'Europe', - ]; - array_push($array, $object2); - // Object 3. - $object3 = (object) [ - 'id' => 3, - 'name' => 'France', - 'population' => '40 mn', - ]; - array_push($array, $object3); - - // Expected output when filter of population is applied. - $outputWithPopulationFilter[0] = $object1; - $outputWithPopulationFilter[2] = $object3; - // Expected output when filter of continent is applied. - $outputWithContinentFilter[1] = $object2; - - return [ - [$array, ['population'], $outputWithPopulationFilter], - [$array, ['continent'], $outputWithContinentFilter], - ]; - } - - /** - * Test filter array by property. - * - * @group ArrayHelper - * @dataProvider filterArrayByPropertiesDataProvider - */ - public function testFilterArrayByProperties($array, $properties, $result) { - $this->assertEquals($result, $this->arrayHelper->filterArrayByProperties($array, $properties)); - } - - /** - * Data provider for filterArrayBySearchArray. - */ - public function filterArrayBySearchArrayDataProvider() { - // Prepare a mock array. - $array = [ - [ - 'field' => 'flow_property_simple', - 'property' => 'id', - ], - [ - 'field' => 'flow_property_directional', - 'object_type' => 'organizations', - 'direction' => 'source', - ], - [ - 'field' => 'flow_property_simple', - 'property' => 'description', - ], - [ - 'field' => 'flow_property_simple', - 'property' => 'amountUSD', - ], - [ - 'field' => 'flow_property_directional', - 'object_type' => 'plans', - 'direction' => 'destination', - ], - [ - 'field' => 'flow_property_directional', - 'object_type' => 'locations', - 'direction' => 'destination', - ], - ]; - - // Prepare mock search array. - $search_array_1 = [ - 'field' => 'flow_property_simple', - 'property' => 'amountUSD', - ]; - - // Prepare mock result for the above search. - $result_1 = [ - 3 => [ - 'field' => 'flow_property_simple', - 'property' => 'amountUSD', - ], - ]; - - // Prepare mock search array. - $search_array_2 = ['direction' => 'destination']; - - // Prepare mock result for the above search. - $result_2 = [ - 4 => [ - 'field' => 'flow_property_directional', - 'object_type' => 'plans', - 'direction' => 'destination', - ], - 5 => [ - 'field' => 'flow_property_directional', - 'object_type' => 'locations', - 'direction' => 'destination', - ], - ]; - - return [ - [$array, $search_array_1, $result_1], - [$array, $search_array_2, $result_2], - ]; - } - - /** - * Test filter array by search array. - * - * @group ArrayHelper - * @dataProvider filterArrayBySearchArrayDataProvider - */ - public function testFilterArrayBySearchArray($array, $search_array, $result) { - $this->assertEquals($result, $this->arrayHelper->filterArrayBySearchArray($array, $search_array)); - } - - /** - * Data provider for sortArray. - */ - public function sortArrayDataProvider() { - $array = [ - 'apple' => [ - 'total' => 200, - 'name' => 'Apple', - ], - 'strawberry' => [ - 'total' => 500, - 'name' => 'Strawberry', - ], - 'orange' => [ - 'total' => 250, - 'name' => 'Orange', - ], - ]; - - // Result sorted by total. - $result_1 = [ - 'apple' => $array['apple'], - 'orange' => $array['orange'], - 'strawberry' => $array['strawberry'], - ]; - $result_2 = array_reverse($result_1, TRUE); - - // Result sorted by name. - $result_3 = [ - 'apple' => $array['apple'], - 'orange' => $array['orange'], - 'strawberry' => $array['strawberry'], - ]; - $result_4 = array_reverse($result_3); - - return [ - [$array, 'total', EndpointQuery::SORT_ASC, SORT_NUMERIC, $result_1], - [$array, 'total', EndpointQuery::SORT_DESC, SORT_NUMERIC, $result_2], - [$array, 'name', EndpointQuery::SORT_ASC, SORT_STRING, $result_3], - [$array, 'name', EndpointQuery::SORT_DESC, SORT_STRING, $result_4], - ]; - } - - /** - * Test sort array. - * - * @group ArrayHelper - * @dataProvider sortArrayDataProvider - */ - public function testSortArray($data, $order, $sort, $sort_type, $result) { - $this->arrayHelper->sortArray($data, $order, $sort, $sort_type); - $this->assertEquals($result, $data); - } - - /** - * Data provider for sortArrayByProgress. - */ - public function sortArrayByProgressDataProvider() { - $array = [ - ['total' => 200, 'name' => 'Apple'], - ['total' => 500, 'name' => 'Strawberry'], - ['total' => 250, 'name' => 'Orange'], - ]; - - // Result for 1st set of options. - $result_1 = [ - 0 => ['total' => 200, 'name' => 'Apple'], - 2 => ['total' => 250, 'name' => 'Orange'], - 1 => ['total' => 500, 'name' => 'Strawberry'], - ]; - - // Result for 2nd set of options. - $result_2 = [ - 1 => ['total' => 500, 'name' => 'Strawberry'], - 2 => ['total' => 250, 'name' => 'Orange'], - 0 => ['total' => 200, 'name' => 'Apple'], - ]; - - return [ - [$array, 'total', EndpointQuery::SORT_ASC, 100, $result_1], - [$array, 'total', EndpointQuery::SORT_DESC, 100, $result_2], - ]; - } - - /** - * Test sort array by progress. - * - * @group ArrayHelper - * @dataProvider sortArrayByProgressDataProvider - */ - public function testSortArrayByProgress($data, $order, $sort, $total, $result) { - $this->arrayHelper->sortArrayByProgress($data, $order, $sort, $total); - $this->assertEquals($result, $data); - } - - /** - * Data provider for sortArrayByCompositeArrayKey. + * Data provider for testSwapArray. */ - public function sortArrayByCompositeArrayKeyDataProvider() { + public function swapArrayDataProvider() { $array = [ - 0 => [ - 'name' => 'WASH Emergency Rapid Response to Conflict Affected Populations', - 'organizations' => ['2178:Norwegian Refugee Council'], - 'code' => 'NGA-18/WS/122391/5834', - ], - 1 => [ - 'name' => 'Emergency shelter and Camp Management to support displaced population', - 'organizations' => ['3244:INTERSOS Humanitarian Aid Organization'], - 'code' => 'NGA-18/CCCM/120179/5660', - ], - 2 => [ - 'name' => 'Provision of Humanitarian Air Services in Nigeria', - 'organizations' => ['3049:World Food Programme'], - 'code' => 'NGA-18/LOG/122420/561', - ], - 3 => [ - 'name' => 'Provision of safe and equitable access to inclusive education', - 'organizations' => ['2915:United Nations Children Fund'], - 'code' => 'NGA-18/E/122686/124', - ], - ]; - - // Result for 1st data set. - $result_1 = [ - 1 => [ - 'name' => 'Emergency shelter and Camp Management to support displaced population', - 'organizations' => ['3244:INTERSOS Humanitarian Aid Organization'], - 'code' => 'NGA-18/CCCM/120179/5660', - ], - 0 => [ - 'name' => 'WASH Emergency Rapid Response to Conflict Affected Populations', - 'organizations' => ['2178:Norwegian Refugee Council'], - 'code' => 'NGA-18/WS/122391/5834', - ], - 3 => [ - 'name' => 'Provision of safe and equitable access to inclusive education', - 'organizations' => ['2915:United Nations Children Fund'], - 'code' => 'NGA-18/E/122686/124', - ], - 2 => [ - 'name' => 'Provision of Humanitarian Air Services in Nigeria', - 'organizations' => ['3049:World Food Programme'], - 'code' => 'NGA-18/LOG/122420/561', - ], - ]; - - // Result for 2nd data set. - $result_2 = [ - 2 => [ - 'name' => 'Provision of Humanitarian Air Services in Nigeria', - 'organizations' => ['3049:World Food Programme'], - 'code' => 'NGA-18/LOG/122420/561', - ], - 3 => [ - 'name' => 'Provision of safe and equitable access to inclusive education', - 'organizations' => ['2915:United Nations Children Fund'], - 'code' => 'NGA-18/E/122686/124', - ], - 0 => [ - 'name' => 'WASH Emergency Rapid Response to Conflict Affected Populations', - 'organizations' => ['2178:Norwegian Refugee Council'], - 'code' => 'NGA-18/WS/122391/5834', - ], - 1 => [ - 'name' => 'Emergency shelter and Camp Management to support displaced population', - 'organizations' => ['3244:INTERSOS Humanitarian Aid Organization'], - 'code' => 'NGA-18/CCCM/120179/5660', - ], + 1 => 'one', + 2 => 'two', + 'one' => 'second one', + 'two' => 'second two', ]; return [ - [$array, 'organizations', EndpointQuery::SORT_ASC, $result_1], - [$array, 'organizations', EndpointQuery::SORT_DESC, $result_2], + [$array, 1, 2, FALSE, [2, 1, 'one', 'two'], NULL], + [$array, 1, 'one', FALSE, ['one', 2, 1, 'two'], NULL], + [$array, 1, 3, FALSE, [1, 2, 'one', 'two'], FALSE], + [$array, '1', 2, TRUE, [1, 2, 'one', 'two'], FALSE], + [$array, 1, '2', TRUE, [1, 2, 'one', 'two'], FALSE], ]; } /** - * Test sort array by composite array key. + * Test swap array function. * * @group ArrayHelper - * @dataProvider sortArrayByCompositeArrayKeyDataProvider + * @dataProvider swapArrayDataProvider */ - public function testSortArrayByCompositeArrayKey($data, $order, $sort, $result) { - $this->arrayHelper->sortArrayByCompositeArrayKey($data, $order, $sort); - $this->assertEquals($result, $data); + public function testSwapArray($data, $key1, $key2, $strict, $result_order, $return_value) { + $this->assertEquals($return_value, ArrayHelper::swap($data, $key1, $key2, $strict)); + $expected = array_combine($result_order, array_map(function ($key) use ($data) { + return $data[$key]; + }, $result_order)); + $this->assertEquals($expected, $data); } /** - * Data provider for sortArrayByObjectListProperty. - */ - public function sortArrayByObjectListPropertyDataProvider() { - $array = [ - 0 => [ - 'name' => 'WASH Emergency Rapid Response to Conflict Affected Populations', - 'fields' => [ - (object) ['name' => 'Gender Marker', 'value' => 'Mark females'], - (object) ['name' => 'Project Priority', 'value' => 'High'], - ], - ], - 1 => [ - 'name' => 'Emergency shelter and Camp Management to support displaced population', - 'fields' => [ - (object) ['name' => 'Gender Marker', 'value' => 'Highlight Males'], - (object) ['name' => 'Project Priority', 'value' => 'Low'], - ], - ], - 2 => [ - 'name' => 'Provision of Humanitarian Air Services in Nigeria', - 'fields' => [ - (object) ['name' => 'Gender Marker', 'value' => 'Not specified'], - (object) ['name' => 'Project Priority', 'value' => 'High'], - ], - ], - ]; - - // Result for 1st data set. - $result_1 = [ - 0 => [ - 'name' => 'Emergency shelter and Camp Management to support displaced population', - 'fields' => [ - (object) ['name' => 'Gender Marker', 'value' => 'Highlight Males'], - (object) ['name' => 'Project Priority', 'value' => 'Low'], - ], - ], - 1 => [ - 'name' => 'WASH Emergency Rapid Response to Conflict Affected Populations', - 'fields' => [ - (object) ['name' => 'Gender Marker', 'value' => 'Mark females'], - (object) ['name' => 'Project Priority', 'value' => 'High'], - ], - ], - 2 => [ - 'name' => 'Provision of Humanitarian Air Services in Nigeria', - 'fields' => [ - (object) ['name' => 'Gender Marker', 'value' => 'Not specified'], - (object) ['name' => 'Project Priority', 'value' => 'High'], - ], - ], - ]; - - // Result for 2nd data set. - $result_2 = [ - 0 => [ - 'name' => 'Provision of Humanitarian Air Services in Nigeria', - 'fields' => [ - (object) ['name' => 'Gender Marker', 'value' => 'Not specified'], - (object) ['name' => 'Project Priority', 'value' => 'High'], - ], - ], - 1 => [ - 'name' => 'WASH Emergency Rapid Response to Conflict Affected Populations', - 'fields' => [ - (object) ['name' => 'Gender Marker', 'value' => 'Mark females'], - (object) ['name' => 'Project Priority', 'value' => 'High'], - ], - ], - 2 => [ - 'name' => 'Emergency shelter and Camp Management to support displaced population', - 'fields' => [ - (object) ['name' => 'Gender Marker', 'value' => 'Highlight Males'], - (object) ['name' => 'Project Priority', 'value' => 'Low'], - ], - ], - ]; - - return [ - [$array, 'Gender Marker', EndpointQuery::SORT_ASC, $result_1], - [$array, 'Gender Marker', EndpointQuery::SORT_DESC, $result_2], - ]; - } - - /** - * Test sort array by object list property. + * Test arrayMapAssoc function. * * @group ArrayHelper - * @dataProvider sortArrayByObjectListPropertyDataProvider */ - public function testSortArrayByObjectListProperty($data, $order, $sort, $result) { - $this->arrayHelper->sortArrayByObjectListProperty($data, $order, $sort); - $this->assertEquals($result, $data); - } - - /** - * Data provider for findFirstItemByProperties. - */ - public function findFirstItemByPropertiesDataProvider() { + public function testArrayMapAssoc() { $array = [ - [ - 'name' => 'Bill', - 'surname' => 'Gates', - 'country' => 'USA', - ], - [ - 'name' => 'Abdul', - 'surname' => 'Kalam', - 'country' => 'India', - ], - [ - 'name' => 'Abdul', - 'surname' => 'Hafiz', - 'country' => 'Pakistan', - ], + 6 => ['six'], + 2 => ['two'], + 3 => ['three'], + 10 => ['ten'], ]; - - // Result of 1st data set. - $result_1 = [ - 'name' => 'Bill', - 'surname' => 'Gates', - 'country' => 'USA', - ]; - - // Result of 2nd data set. - $result_2 = [ - 'name' => 'Abdul', - 'surname' => 'Kalam', - 'country' => 'India', - ]; - - // Result of 3rd data set. - $result_3 = [ - 'name' => 'Abdul', - 'surname' => 'Hafiz', - 'country' => 'Pakistan', - ]; - - return [ - [$array, ['name' => 'Bill'], $result_1], - [$array, ['name' => 'Abdul'], $result_2], - [$array, ['name' => 'Abdul', 'country' => 'Pakistan'], $result_3], + $result = ArrayHelper::arrayMapAssoc(function ($item) { + return $item[0]; + }, $array); + $expected = [ + 6 => 'six', + 2 => 'two', + 3 => 'three', + 10 => 'ten', ]; + $this->assertSame($expected, $result); } /** - * Test find first by property. + * Test mapObjectsToString function. * * @group ArrayHelper - * @dataProvider findFirstItemByPropertiesDataProvider - */ - public function testFindFirstItemByProperties($data, $parameters, $result) { - $this->assertEquals($result, $this->arrayHelper->findFirstItemByProperties($data, $parameters)); - } - - /** - * Data provider for extendAssociativeArray. */ - public function extendAssociativeArrayDataProvider() { + public function testMapObjectsToString() { + $class = function ($value) { + // @codingStandardsIgnoreStart + return new class ($value) { + private $value; + public function __construct($value) { + $this->value = $value; + } + public function __toString() { return $this->value; } + }; + // @codingStandardsIgnoreEnd + }; $array = [ - 'name' => 'Bill', - 'surname' => 'Gates', - 'country' => 'USA', + 6 => [6 => 'six', 5 => $class('eleven'), 9 => ['one', 'three', $class('two')]], + 2 => [2 => 'two', 7 => 'seven', 5 => 'five'], ]; - - // Result for 1st data set. - $result_1 = [ - 'name' => 'Bill', - 'surname' => 'Gates', - 'country' => 'USA', - 'company' => 'Microsoft', - ]; - - // Result for 2nd data set. - $result_2 = [ - 'name' => 'Bill', - 'surname' => 'Gates', - 'job' => 'CEO', - 'country' => 'USA', - ]; - - return [ - [$array, 'company', 'Microsoft', NULL, $result_1], - [$array, 'job', 'CEO', 2, $result_2], + $expected = [ + 6 => [6 => 'six', 5 => 'eleven', 9 => ['one', 'three', 'two']], + 2 => [2 => 'two', 7 => 'seven', 5 => 'five'], ]; + $this->assertSame($expected, ArrayHelper::mapObjectsToString($array)); } /** - * Test extending an associative array. + * Test sortMultiDimensionalArrayByKeys function. * * @group ArrayHelper - * @dataProvider extendAssociativeArrayDataProvider */ - public function testExtendAssociativeArray($data, $key, $value, $pos, $result) { - $this->arrayHelper->extendAssociativeArray($data, $key, $value, $pos); - $this->assertEquals($result, $data); - } - - /** - * Data provider for sumObjectsByProperty. - */ - public function sumObjectsByPropertyDataProvider() { + public function testSortMultiDimensionalArrayByKeys() { $array = [ - (object) ['item' => 'mobile', 'cost' => 1500], - (object) ['item' => 'tshirt', 'cost' => 200], - (object) ['item' => 'laptop', 'cost' => 2000], + 6 => [6 => 'six', 5 => 'five', 9 => ['one', 'three', 'two']], + 2 => [2 => 'two', 7 => 'seven', 5 => 'five'], ]; - - return [ - [$array, 'cost', 3700], - [[], 'cost', 0], + $expected = [ + 2 => [2 => 'two', 5 => 'five', 7 => 'seven'], + 6 => [5 => 'five', 6 => 'six', 9 => ['one', 'three', 'two']], ]; + ArrayHelper::sortMultiDimensionalArrayByKeys($array); + $this->assertSame($expected, $array); } /** - * Test sum objects by property. + * Test reduceArray function. * * @group ArrayHelper - * @dataProvider sumObjectsByPropertyDataProvider - */ - public function testSumObjectsByProperty($data, $property, $result) { - $this->assertEquals($result, $this->arrayHelper->sumObjectsByProperty($data, $property)); - } - - /** - * Data provider for sortObjectsByProperty. */ - public function sortObjectsByPropertyDataProvider() { + public function testReduceArray() { $array = [ - 0 => (object) ['item' => 'mobile', 'cost' => 1500], - 1 => (object) ['item' => 'tshirt', 'cost' => 200], - 2 => (object) ['item' => 'laptop', 'cost' => 2000], - ]; - - // Result for 1st data set. - $result_1 = [ - 1 => (object) ['item' => 'tshirt', 'cost' => 200], - 0 => (object) ['item' => 'mobile', 'cost' => 1500], - 2 => (object) ['item' => 'laptop', 'cost' => 2000], + 6 => [6 => 'six', 5 => 0, 9 => [], 10 => [1 => 'one', 2 => FALSE]], + 2 => [2 => 'two', 7 => NULL, 5 => 'five'], ]; - - // Result for 2nd data set. - $result_2 = [ - 2 => (object) ['item' => 'laptop', 'cost' => 2000], - 0 => (object) ['item' => 'mobile', 'cost' => 1500], - 1 => (object) ['item' => 'tshirt', 'cost' => 200], - ]; - - // Result for 3rd data set. - $result_3 = [ - 2 => (object) ['item' => 'laptop', 'cost' => 2000], - 0 => (object) ['item' => 'mobile', 'cost' => 1500], - 1 => (object) ['item' => 'tshirt', 'cost' => 200], - ]; - - // Result for 4th data set. - $result_4 = [ - 1 => (object) ['item' => 'tshirt', 'cost' => 200], - 0 => (object) ['item' => 'mobile', 'cost' => 1500], - 2 => (object) ['item' => 'laptop', 'cost' => 2000], + $expected = [ + 6 => [6 => 'six', 10 => [1 => 'one']], + 2 => [2 => 'two', 5 => 'five'], ]; - - return [ - [$array, 'cost', EndpointQuery::SORT_ASC, SORT_NUMERIC, $result_1], - [$array, 'cost', EndpointQuery::SORT_DESC, SORT_NUMERIC, $result_2], - [$array, 'item', EndpointQuery::SORT_ASC, SORT_STRING, $result_3], - [$array, 'item', EndpointQuery::SORT_DESC, SORT_STRING, $result_4], - ]; - } - - /** - * Test sort objects by property. - * - * @group ArrayHelper - * @dataProvider sortObjectsByPropertyDataProvider - */ - public function testSortObjectsByProperty($data, $property, $sort, $sort_type, $result) { - $this->arrayHelper->sortObjectsByProperty($data, $property, $sort, $sort_type); - $this->assertEquals($result, $data); + ArrayHelper::reduceArray($array); + $this->assertSame($expected, $array); } } diff --git a/html/modules/custom/hpc_common/tests/src/Unit/ThemeHelperTest.php b/html/modules/custom/hpc_common/tests/src/Unit/ThemeHelperTest.php index c90ba01b0..92982761d 100644 --- a/html/modules/custom/hpc_common/tests/src/Unit/ThemeHelperTest.php +++ b/html/modules/custom/hpc_common/tests/src/Unit/ThemeHelperTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\hpc_common\Unit; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Extension\ExtensionPathResolver; use Drupal\Core\Render\RendererInterface; use Drupal\Tests\UnitTestCase; use Drupal\hpc_common\Helpers\ThemeHelper; @@ -16,13 +17,6 @@ class ThemeHelperTest extends UnitTestCase { use ProphecyTrait; - /** - * The theme helper class. - * - * @var \Drupal\hpc_common\Helpers\ThemeHelper - */ - protected $themeHelper; - /** * {@inheritdoc} */ @@ -32,6 +26,8 @@ protected function setUp(): void { // Mock renderer service. $renderer = $this->prophesize(RendererInterface::class); $twig = $this->prophesize(Environment::class); + $path_resolver = $this->prophesize(ExtensionPathResolver::class); + $path_resolver->getPath('module', 'hpc_common')->willReturn('path'); // Mock render. $build = [ @@ -47,9 +43,9 @@ protected function setUp(): void { $container = new ContainerBuilder(); $container->set('renderer', $renderer->reveal()); $container->set('twig', $twig->reveal()); + $container->set('extension.path.resolver', $path_resolver->reveal()); + $container->set('string_translation', $this->getStringTranslationStub()); \Drupal::setContainer($container); - - $this->themeHelper = new ThemeHelper(); } /** @@ -57,14 +53,13 @@ protected function setUp(): void { */ protected function tearDown(): void { parent::tearDown(); - unset($this->themeHelper); $container = new ContainerBuilder(); \Drupal::setContainer($container); } /** - * Data provider for theme. + * Data provider for testTheme. */ public function themeDataProvider() { return [ @@ -105,7 +100,146 @@ public function themeDataProvider() { * @dataProvider themeDataProvider */ public function testTheme($theme_key, $options, $cast_to_string, $xss_filter, $result) { - $this->assertEquals($result, $this->themeHelper->theme($theme_key, $options, $cast_to_string, $xss_filter)); + $this->assertEquals($result, ThemeHelper::theme($theme_key, $options, $cast_to_string, $xss_filter)); + } + + /** + * Data provider for testGetThemeOptions. + */ + public function getThemeOptionsDataProvider() { + // @codingStandardsIgnoreStart + $items = [ + // Amount. + ['hpc_amount', 100, [], [ + '#theme' => 'hpc_amount', + '#amount' => 100, + '#scale' => 'auto', + '#decimal_format' => 'point', + '#decimals' => 0, + ]], + ['hpc_amount', 100, ['scale' => 'million'], [ + '#theme' => 'hpc_amount', + '#amount' => 100, + '#scale' => 'million', + '#decimal_format' => 'point', + '#decimals' => 0, + ]], + ['hpc_amount', 100, ['decimal_format' => 'comma'], [ + '#theme' => 'hpc_amount', + '#amount' => 100, + '#scale' => 'auto', + '#decimal_format' => 'comma', + '#decimals' => 0, + ]], + ['hpc_amount', 100, ['decimals' => '1'], [ + '#theme' => 'hpc_amount', + '#amount' => 100, + '#scale' => 'auto', + '#decimal_format' => 'point', + '#decimals' => 1, + ]], + // Currency. + ['hpc_currency', 100, [], [ + '#theme' => 'hpc_currency', + '#value' => 100, + '#scale' => 'auto', + '#decimal_format' => 'point', + '#decimals' => 0, + ]], + ['hpc_currency', 100, ['scale' => 'million'], [ + '#theme' => 'hpc_currency', + '#value' => 100, + '#scale' => 'million', + '#decimal_format' => 'point', + '#decimals' => 0, + ]], + ['hpc_currency', 100, ['decimal_format' => 'comma'], [ + '#theme' => 'hpc_currency', + '#value' => 100, + '#scale' => 'auto', + '#decimal_format' => 'comma', + '#decimals' => 0, + ]], + ['hpc_currency', 100, ['decimals' => '1'], [ + '#theme' => 'hpc_currency', + '#value' => 100, + '#scale' => 'auto', + '#decimal_format' => 'point', + '#decimals' => 1, + ]], + // Percent. + ['hpc_percent', 100, [], [ + '#theme' => 'hpc_percent', + '#percent' => 100, + '#decimal_format' => 'point', + ]], + ['hpc_percent', 100, ['decimal_format' => 'comma'], [ + '#theme' => 'hpc_percent', + '#percent' => 100, + '#decimal_format' => 'comma', + ]], + // Progress bar. + ['hpc_progress_bar', 100, [], [ + '#theme' => 'hpc_progress_bar', + '#percent' => 100, + '#hide_value' => FALSE, + ]], + ['hpc_progress_bar', 100, ['hide_value' => TRUE], [ + '#theme' => 'hpc_progress_bar', + '#percent' => 100, + '#hide_value' => TRUE, + ]], + // Invalid theme argument. + ['unknown_theme_function', 100, [], new \InvalidArgumentException('Unknown theme function "unknown_theme_function"')], + ]; + // @codingStandardsIgnoreEnd + return $items; + } + + /** + * Test calling the theme function. + * + * @group ThemeHelper + * @dataProvider getThemeOptionsDataProvider + */ + public function testGetThemeOptions($theme_function, $value, $options, $expected) { + if ($expected instanceof \Exception) { + $this->expectExceptionObject($expected); + } + $build = ThemeHelper::getThemeOptions($theme_function, $value, $options); + $this->assertEquals($expected, $build); + } + + /** + * Test the getNumberSuffix function. + * + * @group ThemeHelper + */ + public function testGetNumberSuffix() { + $this->assertEquals('k', ThemeHelper::getNumberSuffix('thousand')); + $this->assertEquals(' thousand', ThemeHelper::getNumberSuffix('thousand', FALSE)); + $this->assertEquals('m', ThemeHelper::getNumberSuffix('million')); + $this->assertEquals(' million', ThemeHelper::getNumberSuffix('million', FALSE)); + $this->assertEquals('bn', ThemeHelper::getNumberSuffix('billion')); + $this->assertEquals(' billion', ThemeHelper::getNumberSuffix('billion', FALSE)); + $this->assertEquals('', ThemeHelper::getNumberSuffix('random_string')); + $this->assertEquals('', ThemeHelper::getNumberSuffix('random_string', FALSE)); + } + + /** + * Test the themeFtsIcon function. + * + * @group ThemeHelper + */ + public function testThemeFtsIcon() { + $expected = [ + '#theme' => 'image', + '#uri' => '/path/assets/fts-logo-mobile.png', + '#attributes' => [ + 'class' => 'fts-icon', + ], + ]; + $this->assertEquals($expected, ThemeHelper::themeFtsIcon()); } }