diff --git a/composer.json b/composer.json index 6239409883..73e7a4f102 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ "openeuropa/oe_corporate_countries": "^2.0.0-alpha6", "openeuropa/oe_media": "^1.22", "openeuropa/oe_multilingual": "dev-master", - "openeuropa/oe_paragraphs": "^1.17", + "openeuropa/oe_paragraphs": "dev-EWPP-3295", "openeuropa/oe_search": "^1.9", "openeuropa/oe_webtools": "^1.21", "openeuropa/rdf_skos": "^1.0.0-alpha9", diff --git a/oe_theme.theme b/oe_theme.theme index 4f826b63dc..4de0163653 100644 --- a/oe_theme.theme +++ b/oe_theme.theme @@ -2204,3 +2204,88 @@ function oe_theme_preprocess_oe_theme_helper_in_page_navigation__node__oe_person } } } + +/** + * Implements hook_preprocess_paragraph() for paragraph--oe-av-media.html.twig. + */ +function oe_theme_preprocess_paragraph__oe_av_media(array &$variables): void { + /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */ + $paragraph = $variables['paragraph']; + + // Bail out if there is no media present. + if ($paragraph->get('field_oe_media')->isEmpty()) { + return; + } + + $cacheability = CacheableMetadata::createFromRenderArray($variables); + + /** @var \Drupal\media\Entity\Media $media */ + $media = $paragraph->get('field_oe_media')->entity; + if (!$media instanceof MediaInterface) { + // The media entity is not available anymore, bail out. + return; + } + + // Retrieve the correct media translation. + $media = \Drupal::service('entity.repository')->getTranslationFromContext($media, $paragraph->language()->getId()); + + // Caches are handled by the formatter usually. Since we are not rendering + // the original render arrays, we need to propagate our caches to the + // paragraph template. + $cacheability->addCacheableDependency($media); + + // Run access checks on the media entity. + $access = $media->access('view', $variables['user'], TRUE); + $cacheability->addCacheableDependency($access); + $cacheability->applyTo($variables); + if (!$access->isAllowed()) { + $cacheability->applyTo($variables); + return; + } + + // Get the media source. + $source = $media->getSource(); + + if ($source instanceof MediaAvPortalVideoSource || $source instanceof OEmbed || $source instanceof Iframe) { + if ($source instanceof MediaAvPortalVideoSource) { + // Default video aspect ratio is set to 16:9 for AV Portal Video. + $variables['ratio'] = '16:9'; + } + + // Load information about the media and the display. + $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($media->bundle()); + $cacheability->addCacheableDependency($media_type); + $source_field = $source->getSourceFieldDefinition($media_type); + $display = EntityViewDisplay::collectRenderDisplay($media, 'oe_theme_main_content'); + $cacheability->addCacheableDependency($display); + $display_options = $display->getComponent($source_field->getName()); + + // If it is an OEmbed resource, render it and pass it as embeddable data + // only if it is of type video or html. + if ($source instanceof OEmbed) { + $oembed_type = $source->getMetadata($media, 'type'); + if (in_array($oembed_type, ['video', 'html'])) { + $variables['video'] = $media->{$source_field->getName()}->view($display_options); + $cacheability->applyTo($variables); + return; + } + } + + // If it's an AV Portal video or a Video iframe, render it. + $variables['video'] = $media->{$source_field->getName()}->view($display_options); + $cacheability->applyTo($variables); + + // When dealing with iframe videos, also respect its given aspect ratio. + if ($media->bundle() === 'video_iframe') { + $ratio = $media->get('oe_media_iframe_ratio')->value; + $variables['ratio'] = str_replace('_', '-', $ratio); + } + return; + } + + // If it's an image media, render it and assign it to the image variable. + if ($source instanceof MediaAvPortalPhotoSource || $source instanceof Image) { + $thumbnail = $media->get('thumbnail')->first(); + $variables['image'] = ImageValueObject::fromStyledImageItem($thumbnail, 'oe_theme_medium_no_crop'); + } +} diff --git a/templates/paragraphs/paragraph--oe-av-media.html.twig b/templates/paragraphs/paragraph--oe-av-media.html.twig new file mode 100644 index 0000000000..d20effcb40 --- /dev/null +++ b/templates/paragraphs/paragraph--oe-av-media.html.twig @@ -0,0 +1,22 @@ +{# +/** + * @file + * Theme override to display the 'Media' type paragraph. + * + * @see ./modules/contrib/paragraphs/templates/paragraph.html.twig + */ +#} +{% if image %} + {% set media_container = { + 'image': image.src, + 'alt': image.alt, + } %} +{% elseif video %} + {% set media_container = { + 'embedded_media': video, + 'ratio': ratio, + } %} +{% endif %} +{% if media_container %} + {% include '@ecl/media-container/media-container.html.twig' with media_container only %} +{% endif %} diff --git a/tests/src/Kernel/Paragraphs/MediaParagraphsTest.php b/tests/src/Kernel/Paragraphs/MediaParagraphsTest.php index 3cfb90e6ee..5c4036a5c5 100644 --- a/tests/src/Kernel/Paragraphs/MediaParagraphsTest.php +++ b/tests/src/Kernel/Paragraphs/MediaParagraphsTest.php @@ -39,6 +39,7 @@ class MediaParagraphsTest extends ParagraphsTestBase { 'file_link', 'oe_paragraphs_carousel', 'composite_reference', + 'oe_paragraphs_av_media', ]; /** @@ -61,6 +62,7 @@ protected function setUp(): void { 'options', 'oe_media_iframe', 'oe_paragraphs_carousel', + 'oe_paragraphs_av_media', ]); // Call the install hook of the Media module. module_load_include('install', 'media'); @@ -1089,4 +1091,168 @@ public function testCarousel(): void { $assert->assertPattern($expected_values, $html); } + /** + * Test Media paragraph rendering. + */ + public function testMediaParagraph(): void { + // Set image media translatable. + $this->container->get('content_translation.manager')->setEnabled('media', 'image', TRUE); + // Make the image field translatable. + $field_config = $this->container->get('entity_type.manager')->getStorage('field_config')->load('media.image.oe_media_image'); + $field_config->set('translatable', TRUE)->save(); + $this->container->get('router.builder')->rebuild(); + + // Create English file. + $en_file = file_save_data(file_get_contents(drupal_get_path('theme', 'oe_theme') . '/tests/fixtures/example_1.jpeg'), 'public://example_1_en.jpeg'); + $en_file->setPermanent(); + $en_file->save(); + + // Create Bulgarian file. + $bg_file = file_save_data(file_get_contents(drupal_get_path('theme', 'oe_theme') . '/tests/fixtures/example_1.jpeg'), 'public://example_1_bg.jpeg'); + $bg_file->setPermanent(); + $bg_file->save(); + + // Create a media. + $media_storage = $this->container->get('entity_type.manager')->getStorage('media'); + $media = $media_storage->create([ + 'bundle' => 'image', + 'name' => 'test image en', + 'oe_media_image' => [ + 'target_id' => $en_file->id(), + 'alt' => 'Alt en', + ], + ]); + $media->save(); + // Translate the media to Bulgarian. + $media_bg = $media->addTranslation('bg', [ + 'name' => 'test image bg', + 'oe_media_image' => [ + 'target_id' => $bg_file->id(), + 'alt' => 'Alt bg', + ], + ]); + $media_bg->save(); + + // Create a Media paragraph. + $paragraph = $this->container + ->get('entity_type.manager') + ->getStorage('paragraph')->create([ + 'type' => 'oe_av_media', + 'field_oe_media' => [ + 'target_id' => $media->id(), + ], + ]); + $paragraph->save(); + + // Add Bulgarian translation. + $paragraph->addTranslation('bg', $paragraph->toArray())->save(); + + // Test the translated media is rendered with the translated paragraph. + $html = $this->renderParagraph($paragraph); + $crawler = new Crawler($html); + + $this->assertCount(1, $crawler->filter('figure.ecl-media-container img.ecl-media-container__media')); + $image_element = $crawler->filter('figure.ecl-media-container img.ecl-media-container__media'); + $this->assertStringContainsString('example_1_en.jpeg', $image_element->attr('src')); + + // Assert bulgarian rendering. + $html = $this->renderParagraph($paragraph, 'bg'); + $crawler = new Crawler($html); + $this->assertCount(1, $crawler->filter('figure.ecl-media-container img.ecl-media-container__media')); + $image_element = $crawler->filter('figure.ecl-media-container img.ecl-media-container__media'); + $this->assertStringContainsString('example_1_bg.jpeg', $image_element->attr('src')); + + // Unpublish the media and assert it is not rendered anymore. + $media->set('status', 0); + $media->save(); + + // Since static cache is not cleared due to lack of requests in the test we + // need to reset manually. + $this->container->get('entity_type.manager')->getAccessControlHandler('media')->resetCache(); + + $html = $this->renderParagraph($paragraph); + $crawler = new Crawler($html); + $this->assertCount(0, $crawler->filter('figure.ecl-media-container')); + + // Create a remote video and add it to the paragraph. + $media = $media_storage->create([ + 'bundle' => 'remote_video', + 'oe_media_oembed_video' => [ + 'value' => 'https://www.youtube.com/watch?v=1-g73ty9v04', + ], + ]); + $media->save(); + $paragraph->set('field_oe_media', ['target_id' => $media->id()]); + $paragraph->save(); + + $html = $this->renderParagraph($paragraph); + $crawler = new Crawler($html); + // Assert remote video is rendered properly. + $this->assertCount(1, $crawler->filter('figure.ecl-media-container div.ecl-media-container__media')); + $media_container = $crawler->filter('div.ecl-media-container__media'); + $existing_classes = $media_container->attr('class'); + $existing_classes = explode(' ', $existing_classes); + $this->assertNotContains('ecl-media-container__media--ratio-16-9', $existing_classes); + $video_iframe = $media_container->filter('iframe'); + $partial_iframe_url = Url::fromRoute('media.oembed_iframe', [], [ + 'query' => [ + 'url' => 'https://www.youtube.com/watch?v=1-g73ty9v04', + ], + ])->toString(); + $this->assertStringContainsString($partial_iframe_url, $video_iframe->attr('src')); + $this->assertStringContainsString('459', $video_iframe->attr('width')); + $this->assertStringContainsString('344', $video_iframe->attr('height')); + + // Create an avportal video and add it to the paragraph. + $media = $media_storage->create([ + 'bundle' => 'av_portal_video', + 'oe_media_avportal_video' => 'I-163162', + ]); + $media->save(); + $paragraph->set('field_oe_media', ['target_id' => $media->id()]); + $paragraph->save(); + + // Assert AV Portal video is rendered properly. + $html = $this->renderParagraph($paragraph); + $crawler = new Crawler($html); + $this->assertCount(1, $crawler->filter('figure.ecl-media-container div.ecl-media-container__media')); + $media_container = $crawler->filter('div.ecl-media-container__media'); + $this->assertEquals('', $media_container->html()); + + // Create Video iframe with ratio 16:9 and add it to the paragraph. + $media = $media_storage->create([ + 'bundle' => 'video_iframe', + 'oe_media_iframe' => '', + 'oe_media_iframe_ratio' => '16_9', + ]); + $media->save(); + $paragraph->set('field_oe_media', ['target_id' => $media->id()]); + $paragraph->save(); + + $html = $this->renderParagraph($paragraph); + $crawler = new Crawler($html); + $this->assertCount(1, $crawler->filter('figure.ecl-media-container div.ecl-media-container__media')); + $media_container = $crawler->filter('div.ecl-media-container__media'); + $this->assertEquals('', $media_container->html()); + + // Create iframe video with aspect ratio 1:1 and add it to the paragraph. + $media = $media_storage->create([ + 'bundle' => 'video_iframe', + 'oe_media_iframe' => '', + 'oe_media_iframe_ratio' => '1_1', + ]); + $media->save(); + $paragraph->set('field_oe_media', ['target_id' => $media->id()]); + $paragraph->save(); + + $html = $this->renderParagraph($paragraph); + $crawler = new Crawler($html); + $this->assertCount(1, $crawler->filter('figure.ecl-media-container div.ecl-media-container__media')); + $media_container = $crawler->filter('div.ecl-media-container__media'); + $this->assertEquals('', $media_container->html()); + $existing_classes = $media_container->attr('class'); + $existing_classes = explode(' ', $existing_classes); + $this->assertTrue(in_array('ecl-media-container__media--ratio-1-1', $existing_classes)); + } + }