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:
@@ -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: {
|
||||
|
||||
21
cms/templates/js/metadata-option-public-access.underscore
Normal file
21
cms/templates/js/metadata-option-public-access.underscore
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user