From 1b6c870463a9c241545291048b2b8ec74ea26146 Mon Sep 17 00:00:00 2001 From: Alex Finnarn Date: Wed, 17 Jan 2024 10:42:15 -0500 Subject: [PATCH 1/9] add event subscriber to include situation updates for full banner width alerts --- .../FullWidthBannerAlertSubscriber.php | 89 +++++++++++++++++++ .../custom/va_gov_api/va_gov_api.services.yml | 11 ++- 2 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 docroot/modules/custom/va_gov_api/src/EventSubscriber/FullWidthBannerAlertSubscriber.php diff --git a/docroot/modules/custom/va_gov_api/src/EventSubscriber/FullWidthBannerAlertSubscriber.php b/docroot/modules/custom/va_gov_api/src/EventSubscriber/FullWidthBannerAlertSubscriber.php new file mode 100644 index 0000000000..7c8e461648 --- /dev/null +++ b/docroot/modules/custom/va_gov_api/src/EventSubscriber/FullWidthBannerAlertSubscriber.php @@ -0,0 +1,89 @@ +entityTypeManager = $entityTypeManager; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + return [ + KernelEvents::RESPONSE => 'onKernelResponse', + ]; + } + + /** + * Modify response to add "field_situation_updates" paragraph data. + */ + public function onKernelResponse(ResponseEvent $event): void { + // Only modify the "/jsonapi/banner-alerts" route. + $request = $event->getRequest(); + if ($request->get('_route') !== 'va_gov_api.banner_alerts') { + return; + } + + $response = $event->getResponse(); + $decoded_content = json_decode($response->getContent(), TRUE); + $base_domain = $request->getSchemeAndHttpHost(); + + // Loop through data array and add "field_situation_updates" paragraph data. + foreach ($decoded_content['data'] as $value) { + if ($situation_updates_meta = $value['relationships']['field_situation_updates']['data']) { + foreach ($situation_updates_meta as $situation_update_meta) { + $situation_update_id = $situation_update_meta['meta']['drupal_internal__target_id']; + /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */ + $paragraph = $this->entityTypeManager->getStorage('paragraph')->load($situation_update_id); + $revision_id = $paragraph->getRevisionId(); + $paragraph_type = $paragraph->getType(); + $uuid = $paragraph->uuid(); + + $paragraphData = [ + 'type' => 'paragraph--' . $paragraph_type, + 'id' => $uuid, + 'links' => [ + 'self' => [ + 'href' => "$base_domain/jsonapi/paragraph/$paragraph_type/$uuid?resourceVersion=id%3A$revision_id", + ], + ], + 'attributes' => [ + 'drupal_internal__id' => $paragraph->id(), + 'drupal_internal__revision_id' => $revision_id, + 'parent_id' => $paragraph->get('parent_id')->getValue()[0], + 'field_datetime_range_timezone' => $paragraph->get('field_datetime_range_timezone')->getValue()[0], + 'field_send_email_to_subscribers' => $paragraph->get('field_send_email_to_subscribers')->getValue()[0], + 'field_wysiwyg' => $paragraph->get('field_wysiwyg')->getValue()[0], + ], + 'relationships' => [], + ]; + + $decoded_content['included'][] = $paragraphData; + } + } + } + + $content = json_encode($decoded_content); + $response->setContent($content); + } + +} diff --git a/docroot/modules/custom/va_gov_api/va_gov_api.services.yml b/docroot/modules/custom/va_gov_api/va_gov_api.services.yml index 5637378076..7717e16bf4 100644 --- a/docroot/modules/custom/va_gov_api/va_gov_api.services.yml +++ b/docroot/modules/custom/va_gov_api/va_gov_api.services.yml @@ -3,7 +3,12 @@ parameters: next_jsonapi.size_max: 2000 services: va_gov_api.add_js_to_ui: - class: Drupal\va_gov_api\EventSubscriber\AddJsEventSubscriber - arguments: ['@request_stack'] - tags: + class: Drupal\va_gov_api\EventSubscriber\AddJsEventSubscriber + arguments: ['@request_stack'] + tags: + - { name: event_subscriber } + va_gov_api.full_width_banner_alert_subscriber: + class: Drupal\va_gov_api\EventSubscriber\FullWidthBannerAlertSubscriber + arguments: ['@entity_type.manager'] + tags: - { name: event_subscriber } From 8e09dc34d683472169c8aa63b1f3aaf366c70d12 Mon Sep 17 00:00:00 2001 From: Alex Finnarn Date: Wed, 17 Jan 2024 12:04:48 -0500 Subject: [PATCH 2/9] add a computed field example for obtaining situation updates for banners --- .../ComputedSituationUpdatesItemList.php | 49 +++++++++++++++++++ .../custom/va_gov_api/va_gov_api.module | 29 +++++++++++ 2 files changed, 78 insertions(+) create mode 100644 docroot/modules/custom/va_gov_api/src/Field/ComputedSituationUpdatesItemList.php create mode 100644 docroot/modules/custom/va_gov_api/va_gov_api.module diff --git a/docroot/modules/custom/va_gov_api/src/Field/ComputedSituationUpdatesItemList.php b/docroot/modules/custom/va_gov_api/src/Field/ComputedSituationUpdatesItemList.php new file mode 100644 index 0000000000..2fb85b9879 --- /dev/null +++ b/docroot/modules/custom/va_gov_api/src/Field/ComputedSituationUpdatesItemList.php @@ -0,0 +1,49 @@ +getEntity(); + if ($parent->isNew()) { + return; + } + + // Get paragraph ids from 'field_situation_updates' field. + $situation_updates = $parent->get('field_situation_updates')->getValue(); + $situation_update_ids = array_column($situation_updates, 'target_id'); + + // Load paragraph entities. + $paragraphs = \Drupal::entityTypeManager() + ->getStorage('paragraph') + ->loadMultiple($situation_update_ids); + + foreach ($paragraphs as $key => $paragraph) { + $paragraphData = [ + 'revision_id' => $paragraph->getRevisionId(), + 'paragraph_type' => $paragraph->getType(), + 'uuid' => $paragraph->uuid(), + 'field_datetime_range_timezone' => $paragraph->get('field_datetime_range_timezone') + ->getValue()[0], + 'field_send_email_to_subscribers' => $paragraph->get('field_send_email_to_subscribers') + ->getValue()[0], + 'field_wysiwyg' => $paragraph->get('field_wysiwyg')->getValue()[0], + ]; + + $this->list[$key] = $this->createItem($key, $paragraphData); + } + } + +} diff --git a/docroot/modules/custom/va_gov_api/va_gov_api.module b/docroot/modules/custom/va_gov_api/va_gov_api.module new file mode 100644 index 0000000000..4c46f987e2 --- /dev/null +++ b/docroot/modules/custom/va_gov_api/va_gov_api.module @@ -0,0 +1,29 @@ +setName('computed_situations_updates') + ->setLabel(t('Computed Situation Updates')) + ->setComputed(TRUE) + ->setClass('\Drupal\va_gov_api\Field\ComputedSituationUpdatesItemList') + ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + } + return $base_field_definitions; +} From 179bdc641eef9a303e834ca266f4c6668a3518d2 Mon Sep 17 00:00:00 2001 From: Alex Finnarn Date: Wed, 17 Jan 2024 13:20:26 -0500 Subject: [PATCH 3/9] try commenting out computed field defintion to see if it affects test failures I cannot view --- .../custom/va_gov_api/va_gov_api.module | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docroot/modules/custom/va_gov_api/va_gov_api.module b/docroot/modules/custom/va_gov_api/va_gov_api.module index 4c46f987e2..fc289b40d2 100644 --- a/docroot/modules/custom/va_gov_api/va_gov_api.module +++ b/docroot/modules/custom/va_gov_api/va_gov_api.module @@ -5,9 +5,9 @@ * Defines the hooks for va_gov_api module. */ -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\Core\Field\FieldStorageDefinitionInterface; +//use Drupal\Core\Entity\EntityTypeInterface; +//use Drupal\Core\Field\BaseFieldDefinition; +//use Drupal\Core\Field\FieldStorageDefinitionInterface; /** * Implements hook_entity_bundle_field_info(). @@ -16,14 +16,14 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface; * * @see core_event_dispatcher.module#L26 */ -function va_gov_api_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { - if ($bundle === 'full_width_banner_alert') { - $base_field_definitions['computed_situations_updates'] = BaseFieldDefinition::create('map') - ->setName('computed_situations_updates') - ->setLabel(t('Computed Situation Updates')) - ->setComputed(TRUE) - ->setClass('\Drupal\va_gov_api\Field\ComputedSituationUpdatesItemList') - ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); - } - return $base_field_definitions; -} +//function va_gov_api_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { +// if ($bundle === 'full_width_banner_alert') { +// $base_field_definitions['computed_situations_updates'] = BaseFieldDefinition::create('map') +// ->setName('computed_situations_updates') +// ->setLabel(t('Computed Situation Updates')) +// ->setComputed(TRUE) +// ->setClass('\Drupal\va_gov_api\Field\ComputedSituationUpdatesItemList') +// ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); +// } +// return $base_field_definitions; +//} From ae4f44cd99a90ce0974a4a39064f0366e5f57f73 Mon Sep 17 00:00:00 2001 From: Alex Finnarn Date: Wed, 17 Jan 2024 13:48:53 -0500 Subject: [PATCH 4/9] uncomment code after checking impact on CI checks --- .../custom/va_gov_api/va_gov_api.module | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docroot/modules/custom/va_gov_api/va_gov_api.module b/docroot/modules/custom/va_gov_api/va_gov_api.module index fc289b40d2..4c46f987e2 100644 --- a/docroot/modules/custom/va_gov_api/va_gov_api.module +++ b/docroot/modules/custom/va_gov_api/va_gov_api.module @@ -5,9 +5,9 @@ * Defines the hooks for va_gov_api module. */ -//use Drupal\Core\Entity\EntityTypeInterface; -//use Drupal\Core\Field\BaseFieldDefinition; -//use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldStorageDefinitionInterface; /** * Implements hook_entity_bundle_field_info(). @@ -16,14 +16,14 @@ * * @see core_event_dispatcher.module#L26 */ -//function va_gov_api_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { -// if ($bundle === 'full_width_banner_alert') { -// $base_field_definitions['computed_situations_updates'] = BaseFieldDefinition::create('map') -// ->setName('computed_situations_updates') -// ->setLabel(t('Computed Situation Updates')) -// ->setComputed(TRUE) -// ->setClass('\Drupal\va_gov_api\Field\ComputedSituationUpdatesItemList') -// ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); -// } -// return $base_field_definitions; -//} +function va_gov_api_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { + if ($bundle === 'full_width_banner_alert') { + $base_field_definitions['computed_situations_updates'] = BaseFieldDefinition::create('map') + ->setName('computed_situations_updates') + ->setLabel(t('Computed Situation Updates')) + ->setComputed(TRUE) + ->setClass('\Drupal\va_gov_api\Field\ComputedSituationUpdatesItemList') + ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + } + return $base_field_definitions; +} From 57799709f67f8a1de6bdfe2af88b14f773ae8203 Mon Sep 17 00:00:00 2001 From: Alex Finnarn Date: Tue, 23 Jan 2024 13:43:53 -0500 Subject: [PATCH 5/9] add new controller for banner alerts --- .../src/Controller/BannerAlertsController.php | 245 ++++++++++++++++++ .../custom/va_gov_api/va_gov_api.module | 29 --- .../custom/va_gov_api/va_gov_api.routing.yml | 10 + .../custom/va_gov_api/va_gov_api.services.yml | 10 +- 4 files changed, 260 insertions(+), 34 deletions(-) create mode 100644 docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php delete mode 100644 docroot/modules/custom/va_gov_api/va_gov_api.module diff --git a/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php b/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php new file mode 100644 index 0000000000..939217db19 --- /dev/null +++ b/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php @@ -0,0 +1,245 @@ +serializer = $serializer; + $this->pathMatcher = $path_matcher; + $this->pathValidator = $path_validator; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('serializer'), + $container->get('path.matcher'), + $container->get('path.validator'), + $container->get('entity_type.manager') + ); + } + + /** + * Get banner alerts by path. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + * The banner alerts. + */ + public function bannerAlertsByPath(Request $request) { + $path = $request->get('path'); + if (!$path) { + return new JsonResponse(['error' => 'No path provided.']); + } + + $banners = $this->collectBannerData($path); + $promo_banners = $this->collectPromoBannerData($path); + $full_width_banner_alerts = $this->collectFullWidthBannerAlertData($path); + + return new JsonResponse([ + 'data' => array_merge($banners, $promo_banners, $full_width_banner_alerts), + ]); + } + + /** + * Collect `banner` entities to be returned in the response. + * + * Given a path, retrieves any `banner` that should show there, constructs a + * ResponseObject for it, and adds it to cacheableDependencies. + * + * @param string $path + * The path to the item to find banners for. + */ + protected function collectBannerData(string $path) { + $node_storage = $this->entityTypeManager->getStorage('node'); + + // Get all published banner nodes. + $banner_nids = $node_storage->getQuery() + ->condition('type', 'banner') + ->condition('status', TRUE) + ->accessCheck(FALSE) + ->execute(); + /** @var \Drupal\node\NodeInterface[] $banners */ + $banners = $node_storage->loadMultiple(array_values($banner_nids) ?? []); + + // Filter the banner list to just the ones that should be displayed for the + // provided item path. + $banners = array_filter($banners, function ($item) use ($path) { + // PathMatcher expects a newline delimited string for multiple paths. + $patterns = ''; + foreach ($item->field_target_paths->getValue() as $target_path) { + $patterns .= $target_path['value'] . "\n"; + } + + return $this->pathMatcher->matchPath($path, $patterns); + }); + + // Add the banners to the response. + $banner_data = []; + foreach ($banners as $entity) { + $banner_data[] = $this->serializer->normalize($entity); + } + return $banner_data; + } + + /** + * Collect `promo_banner` entities to be returned in the response. + * + * Given a path, retrieves any `promo_banner` that should show there, + * constructs a ResponseObject for it, and adds it to cacheableDependencies. + * + * @param string $path + * The path to the item to find promo_banners for. + */ + protected function collectPromoBannerData(string $path) { + $node_storage = $this->entityTypeManager->getStorage('node'); + + // Get all published promo_banner nodes. + $promo_banner_nids = $node_storage->getQuery() + ->condition('type', 'promo_banner') + ->condition('status', TRUE) + ->accessCheck(FALSE) + ->execute(); + /** @var \Drupal\node\NodeInterface[] $promo_banners */ + $promo_banners = $node_storage->loadMultiple(array_values($promo_banner_nids) ?? []); + + // Filter the promo_banner list to just the ones that should be displayed + // for the provided item path. + $promo_banners = array_filter($promo_banners, function ($item) use ($path) { + // PathMatcher expects a newline delimited string for multiple paths. + $patterns = ''; + foreach ($item->field_target_paths->getValue() as $target_path) { + $patterns .= $target_path['value'] . "\n"; + } + + return $this->pathMatcher->matchPath($path, $patterns); + }); + + // Add the promo_banners to the response. + $promo_banner_data = []; + foreach ($promo_banners as $entity) { + $promo_banner_data[] = $this->serializer->normalize($entity); + } + return $promo_banner_data; + } + + /** + * Collect `full_width_banner_alert` entities to be returned in the response. + * + * Given a path, retrieves any `full_width_banner_alert` that should show + * there, constructs a ResponseObject for it, and adds it to + * cacheableDependencies. + * + * @param string $path + * The path to the item to find full_width_banner_alerts for. + */ + protected function collectFullWidthBannerAlertData(string $path) { + // Find the first fragment of the path; this will correspond to a facility, + // if this is a facility page of some kind. + $region_fragment = '__not_a_real_url'; + $path_pieces = explode("/", $path); + if (count($path_pieces) > 1) { + $region_fragment = "/" . $path_pieces[1]; + } + + // Resolve the region fragment to a URL object. + $url = $this->pathValidator->getUrlIfValidWithoutAccessCheck($region_fragment); + if ($url === FALSE || !$url->isRouted() || !isset($url->getRouteParameters()['node'])) { + // If the alias is invalid, it's not a routed URL, or there is not a node + // in the route params, there's not much else that can be done here. + return; + } + + // Load the system that we found. + $node_storage = $this->entityTypeManager->getStorage('node'); + $system_nid = $url->getRouteParameters()['node']; + /** @var \Drupal\node\NodeInterface $system_node */ + $system_node = $node_storage->load($system_nid); + + // If it's not a published VAMC system node, bail early. + if (is_null($system_node) || $system_node->getType() != 'health_care_region_page' || $system_node->isPublished() === FALSE) { + return; + } + + // Find all operating status nodes which have this system as their office. + $operating_status_nids = $node_storage->getQuery() + ->condition('type', 'vamc_operating_status_and_alerts') + ->condition('status', TRUE) + ->condition('field_office', $system_node->id()) + ->accessCheck(FALSE) + ->execute(); + + // If there are no operating status nids, bail. + if (count($operating_status_nids) === 0) { + return; + } + + // Find any facility banners connected to the operating status nodes. + $facility_banner_nids = $node_storage->getQuery() + ->condition('type', 'full_width_banner_alert') + ->condition('status', TRUE) + ->condition('field_banner_alert_vamcs', array_values($operating_status_nids), 'IN') + ->accessCheck(FALSE) + ->execute(); + + /** @var \Drupal\node\NodeInterface[] $facility_banners */ + $facility_banners = $node_storage->loadMultiple($facility_banner_nids); + + // Add the banners to the response. + $full_width_banner_alert_data = []; + foreach ($facility_banners as $entity) { + $full_width_banner_alert_data[] = $this->serializer->normalize($entity); + + // Override field_situation_updates with referenced paragraph data. + $situation_updates = $entity->get('field_situation_updates')->referencedEntities(); + $situation_update_data = []; + foreach ($situation_updates as $situation_update) { + $situation_update_data[] = $this->serializer->normalize($situation_update); + } + $full_width_banner_alert_data['field_situation_updates'] = $situation_update_data; + } + return $full_width_banner_alert_data; + } + +} diff --git a/docroot/modules/custom/va_gov_api/va_gov_api.module b/docroot/modules/custom/va_gov_api/va_gov_api.module deleted file mode 100644 index 4c46f987e2..0000000000 --- a/docroot/modules/custom/va_gov_api/va_gov_api.module +++ /dev/null @@ -1,29 +0,0 @@ -setName('computed_situations_updates') - ->setLabel(t('Computed Situation Updates')) - ->setComputed(TRUE) - ->setClass('\Drupal\va_gov_api\Field\ComputedSituationUpdatesItemList') - ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); - } - return $base_field_definitions; -} diff --git a/docroot/modules/custom/va_gov_api/va_gov_api.routing.yml b/docroot/modules/custom/va_gov_api/va_gov_api.routing.yml index 09e7af57dd..c7c5d584d3 100644 --- a/docroot/modules/custom/va_gov_api/va_gov_api.routing.yml +++ b/docroot/modules/custom/va_gov_api/va_gov_api.routing.yml @@ -8,3 +8,13 @@ va_gov_api.banner_alerts: _permission: 'access content' _content_type_format: api_json _format: api_json + +va_gov_api.new_banner_alerts: + path: '/api/v1/banner-alerts' + defaults: + _title: 'Banner Alerts' + _controller: '\Drupal\va_gov_api\Controller\BannerAlertsController::bannerAlertsByPath' + requirements: + _permission: 'access content' + methods: + - GET diff --git a/docroot/modules/custom/va_gov_api/va_gov_api.services.yml b/docroot/modules/custom/va_gov_api/va_gov_api.services.yml index 7717e16bf4..b4be74fe78 100644 --- a/docroot/modules/custom/va_gov_api/va_gov_api.services.yml +++ b/docroot/modules/custom/va_gov_api/va_gov_api.services.yml @@ -7,8 +7,8 @@ services: arguments: ['@request_stack'] tags: - { name: event_subscriber } - va_gov_api.full_width_banner_alert_subscriber: - class: Drupal\va_gov_api\EventSubscriber\FullWidthBannerAlertSubscriber - arguments: ['@entity_type.manager'] - tags: - - { name: event_subscriber } +# va_gov_api.full_width_banner_alert_subscriber: +# class: Drupal\va_gov_api\EventSubscriber\FullWidthBannerAlertSubscriber +# arguments: ['@entity_type.manager'] +# tags: +# - { name: event_subscriber } From bb599b83cb36eb2e4ab6366dc08049547f0f98c2 Mon Sep 17 00:00:00 2001 From: Alex Finnarn Date: Tue, 23 Jan 2024 16:50:55 -0500 Subject: [PATCH 6/9] adding cache tags to response --- .../src/Controller/BannerAlertsController.php | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php b/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php index 939217db19..f9850dd69c 100644 --- a/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php +++ b/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php @@ -2,6 +2,7 @@ namespace Drupal\va_gov_api\Controller; +use Drupal\Core\Cache\CacheableJsonResponse; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Path\PathMatcherInterface; @@ -73,13 +74,17 @@ public function bannerAlertsByPath(Request $request) { return new JsonResponse(['error' => 'No path provided.']); } - $banners = $this->collectBannerData($path); - $promo_banners = $this->collectPromoBannerData($path); - $full_width_banner_alerts = $this->collectFullWidthBannerAlertData($path); + [$banners, $banner_cache_tags] = $this->collectBannerData($path); + [$promo_banners, $promo_cache_tags] = $this->collectPromoBannerData($path); + [$full_width_banner_alerts, $full_width_banner_alert_cache_tags] = $this->collectFullWidthBannerAlertData($path); - return new JsonResponse([ + $response = new CacheableJsonResponse([ 'data' => array_merge($banners, $promo_banners, $full_width_banner_alerts), ]); + + $cache_tags = array_merge($banner_cache_tags, $promo_cache_tags, $full_width_banner_alert_cache_tags); + $response->getCacheableMetadata()->addCacheTags($cache_tags); + return $response; } /** @@ -117,10 +122,13 @@ protected function collectBannerData(string $path) { // Add the banners to the response. $banner_data = []; + $cache_tags = []; foreach ($banners as $entity) { $banner_data[] = $this->serializer->normalize($entity); + $cache_tags = array_merge($cache_tags, $entity->getCacheTags()); } - return $banner_data; + + return ['data' => $banner_data, 'cache_tags' => $cache_tags]; } /** @@ -158,10 +166,12 @@ protected function collectPromoBannerData(string $path) { // Add the promo_banners to the response. $promo_banner_data = []; + $cache_tags = []; foreach ($promo_banners as $entity) { $promo_banner_data[] = $this->serializer->normalize($entity); + $cache_tags = array_merge($cache_tags, $entity->getCacheTags()); } - return $promo_banner_data; + return ['data' => $promo_banner_data, 'cache_tags' => $cache_tags]; } /** @@ -228,18 +238,20 @@ protected function collectFullWidthBannerAlertData(string $path) { // Add the banners to the response. $full_width_banner_alert_data = []; + $cache_tags = []; foreach ($facility_banners as $entity) { - $full_width_banner_alert_data[] = $this->serializer->normalize($entity); + $full_width_banner_alert_data[] = $this->serializer->normalize($entity, 'json', ['plugin_id' => 'entity']); + $cache_tags = array_merge($cache_tags, $entity->getCacheTags()); // Override field_situation_updates with referenced paragraph data. $situation_updates = $entity->get('field_situation_updates')->referencedEntities(); $situation_update_data = []; foreach ($situation_updates as $situation_update) { - $situation_update_data[] = $this->serializer->normalize($situation_update); + $situation_update_data[] = $this->serializer->normalize($situation_update, 'json', ['plugin_id' => 'entity']); } - $full_width_banner_alert_data['field_situation_updates'] = $situation_update_data; + $full_width_banner_alert_data[count($full_width_banner_alert_data) - 1]['field_situation_updates'] = $situation_update_data; } - return $full_width_banner_alert_data; + return ['data' => $full_width_banner_alert_data, 'cache_tags' => $cache_tags]; } } From 25e9f8524ecd4409e6e84a24494de342e9de9c95 Mon Sep 17 00:00:00 2001 From: Alex Finnarn Date: Thu, 25 Jan 2024 12:46:03 -0500 Subject: [PATCH 7/9] add WIP test but pause work --- composer.json | 2 +- .../src/Controller/BannerAlertsController.php | 60 +++++++-- tests/phpunit/API/BannerEndpointTest.php | 2 +- tests/phpunit/API/NewBannerEndpointTest.php | 114 ++++++++++++++++++ 4 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 tests/phpunit/API/NewBannerEndpointTest.php diff --git a/composer.json b/composer.json index f689c18d31..d35ff3e2ad 100644 --- a/composer.json +++ b/composer.json @@ -772,7 +772,7 @@ ], "va:test:phpunit-functional": [ "# Run PHPUnit (functional) tests.", - "! ./scripts/should-run-directly.sh || bin/phpunit --group functional --exclude-group disabled tests/phpunit --", + "! ./scripts/should-run-directly.sh || bin/phpunit --group foo-functional --exclude-group disabled tests/phpunit --", "./scripts/should-run-directly.sh || ddev composer va:test:phpunit-functional --" ], "va:test:phpunit-unit": [ diff --git a/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php b/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php index f9850dd69c..3589e52337 100644 --- a/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php +++ b/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php @@ -82,8 +82,11 @@ public function bannerAlertsByPath(Request $request) { 'data' => array_merge($banners, $promo_banners, $full_width_banner_alerts), ]); + // @todo Add path to cache somehow... + // $item_path_context = (new CacheableMetadata())->addCacheContexts(['url.query_args:item-path']); $cache_tags = array_merge($banner_cache_tags, $promo_cache_tags, $full_width_banner_alert_cache_tags); $response->getCacheableMetadata()->addCacheTags($cache_tags); + return $response; } @@ -128,7 +131,7 @@ protected function collectBannerData(string $path) { $cache_tags = array_merge($cache_tags, $entity->getCacheTags()); } - return ['data' => $banner_data, 'cache_tags' => $cache_tags]; + return [$this->flattenData($banner_data), $cache_tags]; } /** @@ -171,7 +174,7 @@ protected function collectPromoBannerData(string $path) { $promo_banner_data[] = $this->serializer->normalize($entity); $cache_tags = array_merge($cache_tags, $entity->getCacheTags()); } - return ['data' => $promo_banner_data, 'cache_tags' => $cache_tags]; + return [$this->flattenData($promo_banner_data), $cache_tags]; } /** @@ -240,18 +243,61 @@ protected function collectFullWidthBannerAlertData(string $path) { $full_width_banner_alert_data = []; $cache_tags = []; foreach ($facility_banners as $entity) { - $full_width_banner_alert_data[] = $this->serializer->normalize($entity, 'json', ['plugin_id' => 'entity']); - $cache_tags = array_merge($cache_tags, $entity->getCacheTags()); + $normalized_data = $this->serializer->normalize($entity); // Override field_situation_updates with referenced paragraph data. $situation_updates = $entity->get('field_situation_updates')->referencedEntities(); $situation_update_data = []; foreach ($situation_updates as $situation_update) { - $situation_update_data[] = $this->serializer->normalize($situation_update, 'json', ['plugin_id' => 'entity']); + $situation_update_data[] = $this->serializer->normalize($situation_update); } - $full_width_banner_alert_data[count($full_width_banner_alert_data) - 1]['field_situation_updates'] = $situation_update_data; + $normalized_data['field_situation_updates'] = $this->flattenData($situation_update_data); + + $full_width_banner_alert_data[] = $normalized_data; + $cache_tags = array_merge($cache_tags, $entity->getCacheTags()); } - return ['data' => $full_width_banner_alert_data, 'cache_tags' => $cache_tags]; + + return [$this->flattenData($full_width_banner_alert_data), $cache_tags]; + } + + /** + * Format the data into a flatter structure. + * + * @param array $data + * The data to flatten. + * + * @return array + * The flattened data. + */ + private function flattenData(array $data): array { + return array_map(function ($item) { + $result = []; + foreach ($item as $key => $value) { + // Check if value is an array with exactly one element. + if (is_array($value) && count($value) === 1) { + // Get the first element of the array. + $firstElement = reset($value); + + // Check if the first element itself is an associative array with exactly one key. + if (is_array($firstElement) + && count($firstElement) === 1 + && array_key_exists('value', $firstElement)) { + // Assign the 'value' directly. + $result[$key] = $firstElement['value']; + } + else { + // Keep the first element as is, since it's an associative array with multiple keys. + $result[$key] = $firstElement; + } + } + else { + // Copy the value as is. + $result[$key] = $value; + } + } + return $result; + }, + $data); } } diff --git a/tests/phpunit/API/BannerEndpointTest.php b/tests/phpunit/API/BannerEndpointTest.php index eafe504cb9..b4a52f1f36 100644 --- a/tests/phpunit/API/BannerEndpointTest.php +++ b/tests/phpunit/API/BannerEndpointTest.php @@ -64,7 +64,7 @@ public function testBanner($path, $shouldBeIncluded) { $url = $this->baseUrl; - // Make sure the banner is found in all of the requests that it _should_ be + // Make sure the banner is found in all the requests that it _should_ be // included in (and not in the places where it should not be). $response = \Drupal::httpClient()->get($url . '/jsonapi/banner-alerts?item-path=' . $path); $this->assertEquals('200', $response->getStatusCode(), 'request returned status code ' . $response->getStatusCode()); diff --git a/tests/phpunit/API/NewBannerEndpointTest.php b/tests/phpunit/API/NewBannerEndpointTest.php new file mode 100644 index 0000000000..8c617fa6e7 --- /dev/null +++ b/tests/phpunit/API/NewBannerEndpointTest.php @@ -0,0 +1,114 @@ +drupalLogin($this->createUser(['access content'])); + } + + /** + * Test that the banner endpoint handles full width alert banners. + */ + public function testFullWidthAlertBanner() { + // Create 'situation_update' paragraph. + $situation_update = Paragraph::create([ + 'type' => 'situation_update', + 'status' => 1, + 'field_datetime_range_timezone' => [ + [ + 'value' => '2024-01-23T12:22:00+00:00', + 'end_value' => '2024-01-23T13:22:00+00:00', + ], + ], + 'field_send_email_to_subscribers' => [ + [ + 'value' => FALSE, + ], + ], + 'field_wysiwyg' => [ + [ + 'value' => '

blah blah blah

', + 'format' => 'rich_text', + ], + ], + ]); + + // Look for a 'full_width_banner_alert' node. + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + $banner_nids = $node_storage->getQuery() + ->condition('type', 'full_width_banner_alert') + // ->condition('status', TRUE) + ->range(0, 1) + ->accessCheck(FALSE) + ->execute(); + + // Load and publish the node. + $banner_nid = reset($banner_nids); + $banner = $node_storage->load($banner_nid); + + // Get 'field_banner_alert_vamcs' data. + $banner_alert_vamcs = $banner->get('field_banner_alert_vamcs')->getValue(); + + $banner->set('status', TRUE); + $banner->save(); + + // Look for a published node of type "vamc_operating_status_and_alerts". + $system_nids = $node_storage->getQuery() + ->condition('type', 'vamc_operating_status_and_alerts') + ->condition('status', TRUE) + ->condition('id', $banner_alert_vamcs[0]['target_id']) + ->range(0, 1) + ->accessCheck(FALSE) + ->execute(); + + // Get first array value. + $system_nid = reset($system_nids); + + // Load the entity. + $system_node = $node_storage->load($system_nid); + // Get entity id from "field_office" field. + $office_nid = $system_node->get('field_office')->target_id; + $path_alias = \Drupal::database()->select('path_alias', 'pa') + ->fields('pa', ['alias']) + ->condition('path', '/node/' . $office_nid) + ->execute() + ->fetchField(); + + // Visit the banner endpoint using the path alias. + $this->visit('/api/v1/banner-alerts?path=' . $path_alias); + $this->assertEquals(200, $this->getSession()->getStatusCode()); + + $json = json_decode($this->getSession()->getPage()->getContent(), TRUE); + + // Confirm that the banner is included in the response. + $this->assertIsArray($json['data']); + + $filtered_nodes = array_filter($json['data'], function ($item) use ($banner) { + return isset($item['nid']) && $item['nid'] == $banner->id(); + }); + + $this->assertNotEmpty($filtered_nodes, 'Node with nid ' . $banner->id() . ' found in response.'); + + // Confirm that the string "65130" is in the response. + $this->assertStringContainsString('"nid":' . $banner->id(), $this->getSession()->getPage()->getContent()); + + // Delete the paragraph. + $situation_update->delete(); + } + +} From b8864da524ed0a6a55f86822130619908f7dab96 Mon Sep 17 00:00:00 2001 From: Alex Finnarn Date: Tue, 30 Jan 2024 11:01:45 -0500 Subject: [PATCH 8/9] add tests for each banner type --- composer.json | 2 +- .../src/Controller/BannerAlertsController.php | 28 +- .../FullWidthBannerAlertSubscriber.php | 89 ----- .../ComputedSituationUpdatesItemList.php | 49 --- .../va_gov_api/src/Resources/BannerAlerts.php | 353 ------------------ .../custom/va_gov_api/va_gov_api.routing.yml | 11 - .../custom/va_gov_api/va_gov_api.services.yml | 5 - tests/phpunit/API/BannerEndpointTest.php | 199 +++++++++- tests/phpunit/API/NewBannerEndpointTest.php | 114 ------ 9 files changed, 203 insertions(+), 647 deletions(-) delete mode 100644 docroot/modules/custom/va_gov_api/src/EventSubscriber/FullWidthBannerAlertSubscriber.php delete mode 100644 docroot/modules/custom/va_gov_api/src/Field/ComputedSituationUpdatesItemList.php delete mode 100644 docroot/modules/custom/va_gov_api/src/Resources/BannerAlerts.php delete mode 100644 tests/phpunit/API/NewBannerEndpointTest.php diff --git a/composer.json b/composer.json index d35ff3e2ad..f689c18d31 100644 --- a/composer.json +++ b/composer.json @@ -772,7 +772,7 @@ ], "va:test:phpunit-functional": [ "# Run PHPUnit (functional) tests.", - "! ./scripts/should-run-directly.sh || bin/phpunit --group foo-functional --exclude-group disabled tests/phpunit --", + "! ./scripts/should-run-directly.sh || bin/phpunit --group functional --exclude-group disabled tests/phpunit --", "./scripts/should-run-directly.sh || ddev composer va:test:phpunit-functional --" ], "va:test:phpunit-unit": [ diff --git a/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php b/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php index 3589e52337..3602f92388 100644 --- a/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php +++ b/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php @@ -82,10 +82,14 @@ public function bannerAlertsByPath(Request $request) { 'data' => array_merge($banners, $promo_banners, $full_width_banner_alerts), ]); - // @todo Add path to cache somehow... - // $item_path_context = (new CacheableMetadata())->addCacheContexts(['url.query_args:item-path']); + // Add the 'path' query parameter to the cache contexts. + $response->getCacheableMetadata()->addCacheContexts(['url.query_args:path']); + + // Add the cache tags from the banner nodes and set TTL. $cache_tags = array_merge($banner_cache_tags, $promo_cache_tags, $full_width_banner_alert_cache_tags); $response->getCacheableMetadata()->addCacheTags($cache_tags); + $one_day = 60 * 60 * 24; + $response->getCacheableMetadata()->setCacheMaxAge($one_day); return $response; } @@ -99,7 +103,7 @@ public function bannerAlertsByPath(Request $request) { * @param string $path * The path to the item to find banners for. */ - protected function collectBannerData(string $path) { + protected function collectBannerData(string $path): array { $node_storage = $this->entityTypeManager->getStorage('node'); // Get all published banner nodes. @@ -143,7 +147,7 @@ protected function collectBannerData(string $path) { * @param string $path * The path to the item to find promo_banners for. */ - protected function collectPromoBannerData(string $path) { + protected function collectPromoBannerData(string $path): array { $node_storage = $this->entityTypeManager->getStorage('node'); // Get all published promo_banner nodes. @@ -187,7 +191,7 @@ protected function collectPromoBannerData(string $path) { * @param string $path * The path to the item to find full_width_banner_alerts for. */ - protected function collectFullWidthBannerAlertData(string $path) { + protected function collectFullWidthBannerAlertData(string $path): array { // Find the first fragment of the path; this will correspond to a facility, // if this is a facility page of some kind. $region_fragment = '__not_a_real_url'; @@ -201,7 +205,7 @@ protected function collectFullWidthBannerAlertData(string $path) { if ($url === FALSE || !$url->isRouted() || !isset($url->getRouteParameters()['node'])) { // If the alias is invalid, it's not a routed URL, or there is not a node // in the route params, there's not much else that can be done here. - return; + return [[], []]; } // Load the system that we found. @@ -212,7 +216,7 @@ protected function collectFullWidthBannerAlertData(string $path) { // If it's not a published VAMC system node, bail early. if (is_null($system_node) || $system_node->getType() != 'health_care_region_page' || $system_node->isPublished() === FALSE) { - return; + return [[], []]; } // Find all operating status nodes which have this system as their office. @@ -225,7 +229,7 @@ protected function collectFullWidthBannerAlertData(string $path) { // If there are no operating status nids, bail. if (count($operating_status_nids) === 0) { - return; + return [[], []]; } // Find any facility banners connected to the operating status nodes. @@ -269,7 +273,7 @@ protected function collectFullWidthBannerAlertData(string $path) { * @return array * The flattened data. */ - private function flattenData(array $data): array { + private function flattenData(array $data = []): array { return array_map(function ($item) { $result = []; foreach ($item as $key => $value) { @@ -278,7 +282,8 @@ private function flattenData(array $data): array { // Get the first element of the array. $firstElement = reset($value); - // Check if the first element itself is an associative array with exactly one key. + // Check if the first element itself is an associative array + // with exactly one key. if (is_array($firstElement) && count($firstElement) === 1 && array_key_exists('value', $firstElement)) { @@ -286,7 +291,8 @@ private function flattenData(array $data): array { $result[$key] = $firstElement['value']; } else { - // Keep the first element as is, since it's an associative array with multiple keys. + // Keep the first element as is, + // since it's an associative array with multiple keys. $result[$key] = $firstElement; } } diff --git a/docroot/modules/custom/va_gov_api/src/EventSubscriber/FullWidthBannerAlertSubscriber.php b/docroot/modules/custom/va_gov_api/src/EventSubscriber/FullWidthBannerAlertSubscriber.php deleted file mode 100644 index 7c8e461648..0000000000 --- a/docroot/modules/custom/va_gov_api/src/EventSubscriber/FullWidthBannerAlertSubscriber.php +++ /dev/null @@ -1,89 +0,0 @@ -entityTypeManager = $entityTypeManager; - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents(): array { - return [ - KernelEvents::RESPONSE => 'onKernelResponse', - ]; - } - - /** - * Modify response to add "field_situation_updates" paragraph data. - */ - public function onKernelResponse(ResponseEvent $event): void { - // Only modify the "/jsonapi/banner-alerts" route. - $request = $event->getRequest(); - if ($request->get('_route') !== 'va_gov_api.banner_alerts') { - return; - } - - $response = $event->getResponse(); - $decoded_content = json_decode($response->getContent(), TRUE); - $base_domain = $request->getSchemeAndHttpHost(); - - // Loop through data array and add "field_situation_updates" paragraph data. - foreach ($decoded_content['data'] as $value) { - if ($situation_updates_meta = $value['relationships']['field_situation_updates']['data']) { - foreach ($situation_updates_meta as $situation_update_meta) { - $situation_update_id = $situation_update_meta['meta']['drupal_internal__target_id']; - /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */ - $paragraph = $this->entityTypeManager->getStorage('paragraph')->load($situation_update_id); - $revision_id = $paragraph->getRevisionId(); - $paragraph_type = $paragraph->getType(); - $uuid = $paragraph->uuid(); - - $paragraphData = [ - 'type' => 'paragraph--' . $paragraph_type, - 'id' => $uuid, - 'links' => [ - 'self' => [ - 'href' => "$base_domain/jsonapi/paragraph/$paragraph_type/$uuid?resourceVersion=id%3A$revision_id", - ], - ], - 'attributes' => [ - 'drupal_internal__id' => $paragraph->id(), - 'drupal_internal__revision_id' => $revision_id, - 'parent_id' => $paragraph->get('parent_id')->getValue()[0], - 'field_datetime_range_timezone' => $paragraph->get('field_datetime_range_timezone')->getValue()[0], - 'field_send_email_to_subscribers' => $paragraph->get('field_send_email_to_subscribers')->getValue()[0], - 'field_wysiwyg' => $paragraph->get('field_wysiwyg')->getValue()[0], - ], - 'relationships' => [], - ]; - - $decoded_content['included'][] = $paragraphData; - } - } - } - - $content = json_encode($decoded_content); - $response->setContent($content); - } - -} diff --git a/docroot/modules/custom/va_gov_api/src/Field/ComputedSituationUpdatesItemList.php b/docroot/modules/custom/va_gov_api/src/Field/ComputedSituationUpdatesItemList.php deleted file mode 100644 index 2fb85b9879..0000000000 --- a/docroot/modules/custom/va_gov_api/src/Field/ComputedSituationUpdatesItemList.php +++ /dev/null @@ -1,49 +0,0 @@ -getEntity(); - if ($parent->isNew()) { - return; - } - - // Get paragraph ids from 'field_situation_updates' field. - $situation_updates = $parent->get('field_situation_updates')->getValue(); - $situation_update_ids = array_column($situation_updates, 'target_id'); - - // Load paragraph entities. - $paragraphs = \Drupal::entityTypeManager() - ->getStorage('paragraph') - ->loadMultiple($situation_update_ids); - - foreach ($paragraphs as $key => $paragraph) { - $paragraphData = [ - 'revision_id' => $paragraph->getRevisionId(), - 'paragraph_type' => $paragraph->getType(), - 'uuid' => $paragraph->uuid(), - 'field_datetime_range_timezone' => $paragraph->get('field_datetime_range_timezone') - ->getValue()[0], - 'field_send_email_to_subscribers' => $paragraph->get('field_send_email_to_subscribers') - ->getValue()[0], - 'field_wysiwyg' => $paragraph->get('field_wysiwyg')->getValue()[0], - ]; - - $this->list[$key] = $this->createItem($key, $paragraphData); - } - } - -} diff --git a/docroot/modules/custom/va_gov_api/src/Resources/BannerAlerts.php b/docroot/modules/custom/va_gov_api/src/Resources/BannerAlerts.php deleted file mode 100644 index 7440f9eb05..0000000000 --- a/docroot/modules/custom/va_gov_api/src/Resources/BannerAlerts.php +++ /dev/null @@ -1,353 +0,0 @@ -pathMatcher = $path_matcher; - $this->pathValidator = $path_validator; - $this->pageCacheKillSwitch = $page_cache_kill_switch; - } - - /** - * {@inheritDoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('path.matcher'), - $container->get('path.validator'), - $container->get('page_cache_kill_switch') - ); - } - - /** - * Process the resource request. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request. - * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types - * The route resource types. - * @param mixed $resource_tag - * An arg that is passed. - * - * @return \Drupal\jsonapi\ResourceResponse - * The response. - */ - public function process(Request $request, array $resource_types, $resource_tag = NULL): ResourceResponse { - // Vary the response by item-path. - $this->addItemPathCacheableDependency(); - - // Make the resource parameter available to other methods. - if (!is_null($resource_tag)) { - // @phpstan-ignore-next-line - $this->addRouteParameter('resource_tag', $resource_tag); - } - - $path = $request->get('item-path'); - - // Collect the data. - if (!is_null($path)) { - foreach ($resource_types as $resource_type) { - switch ($resource_type->getTypeName()) { - case 'node--banner': - $this->collectBannerData($path, $resource_type); - break; - - case 'node--promo_banner': - $this->collectPromoBannerData($path, $resource_type); - break; - - case 'node--full_width_banner_alert': - $this->collectFullWidthBannerAlertData($path, $resource_type); - break; - } - } - } - - return $this->buildResponse($request); - } - - /** - * Build a resource response. - */ - protected function buildResponse($request) { - $resource_object_data = new ResourceObjectData($this->resourceObjects); - - /** @var \Drupal\Core\Cache\CacheableResponseInterface $response */ - $response = $this->createJsonapiResponse($resource_object_data, $request); - - foreach ($this->cacheableDependencies as $cacheable_dependency) { - $response->addCacheableDependency($cacheable_dependency); - } - - // If it's an empty response, then we shouldn't cache it at all -- we don't - // have a good way of invalidating the cache of an empty result (since the - // cache invlidation is based on the nodes that are actually included in the - // response). - if (empty($this->resourceObjects) || empty($this->cacheableDependencies)) { - $this->pageCacheKillSwitch->trigger(); - } - - return $response; - } - - /** - * Collect `banner` entities to be returned in the response. - * - * Given a path, retrieves any `banner` that should show there, constructs a - * ResponseObject for it, and adds it to cacheableDependencies. - * - * @param string $path - * The path to the item to find banners for. - * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type - * The ResourceType we want to collect data for. - */ - protected function collectBannerData(string $path, ResourceType $resource_type) { - $node_storage = $this->entityTypeManager->getStorage('node'); - - // Get all published banner nodes. - $banner_nids = $node_storage->getQuery() - ->condition('type', 'banner') - ->condition('status', TRUE) - ->accessCheck(FALSE) - ->execute(); - /** @var \Drupal\node\NodeInterface[] $banners */ - $banners = $node_storage->loadMultiple(array_values($banner_nids) ?? []); - - // Filter the banner list to just the ones that should be displayed for the - // provided item path. - $banners = array_filter($banners, function ($item) use ($path) { - // PathMatcher expects a newline delimited string for multiple paths. - $patterns = ''; - foreach ($item->field_target_paths->getValue() as $target_path) { - $patterns .= $target_path['value'] . "\n"; - } - - return $this->pathMatcher->matchPath($path, $patterns); - }); - - // Add the banners to the response. - foreach ($banners as $entity) { - $this->addEntityToResponse($resource_type, $entity); - } - } - - /** - * Collect `promo_banner` entities to be returned in the response. - * - * Given a path, retrieves any `promo_banner` that should show there, - * constructs a ResponseObject for it, and adds it to cacheableDependencies. - * - * @param string $path - * The path to the item to find promo_banners for. - * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type - * The ResourceType we want to collect data for. - */ - protected function collectPromoBannerData(string $path, ResourceType $resource_type) { - $node_storage = $this->entityTypeManager->getStorage('node'); - - // Get all published promo_banner nodes. - $promo_banner_nids = $node_storage->getQuery() - ->condition('type', 'promo_banner') - ->condition('status', TRUE) - ->accessCheck(FALSE) - ->execute(); - /** @var \Drupal\node\NodeInterface[] $promo_banners */ - $promo_banners = $node_storage->loadMultiple(array_values($promo_banner_nids) ?? []); - - // Filter the promo_banner list to just the ones that should be displayed - // for the provided item path. - $promo_banners = array_filter($promo_banners, function ($item) use ($path) { - // PathMatcher expects a newline delimited string for multiple paths. - $patterns = ''; - foreach ($item->field_target_paths->getValue() as $target_path) { - $patterns .= $target_path['value'] . "\n"; - } - - return $this->pathMatcher->matchPath($path, $patterns); - }); - - // Add the promo_banners to the response. - foreach ($promo_banners as $entity) { - $this->addEntityToResponse($resource_type, $entity); - } - } - - /** - * Collect `full_width_banner_alert` entities to be returned in the response. - * - * Given a path, retrieves any `full_width_banner_alert` that should show - * there, constructs a ResponseObject for it, and adds it to - * cacheableDependencies. - * - * @param string $path - * The path to the item to find full_width_banner_alerts for. - * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type - * The ResourceType we want to collect data for. - */ - protected function collectFullWidthBannerAlertData(string $path, ResourceType $resource_type) { - // Find the first fragment of the path; this will correspond to a facility, - // if this is a facility page of some kind. - $region_fragment = '__not_a_real_url'; - $path_pieces = explode("/", $path); - if (count($path_pieces) > 1) { - $region_fragment = "/" . $path_pieces[1]; - } - - // Resolve the region fragment to a URL object. - $url = $this->pathValidator->getUrlIfValidWithoutAccessCheck($region_fragment); - if ($url === FALSE || !$url->isRouted() || !isset($url->getRouteParameters()['node'])) { - // If the alias is invalid, it's not a routed URL, or there is not a node - // in the route params, there's not much else that can be done here. - return; - } - - // Load the system that we found. - $node_storage = $this->entityTypeManager->getStorage('node'); - $system_nid = $url->getRouteParameters()['node']; - /** @var \Drupal\node\NodeInterface $system_node */ - $system_node = $node_storage->load($system_nid); - - // If it's not a published VAMC system node, bail early. - if (is_null($system_node) || $system_node->getType() != 'health_care_region_page' || $system_node->isPublished() === FALSE) { - return; - } - - // Find all operating status nodes which have this system as their office. - $operating_status_nids = $node_storage->getQuery() - ->condition('type', 'vamc_operating_status_and_alerts') - ->condition('status', TRUE) - ->condition('field_office', $system_node->id()) - ->accessCheck(FALSE) - ->execute(); - - // If there are no operating status nids, bail. - if (count($operating_status_nids) === 0) { - return; - } - - // Find any facility banners connected to the operating status nodes. - $facility_banner_nids = $node_storage->getQuery() - ->condition('type', 'full_width_banner_alert') - ->condition('status', TRUE) - ->condition('field_banner_alert_vamcs', array_values($operating_status_nids), 'IN') - ->accessCheck(FALSE) - ->execute(); - - /** @var \Drupal\node\NodeInterface[] $facility_banners */ - $facility_banners = $node_storage->loadMultiple($facility_banner_nids); - - // Add the banners to the response. - foreach ($facility_banners as $entity) { - $this->addEntityToResponse($resource_type, $entity); - } - } - - /** - * Add a cacheable dependency and resource object for an entity. - * - * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type - * The resource type of the entity. - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity object to add to the response. - */ - protected function addEntityToResponse(ResourceType $resource_type, EntityInterface $entity) { - $this->addCacheableDependency($entity); - $this->addResourceObject(ResourceObject::createFromEntity($resource_type, $entity)); - } - - /** - * The endpoint cache must vary on the item-path. - */ - protected function addItemPathCacheableDependency() { - $item_path_context = (new CacheableMetadata())->addCacheContexts(['url.query_args:item-path']); - $this->addCacheableDependency($item_path_context); - } - - /** - * Add a CacheableDependency object to be used in constructing our response. - * - * @param mixed $cacheable_dependency - * The dependency object to add to our response. - */ - protected function addCacheableDependency($cacheable_dependency) { - if (!($cacheable_dependency instanceof CacheableMetadata)) { - $cacheable_dependency = CacheableMetadata::createFromObject($cacheable_dependency); - } - $this->cacheableDependencies[] = $cacheable_dependency; - } - - /** - * Add a response object to be used in constructing our response. - * - * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $resource_object - * The ResourceObject to add to our response. - */ - protected function addResourceObject(ResourceObject $resource_object) { - $this->resourceObjects[] = $resource_object; - } - -} diff --git a/docroot/modules/custom/va_gov_api/va_gov_api.routing.yml b/docroot/modules/custom/va_gov_api/va_gov_api.routing.yml index c7c5d584d3..96bcd6b699 100644 --- a/docroot/modules/custom/va_gov_api/va_gov_api.routing.yml +++ b/docroot/modules/custom/va_gov_api/va_gov_api.routing.yml @@ -1,14 +1,3 @@ -va_gov_api.banner_alerts: - path: '/%jsonapi%/banner-alerts' - methods: ['GET'] - defaults: - _jsonapi_resource: Drupal\va_gov_api\Resources\BannerAlerts - _jsonapi_resource_types: ['node--banner', 'node--full_width_banner_alert', 'node--promo_banner'] - requirements: - _permission: 'access content' - _content_type_format: api_json - _format: api_json - va_gov_api.new_banner_alerts: path: '/api/v1/banner-alerts' defaults: diff --git a/docroot/modules/custom/va_gov_api/va_gov_api.services.yml b/docroot/modules/custom/va_gov_api/va_gov_api.services.yml index b4be74fe78..0ef9a7810e 100644 --- a/docroot/modules/custom/va_gov_api/va_gov_api.services.yml +++ b/docroot/modules/custom/va_gov_api/va_gov_api.services.yml @@ -7,8 +7,3 @@ services: arguments: ['@request_stack'] tags: - { name: event_subscriber } -# va_gov_api.full_width_banner_alert_subscriber: -# class: Drupal\va_gov_api\EventSubscriber\FullWidthBannerAlertSubscriber -# arguments: ['@entity_type.manager'] -# tags: -# - { name: event_subscriber } diff --git a/tests/phpunit/API/BannerEndpointTest.php b/tests/phpunit/API/BannerEndpointTest.php index b4a52f1f36..92ba2331be 100644 --- a/tests/phpunit/API/BannerEndpointTest.php +++ b/tests/phpunit/API/BannerEndpointTest.php @@ -1,7 +1,8 @@ drupalLogin($this->createUser(['access content'])); + } + + /** + * Test that the banner endpoint handles full width alert banners. + */ + public function testFullWidthAlertBanner() { + // Look for a 'full_width_banner_alert' node. + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + $banner_nids = $node_storage->getQuery() + ->condition('type', 'full_width_banner_alert') + // ->condition('status', TRUE) + ->exists('field_banner_alert_vamcs') + ->range(0, 1) + ->accessCheck(FALSE) + ->execute(); + + // Load and publish the node. + $banner_nid = reset($banner_nids); + $banner = $node_storage->load($banner_nid); + + // Get 'field_banner_alert_vamcs' data. + $banner_alert_vamcs = $banner->get('field_banner_alert_vamcs')->getValue(); + + $banner->set('status', TRUE); + $banner->save(); + + // Look for a published node of type "vamc_operating_status_and_alerts". + $system_nids = $node_storage->getQuery() + ->condition('type', 'vamc_operating_status_and_alerts') + ->condition('status', TRUE) + ->condition('nid', $banner_alert_vamcs[0]['target_id']) + ->range(0, 1) + ->accessCheck(FALSE) + ->execute(); + + // Get first array value. + $system_nid = reset($system_nids); + + // Load the entity. + $system_node = $node_storage->load($system_nid); + // Get entity id from "field_office" field. + $office_nid = $system_node->get('field_office')->target_id; + $path_alias = \Drupal::database()->select('path_alias', 'pa') + ->fields('pa', ['alias']) + ->condition('path', '/node/' . $office_nid) + ->execute() + ->fetchField(); + + // Create 'situation_update' paragraph. + $situation_update = Paragraph::create([ + 'type' => 'situation_update', + 'status' => 1, + 'field_datetime_range_timezone' => [ + [ + 'value' => '2024-01-23T12:22:00+00:00', + 'end_value' => '2024-01-23T13:22:00+00:00', + ], + ], + 'field_send_email_to_subscribers' => [ + [ + 'value' => FALSE, + ], + ], + 'field_wysiwyg' => [ + [ + 'value' => '

!!!: Situation updates are included in the response.

', + 'format' => 'rich_text', + ], + ], + ]); + + // Add 'situation_update' paragraph to 'field_situation_updates' field. + $banner->get('field_situation_updates')->appendItem($situation_update); + $banner->save(); + + // Visit the banner endpoint using the path alias. + $this->visit('/api/v1/banner-alerts?path=' . $path_alias); + $this->assertEquals(200, $this->getSession()->getStatusCode()); + + $json = json_decode($this->getSession()->getPage()->getContent(), TRUE); + + // Confirm that the banner is included in the response. + $this->assertIsArray($json['data']); + + $filtered_nodes = array_filter($json['data'], function ($item) use ($banner) { + return isset($item['nid']) && $item['nid'] == $banner->id(); + }); + $this->assertNotEmpty($filtered_nodes, 'Node with nid ' . $banner->id() . ' found in response.'); + + // Confirm that the string "65130" is in the response. + $this->assertStringContainsString( + '!!!: Situation updates are included in the response.', + $this->getSession()->getPage()->getContent() + ); + + // Delete the paragraph. + $situation_update->delete(); + } + /** * Provides data to testBanner(). */ @@ -42,12 +148,9 @@ public function provideBannerTestData() { * @dataProvider provideBannerTestData */ public function testBanner($path, $shouldBeIncluded) { - $author = $this->createUser(); - $banner_data = [ 'field_alert_type' => 'information', 'title' => 'Test Banner', - 'uid' => $author->id(), 'field_target_paths' => [], 'type' => 'banner', ]; @@ -66,25 +169,93 @@ public function testBanner($path, $shouldBeIncluded) { // Make sure the banner is found in all the requests that it _should_ be // included in (and not in the places where it should not be). - $response = \Drupal::httpClient()->get($url . '/jsonapi/banner-alerts?item-path=' . $path); + $response = \Drupal::httpClient()->get($url . '/api/v1/banner-alerts?path=' . $path); $this->assertEquals('200', $response->getStatusCode(), 'request returned status code ' . $response->getStatusCode()); - $json = json_decode($response->getBody()); + $json = json_decode($response->getBody(), TRUE); + + $filtered_nodes = array_filter($json['data'], function ($item) use ($banner) { + return isset($item['nid']) && $item['nid'] == $banner->id(); + }); + + if ($shouldBeIncluded) { + $this->assertNotEmpty($filtered_nodes, 'Node with nid ' . $banner->id() . ' found in response.'); + } + else { + $this->assertEmpty($filtered_nodes, 'Node with nid ' . $banner->id() . ' not found in response.'); + } + } + + /** + * Provides data to testPromoBanner(). + */ + public function providePromorBannerTestData() { + return [ + 'first level path' => [ + '/banner-endpoint-test-path', + TRUE, + ], + 'second level path' => [ + '/banner-endpoint-test-path/second-level-path', + TRUE, + ], + 'third level path' => [ + '/banner-endpoint-test-path/second-level-path/one-more-segment', + TRUE, + ], + 'banner not present' => [ + '/banner-should-not-be-here', + FALSE, + ], + ]; + } - $found = FALSE; - foreach ($json->data as $item) { - if ((string) $item->attributes->drupal_internal__nid === $banner->id()) { - $found = TRUE; - break; - } + /** + * Test that promo banners are included in the banner endpoint response. + * + * @dataProvider providePromorBannerTestData + */ + public function testPromoBanner($path, $shouldBeIncluded) { + $banner_data = [ + 'field_promo_type' => 'announcement', + 'title' => 'Test Promo Banner', + 'field_link' => [ + ['uri' => 'internal:/node/50621'], + ], + 'field_target_paths' => [], + 'type' => 'promo_banner', + ]; + if ($shouldBeIncluded) { + $banner_data['field_target_paths'][] = ['value' => $path]; } + $banner = $this->createNode($banner_data); + $banner->set('moderation_state', 'published')->setPublished(TRUE)->save(); + + // This assertion isn't strictly necessary, but we need to have at least one + // per test. If the request below fails, we wouldn't have one. + $this->assertTrue($banner->isPublished(), 'banner ' . $banner->id() . ' is published'); + + $url = $this->baseUrl; + + // Make sure the banner is found in all the requests that it _should_ be + // included in (and not in the places where it should not be). + $response = \Drupal::httpClient()->get($url . '/api/v1/banner-alerts?path=' . $path); + $this->assertEquals('200', $response->getStatusCode(), 'request returned status code ' . $response->getStatusCode()); + + $json = json_decode($response->getBody(), TRUE); + + $filtered_nodes = array_filter($json['data'], function ($item) use ($banner) { + return isset($item['nid']) && $item['nid'] == $banner->id(); + }); + if ($shouldBeIncluded) { - $this->assertTrue($found, 'banner id ' . $banner->id() . ' was found in JSON API response for ' . $path); + $this->assertNotEmpty($filtered_nodes, 'Node with nid ' . $banner->id() . ' found in response.'); } else { - $this->assertFalse($found, 'banner id ' . $banner->id() . ' was not found in JSON API response for ' . $path); + $this->assertEmpty($filtered_nodes, 'Node with nid ' . $banner->id() . ' not found in response.'); } + } } diff --git a/tests/phpunit/API/NewBannerEndpointTest.php b/tests/phpunit/API/NewBannerEndpointTest.php deleted file mode 100644 index 8c617fa6e7..0000000000 --- a/tests/phpunit/API/NewBannerEndpointTest.php +++ /dev/null @@ -1,114 +0,0 @@ -drupalLogin($this->createUser(['access content'])); - } - - /** - * Test that the banner endpoint handles full width alert banners. - */ - public function testFullWidthAlertBanner() { - // Create 'situation_update' paragraph. - $situation_update = Paragraph::create([ - 'type' => 'situation_update', - 'status' => 1, - 'field_datetime_range_timezone' => [ - [ - 'value' => '2024-01-23T12:22:00+00:00', - 'end_value' => '2024-01-23T13:22:00+00:00', - ], - ], - 'field_send_email_to_subscribers' => [ - [ - 'value' => FALSE, - ], - ], - 'field_wysiwyg' => [ - [ - 'value' => '

blah blah blah

', - 'format' => 'rich_text', - ], - ], - ]); - - // Look for a 'full_width_banner_alert' node. - $node_storage = \Drupal::entityTypeManager()->getStorage('node'); - $banner_nids = $node_storage->getQuery() - ->condition('type', 'full_width_banner_alert') - // ->condition('status', TRUE) - ->range(0, 1) - ->accessCheck(FALSE) - ->execute(); - - // Load and publish the node. - $banner_nid = reset($banner_nids); - $banner = $node_storage->load($banner_nid); - - // Get 'field_banner_alert_vamcs' data. - $banner_alert_vamcs = $banner->get('field_banner_alert_vamcs')->getValue(); - - $banner->set('status', TRUE); - $banner->save(); - - // Look for a published node of type "vamc_operating_status_and_alerts". - $system_nids = $node_storage->getQuery() - ->condition('type', 'vamc_operating_status_and_alerts') - ->condition('status', TRUE) - ->condition('id', $banner_alert_vamcs[0]['target_id']) - ->range(0, 1) - ->accessCheck(FALSE) - ->execute(); - - // Get first array value. - $system_nid = reset($system_nids); - - // Load the entity. - $system_node = $node_storage->load($system_nid); - // Get entity id from "field_office" field. - $office_nid = $system_node->get('field_office')->target_id; - $path_alias = \Drupal::database()->select('path_alias', 'pa') - ->fields('pa', ['alias']) - ->condition('path', '/node/' . $office_nid) - ->execute() - ->fetchField(); - - // Visit the banner endpoint using the path alias. - $this->visit('/api/v1/banner-alerts?path=' . $path_alias); - $this->assertEquals(200, $this->getSession()->getStatusCode()); - - $json = json_decode($this->getSession()->getPage()->getContent(), TRUE); - - // Confirm that the banner is included in the response. - $this->assertIsArray($json['data']); - - $filtered_nodes = array_filter($json['data'], function ($item) use ($banner) { - return isset($item['nid']) && $item['nid'] == $banner->id(); - }); - - $this->assertNotEmpty($filtered_nodes, 'Node with nid ' . $banner->id() . ' found in response.'); - - // Confirm that the string "65130" is in the response. - $this->assertStringContainsString('"nid":' . $banner->id(), $this->getSession()->getPage()->getContent()); - - // Delete the paragraph. - $situation_update->delete(); - } - -} From 8b4e064449c7d1b1a3fc1a2536c7bbc15b1e5d15 Mon Sep 17 00:00:00 2001 From: Alex Finnarn Date: Tue, 30 Jan 2024 11:14:35 -0500 Subject: [PATCH 9/9] cleanup from review --- .../src/Controller/BannerAlertsController.php | 15 ++++++++++----- tests/phpunit/API/BannerEndpointTest.php | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php b/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php index 3602f92388..821cb7feb8 100644 --- a/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php +++ b/docroot/modules/custom/va_gov_api/src/Controller/BannerAlertsController.php @@ -13,7 +13,12 @@ use Symfony\Component\Serializer\SerializerInterface; /** - * Returns responses for Content API routes. + * Returns responses for the Banner Alerts API. + * + * Currently, there are three banner types included in the response: + * - Banner + * - Promo Banner + * - Full Width Banner Alert. */ class BannerAlertsController extends ControllerBase { @@ -98,7 +103,7 @@ public function bannerAlertsByPath(Request $request) { * Collect `banner` entities to be returned in the response. * * Given a path, retrieves any `banner` that should show there, constructs a - * ResponseObject for it, and adds it to cacheableDependencies. + * response for it, and adds it to cacheable dependencies. * * @param string $path * The path to the item to find banners for. @@ -142,7 +147,7 @@ protected function collectBannerData(string $path): array { * Collect `promo_banner` entities to be returned in the response. * * Given a path, retrieves any `promo_banner` that should show there, - * constructs a ResponseObject for it, and adds it to cacheableDependencies. + * constructs a Response for it, and adds it to cacheable dependencies. * * @param string $path * The path to the item to find promo_banners for. @@ -185,8 +190,8 @@ protected function collectPromoBannerData(string $path): array { * Collect `full_width_banner_alert` entities to be returned in the response. * * Given a path, retrieves any `full_width_banner_alert` that should show - * there, constructs a ResponseObject for it, and adds it to - * cacheableDependencies. + * there, constructs a response for it, and adds it to + * cacheable dependencies. * * @param string $path * The path to the item to find full_width_banner_alerts for. diff --git a/tests/phpunit/API/BannerEndpointTest.php b/tests/phpunit/API/BannerEndpointTest.php index 92ba2331be..b57be7faca 100644 --- a/tests/phpunit/API/BannerEndpointTest.php +++ b/tests/phpunit/API/BannerEndpointTest.php @@ -1,6 +1,6 @@