Skip to content

Commit

Permalink
Merge pull request #2 from Stanford-Online/giulio/basic-functionality
Browse files Browse the repository at this point in the history
Basic Functionality of in-video-quiz XBlock
  • Loading branch information
caesar2164 committed Dec 1, 2016
2 parents 5a58e5e + ed49d78 commit a08eece
Show file tree
Hide file tree
Showing 22 changed files with 623 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.coverage
.tox/
24 changes: 24 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
sudo: false
language: python
cache: pip
python:
- '2.7'
before_install:
- 'uname -a'
- 'python --version'
install:
- 'pip install tox'
- 'virtualenv --version'
- 'easy_install --version'
- 'pip --version'
- 'tox --version'
script:
- 'tox -v'
branches:
only:
- 'master'
env:
- TOXENV=py27-dj18
- TOXENV=coveralls
- TOXENV=pep8
- TOXENV=pylint
2 changes: 0 additions & 2 deletions README.md

This file was deleted.

47 changes: 47 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
In Video Quiz XBlock |BS| |CA|
==============================

This XBlock allows for edX components to be displayed to users inside of videos at specific time points.

Installation
------------

Install the requirements into the python virtual environment of your
``edx-platform`` installation by running the following command from the
root folder:

.. code:: bash
$ pip install -r requirements.txt
Enabling in Studio
------------------

You can enable the In Video Quiz XBlock in Studio through the
advanced settings.

1. From the main page of a specific course, navigate to
``Settings -> Advanced Settings`` from the top menu.
2. Check for the ``advanced_modules`` policy key, and add
``"invideoquiz"`` to the policy value list.
3. Click the "Save changes" button.

Package Requirements
--------------------

setup.py contains a list of package dependencies which are required for this XBlock package.
This list is what is used to resolve dependencies when an upstream project is consuming
this XBlock package. requirements.txt is used to install the same dependencies when running
the tests for this package.

License
-------

The In Video Quiz XBlock is available under the AGPL Version 3.0 License.


.. |BS| image:: https://travis-ci.org/Stanford-Online/xblock-in-video-quiz.svg
:target: https://travis-ci.org/Stanford-Online/xblock-in-video-quiz

.. |CA| image:: https://coveralls.io/repos/Stanford-Online/xblock-in-video-quiz/badge.svg?branch=master&service=github
:target: https://coveralls.io/github/Stanford-Online/xblock-in-video-quiz?branch=master
4 changes: 4 additions & 0 deletions invideoquiz/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
Runtime will load the XBlock class from here.
"""
from .invideoquiz import InVideoQuizXBlock
171 changes: 171 additions & 0 deletions invideoquiz/invideoquiz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""
This XBlock allows for edX components to be displayed to users inside of
videos at specific time points.
"""

import os
import pkg_resources

from xblock.core import XBlock
from xblock.fields import Scope
from xblock.fields import String
from xblock.fragment import Fragment
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.
"""

display_name = String(
display_name=_('Display Name'),
default=_('In-Video Quiz XBlock'),
scope=Scope.settings,
)

video_id = String(
display_name=_('Video ID'),
default='',
scope=Scope.settings,
help=_(
'This is the component ID for the video in which '
'you want to insert your quiz question.'
),
)

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"}'
),
multiline_editor=True,
)

editable_fields = [
'video_id',
'timemap',
]

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
19 changes: 19 additions & 0 deletions invideoquiz/public/README.txt
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"))

52 changes: 52 additions & 0 deletions invideoquiz/public/css/invideoquiz.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* CSS for InVideoQuizXBlock */

#seq_content .vert-mod {
position: relative;
}
#seq_content .vert-mod .in-video-alert {
padding: 5px 10px;
background: #eee;
border-radius: 5px;
}

#seq_content .vert-mod .vert.in-video-problem-wrapper {
padding-bottom: 0;
margin-bottom: 0;
border-bottom: none;
}
#seq_content .vert-mod .vert.in-video-problem-wrapper .in-video-problem {
position: absolute;
top: 50px;
padding: 25px 25px 0 25px;
background: white;
box-sizing: border-box;
width: 100%;
height: 461px;
overflow-y: scroll;
z-index: 99;
}
.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 #seq_content .vert-mod .video-controls .slider {
height: 13px;
}
.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;
}
3 changes: 3 additions & 0 deletions invideoquiz/public/html/invideoquiz.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="in-video-quiz-block" data-videoid="{video_id}" data-mode='{user_mode}'>
<p>This is an XBlock to display problem components inside of videos.</p>
</div>
12 changes: 12 additions & 0 deletions invideoquiz/public/js/src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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}';
if (videoId) {{
InVideoQuizXBlock.config[videoId] = {timemap};
}}
}}());

0 comments on commit a08eece

Please sign in to comment.