diff --git a/requirements/base.in b/requirements/base.in
index ed641fdc..22557c64 100644
--- a/requirements/base.in
+++ b/requirements/base.in
@@ -1,10 +1,23 @@
# Core requirements for using this application
-c constraints.txt
+beautifulsoup4
+chem
+ddt
+defusedxml
django-statici18n
+edx-codejail
edx-i18n-tools
edx-opaque-keys
+edx-submissions
+edx-toggles
+html5lib
nh3
+numpy
oauthlib
+openedx-calc
openedx-django-pyfs
+pillow
+random2
+shapely
XBlock
diff --git a/requirements/base.txt b/requirements/base.txt
index f98c619d..96c65baf 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -8,29 +8,77 @@ appdirs==1.4.4
# via fs
asgiref==3.11.0
# via django
-boto3==1.42.34
+beautifulsoup4==4.14.3
+ # via -r requirements/base.in
+boto3==1.42.39
# via fs-s3fs
-botocore==1.42.34
+botocore==1.42.39
# via
# boto3
# s3transfer
+cffi==2.0.0
+ # via pynacl
+chem==2.0.0
+ # via -r requirements/base.in
+click==8.3.1
+ # via
+ # code-annotations
+ # edx-django-utils
+ # nltk
+code-annotations==2.3.0
+ # via edx-toggles
+ddt==1.7.2
+ # via -r requirements/base.in
+defusedxml==0.7.1
+ # via -r requirements/base.in
django==5.2.10
# via
# -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
# django-appconf
+ # django-crum
+ # django-model-utils
# django-statici18n
+ # django-waffle
+ # djangorestframework
+ # edx-django-release-util
+ # edx-django-utils
# edx-i18n-tools
+ # edx-submissions
+ # edx-toggles
+ # jsonfield
# openedx-django-pyfs
django-appconf==1.2.0
# via django-statici18n
+django-crum==0.7.9
+ # via
+ # edx-django-utils
+ # edx-toggles
+django-model-utils==5.0.0
+ # via edx-submissions
django-statici18n==2.6.0
# via -r requirements/base.in
+django-waffle==5.0.0
+ # via
+ # edx-django-utils
+ # edx-toggles
+djangorestframework==3.16.1
+ # via edx-submissions
dnspython==2.8.0
# via pymongo
+edx-codejail==4.1.0
+ # via -r requirements/base.in
+edx-django-release-util==1.5.0
+ # via edx-submissions
+edx-django-utils==8.0.1
+ # via edx-toggles
edx-i18n-tools==1.9.0
# via -r requirements/base.in
edx-opaque-keys==3.0.0
# via -r requirements/base.in
+edx-submissions==3.12.2
+ # via -r requirements/base.in
+edx-toggles==5.4.1
+ # via -r requirements/base.in
fs==2.4.16
# via
# fs-s3fs
@@ -38,14 +86,23 @@ fs==2.4.16
# xblock
fs-s3fs==1.1.1
# via openedx-django-pyfs
+html5lib==1.1
+ # via -r requirements/base.in
+jinja2==3.1.6
+ # via code-annotations
jmespath==1.1.0
# via
# boto3
# botocore
+joblib==1.5.3
+ # via nltk
+jsonfield==3.2.0
+ # via edx-submissions
lxml[html-clean]==6.0.2
# via
# edx-i18n-tools
# lxml-html-clean
+ # openedx-calc
# xblock
lxml-html-clean==0.4.3
# via lxml
@@ -53,49 +110,109 @@ mako==1.3.10
# via xblock
markupsafe==3.0.3
# via
+ # chem
+ # jinja2
# mako
+ # openedx-calc
# xblock
+mpmath==1.3.0
+ # via sympy
nh3==0.3.2
# via -r requirements/base.in
+nltk==3.9.2
+ # via chem
+numpy==2.4.2
+ # via
+ # -r requirements/base.in
+ # chem
+ # openedx-calc
+ # scipy
+ # shapely
oauthlib==3.3.1
# via -r requirements/base.in
+openedx-calc==4.0.3
+ # via -r requirements/base.in
openedx-django-pyfs==3.8.0
# via -r requirements/base.in
path==16.16.0
# via edx-i18n-tools
+pillow==12.1.0
+ # via -r requirements/base.in
polib==1.2.0
# via edx-i18n-tools
+psutil==7.2.2
+ # via edx-django-utils
+pycparser==3.0
+ # via cffi
pymongo==4.16.0
# via edx-opaque-keys
+pynacl==1.6.2
+ # via edx-django-utils
+pyparsing==3.3.2
+ # via
+ # chem
+ # openedx-calc
python-dateutil==2.9.0.post0
# via
# botocore
# xblock
+python-slugify==8.0.4
+ # via code-annotations
pytz==2025.2
- # via xblock
+ # via
+ # edx-submissions
+ # xblock
pyyaml==6.0.3
# via
+ # code-annotations
+ # edx-django-release-util
# edx-i18n-tools
# xblock
+random2==1.0.2
+ # via -r requirements/base.in
+regex==2026.1.15
+ # via nltk
s3transfer==0.16.0
# via boto3
+scipy==1.17.0
+ # via chem
+shapely==2.1.2
+ # via -r requirements/base.in
simplejson==3.20.2
# via xblock
six==1.17.0
# via
+ # edx-codejail
+ # edx-django-release-util
# fs
# fs-s3fs
+ # html5lib
# python-dateutil
+soupsieve==2.8.3
+ # via beautifulsoup4
sqlparse==0.5.5
# via django
stevedore==5.6.0
- # via edx-opaque-keys
+ # via
+ # code-annotations
+ # edx-django-utils
+ # edx-opaque-keys
+sympy==1.14.0
+ # via openedx-calc
+text-unidecode==1.3
+ # via python-slugify
+tqdm==4.67.2
+ # via nltk
typing-extensions==4.15.0
- # via edx-opaque-keys
+ # via
+ # beautifulsoup4
+ # edx-opaque-keys
urllib3==2.6.3
# via botocore
web-fragments==3.1.0
# via xblock
+webencodings==0.5.1
+ # via html5lib
webob==1.8.9
# via xblock
xblock==5.3.0
diff --git a/requirements/dev.txt b/requirements/dev.txt
index b90adc1d..84fc4a9d 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -24,17 +24,21 @@ astroid==4.0.3
# -r requirements/quality.txt
# pylint
# pylint-celery
+beautifulsoup4==4.14.3
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
binaryornot==0.4.4
# via
# -r requirements/quality.txt
# -r requirements/test.txt
# cookiecutter
-boto3==1.42.34
+boto3==1.42.39
# via
# -r requirements/quality.txt
# -r requirements/test.txt
# fs-s3fs
-botocore==1.42.34
+botocore==1.42.39
# via
# -r requirements/quality.txt
# -r requirements/test.txt
@@ -44,7 +48,7 @@ build==1.4.0
# via
# -r requirements/pip-tools.txt
# pip-tools
-cachetools==6.2.5
+cachetools==7.0.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
@@ -54,6 +58,11 @@ certifi==2026.1.4
# -r requirements/quality.txt
# -r requirements/test.txt
# requests
+cffi==2.0.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # pynacl
chardet==5.2.0
# via
# -r requirements/quality.txt
@@ -66,6 +75,10 @@ charset-normalizer==3.4.4
# -r requirements/quality.txt
# -r requirements/test.txt
# requests
+chem==2.0.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
click==8.3.1
# via
# -r requirements/pip-tools.txt
@@ -74,7 +87,9 @@ click==8.3.1
# click-log
# code-annotations
# cookiecutter
+ # edx-django-utils
# edx-lint
+ # nltk
# pip-tools
click-log==0.4.0
# via
@@ -85,6 +100,7 @@ code-annotations==2.3.0
# -r requirements/quality.txt
# -r requirements/test.txt
# edx-lint
+ # edx-toggles
colorama==0.4.6
# via
# -r requirements/quality.txt
@@ -104,6 +120,10 @@ ddt==1.7.2
# via
# -r requirements/quality.txt
# -r requirements/test.txt
+defusedxml==0.7.1
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
diff-cover==10.2.0
# via -r requirements/dev.in
dill==0.4.1
@@ -121,8 +141,17 @@ django==5.2.10
# -r requirements/quality.txt
# -r requirements/test.txt
# django-appconf
+ # django-crum
+ # django-model-utils
# django-statici18n
+ # django-waffle
+ # djangorestframework
+ # edx-django-release-util
+ # edx-django-utils
# edx-i18n-tools
+ # edx-submissions
+ # edx-toggles
+ # jsonfield
# openedx-django-pyfs
# xblock-sdk
django-appconf==1.2.0
@@ -130,15 +159,51 @@ django-appconf==1.2.0
# -r requirements/quality.txt
# -r requirements/test.txt
# django-statici18n
+django-crum==0.7.9
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # edx-django-utils
+ # edx-toggles
+django-model-utils==5.0.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # edx-submissions
django-statici18n==2.6.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
+django-waffle==5.0.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # edx-django-utils
+ # edx-toggles
+djangorestframework==3.16.1
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # edx-submissions
dnspython==2.8.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
# pymongo
+edx-codejail==4.1.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+edx-django-release-util==1.5.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # edx-submissions
+edx-django-utils==8.0.1
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # edx-toggles
edx-i18n-tools==1.9.0
# via
# -r requirements/dev.in
@@ -150,6 +215,14 @@ edx-opaque-keys==3.0.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
+edx-submissions==3.12.2
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+edx-toggles==5.4.1
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
filelock==3.20.3
# via
# -r requirements/quality.txt
@@ -169,6 +242,10 @@ fs-s3fs==1.1.1
# -r requirements/test.txt
# openedx-django-pyfs
# xblock-sdk
+html5lib==1.1
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
idna==3.11
# via
# -r requirements/quality.txt
@@ -196,12 +273,23 @@ jmespath==1.1.0
# -r requirements/test.txt
# boto3
# botocore
+joblib==1.5.3
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # nltk
+jsonfield==3.2.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # edx-submissions
lxml[html-clean]==6.0.2
# via
# -r requirements/quality.txt
# -r requirements/test.txt
# edx-i18n-tools
# lxml-html-clean
+ # openedx-calc
# xblock
# xblock-sdk
lxml-html-clean==0.4.3
@@ -223,8 +311,10 @@ markupsafe==3.0.3
# via
# -r requirements/quality.txt
# -r requirements/test.txt
+ # chem
# jinja2
# mako
+ # openedx-calc
# xblock
mccabe==0.7.0
# via
@@ -235,14 +325,36 @@ mdurl==0.1.2
# -r requirements/quality.txt
# -r requirements/test.txt
# markdown-it-py
+mpmath==1.3.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # sympy
nh3==0.3.2
# via
# -r requirements/quality.txt
# -r requirements/test.txt
+nltk==3.9.2
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # chem
+numpy==2.4.2
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # chem
+ # openedx-calc
+ # scipy
+ # shapely
oauthlib==3.3.1
# via
# -r requirements/quality.txt
# -r requirements/test.txt
+openedx-calc==4.0.3
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
openedx-django-pyfs==3.8.0
# via
# -r requirements/quality.txt
@@ -262,6 +374,10 @@ path==16.16.0
# -r requirements/quality.txt
# -r requirements/test.txt
# edx-i18n-tools
+pillow==12.1.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
pip-tools==7.5.2
# via -r requirements/pip-tools.txt
platformdirs==4.5.1
@@ -284,8 +400,18 @@ polib==1.2.0
# -r requirements/quality.txt
# -r requirements/test.txt
# edx-i18n-tools
+psutil==7.2.2
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # edx-django-utils
pycodestyle==2.14.0
# via -r requirements/quality.txt
+pycparser==3.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # cffi
pydocstyle==6.3.0
# via -r requirements/quality.txt
pygments==2.19.2
@@ -320,6 +446,17 @@ pymongo==4.16.0
# -r requirements/quality.txt
# -r requirements/test.txt
# edx-opaque-keys
+pynacl==1.6.2
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # edx-django-utils
+pyparsing==3.3.2
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # chem
+ # openedx-calc
pypng==0.20220715.0
# via
# -r requirements/quality.txt
@@ -366,6 +503,7 @@ pytz==2025.2
# via
# -r requirements/quality.txt
# -r requirements/test.txt
+ # edx-submissions
# xblock
pyyaml==6.0.3
# via
@@ -373,15 +511,25 @@ pyyaml==6.0.3
# -r requirements/test.txt
# code-annotations
# cookiecutter
+ # edx-django-release-util
# edx-i18n-tools
# xblock
+random2==1.0.2
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+regex==2026.1.15
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # nltk
requests==2.32.5
# via
# -r requirements/quality.txt
# -r requirements/test.txt
# cookiecutter
# xblock-sdk
-rich==14.3.1
+rich==14.3.2
# via
# -r requirements/quality.txt
# -r requirements/test.txt
@@ -391,6 +539,15 @@ s3transfer==0.16.0
# -r requirements/quality.txt
# -r requirements/test.txt
# boto3
+scipy==1.17.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # chem
+shapely==2.1.2
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
simplejson==3.20.2
# via
# -r requirements/quality.txt
@@ -401,14 +558,22 @@ six==1.17.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
+ # edx-codejail
+ # edx-django-release-util
# edx-lint
# fs
# fs-s3fs
+ # html5lib
# python-dateutil
snowballstemmer==3.0.1
# via
# -r requirements/quality.txt
# pydocstyle
+soupsieve==2.8.3
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # beautifulsoup4
sqlparse==0.5.5
# via
# -r requirements/quality.txt
@@ -419,7 +584,13 @@ stevedore==5.6.0
# -r requirements/quality.txt
# -r requirements/test.txt
# code-annotations
+ # edx-django-utils
# edx-opaque-keys
+sympy==1.14.0
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # openedx-calc
text-unidecode==1.3
# via
# -r requirements/quality.txt
@@ -433,10 +604,16 @@ tox==4.34.1
# via
# -r requirements/quality.txt
# -r requirements/test.txt
+tqdm==4.67.2
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # nltk
typing-extensions==4.15.0
# via
# -r requirements/quality.txt
# -r requirements/test.txt
+ # beautifulsoup4
# edx-opaque-keys
tzdata==2025.3
# via
@@ -460,6 +637,11 @@ web-fragments==3.1.0
# -r requirements/test.txt
# xblock
# xblock-sdk
+webencodings==0.5.1
+ # via
+ # -r requirements/quality.txt
+ # -r requirements/test.txt
+ # html5lib
webob==1.8.9
# via
# -r requirements/quality.txt
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 9ddd94a5..6dd742a5 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -20,30 +20,32 @@ asgiref==3.11.0
# via
# -r requirements/test.txt
# django
-babel==2.17.0
+babel==2.18.0
# via
# pydata-sphinx-theme
# sphinx
backports-tarfile==1.2.0
# via jaraco-context
beautifulsoup4==4.14.3
- # via pydata-sphinx-theme
+ # via
+ # -r requirements/test.txt
+ # pydata-sphinx-theme
binaryornot==0.4.4
# via
# -r requirements/test.txt
# cookiecutter
-boto3==1.42.34
+boto3==1.42.39
# via
# -r requirements/test.txt
# fs-s3fs
-botocore==1.42.34
+botocore==1.42.39
# via
# -r requirements/test.txt
# boto3
# s3transfer
build==1.4.0
# via -r requirements/doc.in
-cachetools==6.2.5
+cachetools==7.0.0
# via
# -r requirements/test.txt
# tox
@@ -52,7 +54,9 @@ certifi==2026.1.4
# -r requirements/test.txt
# requests
cffi==2.0.0
- # via cryptography
+ # via
+ # -r requirements/test.txt
+ # pynacl
chardet==5.2.0
# via
# -r requirements/test.txt
@@ -62,13 +66,19 @@ charset-normalizer==3.4.4
# via
# -r requirements/test.txt
# requests
+chem==2.0.0
+ # via -r requirements/test.txt
click==8.3.1
# via
# -r requirements/test.txt
# code-annotations
# cookiecutter
+ # edx-django-utils
+ # nltk
code-annotations==2.3.0
- # via -r requirements/test.txt
+ # via
+ # -r requirements/test.txt
+ # edx-toggles
colorama==0.4.6
# via
# -r requirements/test.txt
@@ -81,10 +91,10 @@ coverage[toml]==7.13.2
# via
# -r requirements/test.txt
# pytest-cov
-cryptography==46.0.3
- # via secretstorage
ddt==1.7.2
# via -r requirements/test.txt
+defusedxml==0.7.1
+ # via -r requirements/test.txt
distlib==0.4.0
# via
# -r requirements/test.txt
@@ -94,16 +104,43 @@ django==5.2.10
# -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
# -r requirements/test.txt
# django-appconf
+ # django-crum
+ # django-model-utils
# django-statici18n
+ # django-waffle
+ # djangorestframework
+ # edx-django-release-util
+ # edx-django-utils
# edx-i18n-tools
+ # edx-submissions
+ # edx-toggles
+ # jsonfield
# openedx-django-pyfs
# xblock-sdk
django-appconf==1.2.0
# via
# -r requirements/test.txt
# django-statici18n
+django-crum==0.7.9
+ # via
+ # -r requirements/test.txt
+ # edx-django-utils
+ # edx-toggles
+django-model-utils==5.0.0
+ # via
+ # -r requirements/test.txt
+ # edx-submissions
django-statici18n==2.6.0
# via -r requirements/test.txt
+django-waffle==5.0.0
+ # via
+ # -r requirements/test.txt
+ # edx-django-utils
+ # edx-toggles
+djangorestframework==3.16.1
+ # via
+ # -r requirements/test.txt
+ # edx-submissions
dnspython==2.8.0
# via
# -r requirements/test.txt
@@ -117,10 +154,24 @@ docutils==0.21.2
# readme-renderer
# restructuredtext-lint
# sphinx
+edx-codejail==4.1.0
+ # via -r requirements/test.txt
+edx-django-release-util==1.5.0
+ # via
+ # -r requirements/test.txt
+ # edx-submissions
+edx-django-utils==8.0.1
+ # via
+ # -r requirements/test.txt
+ # edx-toggles
edx-i18n-tools==1.9.0
# via -r requirements/test.txt
edx-opaque-keys==3.0.0
# via -r requirements/test.txt
+edx-submissions==3.12.2
+ # via -r requirements/test.txt
+edx-toggles==5.4.1
+ # via -r requirements/test.txt
filelock==3.20.3
# via
# -r requirements/test.txt
@@ -137,6 +188,8 @@ fs-s3fs==1.1.1
# -r requirements/test.txt
# openedx-django-pyfs
# xblock-sdk
+html5lib==1.1
+ # via -r requirements/test.txt
id==1.5.0
# via twine
idna==3.11
@@ -157,10 +210,6 @@ jaraco-context==6.1.0
# via keyring
jaraco-functools==4.4.0
# via keyring
-jeepney==0.9.0
- # via
- # keyring
- # secretstorage
jinja2==3.1.6
# via
# -r requirements/test.txt
@@ -172,6 +221,14 @@ jmespath==1.1.0
# -r requirements/test.txt
# boto3
# botocore
+joblib==1.5.3
+ # via
+ # -r requirements/test.txt
+ # nltk
+jsonfield==3.2.0
+ # via
+ # -r requirements/test.txt
+ # edx-submissions
keyring==25.7.0
# via twine
lxml[html-clean]==6.0.2
@@ -179,6 +236,7 @@ lxml[html-clean]==6.0.2
# -r requirements/test.txt
# edx-i18n-tools
# lxml-html-clean
+ # openedx-calc
# xblock
# xblock-sdk
lxml-html-clean==0.4.3
@@ -196,8 +254,10 @@ markdown-it-py==4.0.0
markupsafe==3.0.3
# via
# -r requirements/test.txt
+ # chem
# jinja2
# mako
+ # openedx-calc
# xblock
mdurl==0.1.2
# via
@@ -207,12 +267,29 @@ more-itertools==10.8.0
# via
# jaraco-classes
# jaraco-functools
+mpmath==1.3.0
+ # via
+ # -r requirements/test.txt
+ # sympy
nh3==0.3.2
# via
# -r requirements/test.txt
# readme-renderer
+nltk==3.9.2
+ # via
+ # -r requirements/test.txt
+ # chem
+numpy==2.4.2
+ # via
+ # -r requirements/test.txt
+ # chem
+ # openedx-calc
+ # scipy
+ # shapely
oauthlib==3.3.1
# via -r requirements/test.txt
+openedx-calc==4.0.3
+ # via -r requirements/test.txt
openedx-django-pyfs==3.8.0
# via -r requirements/test.txt
packaging==26.0
@@ -229,6 +306,8 @@ path==16.16.0
# via
# -r requirements/test.txt
# edx-i18n-tools
+pillow==12.1.0
+ # via -r requirements/test.txt
platformdirs==4.5.1
# via
# -r requirements/test.txt
@@ -244,8 +323,14 @@ polib==1.2.0
# via
# -r requirements/test.txt
# edx-i18n-tools
+psutil==7.2.2
+ # via
+ # -r requirements/test.txt
+ # edx-django-utils
pycparser==3.0
- # via cffi
+ # via
+ # -r requirements/test.txt
+ # cffi
pydata-sphinx-theme==0.15.4
# via sphinx-book-theme
pygments==2.19.2
@@ -262,6 +347,15 @@ pymongo==4.16.0
# via
# -r requirements/test.txt
# edx-opaque-keys
+pynacl==1.6.2
+ # via
+ # -r requirements/test.txt
+ # edx-django-utils
+pyparsing==3.3.2
+ # via
+ # -r requirements/test.txt
+ # chem
+ # openedx-calc
pypng==0.20220715.0
# via
# -r requirements/test.txt
@@ -295,16 +389,24 @@ python-slugify==8.0.4
pytz==2025.2
# via
# -r requirements/test.txt
+ # edx-submissions
# xblock
pyyaml==6.0.3
# via
# -r requirements/test.txt
# code-annotations
# cookiecutter
+ # edx-django-release-util
# edx-i18n-tools
# xblock
+random2==1.0.2
+ # via -r requirements/test.txt
readme-renderer==44.0
# via twine
+regex==2026.1.15
+ # via
+ # -r requirements/test.txt
+ # nltk
requests==2.32.5
# via
# -r requirements/test.txt
@@ -320,7 +422,7 @@ restructuredtext-lint==2.0.2
# via doc8
rfc3986==2.0.0
# via twine
-rich==14.3.1
+rich==14.3.2
# via
# -r requirements/test.txt
# cookiecutter
@@ -331,8 +433,12 @@ s3transfer==0.16.0
# via
# -r requirements/test.txt
# boto3
-secretstorage==3.5.0
- # via keyring
+scipy==1.17.0
+ # via
+ # -r requirements/test.txt
+ # chem
+shapely==2.1.2
+ # via -r requirements/test.txt
simplejson==3.20.2
# via
# -r requirements/test.txt
@@ -341,13 +447,18 @@ simplejson==3.20.2
six==1.17.0
# via
# -r requirements/test.txt
+ # edx-codejail
+ # edx-django-release-util
# fs
# fs-s3fs
+ # html5lib
# python-dateutil
snowballstemmer==3.0.1
# via sphinx
soupsieve==2.8.3
- # via beautifulsoup4
+ # via
+ # -r requirements/test.txt
+ # beautifulsoup4
sphinx==9.0.4
# via
# -r requirements/doc.in
@@ -376,13 +487,22 @@ stevedore==5.6.0
# -r requirements/test.txt
# code-annotations
# doc8
+ # edx-django-utils
# edx-opaque-keys
+sympy==1.14.0
+ # via
+ # -r requirements/test.txt
+ # openedx-calc
text-unidecode==1.3
# via
# -r requirements/test.txt
# python-slugify
tox==4.34.1
# via -r requirements/test.txt
+tqdm==4.67.2
+ # via
+ # -r requirements/test.txt
+ # nltk
twine==6.2.0
# via -r requirements/doc.in
typing-extensions==4.15.0
@@ -410,6 +530,10 @@ web-fragments==3.1.0
# -r requirements/test.txt
# xblock
# xblock-sdk
+webencodings==0.5.1
+ # via
+ # -r requirements/test.txt
+ # html5lib
webob==1.8.9
# via
# -r requirements/test.txt
diff --git a/requirements/quality.txt b/requirements/quality.txt
index e8a2f22d..6cb73a0e 100644
--- a/requirements/quality.txt
+++ b/requirements/quality.txt
@@ -20,20 +20,22 @@ astroid==4.0.3
# via
# pylint
# pylint-celery
+beautifulsoup4==4.14.3
+ # via -r requirements/test.txt
binaryornot==0.4.4
# via
# -r requirements/test.txt
# cookiecutter
-boto3==1.42.34
+boto3==1.42.39
# via
# -r requirements/test.txt
# fs-s3fs
-botocore==1.42.34
+botocore==1.42.39
# via
# -r requirements/test.txt
# boto3
# s3transfer
-cachetools==6.2.5
+cachetools==7.0.0
# via
# -r requirements/test.txt
# tox
@@ -41,6 +43,10 @@ certifi==2026.1.4
# via
# -r requirements/test.txt
# requests
+cffi==2.0.0
+ # via
+ # -r requirements/test.txt
+ # pynacl
chardet==5.2.0
# via
# -r requirements/test.txt
@@ -50,19 +56,24 @@ charset-normalizer==3.4.4
# via
# -r requirements/test.txt
# requests
+chem==2.0.0
+ # via -r requirements/test.txt
click==8.3.1
# via
# -r requirements/test.txt
# click-log
# code-annotations
# cookiecutter
+ # edx-django-utils
# edx-lint
+ # nltk
click-log==0.4.0
# via edx-lint
code-annotations==2.3.0
# via
# -r requirements/test.txt
# edx-lint
+ # edx-toggles
colorama==0.4.6
# via
# -r requirements/test.txt
@@ -77,6 +88,8 @@ coverage[toml]==7.13.2
# pytest-cov
ddt==1.7.2
# via -r requirements/test.txt
+defusedxml==0.7.1
+ # via -r requirements/test.txt
dill==0.4.1
# via pylint
distlib==0.4.0
@@ -88,26 +101,67 @@ django==5.2.10
# -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
# -r requirements/test.txt
# django-appconf
+ # django-crum
+ # django-model-utils
# django-statici18n
+ # django-waffle
+ # djangorestframework
+ # edx-django-release-util
+ # edx-django-utils
# edx-i18n-tools
+ # edx-submissions
+ # edx-toggles
+ # jsonfield
# openedx-django-pyfs
# xblock-sdk
django-appconf==1.2.0
# via
# -r requirements/test.txt
# django-statici18n
+django-crum==0.7.9
+ # via
+ # -r requirements/test.txt
+ # edx-django-utils
+ # edx-toggles
+django-model-utils==5.0.0
+ # via
+ # -r requirements/test.txt
+ # edx-submissions
django-statici18n==2.6.0
# via -r requirements/test.txt
+django-waffle==5.0.0
+ # via
+ # -r requirements/test.txt
+ # edx-django-utils
+ # edx-toggles
+djangorestframework==3.16.1
+ # via
+ # -r requirements/test.txt
+ # edx-submissions
dnspython==2.8.0
# via
# -r requirements/test.txt
# pymongo
+edx-codejail==4.1.0
+ # via -r requirements/test.txt
+edx-django-release-util==1.5.0
+ # via
+ # -r requirements/test.txt
+ # edx-submissions
+edx-django-utils==8.0.1
+ # via
+ # -r requirements/test.txt
+ # edx-toggles
edx-i18n-tools==1.9.0
# via -r requirements/test.txt
edx-lint==5.6.0
# via -r requirements/quality.in
edx-opaque-keys==3.0.0
# via -r requirements/test.txt
+edx-submissions==3.12.2
+ # via -r requirements/test.txt
+edx-toggles==5.4.1
+ # via -r requirements/test.txt
filelock==3.20.3
# via
# -r requirements/test.txt
@@ -124,6 +178,8 @@ fs-s3fs==1.1.1
# -r requirements/test.txt
# openedx-django-pyfs
# xblock-sdk
+html5lib==1.1
+ # via -r requirements/test.txt
idna==3.11
# via
# -r requirements/test.txt
@@ -146,11 +202,20 @@ jmespath==1.1.0
# -r requirements/test.txt
# boto3
# botocore
+joblib==1.5.3
+ # via
+ # -r requirements/test.txt
+ # nltk
+jsonfield==3.2.0
+ # via
+ # -r requirements/test.txt
+ # edx-submissions
lxml[html-clean]==6.0.2
# via
# -r requirements/test.txt
# edx-i18n-tools
# lxml-html-clean
+ # openedx-calc
# xblock
# xblock-sdk
lxml-html-clean==0.4.3
@@ -168,8 +233,10 @@ markdown-it-py==4.0.0
markupsafe==3.0.3
# via
# -r requirements/test.txt
+ # chem
# jinja2
# mako
+ # openedx-calc
# xblock
mccabe==0.7.0
# via pylint
@@ -177,10 +244,27 @@ mdurl==0.1.2
# via
# -r requirements/test.txt
# markdown-it-py
+mpmath==1.3.0
+ # via
+ # -r requirements/test.txt
+ # sympy
nh3==0.3.2
# via -r requirements/test.txt
+nltk==3.9.2
+ # via
+ # -r requirements/test.txt
+ # chem
+numpy==2.4.2
+ # via
+ # -r requirements/test.txt
+ # chem
+ # openedx-calc
+ # scipy
+ # shapely
oauthlib==3.3.1
# via -r requirements/test.txt
+openedx-calc==4.0.3
+ # via -r requirements/test.txt
openedx-django-pyfs==3.8.0
# via -r requirements/test.txt
packaging==26.0
@@ -193,6 +277,8 @@ path==16.16.0
# via
# -r requirements/test.txt
# edx-i18n-tools
+pillow==12.1.0
+ # via -r requirements/test.txt
platformdirs==4.5.1
# via
# -r requirements/test.txt
@@ -209,8 +295,16 @@ polib==1.2.0
# via
# -r requirements/test.txt
# edx-i18n-tools
+psutil==7.2.2
+ # via
+ # -r requirements/test.txt
+ # edx-django-utils
pycodestyle==2.14.0
# via -r requirements/quality.in
+pycparser==3.0
+ # via
+ # -r requirements/test.txt
+ # cffi
pydocstyle==6.3.0
# via -r requirements/quality.in
pygments==2.19.2
@@ -236,6 +330,15 @@ pymongo==4.16.0
# via
# -r requirements/test.txt
# edx-opaque-keys
+pynacl==1.6.2
+ # via
+ # -r requirements/test.txt
+ # edx-django-utils
+pyparsing==3.3.2
+ # via
+ # -r requirements/test.txt
+ # chem
+ # openedx-calc
pypng==0.20220715.0
# via
# -r requirements/test.txt
@@ -267,20 +370,28 @@ python-slugify==8.0.4
pytz==2025.2
# via
# -r requirements/test.txt
+ # edx-submissions
# xblock
pyyaml==6.0.3
# via
# -r requirements/test.txt
# code-annotations
# cookiecutter
+ # edx-django-release-util
# edx-i18n-tools
# xblock
+random2==1.0.2
+ # via -r requirements/test.txt
+regex==2026.1.15
+ # via
+ # -r requirements/test.txt
+ # nltk
requests==2.32.5
# via
# -r requirements/test.txt
# cookiecutter
# xblock-sdk
-rich==14.3.1
+rich==14.3.2
# via
# -r requirements/test.txt
# cookiecutter
@@ -288,6 +399,12 @@ s3transfer==0.16.0
# via
# -r requirements/test.txt
# boto3
+scipy==1.17.0
+ # via
+ # -r requirements/test.txt
+ # chem
+shapely==2.1.2
+ # via -r requirements/test.txt
simplejson==3.20.2
# via
# -r requirements/test.txt
@@ -296,12 +413,19 @@ simplejson==3.20.2
six==1.17.0
# via
# -r requirements/test.txt
+ # edx-codejail
+ # edx-django-release-util
# edx-lint
# fs
# fs-s3fs
+ # html5lib
# python-dateutil
snowballstemmer==3.0.1
# via pydocstyle
+soupsieve==2.8.3
+ # via
+ # -r requirements/test.txt
+ # beautifulsoup4
sqlparse==0.5.5
# via
# -r requirements/test.txt
@@ -310,7 +434,12 @@ stevedore==5.6.0
# via
# -r requirements/test.txt
# code-annotations
+ # edx-django-utils
# edx-opaque-keys
+sympy==1.14.0
+ # via
+ # -r requirements/test.txt
+ # openedx-calc
text-unidecode==1.3
# via
# -r requirements/test.txt
@@ -319,9 +448,14 @@ tomlkit==0.14.0
# via pylint
tox==4.34.1
# via -r requirements/test.txt
+tqdm==4.67.2
+ # via
+ # -r requirements/test.txt
+ # nltk
typing-extensions==4.15.0
# via
# -r requirements/test.txt
+ # beautifulsoup4
# edx-opaque-keys
tzdata==2025.3
# via
@@ -341,6 +475,10 @@ web-fragments==3.1.0
# -r requirements/test.txt
# xblock
# xblock-sdk
+webencodings==0.5.1
+ # via
+ # -r requirements/test.txt
+ # html5lib
webob==1.8.9
# via
# -r requirements/test.txt
diff --git a/requirements/test.txt b/requirements/test.txt
index 15b53e11..ea1287d1 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -14,33 +14,47 @@ asgiref==3.11.0
# via
# -r requirements/base.txt
# django
+beautifulsoup4==4.14.3
+ # via -r requirements/base.txt
binaryornot==0.4.4
# via cookiecutter
-boto3==1.42.34
+boto3==1.42.39
# via
# -r requirements/base.txt
# fs-s3fs
-botocore==1.42.34
+botocore==1.42.39
# via
# -r requirements/base.txt
# boto3
# s3transfer
-cachetools==6.2.5
+cachetools==7.0.0
# via tox
certifi==2026.1.4
# via requests
+cffi==2.0.0
+ # via
+ # -r requirements/base.txt
+ # pynacl
chardet==5.2.0
# via
# binaryornot
# tox
charset-normalizer==3.4.4
# via requests
+chem==2.0.0
+ # via -r requirements/base.txt
click==8.3.1
# via
+ # -r requirements/base.txt
# code-annotations
# cookiecutter
+ # edx-django-utils
+ # nltk
code-annotations==2.3.0
- # via -r requirements/test.in
+ # via
+ # -r requirements/base.txt
+ # -r requirements/test.in
+ # edx-toggles
colorama==0.4.6
# via tox
cookiecutter==2.6.0
@@ -48,33 +62,78 @@ cookiecutter==2.6.0
coverage[toml]==7.13.2
# via pytest-cov
ddt==1.7.2
- # via -r requirements/test.in
+ # via
+ # -r requirements/base.txt
+ # -r requirements/test.in
+defusedxml==0.7.1
+ # via -r requirements/base.txt
distlib==0.4.0
# via virtualenv
# via
# -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
# -r requirements/base.txt
# django-appconf
+ # django-crum
+ # django-model-utils
# django-statici18n
+ # django-waffle
+ # djangorestframework
+ # edx-django-release-util
+ # edx-django-utils
# edx-i18n-tools
+ # edx-submissions
+ # edx-toggles
+ # jsonfield
# openedx-django-pyfs
# xblock-sdk
django-appconf==1.2.0
# via
# -r requirements/base.txt
# django-statici18n
+django-crum==0.7.9
+ # via
+ # -r requirements/base.txt
+ # edx-django-utils
+ # edx-toggles
+django-model-utils==5.0.0
+ # via
+ # -r requirements/base.txt
+ # edx-submissions
django-statici18n==2.6.0
# via -r requirements/base.txt
+django-waffle==5.0.0
+ # via
+ # -r requirements/base.txt
+ # edx-django-utils
+ # edx-toggles
+djangorestframework==3.16.1
+ # via
+ # -r requirements/base.txt
+ # edx-submissions
dnspython==2.8.0
# via
# -r requirements/base.txt
# pymongo
+edx-codejail==4.1.0
+ # via -r requirements/base.txt
+edx-django-release-util==1.5.0
+ # via
+ # -r requirements/base.txt
+ # edx-submissions
+edx-django-utils==8.0.1
+ # via
+ # -r requirements/base.txt
+ # edx-toggles
edx-i18n-tools==1.9.0
# via -r requirements/base.txt
edx-opaque-keys==3.0.0
# via
# -r requirements/base.txt
# -r requirements/test.in
+edx-submissions==3.12.2
+ # via -r requirements/base.txt
+edx-toggles==5.4.1
+ # via -r requirements/base.txt
filelock==3.20.3
# via
# tox
@@ -90,12 +149,15 @@ fs-s3fs==1.1.1
# -r requirements/base.txt
# openedx-django-pyfs
# xblock-sdk
+html5lib==1.1
+ # via -r requirements/base.txt
idna==3.11
# via requests
iniconfig==2.3.0
# via pytest
jinja2==3.1.6
# via
+ # -r requirements/base.txt
# code-annotations
# cookiecutter
jmespath==1.1.0
@@ -103,11 +165,20 @@ jmespath==1.1.0
# -r requirements/base.txt
# boto3
# botocore
+joblib==1.5.3
+ # via
+ # -r requirements/base.txt
+ # nltk
+jsonfield==3.2.0
+ # via
+ # -r requirements/base.txt
+ # edx-submissions
lxml[html-clean]==6.0.2
# via
# -r requirements/base.txt
# edx-i18n-tools
# lxml-html-clean
+ # openedx-calc
# xblock
# xblock-sdk
lxml-html-clean==0.4.3
@@ -123,15 +194,34 @@ markdown-it-py==4.0.0
markupsafe==3.0.3
# via
# -r requirements/base.txt
+ # chem
# jinja2
# mako
+ # openedx-calc
# xblock
mdurl==0.1.2
# via markdown-it-py
+mpmath==1.3.0
+ # via
+ # -r requirements/base.txt
+ # sympy
nh3==0.3.2
# via -r requirements/base.txt
+nltk==3.9.2
+ # via
+ # -r requirements/base.txt
+ # chem
+numpy==2.4.2
+ # via
+ # -r requirements/base.txt
+ # chem
+ # openedx-calc
+ # scipy
+ # shapely
oauthlib==3.3.1
# via -r requirements/base.txt
+openedx-calc==4.0.3
+ # via -r requirements/base.txt
openedx-django-pyfs==3.8.0
# via -r requirements/base.txt
packaging==26.0
@@ -143,6 +233,8 @@ path==16.16.0
# via
# -r requirements/base.txt
# edx-i18n-tools
+pillow==12.1.0
+ # via -r requirements/base.txt
platformdirs==4.5.1
# via
# tox
@@ -156,6 +248,14 @@ polib==1.2.0
# via
# -r requirements/base.txt
# edx-i18n-tools
+psutil==7.2.2
+ # via
+ # -r requirements/base.txt
+ # edx-django-utils
+pycparser==3.0
+ # via
+ # -r requirements/base.txt
+ # cffi
pygments==2.19.2
# via
# pytest
@@ -164,6 +264,15 @@ pymongo==4.16.0
# via
# -r requirements/base.txt
# edx-opaque-keys
+pynacl==1.6.2
+ # via
+ # -r requirements/base.txt
+ # edx-django-utils
+pyparsing==3.3.2
+ # via
+ # -r requirements/base.txt
+ # chem
+ # openedx-calc
pypng==0.20220715.0
# via xblock-sdk
pyproject-api==1.10.0
@@ -184,29 +293,44 @@ python-dateutil==2.9.0.post0
# xblock
python-slugify==8.0.4
# via
+ # -r requirements/base.txt
# code-annotations
# cookiecutter
pytz==2025.2
# via
# -r requirements/base.txt
+ # edx-submissions
# xblock
pyyaml==6.0.3
# via
# -r requirements/base.txt
# code-annotations
# cookiecutter
+ # edx-django-release-util
# edx-i18n-tools
# xblock
+random2==1.0.2
+ # via -r requirements/base.txt
+regex==2026.1.15
+ # via
+ # -r requirements/base.txt
+ # nltk
requests==2.32.5
# via
# cookiecutter
# xblock-sdk
-rich==14.3.1
+rich==14.3.2
# via cookiecutter
s3transfer==0.16.0
# via
# -r requirements/base.txt
# boto3
+scipy==1.17.0
+ # via
+ # -r requirements/base.txt
+ # chem
+shapely==2.1.2
+ # via -r requirements/base.txt
simplejson==3.20.2
# via
# -r requirements/base.txt
@@ -215,9 +339,16 @@ simplejson==3.20.2
six==1.17.0
# via
# -r requirements/base.txt
+ # edx-codejail
+ # edx-django-release-util
# fs
# fs-s3fs
+ # html5lib
# python-dateutil
+soupsieve==2.8.3
+ # via
+ # -r requirements/base.txt
+ # beautifulsoup4
sqlparse==0.5.5
# via
# -r requirements/base.txt
@@ -226,14 +357,26 @@ stevedore==5.6.0
# via
# -r requirements/base.txt
# code-annotations
+ # edx-django-utils
# edx-opaque-keys
+sympy==1.14.0
+ # via
+ # -r requirements/base.txt
+ # openedx-calc
text-unidecode==1.3
- # via python-slugify
+ # via
+ # -r requirements/base.txt
+ # python-slugify
tox==4.34.1
# via -r requirements/test.in
+tqdm==4.67.2
+ # via
+ # -r requirements/base.txt
+ # nltk
typing-extensions==4.15.0
# via
# -r requirements/base.txt
+ # beautifulsoup4
# edx-opaque-keys
tzdata==2025.3
# via arrow
@@ -249,6 +392,10 @@ web-fragments==3.1.0
# -r requirements/base.txt
# xblock
# xblock-sdk
+webencodings==0.5.1
+ # via
+ # -r requirements/base.txt
+ # html5lib
webob==1.8.9
# via
# -r requirements/base.txt
diff --git a/xblocks_contrib/problem/assets/fixtures/checkbox_problem.html b/xblocks_contrib/problem/assets/fixtures/checkbox_problem.html
new file mode 100644
index 00000000..053f9b31
--- /dev/null
+++ b/xblocks_contrib/problem/assets/fixtures/checkbox_problem.html
@@ -0,0 +1,14 @@
+
+
+
+ One
+
+
+
+ Two
+
+
+
+ Three
+
+
diff --git a/xblocks_contrib/problem/assets/fixtures/codeinput_problem.html b/xblocks_contrib/problem/assets/fixtures/codeinput_problem.html
new file mode 100644
index 00000000..904cdce0
--- /dev/null
+++ b/xblocks_contrib/problem/assets/fixtures/codeinput_problem.html
@@ -0,0 +1,20 @@
+
+
question label here
+
+
Press ESC then TAB or click outside of the code editor to exit
+
+
+ correct
+
+
+
diff --git a/xblocks_contrib/problem/assets/fixtures/imageinput.html b/xblocks_contrib/problem/assets/fixtures/imageinput.html
new file mode 100644
index 00000000..d77f4b8e
--- /dev/null
+++ b/xblocks_contrib/problem/assets/fixtures/imageinput.html
@@ -0,0 +1,30 @@
+
+
+
+
diff --git a/xblocks_contrib/problem/assets/fixtures/imageinput.underscore b/xblocks_contrib/problem/assets/fixtures/imageinput.underscore
new file mode 100644
index 00000000..a797aa44
--- /dev/null
+++ b/xblocks_contrib/problem/assets/fixtures/imageinput.underscore
@@ -0,0 +1,30 @@
+
+
+
+
diff --git a/xblocks_contrib/problem/assets/fixtures/jsinput_problem.html b/xblocks_contrib/problem/assets/fixtures/jsinput_problem.html
new file mode 100644
index 00000000..1c382ed8
--- /dev/null
+++ b/xblocks_contrib/problem/assets/fixtures/jsinput_problem.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+ Show Answer Reveal Answer
+
+
+
diff --git a/xblocks_contrib/problem/assets/fixtures/matlabinput_problem.html b/xblocks_contrib/problem/assets/fixtures/matlabinput_problem.html
new file mode 100644
index 00000000..93e06ffe
--- /dev/null
+++ b/xblocks_contrib/problem/assets/fixtures/matlabinput_problem.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+ processing
+
+
1
+
processing
+
+
+
+ Submitted. As soon as a response is returned, this message will be replaced by that feedback.
+
+
+
+
+
+
+
+
+
+
+
+ Resetyour answer
+
+
+ Show Answer
+
+
+
+
diff --git a/xblocks_contrib/problem/assets/fixtures/problem.html b/xblocks_contrib/problem/assets/fixtures/problem.html
new file mode 100644
index 00000000..841b8dc1
--- /dev/null
+++ b/xblocks_contrib/problem/assets/fixtures/problem.html
@@ -0,0 +1,8 @@
+
diff --git a/xblocks_contrib/problem/assets/fixtures/problem_content.html b/xblocks_contrib/problem/assets/fixtures/problem_content.html
new file mode 100644
index 00000000..9f252b6f
--- /dev/null
+++ b/xblocks_contrib/problem/assets/fixtures/problem_content.html
@@ -0,0 +1,44 @@
+
+
+
+
${_("Problem Content")}
+
+
+
+
+
+
+
+ Reset
+
+
+ Save
+
+
+
+ Show Answer(s) (for question(s) above - adjacent to each field)
+
+
+
+
+ Submit your answer
+
+
Explanation
+
+
+
+
diff --git a/xblocks_contrib/problem/assets/fixtures/problem_content_1240.html b/xblocks_contrib/problem/assets/fixtures/problem_content_1240.html
new file mode 100644
index 00000000..700286b7
--- /dev/null
+++ b/xblocks_contrib/problem/assets/fixtures/problem_content_1240.html
@@ -0,0 +1,23 @@
+
+
+
+
${_("Problem Content")}
+
+
+
+
+
+
+
+
+
+ Show Answer(s) (for question(s) above - adjacent to each field)
+
+
Explanation
+
+
+
diff --git a/xblocks_contrib/problem/assets/fixtures/radiobutton_problem.html b/xblocks_contrib/problem/assets/fixtures/radiobutton_problem.html
new file mode 100644
index 00000000..21428cc0
--- /dev/null
+++ b/xblocks_contrib/problem/assets/fixtures/radiobutton_problem.html
@@ -0,0 +1,14 @@
+
+
+
+ One
+
+
+
+ Two
+
+
+
+ Three
+
+
diff --git a/xblocks_contrib/problem/assets/karma_runner.js b/xblocks_contrib/problem/assets/karma_runner.js
new file mode 100644
index 00000000..62cf12c8
--- /dev/null
+++ b/xblocks_contrib/problem/assets/karma_runner.js
@@ -0,0 +1,12 @@
+/* eslint-env node */
+
+// overwrite the loaded method and manually start the karma after a delay
+// Somehow the code initialized in jQuery's onready doesn't get called before karma auto starts
+
+'use strict';
+
+window.__karma__.loaded = function() {
+ setTimeout(function() {
+ window.__karma__.start();
+ }, 1000);
+};
diff --git a/xblocks_contrib/problem/assets/spec/collapsible_spec.js b/xblocks_contrib/problem/assets/spec/collapsible_spec.js
new file mode 100644
index 00000000..a924e1cb
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/collapsible_spec.js
@@ -0,0 +1,130 @@
+// eslint-disable-next-line no-shadow-restricted-names
+(function (undefined) {
+ "use strict";
+
+ describe("Collapsible", function () {
+ var $el,
+ html,
+ html_custom,
+ initialize = function (template) {
+ setFixtures(template);
+ $el = $(".collapsible");
+ Collapsible.setCollapsibles($el);
+ },
+ disableFx = function () {
+ $.fx.off = true;
+ },
+ enableFx = function () {
+ $.fx.off = false;
+ };
+
+ beforeEach(function () {
+ html =
+ "" +
+ '' +
+ 'shortform message
' +
+ '" +
+ " ";
+ html_custom =
+ "" +
+ '' +
+ "shortform message
" +
+ '" +
+ " ";
+ });
+
+ describe("setCollapsibles", function () {
+ it("Default container initialized correctly", function () {
+ initialize(html);
+
+ expect($el.find(".shortform")).toContainElement(".full-top");
+ expect($el.find(".shortform")).toContainElement(".full-bottom");
+ expect($el.find(".longform")).toBeHidden();
+ expect($el.find(".full")).toHandle("click");
+ });
+
+ it("Custom container initialized correctly", function () {
+ initialize(html_custom);
+
+ expect($el.find(".shortform-custom")).toContainElement(".full-custom");
+ expect($el.find(".full-custom")).toHaveText("Show shortform-custom");
+ expect($el.find(".longform")).toBeHidden();
+ expect($el.find(".full-custom")).toHandle("click");
+ });
+ });
+
+ describe("toggleFull", function () {
+ var assertChanges = function (state, anchorsElClass, showText, hideText) {
+ var anchors, text;
+
+ if (state == null) {
+ state = "closed";
+ }
+
+ anchors = $el.find("." + anchorsElClass);
+
+ if (state === "closed") {
+ expect($el.find(".longform")).toBeHidden();
+ expect($el).not.toHaveClass("open");
+ text = showText;
+ } else {
+ expect($el.find(".longform")).toBeVisible();
+ expect($el).toHaveClass("open");
+ text = hideText;
+ }
+
+ $.each(anchors, function (index, el) {
+ expect(el).toHaveText(text);
+ });
+ };
+
+ beforeEach(function () {
+ disableFx();
+ });
+
+ afterEach(function () {
+ enableFx();
+ });
+
+ it("Default container", function () {
+ var event;
+
+ initialize(html);
+
+ event = jQuery.Event("click", {
+ target: $el.find(".full").get(0),
+ });
+
+ Collapsible.toggleFull(event, "See full output", "Hide output");
+ assertChanges("opened", "full", "See full output", "Hide output");
+
+ Collapsible.toggleFull(event, "See full output", "Hide output");
+ assertChanges("closed", "full", "See full output", "Hide output");
+ });
+
+ it("Custom container", function () {
+ var event;
+
+ initialize(html_custom);
+
+ event = jQuery.Event("click", {
+ target: $el.find(".full-custom").get(0),
+ });
+
+ Collapsible.toggleFull(event, "Show shortform-custom", "Hide shortform-custom");
+ assertChanges("opened", "full-custom", "Show shortform-custom", "Hide shortform-custom");
+
+ Collapsible.toggleFull(event, "Show shortform-custom", "Hide shortform-custom");
+ assertChanges("closed", "full-custom", "Show shortform-custom", "Hide shortform-custom");
+ });
+ });
+ });
+}).call(this);
diff --git a/xblocks_contrib/problem/assets/spec/display_spec.js b/xblocks_contrib/problem/assets/spec/display_spec.js
new file mode 100644
index 00000000..f55106f3
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/display_spec.js
@@ -0,0 +1,1176 @@
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+describe("Problem", function () {
+ const problem_content_default = readFixtures("problem_content.html");
+
+ beforeEach(function () {
+ // Stub MathJax
+ window.MathJax = {
+ Hub: jasmine.createSpyObj("MathJax.Hub", ["getAllJax", "Queue"]),
+ Callback: jasmine.createSpyObj("MathJax.Callback", ["After"]),
+ };
+ this.stubbedJax = { root: jasmine.createSpyObj("jax.root", ["toMathML"]) };
+ MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]);
+ window.update_schematics = function () {};
+ spyOn(SR, "readText");
+ spyOn(SR, "readTexts");
+
+ // Load this function from spec/helper.js
+ // Note that if your test fails with a message like:
+ // 'External request attempted for blah, which is not defined.'
+ // this msg is coming from the stubRequests function else clause.
+ jasmine.stubRequests();
+
+ loadFixtures("problem.html");
+
+ spyOn(Logger, "log");
+ spyOn($.fn, "load").and.callFake(function (url, callback) {
+ $(this).html(readFixtures("problem_content.html"));
+ return callback();
+ });
+ });
+
+ describe("constructor", function () {
+ it("set the element from html", function () {
+ this.problem999 = new Problem(`\
+\
+`);
+ expect(this.problem999.element_id).toBe("problem_999");
+ });
+
+ it("set the element from loadFixtures", function () {
+ this.problem1 = new Problem($(".xblock-student_view"));
+ expect(this.problem1.element_id).toBe("problem_1");
+ });
+ });
+
+ describe("bind", function () {
+ beforeEach(function () {
+ spyOn(window, "update_schematics");
+ MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]);
+ this.problem = new Problem($(".xblock-student_view"));
+ });
+
+ it("set mathjax typeset", () => expect(MathJax.Hub.Queue).toHaveBeenCalled());
+
+ it("update schematics", () => expect(window.update_schematics).toHaveBeenCalled());
+
+ it("bind answer refresh on button click", function () {
+ expect($("div.action button")).toHandleWith("click", this.problem.refreshAnswers);
+ });
+
+ it("bind the submit button", function () {
+ expect($(".action .submit")).toHandleWith("click", this.problem.submit_fd);
+ });
+
+ it("bind the reset button", function () {
+ expect($("div.action button.reset")).toHandleWith("click", this.problem.reset);
+ });
+
+ it("bind the show button", function () {
+ expect($(".action .show")).toHandleWith("click", this.problem.show);
+ });
+
+ it("bind the save button", function () {
+ expect($("div.action button.save")).toHandleWith("click", this.problem.save);
+ });
+
+ it("bind the math input", function () {
+ expect($("input.math")).toHandleWith("keyup", this.problem.refreshMath);
+ });
+ });
+
+ describe("bind_with_custom_input_id", function () {
+ beforeEach(function () {
+ spyOn(window, "update_schematics");
+ MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]);
+ this.problem = new Problem($(".xblock-student_view"));
+ return $(this).html(readFixtures("problem_content_1240.html"));
+ });
+
+ it("bind the submit button", function () {
+ expect($(".action .submit")).toHandleWith("click", this.problem.submit_fd);
+ });
+
+ it("bind the show button", function () {
+ expect($("div.action button.show")).toHandleWith("click", this.problem.show);
+ });
+ });
+
+ describe("renderProgressState", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ });
+
+ const testProgessData = function (
+ problem,
+ score,
+ total_possible,
+ attempts,
+ graded,
+ expected_progress_after_render,
+ ) {
+ problem.el.data("problem-score", score);
+ problem.el.data("problem-total-possible", total_possible);
+ problem.el.data("attempts-used", attempts);
+ problem.el.data("graded", graded);
+ expect(problem.$(".problem-progress").html()).toEqual("");
+ problem.renderProgressState();
+ expect(problem.$(".problem-progress").html()).toEqual(expected_progress_after_render);
+ };
+
+ describe('with a status of "none"', function () {
+ it("reports the number of points possible and graded", function () {
+ testProgessData(this.problem, 0, 1, 0, "True", "1 point possible (graded)");
+ });
+
+ it("displays the number of points possible when rendering happens with the content", function () {
+ testProgessData(this.problem, 0, 2, 0, "True", "2 points possible (graded)");
+ });
+
+ it("reports the number of points possible and ungraded", function () {
+ testProgessData(this.problem, 0, 1, 0, "False", "1 point possible (ungraded)");
+ });
+
+ it("displays ungraded if number of points possible is 0", function () {
+ testProgessData(this.problem, 0, 0, 0, "False", "0 points possible (ungraded)");
+ });
+
+ it("displays ungraded if number of points possible is 0, even if graded value is True", function () {
+ testProgessData(this.problem, 0, 0, 0, "True", "0 points possible (ungraded)");
+ });
+
+ it("reports the correct score with status none and >0 attempts", function () {
+ testProgessData(this.problem, 0, 1, 1, "True", "0/1 point (graded)");
+ });
+
+ it("reports the correct score with >1 weight, status none, and >0 attempts", function () {
+ testProgessData(this.problem, 0, 2, 2, "True", "0/2 points (graded)");
+ });
+ });
+
+ describe("with any other valid status", function () {
+ it("reports the current score", function () {
+ testProgessData(this.problem, 1, 1, 1, "True", "1/1 point (graded)");
+ });
+
+ it("shows current score when rendering happens with the content", function () {
+ testProgessData(this.problem, 2, 2, 1, "True", "2/2 points (graded)");
+ });
+
+ it("reports the current score even if problem is ungraded", function () {
+ testProgessData(this.problem, 1, 1, 1, "False", "1/1 point (ungraded)");
+ });
+ });
+
+ describe('with valid status and string containing an integer like "0" for detail', () =>
+ // These tests are to address a failure specific to Chrome 51 and 52 +
+ it("shows 0 points possible for the detail", function () {
+ testProgessData(this.problem, 0, 0, 1, "False", "0 points possible (ungraded)");
+ }));
+
+ describe("with a score of null (show_correctness == false)", function () {
+ it("reports the number of points possible and graded, results hidden", function () {
+ testProgessData(this.problem, null, 1, 0, "True", "1 point possible (graded, results hidden)");
+ });
+
+ it("reports the number of points possible (plural) and graded, results hidden", function () {
+ testProgessData(this.problem, null, 2, 0, "True", "2 points possible (graded, results hidden)");
+ });
+
+ it("reports the number of points possible and ungraded, results hidden", function () {
+ testProgessData(this.problem, null, 1, 0, "False", "1 point possible (ungraded, results hidden)");
+ });
+
+ it("displays ungraded if number of points possible is 0, results hidden", function () {
+ testProgessData(this.problem, null, 0, 0, "False", "0 points possible (ungraded, results hidden)");
+ });
+
+ it("displays ungraded if number of points possible is 0, even if graded value is True, results hidden", function () {
+ testProgessData(this.problem, null, 0, 0, "True", "0 points possible (ungraded, results hidden)");
+ });
+
+ it("reports the correct score with status none and >0 attempts, results hidden", function () {
+ testProgessData(this.problem, null, 1, 1, "True", "1 point possible (graded, results hidden)");
+ });
+
+ it("reports the correct score with >1 weight, status none, and >0 attempts, results hidden", function () {
+ testProgessData(this.problem, null, 2, 2, "True", "2 points possible (graded, results hidden)");
+ });
+ });
+ });
+
+ describe("render", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ this.bind = this.problem.bind;
+ spyOn(this.problem, "bind");
+ });
+
+ describe("with content given", function () {
+ beforeEach(function () {
+ this.problem.render("Hello World");
+ });
+
+ it("render the content", function () {
+ expect(this.problem.el.html()).toEqual("Hello World");
+ });
+
+ it("re-bind the content", function () {
+ expect(this.problem.bind).toHaveBeenCalled();
+ });
+ });
+
+ describe("with no content given", function () {
+ beforeEach(function () {
+ spyOn($, "postWithPrefix").and.callFake((url, callback) => callback({ html: "Hello World" }));
+ this.problem.render();
+ });
+
+ it("load the content via ajax", function () {
+ expect(this.problem.el.html()).toEqual("Hello World");
+ });
+
+ it("re-bind the content", function () {
+ expect(this.problem.bind).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe("submit_fd", function () {
+ beforeEach(function () {
+ // Insert an input of type file outside of the problem.
+ $(".xblock-student_view").after(' ');
+ this.problem = new Problem($(".xblock-student_view"));
+ spyOn(this.problem, "submit");
+ });
+
+ it("submit method is called if input of type file is not in problem", function () {
+ this.problem.submit_fd();
+ expect(this.problem.submit).toHaveBeenCalled();
+ });
+ });
+
+ describe("submit", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ this.problem.answers = "foo=1&bar=2";
+ });
+
+ it("log the problem_check event", function () {
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ done(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.submit();
+ expect(Logger.log).toHaveBeenCalledWith("problem_check", "foo=1&bar=2");
+ });
+
+ it("log the problem_graded event, after the problem is done grading.", function () {
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ const response = {
+ success: "correct",
+ contents: "mock grader response",
+ };
+ callback(response);
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ done(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.submit();
+ expect(Logger.log).toHaveBeenCalledWith(
+ "problem_graded",
+ ["foo=1&bar=2", "mock grader response"],
+ this.problem.id,
+ );
+ });
+
+ it("submit the answer for submit", function () {
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ done(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.submit();
+ expect($.postWithPrefix).toHaveBeenCalledWith(
+ "/problem/Problem1/problem_check",
+ "foo=1&bar=2",
+ jasmine.any(Function),
+ );
+ });
+
+ describe("when the response is correct", () =>
+ it("call render with returned content", function () {
+ const contents =
+ '' +
+ '';
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ callback({ success: "correct", contents });
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ done(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.submit();
+ expect(this.problem.el).toHaveHtml(contents);
+ expect(window.SR.readTexts).toHaveBeenCalledWith(["Question 1: excellent", "Question 2: correct"]);
+ }));
+
+ describe("when the response is incorrect", () =>
+ it("call render with returned content", function () {
+ const contents = 'Incorrectno, try again
';
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ callback({ success: "incorrect", contents });
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ done(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.submit();
+ expect(this.problem.el).toHaveHtml(contents);
+ expect(window.SR.readTexts).toHaveBeenCalledWith(["no, try again"]);
+ }));
+
+ it("tests if the submit button is disabled while submitting and the text changes on the button", function () {
+ const self = this;
+ const curr_html = this.problem.el.html();
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ // At this point enableButtons should have been called, making the submit button disabled with text 'submitting'
+ let promise;
+ expect(self.problem.submitButton).toHaveAttr("disabled");
+ expect(self.problem.submitButtonLabel.text()).toBe("Submitting");
+ callback({
+ success: "incorrect", // does not matter if correct or incorrect here
+ contents: curr_html,
+ });
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ done(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ // Make sure the submit button is enabled before submitting
+ $("#input_example_1").val("test").trigger("input");
+ expect(this.problem.submitButton).not.toHaveAttr("disabled");
+ this.problem.submit();
+ // After submit, the button should not be disabled and should have text as 'Submit'
+ expect(this.problem.submitButtonLabel.text()).toBe("Submit");
+ expect(this.problem.submitButton).not.toHaveAttr("disabled");
+ });
+ });
+
+ describe("submit button on problems", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ this.submitDisabled = (disabled) => {
+ if (disabled) {
+ expect(this.problem.submitButton).toHaveAttr("disabled");
+ } else {
+ expect(this.problem.submitButton).not.toHaveAttr("disabled");
+ }
+ };
+ });
+
+ describe("some basic tests for submit button", () =>
+ it("should become enabled after a value is entered into the text box", function () {
+ $("#input_example_1").val("test").trigger("input");
+ this.submitDisabled(false);
+ $("#input_example_1").val("").trigger("input");
+ this.submitDisabled(true);
+ }));
+
+ describe("some advanced tests for submit button", function () {
+ const radioButtonProblemHtml = readFixtures("radiobutton_problem.html");
+ const checkboxProblemHtml = readFixtures("checkbox_problem.html");
+
+ it("should become enabled after a checkbox is checked", function () {
+ $("#input_example_1").replaceWith(checkboxProblemHtml);
+ this.problem.submitAnswersAndSubmitButton(true);
+ this.submitDisabled(true);
+ $("#input_1_1_1").click();
+ this.submitDisabled(false);
+ $("#input_1_1_1").click();
+ this.submitDisabled(true);
+ });
+
+ it("should become enabled after a radiobutton is checked", function () {
+ $("#input_example_1").replaceWith(radioButtonProblemHtml);
+ this.problem.submitAnswersAndSubmitButton(true);
+ this.submitDisabled(true);
+ $("#input_1_1_1").attr("checked", true).trigger("click");
+ this.submitDisabled(false);
+ $("#input_1_1_1").attr("checked", false).trigger("click");
+ this.submitDisabled(true);
+ });
+
+ it("should become enabled after a value is selected in a selector", function () {
+ const html = `\
+
+
+Select an option
+1
+2
+
+
\
+`;
+ $("#input_example_1").replaceWith(html);
+ this.problem.submitAnswersAndSubmitButton(true);
+ this.submitDisabled(true);
+ $("#problem_sel select").val("val2").trigger("change");
+ this.submitDisabled(false);
+ $("#problem_sel select").val("val0").trigger("change");
+ this.submitDisabled(true);
+ });
+
+ it("should become enabled after a radiobutton is checked and a value is entered into the text box", function () {
+ $(radioButtonProblemHtml).insertAfter("#input_example_1");
+ this.problem.submitAnswersAndSubmitButton(true);
+ this.submitDisabled(true);
+ $("#input_1_1_1").attr("checked", true).trigger("click");
+ this.submitDisabled(true);
+ $("#input_example_1").val("111").trigger("input");
+ this.submitDisabled(false);
+ $("#input_1_1_1").attr("checked", false).trigger("click");
+ this.submitDisabled(true);
+ });
+
+ it("should become enabled if there are only hidden input fields", function () {
+ const html = `\
+ \
+`;
+ $("#input_example_1").replaceWith(html);
+ this.problem.submitAnswersAndSubmitButton(true);
+ this.submitDisabled(false);
+ });
+ });
+ });
+
+ describe("reset", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ });
+
+ it("log the problem_reset event", function () {
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.answers = "foo=1&bar=2";
+ this.problem.reset();
+ expect(Logger.log).toHaveBeenCalledWith("problem_reset", "foo=1&bar=2");
+ });
+
+ it("POST to the problem reset page", function () {
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.reset();
+ expect($.postWithPrefix).toHaveBeenCalledWith(
+ "/problem/Problem1/problem_reset",
+ { id: "i4x://edX/101/problem/Problem1" },
+ jasmine.any(Function),
+ );
+ });
+
+ it("render the returned content", function () {
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ callback({ html: "Reset", success: true });
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.reset();
+ expect(this.problem.el.html()).toEqual("Reset");
+ });
+
+ it("sends a message to the window SR element", function () {
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ callback({ html: "Reset", success: true });
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.reset();
+ expect(window.SR.readText).toHaveBeenCalledWith("This problem has been reset.");
+ });
+
+ it("shows a notification on error", function () {
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ callback({ msg: "Error on reset.", success: false });
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.reset();
+ expect($(".notification-gentle-alert .notification-message").text()).toEqual("Error on reset.");
+ });
+
+ it("tests that reset does not enable submit or modify the text while resetting", function () {
+ const self = this;
+ const curr_html = this.problem.el.html();
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ // enableButtons should have been called at this point to set them to all disabled
+ let promise;
+ expect(self.problem.submitButton).toHaveAttr("disabled");
+ expect(self.problem.submitButtonLabel.text()).toBe("Submit");
+ callback({ success: "correct", html: curr_html });
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ // Submit should be disabled
+ expect(this.problem.submitButton).toHaveAttr("disabled");
+ this.problem.reset();
+ // Submit should remain disabled
+ expect(self.problem.submitButton).toHaveAttr("disabled");
+ expect(self.problem.submitButtonLabel.text()).toBe("Submit");
+ });
+ });
+
+ describe("show problem with column in id", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ this.problem.el.prepend('
');
+ });
+
+ it("log the problem_show event", function () {
+ this.problem.show();
+ expect(Logger.log).toHaveBeenCalledWith("problem_show", { problem: "i4x://edX/101/problem/Problem1" });
+ });
+
+ it("fetch the answers", function () {
+ spyOn($, "postWithPrefix");
+ this.problem.show();
+ expect($.postWithPrefix).toHaveBeenCalledWith("/problem/Problem1/problem_show", jasmine.any(Function));
+ });
+
+ it("show the answers", function () {
+ spyOn($, "postWithPrefix").and.callFake((url, callback) =>
+ callback({ answers: { "1_1:11": "One", "1_2:12": "Two" } }),
+ );
+ this.problem.show();
+ expect($("#answer_1_1\\:11")).toHaveHtml("One");
+ expect($("#answer_1_2\\:12")).toHaveHtml("Two");
+ });
+
+ it("disables the show answer button", function () {
+ spyOn($, "postWithPrefix").and.callFake((url, callback) => callback({ answers: {} }));
+ this.problem.show();
+ expect(this.problem.el.find(".show").attr("disabled")).toEqual("disabled");
+ });
+ });
+
+ describe("show", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ this.problem.el.prepend('
');
+ });
+
+ describe("when the answer has not yet shown", function () {
+ beforeEach(function () {
+ expect(this.problem.el.find(".show").attr("disabled")).not.toEqual("disabled");
+ });
+
+ it("log the problem_show event", function () {
+ this.problem.show();
+ expect(Logger.log).toHaveBeenCalledWith("problem_show", { problem: "i4x://edX/101/problem/Problem1" });
+ });
+
+ it("fetch the answers", function () {
+ spyOn($, "postWithPrefix");
+ this.problem.show();
+ expect($.postWithPrefix).toHaveBeenCalledWith("/problem/Problem1/problem_show", jasmine.any(Function));
+ });
+
+ it("show the answers", function () {
+ spyOn($, "postWithPrefix").and.callFake((url, callback) =>
+ callback({ answers: { "1_1": "One", "1_2": "Two" } }),
+ );
+ this.problem.show();
+ expect($("#answer_1_1")).toHaveHtml("One");
+ expect($("#answer_1_2")).toHaveHtml("Two");
+ });
+
+ it("disables the show answer button", function () {
+ spyOn($, "postWithPrefix").and.callFake((url, callback) => callback({ answers: {} }));
+ this.problem.show();
+ expect(this.problem.el.find(".show").attr("disabled")).toEqual("disabled");
+ });
+
+ describe("radio text question", function () {
+ const radio_text_xml = `\
+\
+`;
+ beforeEach(function () {
+ // Append a radiotextresponse problem to the problem, so we can check it's javascript functionality
+ this.problem.el.prepend(radio_text_xml);
+ });
+
+ it("sets the correct class on the section for the correct choice", function () {
+ spyOn($, "postWithPrefix").and.callFake((url, callback) =>
+ callback({ answers: { "1_2_1": ["1_2_1_choiceinput_0bc"], "1_2_1_choiceinput_0bc": "3" } }),
+ );
+ this.problem.show();
+
+ expect($("#forinput1_2_1_choiceinput_0bc").attr("class")).toEqual("choicetextgroup_show_correct");
+ expect($("#answer_1_2_1_choiceinput_0bc").text()).toEqual("3");
+ expect($("#answer_1_2_1_choiceinput_1bc").text()).toEqual("");
+ expect($("#answer_1_2_1_choiceinput_2bc").text()).toEqual("");
+ });
+
+ it("Should not disable input fields", function () {
+ spyOn($, "postWithPrefix").and.callFake((url, callback) =>
+ callback({ answers: { "1_2_1": ["1_2_1_choiceinput_0bc"], "1_2_1_choiceinput_0bc": "3" } }),
+ );
+ this.problem.show();
+ expect($("input#1_2_1_choiceinput_0bc").attr("disabled")).not.toEqual("disabled");
+ expect($("input#1_2_1_choiceinput_1bc").attr("disabled")).not.toEqual("disabled");
+ expect($("input#1_2_1_choiceinput_2bc").attr("disabled")).not.toEqual("disabled");
+ expect($("input#1_2_1").attr("disabled")).not.toEqual("disabled");
+ });
+ });
+
+ describe("imageinput", function () {
+ let el, height, width;
+ const imageinput_html = readFixtures("imageinput.underscore");
+
+ const DEFAULTS = {
+ id: "12345",
+ width: "300",
+ height: "400",
+ };
+
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ this.problem.el.prepend(_.template(imageinput_html)(DEFAULTS));
+ });
+
+ const assertAnswer = (problem, data) => {
+ stubRequest(data);
+ problem.show();
+
+ $.each(data["answers"], (id, answer) => {
+ const img = getImage(answer);
+ el = $(`#inputtype_${id}`);
+ expect(img).toImageDiffEqual(el.find("canvas")[0]);
+ });
+ };
+
+ var stubRequest = (data) => {
+ spyOn($, "postWithPrefix").and.callFake((url, callback) => callback(data));
+ };
+
+ var getImage = (coords, c_width, c_height) => {
+ let ctx, reg;
+ const types = {
+ rectangle: (coords) => {
+ reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/;
+ const rects = coords.replace(/\s*/g, "").split(/;/);
+
+ $.each(rects, (index, rect) => {
+ const { abs } = Math;
+ const points = reg.exec(rect);
+ if (points) {
+ width = abs(points[3] - points[1]);
+ height = abs(points[4] - points[2]);
+
+ return ctx.rect(points[1], points[2], width, height);
+ }
+ });
+
+ ctx.stroke();
+ ctx.fill();
+ },
+
+ regions: (coords) => {
+ const parseCoords = (coords) => {
+ reg = JSON.parse(coords);
+
+ if (typeof reg[0][0][0] === "undefined") {
+ reg = [reg];
+ }
+
+ return reg;
+ };
+
+ return $.each(parseCoords(coords), (index, region) => {
+ ctx.beginPath();
+ $.each(region, (index, point) => {
+ if (index === 0) {
+ return ctx.moveTo(point[0], point[1]);
+ } else {
+ return ctx.lineTo(point[0], point[1]);
+ }
+ });
+
+ ctx.closePath();
+ ctx.stroke();
+ ctx.fill();
+ });
+ },
+ };
+
+ const canvas = document.createElement("canvas");
+ canvas.width = c_width || 100;
+ canvas.height = c_height || 100;
+
+ if (canvas.getContext) {
+ ctx = canvas.getContext("2d");
+ } else {
+ console.log("Canvas is not supported.");
+ }
+
+ ctx.fillStyle = "rgba(255,255,255,.3)";
+ ctx.strokeStyle = "#FF0000";
+ ctx.lineWidth = "2";
+
+ $.each(coords, (key, value) => {
+ if (types[key] != null && value) {
+ return types[key](value);
+ }
+ });
+
+ return canvas;
+ };
+
+ it("rectangle is drawn correctly", function () {
+ assertAnswer(this.problem, {
+ answers: {
+ 12345: {
+ rectangle: "(10,10)-(30,30)",
+ regions: null,
+ },
+ },
+ });
+ });
+
+ it("region is drawn correctly", function () {
+ assertAnswer(this.problem, {
+ answers: {
+ 12345: {
+ rectangle: null,
+ regions: "[[10,10],[30,30],[70,30],[20,30]]",
+ },
+ },
+ });
+ });
+
+ it("mixed shapes are drawn correctly", function () {
+ assertAnswer(this.problem, {
+ answers: {
+ 12345: {
+ rectangle: "(10,10)-(30,30);(5,5)-(20,20)",
+ regions: `[
+ [[50,50],[40,40],[70,30],[50,70]],
+ [[90,95],[95,95],[90,70],[70,70]]
+]`,
+ },
+ },
+ });
+ });
+
+ it("multiple image inputs draw answers on separate canvases", function () {
+ const data = {
+ id: "67890",
+ width: "400",
+ height: "300",
+ };
+
+ this.problem.el.prepend(_.template(imageinput_html)(data));
+ assertAnswer(this.problem, {
+ answers: {
+ 12345: {
+ rectangle: null,
+ regions: "[[10,10],[30,30],[70,30],[20,30]]",
+ },
+ 67890: {
+ rectangle: "(10,10)-(30,30)",
+ regions: null,
+ },
+ },
+ });
+ });
+
+ it("dictionary with answers doesn't contain answer for current id", function () {
+ spyOn(console, "log");
+ stubRequest({ answers: {} });
+ this.problem.show();
+ el = $("#inputtype_12345");
+ expect(el.find("canvas")).not.toExist();
+ expect(console.log).toHaveBeenCalledWith("Answer is absent for image input with id=12345");
+ });
+ });
+ });
+ });
+
+ describe("save", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ this.problem.answers = "foo=1&bar=2";
+ });
+
+ it("log the problem_save event", function () {
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.save();
+ expect(Logger.log).toHaveBeenCalledWith("problem_save", "foo=1&bar=2");
+ });
+
+ it("POST to save problem", function () {
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ let promise;
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ this.problem.save();
+ expect($.postWithPrefix).toHaveBeenCalledWith(
+ "/problem/Problem1/problem_save",
+ "foo=1&bar=2",
+ jasmine.any(Function),
+ );
+ });
+
+ it("tests that save does not enable the submit button or change the text when submit is originally disabled", function () {
+ const self = this;
+ const curr_html = this.problem.el.html();
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ // enableButtons should have been called at this point and the submit button should be unaffected
+ let promise;
+ expect(self.problem.submitButton).toHaveAttr("disabled");
+ expect(self.problem.submitButtonLabel.text()).toBe("Submit");
+ callback({ success: "correct", html: curr_html });
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ // Expect submit to be disabled and labeled properly at the start
+ expect(this.problem.submitButton).toHaveAttr("disabled");
+ expect(this.problem.submitButtonLabel.text()).toBe("Submit");
+ this.problem.save();
+ // Submit button should have the same state after save has completed
+ expect(this.problem.submitButton).toHaveAttr("disabled");
+ expect(this.problem.submitButtonLabel.text()).toBe("Submit");
+ });
+
+ it("tests that save does not disable the submit button or change the text when submit is originally enabled", function () {
+ const self = this;
+ const curr_html = this.problem.el.html();
+ spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
+ // enableButtons should have been called at this point, and the submit button should be disabled while submitting
+ let promise;
+ expect(self.problem.submitButton).toHaveAttr("disabled");
+ expect(self.problem.submitButtonLabel.text()).toBe("Submit");
+ callback({ success: "correct", html: curr_html });
+ promise = {
+ always(callable) {
+ return callable();
+ },
+ };
+ return promise;
+ });
+ // Expect submit to be enabled and labeled properly at the start after adding an input
+ $("#input_example_1").val("test").trigger("input");
+ expect(this.problem.submitButton).not.toHaveAttr("disabled");
+ expect(this.problem.submitButtonLabel.text()).toBe("Submit");
+ this.problem.save();
+ // Submit button should have the same state after save has completed
+ expect(this.problem.submitButton).not.toHaveAttr("disabled");
+ expect(this.problem.submitButtonLabel.text()).toBe("Submit");
+ });
+ });
+
+ describe("refreshMath", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ $("#input_example_1").val("E=mc^2");
+ this.problem.refreshMath({ target: $("#input_example_1").get(0) });
+ });
+
+ it("should queue the conversion and MathML element update", function () {
+ expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
+ ["Text", this.stubbedJax, "E=mc^2"],
+ [this.problem.updateMathML, this.stubbedJax, $("#input_example_1").get(0)],
+ );
+ });
+ });
+
+ describe("updateMathML", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ this.stubbedJax.root.toMathML.and.returnValue("");
+ });
+
+ describe("when there is no exception", function () {
+ beforeEach(function () {
+ this.problem.updateMathML(this.stubbedJax, $("#input_example_1").get(0));
+ });
+
+ it("convert jax to MathML", () => expect($("#input_example_1_dynamath")).toHaveValue(""));
+ });
+
+ describe("when there is an exception", function () {
+ beforeEach(function () {
+ const error = new Error();
+ error.restart = true;
+ this.stubbedJax.root.toMathML.and.throwError(error);
+ this.problem.updateMathML(this.stubbedJax, $("#input_example_1").get(0));
+ });
+
+ it("should queue up the exception", function () {
+ expect(MathJax.Callback.After).toHaveBeenCalledWith([this.problem.refreshMath, this.stubbedJax], true);
+ });
+ });
+ });
+
+ describe("refreshAnswers", function () {
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ this.problem.el.html(`\
+
+
+
+ \
+`);
+ this.stubSchematic = { update_value: jasmine.createSpy("schematic") };
+ this.stubCodeMirror = { save: jasmine.createSpy("CodeMirror") };
+ $("input.schematic").get(0).schematic = this.stubSchematic;
+ $("textarea.CodeMirror").get(0).CodeMirror = this.stubCodeMirror;
+ });
+
+ it("update each schematic", function () {
+ this.problem.refreshAnswers();
+ expect(this.stubSchematic.update_value).toHaveBeenCalled();
+ });
+
+ it("update each code block", function () {
+ this.problem.refreshAnswers();
+ expect(this.stubCodeMirror.save).toHaveBeenCalled();
+ });
+ });
+
+ describe("multiple JsInput in single problem", function () {
+ const jsinput_html = readFixtures("jsinput_problem.html");
+
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+ this.problem.render(jsinput_html);
+ });
+
+ it("submit_save_waitfor should return false", function () {
+ $(this.problem.inputs[0]).data("waitfor", function () {});
+ expect(this.problem.submit_save_waitfor()).toEqual(false);
+ });
+ });
+
+ describe("Submitting an xqueue-graded problem", function () {
+ const matlabinput_html = readFixtures("matlabinput_problem.html");
+
+ beforeEach(function () {
+ spyOn($, "postWithPrefix").and.callFake((url, callback) => callback({ html: matlabinput_html }));
+ jasmine.clock().install();
+ this.problem = new Problem($(".xblock-student_view"));
+ spyOn(this.problem, "poll").and.callThrough();
+ this.problem.render(matlabinput_html);
+ });
+
+ afterEach(() => jasmine.clock().uninstall());
+
+ it("check that we stop polling after a fixed amount of time", function () {
+ expect(this.problem.poll).not.toHaveBeenCalled();
+ jasmine.clock().tick(1);
+ const time_steps = [1000, 2000, 4000, 8000, 16000, 32000];
+ let num_calls = 1;
+ for (let time_step of Array.from(time_steps)) {
+ ((time_step) => {
+ jasmine.clock().tick(time_step);
+ expect(this.problem.poll.calls.count()).toEqual(num_calls);
+ num_calls += 1;
+ })(time_step);
+ }
+
+ // jump the next step and verify that we are not still continuing to poll
+ jasmine.clock().tick(64000);
+ expect(this.problem.poll.calls.count()).toEqual(6);
+
+ expect($(".notification-gentle-alert .notification-message").text()).toEqual(
+ "The grading process is still running. Refresh the page to see updates.",
+ );
+ });
+ });
+
+ describe("codeinput problem", function () {
+ const codeinputProblemHtml = readFixtures("codeinput_problem.html");
+
+ beforeEach(function () {
+ spyOn($, "postWithPrefix").and.callFake((url, callback) => callback({ html: codeinputProblemHtml }));
+ this.problem = new Problem($(".xblock-student_view"));
+ this.problem.render(codeinputProblemHtml);
+ });
+
+ it("has rendered with correct a11y info", function () {
+ const CodeMirrorTextArea = $("textarea")[1];
+ const CodeMirrorTextAreaId = "cm-textarea-101";
+
+ // verify that question label has correct `for` attribute value
+ expect($(".problem-group-label").attr("for")).toEqual(CodeMirrorTextAreaId);
+
+ // verify that codemirror textarea has correct `id` attribute value
+ expect($(CodeMirrorTextArea).attr("id")).toEqual(CodeMirrorTextAreaId);
+
+ // verify that codemirror textarea has correct `aria-describedby` attribute value
+ expect($(CodeMirrorTextArea).attr("aria-describedby")).toEqual("cm-editor-exit-message-101 status_101");
+ });
+ });
+
+ describe("show answer button", function () {
+ const radioButtonProblemHtml = readFixtures("radiobutton_problem.html");
+ const checkboxProblemHtml = readFixtures("checkbox_problem.html");
+
+ beforeEach(function () {
+ this.problem = new Problem($(".xblock-student_view"));
+
+ this.checkAssertionsAfterClickingAnotherOption = () => {
+ // verify that 'show answer button is no longer disabled'
+ expect(this.problem.el.find(".show").attr("disabled")).not.toEqual("disabled");
+
+ // verify that displayed answer disappears
+ expect(this.problem.el.find("div.choicegroup")).not.toHaveClass("choicegroup_correct");
+
+ // verify that radio/checkbox label has no span having class '.status.correct'
+ expect(this.problem.el.find("div.choicegroup")).not.toHaveAttr("span.status.correct");
+ };
+ });
+
+ it("should become enabled after a radiobutton is selected", function () {
+ $("#input_example_1").replaceWith(radioButtonProblemHtml);
+ // assume that 'ShowAnswer' button is clicked,
+ // clicking make it disabled.
+ this.problem.el.find(".show").attr("disabled", "disabled");
+ // bind click event to input fields
+ this.problem.submitAnswersAndSubmitButton(true);
+ // selects option 2
+ $("#input_1_1_2").attr("checked", true).trigger("click");
+ this.checkAssertionsAfterClickingAnotherOption();
+ });
+
+ it("should become enabled after a checkbox is selected", function () {
+ $("#input_example_1").replaceWith(checkboxProblemHtml);
+ this.problem.el.find(".show").attr("disabled", "disabled");
+ this.problem.submitAnswersAndSubmitButton(true);
+ $("#input_1_1_2").click();
+ this.checkAssertionsAfterClickingAnotherOption();
+ });
+ });
+});
diff --git a/xblocks_contrib/problem/assets/spec/imageinput_spec.js b/xblocks_contrib/problem/assets/spec/imageinput_spec.js
new file mode 100644
index 00000000..f6c2f9f9
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/imageinput_spec.js
@@ -0,0 +1,132 @@
+/**
+ * "Beware of bugs in the above code; I have only proved it correct, not tried
+ * it."
+ *
+ * ~ Donald Knuth
+ */
+
+// eslint-disable-next-line no-shadow-restricted-names
+(function ($, ImageInput, undefined) {
+ describe("ImageInput", function () {
+ var state;
+
+ beforeEach(function () {
+ var $el;
+
+ loadFixtures("imageinput.html");
+ $el = $("#imageinput_12345");
+
+ $el.append(createTestImage("cross_12345", 300, 400, "red"));
+
+ state = new ImageInput("12345");
+ });
+
+ it("initialization", function () {
+ // Check that object's properties are present, and that the DOM
+ // elements they reference exist.
+ expect(state.el).toBeDefined();
+ expect(state.el).toExist();
+
+ expect(state.crossEl).toBeDefined();
+ expect(state.crossEl).toExist();
+
+ expect(state.inputEl).toBeDefined();
+ expect(state.inputEl).toExist();
+
+ expect(state.el).toHandle("click");
+ });
+
+ it("cross becomes visible after first click", function () {
+ expect(state.crossEl.css("visibility")).toBe("hidden");
+
+ state.el.click();
+
+ expect(state.crossEl.css("visibility")).toBe("visible");
+ });
+
+ it("coordinates are updated [offsetX is set]", function () {
+ var event, posX, posY, cssLeft, cssTop;
+
+ // Set up of 'click' event.
+ event = jQuery.Event("click", { offsetX: 35.3, offsetY: 42.7 });
+
+ // Calculating the expected coordinates.
+ posX = event.offsetX;
+ posY = event.offsetY;
+
+ // Triggering 'click' event.
+ jQuery(state.el).trigger(event);
+
+ // Getting actual (new) coordinates, and testing them against the
+ // expected.
+ cssLeft = stripPx(state.crossEl.css("left"));
+ cssTop = stripPx(state.crossEl.css("top"));
+
+ expect(cssLeft).toBeCloseTo(posX - 15, 1);
+ expect(cssTop).toBeCloseTo(posY - 15, 1);
+ expect(state.inputEl.val()).toBe("[" + Math.round(posX) + "," + Math.round(posY) + "]");
+ });
+
+ it("coordinates are updated [offsetX is NOT set]", function () {
+ var offset = state.el.offset(),
+ event,
+ posX,
+ posY,
+ cssLeft,
+ cssTop;
+
+ // Set up of 'click' event.
+ event = jQuery.Event("click", {
+ offsetX: undefined,
+ offsetY: undefined,
+ pageX: 35.3,
+ pageY: 42.7,
+ });
+
+ // Calculating the expected coordinates.
+ posX = event.pageX - offset.left;
+ posY = event.pageY - offset.top;
+
+ // Triggering 'click' event.
+ jQuery(state.el).trigger(event);
+
+ // Getting actual (new) coordinates, and testing them against the
+ // expected.
+ cssLeft = stripPx(state.crossEl.css("left"));
+ cssTop = stripPx(state.crossEl.css("top"));
+
+ expect(cssLeft).toBeCloseTo(posX - 15, 1);
+ expect(cssTop).toBeCloseTo(posY - 15, 1);
+ expect(state.inputEl.val()).toBe("[" + Math.round(posX) + "," + Math.round(posY) + "]");
+ });
+ });
+
+ // Instead of storing an image, and then including it in the template via
+ // the tag, we will generate one on the fly.
+ //
+ // Create a simple image from a canvas. The canvas is filled by a colored
+ // rectangle.
+ function createTestImage(id, width, height, fillStyle) {
+ var canvas, ctx, img;
+
+ canvas = document.createElement("canvas");
+ canvas.width = width;
+ canvas.height = height;
+
+ ctx = canvas.getContext("2d");
+ ctx.fillStyle = fillStyle;
+ ctx.fillRect(0, 0, width, height);
+
+ img = document.createElement("img");
+ img.src = canvas.toDataURL("image/png");
+ img.id = id;
+
+ return img;
+ }
+
+ // Strip the trailing 'px' substring from a CSS string containing the
+ // `left` and `top` properties of an element's style.
+ function stripPx(str) {
+ return str.substring(0, str.length - 2);
+ }
+}).call(this, window.jQuery, window.ImageInput);
diff --git a/xblocks_contrib/problem/assets/spec/spec_helpers/accessibility_tools.js b/xblocks_contrib/problem/assets/spec/spec_helpers/accessibility_tools.js
new file mode 100644
index 00000000..8e3ad53c
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/spec_helpers/accessibility_tools.js
@@ -0,0 +1,256 @@
+/*
+
+============================================
+License for Application
+============================================
+
+This license is governed by United States copyright law, and with respect to matters
+of tort, contract, and other causes of action it is governed by North Carolina law,
+without regard to North Carolina choice of law provisions. The forum for any dispute
+resolution shall be in Wake County, North Carolina.
+
+Redistribution and use in source and binary forms, with or without modification, are
+permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list
+ of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or other
+ materials provided with the distribution.
+
+3. The name of the author may not be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*/
+
+var $focusedElementBeforeModal,
+ focusableElementsString = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]';
+
+var reassignTabIndexesAndAriaHidden = function(focusableElementsFilterString, closeButtonId, modalId, mainPageId) {
+ // Sets appropriate elements to tab indexable and properly sets aria_hidden on content outside of modal
+ // "focusableElementsFilterString" is a string that indicates all elements that should be focusable
+ // "closeButtonId" is the selector for the button that closes out the modal.
+ // "modalId" is the selector for the modal being managed
+ // "mainPageId" is the selector for the main part of the page
+ // Returns a list of focusableItems
+ var focusableItems;
+
+ $(mainPageId).attr('aria-hidden', 'true');
+ $(modalId).attr('aria-hidden', 'false');
+
+ focusableItems = $(modalId).find('*')
+ .filter(focusableElementsFilterString)
+ .filter(':visible');
+
+ focusableItems.attr('tabindex', '2');
+ $(closeButtonId).attr('tabindex', '1').focus();
+
+ return focusableItems;
+};
+
+var trapTabFocus = function(focusableItems, closeButtonId) {
+ // Determines last element in modal and traps focus by causing tab
+ // to focus on the first modal element (close button)
+ // "focusableItems" all elements in the modal that are focusable
+ // "closeButtonId" is the selector for the button that closes out the modal.
+ // returns the last focusable element in the modal.
+ var $last;
+ if (focusableItems.length !== 0) {
+ $last = focusableItems.last();
+ } else {
+ $last = $(closeButtonId);
+ }
+
+ // tab on last element in modal returns to the first one
+ $last.on('keydown', function(e) {
+ var keyCode = e.keyCode || e.which;
+ // 9 is the js keycode for tab
+ if (!e.shiftKey && keyCode === 9) {
+ e.preventDefault();
+ $(closeButtonId).focus();
+ }
+ });
+
+ return $last;
+};
+
+var trapShiftTabFocus = function($last, closeButtonId) {
+ $(closeButtonId).on('keydown', function(e) {
+ var keyCode = e.keyCode || e.which;
+ // 9 is the js keycode for tab
+ if (e.shiftKey && keyCode === 9) {
+ e.preventDefault();
+ $last.focus();
+ }
+ });
+};
+
+var bindReturnFocusListener = function($previouslyFocusedElement, closeButtonId, modalId, mainPageId) {
+ // Ensures that on modal close, focus is returned to the element
+ // that had focus before the modal was opened.
+ $('#lean_overlay, ' + closeButtonId).click(function() {
+ $(mainPageId).attr('aria-hidden', 'false');
+ $(modalId).attr('aria-hidden', 'true');
+ $previouslyFocusedElement.focus();
+ });
+};
+
+var bindEscapeKeyListener = function(modalId, closeButtonId) {
+ $(modalId).on('keydown', function(e) {
+ var keyCode = e.keyCode || e.which;
+ // 27 is the javascript keycode for the ESC key
+ if (keyCode === 27) {
+ e.preventDefault();
+ $(closeButtonId).click();
+ }
+ });
+};
+
+var trapFocusForAccessibleModal = function(
+ $previouslyFocusedElement,
+ focusableElementsFilterString,
+ closeButtonId,
+ modalId,
+ mainPageId) {
+ // Re assess the page for which items internal to the modal should be focusable,
+ // Should be called after the content of the accessible_modal is changed in order
+ // to ensure that the correct elements are accessible.
+ var focusableItems, $last;
+ focusableItems = reassignTabIndexesAndAriaHidden(
+ focusableElementsFilterString,
+ closeButtonId,
+ modalId,
+ mainPageId
+ );
+ $last = trapTabFocus(focusableItems, closeButtonId);
+ trapShiftTabFocus($last, closeButtonId);
+ bindReturnFocusListener($previouslyFocusedElement, closeButtonId, modalId, mainPageId);
+ bindEscapeKeyListener(modalId, closeButtonId);
+};
+
+var accessible_modal = function(trigger, closeButtonId, modalId, mainPageId) {
+ // Modifies a lean modal to optimize focus management.
+ // "trigger" is the selector for the link element that triggers the modal.
+ // "closeButtonId" is the selector for the button that closes out the modal.
+ // "modalId" is the selector for the modal being managed
+ // "mainPageId" is the selector for the main part of the page
+ //
+ // based on http://accessibility.oit.ncsu.edu/training/aria/modal-window/modal-window.js
+ //
+ // see http://accessibility.oit.ncsu.edu/blog/2013/09/13/the-incredible-accessible-modal-dialog/
+ // for more information on managing modals
+ //
+ var initialFocus;
+ $(trigger).click(function() {
+ $focusedElementBeforeModal = $(trigger);
+
+ trapFocusForAccessibleModal(
+ $focusedElementBeforeModal,
+ focusableElementsString,
+ closeButtonId,
+ modalId,
+ mainPageId
+ );
+
+ // In IE, focus shifts to iframes when they load.
+ // These lines ensure that focus is shifted back to the close button
+ // in the case that a modal that contains an iframe is opened in IE.
+ // see http://stackoverflow.com/questions/15792620/
+ initialFocus = true;
+ $(modalId).find('iframe').on('focus', function() {
+ if (initialFocus) {
+ $(closeButtonId).focus();
+ initialFocus = false;
+ }
+ });
+ });
+};
+
+// NOTE: This is a gross hack to make the skip links work for Webkit browsers
+// see http://stackoverflow.com/questions/6280399/skip-links-not-working-in-chrome/12720183#12720183
+
+// handle things properly for clicks
+$('.nav-skip').click(function() {
+ var href = $(this).attr('href');
+ if (href) {
+ $(href).attr('tabIndex', -1).focus();
+ }
+});
+// and for the enter key
+$('.nav-skip').keypress(function(e) {
+ var href;
+ if (e.which === 13) {
+ href = $(this).attr('href');
+ if (href) {
+ $(href).attr('tabIndex', -1).focus();
+ }
+ }
+});
+
+// Creates a window level SR object that can be used for giving audible feedback to screen readers.
+$(function() {
+ var SRAlert;
+
+ SRAlert = (function() {
+ // eslint-disable-next-line no-shadow
+ function SRAlert() {
+ // This initialization sometimes gets done twice, so take to only create a single reader-feedback div.
+ var readerFeedbackID = 'reader-feedback',
+ $readerFeedbackSelector = $('#' + readerFeedbackID);
+
+ if ($readerFeedbackSelector.length === 0) {
+ edx.HtmlUtils.append(
+ $('body'),
+ edx.HtmlUtils.interpolateHtml(
+ edx.HtmlUtils.HTML('
'),
+ {readerFeedbackID: readerFeedbackID}
+ )
+ );
+ }
+ this.el = $('#' + readerFeedbackID);
+ }
+
+ SRAlert.prototype.clear = function() {
+ edx.HtmlUtils.setHtml(this.el, '');
+ };
+
+ SRAlert.prototype.readElts = function(elts) {
+ var texts = [];
+ $.each(elts, function(idx, value) {
+ texts.push($(value).html());
+ });
+ return this.readTexts(texts);
+ };
+
+ SRAlert.prototype.readText = function(text) {
+ return this.readTexts([text]);
+ };
+
+ SRAlert.prototype.readTexts = function(texts) {
+ var htmlFeedback = edx.HtmlUtils.HTML('');
+ $.each(texts, function(idx, value) {
+ htmlFeedback = edx.HtmlUtils.interpolateHtml(
+ edx.HtmlUtils.HTML('{previous_feedback}{value}
\n'),
+ // "value" may be HTML, if an element is being passed
+ {previous_feedback: htmlFeedback, value: edx.HtmlUtils.HTML(value)}
+ );
+ });
+ edx.HtmlUtils.setHtml(this.el, htmlFeedback);
+ };
+
+ return SRAlert;
+ }());
+
+ window.SR = new SRAlert();
+});
diff --git a/xblocks_contrib/problem/assets/spec/spec_helpers/add_ajax_prefix.js b/xblocks_contrib/problem/assets/spec/spec_helpers/add_ajax_prefix.js
new file mode 100644
index 00000000..33bfbed2
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/spec_helpers/add_ajax_prefix.js
@@ -0,0 +1,5 @@
+// Tests require that addAjaxPrefix is called
+// before the tests are run.
+AjaxPrefix.addAjaxPrefix($, function() {
+ return '';
+});
diff --git a/xblocks_contrib/problem/assets/spec/spec_helpers/ajax_prefix.js b/xblocks_contrib/problem/assets/spec/spec_helpers/ajax_prefix.js
new file mode 100644
index 00000000..d5b1324d
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/spec_helpers/ajax_prefix.js
@@ -0,0 +1,24 @@
+// Once generated by CoffeeScript 1.9.3, but now lives as pure JS
+/* eslint-disable */
+(function() {
+ this.AjaxPrefix = {
+ addAjaxPrefix: function(jQuery, prefix) {
+ jQuery.postWithPrefix = function(url, data, callback, type) {
+ return $.post("" + (prefix()) + url, data, callback, type);
+ };
+ jQuery.getWithPrefix = function(url, data, callback, type) {
+ return $.get("" + (prefix()) + url, data, callback, type);
+ };
+ return jQuery.ajaxWithPrefix = function(url, settings) {
+ if (settings != null) {
+ return $.ajax("" + (prefix()) + url, settings);
+ } else {
+ settings = url;
+ settings.url = "" + (prefix()) + settings.url;
+ return $.ajax(settings);
+ }
+ };
+ }
+ };
+
+}).call(this);
diff --git a/xblocks_contrib/problem/assets/spec/spec_helpers/helper.js b/xblocks_contrib/problem/assets/spec/spec_helpers/helper.js
new file mode 100644
index 00000000..9c3a29a7
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/spec_helpers/helper.js
@@ -0,0 +1,325 @@
+/* global _ */
+
+(function () {
+ "use strict";
+
+ var origAjax = $.ajax;
+
+ var stubbedYT = {
+ Player: function () {
+ var Player = jasmine.createSpyObj("YT.Player", [
+ "cueVideoById",
+ "getVideoEmbedCode",
+ "getCurrentTime",
+ "getPlayerState",
+ "getVolume",
+ "setVolume",
+ "loadVideoById",
+ "getAvailablePlaybackRates",
+ "playVideo",
+ "pauseVideo",
+ "seekTo",
+ "getDuration",
+ "setPlaybackRate",
+ "getAvailableQualityLevels",
+ "getPlaybackQuality",
+ "setPlaybackQuality",
+ "destroy",
+ ]);
+
+ Player.getDuration.and.returnValue(60);
+ Player.getAvailablePlaybackRates.and.returnValue([0.5, 1.0, 1.5, 2.0]);
+ Player.getAvailableQualityLevels.and.returnValue(["highres", "hd1080", "hd720", "large", "medium", "small"]);
+
+ return Player;
+ },
+
+ PlayerState: {
+ UNSTARTED: -1,
+ ENDED: 0,
+ PLAYING: 1,
+ PAUSED: 2,
+ BUFFERING: 3,
+ CUED: 5,
+ },
+ ready: function (f) {
+ return f();
+ },
+ };
+ jasmine.YT = stubbedYT;
+ // Stub YouTube API.
+ window.YT = stubbedYT;
+
+ window.STATUS = window.YT.PlayerState;
+
+ window.onTouchBasedDevice = function () {
+ return navigator.userAgent.match(/iPhone|iPod|iPad/i);
+ };
+
+ jasmine.stubbedCaption = {
+ end: [3120, 6270, 8490, 21620, 24920, 25750, 27900, 34380, 35550, 40250],
+ start: [1180, 3120, 6270, 14910, 21620, 24920, 25750, 27900, 34380, 35550],
+ text: [
+ "MICHAEL CIMA: So let's do the first one here.",
+ "Vacancies, where do they come from?",
+ "Well, imagine a perfect crystal.",
+ "Now we know at any temperature other than absolute zero " + "there's enough",
+ "energy going around that some atoms will have more energy",
+ "than others, right?",
+ "There's a distribution.",
+ "If I plot energy here and number, these atoms in the crystal " + "will have a",
+ "distribution of energy.",
+ "And some will have quite a bit of energy, just for a moment.",
+ ],
+ };
+
+ // Time waitsFor() should wait for before failing a test.
+ window.WAIT_TIMEOUT = 5000;
+
+ jasmine.getFixtures().fixturesPath += "fixtures";
+
+ jasmine.stubbedMetadata = {
+ "7tqY6eQzVhE": {
+ contentDetails: {
+ id: "7tqY6eQzVhE",
+ duration: "PT5M0S",
+ },
+ },
+ cogebirgzzM: {
+ contentDetails: {
+ id: "cogebirgzzM",
+ duration: "PT3M20S",
+ },
+ },
+ abcdefghijkl: {
+ contentDetails: {
+ id: "abcdefghijkl",
+ duration: "PT6M40S",
+ },
+ },
+ bogus: {
+ contentDetails: {
+ duration: "PT1M40S",
+ },
+ },
+ };
+
+ jasmine.fireEvent = function (el, eventName) {
+ var event;
+
+ if (document.createEvent) {
+ event = document.createEvent("HTMLEvents");
+ event.initEvent(eventName, true, true);
+ } else {
+ event = document.createEventObject();
+ event.eventType = eventName;
+ }
+
+ event.eventName = eventName;
+
+ if (document.createEvent) {
+ el.dispatchEvent(event);
+ } else {
+ el.fireEvent("on" + event.eventType, event);
+ }
+ };
+
+ jasmine.stubbedHtml5Speeds = ["0.75", "1.0", "1.25", "1.50", "2.0"];
+
+ jasmine.stubRequests = function () {
+ var spy = $.ajax;
+ if (!jasmine.isSpy($.ajax)) {
+ spy = spyOn($, "ajax");
+ }
+
+ return spy.and.callFake(function (settings) {
+ var match = settings.url.match(/googleapis\.com\/.+\/videos\/\?id=(.+)&part=contentDetails/),
+ status,
+ callCallback;
+ if (match) {
+ status = match[1].split("_");
+ if (status && status[0] === "status") {
+ callCallback = function (callback) {
+ callback.call(window, {}, status[1]);
+ };
+
+ return {
+ always: callCallback,
+ error: callCallback,
+ done: callCallback,
+ };
+ } else if (settings.success) {
+ return settings.success({
+ items: jasmine.stubbedMetadata[match[1]],
+ });
+ } else {
+ return {
+ always: function (callback) {
+ return callback.call(window, {}, "success");
+ },
+ done: function (callback) {
+ return callback.call(window, {}, "success");
+ },
+ };
+ }
+ } else if (settings.url.match(/transcript\/translation\/.+$/)) {
+ return settings.success(jasmine.stubbedCaption);
+ } else if (settings.url === "/transcript/available_translations") {
+ return settings.success(["uk", "de"]);
+ } else if (settings.url.match(/.+\/problem_get$/)) {
+ return settings.success({
+ html: window.readFixtures("problem_content.html"),
+ });
+ } else if (
+ settings.url === "/calculate" ||
+ settings.url.match(/.+\/goto_position$/) ||
+ settings.url.match(/event$/) ||
+ settings.url.match(/.+\/problem_(check|reset|show|save)$/)
+ ) {
+ // Do nothing.
+ return {};
+ } else if (settings.url === "/save_user_state") {
+ return { success: true };
+ } else if (settings.url.match(/.+video-transcript.+$/)) {
+ if (settings.url.match(/.+&video_id=notAIGenerated/)) {
+ return settings.success(null);
+ }
+ if (settings.url.match(/.+&video_id=inProgress/)) {
+ return settings.success({
+ status: "In Progress",
+ });
+ }
+ if (settings.url.match(/.+&video_id=error/)) {
+ return settings.error();
+ }
+ return settings.success({
+ status: "Completed",
+ });
+ } else if (settings.url.match(/.+transcript-feedback.+$/) && settings.type === "GET") {
+ if (settings.url.match(/.+&video_id=error.+$/)) {
+ return settings.error();
+ }
+ if (settings.url.match(/.+&video_id=negative.+$/)) {
+ return settings.success({
+ value: false,
+ });
+ }
+ if (settings.url.match(/.+&video_id=none.+$/)) {
+ return settings.success(null);
+ }
+ return settings.success({
+ value: true,
+ });
+ } else if (settings.url.match(/.+transcript-feedback.+$/) && settings.type === "POST") {
+ return settings.success(settings.data.value !== null ? { value: settings.data.value } : null);
+ } else if (settings.url.match(new RegExp(jasmine.getFixtures().fixturesPath + ".+", "g"))) {
+ return origAjax(settings);
+ } else {
+ return $.ajax.and.callThrough();
+ }
+ });
+ };
+
+ // Stub jQuery.cookie module.
+ $.cookie = jasmine.createSpy("jQuery.cookie").and.returnValue("1.0");
+
+ // # Stub jQuery.qtip module.
+ $.fn.qtip = jasmine.createSpy("jQuery.qtip");
+
+ // Stub jQuery.scrollTo module.
+ $.fn.scrollTo = jasmine.createSpy("jQuery.scrollTo");
+
+ jasmine.initializePlayer = function (fixture, params) {
+ var state, metadata;
+
+ if (_.isString(fixture)) {
+ // `fixture` is a name of a fixture file.
+ loadFixtures(fixture);
+ } else {
+ // `fixture` is not a string. The first parameter is an object?
+ if (_.isObject(fixture)) {
+ // The first parameter contains attributes for the main video
+ // DIV element.
+ params = fixture;
+ }
+
+ // "video_all.html" is the default HTML template for HTML5 video.
+ loadFixtures("video_all.html");
+ }
+
+ // If `params` is an object, assign its properties as data attributes
+ // to the main video DIV element.
+ if (_.isObject(params)) {
+ metadata = _.extend($("#video_id").data("metadata"), params);
+ $("#video_id").data("metadata", metadata);
+ }
+
+ jasmine.stubRequests();
+ let runtime = jasmine.createSpyObj("TestRuntime", ["handlerUrl"]);
+ state = new window.Video(runtime, "#example");
+
+ state.resizer = (function () {
+ var methods = ["align", "alignByWidthOnly", "alignByHeightOnly", "setParams", "setMode", "setElement"],
+ obj = {},
+ delta = {
+ add: jasmine.createSpy().and.returnValue(obj),
+ substract: jasmine.createSpy().and.returnValue(obj),
+ reset: jasmine.createSpy().and.returnValue(obj),
+ };
+
+ $.each(methods, function (index, method) {
+ obj[method] = jasmine.createSpy(method).and.returnValue(obj);
+ });
+
+ obj.delta = delta;
+
+ return obj;
+ })();
+
+ // We return the `state` object of the newly initialized Video.
+ return state;
+ };
+
+ jasmine.initializeHLSPlayer = function (params) {
+ return jasmine.initializePlayer("video_hls.html", params);
+ };
+
+ jasmine.initializePlayerYouTube = function (params) {
+ // "video.html" contains HTML template for a YouTube video.
+ return jasmine.initializePlayer("video.html", params);
+ };
+
+ jasmine.DescribeInfo = function (description, specDefinitions) {
+ this.description = description;
+ this.specDefinitions = specDefinitions;
+ };
+
+ // This HTML Fullscreen API mock should use promises or async functions
+ // as the spec defines. We do not use them here because we're locked
+ // in to a version of jasmine that doesn't fully support async functions
+ // or promises. This mock also assumes that if non-vendor prefixed methods
+ // and properties are missing, then we'll use mozilla prefixed names since
+ // automated tests happen in firefox.
+ jasmine.mockFullscreenAPI = function () {
+ var fullscreenElement;
+ var vendorChangeEvent = "fullscreenEnabled" in document ? "fullscreenchange" : "mozfullscreenchange";
+ var vendorRequestFullscreen =
+ "requestFullscreen" in window.HTMLElement.prototype ? "requestFullscreen" : "mozRequestFullScreen";
+ var vendorExitFullscreen = "exitFullscreen" in document ? "exitFullscreen" : "mozCancelFullScreen";
+ var vendorFullscreenElement = "fullscreenEnabled" in document ? "fullscreenElement" : "mozFullScreenElement";
+
+ spyOn(window.HTMLElement.prototype, vendorRequestFullscreen).and.callFake(function () {
+ fullscreenElement = this;
+ document.dispatchEvent(new Event(vendorChangeEvent));
+ });
+
+ spyOn(document, vendorExitFullscreen).and.callFake(function () {
+ fullscreenElement = null;
+ document.dispatchEvent(new Event(vendorChangeEvent));
+ });
+
+ spyOnProperty(document, vendorFullscreenElement).and.callFake(function () {
+ return fullscreenElement;
+ });
+ };
+}).call(this);
diff --git a/xblocks_contrib/problem/assets/spec/spec_helpers/i18n.js b/xblocks_contrib/problem/assets/spec/spec_helpers/i18n.js
new file mode 100644
index 00000000..267fd6d3
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/spec_helpers/i18n.js
@@ -0,0 +1,10 @@
+window.gettext = function(s) { return s; };
+window.ngettext = function(singular, plural, num) { return num == 1 ? singular : plural; };
+
+function interpolate(fmt, obj, named) {
+ if (named) {
+ return fmt.replace(/%\(\w+\)s/g, function(match) { return String(obj[match.slice(2, -2)]); });
+ } else {
+ return fmt.replace(/%s/g, function(match) { return String(obj.shift()); });
+ }
+}
diff --git a/xblocks_contrib/problem/assets/spec/spec_helpers/jasmine-extensions.js b/xblocks_contrib/problem/assets/spec/spec_helpers/jasmine-extensions.js
new file mode 100644
index 00000000..c5a766e0
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/spec_helpers/jasmine-extensions.js
@@ -0,0 +1,284 @@
+/* eslint-env node */
+
+// Extensions to Jasmine.
+//
+// This file adds the following:
+// 1. Custom matchers that may be helpful project-wise.
+// 2. Copies of some matchers from Jasmine-jQuery.
+// Because Jasmine-Jquery uses its own version of JQuery, events registered in the code
+// using the platform version of JQuery are not "noticed" by Jasmine-jQuery matchers.
+// Similarly equality matching does not work either. So after the platform version of
+// jQuery has been loaded, we set these matchers up again in this module.
+
+(function(root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // eslint-disable-next-line global-require
+ require(['jquery'], function($) {
+ factory(root, $);
+ });
+ } else {
+ factory(root, root.jQuery);
+ }
+}((function() {
+ return this;
+}()), function(window, $) {
+ 'use strict';
+
+ // Add custom Jasmine matchers.
+ beforeEach(function() {
+ if (window.imagediff) {
+ jasmine.addMatchers(window.imagediff.jasmine);
+ }
+
+ jasmine.addMatchers({
+ toHaveAttrs: function() {
+ return {
+ compare: function(actual, attrs) {
+ var result = {},
+ element = actual;
+
+ if ($.isEmptyObject(attrs)) {
+ return {
+ pass: false
+ };
+ }
+
+ result.pass = _.every(attrs, function(value, name) {
+ return element.attr(name) === value;
+ });
+
+ return result;
+ }
+ };
+ },
+ toBeInRange: function() {
+ return {
+ compare: function(actual, min, max) {
+ return {
+ pass: min <= actual && actual <= max
+ };
+ }
+ };
+ },
+ toBeInArray: function() {
+ return {
+ compare: function(actual, array) {
+ return {
+ pass: $.inArray(actual, array) > -1
+ };
+ }
+ };
+ },
+ toBeInstanceOf: function() {
+ // Assert the type of the value being tested matches the provided type
+ return {
+ compare: function(actual, expected) {
+ return {
+ pass: actual instanceof expected
+ };
+ }
+ };
+ },
+ toHaveIndex: function() {
+ return {
+ compare: function(actual, expected) {
+ return {
+ pass: $(actual).index() === expected
+ };
+ }
+ };
+ },
+ toXMLEqual: function() {
+ return {
+ compare: function(actual, expected) {
+ return {
+ pass: actual.replace(/\s+/g, '') === expected.replace(/\s+/g, '')
+ };
+ }
+ };
+ }
+ });
+ });
+
+ /* eslint-disable */
+ // All the code below is taken from:
+ // https://github.com/velesin/jasmine-jquery/blob/2.1.1/lib/jasmine-jquery.js
+ beforeEach(function() {
+ jasmine.addMatchers({
+ toHandle: function() {
+ return {
+ compare: function(actual, event) {
+ if (!actual || actual.length === 0) return {
+ pass: false
+ };
+ var events = $._data($(actual).get(0), "events");
+
+ if (!events || !event || typeof event !== "string") {
+ return {
+ pass: false
+ };
+ }
+
+ var namespaces = event.split("."),
+ eventType = namespaces.shift(),
+ sortedNamespaces = namespaces.slice(0).sort(),
+ namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)");
+
+ if (events[eventType] && namespaces.length) {
+ for (var i = 0; i < events[eventType].length; i++) {
+ var namespace = events[eventType][i].namespace;
+
+ if (namespaceRegExp.test(namespace))
+ return {
+ pass: true
+ };
+ }
+ } else {
+ return {
+ pass: (events[eventType] && events[eventType].length > 0)
+ };
+ }
+
+ return {
+ pass: false
+ };
+ }
+ };
+ },
+
+ toHandleWith: function() {
+ return {
+ compare: function(actual, eventName, eventHandler) {
+ if (!actual || actual.length === 0) return {
+ pass: false
+ };
+ var normalizedEventName = eventName.split('.')[0],
+ stack = $._data($(actual).get(0), "events")[normalizedEventName];
+
+ for (var i = 0; i < stack.length; i++) {
+ if (stack[i].handler == eventHandler) return {
+ pass: true
+ };
+ }
+
+ return {
+ pass: false
+ };
+ }
+ };
+ }
+ });
+
+ jasmine.addCustomEqualityTester(function(a, b) {
+ if (a && b) {
+ if (a instanceof $ || jasmine.isDomNode(a)) {
+ var $a = $(a);
+
+ if (b instanceof $)
+ return $a.length == b.length && a.is(b);
+
+ return $a.is(b);
+ }
+
+ if (b instanceof $ || jasmine.isDomNode(b)) {
+ var $b = $(b);
+
+ if (a instanceof $)
+ return a.length == $b.length && $b.is(a);
+
+ return $(b).is(a);
+ }
+ }
+ });
+
+ jasmine.addCustomEqualityTester(function(a, b) {
+ if (a instanceof $ && b instanceof $ && a.size() == b.size())
+ return a.is(b);
+ });
+
+ });
+
+ var data = {
+ spiedEvents: {},
+ handlers: []
+ };
+
+ jasmine.jQuery.events = {
+ spyOn: function(selector, eventName) {
+ var handler = function(e) {
+ var calls = (typeof data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] !== 'undefined') ? data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].calls : 0;
+ data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = {
+ args: jasmine.util.argsToArray(arguments),
+ calls: ++calls
+ };
+ };
+
+ $(selector).on(eventName, handler);
+ data.handlers.push(handler);
+
+ return {
+ selector: selector,
+ eventName: eventName,
+ handler: handler,
+ reset: function() {
+ delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)];
+ },
+ calls: {
+ count: function() {
+ return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] ?
+ data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].calls : 0;
+ },
+ any: function() {
+ return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] ?
+ !!data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].calls : false;
+ }
+ }
+ };
+ },
+
+ args: function(selector, eventName) {
+ var actualArgs = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].args;
+
+ if (!actualArgs) {
+ throw "There is no spy for " + eventName + " on " + selector.toString() + ". Make sure to create a spy using spyOnEvent.";
+ }
+
+ return actualArgs;
+ },
+
+ wasTriggered: function(selector, eventName) {
+ return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]);
+ },
+
+ wasTriggeredWith: function(selector, eventName, expectedArgs, util, customEqualityTesters) {
+ var actualArgs = jasmine.jQuery.events.args(selector, eventName).slice(1);
+
+ if (Object.prototype.toString.call(expectedArgs) !== '[object Array]')
+ actualArgs = actualArgs[0];
+
+ return util.equals(actualArgs, expectedArgs, customEqualityTesters);
+ },
+
+ wasPrevented: function(selector, eventName) {
+ var spiedEvent = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)],
+ args = (jasmine.util.isUndefined(spiedEvent)) ? {} : spiedEvent.args,
+ e = args ? args[0] : undefined;
+
+ return e && e.isDefaultPrevented();
+ },
+
+ wasStopped: function(selector, eventName) {
+ var spiedEvent = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)],
+ args = (jasmine.util.isUndefined(spiedEvent)) ? {} : spiedEvent.args,
+ e = args ? args[0] : undefined;
+
+ return e && e.isPropagationStopped();
+ },
+
+ cleanUp: function() {
+ data.spiedEvents = {};
+ data.handlers = [];
+ }
+ };
+ /* eslint-enable */
+}));
diff --git a/xblocks_contrib/problem/assets/spec/spec_helpers/jasmine-imagediff.js b/xblocks_contrib/problem/assets/spec/spec_helpers/jasmine-imagediff.js
new file mode 100644
index 00000000..022dec13
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/spec_helpers/jasmine-imagediff.js
@@ -0,0 +1,401 @@
+// js-imagediff 1.0.3
+// (c) 2011-2012 Carl Sutherland, Humble Software
+// Distributed under the MIT License
+// For original source and documentation visit:
+// http://www.github.com/HumbleSoftware/js-imagediff
+
+(function (name, definition) {
+ var root = this;
+ if (typeof module !== 'undefined') {
+ var Canvas = require('canvas');
+ module.exports = definition(root, name, Canvas);
+ } else if (typeof define === 'function' && typeof define.amd === 'object') {
+ define(definition);
+ } else {
+ root[name] = definition(root, name);
+ }
+})('imagediff', function (root, name, Canvas) {
+
+ var
+ TYPE_ARRAY = /\[object Array\]/i,
+ TYPE_CANVAS = /\[object (Canvas|HTMLCanvasElement)\]/i,
+ TYPE_CONTEXT = /\[object CanvasRenderingContext2D\]/i,
+ TYPE_IMAGE = /\[object (Image|HTMLImageElement)\]/i,
+ TYPE_IMAGE_DATA = /\[object ImageData\]/i,
+
+ UNDEFINED = 'undefined',
+
+ canvas = getCanvas(),
+ context = canvas.getContext('2d'),
+ previous = root[name],
+ imagediff, jasmine;
+
+ // Creation
+ function getCanvas (width, height) {
+ var
+ canvas = Canvas ?
+ new Canvas() :
+ document.createElement('canvas');
+ if (width) canvas.width = width;
+ if (height) canvas.height = height;
+ return canvas;
+ }
+ function getImageData (width, height) {
+ canvas.width = width;
+ canvas.height = height;
+ context.clearRect(0, 0, width, height);
+ return context.createImageData(width, height);
+ }
+
+
+ // Type Checking
+ function isImage (object) {
+ return isType(object, TYPE_IMAGE);
+ }
+ function isCanvas (object) {
+ return isType(object, TYPE_CANVAS);
+ }
+ function isContext (object) {
+ return isType(object, TYPE_CONTEXT);
+ }
+ function isImageData (object) {
+ return !!(object &&
+ isType(object, TYPE_IMAGE_DATA) &&
+ typeof(object.width) !== UNDEFINED &&
+ typeof(object.height) !== UNDEFINED &&
+ typeof(object.data) !== UNDEFINED);
+ }
+ function isImageType (object) {
+ return (
+ isImage(object) ||
+ isCanvas(object) ||
+ isContext(object) ||
+ isImageData(object)
+ );
+ }
+ function isType (object, type) {
+ return typeof object === 'object' && !!Object.prototype.toString.apply(object).match(type);
+ }
+
+
+ // Type Conversion
+ function copyImageData (imageData) {
+ var
+ height = imageData.height,
+ width = imageData.width,
+ data = imageData.data,
+ newImageData, newData, i;
+
+ canvas.width = width;
+ canvas.height = height;
+ newImageData = context.getImageData(0, 0, width, height);
+ newData = newImageData.data;
+
+ for (i = imageData.data.length; i--;) {
+ newData[i] = data[i];
+ }
+
+ return newImageData;
+ }
+ function toImageData (object) {
+ if (isImage(object)) { return toImageDataFromImage(object); }
+ if (isCanvas(object)) { return toImageDataFromCanvas(object); }
+ if (isContext(object)) { return toImageDataFromContext(object); }
+ if (isImageData(object)) { return object; }
+ }
+ function toImageDataFromImage (image) {
+ var
+ height = image.height,
+ width = image.width;
+ canvas.width = width;
+ canvas.height = height;
+ context.clearRect(0, 0, width, height);
+ context.drawImage(image, 0, 0);
+ return context.getImageData(0, 0, width, height);
+ }
+ function toImageDataFromCanvas (canvas) {
+ var
+ height = canvas.height,
+ width = canvas.width,
+ context = canvas.getContext('2d');
+ return context.getImageData(0, 0, width, height);
+ }
+ function toImageDataFromContext (context) {
+ var
+ canvas = context.canvas,
+ height = canvas.height,
+ width = canvas.width;
+ return context.getImageData(0, 0, width, height);
+ }
+ function toCanvas (object) {
+ var
+ data = toImageData(object),
+ canvas = getCanvas(data.width, data.height),
+ context = canvas.getContext('2d');
+
+ context.putImageData(data, 0, 0);
+ return canvas;
+ }
+
+
+ // ImageData Equality Operators
+ function equalWidth (a, b) {
+ return a.width === b.width;
+ }
+ function equalHeight (a, b) {
+ return a.height === b.height;
+ }
+ function equalDimensions (a, b) {
+ return equalHeight(a, b) && equalWidth(a, b);
+ }
+ function equal (a, b, tolerance) {
+
+ var
+ aData = a.data,
+ bData = b.data,
+ length = aData.length,
+ i;
+
+ tolerance = tolerance || 0;
+
+ if (!equalDimensions(a, b)) return false;
+ for (i = length; i--;) if (aData[i] !== bData[i] && Math.abs(aData[i] - bData[i]) > tolerance) return false;
+
+ return true;
+ }
+
+
+ // Diff
+ function diff (a, b) {
+ return (equalDimensions(a, b) ? diffEqual : diffUnequal)(a, b);
+ }
+ function diffEqual (a, b) {
+
+ var
+ height = a.height,
+ width = a.width,
+ c = getImageData(width, height), // c = a - b
+ aData = a.data,
+ bData = b.data,
+ cData = c.data,
+ length = cData.length,
+ row, column,
+ i, j, k, v;
+
+ for (i = 0; i < length; i += 4) {
+ cData[i] = Math.abs(aData[i] - bData[i]);
+ cData[i+1] = Math.abs(aData[i+1] - bData[i+1]);
+ cData[i+2] = Math.abs(aData[i+2] - bData[i+2]);
+ cData[i+3] = Math.abs(255 - Math.abs(aData[i+3] - bData[i+3]));
+ }
+
+ return c;
+ }
+ function diffUnequal (a, b) {
+
+ var
+ height = Math.max(a.height, b.height),
+ width = Math.max(a.width, b.width),
+ c = getImageData(width, height), // c = a - b
+ aData = a.data,
+ bData = b.data,
+ cData = c.data,
+ rowOffset,
+ columnOffset,
+ row, column,
+ i, j, k, v;
+
+
+ for (i = cData.length - 1; i > 0; i = i - 4) {
+ cData[i] = 255;
+ }
+
+ // Add First Image
+ offsets(a);
+ for (row = a.height; row--;){
+ for (column = a.width; column--;) {
+ i = 4 * ((row + rowOffset) * width + (column + columnOffset));
+ j = 4 * (row * a.width + column);
+ cData[i+0] = aData[j+0]; // r
+ cData[i+1] = aData[j+1]; // g
+ cData[i+2] = aData[j+2]; // b
+ // cData[i+3] = aData[j+3]; // a
+ }
+ }
+
+ // Subtract Second Image
+ offsets(b);
+ for (row = b.height; row--;){
+ for (column = b.width; column--;) {
+ i = 4 * ((row + rowOffset) * width + (column + columnOffset));
+ j = 4 * (row * b.width + column);
+ cData[i+0] = Math.abs(cData[i+0] - bData[j+0]); // r
+ cData[i+1] = Math.abs(cData[i+1] - bData[j+1]); // g
+ cData[i+2] = Math.abs(cData[i+2] - bData[j+2]); // b
+ }
+ }
+
+ // Helpers
+ function offsets (imageData) {
+ rowOffset = Math.floor((height - imageData.height) / 2);
+ columnOffset = Math.floor((width - imageData.width) / 2);
+ }
+
+ return c;
+ }
+
+
+ // Validation
+ function checkType () {
+ var i;
+ for (i = 0; i < arguments.length; i++) {
+ if (!isImageType(arguments[i])) {
+ throw {
+ name : 'ImageTypeError',
+ message : 'Submitted object was not an image.'
+ };
+ }
+ }
+ }
+
+
+ // Jasmine Matchers
+ function get (element, content) {
+ element = document.createElement(element);
+ if (element && content) {
+ element.innerHTML = content;
+ }
+ return element;
+ }
+
+ function imageDiffEqualMessage (actual, expected) {
+ return function () {
+ var
+ div = get('div'),
+ a = get('div', 'Actual:
'),
+ b = get('div', 'Expected:
'),
+ c = get('div', 'Diff:
'),
+ diff = imagediff.diff(actual, expected),
+ canvas = getCanvas(),
+ context;
+
+ canvas.height = diff.height;
+ canvas.width = diff.width;
+
+ div.style.overflow = 'hidden';
+ a.style.float = 'left';
+ b.style.float = 'left';
+ c.style.float = 'left';
+
+ context = canvas.getContext('2d');
+ context.putImageData(diff, 0, 0);
+
+ a.appendChild(toCanvas(actual));
+ b.appendChild(toCanvas(expected));
+ c.appendChild(canvas);
+
+ div.appendChild(a);
+ div.appendChild(b);
+ div.appendChild(c);
+
+ return div;
+ };
+ }
+
+ jasmine = {
+
+ toBeImageData : function () {
+ return {
+ compare: function () {
+ return {
+ pass: imagediff.isImageData(this.actual)
+ }
+ }
+ };
+ },
+
+ toImageDiffEqual: function () {
+ return {
+ compare: function (actual, expected, tolerance) {
+ var
+ result = {};
+
+ result.pass = imagediff.equal(actual, expected, tolerance);
+ if (typeof document !== UNDEFINED) {
+ result.message = imageDiffEqualMessage(actual, expected);
+ }
+ return result;
+ },
+ negativeCompare: function (actual, expected, tolerance) {
+ return {
+ pass: !imagediff.equal(actual, expected, tolerance),
+ message: 'Expected not to be equal.'
+ };
+ }
+ };
+ }
+ };
+
+
+ // Image Output
+ function imageDataToPNG (imageData, outputFile, callback) {
+
+ var
+ canvas = toCanvas(imageData),
+ base64Data,
+ decodedImage;
+
+ callback = callback || Function;
+
+ base64Data = canvas.toDataURL().replace(/^data:image\/\w+;base64,/,"");
+ decodedImage = new Buffer(base64Data, 'base64');
+ require('fs').writeFile(outputFile, decodedImage, callback);
+ }
+
+
+ // Definition
+ imagediff = {
+
+ createCanvas : getCanvas,
+ createImageData : getImageData,
+
+ isImage : isImage,
+ isCanvas : isCanvas,
+ isContext : isContext,
+ isImageData : isImageData,
+ isImageType : isImageType,
+
+ toImageData : function (object) {
+ checkType(object);
+ if (isImageData(object)) { return copyImageData(object); }
+ return toImageData(object);
+ },
+
+ equal : function (a, b, tolerance) {
+ checkType(a, b);
+ a = toImageData(a);
+ b = toImageData(b);
+ return equal(a, b, tolerance);
+ },
+ diff : function (a, b) {
+ checkType(a, b);
+ a = toImageData(a);
+ b = toImageData(b);
+ return diff(a, b);
+ },
+
+ jasmine : jasmine,
+
+ // Compatibility
+ noConflict : function () {
+ root[name] = previous;
+ return imagediff;
+ }
+ };
+
+ if (typeof module !== 'undefined') {
+ imagediff.imageDataToPNG = imageDataToPNG;
+ }
+
+ return imagediff;
+});
diff --git a/xblocks_contrib/problem/assets/spec/spec_helpers/jasmine-waituntil.js b/xblocks_contrib/problem/assets/spec/spec_helpers/jasmine-waituntil.js
new file mode 100644
index 00000000..4d0a5db9
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/spec_helpers/jasmine-waituntil.js
@@ -0,0 +1,47 @@
+/* eslint-env node */
+
+// Takes a latch function and optionally timeout and error message.
+// Polls the latch function until the it returns true or the maximum timeout expires
+// whichever comes first.
+(function(root, factory) {
+ factory(root, root.jQuery);
+}((function() {
+ return this;
+}()), function(window, $) {
+ 'use strict';
+
+ var MAX_TIMEOUT = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+ var realSetTimeout = setTimeout;
+ var realClearTimeout = clearTimeout;
+ jasmine.waitUntil = function(conditionalFn, maxTimeout, message) {
+ var deferred = $.Deferred(),
+ elapsedTimeInMs = 0,
+ timeout;
+
+ maxTimeout = maxTimeout || MAX_TIMEOUT;
+ message = message || 'Timeout has expired';
+
+ var fn = function() {
+ elapsedTimeInMs += 50;
+ if (conditionalFn()) {
+ if (timeout) { realClearTimeout(timeout); }
+ deferred.resolve();
+ } else {
+ if (elapsedTimeInMs >= maxTimeout) {
+ // explicitly fail the spec with the given message
+ fail(message);
+
+ // clear timeout and reject the promise
+ realClearTimeout(timeout);
+ deferred.reject();
+
+ return;
+ }
+ timeout = realSetTimeout(fn, 50);
+ }
+ };
+
+ realSetTimeout(fn, 50);
+ return deferred.promise();
+ };
+}));
diff --git a/xblocks_contrib/problem/assets/spec/spec_helpers/logger.js b/xblocks_contrib/problem/assets/spec/spec_helpers/logger.js
new file mode 100644
index 00000000..207088de
--- /dev/null
+++ b/xblocks_contrib/problem/assets/spec/spec_helpers/logger.js
@@ -0,0 +1,102 @@
+(function() {
+ 'use strict';
+
+ var Logger = (function() {
+ // listeners[event_type][element] -> list of callbacks
+ var listeners = {},
+ sendRequest, has;
+
+ sendRequest = function(data, options) {
+ var request = $.ajaxWithPrefix ? $.ajaxWithPrefix : $.ajax;
+
+ options = $.extend(true, {
+ url: '/event',
+ type: 'POST',
+ data: data,
+ async: true
+ }, options);
+ return request(options);
+ };
+
+ has = function(object, propertyName) {
+ return {}.hasOwnProperty.call(object, propertyName);
+ };
+
+ return {
+ /**
+ * Emits an event.
+ *
+ * Note that this method is used by external XBlocks, and the API cannot change without
+ * proper deprecation and notification for external authors.
+ */
+ log: function(eventType, data, element, requestOptions) {
+ var callbacks;
+
+ if (!element) {
+ // null element in the listener dictionary means any element will do.
+ // null element in the Logger.log call means we don't know the element name.
+ element = null;
+ }
+ // Check to see if we're listening for the event type.
+ if (has(listeners, eventType)) {
+ if (has(listeners[eventType], element)) {
+ // Make the callbacks.
+ callbacks = listeners[eventType][element];
+ $.each(callbacks, function(index, callback) {
+ try {
+ callback(eventType, data, element);
+ } catch (err) {
+ console.error({
+ eventType: eventType,
+ data: data,
+ element: element,
+ error: err
+ });
+ }
+ });
+ }
+ }
+ // Regardless of whether any callbacks were made, log this event.
+ return sendRequest({
+ event_type: eventType,
+ event: JSON.stringify(data),
+ courserun_key: typeof $$course_id !== 'undefined' ? $$course_id : null,
+ page: window.location.href
+ }, requestOptions);
+ },
+
+ /**
+ * Adds a listener. If you want any element to trigger this listener,
+ * do element = null.
+ *
+ * Note that this method is used by external XBlocks, and the API cannot change without
+ * proper deprecation and notification for external authors.
+ */
+ listen: function(eventType, element, callback) {
+ listeners[eventType] = listeners[eventType] || {};
+ listeners[eventType][element] = listeners[eventType][element] || [];
+ listeners[eventType][element].push(callback);
+ },
+
+ /**
+ * Binds `page_close` event.
+ *
+ * Note that this method is used by external XBlocks, and the API cannot change without
+ * proper deprecation and notification for external authors.
+ */
+ bind: function() {
+ window.onunload = function() {
+ sendRequest({
+ event_type: 'page_close',
+ event: '',
+ page: window.location.href
+ }, {type: 'GET', async: false});
+ };
+ }
+ };
+ }());
+
+ this.Logger = Logger;
+ // log_event exists for compatibility reasons and will soon be deprecated.
+ this.log_event = Logger.log;
+}).call(this);
diff --git a/xblocks_contrib/problem/assets/static/applets/Protex.jar b/xblocks_contrib/problem/assets/static/applets/Protex.jar
new file mode 100644
index 00000000..2c6a819e
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/applets/Protex.jar differ
diff --git a/xblocks_contrib/problem/assets/static/applets/genex.jar b/xblocks_contrib/problem/assets/static/applets/genex.jar
new file mode 100644
index 00000000..75f784b5
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/applets/genex.jar differ
diff --git a/xblocks_contrib/problem/assets/static/css/ProblemBlockDisplay.css b/xblocks_contrib/problem/assets/static/css/ProblemBlockDisplay.css
new file mode 100644
index 00000000..aeb93480
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/css/ProblemBlockDisplay.css
@@ -0,0 +1,2304 @@
+@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700");
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_correct .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicegroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + label.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + section.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + section.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicegroup
+ input
+ + label.choicegroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicegroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + label.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + section.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + section.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_incorrect .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicegroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + label.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + section.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + section.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.correct .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.partially-correct .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.incorrect .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .incorrect .status .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .incorrect .status .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ .problem
+ .capa_inputtype.textline
+ > .partially-correct
+ .status
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ .problem
+ .inputtype.formulaequationinput
+ > .partially-correct
+ .status
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .correct .status .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .correct .status .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .correct .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .incorrect .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .partially-correct .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .correct .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .incorrect .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .partially-correct .status-icon::after {
+ font-family: FontAwesome;
+ -webkit-font-smoothing: antialiased;
+ display: inline-block;
+ speak: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock h2 {
+ margin-top: 0;
+ margin-bottom: calc((var(--baseline, 20px) * 0.75));
+}
+
+.xmodule_display.xmodule_ProblemBlock h2.problem-header {
+ display: inline-block;
+}
+
+.xmodule_display.xmodule_ProblemBlock h2.problem-header section.staff {
+ margin-top: calc((var(--baseline, 20px) * 1.5));
+ font-size: 80%;
+}
+
+@media print {
+ .xmodule_display.xmodule_ProblemBlock h2 {
+ display: block;
+ width: auto;
+ border-right: 0;
+ }
+}
+
+.xmodule_display.xmodule_ProblemBlock .explanation-title {
+ font-weight: bold;
+}
+
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-incorrect,
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-partially-correct,
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-correct {
+ margin-top: calc((var(--baseline, 20px) / 4));
+}
+
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-incorrect .icon,
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-partially-correct .icon,
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-correct .icon {
+ margin-right: calc((var(--baseline, 20px) / 4));
+}
+
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-incorrect .icon {
+ color: var(--incorrect, #b20610);
+}
+
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-partially-correct .icon,
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-correct .icon {
+ color: var(--correct, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-text {
+ color: #646464;
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem-hint {
+ margin-bottom: 20px;
+ width: 100%;
+}
+
+.xmodule_display.xmodule_ProblemBlock .hint-label {
+ display: inline-block;
+ padding-right: 0.5em;
+}
+
+.xmodule_display.xmodule_ProblemBlock .hint-text {
+ display: inline-block;
+}
+
+.xmodule_display.xmodule_ProblemBlock .feedback-hint-multi .hint-text {
+ display: block;
+}
+
+.xmodule_display.xmodule_ProblemBlock iframe[seamless] {
+ overflow: hidden;
+ padding: 0;
+ border: 0 none transparent;
+ background-color: transparent;
+}
+
+.xmodule_display.xmodule_ProblemBlock .inline-error {
+ color: var(--error-color-dark, #95050d);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem-progress {
+ display: inline-block;
+ color: var(--gray-d1, #5e5e5e);
+ font-size: 0.875em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem {
+ padding-top: var(--baseline, 20px);
+}
+
+@media print {
+ .xmodule_display.xmodule_ProblemBlock div.problem {
+ display: block;
+ padding: 0;
+ width: auto;
+ }
+
+ .xmodule_display.xmodule_ProblemBlock div.problem canvas,
+ .xmodule_display.xmodule_ProblemBlock div.problem img {
+ page-break-inside: avoid;
+ }
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem input.math {
+ direction: ltr;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .inline {
+ display: inline;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .inline + p {
+ margin-top: var(--baseline, 20px);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .question-description {
+ color: var(--gray-d1, #5e5e5e);
+ font-size: var(--small-font-size, 80%);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem form > label,
+.xmodule_display.xmodule_ProblemBlock div.problem .problem-group-label {
+ display: block;
+ margin-bottom: var(--baseline, 20px);
+ font: inherit;
+ color: inherit;
+ -webkit-font-smoothing: initial;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .problem-group-label + .question-description {
+ margin-top: calc(-1 * var(--baseline, 20px));
+}
+
+.xmodule_display.xmodule_ProblemBlock .wrapper-problem-response + .wrapper-problem-response,
+.xmodule_display.xmodule_ProblemBlock .wrapper-problem-response + p {
+ margin-top: calc((var(--baseline, 20px) * 1.5));
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup {
+ margin: var(--baseline, 20px) 0 0 0;
+ min-width: 100px;
+ width: auto !important;
+ width: 100px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup:after,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup:after {
+ content: "";
+ display: table;
+ clear: both;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup label,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup label {
+ box-sizing: border-box;
+ display: inline-block;
+ clear: both;
+ margin-bottom: calc((var(--baseline, 20px) / 2));
+ border: 2px solid var(--gray-l4, #e4e4e4);
+ border-radius: 3px;
+ padding: calc((var(--baseline, 20px) / 2));
+ width: 100%;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup label::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup label::after {
+ margin-left: calc((var(--baseline, 20px) * 0.75));
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup .indicator-container,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup .indicator-container {
+ min-height: 1px;
+ width: 25px;
+ display: inline-block;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup fieldset,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup fieldset {
+ box-sizing: border-box;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input[type="radio"],
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input[type="radio"],
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input[type="checkbox"],
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input[type="checkbox"] {
+ margin: calc((var(--baseline, 20px) / 4));
+ margin-right: calc((var(--baseline, 20px) / 2));
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:focus + label,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:hover + label,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label {
+ border: 2px solid var(--blue, #0075b4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicegroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup .choicegroup input + label.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup .choicegroup input + section.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + section.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:focus + label.choicegroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicegroup_correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + label.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + section.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + section.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:hover + label.choicegroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicegroup_correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + label.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + section.choicetextgroup_correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + section.choicetextgroup_correct {
+ border: 2px solid var(--correct, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_correct .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicegroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + label.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + section.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + section.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicegroup
+ input:focus
+ + label.choicegroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + label.choicegroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + label.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + label.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + section.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + section.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicegroup
+ input:hover
+ + label.choicegroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + label.choicegroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + label.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + label.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + section.choicetextgroup_correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + section.choicetextgroup_correct
+ .status-icon::after {
+ color: var(--correct, #008100);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicegroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + label.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + section.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + section.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:focus + label.choicegroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicegroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + label.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + label.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + section.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + section.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:hover + label.choicegroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicegroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + label.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + label.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + section.choicetextgroup_partially-correct,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + section.choicetextgroup_partially-correct {
+ border: 2px solid var(--partially-correct, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicegroup
+ input
+ + label.choicegroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicegroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + label.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + section.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + section.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicegroup
+ input:focus
+ + label.choicegroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + label.choicegroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + label.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + label.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + section.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + section.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicegroup
+ input:hover
+ + label.choicegroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + label.choicegroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + label.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + label.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + section.choicetextgroup_partially-correct
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + section.choicetextgroup_partially-correct
+ .status-icon::after {
+ color: var(--partially-correct, #008100);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicegroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup .choicegroup input + label.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + section.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + section.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:focus + label.choicegroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicegroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + label.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + section.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + section.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:hover + label.choicegroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicegroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + label.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + section.choicetextgroup_incorrect,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + section.choicetextgroup_incorrect {
+ border: 2px solid var(--incorrect, #b20610);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_incorrect .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicegroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + label.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + label.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + section.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input
+ + section.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicegroup
+ input:focus
+ + label.choicegroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + label.choicegroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + label.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + label.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + section.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:focus
+ + section.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicegroup
+ input:hover
+ + label.choicegroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + label.choicegroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + label.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + label.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + section.choicetextgroup_incorrect
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ input:hover
+ + section.choicetextgroup_incorrect
+ .status-icon::after {
+ color: var(--incorrect, #b20610);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicegroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup .choicegroup input + label.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input
+ + section.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + section.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:focus + label.choicegroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicegroup_submitted,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + label.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:focus
+ + section.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + section.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:hover + label.choicegroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicegroup_submitted,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + label.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ .choicegroup
+ input:hover
+ + section.choicetextgroup_submitted,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + section.choicetextgroup_submitted {
+ border: 2px solid var(--submitted, #0075b4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup .field {
+ position: relative;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup label {
+ padding: calc((var(--baseline, 20px) / 2));
+ padding-left: calc((var(--baseline, 20px) * 2.3));
+ position: relative;
+ font-size: var(--base-font-size, 18px);
+ line-height: normal;
+ cursor: pointer;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input[type="radio"],
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input[type="checkbox"] {
+ left: 0.5625em;
+ position: absolute;
+ top: 0.43em;
+ width: calc(var(--baseline, 20px) * 1.1);
+ height: calc(var(--baseline, 20px) * 1.1);
+ z-index: 1;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup legend {
+ margin-bottom: var(--baseline, 20px);
+ max-width: 100%;
+ white-space: normal;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup legend + .question-description {
+ margin-top: calc(-1 * var(--baseline, 20px));
+ max-width: 100%;
+ white-space: normal;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container {
+ margin-left: calc((var(--baseline, 20px) * 0.75));
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status {
+ width: var(--baseline, 20px);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.correct .status-icon::after {
+ color: var(--correct, #008100);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.partially-correct .status-icon::after {
+ color: var(--partially-correct, #008100);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.incorrect .status-icon::after {
+ color: var(--incorrect, #b20610);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.submitted .status-icon,
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.unsubmitted .status-icon,
+.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.unanswered .status-icon {
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem ol.enumerate li::before {
+ display: block;
+ visibility: hidden;
+ height: 0;
+ content: " ";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .solution-span > span {
+ margin: var(--baseline, 20px) 0;
+ display: block;
+ position: relative;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .solution-span > span:empty {
+ display: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .targeted-feedback-span > span {
+ display: block;
+ position: relative;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .targeted-feedback-span > span:empty {
+ display: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div p.answer {
+ margin-top: -2px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div p span.clarification i {
+ font-style: normal;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div p span.clarification i:hover {
+ color: var(--blue, #0075b4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div.correct input,
+.xmodule_display.xmodule_ProblemBlock div.problem div.ui-icon-check input {
+ border-color: var(--correct, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div.partially-correct input,
+.xmodule_display.xmodule_ProblemBlock div.problem div.ui-icon-check input {
+ border-color: var(--partially-correct, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div.processing input {
+ border-color: #aaa;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div.ui-icon-close input {
+ border-color: var(--incorrect, #b20610);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div.incorrect input,
+.xmodule_display.xmodule_ProblemBlock div.problem div.incomplete input {
+ border-color: var(--incorrect, #b20610);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div.submitted input,
+.xmodule_display.xmodule_ProblemBlock div.problem div.ui-icon-check input {
+ border-color: var(--submitted, #0075b4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div p.answer {
+ display: inline-block;
+ margin-top: calc((var(--baseline, 20px) / 2));
+ margin-bottom: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div p.answer::before {
+ display: inline;
+ content: "Answer: ";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div p.answer:empty::before {
+ display: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div div.equation {
+ clear: both;
+ margin-top: 3px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div div.equation .MathJax_Display {
+ width: auto;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div div.equation img.loading {
+ padding-left: calc((var(--baseline, 20px) / 2));
+ display: inline-block;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div div.equation span {
+ margin-bottom: 0;
+ display: inline-block;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div div.equation span.MathJax_CHTML,
+.xmodule_display.xmodule_ProblemBlock div.problem div div.equation span.MathJax,
+.xmodule_display.xmodule_ProblemBlock div.problem div div.equation span.MathJax_SVG {
+ padding: 6px;
+ min-width: 30px;
+ border: 1px solid #e3e3e3;
+ border-radius: 4px;
+ background: #f1f1f1;
+}
+
+@media print {
+ .xmodule_display.xmodule_ProblemBlock div.problem div [id^="display"].equation {
+ display: none;
+ }
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div span.ui-icon-bullet {
+ display: inline-block;
+ position: relative;
+ top: 4px;
+ width: 14px;
+ height: 14px;
+ background: var(--icon-unanswered) center center no-repeat;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div span.processing,
+.xmodule_display.xmodule_ProblemBlock div.problem div span.ui-icon-processing {
+ display: inline-block;
+ position: relative;
+ top: 6px;
+ width: 25px;
+ height: 20px;
+ background: var(--icon-spinner) center center no-repeat;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div span.ui-icon-check {
+ display: inline-block;
+ position: relative;
+ top: 3px;
+ width: 25px;
+ height: 20px;
+ background: var(--icon-correct) center center no-repeat;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div span.incomplete,
+.xmodule_display.xmodule_ProblemBlock div.problem div span.ui-icon-close {
+ display: inline-block;
+ position: relative;
+ top: 3px;
+ width: 20px;
+ height: 20px;
+ background: var(--icon-incorrect) center center no-repeat;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .reload {
+ float: right;
+ margin: calc((var(--baseline, 20px) / 2));
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status {
+ margin: calc(var(--baseline, 20px) / 2) 0;
+ padding: calc(var(--baseline, 20px) / 2);
+ border-radius: 5px;
+ background: var(--gray-l6, #f8f8f8);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status:after {
+ content: "";
+ display: table;
+ clear: both;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status span {
+ display: block;
+ float: left;
+ overflow: hidden;
+ margin: -7px 7px 0 0;
+ text-indent: -9999px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status .grading {
+ margin: 0 7px 0 0;
+ padding-left: 25px;
+ background: var(--icon-info) left center no-repeat;
+ text-indent: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status p {
+ float: left;
+ margin-bottom: 0;
+ text-transform: capitalize;
+ line-height: 20px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status.file {
+ margin-top: var(--baseline, 20px);
+ padding: var(--baseline, 20px) 0 0 0;
+ border: 0;
+ border-top: 1px solid #eee;
+ background: var(--white, #fff);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status.file p.debug {
+ display: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status.file input {
+ float: left;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation p {
+ margin-bottom: calc((var(--baseline, 20px) / 5));
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .feedback-on-feedback {
+ margin-right: var(--baseline, 20px);
+ height: 100px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-response header {
+ text-align: right;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-response header a {
+ font-size: 0.85em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-scoring .scoring-list {
+ margin-left: 3px;
+ list-style-type: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-scoring .scoring-list li {
+ display: inline;
+ margin-left: 50px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-scoring .scoring-list li:first-child {
+ margin-left: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-scoring .scoring-list li label {
+ font-size: 0.9em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div .submit-message-container {
+ margin: var(--baseline, 20px) 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div.inline > span {
+ display: inline;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem ul {
+ padding-left: 1em;
+ margin-bottom: lh();
+ margin-left: 0.75em;
+ margin-left: 0.75rem;
+ list-style: disc outside none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem ol {
+ padding-left: 1em;
+ margin-bottom: lh();
+ margin-left: 0.75em;
+ margin-left: 0.75rem;
+ list-style: decimal outside none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem dl {
+ line-height: 1.4em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem dl dd {
+ margin-bottom: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem dd {
+ margin-left: 0.5em;
+ margin-left: 0.5rem;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem li {
+ margin-bottom: lh(0.5);
+ line-height: 1.4em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem li:last-child {
+ margin-bottom: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem p {
+ margin-bottom: lh();
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem table {
+ margin: lh() 0;
+ border-collapse: collapse;
+ table-layout: auto;
+ max-width: 100%;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem table td.cont-justified-left,
+.xmodule_display.xmodule_ProblemBlock div.problem table th.cont-justified-left {
+ text-align: left !important;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem table td.cont-justified-right,
+.xmodule_display.xmodule_ProblemBlock div.problem table th.cont-justified-right {
+ text-align: right !important;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem table td.cont-justified-center,
+.xmodule_display.xmodule_ProblemBlock div.problem table th.cont-justified-center {
+ text-align: center !important;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem table th {
+ text-align: left;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem table td {
+ text-align: left;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem table caption,
+.xmodule_display.xmodule_ProblemBlock div.problem table th,
+.xmodule_display.xmodule_ProblemBlock div.problem table td {
+ padding: 0.25em 0.75em 0.25em 0;
+ padding: 0.25rem 0.75rem 0.25rem 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem table caption {
+ margin-bottom: 0.75em;
+ margin-bottom: 0.75rem;
+ padding: 0.75em 0;
+ padding: 0.75rem 0;
+ background: #f1f1f1;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem table tr,
+.xmodule_display.xmodule_ProblemBlock div.problem table td,
+.xmodule_display.xmodule_ProblemBlock div.problem table th {
+ vertical-align: middle;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem code {
+ margin: 0 2px;
+ padding: 0 5px;
+ border: 1px solid #eaeaea;
+ border-radius: 3px;
+ background-color: var(--gray-l6, #f8f8f8);
+ white-space: nowrap;
+ font-size: 0.9em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem pre {
+ overflow: auto;
+ padding: 6px calc(var(--baseline, 20px) / 2);
+ border: 1px solid var(--gray-l3, #c8c8c8);
+ border-radius: 3px;
+ background-color: var(--gray-l6, #f8f8f8);
+ font-size: 0.9em;
+ line-height: 1.4;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem pre > code {
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: transparent;
+ white-space: pre;
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline input,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput input {
+ box-sizing: border-box;
+ border: 2px solid var(--gray-l4, #e4e4e4);
+ border-radius: 3px;
+ min-width: 160px;
+ height: 46px;
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline .status,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput .status {
+ display: inline-block;
+ margin-top: calc((var(--baseline, 20px) / 2));
+ background: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .incorrect input,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .incorrect input {
+ border: 2px solid var(--incorrect, #b20610);
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .incorrect .status .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ .problem
+ .inputtype.formulaequationinput
+ > .incorrect
+ .status
+ .status-icon::after {
+ color: var(--incorrect, #b20610);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .partially-correct input,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .partially-correct input {
+ border: 2px solid var(--partially-correct, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock
+ .problem
+ .capa_inputtype.textline
+ > .partially-correct
+ .status
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ .problem
+ .inputtype.formulaequationinput
+ > .partially-correct
+ .status
+ .status-icon::after {
+ color: var(--partially-correct, #008100);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .correct input,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .correct input {
+ border: 2px solid var(--correct, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .correct .status .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .correct .status .status-icon::after {
+ color: var(--correct, #008100);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .submitted,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .submitted {
+ margin: var(--baseline, 20px) 0 0 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .submitted input,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .submitted input {
+ border: 2px solid var(--submitted, #0075b4);
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .submitted .status,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .submitted .status {
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .unanswered input,
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .unsubmitted input,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .unanswered input,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .unsubmitted input {
+ border: 2px solid var(--gray-l4, #e4e4e4);
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .unanswered .status .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .unsubmitted .status .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ .problem
+ .inputtype.formulaequationinput
+ > .unanswered
+ .status
+ .status-icon::after,
+.xmodule_display.xmodule_ProblemBlock
+ .problem
+ .inputtype.formulaequationinput
+ > .unsubmitted
+ .status
+ .status-icon::after {
+ content: "";
+ display: inline-block;
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > div input {
+ direction: ltr;
+ text-align: left;
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .trailing_text {
+ margin-right: calc((var(--baseline, 20px) / 2));
+ display: inline-block;
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input {
+ margin: var(--baseline, 20px) 0 0 0 !important;
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input .indicator-container {
+ display: inline-block;
+}
+
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input .indicator-container .status.correct::after,
+.xmodule_display.xmodule_ProblemBlock
+ .problem
+ .inputtype.option-input
+ .indicator-container
+ .status.partially-correct::after,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input .indicator-container .status.incorrect::after,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input .indicator-container .status.submitted::after,
+.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input .indicator-container .status.unanswered::after {
+ margin-left: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror {
+ border: 1px solid black;
+ font-size: 14px;
+ line-height: 18px;
+ resize: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror .cm-tab {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=);
+ background-position: right;
+ background-repeat: no-repeat;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror pre {
+ overflow: hidden;
+ margin: 0;
+ padding: 0;
+ border-width: 0;
+ border-radius: 0;
+ background: transparent;
+ white-space: pre;
+ word-wrap: normal;
+ font-size: inherit;
+ font-family: inherit;
+ resize: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror pre.CodeMirror-cursor {
+ position: absolute;
+ visibility: hidden;
+ width: 0;
+ border-right: none;
+ border-left: 1px solid var(--black, #000);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror-focused pre.CodeMirror-cursor {
+ visibility: visible;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror-code pre {
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror-scroll {
+ margin-right: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock .capa-message {
+ display: inline-block;
+ color: var(--gray-d1, #5e5e5e);
+ -webkit-font-smoothing: antialiased;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action {
+ min-height: var(--baseline, 20px);
+ width: 100%;
+ display: flex;
+ display: -ms-flexbox;
+ -ms-flex-align: start;
+ flex-direction: row;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-buttons-wrapper {
+ display: inline-flex;
+ justify-content: flex-end;
+ width: 100%;
+ padding-bottom: var(--baseline, 20px);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-button-wrapper {
+ border-right: 1px solid var(--gray-300, #d9d9d9);
+ padding: 0 13px;
+ display: inline-block;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-button-wrapper:last-child {
+ border: none;
+ padding-right: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn {
+ border: none;
+ max-width: 110px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn:hover,
+.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn:focus,
+.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn:active {
+ color: var(--primary, #0075b4) !important;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn .icon {
+ margin-bottom: calc(var(--baseline, 20px) / 10);
+ display: block;
+}
+
+@media print {
+ .xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn {
+ display: none;
+ }
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .submit-attempt-container {
+ padding-bottom: var(--baseline, 20px);
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+}
+
+@media (max-width: var(--bp-screen-lg, 1024px)) {
+ .xmodule_display.xmodule_ProblemBlock div.problem .action .submit-attempt-container {
+ max-width: 100%;
+ padding-bottom: var(--baseline, 20px);
+ }
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .submit-attempt-container .submit {
+ margin-right: calc((var(--baseline, 20px) / 2));
+ float: left;
+ white-space: nowrap;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .submit-attempt-container .submit-cta-description {
+ color: var(--primary, #0075b4);
+ font-size: small;
+ padding-right: calc(var(--baseline, 20px) / 2);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .submit-attempt-container .submit-cta-link-button {
+ color: var(--primary, #0075b4);
+ padding-right: calc(var(--baseline, 20px) / 4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .submission-feedback {
+ margin-right: calc((var(--baseline, 20px) / 2));
+ margin-top: calc(var(--baseline, 20px) / 2);
+ display: inline-block;
+ color: var(--gray-d1, #5e5e5e);
+ font-size: var(--medium-font-size, 0.9em);
+ -webkit-font-smoothing: antialiased;
+ vertical-align: middle;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .action .submission-feedback.cta-enabled {
+ margin-top: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem hr {
+ float: none;
+ clear: both;
+ margin: 0 0 0.75rem;
+ width: 100%;
+ height: 1px;
+ border: none;
+ background: #ddd;
+ color: #ddd;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .hidden {
+ display: none;
+ visibility: hidden;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem var (--all-text-inputs) {
+ display: inline;
+ width: auto;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem center {
+ display: block;
+ margin: lh() 0;
+ padding: lh();
+ border: 1px solid var(--gray-l3, #c8c8c8);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .message {
+ font-size: inherit;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .detailed-solution > p {
+ margin: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .detailed-solution > p:first-child {
+ margin-bottom: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .detailed-targeted-feedback > p,
+.xmodule_display.xmodule_ProblemBlock div.problem .detailed-targeted-feedback-partially-correct > p,
+.xmodule_display.xmodule_ProblemBlock div.problem .detailed-targeted-feedback-correct > p {
+ margin: 0;
+ font-weight: normal;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div.capa_alert {
+ margin-top: var(--baseline, 20px);
+ padding: 8px 12px;
+ border: 1px solid var(--warning-color, #ffc01f);
+ border-radius: 3px;
+ background: var(--warning-color-accent, #fffcdd);
+ font-size: 0.9em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification {
+ float: left;
+ margin-top: calc(var(--baseline, 20px) / 2);
+ padding: calc((var(--baseline, 20px) / 2.5)) calc((var(--baseline, 20px) / 2)) calc((var(--baseline, 20px) / 5))
+ calc((var(--baseline, 20px) / 2));
+ line-height: var(--base-line-height, 1.5em);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.success {
+ border-top: 3px solid var(--success, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.success .icon {
+ margin-right: 15px;
+ color: var(--success, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.error {
+ border-top: 3px solid var(--danger, #b20610);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.error .icon {
+ margin-right: 15px;
+ color: var(--danger, #b20610);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.warning {
+ border-top: 3px solid var(--warning, #e2c01f);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.warning .icon {
+ margin-right: 15px;
+ color: var(--warning, #e2c01f);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.general {
+ border-top: 3px solid var(--general-color-accent, #0075b4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.general .icon {
+ margin-right: 15px;
+ color: var(--general-color-accent, #0075b4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.problem-hint {
+ border: 1px solid var(--uxpl-gray-background, #d9d9d9);
+ border-radius: 6px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.problem-hint .icon {
+ margin-right: calc(3 * var(--baseline, 20px) / 4);
+ color: var(--uxpl-gray-dark, #111111);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.problem-hint li[class*="hint-index-"] {
+ color: var(--uxpl-gray-base, #414141);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification.problem-hint li[class*="hint-index-"] > strong {
+ color: var(--uxpl-gray-dark, #111111);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification .icon {
+ float: left;
+ position: relative;
+ top: calc(var(--baseline, 20px) / 5);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification .notification-message {
+ display: inline-block;
+ width: 69.38776%;
+ margin-bottom: 8px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification .notification-message ol {
+ list-style: none outside none;
+ padding: 0;
+ margin: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification .notification-message ol li:not(:last-child) {
+ margin-bottom: calc(var(--baseline, 20px) / 4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification li[class*="hint-index-"] ul,
+.xmodule_display.xmodule_ProblemBlock div.problem .notification li[class*="hint-index-"] ol {
+ padding: 0 0 0 1em;
+ margin-left: 0.75rem;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification li[class*="hint-index-"] ol {
+ list-style: decimal outside none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification .notification-btn-wrapper {
+ float: right;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification-btn {
+ float: right;
+ padding: calc((var(--baseline, 20px) / 10)) calc((var(--baseline, 20px) / 4));
+ min-width: calc((var(--baseline, 20px) * 3));
+ display: block;
+ clear: both;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .notification-btn:first-child {
+ margin-bottom: calc(var(--baseline, 20px) / 4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem button:hover {
+ background-image: none;
+ box-shadow: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem button:focus {
+ box-shadow: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem button.btn-default {
+ background-color: transparent;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem button.btn-brand:hover {
+ background-color: var(--btn-brand-focus-background, #065683);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .review-btn {
+ color: var(--blue, #0075b4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .review-btn.sr {
+ color: var(--blue, #0075b4);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem div.capa_reset {
+ padding: 25px;
+ background-color: var(--error-color-light, #f95861);
+ border: 1px solid var(--error-color, #cb0712);
+ border-radius: 3px;
+ font-size: 1em;
+ margin-top: calc(var(--baseline, 20px) / 2);
+ margin-bottom: calc(var(--baseline, 20px) / 2);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .capa_reset > h2 {
+ color: #a00;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .capa_reset li {
+ font-size: 0.9em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .hints {
+ border: 1px solid var(--gray-l3, #c8c8c8);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .hints h3 {
+ padding: 9px;
+ border-bottom: 1px solid #e3e3e3;
+ background: #eee;
+ text-shadow: 0 1px 0 var(--white, #fff);
+ font-size: 1em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .hints div {
+ border-bottom: 1px solid #ddd;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .hints div:last-child {
+ border-bottom: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .hints div p {
+ margin-bottom: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .hints div header a {
+ display: block;
+ padding: 9px;
+ background: var(--gray-l6, #f8f8f8);
+ box-shadow: inset 0 0 0 1px var(--white, #fff);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .hints div > section {
+ padding: 9px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .test {
+ padding-top: 18px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .test header {
+ margin-bottom: 12px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .test header h3 {
+ color: #aaa;
+ font-style: normal;
+ font-size: 0.9em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .test > section {
+ position: relative;
+ margin-bottom: calc((var(--baseline, 20px) / 2));
+ padding: 9px 9px var(--baseline, 20px);
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ background: var(--white, #fff);
+ box-shadow: inset 0 0 0 1px #eee;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .test > section p:last-of-type {
+ margin-bottom: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .test > section .shortform {
+ margin-bottom: 0.6em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .test > section a.full {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 1px;
+ left: 0;
+ box-sizing: border-box;
+ display: block;
+ padding: calc((var(--baseline, 20px) / 5));
+ background: var(--gray-l4, #e4e4e4);
+ text-align: right;
+ font-size: 1em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .test > section a.full.full-top {
+ position: absolute;
+ top: 1px;
+ right: 0;
+ bottom: auto;
+ left: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .test > section a.full.full-bottom {
+ position: absolute;
+ top: auto;
+ right: 0;
+ bottom: 1px;
+ left: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section {
+ padding-top: calc((var(--baseline, 20px) * 1.5));
+ padding-left: var(--baseline, 20px);
+ background-color: #fafafa;
+ color: #2c2c2c;
+ font-size: 1em;
+ font-family: monospace;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section header {
+ font-size: 1.4em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform {
+ margin: 0;
+ padding: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-errors {
+ margin: calc((var(--baseline, 20px) / 4));
+ padding: calc((var(--baseline, 20px) / 2)) calc((var(--baseline, 20px) / 2)) calc((var(--baseline, 20px) / 2))
+ calc((var(--baseline, 20px) * 2));
+ background: var(--icon-incorrect) center left no-repeat;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-errors li {
+ color: #b00;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-output {
+ margin: calc(var(--baseline, 20px) / 4);
+ padding: var(--baseline, 20px) 0 calc((var(--baseline, 20px) * 0.75)) 50px;
+ border-top: 1px solid #ddd;
+ border-left: var(--baseline, 20px) solid #fafafa;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-output h4 {
+ font-size: 1em;
+ font-family: monospace;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-output dl {
+ margin: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-output dt {
+ margin-top: var(--baseline, 20px);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-output dd {
+ margin-left: 24pt;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-correct {
+ background: var(--icon-correct) left 20px no-repeat;
+}
+
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .external-grader-message
+ section
+ .longform
+ .result-correct
+ .result-actual-output {
+ color: #090;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-partially-correct {
+ background: var(--icon-partially-correct) left 20px no-repeat;
+}
+
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .external-grader-message
+ section
+ .longform
+ .result-partially-correct
+ .result-actual-output {
+ color: #090;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-incorrect {
+ background: var(--icon-incorrect) left 20px no-repeat;
+}
+
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .external-grader-message
+ section
+ .longform
+ .result-incorrect
+ .result-actual-output {
+ color: #b00;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .markup-text {
+ margin: calc((var(--baseline, 20px) / 4));
+ padding: var(--baseline, 20px) 0 15px 50px;
+ border-top: 1px solid #ddd;
+ border-left: 20px solid #fafafa;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .markup-text bs {
+ color: #b00;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .markup-text bg {
+ color: #bda046;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .rubric tr {
+ margin: calc((var(--baseline, 20px) / 2)) 0;
+ height: 100%;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .rubric td {
+ margin: calc((var(--baseline, 20px) / 2)) 0;
+ padding: var(--baseline, 20px) 0;
+ height: 100%;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .rubric th {
+ margin: calc((var(--baseline, 20px) / 4));
+ padding: calc((var(--baseline, 20px) / 4));
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .rubric label,
+.xmodule_display.xmodule_ProblemBlock div.problem .rubric .view-only {
+ position: relative;
+ display: inline-block;
+ margin: 3px;
+ padding: calc((var(--baseline, 20px) * 0.75));
+ min-width: 50px;
+ min-height: 50px;
+ width: 150px;
+ height: 100%;
+ background-color: var(--gray-l3, #c8c8c8);
+ font-size: 0.9em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .rubric .grade {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ margin: calc((var(--baseline, 20px) / 2));
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .rubric .selected-grade {
+ background: #666;
+ color: white;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .rubric input[type="radio"]:checked + label {
+ background: #666;
+ color: white;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .rubric input[class="score-selection"] {
+ display: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input {
+ margin: 0 0 1em 0;
+ border: 1px solid var(--gray-l3, #c8c8c8);
+ border-radius: 1em;
+ /* for debugging the input value field. enable the debug flag on the inputtype */
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .annotation-header {
+ padding: 0.5em 1em;
+ border-bottom: 1px solid var(--gray-l3, #c8c8c8);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .annotation-body {
+ padding: 0.5em 1em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input a.annotation-return {
+ float: right;
+ font: inherit;
+ font-weight: normal;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input a.annotation-return::after {
+ content: " \2191";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .block,
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags {
+ margin: 0.5em 0;
+ padding: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .block-highlight {
+ padding: 0.5em;
+ border: 1px solid rgba(214, 214, 0, 0.3);
+ background-color: rgba(255, 255, 10, 0.3);
+ color: #333;
+ font-style: normal;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .block-comment {
+ font-style: italic;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags {
+ display: block;
+ margin-left: 1em;
+ list-style-type: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li {
+ position: relative;
+ display: block;
+ margin: 1em 0 0 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li .tag {
+ display: inline-block;
+ margin-left: calc((var(--baseline, 20px) * 2));
+ border: 1px solid #666666;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li .tag.selected {
+ background-color: rgba(255, 255, 10, 0.3);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li .tag-status {
+ position: absolute;
+ left: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li .tag-status,
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li .tag {
+ padding: 0.25em 0.5em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input textarea.comment {
+ padding: 0.2em 0.4em;
+ width: 100%;
+ height: 7.2em;
+ line-height: 1.4em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .answer-annotation {
+ display: block;
+ margin: 0;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .debug-value {
+ margin: 1em 0;
+ padding: 1em;
+ border: 1px solid var(--black, #000);
+ background-color: #999;
+ color: var(--white, #fff);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .debug-value input[type="text"] {
+ width: 100%;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .debug-value pre {
+ background-color: var(--gray-l3, #c8c8c8);
+ color: var(--black, #000);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .debug-value::before {
+ display: block;
+ content: "debug input value";
+ font-size: 1.5em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input[type="text"] {
+ margin-bottom: 0.5em;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup label.choicetextgroup_correct input[type="text"],
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup section.choicetextgroup_correct input[type="text"] {
+ border-color: var(--correct, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ label.choicetextgroup_partially-correct
+ input[type="text"],
+.xmodule_display.xmodule_ProblemBlock
+ div.problem
+ .choicetextgroup
+ section.choicetextgroup_partially-correct
+ input[type="text"] {
+ border-color: var(--partially-correct, #008100);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup label.choicetextgroup_show_correct::after,
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup section.choicetextgroup_show_correct::after {
+ margin-left: calc((var(--baseline, 20px) * 0.75));
+ content: var(--icon-correct);
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup span.mock_label {
+ cursor: default;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .status {
+ display: inline-block;
+ position: relative;
+ top: 3px;
+ width: 25px;
+ height: 20px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .status.unsubmitted .status-icon,
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .status.unanswered .status-icon {
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .status.unsubmitted .status-message,
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .status.unanswered .status-message {
+ display: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .correct .status-icon::after {
+ color: var(--correct, #008100);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .incorrect .status-icon::after {
+ color: var(--incorrect, #b20610);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .partially-correct .status-icon::after {
+ color: var(--partially-correct, #008100);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .submitted {
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .tag-status {
+ display: inline-block;
+ position: relative;
+ top: 3px;
+ width: 25px;
+ height: 20px;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .tag-status.unsubmitted .status-icon,
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .tag-status.unanswered .status-icon {
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .tag-status.unsubmitted .status-message,
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .tag-status.unanswered .status-message {
+ display: none;
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .correct .status-icon::after {
+ color: var(--correct, #008100);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .incorrect .status-icon::after {
+ color: var(--incorrect, #b20610);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .partially-correct .status-icon::after {
+ color: var(--partially-correct, #008100);
+ font-size: 1.2em;
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .submitted {
+ content: "";
+}
+
+.xmodule_display.xmodule_ProblemBlock .problems-wrapper .loading-spinner {
+ text-align: center;
+ color: var(--gray-d1, #5e5e5e);
+}
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX2E0-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX2E0-3D-balls.png
new file mode 100644
index 00000000..7595cf2a
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX2E0-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX2E1-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX2E1-3D-balls.png
new file mode 100644
index 00000000..c8816feb
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX2E1-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX2E2-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX2E2-3D-balls.png
new file mode 100644
index 00000000..00faa7e6
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX2E2-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX2E3-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX2E3-3D-balls.png
new file mode 100644
index 00000000..4fb7022f
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX2E3-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX3E0-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX3E0-3D-balls.png
new file mode 100644
index 00000000..eeff5b22
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX3E0-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX3E1-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX3E1-3D-balls.png
new file mode 100644
index 00000000..e5386bf0
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX3E1-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX3E2-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX3E2-3D-balls.png
new file mode 100644
index 00000000..185abaa4
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX3E2-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX4E0-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX4E0-3D-balls.png
new file mode 100644
index 00000000..f690ad1f
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX4E0-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX4E1-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX4E1-3D-balls.png
new file mode 100644
index 00000000..1e940f75
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX4E1-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX4E2-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX4E2-3D-balls.png
new file mode 100644
index 00000000..ec74eb90
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX4E2-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX5E1-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX5E1-3D-balls.png
new file mode 100644
index 00000000..dc12dd9f
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX5E1-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX5E2-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX5E2-3D-balls.png
new file mode 100644
index 00000000..38755c14
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX5E2-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX6E0-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX6E0-3D-balls.png
new file mode 100644
index 00000000..775ffc46
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX6E0-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX6E1-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX6E1-3D-balls.png
new file mode 100644
index 00000000..3e51b877
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX6E1-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX7E0-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX7E0-3D-balls.png
new file mode 100644
index 00000000..95d7aebf
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX7E0-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX8E0-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX8E0-3D-balls.png
new file mode 100644
index 00000000..3a24f779
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX8E0-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/AX9E0-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/AX9E0-3D-balls.png
new file mode 100644
index 00000000..b457f58e
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/AX9E0-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Bent-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Bent-3D-balls.png
new file mode 100644
index 00000000..71bb9526
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Bent-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Linear-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Linear-3D-balls.png
new file mode 100644
index 00000000..af5f92c7
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Linear-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Linear-stick.png b/xblocks_contrib/problem/assets/static/images/vsepr/Linear-stick.png
new file mode 100644
index 00000000..e1306f9a
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Linear-stick.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Octahedral-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Octahedral-3D-balls.png
new file mode 100644
index 00000000..0032fe1e
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Octahedral-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Octahedral-stick.png b/xblocks_contrib/problem/assets/static/images/vsepr/Octahedral-stick.png
new file mode 100644
index 00000000..3d2b7ca5
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Octahedral-stick.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Pentagonal-bipyramidal-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Pentagonal-bipyramidal-3D-balls.png
new file mode 100644
index 00000000..23e02daf
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Pentagonal-bipyramidal-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Pentagonal-planar-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Pentagonal-planar-3D-balls.png
new file mode 100644
index 00000000..0768ec75
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Pentagonal-planar-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Pentagonal-pyramidal-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Pentagonal-pyramidal-3D-balls.png
new file mode 100644
index 00000000..511db1ea
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Pentagonal-pyramidal-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Pyramidal-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Pyramidal-3D-balls.png
new file mode 100644
index 00000000..0383528d
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Pyramidal-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Seesaw-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Seesaw-3D-balls.png
new file mode 100644
index 00000000..b7665da3
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Seesaw-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Square-antiprismatic-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Square-antiprismatic-3D-balls.png
new file mode 100644
index 00000000..675ba88c
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Square-antiprismatic-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Square-planar-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Square-planar-3D-balls.png
new file mode 100644
index 00000000..6a90a0c3
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Square-planar-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/T-shaped-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/T-shaped-3D-balls.png
new file mode 100644
index 00000000..b3eaa47f
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/T-shaped-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Tetrahedral-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Tetrahedral-3D-balls.png
new file mode 100644
index 00000000..9ff8731a
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Tetrahedral-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Tetrahedral-stick.png b/xblocks_contrib/problem/assets/static/images/vsepr/Tetrahedral-stick.png
new file mode 100644
index 00000000..09a41164
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Tetrahedral-stick.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-3D-balls.png
new file mode 100644
index 00000000..20e7c957
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-bipyramidal-3D-balls.png b/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-bipyramidal-3D-balls.png
new file mode 100644
index 00000000..ab5f0133
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-bipyramidal-3D-balls.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-bipyramidal-stick.png b/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-bipyramidal-stick.png
new file mode 100644
index 00000000..3b36dcd5
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-bipyramidal-stick.png differ
diff --git a/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-planar-stick.png b/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-planar-stick.png
new file mode 100644
index 00000000..f2fd25c5
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/images/vsepr/Trigonal-planar-stick.png differ
diff --git a/xblocks_contrib/problem/assets/static/js/.gitignore b/xblocks_contrib/problem/assets/static/js/.gitignore
new file mode 100644
index 00000000..a25aa577
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/.gitignore
@@ -0,0 +1,3 @@
+!imageinput.js
+!schematic.js
+!display.js
diff --git a/xblocks_contrib/problem/assets/static/js/capa/README b/xblocks_contrib/problem/assets/static/js/capa/README
new file mode 100644
index 00000000..ca845717
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/README
@@ -0,0 +1 @@
+These files really should be in the capa block, but we don't have a way to load js from there at the moment. (TODO)
diff --git a/xblocks_contrib/problem/assets/static/js/capa/annotationinput.js b/xblocks_contrib/problem/assets/static/js/capa/annotationinput.js
new file mode 100644
index 00000000..09adde93
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/annotationinput.js
@@ -0,0 +1,98 @@
+(function() {
+ var debug = false;
+
+ var module = {
+ debug: debug,
+ inputSelector: '.annotation-input',
+ tagSelector: '.tag',
+ tagsSelector: '.tags',
+ commentSelector: 'textarea.comment',
+ valueSelector: 'input.value', // stash tag selections and comment here as a JSON string...
+
+ singleSelect: true,
+
+ init: function() {
+ var that = this;
+
+ if (this.debug) { console.log('annotation input loaded: '); }
+
+ $(this.inputSelector).each(function(index, el) {
+ if (!$(el).data('listening')) {
+ $(el).delegate(that.tagSelector, 'click', $.proxy(that.onClickTag, that));
+ $(el).delegate(that.commentSelector, 'change', $.proxy(that.onChangeComment, that));
+ $(el).data('listening', 'yes');
+ }
+ });
+ },
+ onChangeComment: function(e) {
+ var value_el = this.findValueEl(e.target);
+ var current_value = this.loadValue(value_el);
+ var target_value = $(e.target).val();
+
+ current_value.comment = target_value;
+ this.storeValue(value_el, current_value);
+ },
+ onClickTag: function(e) {
+ var target_el = e.target,
+ target_value, target_index;
+ var value_el, current_value;
+
+ value_el = this.findValueEl(e.target);
+ current_value = this.loadValue(value_el);
+ target_value = $(e.target).data('id');
+
+ if (!$(target_el).hasClass('selected')) {
+ if (this.singleSelect) {
+ current_value.options = [target_value];
+ } else {
+ current_value.options.push(target_value);
+ }
+ } else {
+ if (this.singleSelect) {
+ current_value.options = [];
+ } else {
+ target_index = current_value.options.indexOf(target_value);
+ if (target_index !== -1) {
+ current_value.options.splice(target_index, 1);
+ }
+ }
+ }
+
+ this.storeValue(value_el, current_value);
+
+ if (this.singleSelect) {
+ $(target_el).closest(this.tagsSelector)
+ .find(this.tagSelector)
+ .not(target_el)
+ .removeClass('selected');
+ }
+ $(target_el).toggleClass('selected');
+ },
+ findValueEl: function(target_el) {
+ var input_el = $(target_el).closest(this.inputSelector);
+ return $(this.valueSelector, input_el);
+ },
+ loadValue: function(value_el) {
+ var json = $(value_el).val();
+
+ var result = JSON.parse(json);
+ if (result === null) {
+ result = {};
+ }
+ if (!result.hasOwnProperty('options')) {
+ result.options = [];
+ }
+ if (!result.hasOwnProperty('comment')) {
+ result.comment = '';
+ }
+
+ return result;
+ },
+ storeValue: function(value_el, new_value) {
+ var json = JSON.stringify(new_value);
+ $(value_el).val(json);
+ }
+ };
+
+ module.init();
+}).call(this);
diff --git a/xblocks_contrib/problem/assets/static/js/capa/chemical_equation_preview.js b/xblocks_contrib/problem/assets/static/js/capa/chemical_equation_preview.js
new file mode 100644
index 00000000..d23d8e51
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/chemical_equation_preview.js
@@ -0,0 +1,36 @@
+(function() {
+ update = function() {
+ function create_handler(saved_div) {
+ return (function(response) {
+ if (response.error) {
+ edx.HtmlUtils.setHtml(
+ saved_div,
+ edx.HtmlUtils.joinHtml(
+ edx.HtmlUtils.HTML(""),
+ response.error,
+ edx.HtmlUtils.HTML(' ')
+ )
+ );
+ } else {
+ saved_div.html(edx.HtmlUtils.HTML(response.preview).toString());
+ }
+ });
+ }
+
+ prev_id = '#' + this.id + '_preview';
+ preview_div = $(prev_id);
+
+ // find the closest parent problems-wrapper and use that url
+ url = $(this).closest('.problems-wrapper').data('url');
+ // grab the input id from the input
+ input_id = $(this).data('input-id');
+
+ Problem.inputAjax(url, input_id, 'preview_chemcalc', {formula: this.value}, create_handler(preview_div));
+ };
+
+ inputs = $('.chemicalequationinput input');
+ // update on load
+ inputs.each(update);
+ // and on every change
+ inputs.bind('input', update);
+}).call(this);
diff --git a/xblocks_contrib/problem/assets/static/js/capa/choicetextinput.js b/xblocks_contrib/problem/assets/static/js/capa/choicetextinput.js
new file mode 100644
index 00000000..3d08c94b
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/choicetextinput.js
@@ -0,0 +1,73 @@
+(function() {
+ var update = function() {
+ // Whenever a value changes create a new serialized version of this
+ // problem's inputs and set the hidden input field's value to equal it.
+ var parent = $(this).closest('section.choicetextinput');
+ // find the closest parent problems-wrapper and use that as the problem
+ // grab the input id from the input
+ // real_input is the hidden input field
+ var $real_input = $('input.choicetextvalue', parent);
+ var $all_inputs = $('input.ctinput', parent);
+ var user_inputs = {};
+ $($all_inputs).each(function(index, elt) {
+ var $node = $(elt);
+ var name = $node.attr('id');
+ var val = $node.val();
+ var radio_value = $node.attr('value');
+ var type = $node.attr('type');
+ var is_checked = $node.attr('checked');
+ if (type === 'radio' || type === 'checkbox') {
+ if (is_checked === 'checked' || is_checked === 'true') {
+ user_inputs[name] = radio_value;
+ }
+ } else {
+ user_inputs[name] = val;
+ }
+ });
+ var val_string = JSON.stringify(user_inputs);
+ // this is what gets submitted as the answer, we deserialize it later
+ $real_input.val(val_string);
+ };
+
+ var check_parent = function(event) {
+ // This looks for the containing choice of a textinput
+ // and sets it to be checked.
+ var $elt = $(event.target);
+ var parent_container = $elt.closest('section[id^="forinput"]');
+ var choice = parent_container.find("input[type='checkbox'], input[type='radio']");
+ choice.attr('checked', 'checked');
+ choice.change();
+ // need to check it then trigger the change event
+ };
+
+ var imitate_label = function(event) {
+ // This causes a section to check and uncheck
+ // a radiobutton/checkbox whenever a user clicks on it
+ // If the button/checkbox is disabled, nothing happens
+ var $elt = $(event.target);
+ var parent_container = $elt.closest('section[id^="forinput"]');
+ var choice = parent_container.find("input[type='checkbox'], input[type='radio']");
+ if (choice.attr('type') === 'radio') {
+ choice.attr('checked', 'checked');
+ } else {
+ if (choice.attr('checked')) {
+ choice.prop('checked', false);
+ } else {
+ choice.prop('checked', true);
+ }
+ }
+ choice.change();
+ update();
+ };
+ var $choices = $('.mock_label');
+ var $inputs = $('.choicetextinput .ctinput');
+ var $text_inputs = $('.choicetextinput .ctinput[type="text"]');
+ // update on load
+ $inputs.each(update);
+ // and on every change
+ // This allows text inside of choices to behave as if they were part of
+ // a label for the choice's button/checkbox
+ $choices.click(imitate_label);
+ $inputs.bind('change', update);
+ $text_inputs.click(check_parent);
+}).call(this);
diff --git a/xblocks_contrib/problem/assets/static/js/capa/design-protein-2d.js b/xblocks_contrib/problem/assets/static/js/capa/design-protein-2d.js
new file mode 100644
index 00000000..84a37215
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/design-protein-2d.js
@@ -0,0 +1,85 @@
+(function() {
+ var timeout = 1000;
+
+ waitForProtex();
+
+ function waitForProtex() {
+ if (typeof protex !== 'undefined' && protex) {
+ protex.onInjectionDone('protex');
+ // eslint-disable-next-line brace-style
+ }
+ /* if (typeof(protex) !== "undefined") {
+ //initializeProtex();
+ } */
+ else {
+ setTimeout(function() { waitForProtex(); }, timeout);
+ }
+ }
+
+ // NOTE:
+ // Protex uses three global functions:
+ // protexSetTargetShape (exported from GWT)
+ // exported protexCheckAnswer (exported from GWT)
+ // It calls protexIsReady with a deferred command when it has finished
+ // initialization and has drawn itself
+
+ function updateProtexField() {
+ var problem = $('#protex_container').parents('.problem');
+ var input_field = problem.find('input[type=hidden]');
+ var protex_answer = protexCheckAnswer();
+ var value = {protex_answer: protex_answer};
+ // console.log(JSON.stringify(value));
+ input_field.val(JSON.stringify(value));
+ }
+
+ protexIsReady = function() {
+ // Load target shape
+ var target_shape = $('#target_shape').val();
+ protexSetTargetShape(target_shape);
+
+ // Get answer from protex and store it into the hidden input field
+ // when Check button is clicked
+ var $fold_button = $('#fold-button');
+ $fold_button.on('click', function() {
+ var problem = $('#protex_container').parents('.problem');
+ var input_field = problem.find('input[type=hidden]');
+ var protex_answer = protexCheckAnswer();
+ var value = {protex_answer: protex_answer};
+ // console.log(JSON.stringify(value));
+ input_field.val(JSON.stringify(value));
+ });
+ updateProtexField();
+ };
+
+ /* function initializeProtex() {
+ //Check to see if the two exported GWT functions protexSetTargetShape
+ // and protexCheckAnswer have been appended to global scope -- this
+ //happens at the end of onModuleLoad() in GWT
+ if (typeof(protexSetTargetShape) === "function" &&
+ typeof(protexCheckAnswer) === "function") {
+
+ //Load target shape
+ var target_shape = $('#target_shape').val();
+ //protexSetTargetShape(target_shape);
+
+ //Get answer from protex and store it into the hidden input field
+ //when Check button is clicked
+ var problem = $('#protex_container').parents('.problem');
+ var check_button = problem.find('input.check');
+ var input_field = problem.find('input[type=hidden]');
+ check_button.on('click', function() {
+ var protex_answer = protexCheckAnswer();
+ var value = {protex_answer: protex_answer};
+ input_field.val(JSON.stringify(value));
+ });
+
+ //TO DO: Fix this, it works but is utterly ugly and unreliable
+ setTimeout(function() {
+ protexSetTargetShape(target_shape);}, 2000);
+
+ }
+ else {
+ setTimeout(function() {initializeProtex(); }, timeout);
+ }
+ } */
+}).call(this);
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop.js
new file mode 100644
index 00000000..aedf8661
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop.js
@@ -0,0 +1,25 @@
+// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
+// define() functions from Require JS available inside the anonymous function.
+//
+// See https://openedx.atlassian.net/wiki/display/PLAT/Integration+of+Require+JS+into+the+system
+(function(requirejs, require, define) {
+// HACK: this should be removed when it is safe to do so
+ if (window.baseUrl) {
+ requirejs.config({baseUrl: baseUrl});
+ }
+
+ // The current JS file will be loaded and run each time. It will require a
+ // single dependency which will be loaded and stored by RequireJS. On
+ // subsequent runs, RequireJS will return the dependency from memory, rather
+ // than loading it again from the server. For that reason, it is a good idea to
+ // keep the current JS file as small as possible, and move everything else into
+ // RequireJS module dependencies.
+ require(['js/capa/drag_and_drop/main'], function(Main) {
+ Main();
+ });
+
+// End of wrapper for RequireJS. As you can see, we are passing
+// namespaced Require JS variables to an anonymous function. Within
+// it, you can use the standard requirejs(), require(), and define()
+// functions as if they were in the global namespace.
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/base_image.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/base_image.js
new file mode 100644
index 00000000..736d8d34
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/base_image.js
@@ -0,0 +1,45 @@
+(function(requirejs, require, define) {
+ define(['edx-ui-toolkit/js/utils/html-utils'], function(HtmlUtils) {
+ return BaseImage;
+
+ function BaseImage(state) {
+ var $baseImageElContainer;
+
+ $baseImageElContainer = $(HtmlUtils.joinHtml(
+ HtmlUtils.HTML('
')
+ ).toString());
+
+ state.baseImageEl = $(' ', {
+ alt: gettext('Drop target image')
+ });
+
+ state.baseImageEl.attr('src', state.config.baseImage);
+ state.baseImageEl.load(function() {
+ $baseImageElContainer.css({
+ width: this.width,
+ height: this.height
+ });
+
+ state.baseImageEl.appendTo($baseImageElContainer);
+ $baseImageElContainer.appendTo(state.containerEl);
+
+ state.baseImageEl.mousedown(function(event) {
+ event.preventDefault();
+ });
+
+ state.baseImageLoaded = true;
+ });
+ state.baseImageEl.error(function() {
+ var errorMsg = HtmlUtils.joinHtml(
+ HtmlUtils.HTML(''),
+ HtmlUtils.HTML('ERROR: Image "'), state.config.baseImage, HtmlUtils.HTML('" was not found!'),
+ HtmlUtils.HTML(' ')
+ );
+ console.log('ERROR: Image "' + state.config.baseImage + '" was not found!');
+ HtmlUtils.setHtml($baseImageElContainer, errorMsg);
+ $baseImageElContainer.appendTo(state.containerEl);
+ });
+ }
+ }); // End-of: define([], function () {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/config_parser.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/config_parser.js
new file mode 100644
index 00000000..bbd574b0
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/config_parser.js
@@ -0,0 +1,271 @@
+(function(requirejs, require, define) {
+ define([], function() {
+ return configParser;
+
+ function configParser(state, config) {
+ state.config = {
+ draggables: [],
+ baseImage: '',
+ targets: [],
+ onePerTarget: null, // Specified by user. No default.
+ targetOutline: true,
+ labelBgColor: '#d6d6d6',
+ individualTargets: null, // Depends on 'targets'.
+ foundErrors: false // Whether or not we find errors while processing the config.
+ };
+
+ getDraggables(state, config);
+ getBaseImage(state, config);
+ getTargets(state, config);
+ getOnePerTarget(state, config);
+ getTargetOutline(state, config);
+ getLabelBgColor(state, config);
+
+ setIndividualTargets(state);
+
+ if (state.config.foundErrors !== false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function getDraggables(state, config) {
+ if (config.hasOwnProperty('draggables') === false) {
+ console.log('ERROR: "config" does not have a property "draggables".');
+ state.config.foundErrors = true;
+ } else if ($.isArray(config.draggables) === true) {
+ config.draggables.every(function(draggable) {
+ if (processDraggable(state, draggable) !== true) {
+ state.config.foundErrors = true;
+
+ // Exit immediately from .every() call.
+ return false;
+ }
+
+ // Continue to next .every() call.
+ return true;
+ });
+ } else {
+ console.log('ERROR: The type of config.draggables is no supported.');
+ state.config.foundErrors = true;
+ }
+ }
+
+ function getBaseImage(state, config) {
+ if (config.hasOwnProperty('base_image') === false) {
+ console.log('ERROR: "config" does not have a property "base_image".');
+ state.config.foundErrors = true;
+ } else if (typeof config.base_image === 'string') {
+ state.config.baseImage = config.base_image;
+ } else {
+ console.log('ERROR: Property config.base_image is not of type "string".');
+ state.config.foundErrors = true;
+ }
+ }
+
+ function getTargets(state, config) {
+ if (config.hasOwnProperty('targets') === false) {
+ // It is possible that no "targets" were specified. This is not an error.
+ // In this case the default value of "[]" (empty array) will be used.
+ // Draggables can be positioned anywhere on the image, and the server will
+ // get an answer in the form of (x, y) coordinates for each draggable.
+ } else if ($.isArray(config.targets) === true) {
+ config.targets.every(function(target) {
+ if (processTarget(state, target) !== true) {
+ state.config.foundErrors = true;
+
+ // Exit immediately from .every() call.
+ return false;
+ }
+
+ // Continue to next .every() call.
+ return true;
+ });
+ } else {
+ console.log('ERROR: Property config.targets is not of a supported type.');
+ state.config.foundErrors = true;
+ }
+ }
+
+ function getOnePerTarget(state, config) {
+ if (config.hasOwnProperty('one_per_target') === false) {
+ console.log('ERROR: "config" does not have a property "one_per_target".');
+ state.config.foundErrors = true;
+ } else if (typeof config.one_per_target === 'string') {
+ if (config.one_per_target.toLowerCase() === 'true') {
+ state.config.onePerTarget = true;
+ } else if (config.one_per_target.toLowerCase() === 'false') {
+ state.config.onePerTarget = false;
+ } else {
+ console.log('ERROR: Property config.one_per_target can either be "true", or "false".');
+ state.config.foundErrors = true;
+ }
+ } else {
+ console.log('ERROR: Property config.one_per_target is not of a supported type.');
+ state.config.foundErrors = true;
+ }
+ }
+
+ function getTargetOutline(state, config) {
+ // It is possible that no "target_outline" was specified. This is not an error.
+ // In this case the default value of 'true' (boolean) will be used.
+
+ if (config.hasOwnProperty('target_outline') === true) {
+ if (typeof config.target_outline === 'string') {
+ if (config.target_outline.toLowerCase() === 'true') {
+ state.config.targetOutline = true;
+ } else if (config.target_outline.toLowerCase() === 'false') {
+ state.config.targetOutline = false;
+ } else {
+ console.log('ERROR: Property config.target_outline can either be "true", or "false".');
+ state.config.foundErrors = true;
+ }
+ } else {
+ console.log('ERROR: Property config.target_outline is not of a supported type.');
+ state.config.foundErrors = true;
+ }
+ }
+ }
+
+ function getLabelBgColor(state, config) {
+ // It is possible that no "label_bg_color" was specified. This is not an error.
+ // In this case the default value of '#d6d6d6' (string) will be used.
+
+ if (config.hasOwnProperty('label_bg_color') === true) {
+ if (typeof config.label_bg_color === 'string') {
+ state.config.labelBgColor = config.label_bg_color;
+ } else {
+ console.log('ERROR: Property config.label_bg_color is not of a supported type.');
+ }
+ }
+ }
+
+ function setIndividualTargets(state) {
+ if (state.config.targets.length === 0) {
+ state.config.individualTargets = false;
+ } else {
+ state.config.individualTargets = true;
+ }
+ }
+
+ function processDraggable(state, obj) {
+ if (
+ (attrIsString(obj, 'id') === false)
+ || (attrIsString(obj, 'icon') === false)
+ || (attrIsString(obj, 'label') === false)
+
+ || (attrIsBoolean(obj, 'can_reuse', false) === false)
+
+ || (obj.hasOwnProperty('target_fields') === false)
+ ) {
+ return false;
+ }
+
+ // Check that all targets in the 'target_fields' property are proper target objects.
+ // We will be testing the return value from .every() call (it can be 'true' or 'false').
+ if (obj.target_fields.every(
+ function(targetObj) {
+ return processTarget(state, targetObj, false);
+ }
+ ) === false) {
+ return false;
+ }
+
+ state.config.draggables.push(obj);
+
+ return true;
+ }
+
+ // We need 'pushToState' parameter in order to simply test an object for the fact that it is a
+ // proper target (without pushing it to the 'state' object). When
+ //
+ // pushToState === false
+ //
+ // the object being tested is not going to be pushed to 'state'. The function will onyl return
+ // 'true' or 'false.
+ function processTarget(state, obj, pushToState) {
+ if (
+ (attrIsString(obj, 'id') === false)
+
+ || (attrIsInteger(obj, 'w') === false)
+ || (attrIsInteger(obj, 'h') === false)
+
+ || (attrIsInteger(obj, 'x') === false)
+ || (attrIsInteger(obj, 'y') === false)
+ ) {
+ return false;
+ }
+
+ if (pushToState !== false) {
+ state.config.targets.push(obj);
+ }
+
+ return true;
+ }
+
+ function attrIsString(obj, attr) {
+ if (obj.hasOwnProperty(attr) === false) {
+ console.log('ERROR: Attribute "obj.' + attr + '" is not present.');
+
+ return false;
+ } else if (typeof obj[attr] !== 'string') {
+ console.log('ERROR: Attribute "obj.' + attr + '" is not a string.');
+
+ return false;
+ }
+
+ return true;
+ }
+
+ function attrIsInteger(obj, attr) {
+ var tempInt;
+
+ if (obj.hasOwnProperty(attr) === false) {
+ console.log('ERROR: Attribute "obj.' + attr + '" is not present.');
+
+ return false;
+ }
+
+ tempInt = parseInt(obj[attr], 10);
+
+ if (isFinite(tempInt) === false) {
+ console.log('ERROR: Attribute "obj.' + attr + '" is not an integer.');
+
+ return false;
+ }
+
+ obj[attr] = tempInt;
+
+ return true;
+ }
+
+ function attrIsBoolean(obj, attr, defaultVal) {
+ if (obj.hasOwnProperty(attr) === false) {
+ if (defaultVal === undefined) {
+ console.log('ERROR: Attribute "obj.' + attr + '" is not present.');
+
+ return false;
+ } else {
+ obj[attr] = defaultVal;
+
+ return true;
+ }
+ }
+
+ if (obj[attr] === '') {
+ obj[attr] = defaultVal;
+ } else if ((obj[attr] === 'false') || (obj[attr] === false)) {
+ obj[attr] = false;
+ } else if ((obj[attr] === 'true') || (obj[attr] === true)) {
+ obj[attr] = true;
+ } else {
+ console.log('ERROR: Attribute "obj.' + attr + '" is not a boolean.');
+
+ return false;
+ }
+
+ return true;
+ }
+ }); // End-of: define([], function () {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/container.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/container.js
new file mode 100644
index 00000000..35107a8b
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/container.js
@@ -0,0 +1,13 @@
+(function(requirejs, require, define) {
+ define(['edx-ui-toolkit/js/utils/html-utils'], function(HtmlUtils) {
+ return Container;
+
+ function Container(state) {
+ state.containerEl = $(
+ '
'
+ );
+
+ $('#inputtype_' + state.problemId).before(HtmlUtils.HTML(state.containerEl).toString());
+ }
+ }); // End-of: define([], function () {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/draggable_events.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/draggable_events.js
new file mode 100644
index 00000000..667cd54f
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/draggable_events.js
@@ -0,0 +1,143 @@
+(function(requirejs, require, define) {
+ define([], function() {
+ return {
+ attachMouseEventsTo: function(element) {
+ var self;
+
+ self = this;
+
+ this[element].mousedown(function(event) {
+ self.mouseDown(event);
+ });
+ this[element].mouseup(function(event) {
+ self.mouseUp(event);
+ });
+ this[element].mousemove(function(event) {
+ self.mouseMove(event);
+ });
+ },
+
+ mouseDown: function(event) {
+ if (this.mousePressed === false) {
+ // So that the browser does not perform a default drag.
+ // If we don't do this, each drag operation will
+ // potentially cause the highlghting of the dragged element.
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (this.numDraggablesOnMe > 0) {
+ return;
+ }
+
+ // If this draggable is just being dragged out of the
+ // container, we must perform some additional tasks.
+ if (this.inContainer === true) {
+ if ((this.isReusable === true) && (this.isOriginal === true)) {
+ this.makeDraggableCopy(function(draggableCopy) {
+ draggableCopy.mouseDown(event);
+ });
+
+ return;
+ }
+
+ if (this.isOriginal === true) {
+ this.containerEl.hide();
+ this.iconEl.detach();
+ }
+
+ if (this.iconImgEl !== null) {
+ this.iconImgEl.css({
+ width: this.iconWidth,
+ height: this.iconHeight
+ });
+ }
+ this.iconEl.css({
+ 'background-color': this.iconElBGColor,
+ 'padding-left': this.iconElPadding,
+ 'padding-right': this.iconElPadding,
+ border: this.iconElBorder,
+ width: this.iconWidth,
+ height: this.iconHeight,
+ left: event.pageX - this.state.baseImageEl.offset().left - this.iconWidth * 0.5 - this.iconElLeftOffset,
+ top: event.pageY - this.state.baseImageEl.offset().top - this.iconHeight * 0.5
+ });
+ this.iconEl.appendTo(this.state.baseImageEl.parent());
+
+ if (this.labelEl !== null) {
+ if (this.isOriginal === true) {
+ this.labelEl.detach();
+ }
+ this.labelEl.css({
+ 'background-color': this.state.config.labelBgColor,
+ 'padding-left': 8,
+ 'padding-right': 8,
+ border: '1px solid black',
+ left: event.pageX - this.state.baseImageEl.offset().left - this.labelWidth * 0.5 - 9, // Account for padding, border.
+ top: event.pageY - this.state.baseImageEl.offset().top + this.iconHeight * 0.5 + 5
+ });
+ this.labelEl.appendTo(this.state.baseImageEl.parent());
+ }
+
+ this.inContainer = false;
+ if (this.isOriginal === true) {
+ this.state.numDraggablesInSlider -= 1;
+ }
+ // SR: global "screen reader" object in accessibility_tools.js
+ window.SR.readText(gettext('dragging out of slider'));
+ } else {
+ window.SR.readText(gettext('dragging'));
+ }
+
+ this.zIndex = 1000;
+ this.iconEl.css('z-index', '1000');
+ if (this.labelEl !== null) {
+ this.labelEl.css('z-index', '1000');
+ }
+ this.iconEl.attr('aria-grabbed', 'true').focus();
+ this.toggleTargets(true);
+ this.mousePressed = true;
+ this.state.currentMovingDraggable = this;
+ }
+ },
+
+ mouseUp: function() {
+ if (this.mousePressed === true) {
+ this.state.currentMovingDraggable = null;
+ this.iconEl.attr('aria-grabbed', 'false');
+
+ this.checkLandingElement();
+ if (this.inContainer === true) {
+ window.SR.readText(gettext('dropped in slider'));
+ } else {
+ window.SR.readText(gettext('dropped on target'));
+ }
+ this.toggleTargets(false);
+ }
+ },
+
+ mouseMove: function(event) {
+ if (this.mousePressed === true) {
+ // Because we have also attached a 'mousemove' event to the
+ // 'document' (that will do the same thing), let's tell the
+ // browser not to bubble up this event. The attached event
+ // on the 'document' will only be triggered when the mouse
+ // pointer leaves the draggable while it is in the middle
+ // of a drag operation (user moves the mouse very quickly).
+ event.stopPropagation();
+
+ this.iconEl.css({
+ left: event.pageX - this.state.baseImageEl.offset().left - this.iconWidth * 0.5 - this.iconElLeftOffset,
+ top: event.pageY - this.state.baseImageEl.offset().top - this.iconHeight * 0.5
+ });
+
+ if (this.labelEl !== null) {
+ this.labelEl.css({
+ left: event.pageX - this.state.baseImageEl.offset().left - this.labelWidth * 0.5 - 9, // Acoount for padding, border.
+ top: event.pageY - this.state.baseImageEl.offset().top + this.iconHeight * 0.5 + 5
+ });
+ }
+ }
+ }
+ }; // End-of: return {
+ }); // End-of: define([], function () {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/draggable_logic.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/draggable_logic.js
new file mode 100644
index 00000000..5de987fd
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/draggable_logic.js
@@ -0,0 +1,400 @@
+(function(requirejs, require, define) {
+ define(['js/capa/drag_and_drop/update_input', 'js/capa/drag_and_drop/targets'], function(updateInput, Targets) {
+ return {
+ moveDraggableTo: function(moveType, target, funcCallback) {
+ var self, offset;
+
+ if (this.hasLoaded === false) {
+ self = this;
+
+ setTimeout(function() {
+ self.moveDraggableTo(moveType, target, funcCallback);
+ }, 50);
+
+ return;
+ }
+
+ if ((this.isReusable === true) && (this.isOriginal === true)) {
+ this.makeDraggableCopy(function(draggableCopy) {
+ draggableCopy.moveDraggableTo(moveType, target, funcCallback);
+ });
+
+ return;
+ }
+
+ offset = 0;
+ if (this.state.config.targetOutline === true) {
+ offset = 1;
+ }
+
+ this.inContainer = false;
+
+ if (this.isOriginal === true) {
+ this.containerEl.hide();
+ this.iconEl.detach();
+ }
+
+ if (this.iconImgEl !== null) {
+ this.iconImgEl.css({
+ width: this.iconWidth,
+ height: this.iconHeight
+ });
+ }
+
+ this.iconEl.css({
+ 'background-color': this.iconElBGColor,
+ 'padding-left': this.iconElPadding,
+ 'padding-right': this.iconElPadding,
+ border: this.iconElBorder,
+ width: this.iconWidth,
+ height: this.iconHeight
+ });
+ if (moveType === 'target') {
+ this.iconEl.css({
+ left: target.offset.left + 0.5 * target.w - this.iconWidth * 0.5 + offset - this.iconElLeftOffset,
+ top: target.offset.top + 0.5 * target.h - this.iconHeight * 0.5 + offset
+ });
+ } else {
+ this.iconEl.css({
+ left: target.x - this.iconWidth * 0.5 + offset - this.iconElLeftOffset,
+ top: target.y - this.iconHeight * 0.5 + offset
+ });
+ }
+ this.iconEl.appendTo(this.state.baseImageEl.parent());
+
+ if (this.labelEl !== null) {
+ if (this.isOriginal === true) {
+ this.labelEl.detach();
+ }
+ this.labelEl.css({
+ 'background-color': this.state.config.labelBgColor,
+ 'padding-left': 8,
+ 'padding-right': 8,
+ border: '1px solid black'
+ });
+ if (moveType === 'target') {
+ this.labelEl.css({
+ left: target.offset.left + 0.5 * target.w - this.labelWidth * 0.5 + offset - 9, // Account for padding, border.
+ top: target.offset.top + 0.5 * target.h + this.iconHeight * 0.5 + 5 + offset
+ });
+ } else {
+ this.labelEl.css({
+ left: target.x - this.labelWidth * 0.5 + offset - 9, // Account for padding, border.
+ top: target.y - this.iconHeight * 0.5 + this.iconHeight + 5 + offset
+ });
+ }
+ this.labelEl.appendTo(this.state.baseImageEl.parent());
+ }
+
+ if (moveType === 'target') {
+ target.addDraggable(this);
+ } else {
+ this.x = target.x;
+ this.y = target.y;
+ }
+
+ this.zIndex = 1000;
+ this.correctZIndexes();
+
+ Targets.initializeTargetField(this);
+
+ if (this.isOriginal === true) {
+ this.state.numDraggablesInSlider -= 1;
+ this.state.updateArrowOpacity();
+ }
+
+ if ($.isFunction(funcCallback) === true) {
+ funcCallback();
+ }
+ },
+
+ // At this point the mouse was realeased, and we need to check
+ // where the draggable eneded up. Based on several things, we
+ // will either move the draggable back to the slider, or update
+ // the input with the user's answer (X-Y position of the draggable,
+ // or the ID of the target where it landed.
+ checkLandingElement: function() {
+ var positionIE;
+
+ this.mousePressed = false;
+ positionIE = this.iconEl.position();
+
+ if (this.state.config.individualTargets === true) {
+ if (this.checkIfOnTarget(positionIE) === true) {
+ this.correctZIndexes();
+
+ Targets.initializeTargetField(this);
+ } else {
+ if (this.onTarget !== null) {
+ this.onTarget.removeDraggable(this);
+ }
+
+ this.moveBackToSlider();
+
+ if (this.isOriginal === true) {
+ this.state.numDraggablesInSlider += 1;
+ }
+ }
+ } else {
+ if (
+ (positionIE.left < 0)
+ || (positionIE.left + this.iconWidth > this.state.baseImageEl.width())
+ || (positionIE.top < 0)
+ || (positionIE.top + this.iconHeight > this.state.baseImageEl.height())
+ ) {
+ this.moveBackToSlider();
+
+ this.x = -1;
+ this.y = -1;
+
+ if (this.isOriginal === true) {
+ this.state.numDraggablesInSlider += 1;
+ }
+ } else {
+ this.correctZIndexes();
+
+ this.x = positionIE.left + this.iconWidth * 0.5;
+ this.y = positionIE.top + this.iconHeight * 0.5;
+
+ Targets.initializeTargetField(this);
+ }
+ }
+
+ if (this.isOriginal === true) {
+ this.state.updateArrowOpacity();
+ }
+ updateInput.update(this.state);
+ },
+
+ // Determine if a draggable, after it was relased, ends up on a
+ // target. We do this by iterating over all of the targets, and
+ // for each one we check whether the draggable's center is
+ // within the target's dimensions.
+ //
+ // positionIE is the object as returned by
+ //
+ // this.iconEl.position()
+ checkIfOnTarget: function(positionIE) {
+ var c1, target;
+
+ for (c1 = 0; c1 < this.state.targets.length; c1 += 1) {
+ target = this.state.targets[c1];
+
+ // If only one draggable per target is allowed, and
+ // the current target already has a draggable on it
+ // (with an ID different from the one we are checking
+ // against), then go to next target.
+ if (
+ (this.state.config.onePerTarget === true)
+ && (target.draggableList.length === 1)
+ && (target.draggableList[0].uniqueId !== this.uniqueId)
+ ) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ // If the target is on a draggable (from target field), we must make sure that
+ // this draggable is not the same as "this" one.
+ if ((target.type === 'on_drag') && (target.draggableObj.uniqueId === this.uniqueId)) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ // Check if the draggable's center coordinate is within
+ // the target's dimensions. If not, go to next target.
+ if (
+ (positionIE.top + this.iconHeight * 0.5 < target.offset.top)
+ || (positionIE.top + this.iconHeight * 0.5 > target.offset.top + target.h)
+ || (positionIE.left + this.iconWidth * 0.5 < target.offset.left)
+ || (positionIE.left + this.iconWidth * 0.5 > target.offset.left + target.w)
+ ) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ // If the draggable was moved from one target to
+ // another, then we need to remove it from the
+ // previous target's draggables list, and add it to the
+ // new target's draggables list.
+ if ((this.onTarget !== null) && (this.onTarget.uniqueId !== target.uniqueId)) {
+ this.onTarget.removeDraggable(this);
+ target.addDraggable(this);
+ // eslint-disable-next-line brace-style
+ }
+ // If the draggable was moved from the slider to a
+ // target, remember the target, and add ID to the
+ // target's draggables list.
+ else if (this.onTarget === null) {
+ target.addDraggable(this);
+ }
+
+ // Reposition the draggable so that it's center
+ // coincides with the center of the target.
+ this.snapToTarget(target);
+
+ // Target was found.
+ return true;
+ }
+
+ // Target was not found.
+ return false;
+ },
+
+ toggleTargets: function(isEnabled) {
+ var effect = isEnabled ? 'move' : null;
+
+ this.state.baseImageEl.attr('aria-dropeffect', effect);
+ $.each(this.state.targets, function(index, target) {
+ target.targetEl.attr('aria-dropeffect', effect);
+ });
+ },
+
+ snapToTarget: function(target) {
+ var offset;
+
+ offset = 0;
+ if (this.state.config.targetOutline === true) {
+ offset = 1;
+ }
+
+ this.iconEl.css({
+ left: target.offset.left + 0.5 * target.w - this.iconWidth * 0.5 + offset - this.iconElLeftOffset,
+ top: target.offset.top + 0.5 * target.h - this.iconHeight * 0.5 + offset
+ });
+
+ if (this.labelEl !== null) {
+ this.labelEl.css({
+ left: target.offset.left + 0.5 * target.w - this.labelWidth * 0.5 + offset - 9, // Acoount for padding, border.
+ top: target.offset.top + 0.5 * target.h + this.iconHeight * 0.5 + 5 + offset
+ });
+ }
+ },
+
+ // Go through all of the draggables subtract 1 from the z-index
+ // of all whose z-index is higher than the old z-index of the
+ // current element. After, set the z-index of the current
+ // element to 1 + N (where N is the number of draggables - i.e.
+ // the highest z-index possible).
+ //
+ // This will make sure that after releasing a draggable, it
+ // will be on top of all of the other draggables. Also, the
+ // ordering of the visibility (z-index) of the other draggables
+ // will not change.
+ correctZIndexes: function() {
+ var c1, highestZIndex;
+
+ highestZIndex = -10000;
+
+ if (this.state.config.individualTargets === true) {
+ if (this.onTarget.draggableList.length > 0) {
+ for (c1 = 0; c1 < this.onTarget.draggableList.length; c1 += 1) {
+ if (
+ (this.onTarget.draggableList[c1].zIndex > highestZIndex)
+ && (this.onTarget.draggableList[c1].zIndex !== 1000)
+ ) {
+ highestZIndex = this.onTarget.draggableList[c1].zIndex;
+ }
+ }
+ } else {
+ highestZIndex = 0;
+ }
+ } else {
+ for (c1 = 0; c1 < this.state.draggables.length; c1++) {
+ if (this.inContainer === false) {
+ if (
+ (this.state.draggables[c1].zIndex > highestZIndex)
+ && (this.state.draggables[c1].zIndex !== 1000)
+ ) {
+ highestZIndex = this.state.draggables[c1].zIndex;
+ }
+ }
+ }
+ }
+
+ if (highestZIndex === -10000) {
+ highestZIndex = 0;
+ }
+
+ this.zIndex = highestZIndex + 1;
+
+ this.iconEl.css('z-index', this.zIndex);
+ if (this.labelEl !== null) {
+ this.labelEl.css('z-index', this.zIndex);
+ }
+ },
+
+ // If a draggable was released in a wrong positione, we will
+ // move it back to the slider, placing it in the same position
+ // that it was dragged out of.
+ moveBackToSlider: function() {
+ var c1;
+
+ Targets.destroyTargetField(this);
+
+ if (this.isOriginal === false) {
+ this.iconEl.remove();
+ if (this.labelEl !== null) {
+ this.labelEl.remove();
+ }
+
+ this.state.draggables.splice(this.stateDraggablesIndex, 1);
+
+ for (c1 = 0; c1 < this.state.draggables.length; c1 += 1) {
+ if (this.state.draggables[c1].stateDraggablesIndex > this.stateDraggablesIndex) {
+ this.state.draggables[c1].stateDraggablesIndex -= 1;
+ }
+ }
+
+ return;
+ }
+
+ this.containerEl.show();
+ this.zIndex = 1;
+
+ this.iconEl.detach();
+ if (this.iconImgEl !== null) {
+ this.iconImgEl.css({
+ width: this.iconWidthSmall,
+ height: this.iconHeightSmall
+ });
+ }
+ this.iconEl.css({
+ border: 'none',
+ 'background-color': 'transparent',
+ 'padding-left': 0,
+ 'padding-right': 0,
+ 'z-index': this.zIndex,
+ width: this.iconWidthSmall,
+ height: this.iconHeightSmall,
+ left: 50 - this.iconWidthSmall * 0.5,
+
+ // Before:
+ // 'top': ((this.labelEl !== null) ? (100 - this.iconHeightSmall - 25) * 0.5 : 50 - this.iconHeightSmall * 0.5)
+ // After:
+ top: ((this.labelEl !== null) ? 37.5 : 50.0) - 0.5 * this.iconHeightSmall
+ });
+ this.iconEl.appendTo(this.containerEl);
+
+ if (this.labelEl !== null) {
+ this.labelEl.detach();
+ this.labelEl.css({
+ border: 'none',
+ 'background-color': 'transparent',
+ 'padding-left': 0,
+ 'padding-right': 0,
+ 'z-index': this.zIndex,
+ left: 50 - this.labelWidth * 0.5,
+
+ // Before:
+ // 'top': (100 - this.iconHeightSmall - 25) * 0.5 + this.iconHeightSmall + 5
+ // After:
+ top: 42.5 + 0.5 * this.iconHeightSmall
+ });
+ this.labelEl.appendTo(this.containerEl);
+ }
+
+ this.inContainer = true;
+ }
+ }; // End-of: return {
+ }); // End-of: define(['update_input', 'targets'], function (updateInput, Targets) {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/draggables.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/draggables.js
new file mode 100644
index 00000000..13e26c28
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/draggables.js
@@ -0,0 +1,281 @@
+(function(requirejs, require, define) {
+ define(['js/capa/drag_and_drop/draggable_events', 'js/capa/drag_and_drop/draggable_logic',
+ 'edx-ui-toolkit/js/utils/html-utils'],
+ function(draggableEvents, draggableLogic, HtmlUtils) {
+ return {
+ init: init
+ };
+
+ function init(state) {
+ state.config.draggables.every(function(draggable) {
+ processDraggable(state, draggable);
+
+ return true;
+ });
+ }
+
+ function makeDraggableCopy(callbackFunc) {
+ var draggableObj, property;
+
+ // Make a full proper copy of the draggable object, with some modifications.
+ draggableObj = {};
+ for (property in this) {
+ if (this.hasOwnProperty(property) === true) {
+ draggableObj[property] = this[property];
+ }
+ }
+ // The modifications to the draggable copy.
+ draggableObj.isOriginal = false; // This new draggable is a copy.
+ draggableObj.uniqueId = draggableObj.state.getUniqueId(); // Is newly set.
+ draggableObj.stateDraggablesIndex = null; // Will be set.
+ draggableObj.containerEl = null; // Not needed, since a copy will never return to a container element.
+ draggableObj.iconEl = null; // Will be created.
+ draggableObj.iconImgEl = null; // Will be created.
+ draggableObj.labelEl = null; // Will be created.
+ draggableObj.targetField = []; // Will be populated.
+
+ // Create DOM elements and attach events.
+ if (draggableObj.originalConfigObj.icon.length > 0) {
+ draggableObj.iconEl = $('
');
+ draggableObj.iconImgEl = $(' ');
+ draggableObj.iconImgEl.attr('src', draggableObj.originalConfigObj.icon);
+ draggableObj.iconImgEl.load(function() {
+ draggableObj.iconEl.css({
+ position: 'absolute',
+ width: draggableObj.iconWidthSmall,
+ height: draggableObj.iconHeightSmall,
+ left: 50 - draggableObj.iconWidthSmall * 0.5,
+ top: ((draggableObj.originalConfigObj.label.length > 0) ? 5 : 50 - draggableObj.iconHeightSmall * 0.5)
+ });
+ draggableObj.iconImgEl.css({
+ position: 'absolute',
+ width: draggableObj.iconWidthSmall,
+ height: draggableObj.iconHeightSmall,
+ left: 0,
+ top: 0
+ });
+ draggableObj.iconImgEl.appendTo(draggableObj.iconEl);
+
+ if (draggableObj.originalConfigObj.label.length > 0) {
+ draggableObj.labelEl = $(HtmlUtils.joinHtml(
+ HtmlUtils.HTML(
+ ''
+ ),
+ draggableObj.originalConfigObj.label,
+ HtmlUtils.HTML('
')
+ ).toString());
+ draggableObj.labelEl.css({
+ left: 50 - draggableObj.labelWidth * 0.5,
+ top: 5 + draggableObj.iconHeightSmall + 5
+ });
+
+ draggableObj.attachMouseEventsTo('labelEl');
+ }
+
+ draggableObj.attachMouseEventsTo('iconEl');
+
+ draggableObj.stateDraggablesIndex = draggableObj.state.draggables.push(draggableObj) - 1;
+
+ setTimeout(function() {
+ callbackFunc(draggableObj);
+ }, 0);
+ });
+ } else {
+ if (draggableObj.originalConfigObj.label.length > 0) {
+ draggableObj.iconEl = $(HtmlUtils.joinHtml(
+ HtmlUtils.HTML(''),
+ draggableObj.originalConfigObj.label,
+ HtmlUtils.HTML('
')
+ ).toString());
+ draggableObj.iconEl.css({
+ left: 50 - draggableObj.iconWidthSmall * 0.5,
+ top: 50 - draggableObj.iconHeightSmall * 0.5
+ });
+
+ draggableObj.attachMouseEventsTo('iconEl');
+
+ draggableObj.stateDraggablesIndex = draggableObj.state.draggables.push(draggableObj) - 1;
+
+ setTimeout(function() {
+ callbackFunc(draggableObj);
+ }, 0);
+ }
+ }
+ }
+
+ function processDraggable(state, obj) {
+ var draggableObj;
+
+ draggableObj = {
+ uniqueId: state.getUniqueId(),
+ originalConfigObj: obj,
+ stateDraggablesIndex: null,
+ id: obj.id,
+ isReusable: obj.can_reuse,
+ isOriginal: true,
+ x: -1,
+ y: -1,
+ zIndex: 1,
+ containerEl: null,
+ iconEl: null,
+ iconImgEl: null,
+ iconElBGColor: null,
+ iconElPadding: null,
+ iconElBorder: null,
+ iconElLeftOffset: null,
+ iconWidth: null,
+ iconHeight: null,
+ iconWidthSmall: null,
+ iconHeightSmall: null,
+ labelEl: null,
+ labelWidth: null,
+ hasLoaded: false,
+ inContainer: true,
+ mousePressed: false,
+ onTarget: null,
+ onTargetIndex: null,
+ state: state,
+
+ mouseDown: draggableEvents.mouseDown,
+ mouseUp: draggableEvents.mouseUp,
+ mouseMove: draggableEvents.mouseMove,
+
+ checkLandingElement: draggableLogic.checkLandingElement,
+ checkIfOnTarget: draggableLogic.checkIfOnTarget,
+ snapToTarget: draggableLogic.snapToTarget,
+ correctZIndexes: draggableLogic.correctZIndexes,
+ moveBackToSlider: draggableLogic.moveBackToSlider,
+ moveDraggableTo: draggableLogic.moveDraggableTo,
+ toggleTargets: draggableLogic.toggleTargets,
+
+ makeDraggableCopy: makeDraggableCopy,
+
+ attachMouseEventsTo: draggableEvents.attachMouseEventsTo,
+
+ targetField: [],
+ numDraggablesOnMe: 0
+ };
+
+ draggableObj.containerEl = $(HtmlUtils.joinHtml(
+ HtmlUtils.HTML('
')
+ ).toString());
+
+ draggableObj.containerEl.appendTo(state.sliderEl);
+
+ if (obj.icon.length > 0) {
+ draggableObj.iconElBGColor = 'transparent';
+ draggableObj.iconElPadding = 0;
+ draggableObj.iconElBorder = 'none';
+ draggableObj.iconElLeftOffset = 0;
+
+ draggableObj.iconEl = $('
');
+
+ draggableObj.iconImgEl = $(' ');
+ draggableObj.iconImgEl.attr('src', obj.icon);
+ draggableObj.iconImgEl.load(function() {
+ draggableObj.iconWidth = this.width;
+ draggableObj.iconHeight = this.height;
+
+ if (draggableObj.iconWidth >= draggableObj.iconHeight) {
+ draggableObj.iconWidthSmall = 60;
+ draggableObj.iconHeightSmall = draggableObj.iconWidthSmall * (draggableObj.iconHeight / draggableObj.iconWidth);
+ } else {
+ draggableObj.iconHeightSmall = 60;
+ draggableObj.iconWidthSmall = draggableObj.iconHeightSmall * (draggableObj.iconWidth / draggableObj.iconHeight);
+ }
+
+ draggableObj.iconEl.css({
+ position: 'absolute',
+ width: draggableObj.iconWidthSmall,
+ height: draggableObj.iconHeightSmall,
+ left: 50 - draggableObj.iconWidthSmall * 0.5,
+
+ // Before:
+ // 'top': ((obj.label.length > 0) ? (100 - draggableObj.iconHeightSmall - 25) * 0.5 : 50 - draggableObj.iconHeightSmall * 0.5)
+ // After:
+ top: ((obj.label.length > 0) ? 37.5 : 50.0) - 0.5 * draggableObj.iconHeightSmall
+ });
+ draggableObj.iconImgEl.css({
+ position: 'absolute',
+ width: draggableObj.iconWidthSmall,
+ height: draggableObj.iconHeightSmall,
+ left: 0,
+ top: 0
+ });
+ draggableObj.iconImgEl.appendTo(draggableObj.iconEl);
+ draggableObj.iconEl.appendTo(draggableObj.containerEl);
+
+ if (obj.label.length > 0) {
+ draggableObj.labelEl = $(HtmlUtils.joinHtml(
+ HtmlUtils.HTML(
+ ''
+ ),
+ obj.label,
+ HtmlUtils.HTML('
')
+ ).toString());
+
+ draggableObj.labelEl.appendTo(draggableObj.containerEl);
+ draggableObj.labelWidth = draggableObj.labelEl.width();
+ draggableObj.labelEl.css({
+ left: 50 - draggableObj.labelWidth * 0.5,
+
+ // Before:
+ // 'top': (100 - this.iconHeightSmall - 25) * 0.5 + this.iconHeightSmall + 5
+ // After:
+ top: 42.5 + 0.5 * draggableObj.iconHeightSmall
+ });
+
+ draggableObj.attachMouseEventsTo('labelEl');
+ }
+
+ draggableObj.hasLoaded = true;
+ });
+ } else {
+ // To make life easier, if there is no icon, but there is a
+ // label, we will create a label and store it as if it was an
+ // icon. All the existing code will work, and the user will
+ // see a label instead of an icon.
+ if (obj.label.length > 0) {
+ draggableObj.iconElBGColor = state.config.labelBgColor;
+ draggableObj.iconElPadding = 8;
+ draggableObj.iconElBorder = '1px solid black';
+ draggableObj.iconElLeftOffset = 9;
+
+ draggableObj.iconEl = $(HtmlUtils.joinHtml(
+ HtmlUtils.HTML(
+ ''),
+ obj.label,
+ HtmlUtils.HTML('
')
+ ).toString());
+
+ draggableObj.iconEl.appendTo(draggableObj.containerEl);
+
+ draggableObj.iconWidth = draggableObj.iconEl.width() + 1;
+ draggableObj.iconHeight = draggableObj.iconEl.height();
+ draggableObj.iconWidthSmall = draggableObj.iconWidth;
+ draggableObj.iconHeightSmall = draggableObj.iconHeight;
+
+ draggableObj.iconEl.css({
+ left: 50 - draggableObj.iconWidthSmall * 0.5,
+ top: 50 - draggableObj.iconHeightSmall * 0.5
+ });
+
+ draggableObj.hasLoaded = true;
+ } else {
+ // If no icon and no label, don't create a draggable.
+ return;
+ }
+ }
+
+ draggableObj.attachMouseEventsTo('iconEl');
+ draggableObj.attachMouseEventsTo('containerEl');
+
+ state.numDraggablesInSlider += 1;
+ draggableObj.stateDraggablesIndex = state.draggables.push(draggableObj) - 1;
+ }
+ }); // End-of: define(['draggable_events', 'draggable_logic'], function (draggableEvents, draggableLogic) {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/main.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/main.js
new file mode 100644
index 00000000..3cd15971
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/main.js
@@ -0,0 +1,107 @@
+(function(requirejs, require, define) {
+ define(
+ ['js/capa/drag_and_drop/state',
+ 'js/capa/drag_and_drop/config_parser', 'js/capa/drag_and_drop/container',
+ 'js/capa/drag_and_drop/base_image', 'js/capa/drag_and_drop/scroller',
+ 'js/capa/drag_and_drop/draggables', 'js/capa/drag_and_drop/targets',
+ 'js/capa/drag_and_drop/update_input'],
+ function(State, configParser, Container, BaseImage, Scroller, Draggables, Targets, updateInput) {
+ return Main;
+
+ function Main() {
+ // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/every
+ //
+ // Array.prototype.every is a recent addition to the ECMA-262 standard; as such it may not be present in
+ // other implementations of the standard.
+ if (!Array.prototype.every) {
+ // eslint-disable-next-line no-extend-native
+ Array.prototype.every = function(fun /* , thisp */) {
+ var thisp, t, len, i;
+
+ if (this == null) {
+ throw new TypeError();
+ }
+
+ t = Object(this);
+ // eslint-disable-next-line no-bitwise
+ len = t.length >>> 0;
+ if (typeof fun !== 'function') {
+ throw new TypeError();
+ }
+
+ thisp = arguments[1];
+
+ for (i = 0; i < len; i++) {
+ if (i in t && !fun.call(thisp, t[i], i, t)) {
+ return false;
+ }
+ }
+
+ return true;
+ };
+ }
+
+ $('.drag_and_drop_problem_div').each(processProblem);
+ }
+
+ // $(value) - get the element of the entire problem
+ function processProblem(index, value) {
+ var problemId, config, state;
+
+ if ($(value).attr('data-problem-processed') === 'true') {
+ // This problem was already processed by us before, so we will
+ // skip it.
+
+ return;
+ }
+ $(value).attr('data-problem-processed', 'true');
+
+ problemId = $(value).attr('data-plain-id');
+ if (typeof problemId !== 'string') {
+ console.log('ERROR: Could not find the ID of the problem DOM element.');
+
+ return;
+ }
+
+ try {
+ config = JSON.parse($('#drag_and_drop_json_' + problemId).html());
+ } catch (err) {
+ console.log('ERROR: Could not parse the JSON configuration options.');
+ console.log('Error message: "' + err.message + '".');
+
+ return;
+ }
+
+ state = State(problemId);
+
+ if (configParser(state, config) !== true) {
+ console.log('ERROR: Could not make sense of the JSON configuration options.');
+
+ return;
+ }
+
+ Container(state);
+ BaseImage(state);
+
+ (function addContent() {
+ if (state.baseImageLoaded !== true) {
+ setTimeout(addContent, 50);
+
+ return;
+ }
+
+ Targets.initializeBaseTargets(state);
+ Scroller(state);
+ Draggables.init(state);
+
+ state.updateArrowOpacity();
+
+ // Update the input element, checking first that it is not filled with
+ // an answer from the server.
+ if (updateInput.check(state) === false) {
+ updateInput.update(state);
+ }
+ }());
+ }
+ }); // End-of: define(
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/scroller.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/scroller.js
new file mode 100644
index 00000000..eb664614
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/scroller.js
@@ -0,0 +1,162 @@
+(function(requirejs, require, define) {
+ define(['edx-ui-toolkit/js/utils/html-utils'], function(HtmlUtils) {
+ return Scroller;
+
+ function Scroller(state) {
+ var $parentEl, $moveLeftEl, $showEl, $moveRightEl, showElLeftMargin;
+
+ $parentEl = $(HtmlUtils.HTML(
+ '
'
+ ).toString());
+
+ $moveLeftEl = $(HtmlUtils.joinHtml(
+ HtmlUtils.HTML(''),
+ HtmlUtils.HTML('
'),
+ HtmlUtils.HTML('
'),
+ HtmlUtils.HTML('
')
+ ).toString());
+ $moveLeftEl.appendTo($parentEl);
+
+ // The below is necessary to prevent the browser thinking that we want
+ // to perform a drag operation, or a highlight operation. If we don't
+ // do this, the browser will then highlight with a gray shade the
+ // element.
+ $moveLeftEl.mousemove(function(event) { event.preventDefault(); });
+ $moveLeftEl.mousedown(function(event) { event.preventDefault(); });
+
+ // This event will be responsible for moving the scroller left.
+ // Hidden draggables will be shown.
+ $moveLeftEl.mouseup(function(event) {
+ event.preventDefault();
+
+ // When there are no more hidden draggables, prevent from
+ // scrolling infinitely.
+ if (showElLeftMargin > -102) {
+ return;
+ }
+
+ showElLeftMargin += 102;
+
+ // We scroll by changing the 'margin-left' CSS property smoothly.
+ state.sliderEl.animate({
+ 'margin-left': showElLeftMargin + 'px'
+ }, 100, function() {
+ updateArrowOpacity();
+ });
+ });
+
+ $showEl = $(HtmlUtils.HTML(
+ '
'
+ ).toString());
+ $showEl.appendTo($parentEl);
+
+ showElLeftMargin = 0;
+
+ // Element where the draggables will be contained. It is very long
+ // so that any SANE number of draggables will fit in a single row. It
+ // will be contained in a parent element whose 'overflow' CSS value
+ // will be hidden, preventing the long row from fully being visible.
+ // eslint-disable-next-line no-param-reassign
+ state.sliderEl = $(HtmlUtils.joinHtml(
+ HtmlUtils.HTML('
')
+ ).toString());
+ state.sliderEl.appendTo($showEl);
+
+ state.sliderEl.mousedown(function(event) {
+ event.preventDefault();
+ });
+
+ $moveRightEl = $(HtmlUtils.joinHtml(
+ HtmlUtils.HTML(''),
+ HtmlUtils.HTML('
'),
+ HtmlUtils.HTML('
'),
+ HtmlUtils.HTML('
')
+ ).toString());
+ $moveRightEl.appendTo($parentEl);
+
+ // The below is necessary to prevent the browser thinking that we want
+ // to perform a drag operation, or a highlight operation. If we don't
+ // do this, the browser will then highlight with a gray shade the
+ // element.
+ $moveRightEl.mousemove(function(event) { event.preventDefault(); });
+ $moveRightEl.mousedown(function(event) { event.preventDefault(); });
+
+ // This event will be responsible for moving the scroller right.
+ // Hidden draggables will be shown.
+ $moveRightEl.mouseup(function(event) {
+ event.preventDefault();
+
+ // When there are no more hidden draggables, prevent from
+ // scrolling infinitely.
+ if (showElLeftMargin < -102 * (state.numDraggablesInSlider - 6)) {
+ return;
+ }
+
+ showElLeftMargin -= 102;
+
+ // We scroll by changing the 'margin-left' CSS property smoothly.
+ state.sliderEl.animate({
+ 'margin-left': showElLeftMargin + 'px'
+ }, 100, function() {
+ updateArrowOpacity();
+ });
+ });
+
+ $parentEl.appendTo(state.containerEl);
+
+ // Make the function available throughout the application. We need to
+ // call it in several places:
+ //
+ // 1.) When initially reading answer from server, if draggables will be
+ // positioned on the base image, the scroller's right and left arrows
+ // opacity must be updated.
+ //
+ // 2.) When creating draggable elements, the scroller's right and left
+ // arrows opacity must be updated according to the number of
+ // draggables.
+ state.updateArrowOpacity = updateArrowOpacity;
+
+ // eslint-disable-next-line no-useless-return
+ return;
+
+ function updateArrowOpacity() {
+ $moveLeftEl.children('div').css('opacity', '1');
+ $moveRightEl.children('div').css('opacity', '1');
+
+ if (showElLeftMargin < -102 * (state.numDraggablesInSlider - 6)) {
+ $moveRightEl.children('div').css('opacity', '.4');
+ }
+ if (showElLeftMargin > -102) {
+ $moveLeftEl.children('div').css('opacity', '.4');
+ }
+ }
+ } // End-of: function Scroller(state)
+ }); // End-of: define([], function () {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/state.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/state.js
new file mode 100644
index 00000000..6ea9d5b6
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/state.js
@@ -0,0 +1,95 @@
+(function(requirejs, require, define) {
+ define([], function() {
+ return State;
+
+ function State(problemId) {
+ var state;
+
+ state = {
+ config: null,
+
+ baseImageEl: null,
+ baseImageLoaded: false,
+
+ containerEl: null,
+
+ sliderEl: null,
+
+ problemId: problemId,
+
+ draggables: [],
+ numDraggablesInSlider: 0,
+ currentMovingDraggable: null,
+
+ targets: [],
+
+ updateArrowOpacity: null,
+
+ uniqueId: 0,
+ salt: makeSalt(),
+
+ getUniqueId: getUniqueId
+ };
+
+ $(document).mousemove(function(event) {
+ documentMouseMove(state, event);
+ });
+
+ return state;
+ }
+
+ function getUniqueId() {
+ this.uniqueId += 1;
+
+ return this.salt + '_' + this.uniqueId.toFixed(0);
+ }
+
+ function makeSalt() {
+ var text, possible, i;
+
+ text = '';
+ possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+ for (i = 0; i < 5; i += 1) {
+ text += possible.charAt(Math.floor(Math.random() * possible.length));
+ }
+
+ return text;
+ }
+
+ function documentMouseMove(state, event) {
+ if (state.currentMovingDraggable !== null) {
+ state.currentMovingDraggable.iconEl.css(
+ 'left',
+ event.pageX
+ - state.baseImageEl.offset().left
+ - state.currentMovingDraggable.iconWidth * 0.5
+ - state.currentMovingDraggable.iconElLeftOffset
+ );
+ state.currentMovingDraggable.iconEl.css(
+ 'top',
+ event.pageY
+ - state.baseImageEl.offset().top
+ - state.currentMovingDraggable.iconHeight * 0.5
+ );
+
+ if (state.currentMovingDraggable.labelEl !== null) {
+ state.currentMovingDraggable.labelEl.css(
+ 'left',
+ event.pageX
+ - state.baseImageEl.offset().left
+ - state.currentMovingDraggable.labelWidth * 0.5
+ - 9 // Account for padding, border.
+ );
+ state.currentMovingDraggable.labelEl.css(
+ 'top',
+ event.pageY
+ - state.baseImageEl.offset().top
+ + state.currentMovingDraggable.iconHeight * 0.5
+ + 5
+ );
+ }
+ }
+ }
+ }); // End-of: define([], function () {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/targets.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/targets.js
new file mode 100644
index 00000000..553053fb
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/targets.js
@@ -0,0 +1,266 @@
+(function(requirejs, require, define) {
+ define(['edx-ui-toolkit/js/utils/html-utils'], function(HtmlUtils) {
+ return {
+ initializeBaseTargets: initializeBaseTargets,
+ initializeTargetField: initializeTargetField,
+ destroyTargetField: destroyTargetField
+ };
+
+ function initializeBaseTargets(state) {
+ (function(c1) {
+ while (c1 < state.config.targets.length) {
+ processTarget(state, state.config.targets[c1]);
+
+ c1 += 1;
+ }
+ }(0));
+ }
+
+ function initializeTargetField(draggableObj) {
+ var iconElOffset;
+
+ if (draggableObj.targetField.length === 0) {
+ draggableObj.originalConfigObj.target_fields.every(function(targetObj) {
+ processTarget(draggableObj.state, targetObj, true, draggableObj);
+
+ return true;
+ });
+ } else {
+ iconElOffset = draggableObj.iconEl.position();
+
+ draggableObj.targetField.every(function(targetObj) {
+ targetObj.offset.top = iconElOffset.top + targetObj.y;
+ targetObj.offset.left = iconElOffset.left + targetObj.x;
+
+ return true;
+ });
+ }
+ }
+
+ function destroyTargetField(draggableObj) {
+ var indexOffset, lowestRemovedIndex;
+
+ indexOffset = 0;
+ lowestRemovedIndex = draggableObj.state.targets.length + 1;
+
+ draggableObj.targetField.every(function(target) {
+ target.el.remove();
+
+ if (lowestRemovedIndex > target.indexInStateArray) {
+ lowestRemovedIndex = target.indexInStateArray;
+ }
+
+ draggableObj.state.targets.splice(target.indexInStateArray - indexOffset, 1);
+ indexOffset += 1;
+
+ return true;
+ });
+
+ draggableObj.state.targets.every(function(target) {
+ if (target.indexInStateArray > lowestRemovedIndex) {
+ target.indexInStateArray -= indexOffset;
+ }
+
+ return true;
+ });
+
+ draggableObj.targetField = [];
+ }
+
+ function processTarget(state, obj, fromTargetField, draggableObj) {
+ var $targetEl, borderCss, $numTextEl, targetObj;
+
+ borderCss = '';
+ if (state.config.targetOutline === true) {
+ borderCss = 'border: 1px dashed gray; ';
+ }
+
+ $targetEl = $(
+ HtmlUtils.joinHtml(
+ HtmlUtils.HTML('
')
+ ).toString()
+ );
+ if (fromTargetField === true) {
+ $targetEl.appendTo(draggableObj.iconEl);
+ } else {
+ $targetEl.appendTo(state.baseImageEl.parent());
+ }
+
+ $targetEl.mousedown(function(event) {
+ event.preventDefault();
+ });
+
+ if (state.config.onePerTarget === false) {
+ $numTextEl = $(
+ HtmlUtils.joinHtml(
+ HtmlUtils.HTML('0
')
+ ).toString()
+ );
+ } else {
+ $numTextEl = null;
+ }
+
+ targetObj = {
+ uniqueId: state.getUniqueId(),
+
+ id: obj.id,
+
+ x: obj.x,
+ y: obj.y,
+
+ w: obj.w,
+ h: obj.h,
+
+ el: $targetEl,
+ offset: $targetEl.position(),
+
+ draggableList: [],
+
+ state: state,
+
+ targetEl: $targetEl,
+
+ numTextEl: $numTextEl,
+ updateNumTextEl: updateNumTextEl,
+
+ removeDraggable: removeDraggable,
+ addDraggable: addDraggable,
+
+ type: 'base',
+ draggableObj: null
+ };
+
+ if (fromTargetField === true) {
+ targetObj.offset = draggableObj.iconEl.position();
+ targetObj.offset.top += obj.y;
+ targetObj.offset.left += obj.x;
+
+ targetObj.type = 'on_drag';
+ targetObj.draggableObj = draggableObj;
+ }
+
+ if (state.config.onePerTarget === false) {
+ $numTextEl.appendTo(state.baseImageEl.parent());
+ $numTextEl.mousedown(function(event) {
+ event.preventDefault();
+ });
+ $numTextEl.mouseup(function() {
+ cycleDraggableOrder.call(targetObj);
+ });
+ }
+
+ targetObj.indexInStateArray = state.targets.push(targetObj) - 1;
+
+ if (fromTargetField === true) {
+ draggableObj.targetField.push(targetObj);
+ }
+ }
+
+ function removeDraggable(draggable) {
+ var c1;
+
+ this.draggableList.splice(draggable.onTargetIndex, 1);
+
+ // An item from the array was removed. We need to updated all indexes accordingly.
+ // Shift all indexes down by one if they are higher than the index of the removed item.
+ c1 = 0;
+ while (c1 < this.draggableList.length) {
+ if (this.draggableList[c1].onTargetIndex > draggable.onTargetIndex) {
+ this.draggableList[c1].onTargetIndex -= 1;
+ }
+
+ c1 += 1;
+ }
+
+ draggable.onTarget = null;
+ draggable.onTargetIndex = null;
+
+ if (this.type === 'on_drag') {
+ this.draggableObj.numDraggablesOnMe -= 1;
+ }
+
+ this.updateNumTextEl();
+ }
+
+ function addDraggable(draggable) {
+ draggable.onTarget = this;
+ draggable.onTargetIndex = this.draggableList.push(draggable) - 1;
+
+ if (this.type === 'on_drag') {
+ this.draggableObj.numDraggablesOnMe += 1;
+ }
+
+ this.updateNumTextEl();
+ }
+
+ /*
+ * function cycleDraggableOrder
+ *
+ * Parameters:
+ * none - This function does not expect any parameters.
+ *
+ * Returns:
+ * undefined - The return value of this function is not used.
+ *
+ * Description:
+ * Go through all draggables that are on the current target, and decrease their
+ * z-index by 1, making sure that the bottom-most draggable ends up on the top.
+ */
+ function cycleDraggableOrder() {
+ var c1, lowestZIndex, highestZIndex;
+
+ if (this.draggableList.length < 2) {
+ return;
+ }
+
+ highestZIndex = -10000;
+ lowestZIndex = 10000;
+
+ for (c1 = 0; c1 < this.draggableList.length; c1 += 1) {
+ if (this.draggableList[c1].zIndex < lowestZIndex) {
+ lowestZIndex = this.draggableList[c1].zIndex;
+ }
+
+ if (this.draggableList[c1].zIndex > highestZIndex) {
+ highestZIndex = this.draggableList[c1].zIndex;
+ }
+ }
+
+ for (c1 = 0; c1 < this.draggableList.length; c1 += 1) {
+ if (this.draggableList[c1].zIndex === lowestZIndex) {
+ this.draggableList[c1].zIndex = highestZIndex;
+ } else {
+ this.draggableList[c1].zIndex -= 1;
+ }
+
+ this.draggableList[c1].iconEl.css('z-index', this.draggableList[c1].zIndex);
+ if (this.draggableList[c1].labelEl !== null) {
+ this.draggableList[c1].labelEl.css('z-index', this.draggableList[c1].zIndex);
+ }
+ }
+ }
+
+ function updateNumTextEl() {
+ if (this.numTextEl !== null) {
+ this.numTextEl.text(this.draggableList.length);
+ }
+ }
+ }); // End-of: define([], function () {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/update_input.js b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/update_input.js
new file mode 100644
index 00000000..c7769c48
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/drag_and_drop/update_input.js
@@ -0,0 +1,373 @@
+(function(requirejs, require, define) {
+ define([], function() {
+ return {
+ check: check,
+ update: update
+ };
+
+ function update(state) {
+ var draggables, tempObj;
+
+ draggables = [];
+
+ if (state.config.individualTargets === false) {
+ (function(c1) {
+ while (c1 < state.draggables.length) {
+ if (state.draggables[c1].x !== -1) {
+ tempObj = {};
+ tempObj[state.draggables[c1].id] = [
+ state.draggables[c1].x,
+ state.draggables[c1].y
+ ];
+ draggables.push(tempObj);
+ tempObj = null;
+ }
+
+ c1 += 1;
+ }
+ }(0));
+ } else {
+ (function(c1) {
+ while (c1 < state.targets.length) {
+ // eslint-disable-next-line no-loop-func
+ (function(c2) {
+ while (c2 < state.targets[c1].draggableList.length) {
+ tempObj = {};
+
+ if (state.targets[c1].type === 'base') {
+ tempObj[state.targets[c1].draggableList[c2].id] = state.targets[c1].id;
+ } else {
+ addTargetRecursively(tempObj, state.targets[c1].draggableList[c2], state.targets[c1]);
+ }
+ draggables.push(tempObj);
+ tempObj = null;
+
+ c2 += 1;
+ }
+ }(0));
+
+ c1 += 1;
+ }
+ }(0));
+ }
+
+ $('#input_' + state.problemId).val(JSON.stringify(draggables));
+ }
+
+ function addTargetRecursively(tempObj, draggable, target) {
+ if (target.type === 'base') {
+ tempObj[draggable.id] = target.id;
+ } else {
+ tempObj[draggable.id] = {};
+ tempObj[draggable.id][target.id] = {};
+
+ addTargetRecursively(tempObj[draggable.id][target.id], target.draggableObj, target.draggableObj.onTarget);
+ }
+ }
+
+ // Check if input has an answer from server. If yes, then position
+ // all draggables according to answer.
+ function check(state) {
+ var inputElVal;
+
+ inputElVal = $('#input_' + state.problemId).val();
+
+ if (inputElVal.length === 0) {
+ return false;
+ }
+
+ repositionDraggables(state, JSON.parse(inputElVal));
+
+ return true;
+ }
+
+ function processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i) {
+ var baseDraggableId, baseDraggable, baseTargetId, baseTarget,
+ layeredDraggableId, layeredDraggable, layeredTargetId, layeredTarget,
+ chain;
+
+ if (depth === 0) {
+ // We are at the lowest depth? The end.
+
+ return;
+ }
+
+ if (answerSortedByDepth.hasOwnProperty(depth) === false) {
+ // We have a depth that ts not valid, we decrease the depth by one.
+ processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth - 1, 0);
+
+ return;
+ }
+
+ if (answerSortedByDepth[depth].length <= i) {
+ // We ran out of answers at this depth, go to the next depth down.
+ processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth - 1, 0);
+
+ return;
+ }
+
+ chain = answerSortedByDepth[depth][i];
+
+ baseDraggableId = Object.keys(chain)[0];
+
+ // This is a hack. For now we will work with depths 1 and 3.
+ if (depth === 1) {
+ baseTargetId = chain[baseDraggableId];
+
+ layeredTargetId = null;
+ layeredDraggableId = null;
+
+ // createBaseDraggableOnTarget(state, baseDraggableId, baseTargetId);
+ } else if (depth === 3) {
+ layeredDraggableId = baseDraggableId;
+
+ layeredTargetId = Object.keys(chain[layeredDraggableId])[0];
+
+ baseDraggableId = Object.keys(chain[layeredDraggableId][layeredTargetId])[0];
+
+ baseTargetId = chain[layeredDraggableId][layeredTargetId][baseDraggableId];
+ }
+
+ checkBaseDraggable();
+
+ // eslint-disable-next-line no-useless-return
+ return;
+
+ function checkBaseDraggable() {
+ // eslint-disable-next-line no-cond-assign
+ if ((baseDraggable = getById(state, 'draggables', baseDraggableId, null, false, baseTargetId)) === null) {
+ createBaseDraggableOnTarget(state, baseDraggableId, baseTargetId, true, function() {
+ // eslint-disable-next-line no-cond-assign
+ if ((baseDraggable = getById(state, 'draggables', baseDraggableId, null, false, baseTargetId)) === null) {
+ console.log('ERROR: Could not successfully create a base draggable on a base target.');
+ } else {
+ baseTarget = baseDraggable.onTarget;
+
+ if ((layeredTargetId === null) || (layeredDraggableId === null)) {
+ processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i + 1);
+ } else {
+ checklayeredDraggable();
+ }
+ }
+ });
+ } else {
+ baseTarget = baseDraggable.onTarget;
+
+ if ((layeredTargetId === null) || (layeredDraggableId === null)) {
+ processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i + 1);
+ } else {
+ checklayeredDraggable();
+ }
+ }
+ }
+
+ function checklayeredDraggable() {
+ // eslint-disable-next-line no-cond-assign
+ if ((layeredDraggable = getById(state, 'draggables', layeredDraggableId, null, false, layeredTargetId, baseDraggableId, baseTargetId)) === null) {
+ layeredDraggable = getById(state, 'draggables', layeredDraggableId);
+ layeredTarget = null;
+ baseDraggable.targetField.every(function(target) {
+ if (target.id === layeredTargetId) {
+ layeredTarget = target;
+ }
+
+ return true;
+ });
+
+ if ((layeredDraggable !== null) && (layeredTarget !== null)) {
+ layeredDraggable.moveDraggableTo('target', layeredTarget, function() {
+ processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i + 1);
+ });
+ } else {
+ processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i + 1);
+ }
+ } else {
+ processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, depth, i + 1);
+ }
+ }
+ }
+
+ function createBaseDraggableOnTarget(state, draggableId, targetId, reportError, funcCallback) {
+ var draggable, target;
+
+ // eslint-disable-next-line no-cond-assign
+ if ((draggable = getById(state, 'draggables', draggableId)) === null) {
+ if (reportError !== false) {
+ console.log(
+ 'ERROR: In answer there exists a '
+ + 'draggable ID "' + draggableId + '". No '
+ + 'draggable with this ID could be found.'
+ );
+ }
+
+ return false;
+ }
+
+ // eslint-disable-next-line no-cond-assign
+ if ((target = getById(state, 'targets', targetId)) === null) {
+ if (reportError !== false) {
+ console.log(
+ 'ERROR: In answer there exists a target '
+ + 'ID "' + targetId + '". No target with this '
+ + 'ID could be found.'
+ );
+ }
+
+ return false;
+ }
+
+ draggable.moveDraggableTo('target', target, funcCallback);
+
+ return true;
+ }
+
+ function processAnswerPositions(state, answer) {
+ var draggableId, draggable;
+
+ (function(c1) {
+ while (c1 < answer.length) {
+ for (draggableId in answer[c1]) {
+ if (answer[c1].hasOwnProperty(draggableId) === false) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ // eslint-disable-next-line no-cond-assign
+ if ((draggable = getById(state, 'draggables', draggableId)) === null) {
+ console.log(
+ 'ERROR: In answer there exists a '
+ + 'draggable ID "' + draggableId + '". No '
+ + 'draggable with this ID could be found.'
+ );
+
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ draggable.moveDraggableTo('XY', {
+ x: answer[c1][draggableId][0],
+ y: answer[c1][draggableId][1]
+ });
+ }
+
+ c1 += 1;
+ }
+ }(0));
+ }
+
+ function repositionDraggables(state, answer) {
+ var answerSortedByDepth, minDepth, maxDepth;
+
+ answerSortedByDepth = {};
+ minDepth = 1000;
+ maxDepth = 0;
+
+ answer.every(function(chain) {
+ var depth;
+
+ depth = findDepth(chain, 0);
+
+ if (depth < minDepth) {
+ minDepth = depth;
+ }
+ if (depth > maxDepth) {
+ maxDepth = depth;
+ }
+
+ if (answerSortedByDepth.hasOwnProperty(depth) === false) {
+ answerSortedByDepth[depth] = [];
+ }
+
+ answerSortedByDepth[depth].push(chain);
+
+ return true;
+ });
+
+ if (answer.length === 0) {
+ return;
+ }
+
+ // For now we support only one case.
+ if ((minDepth < 1) || (maxDepth > 3)) {
+ return;
+ }
+
+ if (state.config.individualTargets === true) {
+ processAnswerTargets(state, answerSortedByDepth, minDepth, maxDepth, maxDepth, 0);
+ } else if (state.config.individualTargets === false) {
+ processAnswerPositions(state, answer);
+ }
+ }
+
+ function findDepth(tempObj, depth) {
+ var i;
+
+ if ($.isPlainObject(tempObj) === false) {
+ return depth;
+ }
+
+ depth += 1;
+
+ for (i in tempObj) {
+ if (tempObj.hasOwnProperty(i) === true) {
+ depth = findDepth(tempObj[i], depth);
+ }
+ }
+
+ return depth;
+ }
+
+ function getById(state, type, id, fromTargetField, inContainer, targetId, baseDraggableId, baseTargetId) {
+ return (function(c1) {
+ while (c1 < state[type].length) {
+ if (type === 'draggables') {
+ if ((targetId !== undefined) && (inContainer === false) && (baseDraggableId !== undefined) && (baseTargetId !== undefined)) {
+ if (
+ (state[type][c1].id === id)
+ && (state[type][c1].inContainer === false)
+ && (state[type][c1].onTarget.id === targetId)
+ && (state[type][c1].onTarget.type === 'on_drag')
+ && (state[type][c1].onTarget.draggableObj.id === baseDraggableId)
+ && (state[type][c1].onTarget.draggableObj.onTarget.id === baseTargetId)
+ ) {
+ return state[type][c1];
+ }
+ } else if ((targetId !== undefined) && (inContainer === false)) {
+ if (
+ (state[type][c1].id === id)
+ && (state[type][c1].inContainer === false)
+ && (state[type][c1].onTarget.id === targetId)
+ ) {
+ return state[type][c1];
+ }
+ } else {
+ if (inContainer === false) {
+ if ((state[type][c1].id === id) && (state[type][c1].inContainer === false)) {
+ return state[type][c1];
+ }
+ } else {
+ if ((state[type][c1].id === id) && (state[type][c1].inContainer === true)) {
+ return state[type][c1];
+ }
+ }
+ }
+ } else { // 'targets'
+ if (fromTargetField === true) {
+ if ((state[type][c1].id === id) && (state[type][c1].type === 'on_drag')) {
+ return state[type][c1];
+ }
+ } else {
+ if ((state[type][c1].id === id) && (state[type][c1].type === 'base')) {
+ return state[type][c1];
+ }
+ }
+ }
+
+ c1 += 1;
+ }
+
+ return null;
+ }(0));
+ }
+ }); // End-of: define([], function () {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) {
diff --git a/xblocks_contrib/problem/assets/static/js/capa/edit-a-gene.js b/xblocks_contrib/problem/assets/static/js/capa/edit-a-gene.js
new file mode 100644
index 00000000..0ef55a06
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/edit-a-gene.js
@@ -0,0 +1,71 @@
+(function() {
+ var timeout = 1000;
+
+ waitForGenex();
+
+ function waitForGenex() {
+ if (typeof genex !== 'undefined' && genex) {
+ genex.onInjectionDone('genex');
+ } else {
+ setTimeout(function() { waitForGenex(); }, timeout);
+ }
+ }
+
+ // NOTE:
+ // Genex uses 8 global functions, all prefixed with genex:
+ // 6 are exported from GWT:
+ // genexSetDefaultDNASequence
+ // genexSetDNASequence
+ // genexGetDNASequence
+ // genexSetClickEvent
+ // genexSetKeyEvent
+ // genexSetProblemNumber
+ //
+ // It calls genexIsReady with a deferred command when it has finished
+ // initialization and has drawn itself
+ // genexStoreAnswer(answer) is called each time the DNA sequence changes
+ // through user interaction
+
+ // Genex does not call the following function
+ genexGetInputField = function() {
+ var problem = $('#genex_container').parents('.problem');
+ return problem.find('input[type="hidden"][name!="genex_dna_sequence"][name!="genex_problem_number"]');
+ };
+
+ genexIsReady = function() {
+ var input_field = genexGetInputField();
+ var genex_saved_state = input_field.val();
+ var genex_default_dna_sequence;
+ var genex_dna_sequence;
+
+ // Get the DNA sequence from xml file
+ genex_default_dna_sequence = $('#genex_dna_sequence').val();
+ // Set the default DNA
+ genexSetDefaultDNASequence(genex_default_dna_sequence);
+
+ // Now load problem
+ var genex_problem_number = $('#genex_problem_number').val();
+ genexSetProblemNumber(genex_problem_number);
+
+ // Set the DNA sequence that is displayed
+ if (genex_saved_state === '') {
+ // Load DNA sequence from xml file
+ genex_dna_sequence = genex_default_dna_sequence;
+ } else {
+ // Load DNA sequence from saved value
+ genex_saved_state = JSON.parse(genex_saved_state);
+ genex_dna_sequence = genex_saved_state.genex_dna_sequence;
+ }
+ genexSetDNASequence(genex_dna_sequence);
+
+ // Now load mouse and keyboard handlers
+ genexSetClickEvent();
+ genexSetKeyEvent();
+ };
+
+ genexStoreAnswer = function(answer) {
+ var input_field = genexGetInputField();
+ var value = {genex_dna_sequence: genexGetDNASequence(), genex_answer: answer};
+ input_field.val(JSON.stringify(value));
+ };
+}).call(this);
diff --git a/xblocks_contrib/problem/assets/static/js/capa/fixtures/jsinput.html b/xblocks_contrib/problem/assets/static/js/capa/fixtures/jsinput.html
new file mode 100644
index 00000000..039fe3d8
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/fixtures/jsinput.html
@@ -0,0 +1,53 @@
+
+
+
+
diff --git a/xblocks_contrib/problem/assets/static/js/capa/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html b/xblocks_contrib/problem/assets/static/js/capa/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html
new file mode 100644
index 00000000..ec8170eb
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html
@@ -0,0 +1,652 @@
+
+
+
+
\ No newline at end of file
diff --git a/xblocks_contrib/problem/assets/static/js/capa/genex/63308EE54E8033A708B414CAC05B0C32.cache.html b/xblocks_contrib/problem/assets/static/js/capa/genex/63308EE54E8033A708B414CAC05B0C32.cache.html
new file mode 100644
index 00000000..952e3b5f
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/genex/63308EE54E8033A708B414CAC05B0C32.cache.html
@@ -0,0 +1,642 @@
+
+
+
+
\ No newline at end of file
diff --git a/xblocks_contrib/problem/assets/static/js/capa/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html b/xblocks_contrib/problem/assets/static/js/capa/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html
new file mode 100644
index 00000000..95cb9628
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html
@@ -0,0 +1,628 @@
+
+
+
\ No newline at end of file
diff --git a/xblocks_contrib/problem/assets/static/js/capa/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html b/xblocks_contrib/problem/assets/static/js/capa/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html
new file mode 100644
index 00000000..5c828c12
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html
@@ -0,0 +1,654 @@
+
+
+
+
\ No newline at end of file
diff --git a/xblocks_contrib/problem/assets/static/js/capa/genex/A069AC107D79C29D6237614AC340F0C0.cache.html b/xblocks_contrib/problem/assets/static/js/capa/genex/A069AC107D79C29D6237614AC340F0C0.cache.html
new file mode 100644
index 00000000..bcf15330
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/genex/A069AC107D79C29D6237614AC340F0C0.cache.html
@@ -0,0 +1,652 @@
+
+
+
+
\ No newline at end of file
diff --git a/xblocks_contrib/problem/assets/static/js/capa/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html b/xblocks_contrib/problem/assets/static/js/capa/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html
new file mode 100644
index 00000000..5ab12af7
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html
@@ -0,0 +1,642 @@
+
+
+
+
\ No newline at end of file
diff --git a/xblocks_contrib/problem/assets/static/js/capa/genex/clear.cache.gif b/xblocks_contrib/problem/assets/static/js/capa/genex/clear.cache.gif
new file mode 100644
index 00000000..e565824a
Binary files /dev/null and b/xblocks_contrib/problem/assets/static/js/capa/genex/clear.cache.gif differ
diff --git a/xblocks_contrib/problem/assets/static/js/capa/genex/genex.css b/xblocks_contrib/problem/assets/static/js/capa/genex/genex.css
new file mode 100644
index 00000000..459c854f
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/genex/genex.css
@@ -0,0 +1,122 @@
+.genex-button {
+ margin-right: -8px;
+ height: 40px !important;
+}
+
+.genex-label {
+ /*font: normal normal normal 10pt/normal 'Open Sans', Verdana, Geneva, sans-serif !important;*/
+ /*padding: 4px 0px 0px 10px !important;*/
+ font-family: sans-serif !important;
+ font-size: 13px !important;
+ font-style: normal !important;
+ font-variant: normal !important;
+ font-weight: bold !important;
+ padding-top: 6px !important;
+ margin-left: 18px;
+}
+
+.gwt-HTML {
+ cursor: default;
+ overflow-x: auto !important;
+ overflow-y: auto !important;
+ background-color: rgb(248, 248, 248) !important;
+}
+
+.genex-scrollpanel {
+ word-wrap: normal !important;
+ white-space: pre !important;
+}
+
+pre, #dna-strand {
+ font-family: 'courier new', courier !important;
+ font-size: 13px !important;
+ font-style: normal !important;
+ font-variant: normal !important;
+ font-weight: normal !important;
+ border-style: none !important;
+ background-color: rgb(248, 248, 248) !important;
+ word-wrap: normal !important;
+ white-space: pre !important;
+ overflow-x: visible !important;
+ overflow-y: visible !important;
+}
+
+.gwt-DialogBox .Caption {
+ background: #F1F1F1;
+ padding: 4px 8px 4px 4px;
+ cursor: default;
+ font-family: Arial Unicode MS, Arial, sans-serif;
+ font-weight: bold;
+ border-bottom: 1px solid #bbbbbb;
+ border-top: 1px solid #D2D2D2;
+}
+.gwt-DialogBox .dialogContent {
+}
+.gwt-DialogBox .dialogMiddleCenter {
+ padding: 3px;
+ background: white;
+}
+.gwt-DialogBox .dialogBottomCenter {
+ background: url(images/hborder.png) repeat-x 0px -2945px;
+ -background: url(images/hborder_ie6.png) repeat-x 0px -2144px;
+}
+.gwt-DialogBox .dialogMiddleLeft {
+ background: url(images/vborder.png) repeat-y -31px 0px;
+}
+.gwt-DialogBox .dialogMiddleRight {
+ background: url(images/vborder.png) repeat-y -32px 0px;
+ -background: url(images/vborder_ie6.png) repeat-y -32px 0px;
+}
+.gwt-DialogBox .dialogTopLeftInner {
+ width: 10px;
+ height: 8px;
+ zoom: 1;
+}
+.gwt-DialogBox .dialogTopRightInner {
+ width: 12px;
+ zoom: 1;
+}
+.gwt-DialogBox .dialogBottomLeftInner {
+ width: 10px;
+ height: 12px;
+ zoom: 1;
+}
+.gwt-DialogBox .dialogBottomRightInner {
+ width: 12px;
+ height: 12px;
+ zoom: 1;
+}
+.gwt-DialogBox .dialogTopLeft {
+ background: url(images/circles.png) no-repeat -20px 0px;
+ -background: url(images/circles_ie6.png) no-repeat -20px 0px;
+}
+.gwt-DialogBox .dialogTopRight {
+ background: url(images/circles.png) no-repeat -28px 0px;
+ -background: url(images/circles_ie6.png) no-repeat -28px 0px;
+}
+.gwt-DialogBox .dialogBottomLeft {
+ background: url(images/circles.png) no-repeat 0px -36px;
+ -background: url(images/circles_ie6.png) no-repeat 0px -36px;
+}
+.gwt-DialogBox .dialogBottomRight {
+ background: url(images/circles.png) no-repeat -8px -36px;
+ -background: url(images/circles_ie6.png) no-repeat -8px -36px;
+}
+* html .gwt-DialogBox .dialogTopLeftInner {
+ width: 10px;
+ overflow: hidden;
+}
+* html .gwt-DialogBox .dialogTopRightInner {
+ width: 12px;
+ overflow: hidden;
+}
+* html .gwt-DialogBox .dialogBottomLeftInner {
+ width: 10px;
+ height: 12px;
+ overflow: hidden;
+}
+* html .gwt-DialogBox .dialogBottomRightInner {
+ width: 12px;
+ height: 12px;
+ overflow: hidden;
+}
\ No newline at end of file
diff --git a/xblocks_contrib/problem/assets/static/js/capa/genex/genex.nocache.js b/xblocks_contrib/problem/assets/static/js/capa/genex/genex.nocache.js
new file mode 100644
index 00000000..11f9714a
--- /dev/null
+++ b/xblocks_contrib/problem/assets/static/js/capa/genex/genex.nocache.js
@@ -0,0 +1,18 @@
+function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='21B31BA00E7CE7B6BD63DD13A8586A45',Rb='63308EE54E8033A708B414CAC05B0C32',Sb='7AC57DC6EC8C1D8672DDF6E6D4EF57CC',Tb='9B4F4D4EFA24CDE2E4287CC07897F249',Wb=':',pb='::',dc='
+
+This html file is for Development Mode support.
+