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 @@ +
+ + + +
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 @@ +
+ + + 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 @@ + + + +
+ +
+
+ +
+
+
+ + + Status: unanswered + +
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 @@ + + + +
+ +
+
+ +
+
+
+ + + Status: unanswered + +
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 @@ +

Custom Javascript Display and Grading

+
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+
+
+
+ + + +
+
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 + + +

processing

+
+ +
+ Submitted. As soon as a response is returned, this message will be replaced by that feedback. +
+
+
+ +
+
+
+
+
+ + + +
+ +
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 Header

+
+
+

${_("Problem Content")}

+
+ + + + +
+ + + + + + + + + +
+ + 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 Header

+
+
+

${_("Problem Content")}

+
+ + + + + + + + + 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 @@ +
+ + + +
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
' + + '
' + + "

longform is visible

" + + "
" + + "
"; + html_custom = + "" + + '
' + + "
shortform message
" + + '
' + + "

longform is visible

" + + "
" + + "
"; + }); + + 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 = + '

Correctexcellent

' + + '

Yepcorrect

'; + 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 = `\ +
+ +
\ +`; + $("#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(`\ + +
{{ tag_prompt|safe }}
+
    + {% for option in options %} +
  • + {% if has_options_value %} + {% if option.choice == status.classname and status == status.classname %} + + {% include "status_span.html" with status=status %} + + {% endif %} + {% endif %} + {{ option.description|safe }} +
  • + {% endfor %} +
+ {% if debug %} +
+ Rendered with value: +
+
{{ value|safe }}
+ Current input value: +
+ +
+ {% else %} + + {% endif %} + {% include "status_span.html" with status=status status_id=id %} +

+
+
+{% if msg %}{{ msg|safe }}{% endif %} diff --git a/xblocks_contrib/problem/capa/templates/chemicalequationinput.html b/xblocks_contrib/problem/capa/templates/chemicalequationinput.html new file mode 100644 index 00000000..5d2f19e4 --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/chemicalequationinput.html @@ -0,0 +1,19 @@ +
+
+
+ +

+ {{ value }} + {% include "status_span.html" with status=status status_id=id %} +

+
+

+
+
diff --git a/xblocks_contrib/problem/capa/templates/choicegroup.html b/xblocks_contrib/problem/capa/templates/choicegroup.html new file mode 100644 index 00000000..0c9e7427 --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/choicegroup.html @@ -0,0 +1,41 @@ +
+
+ {% if response_data.label %} + {{ response_data.label }} + {% endif %} + {% for description_id, description_text in response_data.descriptions.items %} +

{{ description_text|safe }}

+ {% endfor %} + {% for choice_id, choice_label in choices %} +
+ + +
+ {% endfor %} + +
+
+ {% if show_correctness != 'never' %} + {% include "status_span.html" with status=status status_id=id %} + {% else %} + {% include "status_span.html" with status=status status_id=id hide_correctness=True %} + {% endif %} +
+ {% if show_correctness == "never" %} + {% if value or status != "unsubmitted" %}
{{ submitted_message|safe }}
{% endif %} + {% endif %} + {% if msg %}{{ msg|safe }}{% endif %} +
diff --git a/xblocks_contrib/problem/capa/templates/choicetext.html b/xblocks_contrib/problem/capa/templates/choicetext.html new file mode 100644 index 00000000..26816522 --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/choicetext.html @@ -0,0 +1,61 @@ +{% load i18n %} +{% load static %} +{% with element_checked=False %} + {% for choice_id, _ in choices %} + {% if choice_id in value %} + {% with element_checked=True %}{% endwith %} + {% endif %} + {% endfor %} +
+
+
+
+ {% for choice_id, choice_description in choices %} +
+ + {% for content_node in choice_description %} + {% if content_node.type == 'text' %} + {{ content_node.contents }} + {% else %} + {% with my_id=content_node.contents|default:'' %} + {% with my_val=value.my_id|default:'' %} + + {% endwith %} + {% endwith %} + {% endif %} + {{ content_node.tail_text }} + {% endfor %} +

+
+ {% endfor %} + +
+ +
+ {% if input_type == 'checkbox' or not value or status.classname == 'incomplete' or status.classname == 'unsubmitted' or status.classname == 'unanswered' %} + {% include "status_span.html" with status=status status_id=id %} + {% endif %} +
+ {% if show_correctness == "never" %} + {% if value or status != "unsubmitted" %}
{{ submitted_message }}
{% endif %} + {% endif %} + {% if msg %}{{ msg|safe }}{% endif %} +
+
+{% endwith %} diff --git a/xblocks_contrib/problem/capa/templates/clarification.html b/xblocks_contrib/problem/capa/templates/clarification.html new file mode 100644 index 00000000..85bc3dc6 --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/clarification.html @@ -0,0 +1,10 @@ + + + ({{ clarification }}) + diff --git a/xblocks_contrib/problem/capa/templates/codeinput.html b/xblocks_contrib/problem/capa/templates/codeinput.html new file mode 100644 index 00000000..47e934a5 --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/codeinput.html @@ -0,0 +1,31 @@ +{% load i18n %} +
+ {% if response_data.label %} + + {% else %} + + {% endif %} + + {{ code_mirror_exit_message }} +
+ {% include "status_span.html" with status=status status_id=id %} + {% if status == 'queued' %}{% endif %} + {% if hidden %}
{% endif %} +

{{ status.display_name }}

+
+ +
{{ msg|safe }}
+
diff --git a/xblocks_contrib/problem/capa/templates/crystallography.html b/xblocks_contrib/problem/capa/templates/crystallography.html new file mode 100644 index 00000000..dc1fe0ff --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/crystallography.html @@ -0,0 +1,29 @@ +{% load static %} +
+
+
+ Lattice: + +
+
+
+
+ {% if status == 'unsubmitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+ {% endif %} + + {% include "status_span.html" with status=status status_id=id %} +

+ {% if msg %}{{ msg|safe }}{% endif %} + {% if status == 'unsubmitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+ {% endif %} +
diff --git a/xblocks_contrib/problem/capa/templates/designprotein2dinput.html b/xblocks_contrib/problem/capa/templates/designprotein2dinput.html new file mode 100644 index 00000000..b272b14e --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/designprotein2dinput.html @@ -0,0 +1,25 @@ +{% load static %} +
+
+
+ {% if status == 'unsubmitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+ {% endif %} +
+ + + + {% include "status_span.html" with status=status status_id=id %} +

+ {% if status == 'unsubmitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+{% endif %} +
diff --git a/xblocks_contrib/problem/capa/templates/drag_and_drop_input.html b/xblocks_contrib/problem/capa/templates/drag_and_drop_input.html new file mode 100644 index 00000000..f8fd5526 --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/drag_and_drop_input.html @@ -0,0 +1,27 @@ +{% load static %} +
+
+ +
+ {% if status == 'unsubmitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+ {% endif %} + +

{% include "status_span.html" with status=status status_id=id %}

+

+ {% if msg %}{{ msg|safe }}{% endif %} + {% if status == 'unsubmitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+ {% endif %} +
diff --git a/xblocks_contrib/problem/capa/templates/editageneinput.html b/xblocks_contrib/problem/capa/templates/editageneinput.html new file mode 100644 index 00000000..b800bfa7 --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/editageneinput.html @@ -0,0 +1,32 @@ +{% load static %} +
+
+
+ {% if status == 'unsubmitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+ {% endif %} +
+ + + + + +

+ {% include "status_span.html" with status=status status_id=id %} +

+

+{% if status == 'unsubmitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+{% endif %} +
diff --git a/xblocks_contrib/problem/capa/templates/editamolecule.html b/xblocks_contrib/problem/capa/templates/editamolecule.html new file mode 100644 index 00000000..1ef3314c --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/editamolecule.html @@ -0,0 +1,32 @@ +
+
+ {% if status == 'unsubmitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+ {% endif %} +
+
+ + +

+

+ {% include "status_span.html" with status=status status_id=id %} +

+
+ {% if status == 'unsubmitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+ {% endif %} +
diff --git a/xblocks_contrib/problem/capa/templates/filesubmission.html b/xblocks_contrib/problem/capa/templates/filesubmission.html new file mode 100644 index 00000000..1d493e8c --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/filesubmission.html @@ -0,0 +1,16 @@ +
+
+ {{ status.display_name }} + {% if status == 'queued' %}{% endif %} +

{{ status }}

+ +
+
{{ msg|safe }}
+
diff --git a/xblocks_contrib/problem/capa/templates/formulaequationinput.html b/xblocks_contrib/problem/capa/templates/formulaequationinput.html new file mode 100644 index 00000000..f75bcfe9 --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/formulaequationinput.html @@ -0,0 +1,29 @@ +{% load static %} +
+
+ {% if response_data.label %} + + {% endif %} + {% for description_id, description_text in response_data.descriptions.items %} +

{{ description_text }}

+ {% endfor %} + + {{ trailing_text }} + {% include "status_span.html" with status=status status_id=id %} +

+
+ \(\) + Loading +
+
+
+ {% if msg %}{{ msg|safe }}{% endif %} +
diff --git a/xblocks_contrib/problem/capa/templates/imageinput.html b/xblocks_contrib/problem/capa/templates/imageinput.html new file mode 100644 index 00000000..b630933f --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/imageinput.html @@ -0,0 +1,33 @@ +{% load static %} +
+ +
+
+ Selection indicator +
+
+
+ + {% include "status_span.html" with status=status status_id=id %} +
diff --git a/xblocks_contrib/problem/capa/templates/jsinput.html b/xblocks_contrib/problem/capa/templates/jsinput.html new file mode 100644 index 00000000..56dec9cd --- /dev/null +++ b/xblocks_contrib/problem/capa/templates/jsinput.html @@ -0,0 +1,42 @@ +
+
+
+ {% if status == 'unsubmitted' or status == 'submitted' or status == 'correct' or status == 'incorrect' or status == 'partially-correct' or status == 'incomplete' %} +
+ {% endif %} +