diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f9c0ec8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.coverage
+.tox/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b651071
--- /dev/null
+++ b/.travis.yml
@@ -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
diff --git a/README.md b/README.md
deleted file mode 100644
index e04f22e..0000000
--- a/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-New XBlock
-====================
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..f34edd7
--- /dev/null
+++ b/README.rst
@@ -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
diff --git a/invideoquiz/__init__.py b/invideoquiz/__init__.py
new file mode 100644
index 0000000..a35159a
--- /dev/null
+++ b/invideoquiz/__init__.py
@@ -0,0 +1,4 @@
+"""
+Runtime will load the XBlock class from here.
+"""
+from .invideoquiz import InVideoQuizXBlock
diff --git a/invideoquiz/invideoquiz.py b/invideoquiz/invideoquiz.py
new file mode 100644
index 0000000..85869df
--- /dev/null
+++ b/invideoquiz/invideoquiz.py
@@ -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",
+ """
This is an XBlock to display problem components inside of videos.
+This component will appear in the video at ' + minutes + ':' + seconds + '
'; + component.prepend(timeParagraph); + } + }); + } + + // Bind In Video Quiz display to video time, as well as play and pause buttons + function bindVideoEvents() { + var canDisplayProblem = true; + var intervalObject; + var problemToDisplay; + + video.on('play', function () { + if (problemToDisplay) { + window.setTimeout(function () { + canDisplayProblem = true; + }, intervalTimeout); + problemToDisplay.hide(); + problemToDisplay = null; + } + + intervalObject = setInterval(function () { + var videoTime = parseInt(videoState.videoPlayer.currentTime, 10); + var problemToDisplayId = problemTimesMap[videoTime]; + if (problemToDisplayId && canDisplayProblem) { + $('.wrapper-downloads, .video-controls', video).hide(); + var hasProblemToDisplay = $(this).data('id').indexOf(problemToDisplayId) !== -1; + $('#seq_content .vert-mod .vert').each(function () { + if (hasProblemToDisplay) { + problemToDisplay = $('.xblock-student_view', this) + videoState.videoPlayer.pause(); + problemToDisplay.show(); + canDisplayProblem = false; + } + }); + } + }, intervalTime); + }); + + video.on('pause', function () { + clearInterval(intervalObject); + if (problemToDisplay) { + $('.in-video-continue', problemToDisplay).on('click', function () { + $('.wrapper-downloads, .video-controls', video).show(); + videoState.videoPlayer.play(); + }); + } + }); + } +} diff --git a/invideoquiz/settings.py b/invideoquiz/settings.py new file mode 100644 index 0000000..ce43723 --- /dev/null +++ b/invideoquiz/settings.py @@ -0,0 +1,18 @@ +""" +Settings for in-video-quiz xblock +""" + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': 'intentionally-omitted', + }, +} + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +INSTALLED_APPS = ( + 'django_nose', +) + +SECRET_KEY = 'invideoquiz_SECRET_KEY' diff --git a/invideoquiz/tests.py b/invideoquiz/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/invideoquiz/tests/test_travis.py b/invideoquiz/tests/test_travis.py new file mode 100644 index 0000000..e69de29 diff --git a/invideoquiz/translations/README.txt b/invideoquiz/translations/README.txt new file mode 100644 index 0000000..0493bcc --- /dev/null +++ b/invideoquiz/translations/README.txt @@ -0,0 +1,4 @@ +Use this translations directory to provide internationalized strings for your XBlock project. + +For more information on how to enable translations, visit the Open edX XBlock tutorial on Internationalization: +http://edx.readthedocs.org/projects/xblock-tutorial/en/latest/edx_platform/edx_lms.html diff --git a/invideoquiz/utils.py b/invideoquiz/utils.py new file mode 100644 index 0000000..75cc9cd --- /dev/null +++ b/invideoquiz/utils.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Make '_' a no-op so we can scrape strings +""" + + +def _(text): + """ + :return text + """ + return text diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..af2e2d3 --- /dev/null +++ b/manage.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +""" +Manage the djangoapp +""" +import os +import sys + +from django.core.management import execute_from_command_line + +if __name__ == '__main__': + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'invideoquiz.settings') + execute_from_command_line(sys.argv) diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..9256b31 --- /dev/null +++ b/pylintrc @@ -0,0 +1,8 @@ +[VARIABLES] +dummy-variables-rgx=_|dummy + +[REPORTS] +reports=no + +[MESSAGES CONTROL] +disable=locally-disabled diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..469ba0a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +git+https://github.com/edx/XBlock.git#egg=XBlock +git+https://github.com/edx/xblock-utils.git@v1.0.0#egg=xblock-utils==v1.0.0 +-e . diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..73797c9 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,6 @@ +[metadata] +description-file = README.markdown + +[nosetests] +cover-package=invideoquiz +cover-tests=1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d5d044f --- /dev/null +++ b/setup.py @@ -0,0 +1,72 @@ +"""Setup for invideoquiz XBlock.""" + +import os +from setuptools import setup +from setuptools.command.test import test as TestCommand + + +def package_data(pkg, roots): + """Generic function to find package_data. + + All of the files under each of the `roots` will be declared as package + data for package `pkg`. + + """ + data = [] + for root in roots: + for dirname, _, files in os.walk(os.path.join(pkg, root)): + for fname in files: + data.append(os.path.relpath(os.path.join(dirname, fname), pkg)) + + return {pkg: data} + + +class Tox(TestCommand): + user_options = [('tox-args=', 'a', 'Arguments to pass to tox')] + + def initialize_options(self): + TestCommand.initialize_options(self) + self.tox_args = None + + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + # import here, cause outside the eggs aren't loaded + import tox + import shlex + args = self.tox_args + if args: + args = shlex.split(self.tox_args) + errno = tox.cmdline(args=args) + sys.exit(errno) + +setup( + name='invideoquiz-xblock', + version='0.1', + description='Helper XBlock to locate CAPA problems within videos.', + license='AGPL v3', + packages=[ + 'invideoquiz', + ], + install_requires=[ + 'django >= 1.8, < 1.9', + 'django_nose', + 'mock', + 'coverage', + 'mako', + 'XBlock', + 'xblock-utils', + ], + dependency_links=[ + 'https://github.com/edx/xblock-utils/tarball/c39bf653e4f27fb3798662ef64cde99f57603f79#egg=xblock-utils', + ], + entry_points={ + 'xblock.v1': [ + 'invideoquiz = invideoquiz:InVideoQuizXBlock', + ], + }, + package_data=package_data('invideoquiz', ['static', 'public']), +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..7ff857d --- /dev/null +++ b/tox.ini @@ -0,0 +1,45 @@ + +[tox] +downloadcache = {toxworkdir}/_download/ +envlist = py27-dj18,coverage,pep8,pylint + +[testenv] +commands = {envpython} manage.py test + +[testenv:py27-dj18] +deps = + -rrequirements.txt + +[testenv:pep8] +deps = + -rrequirements.txt + pep8 +commands = {envbindir}/pep8 invideoquiz/ + +[testenv:pylint] +deps = + -rrequirements.txt + pylint +commands = {envbindir}/pylint invideoquiz/ + +[testenv:coverage] +deps = + -rrequirements.txt + coverage +setenv = + NOSE_COVER_TESTS=1 + NOSE_WITH_COVERAGE=1 +commands = + {envpython} manage.py test + +[testenv:coveralls] +deps = + -rrequirements.txt + coveralls +setenv = + NOSE_COVER_TESTS=1 + NOSE_WITH_COVERAGE=1 +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH +commands = + {envbindir}/coverage run --source=invideoquiz manage.py test + {envbindir}/coveralls