diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index a9fe0327eb..b803e1e0e4 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -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 = _( diff --git a/lms/static/sass/_experiments.scss b/lms/static/sass/_experiments.scss index b177a38a2a..27c8452d8d 100644 --- a/lms/static/sass/_experiments.scss +++ b/lms/static/sass/_experiments.scss @@ -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%); + } + } +} diff --git a/lms/templates/courseware/courseware-chromeless.html b/lms/templates/courseware/courseware-chromeless.html index f6dd0738b4..aaa94b9100 100644 --- a/lms/templates/courseware/courseware-chromeless.html +++ b/lms/templates/courseware/courseware-chromeless.html @@ -78,6 +78,8 @@ ${HTML(fragment.foot_html())} +<%block name="body_extra"/> +
% if allows_login: - % if allow_public_account_creation: + % if allow_public_account_creation and not disable_register_button: % if should_redirect_to_authn_mfe: