feat: add cta buttons to public video xblock page

* feat: video share page buttons
* feat: pass along utm params
* feat: disbale register button on public video page
* fix: convert to class-based view
This commit is contained in:
Jansen Kantor
2023-03-07 14:24:27 -05:00
committed by GitHub
parent 674bf2a040
commit ef30d2d32e
6 changed files with 221 additions and 104 deletions

View File

@@ -8,7 +8,7 @@ import logging
import urllib
from collections import OrderedDict, namedtuple
from datetime import datetime
from urllib.parse import quote_plus, urljoin
from urllib.parse import quote_plus, urlencode, urljoin, urlparse, urlunparse
import bleach
import requests
@@ -1650,102 +1650,6 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
return render_to_response('courseware/courseware-chromeless.html', context)
def _render_public_video_xblock(request, usage_key_string, is_embed=False):
"""
Look up a given usage key and render the "public" view or the "embed" view
"""
view = 'public_view'
if is_embed:
template = 'public_video_share_embed.html'
else:
template = 'public_video.html'
usage_key = UsageKey.from_string(usage_key_string)
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
course_key = usage_key.course_key
if not PUBLIC_VIDEO_SHARE.is_enabled(course_key):
raise Http404("Video not found.")
# usage key block type must be `video` else raise 404
if usage_key.block_type != 'video':
raise Http404("Video not found.")
with modulestore().bulk_operations(course_key):
course = get_course_by_id(course_key, 0)
block, _ = get_block_by_usage_id(
request,
str(course_key),
str(usage_key),
disable_staff_debug_info=True,
course=course,
will_recheck_access=False
)
# Block must be marked as public to be viewed
if not block.public_access:
raise Http404("Video not found.")
fragment = block.render(view, context={
'public_video_embed': is_embed,
})
video_description = f"Watch a video from the course {course.display_name} "
if course.display_organization is not None:
video_description += f"by {course.display_organization} "
video_description += "on edX.org"
video_poster = None
if not is_embed:
video_poster = block._poster() # pylint: disable=protected-access
context = {
'fragment': fragment,
'course': course,
'video_title': block.display_name_with_default,
'video_description': video_description,
'video_thumbnail': video_poster if video_poster is not None else '',
'video_embed_url': urljoin(
settings.LMS_ROOT_URL,
reverse('render_public_video_xblock_embed', kwargs={'usage_key_string': str(usage_key)})
),
'disable_accordion': False,
'allow_iframing': True,
'disable_header': False,
'disable_footer': False,
'disable_window_wrap': True,
'edx_notes_enabled': False,
'is_learning_mfe': True,
'is_mobile_app': False,
}
return render_to_response(template, context)
@require_http_methods(["GET"])
@ensure_valid_usage_key
@xframe_options_exempt
@transaction.non_atomic_requests
def render_public_video_xblock_embed(request, usage_key_string):
"""
Returns an HttpResponse with HTML content for the Video xBlock with the given usage_key.
The returned HTML consists of nothing but the Video xBlock content for use in social media embedding.
"""
return _render_public_video_xblock(request, usage_key_string, is_embed=True)
@require_http_methods(["GET"])
@ensure_valid_usage_key
@xframe_options_exempt
@transaction.non_atomic_requests
def render_public_video_xblock(request, usage_key_string):
"""
Returns an HttpResponse with HTML content for the Video xBlock with the given usage_key.
The returned HTML is a chromeless rendering of the Video xBlock (excluding content of the containing courseware).
"""
return _render_public_video_xblock(request, usage_key_string, is_embed=False)
def get_optimization_flags_for_content(block, fragment):
"""
Return a dict with a set of display options appropriate for the block.
@@ -1816,6 +1720,166 @@ class XBlockContentInspector:
return False
class BasePublicVideoXBlockView(View):
"""
Base functionality for public video xblock view and embed view
"""
@method_decorator(ensure_valid_usage_key)
@method_decorator(xframe_options_exempt)
@method_decorator(transaction.non_atomic_requests)
def get(self, _, usage_key_string):
""" Load course and video and render public view """
course, video_block = self.get_course_and_video_block(usage_key_string)
template, context = self.get_template_and_context(course, video_block)
return render_to_response(template, context)
def get_course_and_video_block(self, usage_key_string):
"""
Load course and video from modulestore.
Raises 404 if:
- courseware.public_video_share waffle flag is not enabled for this course
- block is not video
- block is not marked as "public_access"
"""
usage_key = UsageKey.from_string(usage_key_string)
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
course_key = usage_key.course_key
if not PUBLIC_VIDEO_SHARE.is_enabled(course_key):
raise Http404("Video not found.")
# usage key block type must be `video` else raise 404
if usage_key.block_type != 'video':
raise Http404("Video not found.")
with modulestore().bulk_operations(course_key):
course = get_course_by_id(course_key, 0)
video_block, _ = get_block_by_usage_id(
self.request,
str(course_key),
str(usage_key),
disable_staff_debug_info=True,
course=course,
will_recheck_access=False
)
# Block must be marked as public to be viewed
if not video_block.public_access:
raise Http404("Video not found.")
return course, video_block
class PublicVideoXBlockView(BasePublicVideoXBlockView):
""" View for displaying public videos """
def get_template_and_context(self, course, video_block):
"""
Render video xblock, gather social media metadata, and generate CTA links
"""
fragment = video_block.render('public_view', context={
'public_video_embed': False,
})
course_about_page_url, enroll_url = self.get_public_video_cta_button_urls(course)
social_sharing_metadata = self.get_social_sharing_metadata(course, video_block)
context = {
'fragment': fragment,
'course': course,
'social_sharing_metadata': social_sharing_metadata,
'learn_more_url': course_about_page_url,
'enroll_url': enroll_url,
'disable_window_wrap': True,
'disable_register_button': True,
'edx_notes_enabled': False,
'is_learning_mfe': True,
'is_mobile_app': False,
}
return 'public_video.html', context
def get_social_sharing_metadata(self, course, video_block):
"""
Gather the information for the meta OpenGraph and Twitter-specific tags
"""
video_description = f"Watch a video from the course {course.display_name} "
if course.display_organization is not None:
video_description += f"by {course.display_organization} "
video_description += "on edX.org"
video_poster = video_block._poster() # pylint: disable=protected-access
return {
'video_title': video_block.display_name_with_default,
'video_description': video_description,
'video_thumbnail': video_poster if video_poster is not None else '',
'video_embed_url': urljoin(
settings.LMS_ROOT_URL,
reverse('render_public_video_xblock_embed', kwargs={'usage_key_string': str(video_block.location)})
)
}
def get_public_video_cta_button_urls(self, course):
"""
Get the links for the 'enroll' and 'learn more' buttons on the public video page
"""
course_key = str(course.id)
utm_params = self.get_utm_params()
course_about_page_url = self.build_url(
reverse('about_course', kwargs={'course_id': course_key}), {}, utm_params
)
enroll_url = self.build_url(
reverse('register_user'),
{
'course_id': course_key,
'enrollment_action': 'enroll',
'email_opt_in': False,
},
utm_params
)
return course_about_page_url, enroll_url
def get_utm_params(self):
"""
Helper function to pull all utm_ params from the request and return them as a dict
"""
utm_params = {}
for param, value in self.request.GET.items():
if param.startswith("utm_"):
utm_params[param] = value
return utm_params
def build_url(self, base_url, params, utm_params):
"""
Helper function to combine a base URL, params, and utm params into a full URL
"""
if not params and not utm_params:
return base_url
url_parts = urlparse(base_url)
full_params = {**params, **utm_params}
return urlunparse((
url_parts.scheme,
url_parts.netloc,
url_parts.path,
url_parts.params,
urlencode(full_params),
url_parts.fragment
))
class PublicVideoXBlockEmbedView(BasePublicVideoXBlockView):
""" View for viewing public videos embedded within Twitter or other social media """
def get_template_and_context(self, course, video_block):
""" Render the embed view """
fragment = video_block.render('public_view', context={
'public_video_embed': True,
})
context = {
'fragment': fragment,
'course': course,
}
return 'public_video_share_embed.html', context
# Translators: "percent_sign" is the symbol "%". "platform_name" is a
# string identifying the name of this installation, such as "edX".
FINANCIAL_ASSISTANCE_HEADER = _(

View File

@@ -512,3 +512,38 @@
}
}
}
// AU 972 Social Video Sharing Page
.public-video-share-cta {
position: relative;
float: right;
z-index: 1;
.btn-learn-more{
@extend %btn-shims;
color: #00262B;
background: #FFFFFF;
border-color: #F2F0EF;
&:hover,
&:active,
&:focus {
background: darken(#FFFFFF, 7%);
border-color: darken(#F2F0EF, 7%);
}
}
.btn-enroll{
@extend %btn-shims;
color: #FFFFFF;
background: #D23228;
border-color: #D23228;
&:hover,
&:active,
&:focus {
color: darken(#FFFFFF, 7%);
background: darken(#D23228, 7%);
border-color: darken(#D23228, 7%);
}
}
}

View File

@@ -78,6 +78,8 @@ ${HTML(fragment.foot_html())}
</%block>
<%block name="body_extra"/>
<div class="course-wrapper chromeless">
<section class="course-content" id="course-content"\
% if enable_completion_on_view_service:

View File

@@ -48,7 +48,7 @@ from openedx.core.djangoapps.user_authn.toggles import should_redirect_to_authn_
<div class="secondary">
<div>
% if allows_login:
% if allow_public_account_creation:
% if allow_public_account_creation and not disable_register_button:
% if should_redirect_to_authn_mfe:
<div class="mobile-nav-item hidden-mobile nav-item">
<a class="register-btn btn" href="${settings.AUTHN_MICROFRONTEND_URL}/register${login_query()}">${_("Register for free")}</a>

View File

@@ -1,18 +1,34 @@
<%page expression_filter="h"/>
<%!
from django.utils.translation import gettext as _
%>
<%inherit file="courseware/courseware-chromeless.html"/>
<%block name="head_extra">
<!-- OpenGraph tags -->
<meta data-rh="true" property="og:type" content="website">
<meta data-rh="true" property="og:site_name" content="edX">
<meta data-rh="true" property="og:title" content=${video_title}>
<meta data-rh="true" property="og:description" content="${video_description}">
<meta data-rh="true" property="og:image" content="${video_thumbnail}">
<meta data-rh="true" property="og:title" content="${social_sharing_metadata['video_title']}">
<meta data-rh="true" property="og:description" content="${social_sharing_metadata['video_description']}">
<meta data-rh="true" property="og:image" content="${social_sharing_metadata['video_thumbnail']}">
<!-- Twitter-specific video player tags -->
<meta data-rh="true" name="twitter:card" content="player">
<meta data-rh="true" name="twitter:site" content="@edxOnline">
<meta data-rh="true" name="twitter:player" content=${video_embed_url}>
<meta data-rh="true" name="twitter:player" content="${social_sharing_metadata['video_embed_url']}">
<meta data-rh="true" name="twitter:player:width" content="1280">
<meta data-rh="true" name="twitter:player:height" content="720">
</%block>
<%block name="body_extra">
<nav class="public-video-share-cta nav-links" aria-label="Learn More">
<div>
<a class="btn-learn-more btn" href="${learn_more_url}">
${_("Learn more about this course")}
</a>
<a class="btn-enroll btn" href="${enroll_url}">
${_("Enroll in this course")}
</a>
</div>
</nav>
</%block>

View File

@@ -328,12 +328,12 @@ urlpatterns += [
),
re_path(
fr'^videos/{settings.USAGE_KEY_PATTERN}$',
courseware_views.render_public_video_xblock,
courseware_views.PublicVideoXBlockView.as_view(),
name=RENDER_VIDEO_XBLOCK_NAME,
),
re_path(
fr'^videos/{settings.USAGE_KEY_PATTERN}/embed$',
courseware_views.render_public_video_xblock_embed,
courseware_views.PublicVideoXBlockEmbedView.as_view(),
name=RENDER_VIDEO_XBLOCK_EMBED_NAME,
),