From 0b99bf6a2374eebe03ad2a7d86cff356029ae9cc Mon Sep 17 00:00:00 2001 From: Benjamin Rasmussen Date: Wed, 18 Sep 2024 10:40:42 +0200 Subject: [PATCH] Creating a legacy Redia RSS feed for events. DDFHER-60 This feed is needed for some legacy apps to continue working. It will not be updated in the future - rather, this is a 1-1 recreation of the old feed. --- ...w_display.media.image.redia_feed_large.yml | 36 +++ ...w_display.media.image.redia_feed_small.yml | 36 +++ ...ntity_view_mode.media.redia_feed_large.yml | 11 + ...ntity_view_mode.media.redia_feed_small.yml | 11 + config/sync/core.extension.yml | 1 + config/sync/image.style.redia_feed_large.yml | 15 ++ config/sync/image.style.redia_feed_small.yml | 15 ++ .../dpl_redia_legacy.info.yml | 9 + .../dpl_redia_legacy.routing.yml | 9 + .../Controller/RssFeeds/EventsController.php | 234 ++++++++++++++++ .../rest/resource/OpeningHoursResource.php | 249 ++++++++++++++++++ 11 files changed, 626 insertions(+) create mode 100644 config/sync/core.entity_view_display.media.image.redia_feed_large.yml create mode 100644 config/sync/core.entity_view_display.media.image.redia_feed_small.yml create mode 100644 config/sync/core.entity_view_mode.media.redia_feed_large.yml create mode 100644 config/sync/core.entity_view_mode.media.redia_feed_small.yml create mode 100644 config/sync/image.style.redia_feed_large.yml create mode 100644 config/sync/image.style.redia_feed_small.yml create mode 100644 web/modules/custom/dpl_redia_legacy/dpl_redia_legacy.info.yml create mode 100644 web/modules/custom/dpl_redia_legacy/dpl_redia_legacy.routing.yml create mode 100644 web/modules/custom/dpl_redia_legacy/src/Controller/RssFeeds/EventsController.php create mode 100644 web/modules/custom/dpl_redia_legacy/src/Plugin/rest/resource/OpeningHoursResource.php diff --git a/config/sync/core.entity_view_display.media.image.redia_feed_large.yml b/config/sync/core.entity_view_display.media.image.redia_feed_large.yml new file mode 100644 index 0000000000..6b9f0a460f --- /dev/null +++ b/config/sync/core.entity_view_display.media.image.redia_feed_large.yml @@ -0,0 +1,36 @@ +uuid: 014a5ed5-1c85-432f-a1b8-3814869178fd +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.media.redia_feed_large + - field.field.media.image.field_byline + - field.field.media.image.field_media_image + - image.style.redia_feed_large + - media.type.image + module: + - image +id: media.image.redia_feed_large +targetEntityType: media +bundle: image +mode: redia_feed_large +content: + field_media_image: + type: image + label: hidden + settings: + image_link: '' + image_style: redia_feed_large + image_loading: + attribute: lazy + third_party_settings: { } + weight: 0 + region: content +hidden: + created: true + field_byline: true + langcode: true + name: true + search_api_excerpt: true + thumbnail: true + uid: true diff --git a/config/sync/core.entity_view_display.media.image.redia_feed_small.yml b/config/sync/core.entity_view_display.media.image.redia_feed_small.yml new file mode 100644 index 0000000000..a3545c59f0 --- /dev/null +++ b/config/sync/core.entity_view_display.media.image.redia_feed_small.yml @@ -0,0 +1,36 @@ +uuid: 7cd49745-6747-4b76-b64a-f3e2a22a338e +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.media.redia_feed_small + - field.field.media.image.field_byline + - field.field.media.image.field_media_image + - image.style.redia_feed_small + - media.type.image + module: + - image +id: media.image.redia_feed_small +targetEntityType: media +bundle: image +mode: redia_feed_small +content: + field_media_image: + type: image + label: hidden + settings: + image_link: '' + image_style: redia_feed_small + image_loading: + attribute: lazy + third_party_settings: { } + weight: 0 + region: content +hidden: + created: true + field_byline: true + langcode: true + name: true + search_api_excerpt: true + thumbnail: true + uid: true diff --git a/config/sync/core.entity_view_mode.media.redia_feed_large.yml b/config/sync/core.entity_view_mode.media.redia_feed_large.yml new file mode 100644 index 0000000000..a899015970 --- /dev/null +++ b/config/sync/core.entity_view_mode.media.redia_feed_large.yml @@ -0,0 +1,11 @@ +uuid: 74eba980-7ec0-4b81-a6da-5c33830b08d9 +langcode: en +status: true +dependencies: + module: + - media +id: media.redia_feed_large +label: 'Redia feed - large' +description: '' +targetEntityType: media +cache: true diff --git a/config/sync/core.entity_view_mode.media.redia_feed_small.yml b/config/sync/core.entity_view_mode.media.redia_feed_small.yml new file mode 100644 index 0000000000..ab70019940 --- /dev/null +++ b/config/sync/core.entity_view_mode.media.redia_feed_small.yml @@ -0,0 +1,11 @@ +uuid: fd0b39a0-f244-4d82-af84-54c63cf451bc +langcode: en +status: true +dependencies: + module: + - media +id: media.redia_feed_small +label: 'Redia feed - small' +description: '' +targetEntityType: media +cache: true diff --git a/config/sync/core.extension.yml b/config/sync/core.extension.yml index 06913fdc60..90296dd028 100644 --- a/config/sync/core.extension.yml +++ b/config/sync/core.extension.yml @@ -66,6 +66,7 @@ module: dpl_react: 0 dpl_react_apps: 0 dpl_recommender: 0 + dpl_redia_legacy: 0 dpl_related_content: 0 dpl_reservations: 0 dpl_rest_base: 0 diff --git a/config/sync/image.style.redia_feed_large.yml b/config/sync/image.style.redia_feed_large.yml new file mode 100644 index 0000000000..617326ab94 --- /dev/null +++ b/config/sync/image.style.redia_feed_large.yml @@ -0,0 +1,15 @@ +uuid: 923e70e6-ea34-446d-ae06-2f6ac0ac4f00 +langcode: en +status: true +dependencies: { } +name: redia_feed_large +label: 'Redia feed large' +effects: + 71f3eb2a-c6f8-4a85-a16f-651a6e43239e: + uuid: 71f3eb2a-c6f8-4a85-a16f-651a6e43239e + id: image_scale + weight: 1 + data: + width: 2000 + height: null + upscale: false diff --git a/config/sync/image.style.redia_feed_small.yml b/config/sync/image.style.redia_feed_small.yml new file mode 100644 index 0000000000..bfd8dbefc4 --- /dev/null +++ b/config/sync/image.style.redia_feed_small.yml @@ -0,0 +1,15 @@ +uuid: 33f9623f-9eea-4b05-8e9c-f1d3ac224119 +langcode: en +status: true +dependencies: { } +name: redia_feed_small +label: 'Redia feed small' +effects: + 2cd37185-a9d4-43e1-ba81-66b73d18cae2: + uuid: 2cd37185-a9d4-43e1-ba81-66b73d18cae2 + id: image_scale + weight: 1 + data: + width: 220 + height: null + upscale: true diff --git a/web/modules/custom/dpl_redia_legacy/dpl_redia_legacy.info.yml b/web/modules/custom/dpl_redia_legacy/dpl_redia_legacy.info.yml new file mode 100644 index 0000000000..9fbcc60f6c --- /dev/null +++ b/web/modules/custom/dpl_redia_legacy/dpl_redia_legacy.info.yml @@ -0,0 +1,9 @@ +name: "DPL Redia Legacy feeds" +type: module +description: "Various legacy feeds, used for the Redia App." +package: DPL +core_version_requirement: ^10 + +dependencies: + - dpl_opening_hours:dpl_opening_hours + - drupal:drupal_typed diff --git a/web/modules/custom/dpl_redia_legacy/dpl_redia_legacy.routing.yml b/web/modules/custom/dpl_redia_legacy/dpl_redia_legacy.routing.yml new file mode 100644 index 0000000000..14909ab3f1 --- /dev/null +++ b/web/modules/custom/dpl_redia_legacy/dpl_redia_legacy.routing.yml @@ -0,0 +1,9 @@ +dpl_redia_legacy.events: + path: '/ding-redia-rss/event' + defaults: + _controller: '\Drupal\dpl_redia_legacy\Controller\RssFeeds\EventsController::getFeed' + _title: 'Redia APP Event' + requirements: + _permission: 'access content' + options: + _format: 'xml' diff --git a/web/modules/custom/dpl_redia_legacy/src/Controller/RssFeeds/EventsController.php b/web/modules/custom/dpl_redia_legacy/src/Controller/RssFeeds/EventsController.php new file mode 100644 index 0000000000..1b96b0a19c --- /dev/null +++ b/web/modules/custom/dpl_redia_legacy/src/Controller/RssFeeds/EventsController.php @@ -0,0 +1,234 @@ +get('file_url_generator'), + $container->get('request_stack'), + + ); + } + + /** + * Getting the RSS/XML feed of the items. + */ + public function getFeed(): Response { + $items = $this->getItems(); + + $rss_content = $this->buildRss($items); + + $response = new Response(); + $response->setContent($rss_content); + $response->headers->set('Content-Type', 'application/rss+xml'); + return $response; + } + + /** + * Loading events, and turning it into a simple array of relevant values. + * + * @return array + * An array of necessary item fields, used in buildRss(). + */ + private function getItems(): array { + + $storage = $this->entityTypeManager()->getStorage('eventinstance'); + $query = $storage->getQuery() + ->condition('status', TRUE) + ->accessCheck(TRUE) + ->sort('date.value'); + + $ids = $query->execute(); + + $events = EventInstance::loadMultiple($ids); + + $items = []; + + foreach ($events as $event) { + /** @var \Drupal\node\NodeInterface[] $branches */ + $branches = $event->get('branch')->referencedEntities(); + $branch = reset($branches); + $event_dates = $event->get('date')->getValue(); + $changed_date = DrupalDateTime::createFromFormat('U', strval($event->getChangedTime())); + + $items[] = [ + 'title' => $event->label(), + 'description' => $this->getEventDescription($event), + 'author' => $event->getOwner()->get('field_author_name')->getString(), + 'id' => $event->id(), + 'date' => $changed_date->format('r'), + 'promoted' => FALSE, + 'subtitle' => $event->get('event_description')->getString(), + 'start_time' => $event_dates[0]['value'] ?? NULL, + 'end_time' => $event_dates[0]['end_value'] ?? NULL, + 'media' => $this->getEventImageFields($event, 'redia_feed_large'), + 'media_thumbnail' => $this->getEventImageFields($event, 'redia_feed_small'), + 'branch' => [ + 'label' => $branch ? $branch->label() : NULL, + 'id' => $branch ? $branch->id() : NULL, + ], + ]; + } + + return $items; + } + + /** + * Turning event image into fields that Redia understands. + * + * @return array|null + * The fields that Redia understands (or nothing). + */ + private function getEventImageFields(EventInstance $event, string $image_style) { + $media_field = $event->get('event_image'); + + if (!($media_field instanceof FieldItemListInterface)) { + return NULL; + } + + $media = $media_field->referencedEntities()[0] ?? NULL; + $file_field_name = 'field_media_image'; + + if (!($media instanceof MediaInterface) || !$media->hasField($file_field_name)) { + return NULL; + } + + // @phpstan-ignore-next-line PHPStan does not know that entity is available. + $file = $media->get($file_field_name)->first()?->entity; + + if (!($file instanceof FileInterface)) { + return NULL; + } + + $file_uri = $file->getFileUri(); + $style = $this->entityTypeManager()->getStorage('image_style')->load($image_style); + + if (empty($file_uri) || !($style instanceof ImageStyleInterface)) { + return NULL; + } + + $image_url = $style->buildUrl($file_uri); + $image_sizes = getimagesize($file_uri); + $file_size = filesize($file_uri); + + return [ + // Generating a unique MD5. + 'md5' => md5($image_url . $file_size), + 'url' => $image_url, + 'type' => $file->getMimeType(), + 'size' => filesize($file_uri), + 'width' => $image_sizes[0] ?? NULL, + 'height' => $image_sizes[1] ?? NULL, + ]; + } + + /** + * Getting the first paragraph as text, to use as description. + */ + private function getEventDescription(EventInstance $event): ?string { + /** @var \Drupal\paragraphs\ParagraphInterface[] $paragraphs */ + $paragraphs = $event->get('event_paragraphs')->referencedEntities(); + + foreach ($paragraphs as $paragraph) { + if ($paragraph->bundle() === 'text_body') { + return $paragraph->get('field_body')->getValue()[0]['value'] ?? NULL; + } + } + + return NULL; + } + + /** + * Building the actual RSS feed, from the items and site information. + * + * @param array $items + * See $this->getItems();. + */ + private function buildRss(array $items): string { + $config = $this->config('system.site'); + $site_title = $config->get('name'); + $site_url = $this->requestStack->getCurrentRequest()?->getSchemeAndHttpHost(); + $feed_url = Url::fromRoute('dpl_redia_legacy.events'); + $feed_url->setAbsolute(); + $feed_url = $feed_url->toString(); + + $current_date = new DrupalDateTime(); + $date = $current_date->format('r'); + + $rss_feed = ''; + $rss_feed .= ''; + $rss_feed .= ''; + $rss_feed .= "$site_title"; + $rss_feed .= "$site_url"; + $rss_feed .= ''; + $rss_feed .= 'da'; + $rss_feed .= "$date"; + $rss_feed .= "$date"; + + foreach ($items as $item) { + $rss_feed .= ''; + $rss_feed .= "{$item['title']}"; + $rss_feed .= "{$item['description']}"; + $rss_feed .= "{$item['author']}"; + $rss_feed .= "{$item['id']}"; + $rss_feed .= "{$item['date']}"; + $rss_feed .= "$site_title"; + $rss_feed .= " + {$item['media']['md5']} + "; + + $rss_feed .= ""; + + $rss_feed .= "{$item['subtitle']}"; + $rss_feed .= "{$item['start_time']}"; + $rss_feed .= "{$item['end_time']}"; + $rss_feed .= "{$item['branch']['label']}"; + $rss_feed .= "{$item['branch']['id']}"; + + $promoted_title = $item['promoted'] ? 'Sandt' : 'Falsk'; + $rss_feed .= "$promoted_title"; + + $rss_feed .= ''; + } + + $rss_feed .= ''; + $rss_feed .= ''; + + return $rss_feed; + } + +} diff --git a/web/modules/custom/dpl_redia_legacy/src/Plugin/rest/resource/OpeningHoursResource.php b/web/modules/custom/dpl_redia_legacy/src/Plugin/rest/resource/OpeningHoursResource.php new file mode 100644 index 0000000000..0174c8e489 --- /dev/null +++ b/web/modules/custom/dpl_redia_legacy/src/Plugin/rest/resource/OpeningHoursResource.php @@ -0,0 +1,249 @@ +getParameter('serializer.formats'); + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $serializer_formats, + $container->get('logger.factory')->get('rest'), + $container->get('dpl_opening_hours.custom_serializer'), + $container->get('dpl_opening_hours.repository'), + $container->get('dpl_opening_hours.mapper'), + $container->get('cache_tags.invalidator') + ); + } + + /** + * {@inheritdoc} + */ + public function getPluginDefinition(): array { + return NestedArray::mergeDeep( + parent::getPluginDefinition(), + [ + 'route_parameters' => [ + Request::METHOD_GET => [ + 'from_date' => [ + 'name' => 'from_date', + 'type' => 'string', + 'format' => 'date', + 'description' => 'Retrieve opening hours which occur after and including the provided date. In ISO 8601 format.', + 'in' => 'query', + 'required' => TRUE, + ], + 'to_date' => [ + 'name' => 'to_date', + 'type' => 'string', + 'format' => 'date', + 'description' => 'Retrieve opening hours which occur before and including the provided date. In ISO 8601 format.', + 'in' => 'query', + 'required' => TRUE, + ], + 'nid' => [ + 'name' => 'nid', + 'type' => 'integer', + 'description' => 'The (node) id of the node (Branch) that the opening hour applies to.', + 'in' => 'query', + 'required' => TRUE, + ], + ], + ], + ], + [ + 'responses' => [ + Response::HTTP_OK => [ + 'description' => Response::$statusTexts[Response::HTTP_OK], + 'schema' => [ + "type" => "array", + "items" => $this->openingHoursLegacyInstanceSchema(), + ], + ], + Response::HTTP_BAD_REQUEST => [ + 'description' => Response::$statusTexts[Response::HTTP_BAD_REQUEST], + ], + Response::HTTP_INTERNAL_SERVER_ERROR => [ + 'description' => Response::$statusTexts[Response::HTTP_INTERNAL_SERVER_ERROR], + ], + ], + ] + ); + } + + /** + * Responds to GET requests. + */ + public function get(Request $request): Response { + $typedRequest = new RequestTyped($request); + + $nid = $typedRequest->getInt('nid'); + if ($nid === NULL) { + throw new BadRequestHttpException("The 'nid' parameter is required."); + } + $fromDate = $typedRequest->getDateTime('from_date'); + if ($fromDate === NULL) { + throw new BadRequestHttpException("The 'from_date' parameter is required."); + } + $toDate = $typedRequest->getDateTime('to_date'); + if ($toDate === NULL) { + throw new BadRequestHttpException("The 'to_date' parameter is required."); + } + + try { + $openingHoursInstances = $this->repository->loadMultiple( + $nid, + $fromDate, + $toDate + ); + + $responseData = array_map(function (OpeningHoursInstance $instance) : + OpeningHoursLegacyResponse { + return $this->toLegacyResponse($instance); + }, $openingHoursInstances); + + $context = (new DefaultSerializationContextFactory()) + ->createSerializationContext(); + $context->setSerializeNull(TRUE); + + // Cast the serializer to the custom serializer type, to access the custom + // method and satisfy phpstan. + $serializer = $this->serializer; + /** @var \Drupal\dpl_opening_hours\Plugin\rest\resource\v1\CustomContextSerializer $serializer */ + + return (new CacheableResponse($serializer->serializeWithCustomContext($responseData, + $this->serializerFormat($request), $context))) + ->addCacheableDependency($this->cachableMetadata()); + } + catch (\TypeError $e) { + throw new BadRequestHttpException("Invalid input: {$e->getMessage()}",); + } + + } + + /** + * Map a value object to an OpenAPI response. + * + * @throws \Exception + */ + public function toLegacyResponse(OpeningHoursInstance $instance) : OpeningHoursLegacyResponse { + return (new OpeningHoursLegacyResponse()) + ->setNid(intval($instance->branch->id())) + ->setCategoryTid(intval($instance->categoryTerm->id())) + ->setDate(new DateTime($instance->startTime->format('Y-m-d'))) + ->setStartTime($instance->startTime->format("H:i")) + ->setEndTime($instance->endTime->format('H:i')) + ->setNotice(NULL); + } + + /** + * Generate a schema for an opening hours legacy instance. + * + * This allows for reuse across implementing classes. + * + * @return mixed[] + * OpenAPI schema for a single opening hours legacy instance. + */ + protected function openingHoursLegacyInstanceSchema(): array { + return [ + "type" => "object", + "properties" => [ + "nid" => [ + "type" => "integer", + "description" => "The node Id of the branch the opening hours instance belongs to.", + ], + "category_tid" => [ + "type" => "integer", + "description" => "The (t)id of the opening hours category.", + ], + "date" => [ + "type" => "string", + "format" => "date", + "description" => "The date which the opening hours applies to. In ISO 8601 format.", + ], + "start_time" => [ + "type" => "string", + "example" => "09:00", + "description" => "When the opening hours start. In format HH:MM", + ], + "end_time" => [ + "type" => "string", + "example" => "17:00", + "description" => "When the opening hours end. In format HH:MM", + ], + "notice" => [ + "type" => "string", + "description" => "Additional notice regarding the opening hours.", + "nullable" => TRUE, + ], + ], + "required" => [ + "nid", + "category_tid", + "date", + "start_time", + "end_time", + "notice", + ], + ]; + } + + /** + * Generate the format to use by the serializer from the request. + */ + protected function serializerFormat(Request $request): string { + $contentTypeFormat = $request->getContentTypeFormat(); + if (!$contentTypeFormat) { + // Default to JSON format. Some code generators will not provide a default + // value even though it is provided in the spec. + $contentTypeFormat = $request->get('_format', 'json'); + } + $mimeType = $request->getMimeType($contentTypeFormat); + if (!$mimeType) { + throw new \InvalidArgumentException("Unable to identify serializer format from content type form: $contentTypeFormat"); + } + return $mimeType; + } + +}