-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
939ad5b
commit 81a6472
Showing
10 changed files
with
510 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
""" | ||
Runtime will load the XBlock class from here. | ||
""" | ||
from .invideoquiz import InVideoQuizXBlock | ||
|
||
__version__ = '1.3.1' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
""" | ||
This XBlock allows for edX components to be displayed to users inside of | ||
videos at specific time points. | ||
""" | ||
|
||
import os | ||
import json | ||
import pkg_resources | ||
|
||
from xblock.core import XBlock | ||
from xblock.fields import Scope | ||
from xblock.fields import String | ||
from xblock.fragment import Fragment | ||
from xblock.validation import ValidationMessage | ||
from xblockutils.studio_editable import StudioEditableXBlockMixin | ||
|
||
from .utils import _ | ||
|
||
|
||
def get_resource_string(path): | ||
""" | ||
Retrieve string contents for the file path | ||
""" | ||
path = os.path.join('public', path) | ||
resource_string = pkg_resources.resource_string(__name__, path) | ||
return resource_string.decode('utf8') | ||
|
||
|
||
class InVideoQuizXBlock(StudioEditableXBlockMixin, XBlock): | ||
# pylint: disable=too-many-ancestors | ||
""" | ||
Display CAPA problems within a video component at a specified time. | ||
""" | ||
|
||
show_in_read_only_mode = True | ||
|
||
display_name = String( | ||
display_name=_('Display Name'), | ||
default=_('In-Video Quiz XBlock'), | ||
scope=Scope.settings, | ||
) | ||
|
||
video_id = String( | ||
display_name=_('Video Location'), | ||
default='', | ||
scope=Scope.settings, | ||
help=_( | ||
'This is the component ID for the video in which ' | ||
'you want to insert your quiz questions. It can be ' | ||
'obtained from staff debug info of the video in the LMS.' | ||
), | ||
) | ||
|
||
timemap = String( | ||
display_name=_('Problem Timemap'), | ||
default='{}', | ||
scope=Scope.settings, | ||
help=_( | ||
'A simple string field to define problem IDs ' | ||
'and their time maps (in seconds) as JSON. ' | ||
'Example: {"60": "50srvqlii4ru9gonprp35gkcfyd5weju"} ' | ||
'Problem IDs can be obatined from staff debug info of ' | ||
'the problems in the LMS.' | ||
), | ||
multiline_editor=True, | ||
) | ||
|
||
editable_fields = [ | ||
'video_id', | ||
'timemap', | ||
] | ||
|
||
def validate_field_data(self, validation, data): | ||
""" | ||
Validate the user-submitted timemap. | ||
""" | ||
try: | ||
json.loads(data.timemap) | ||
except ValueError: | ||
_ = self.runtime.service(self, "i18n").ugettext | ||
validation.add(ValidationMessage(ValidationMessage.ERROR, str( | ||
_("Invalid Timemap") | ||
))) | ||
|
||
# Decorate the view in order to support multiple devices e.g. mobile | ||
# See: https://openedx.atlassian.net/wiki/display/MA/Course+Blocks+API | ||
# section 'View @supports(multi_device) decorator' | ||
@XBlock.supports('multi_device') | ||
def student_view(self, context=None): # pylint: disable=unused-argument | ||
""" | ||
Show to students when viewing courses | ||
""" | ||
fragment = self.build_fragment( | ||
path_html='html/invideoquiz.html', | ||
paths_css=[ | ||
'css/invideoquiz.css', | ||
], | ||
paths_js=[ | ||
'js/src/invideoquiz.js', | ||
], | ||
fragment_js='InVideoQuizXBlock', | ||
context={ | ||
'video_id': self.video_id, | ||
'user_mode': self.user_mode, | ||
}, | ||
) | ||
config = get_resource_string('js/src/config.js') | ||
config = config.format( | ||
video_id=self.video_id, | ||
timemap=self.timemap, | ||
) | ||
fragment.add_javascript(config) | ||
return fragment | ||
|
||
@property | ||
def user_mode(self): | ||
""" | ||
Check user's permission mode for this XBlock. | ||
Returns: | ||
user permission mode | ||
""" | ||
try: | ||
if self.xmodule_runtime.user_is_staff: | ||
return 'staff' | ||
except AttributeError: | ||
pass | ||
return 'student' | ||
|
||
@staticmethod | ||
def workbench_scenarios(): | ||
""" | ||
A canned scenario for display in the workbench. | ||
""" | ||
return [ | ||
("InVideoQuizXBlock", | ||
"""<invideoquiz video_id='###' timemap='{ 10: "###" }' /> | ||
"""), | ||
("Multiple InVideoQuizXBlock", | ||
"""<vertical_demo> | ||
<invideoquiz video_id='###' timemap='{ 10: "###" }' /> | ||
<invideoquiz video_id='###' timemap='{ 10: "###" }' /> | ||
<invideoquiz video_id='###' timemap='{ 10: "###" }' /> | ||
</vertical_demo> | ||
"""), | ||
] | ||
|
||
def get_resource_url(self, path): | ||
""" | ||
Retrieve a public URL for the file path | ||
""" | ||
path = os.path.join('public', path) | ||
resource_url = self.runtime.local_resource_url(self, path) | ||
return resource_url | ||
|
||
def build_fragment( | ||
self, | ||
path_html='', | ||
paths_css=None, | ||
paths_js=None, | ||
urls_css=None, | ||
urls_js=None, | ||
fragment_js=None, | ||
context=None, | ||
): # pylint: disable=too-many-arguments | ||
""" | ||
Assemble the HTML, JS, and CSS for an XBlock fragment | ||
""" | ||
paths_css = paths_css or [] | ||
paths_js = paths_js or [] | ||
urls_css = urls_css or [] | ||
urls_js = urls_js or [] | ||
# If no context is provided, convert self.fields into a dict | ||
context = context or { | ||
key: getattr(self, key) | ||
for key in self.editable_fields | ||
} | ||
html_source = get_resource_string(path_html) | ||
html_source = html_source.format( | ||
**context | ||
) | ||
fragment = Fragment(html_source) | ||
for path in paths_css: | ||
url = self.get_resource_url(path) | ||
fragment.add_css_url(url) | ||
for path in paths_js: | ||
url = self.get_resource_url(path) | ||
fragment.add_javascript_url(url) | ||
for url in urls_css: | ||
fragment.add_css_url(url) | ||
for url in urls_js: | ||
fragment.add_javascript_url(url) | ||
if fragment_js: | ||
fragment.initialize_js(fragment_js) | ||
return fragment |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
This static directory is for files that should be included in your kit as plain | ||
static files. | ||
|
||
You can ask the runtime for a URL that will retrieve these files with: | ||
|
||
url = self.runtime.local_resource_url(self, "static/js/lib.js") | ||
|
||
The default implementation is very strict though, and will not serve files from | ||
the static directory. It will serve files from a directory named "public". | ||
Create a directory alongside this one named "public", and put files there. | ||
Then you can get a url with code like this: | ||
|
||
url = self.runtime.local_resource_url(self, "public/js/lib.js") | ||
|
||
The sample code includes a function you can use to read the content of files | ||
in the static directory, like this: | ||
|
||
frag.add_javascript(self.resource_string("static/js/my_block.js")) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/* CSS for InVideoQuizXBlock */ | ||
|
||
#course-content .vert-mod, | ||
#seq_content .vert-mod { | ||
position: relative; | ||
} | ||
#course-content .vert-mod .in-video-alert, | ||
#seq_content .vert-mod .in-video-alert { | ||
padding: 5px 10px; | ||
background: #eee; | ||
border-radius: 5px; | ||
} | ||
|
||
#course-content .vert-mod .vert.in-video-problem-wrapper, | ||
#seq_content .vert-mod .vert.in-video-problem-wrapper { | ||
padding-bottom: 0; | ||
margin-bottom: 0; | ||
border-bottom: none; | ||
} | ||
#course-content .vert-mod .vert.in-video-problem-wrapper .in-video-problem, | ||
#seq_content .vert-mod .vert.in-video-problem-wrapper .in-video-problem { | ||
position: absolute; | ||
top: 35px; | ||
padding: 25px 25px 0 25px; | ||
background: white; | ||
box-sizing: border-box; | ||
width: 100%; | ||
height: 467px; | ||
overflow-y: scroll; | ||
z-index: 99; | ||
} | ||
.video-fullscreen #course-content .vert-mod .vert.in-video-problem-wrapper .in-video-problem, | ||
.video-fullscreen #seq_content .vert-mod .vert.in-video-problem-wrapper .in-video-problem { | ||
position: fixed; | ||
height: auto; | ||
top: 0; | ||
left: 0; | ||
right: 0; | ||
bottom: 53px; | ||
z-index: 10000; | ||
} | ||
.video-controls { | ||
z-index: 100; | ||
} | ||
.video-fullscreen #course-content .vert-mod .video-controls .slider, | ||
.video-fullscreen #seq_content .vert-mod .video-controls .slider { | ||
height: 13px; | ||
} | ||
.video-fullscreen #course-content .vert-mod .video-controls .slider .ui-slider-handle, | ||
.video-fullscreen #seq_content .vert-mod .video-controls .slider .ui-slider-handle { | ||
height: 13px; | ||
width: 13px; | ||
} | ||
.in-video-continue { | ||
float: right; | ||
height: 40px; | ||
margin-bottom: 25px; | ||
text-transform: uppercase; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<div class="in-video-quiz-block" data-videoid="{video_id}" data-mode='{user_mode}'></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Curly braces are all doubled because this file gets called and formatted by python | ||
|
||
var InVideoQuizXBlock = InVideoQuizXBlock || {{}}; | ||
|
||
(function () {{ | ||
InVideoQuizXBlock.config = InVideoQuizXBlock.config || {{}}; | ||
|
||
var videoId = '{video_id}'; | ||
// This is (temporary) error handling for previous-submitted invalid timemap. | ||
try {{ | ||
if (videoId) {{ | ||
InVideoQuizXBlock.config[videoId] = JSON.parse(`{timemap}`); | ||
}} | ||
}} | ||
catch {{ | ||
InVideoQuizXBlock.config[videoId] = {{}}; | ||
}} | ||
}}()); |
Oops, something went wrong.