Merge pull request #30042 from openedx/ammar/public-videos-poc

add an xblock renderer endpoint to serve videos as public having public access set by course author in studio
This commit is contained in:
Muhammad Ammar
2022-04-06 10:29:22 +05:00
committed by GitHub
10 changed files with 194 additions and 2 deletions

View File

@@ -299,6 +299,27 @@ function(Backbone, BaseView, _, MetadataModel, AbstractEditor, FileUpload, Uploa
}
});
Metadata.PublicAccess = Metadata.Option.extend({
templateName: 'metadata-option-public-access',
initialize: function() {
Metadata.Option.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.updateUrlFieldVisibility);
this.updateUrlFieldVisibility();
},
updateUrlFieldVisibility: function() {
const urlContainer = this.$el.find('.public-access-block-url-container');
if(this.getValueFromEditor()) {
urlContainer.removeClass('is-hidden');
} else {
urlContainer.addClass('is-hidden');
}
}
});
Metadata.List = AbstractEditor.extend({
events: {

View File

@@ -0,0 +1,21 @@
<div class="wrapper-comp-setting">
<label class="label setting-label" for="<%- uniqueId %>"><%- model.get('display_name') %></label>
<select class="input setting-input" id="<%- uniqueId %>" name="<%- model.get('display_name') %>">
<% _.each(model.get('options'), function(option) { %>
<% if (option.display_name !== undefined) { %>
<option value="<%- option['display_name'] %>"><%- option['display_name'] %></option>
<% } else { %>
<option value="<%- option %>"><%- option %></option>
<% } %>
<% }) %>
</select>
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%- gettext("Clear") %>" data-tooltip="<%- gettext("Clear") %>">
<span class="icon fa fa-undo" aria-hidden="true"></span><span class="sr">"<%- gettext("Clear Value") %>"</span>
</button>
</div>
<span class="tip setting-help"><%- model.get('help') %></span>
<br>
<div class="public-access-block-url-container is-hidden">
<label class="label setting-label" for="<%- uniqueId %>"><%- gettext("URL") %></label>
<input class="input setting-input" type="text" id="<%- uniqueId %>" value='<%- model.get("url") %>' readonly/>
</div>

View File

@@ -16,7 +16,7 @@
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry", "metadata-file-uploader-entry", "metadata-file-uploader-item"]:
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-option-public-access", "metadata-list-entry", "metadata-dict-entry", "metadata-file-uploader-entry", "metadata-file-uploader-item"]:
<script id="${template_name}" type="text/template">
<%static:include path="js/${template_name}.underscore" />
</script>

View File

@@ -8,7 +8,7 @@
<%static:include path="js/metadata-editor.underscore" />
</script>
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry", "metadata-file-uploader-entry", "metadata-file-uploader-item"]:
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-option-public-access", "metadata-list-entry", "metadata-dict-entry", "metadata-file-uploader-entry", "metadata-file-uploader-item"]:
<script id="${template_name}" type="text/template">
<%static:include path="js/${template_name}.underscore" />
</script>

View File

@@ -589,6 +589,13 @@ class VideoBlock(
# Backbonjs view can handle it.
editable_fields['edx_video_id']['type'] = 'VideoID'
# `public_access` is a boolean field and by default backbonejs code render it as a dropdown with 2 options
# but in our case we also need to show an input field with dropdown, the input field will show the url to
# be shared with leaners. This is not possible with default rendering logic in backbonjs code, that is why
# we are setting a new type and then do a custom rendering in backbonejs code to render the desired UI.
editable_fields['public_access']['type'] = 'PublicAccess'
editable_fields['public_access']['url'] = fr'{settings.LMS_ROOT_URL}/videos/{str(self.location)}'
# construct transcripts info and also find if `en` subs exist
transcripts_info = self.get_transcripts_info()
possible_sub_ids = [self.sub, self.youtube_id_1_0] + get_html5_ids(self.html5_sources)

View File

@@ -206,3 +206,9 @@ class VideoFields:
scope=Scope.preferences,
default=False,
)
public_access = Boolean(
help=_("Specify whether the video can be accessed publicly by learners."),
display_name=_("Public Access"),
scope=Scope.settings,
default=False
)

View File

@@ -3282,6 +3282,79 @@ class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase, CompletionWaf
self.assertNotContains(response, banner_text, html=True)
class TestRenderPublicVideoXBlock(ModuleStoreTestCase):
"""
Tests for the courseware.render_public_video_xblock endpoint.
"""
def setup_course(self):
"""
Helper method to create the course.
"""
with self.store.default_store(self.store.default_modulestore.get_modulestore_type()):
course = CourseFactory.create(**{'start': datetime.now() - timedelta(days=1)})
chapter = ItemFactory.create(parent=course, category='chapter')
vertical_block = ItemFactory.create(
parent_location=chapter.location,
category='vertical',
display_name="Vertical"
)
self.html_block = ItemFactory.create( # pylint: disable=attribute-defined-outside-init
parent=vertical_block,
category='html',
data="<p>Test HTML Content<p>"
)
self.video_block_public = ItemFactory.create( # pylint: disable=attribute-defined-outside-init
parent=vertical_block,
category='video',
display_name='Video with public access',
metadata={'public_access': True}
)
self.video_block_not_public = ItemFactory.create( # pylint: disable=attribute-defined-outside-init
parent=vertical_block,
category='video',
display_name='Video with private access'
)
CourseOverview.load_from_module_store(course.id)
def get_response(self, usage_key):
"""
Overridable method to get the response from the endpoint that is being tested.
"""
url = reverse('render_public_video_xblock', kwargs={'usage_key_string': str(usage_key)})
return self.client.get(url)
def test_render_xblock_with_invalid_usage_key(self):
"""
Verify that endpoint returns expected response with invalid usage key
"""
response = self.get_response(usage_key='some_invalid_usage_key')
self.assertContains(response, 'Page not found', status_code=404)
def test_render_xblock_with_non_video_usage_key(self):
"""
Verify that endpoint returns expected response if usage key block type is not `video`
"""
self.setup_course()
response = self.get_response(usage_key=self.html_block.location)
self.assertContains(response, 'Page not found', status_code=404)
def test_render_xblock_with_video_usage_key_with_public_access(self):
"""
Verify that endpoint returns expected response if usage key block type is `video` and video has public access
"""
self.setup_course()
response = self.get_response(usage_key=self.video_block_public.location)
self.assertContains(response, 'Play video', status_code=200)
def test_render_xblock_with_video_usage_key_with_non_public_access(self):
"""
Verify that endpoint returns expected response if usage key block type is `video` and video has private access
"""
self.setup_course()
response = self.get_response(usage_key=self.video_block_not_public.location)
self.assertContains(response, 'Page not found', status_code=404)
class TestRenderXBlockSelfPaced(TestRenderXBlock): # lint-amnesty, pylint: disable=test-inherits-tests
"""
Test rendering XBlocks for a self-paced course. Relies on the query

View File

@@ -116,6 +116,11 @@ class RenderXBlockTestMixin(MasqueradeMixin, metaclass=ABCMeta):
category='problem',
display_name='Problem'
)
self.video_block = ItemFactory.create(
parent=self.vertical_block,
category='video',
display_name='Video'
)
CourseOverview.load_from_module_store(self.course.id)
# block_name_to_be_tested can be `html_block` or `vertical_block`.

View File

@@ -113,6 +113,7 @@ from openedx.core.djangoapps.credit.api import (
is_credit_course,
is_user_eligible_for_credit
)
from openedx.core.lib.courses import get_course_by_id
from openedx.core.djangoapps.enrollments.api import add_enrollment
from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE
from openedx.core.djangoapps.models.course_details import CourseDetails
@@ -1824,6 +1825,58 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
return render_to_response('courseware/courseware-chromeless.html', context)
@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).
"""
view = 'public_view'
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
# 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_module_by_usage_id(
request,
str(course_key),
str(usage_key),
disable_staff_debug_info=True,
course=course,
will_recheck_access=False
)
# video must be public (`Public Access` field set to True) by course author in studio in video advanced settings
if not block.public_access:
raise Http404("Video not found.")
fragment = block.render(view, context={})
context = {
'fragment': fragment,
'course': course,
'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('courseware/courseware-chromeless.html', context)
def get_optimization_flags_for_content(block, fragment):
"""
Return a dict with a set of display options appropriate for the block.

View File

@@ -56,6 +56,7 @@ from common.djangoapps.util import views as util_views
RESET_COURSE_DEADLINES_NAME = 'reset_course_deadlines'
RENDER_XBLOCK_NAME = 'render_xblock'
RENDER_VIDEO_XBLOCK_NAME = 'render_public_video_xblock'
COURSE_DATES_NAME = 'dates'
COURSE_PROGRESS_NAME = 'progress'
@@ -318,6 +319,11 @@ urlpatterns += [
courseware_views.render_xblock,
name=RENDER_XBLOCK_NAME,
),
re_path(
fr'^videos/{settings.USAGE_KEY_PATTERN}$',
courseware_views.render_public_video_xblock,
name=RENDER_VIDEO_XBLOCK_NAME,
),
# xblock Resource URL
re_path(