Skip to content

Commit

Permalink
Update the embed block to be a custom video embed that uses the desig…
Browse files Browse the repository at this point in the history
…n system video embed template (#62)

* Update the embed block to be a custom video embed that uses the design system video embed template

* Translate help text, and extrapolate the embed url from the link url

* Update tests

* Tighten up the allowed urls and update the embed extraction and tests

* override linting

* Linting disable for block only

* Move videoembed tests to their own class

* Use setUp method in tests to improve performance

* Extract getting the embed url to its own method

* Update VideoEmbedBlock test names

* Add tests for the embed_url in context

* Capitalisation tweak

* Speed up tests by using setUpTestData

* Add the embed_url var to the  object not directly to the context, to make the template vars more consistent
  • Loading branch information
helenb authored Jan 15, 2025
1 parent 0ecb45c commit 84bf319
Show file tree
Hide file tree
Showing 7 changed files with 421 additions and 18 deletions.
9 changes: 8 additions & 1 deletion cms/core/blocks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from .embeddable import DocumentBlock, DocumentsBlock, ImageBlock, ONSEmbedBlock
from .embeddable import (
DocumentBlock,
DocumentsBlock,
ImageBlock,
ONSEmbedBlock,
VideoEmbedBlock,
)
from .headline_figures import HeadlineFiguresBlock
from .markup import BasicTableBlock, HeadingBlock, QuoteBlock
from .panels import PanelBlock
Expand All @@ -17,4 +23,5 @@
"QuoteBlock",
"RelatedContentBlock",
"RelatedLinksBlock",
"VideoEmbedBlock",
]
89 changes: 89 additions & 0 deletions cms/core/blocks/embeddable.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import re
from typing import TYPE_CHECKING, ClassVar
from urllib.parse import urlparse

from django.conf import settings
from django.core.exceptions import ValidationError
Expand Down Expand Up @@ -98,3 +100,90 @@ def clean(self, value: "StructValue") -> "StructValue":
class Meta:
icon = "code"
template = "templates/components/streamfield/ons_embed_block.html"


class VideoEmbedBlock(blocks.StructBlock):
"""A video embed block."""

link_url = blocks.URLBlock(
help_text=_(
"The URL to the video hosted on YouTube or Vimeo, for example, "
"https://www.youtube.com/watch?v={ video ID } or https://vimeo.com/video/{ video ID }. "
"Used to link to the video when cookies are not enabled."
)
)
image = ImageChooserBlock(help_text=_("The video cover image, used when cookies are not enabled."))
title = blocks.CharBlock(help_text=_("The descriptive title for the video used by screen readers."))
link_text = blocks.CharBlock(
help_text=_("The text to be shown when cookies are not enabled e.g. 'Watch the {title} on Youtube'.")
)

def get_embed_url(self, link_url: str) -> str:
"""Get the embed URL for the video based on the link URL."""
embed_url = ""
# Vimeo
if urlparse(link_url).hostname in ["www.vimeo.com", "vimeo.com", "player.vimeo.com"]:
url_path = urlparse(link_url).path.strip("/")
# Handle different Vimeo URL patterns
if "video/" in url_path: # noqa: SIM108
# Handle https://vimeo.com/showcase/7934865/video/ID format
video_id = url_path.split("video/")[-1]
else:
# Handle https://player.vimeo.com/video/ID or https://vimeo.com/ID format
video_id = url_path.split("/")[0]
# Remove any query parameters from video ID
video_id = video_id.split("?")[0]
embed_url = "https://player.vimeo.com/video/" + video_id
# YouTube
elif urlparse(link_url).hostname in ["www.youtube.com", "youtube.com", "youtu.be"]:
url_parts = urlparse(link_url)
if url_parts.hostname == "youtu.be":
# Handle https://youtu.be/ID format
video_id = url_parts.path.lstrip("/")
elif "/v/" in url_parts.path:
# Handle https://www.youtube.com/v/ID format
video_id = url_parts.path.split("/v/")[-1]
else:
# Handle https://www.youtube.com/watch?v=ID format
query = dict(param.split("=") for param in url_parts.query.split("&"))
video_id = query.get("v", "")
# Remove any query parameters from video ID
video_id = video_id.split("?")[0]
embed_url = "https://www.youtube.com/embed/" + video_id
return embed_url

def get_context(self, value: "StreamValue", parent_context: dict | None = None) -> dict:
"""Get the embed URL for the video based on the link URL."""
context: dict = super().get_context(value, parent_context=parent_context)
context["value"]["embed_url"] = self.get_embed_url(value["link_url"])
return context

def clean(self, value: "StructValue") -> "StructValue":
"""Checks that the given embed and link urls match youtube or vimeo."""
errors = {}

vimeo_showcase_pattern = r"^https?://vimeo\.com/showcase/[^/]+/video/[^/]+$"
other_patterns = [
r"^https?://(?:[-\w]+\.)?youtube\.com/watch[^/]+$",
r"^https?://(?:[-\w]+\.)?youtube\.com/v/[^/]+$",
r"^https?://youtu\.be/[^/]+$",
r"^https?://vimeo\.com/[^/]+$",
r"^https?://player\.vimeo\.com/video/[^/]+$",
]

# Check if the URL is a Vimeo showcase URL - do this first to avoid it clashing
# with the r"^https?://vimeo\.com/[^/]+$", pattern
if re.match(vimeo_showcase_pattern, value["link_url"]):
return super().clean(value)

if not any(re.match(pattern, value["link_url"]) for pattern in other_patterns):
errors["link_url"] = ValidationError(_("The link URL must use a valid Vimeo or YouTube video URL"))

if errors:
raise StructBlockValidationError(block_errors=errors)

return super().clean(value)

class Meta:
icon = "code"
template = "templates/components/streamfield/video_embed_block.html"
4 changes: 2 additions & 2 deletions cms/core/blocks/stream_blocks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import ClassVar

from wagtail.blocks import RichTextBlock, StreamBlock
from wagtail.embeds.blocks import EmbedBlock
from wagtail.images.blocks import ImageChooserBlock
from wagtailmath.blocks import MathBlock

Expand All @@ -12,6 +11,7 @@
PanelBlock,
QuoteBlock,
RelatedLinksBlock,
VideoEmbedBlock,
)
from cms.core.blocks.section_blocks import SectionBlock

Expand All @@ -32,7 +32,7 @@ class CoreStoryBlock(StreamBlock):
rich_text = RichTextBlock()
quote = QuoteBlock()
panel = PanelBlock()
embed = EmbedBlock(group="Media")
video_embed = VideoEmbedBlock(group="Media")
image = ImageChooserBlock(group="Media")
documents = DocumentsBlock(group="Media")
related_links = RelatedLinksBlock()
Expand Down
Loading

0 comments on commit 84bf319

Please sign in to comment.