Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add event subscriber to include situation updates for full banner width alerts #16883

Merged
merged 15 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
<?php

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;
use Drupal\Core\Path\PathValidatorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\SerializerInterface;

/**
* Returns responses for Content API routes.
*/
class BannerAlertsController extends ControllerBase {

/**
* The serializer service.
*
* @var \Symfony\Component\Serializer\SerializerInterface
*/
protected $serializer;

/**
* Constructor.
*
* @param \Symfony\Component\Serializer\SerializerInterface $serializer
* The serializer service.
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
* The path matcher service.
* @param \Drupal\Core\Path\PathValidatorInterface $path_validator
* The path validator service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
*/
public function __construct(
SerializerInterface $serializer,
PathMatcherInterface $path_matcher,
PathValidatorInterface $path_validator,
EntityTypeManagerInterface $entity_type_manager) {
$this->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, $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);

$response = new CacheableJsonResponse([
'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']);
alexfinnarn marked this conversation as resolved.
Show resolved Hide resolved
$cache_tags = array_merge($banner_cache_tags, $promo_cache_tags, $full_width_banner_alert_cache_tags);
$response->getCacheableMetadata()->addCacheTags($cache_tags);

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.
*/
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 = [];
$cache_tags = [];
foreach ($banners as $entity) {
$banner_data[] = $this->serializer->normalize($entity);
$cache_tags = array_merge($cache_tags, $entity->getCacheTags());
}

return [$this->flattenData($banner_data), $cache_tags];
}

/**
* 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 = [];
$cache_tags = [];
foreach ($promo_banners as $entity) {
$promo_banner_data[] = $this->serializer->normalize($entity);
$cache_tags = array_merge($cache_tags, $entity->getCacheTags());
}
return [$this->flattenData($promo_banner_data), $cache_tags];
}

/**
* 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 = [];
$cache_tags = [];
foreach ($facility_banners as $entity) {
$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);
}
$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 [$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.
alexfinnarn marked this conversation as resolved.
Show resolved Hide resolved
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.
alexfinnarn marked this conversation as resolved.
Show resolved Hide resolved
$result[$key] = $firstElement;
}
}
else {
// Copy the value as is.
$result[$key] = $value;
}
}
return $result;
},
$data);
}

}
Loading
Loading