From 9435393c187f2f464d7c78f61fc527ea984c05cf Mon Sep 17 00:00:00 2001 From: Irtaza Akram Date: Thu, 19 Feb 2026 12:36:32 +0500 Subject: [PATCH] fix: add problem xblock --- requirements/base.in | 13 + requirements/base.txt | 127 +- requirements/dev.txt | 192 +- requirements/doc.txt | 160 +- requirements/quality.txt | 148 +- requirements/test.txt | 163 +- .../assets/fixtures/checkbox_problem.html | 14 + .../assets/fixtures/codeinput_problem.html | 20 + .../problem/assets/fixtures/imageinput.html | 30 + .../assets/fixtures/imageinput.underscore | 30 + .../assets/fixtures/jsinput_problem.html | 60 + .../assets/fixtures/matlabinput_problem.html | 48 + .../problem/assets/fixtures/problem.html | 8 + .../assets/fixtures/problem_content.html | 44 + .../assets/fixtures/problem_content_1240.html | 23 + .../assets/fixtures/radiobutton_problem.html | 14 + .../problem/assets/karma_runner.js | 12 + .../problem/assets/spec/collapsible_spec.js | 130 + .../problem/assets/spec/display_spec.js | 1176 +++ .../problem/assets/spec/imageinput_spec.js | 132 + .../spec_helpers/accessibility_tools.js | 256 + .../assets/spec_helpers/add_ajax_prefix.js | 5 + .../assets/spec_helpers/ajax_prefix.js | 24 + .../problem/assets/spec_helpers/helper.js | 325 + .../problem/assets/spec_helpers/i18n.js | 10 + .../assets/spec_helpers/jasmine-extensions.js | 284 + .../assets/spec_helpers/jasmine-imagediff.js | 401 ++ .../assets/spec_helpers/jasmine-waituntil.js | 47 + .../problem/assets/spec_helpers/logger.js | 102 + .../assets/static/css/ProblemBlockDisplay.css | 2304 ++++++ .../problem/assets/static/js/.gitignore | 3 + .../problem/assets/static/js/collapsible.js | 121 + .../problem/assets/static/js/display.js | 1411 ++++ .../problem/assets/static/js/imageinput.js | 54 + .../assets/static/js/javascript_loader.js | 97 + .../problem/assets/static/js/schematic.js | 6291 +++++++++++++++++ .../static/js/vendor/codemirror-compressed.js | 11 + .../problem/assets/static/js/xmodule.js | 103 + xblocks_contrib/problem/capa/__init__.py | 0 xblocks_contrib/problem/capa/capa_problem.py | 1229 ++++ xblocks_contrib/problem/capa/checker.py | 171 + xblocks_contrib/problem/capa/correctmap.py | 217 + xblocks_contrib/problem/capa/customrender.py | 195 + xblocks_contrib/problem/capa/errors.py | 62 + xblocks_contrib/problem/capa/inputtypes.py | 1804 +++++ xblocks_contrib/problem/capa/registry.py | 57 + xblocks_contrib/problem/capa/responsetypes.py | 3832 ++++++++++ .../problem/capa/safe_exec/README.rst | 43 + .../problem/capa/safe_exec/__init__.py | 3 + .../problem/capa/safe_exec/exceptions.py | 21 + .../problem/capa/safe_exec/lazymod.py | 43 + .../problem/capa/safe_exec/remote_exec.py | 143 + .../problem/capa/safe_exec/safe_exec.py | 506 ++ .../problem/capa/safe_exec/tests/__init__.py | 0 .../tests/test_files/pylib/constant.py | 1 + .../capa/safe_exec/tests/test_lazymod.py | 62 + .../capa/safe_exec/tests/test_remote_exec.py | 35 + .../capa/safe_exec/tests/test_safe_exec.py | 784 ++ .../capa/templates/annotationinput.html | 61 + .../capa/templates/chemicalequationinput.html | 19 + .../problem/capa/templates/choicegroup.html | 41 + .../problem/capa/templates/choicetext.html | 61 + .../problem/capa/templates/clarification.html | 10 + .../problem/capa/templates/codeinput.html | 31 + .../capa/templates/crystallography.html | 29 + .../capa/templates/designprotein2dinput.html | 25 + .../capa/templates/drag_and_drop_input.html | 27 + .../capa/templates/editageneinput.html | 32 + .../problem/capa/templates/editamolecule.html | 32 + .../capa/templates/filesubmission.html | 16 + .../capa/templates/formulaequationinput.html | 29 + .../problem/capa/templates/imageinput.html | 33 + .../problem/capa/templates/jsinput.html | 42 + .../problem/capa/templates/mathstring.html | 8 + .../problem/capa/templates/matlabinput.html | 121 + .../problem/capa/templates/optioninput.html | 20 + .../problem/capa/templates/problem.html | 154 + .../problem/capa/templates/problem_ajax.html | 16 + .../capa/templates/problem_notifications.html | 19 + .../capa/templates/schematicinput.html | 18 + .../problem/capa/templates/solutionspan.html | 3 + .../problem/capa/templates/status_span.html | 12 + .../problem/capa/templates/textline.html | 39 + .../problem/capa/templates/vsepr_input.html | 38 + .../problem/capa/tests/__init__.py | 0 xblocks_contrib/problem/capa/tests/helpers.py | 120 + .../capa/tests/response_xml_factory.py | 940 +++ .../problem/capa/tests/test_answer_pool.py | 672 ++ .../problem/capa/tests/test_capa_problem.py | 824 +++ .../problem/capa/tests/test_correctmap.py | 195 + .../problem/capa/tests/test_customrender.py | 87 + .../problem/capa/tests/test_errors.py | 63 + .../capa/tests/test_files/dynamath_input.txt | 43 + .../capa/tests/test_files/extended_hints.xml | 50 + .../test_files/extended_hints_checkbox.xml | 119 + .../test_files/extended_hints_dropdown.xml | 42 + .../extended_hints_multiple_choice.xml | 35 + ...tended_hints_multiple_choice_with_html.xml | 15 + .../extended_hints_numeric_input.xml | 41 + .../test_files/extended_hints_text_input.xml | 81 + .../test_files/extended_hints_with_errors.xml | 14 + .../test_files/filename_convert_test.txt | 1 + .../test_files/js/mersenne-twister-min.js | 216 + .../test_files/js/test_problem_display.js | 58 + .../test_files/js/test_problem_generator.js | 36 + .../test_files/js/test_problem_grader.js | 56 + .../capa/tests/test_files/js/xproblem.js | 74 + .../tests/test_files/snuggletex_2x+3y.xml | 235 + .../tests/test_files/snuggletex_correct.html | 556 ++ .../tests/test_files/snuggletex_wrong.html | 258 + .../tests/test_files/snuggletex_x+x+3y.xml | 225 + .../tests/test_files/targeted_feedback.xml | 50 + .../test_files/targeted_feedback_multiple.xml | 91 + .../capa/tests/test_hint_functionality.py | 1135 +++ .../problem/capa/tests/test_html_render.py | 318 + .../capa/tests/test_input_templates.py | 1188 ++++ .../problem/capa/tests/test_inputtypes.py | 1734 +++++ .../problem/capa/tests/test_responsetypes.py | 2793 ++++++++ .../problem/capa/tests/test_shuffle.py | 320 + .../capa/tests/test_targeted_feedback.py | 654 ++ .../problem/capa/tests/test_util.py | 197 + .../capa/tests/test_xqueue_interface.py | 76 + .../capa/tests/test_xqueue_submission.py | 125 + xblocks_contrib/problem/capa/util.py | 251 + .../problem/capa/xqueue_interface.py | 185 + .../problem/capa/xqueue_submission.py | 111 + xblocks_contrib/problem/capa_block.py | 2469 +++++++ xblocks_contrib/problem/markup.py | 74 + xblocks_contrib/problem/problem.py | 104 - .../problem/static/applets/Protex.jar | Bin 0 -> 212767 bytes .../problem/static/applets/genex.jar | Bin 0 -> 227154 bytes .../problem/static/css/problem.css | 9 - .../static/images/vsepr/AX2E0-3D-balls.png | Bin 0 -> 26362 bytes .../static/images/vsepr/AX2E1-3D-balls.png | Bin 0 -> 51741 bytes .../static/images/vsepr/AX2E2-3D-balls.png | Bin 0 -> 66279 bytes .../static/images/vsepr/AX2E3-3D-balls.png | Bin 0 -> 65642 bytes .../static/images/vsepr/AX3E0-3D-balls.png | Bin 0 -> 53410 bytes .../static/images/vsepr/AX3E1-3D-balls.png | Bin 0 -> 51196 bytes .../static/images/vsepr/AX3E2-3D-balls.png | Bin 0 -> 55912 bytes .../static/images/vsepr/AX4E0-3D-balls.png | Bin 0 -> 41665 bytes .../static/images/vsepr/AX4E1-3D-balls.png | Bin 0 -> 62210 bytes .../static/images/vsepr/AX4E2-3D-balls.png | Bin 0 -> 71325 bytes .../static/images/vsepr/AX5E1-3D-balls.png | Bin 0 -> 58554 bytes .../static/images/vsepr/AX5E2-3D-balls.png | Bin 0 -> 64400 bytes .../static/images/vsepr/AX6E0-3D-balls.png | Bin 0 -> 47023 bytes .../static/images/vsepr/AX6E1-3D-balls.png | Bin 0 -> 53875 bytes .../static/images/vsepr/AX7E0-3D-balls.png | Bin 0 -> 46381 bytes .../static/images/vsepr/AX8E0-3D-balls.png | Bin 0 -> 55764 bytes .../static/images/vsepr/AX9E0-3D-balls.png | Bin 0 -> 54388 bytes .../static/images/vsepr/Bent-3D-balls.png | Bin 0 -> 51200 bytes .../static/images/vsepr/Linear-3D-balls.png | Bin 0 -> 36709 bytes .../static/images/vsepr/Linear-stick.png | Bin 0 -> 42098 bytes .../images/vsepr/Octahedral-3D-balls.png | Bin 0 -> 50231 bytes .../static/images/vsepr/Octahedral-stick.png | Bin 0 -> 98842 bytes .../vsepr/Pentagonal-bipyramidal-3D-balls.png | Bin 0 -> 49557 bytes .../vsepr/Pentagonal-planar-3D-balls.png | Bin 0 -> 55783 bytes .../vsepr/Pentagonal-pyramidal-3D-balls.png | Bin 0 -> 58082 bytes .../images/vsepr/Pyramidal-3D-balls.png | Bin 0 -> 53022 bytes .../static/images/vsepr/Seesaw-3D-balls.png | Bin 0 -> 54193 bytes .../vsepr/Square-antiprismatic-3D-balls.png | Bin 0 -> 69611 bytes .../images/vsepr/Square-planar-3D-balls.png | Bin 0 -> 57831 bytes .../static/images/vsepr/T-shaped-3D-balls.png | Bin 0 -> 45726 bytes .../images/vsepr/Tetrahedral-3D-balls.png | Bin 0 -> 43145 bytes .../static/images/vsepr/Tetrahedral-stick.png | Bin 0 -> 65541 bytes .../static/images/vsepr/Trigonal-3D-balls.png | Bin 0 -> 45910 bytes .../vsepr/Trigonal-bipyramidal-3D-balls.png | Bin 0 -> 38729 bytes .../vsepr/Trigonal-bipyramidal-stick.png | Bin 0 -> 82648 bytes .../images/vsepr/Trigonal-planar-stick.png | Bin 0 -> 62006 bytes xblocks_contrib/problem/static/js/README | 1 + .../problem/static/js/annotationinput.js | 98 + .../static/js/chemical_equation_preview.js | 36 + .../problem/static/js/choicetextinput.js | 73 + .../problem/static/js/design-protein-2d.js | 85 + .../problem/static/js/drag_and_drop.js | 25 + .../static/js/drag_and_drop/base_image.js | 45 + .../static/js/drag_and_drop/config_parser.js | 271 + .../static/js/drag_and_drop/container.js | 13 + .../js/drag_and_drop/draggable_events.js | 143 + .../js/drag_and_drop/draggable_logic.js | 400 ++ .../static/js/drag_and_drop/draggables.js | 281 + .../problem/static/js/drag_and_drop/main.js | 107 + .../static/js/drag_and_drop/scroller.js | 162 + .../problem/static/js/drag_and_drop/state.js | 95 + .../static/js/drag_and_drop/targets.js | 266 + .../static/js/drag_and_drop/update_input.js | 373 + .../problem/static/js/edit-a-gene.js | 71 + .../problem/static/js/fixtures/jsinput.html | 53 + ...1B31BA00E7CE7B6BD63DD13A8586A45.cache.html | 652 ++ ...3308EE54E8033A708B414CAC05B0C32.cache.html | 642 ++ ...AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html | 628 ++ ...B4F4D4EFA24CDE2E4287CC07897F249.cache.html | 654 ++ ...069AC107D79C29D6237614AC340F0C0.cache.html | 652 ++ ...6220FCC8B9234FEAD8D826A73C6D2A4.cache.html | 642 ++ .../problem/static/js/genex/clear.cache.gif | Bin 0 -> 43 bytes .../problem/static/js/genex/genex.css | 122 + .../problem/static/js/genex/genex.nocache.js | 18 + .../problem/static/js/genex/hosted.html | 365 + .../static/js/genex/images/circles.png | Bin 0 -> 753 bytes .../static/js/genex/images/circles_ie6.png | Bin 0 -> 316 bytes .../problem/static/js/genex/images/corner.png | Bin 0 -> 615 bytes .../static/js/genex/images/corner_ie6.png | Bin 0 -> 325 bytes .../static/js/genex/images/hborder.png | Bin 0 -> 1135 bytes .../static/js/genex/images/hborder_ie6.png | Bin 0 -> 595 bytes .../static/js/genex/images/thumb_horz.png | Bin 0 -> 78 bytes .../static/js/genex/images/thumb_vertical.png | Bin 0 -> 85 bytes .../static/js/genex/images/vborder.png | Bin 0 -> 174 bytes .../static/js/genex/images/vborder_ie6.png | Bin 0 -> 135 bytes .../static/js/jsinput/jsinput_example.css | 9 + .../static/js/jsinput/jsinput_example.html | 15 + .../static/js/jsinput/jsinput_example.js | 86 + ...9CC89519B0E1FCB47B935AC9FE13D7B.cache.html | 743 ++ ...E05B1CD5BFCAF7D53C7C64D84318178.cache.html | 750 ++ ...824A958AB642DC2213DFFDAC640BEAA.cache.html | 760 ++ ...9267DE8FB02F8B995B4A58C66C76E29.cache.html | 758 ++ ...275492F7098103BCB05F4F86ABF6218.cache.html | 750 ++ ...3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html | 754 ++ .../problem/static/js/protex/clear.cache.gif | Bin 0 -> 43 bytes .../problem/static/js/protex/hosted.html | 365 + .../problem/static/js/protex/protex.css | 57 + .../static/js/protex/protex.nocache.js | 18 + .../static/js/protex/scrollTableLoading.gif | Bin 0 -> 1434 bytes .../problem/static/js/schematicinput.js | 49 + .../js/spec/formula_equation_preview_spec.js | 453 ++ .../problem/static/js/spec/jsinput_spec.js | 28 + .../static/js/src/formula_equation_preview.js | 213 + .../problem/static/js/src/jschannel.js | 790 +++ .../problem/static/js/src/jsinput.js | 221 + .../problem/static/js/src/problem.js | 38 - .../js/symbolic_mathjax_preprocessor.js | 35 + xblocks_contrib/problem/stringify.py | 30 + .../problem/templates/problem.html | 7 - .../problem/tests/data/capa/prog1.py | 1 + .../problem/tests/data/capa/prog2.py | 1882 +++++ .../problem/tests/data/capa/prog3.py | 1 + .../problem/tests/test_capa_block.py | 4082 +++++++++++ xblocks_contrib/problem/tests/test_problem.py | 26 - .../problem/tests/test_stringify.py | 47 + xblocks_contrib/problem/xmlparser.py | 148 + 238 files changed, 61341 insertions(+), 225 deletions(-) create mode 100644 xblocks_contrib/problem/assets/fixtures/checkbox_problem.html create mode 100644 xblocks_contrib/problem/assets/fixtures/codeinput_problem.html create mode 100644 xblocks_contrib/problem/assets/fixtures/imageinput.html create mode 100644 xblocks_contrib/problem/assets/fixtures/imageinput.underscore create mode 100644 xblocks_contrib/problem/assets/fixtures/jsinput_problem.html create mode 100644 xblocks_contrib/problem/assets/fixtures/matlabinput_problem.html create mode 100644 xblocks_contrib/problem/assets/fixtures/problem.html create mode 100644 xblocks_contrib/problem/assets/fixtures/problem_content.html create mode 100644 xblocks_contrib/problem/assets/fixtures/problem_content_1240.html create mode 100644 xblocks_contrib/problem/assets/fixtures/radiobutton_problem.html create mode 100644 xblocks_contrib/problem/assets/karma_runner.js create mode 100644 xblocks_contrib/problem/assets/spec/collapsible_spec.js create mode 100644 xblocks_contrib/problem/assets/spec/display_spec.js create mode 100644 xblocks_contrib/problem/assets/spec/imageinput_spec.js create mode 100644 xblocks_contrib/problem/assets/spec_helpers/accessibility_tools.js create mode 100644 xblocks_contrib/problem/assets/spec_helpers/add_ajax_prefix.js create mode 100644 xblocks_contrib/problem/assets/spec_helpers/ajax_prefix.js create mode 100644 xblocks_contrib/problem/assets/spec_helpers/helper.js create mode 100644 xblocks_contrib/problem/assets/spec_helpers/i18n.js create mode 100644 xblocks_contrib/problem/assets/spec_helpers/jasmine-extensions.js create mode 100644 xblocks_contrib/problem/assets/spec_helpers/jasmine-imagediff.js create mode 100644 xblocks_contrib/problem/assets/spec_helpers/jasmine-waituntil.js create mode 100644 xblocks_contrib/problem/assets/spec_helpers/logger.js create mode 100644 xblocks_contrib/problem/assets/static/css/ProblemBlockDisplay.css create mode 100644 xblocks_contrib/problem/assets/static/js/.gitignore create mode 100644 xblocks_contrib/problem/assets/static/js/collapsible.js create mode 100644 xblocks_contrib/problem/assets/static/js/display.js create mode 100644 xblocks_contrib/problem/assets/static/js/imageinput.js create mode 100644 xblocks_contrib/problem/assets/static/js/javascript_loader.js create mode 100644 xblocks_contrib/problem/assets/static/js/schematic.js create mode 100644 xblocks_contrib/problem/assets/static/js/vendor/codemirror-compressed.js create mode 100644 xblocks_contrib/problem/assets/static/js/xmodule.js create mode 100644 xblocks_contrib/problem/capa/__init__.py create mode 100644 xblocks_contrib/problem/capa/capa_problem.py create mode 100755 xblocks_contrib/problem/capa/checker.py create mode 100644 xblocks_contrib/problem/capa/correctmap.py create mode 100644 xblocks_contrib/problem/capa/customrender.py create mode 100644 xblocks_contrib/problem/capa/errors.py create mode 100644 xblocks_contrib/problem/capa/inputtypes.py create mode 100644 xblocks_contrib/problem/capa/registry.py create mode 100644 xblocks_contrib/problem/capa/responsetypes.py create mode 100644 xblocks_contrib/problem/capa/safe_exec/README.rst create mode 100644 xblocks_contrib/problem/capa/safe_exec/__init__.py create mode 100644 xblocks_contrib/problem/capa/safe_exec/exceptions.py create mode 100644 xblocks_contrib/problem/capa/safe_exec/lazymod.py create mode 100644 xblocks_contrib/problem/capa/safe_exec/remote_exec.py create mode 100644 xblocks_contrib/problem/capa/safe_exec/safe_exec.py create mode 100644 xblocks_contrib/problem/capa/safe_exec/tests/__init__.py create mode 100644 xblocks_contrib/problem/capa/safe_exec/tests/test_files/pylib/constant.py create mode 100644 xblocks_contrib/problem/capa/safe_exec/tests/test_lazymod.py create mode 100644 xblocks_contrib/problem/capa/safe_exec/tests/test_remote_exec.py create mode 100644 xblocks_contrib/problem/capa/safe_exec/tests/test_safe_exec.py create mode 100644 xblocks_contrib/problem/capa/templates/annotationinput.html create mode 100644 xblocks_contrib/problem/capa/templates/chemicalequationinput.html create mode 100644 xblocks_contrib/problem/capa/templates/choicegroup.html create mode 100644 xblocks_contrib/problem/capa/templates/choicetext.html create mode 100644 xblocks_contrib/problem/capa/templates/clarification.html create mode 100644 xblocks_contrib/problem/capa/templates/codeinput.html create mode 100644 xblocks_contrib/problem/capa/templates/crystallography.html create mode 100644 xblocks_contrib/problem/capa/templates/designprotein2dinput.html create mode 100644 xblocks_contrib/problem/capa/templates/drag_and_drop_input.html create mode 100644 xblocks_contrib/problem/capa/templates/editageneinput.html create mode 100644 xblocks_contrib/problem/capa/templates/editamolecule.html create mode 100644 xblocks_contrib/problem/capa/templates/filesubmission.html create mode 100644 xblocks_contrib/problem/capa/templates/formulaequationinput.html create mode 100644 xblocks_contrib/problem/capa/templates/imageinput.html create mode 100644 xblocks_contrib/problem/capa/templates/jsinput.html create mode 100644 xblocks_contrib/problem/capa/templates/mathstring.html create mode 100644 xblocks_contrib/problem/capa/templates/matlabinput.html create mode 100644 xblocks_contrib/problem/capa/templates/optioninput.html create mode 100644 xblocks_contrib/problem/capa/templates/problem.html create mode 100644 xblocks_contrib/problem/capa/templates/problem_ajax.html create mode 100644 xblocks_contrib/problem/capa/templates/problem_notifications.html create mode 100644 xblocks_contrib/problem/capa/templates/schematicinput.html create mode 100644 xblocks_contrib/problem/capa/templates/solutionspan.html create mode 100644 xblocks_contrib/problem/capa/templates/status_span.html create mode 100644 xblocks_contrib/problem/capa/templates/textline.html create mode 100644 xblocks_contrib/problem/capa/templates/vsepr_input.html create mode 100644 xblocks_contrib/problem/capa/tests/__init__.py create mode 100644 xblocks_contrib/problem/capa/tests/helpers.py create mode 100644 xblocks_contrib/problem/capa/tests/response_xml_factory.py create mode 100644 xblocks_contrib/problem/capa/tests/test_answer_pool.py create mode 100644 xblocks_contrib/problem/capa/tests/test_capa_problem.py create mode 100644 xblocks_contrib/problem/capa/tests/test_correctmap.py create mode 100644 xblocks_contrib/problem/capa/tests/test_customrender.py create mode 100644 xblocks_contrib/problem/capa/tests/test_errors.py create mode 100644 xblocks_contrib/problem/capa/tests/test_files/dynamath_input.txt create mode 100644 xblocks_contrib/problem/capa/tests/test_files/extended_hints.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/extended_hints_checkbox.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/extended_hints_dropdown.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/extended_hints_multiple_choice.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/extended_hints_multiple_choice_with_html.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/extended_hints_numeric_input.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/extended_hints_text_input.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/extended_hints_with_errors.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/filename_convert_test.txt create mode 100644 xblocks_contrib/problem/capa/tests/test_files/js/mersenne-twister-min.js create mode 100644 xblocks_contrib/problem/capa/tests/test_files/js/test_problem_display.js create mode 100644 xblocks_contrib/problem/capa/tests/test_files/js/test_problem_generator.js create mode 100644 xblocks_contrib/problem/capa/tests/test_files/js/test_problem_grader.js create mode 100644 xblocks_contrib/problem/capa/tests/test_files/js/xproblem.js create mode 100644 xblocks_contrib/problem/capa/tests/test_files/snuggletex_2x+3y.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/snuggletex_correct.html create mode 100644 xblocks_contrib/problem/capa/tests/test_files/snuggletex_wrong.html create mode 100644 xblocks_contrib/problem/capa/tests/test_files/snuggletex_x+x+3y.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/targeted_feedback.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_files/targeted_feedback_multiple.xml create mode 100644 xblocks_contrib/problem/capa/tests/test_hint_functionality.py create mode 100644 xblocks_contrib/problem/capa/tests/test_html_render.py create mode 100644 xblocks_contrib/problem/capa/tests/test_input_templates.py create mode 100644 xblocks_contrib/problem/capa/tests/test_inputtypes.py create mode 100644 xblocks_contrib/problem/capa/tests/test_responsetypes.py create mode 100644 xblocks_contrib/problem/capa/tests/test_shuffle.py create mode 100644 xblocks_contrib/problem/capa/tests/test_targeted_feedback.py create mode 100644 xblocks_contrib/problem/capa/tests/test_util.py create mode 100644 xblocks_contrib/problem/capa/tests/test_xqueue_interface.py create mode 100644 xblocks_contrib/problem/capa/tests/test_xqueue_submission.py create mode 100644 xblocks_contrib/problem/capa/util.py create mode 100644 xblocks_contrib/problem/capa/xqueue_interface.py create mode 100644 xblocks_contrib/problem/capa/xqueue_submission.py create mode 100644 xblocks_contrib/problem/capa_block.py create mode 100644 xblocks_contrib/problem/markup.py delete mode 100644 xblocks_contrib/problem/problem.py create mode 100644 xblocks_contrib/problem/static/applets/Protex.jar create mode 100644 xblocks_contrib/problem/static/applets/genex.jar delete mode 100644 xblocks_contrib/problem/static/css/problem.css create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX2E0-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX2E1-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX2E2-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX2E3-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX3E0-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX3E1-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX3E2-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX4E0-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX4E1-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX4E2-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX5E1-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX5E2-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX6E0-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX6E1-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX7E0-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX8E0-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/AX9E0-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Bent-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Linear-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Linear-stick.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Octahedral-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Octahedral-stick.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Pentagonal-bipyramidal-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Pentagonal-planar-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Pentagonal-pyramidal-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Pyramidal-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Seesaw-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Square-antiprismatic-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Square-planar-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/T-shaped-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Tetrahedral-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Tetrahedral-stick.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Trigonal-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Trigonal-bipyramidal-3D-balls.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Trigonal-bipyramidal-stick.png create mode 100644 xblocks_contrib/problem/static/images/vsepr/Trigonal-planar-stick.png create mode 100644 xblocks_contrib/problem/static/js/README create mode 100644 xblocks_contrib/problem/static/js/annotationinput.js create mode 100644 xblocks_contrib/problem/static/js/chemical_equation_preview.js create mode 100644 xblocks_contrib/problem/static/js/choicetextinput.js create mode 100644 xblocks_contrib/problem/static/js/design-protein-2d.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/base_image.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/config_parser.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/container.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/draggable_events.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/draggable_logic.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/draggables.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/main.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/scroller.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/state.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/targets.js create mode 100644 xblocks_contrib/problem/static/js/drag_and_drop/update_input.js create mode 100644 xblocks_contrib/problem/static/js/edit-a-gene.js create mode 100644 xblocks_contrib/problem/static/js/fixtures/jsinput.html create mode 100644 xblocks_contrib/problem/static/js/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html create mode 100644 xblocks_contrib/problem/static/js/genex/63308EE54E8033A708B414CAC05B0C32.cache.html create mode 100644 xblocks_contrib/problem/static/js/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html create mode 100644 xblocks_contrib/problem/static/js/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html create mode 100644 xblocks_contrib/problem/static/js/genex/A069AC107D79C29D6237614AC340F0C0.cache.html create mode 100644 xblocks_contrib/problem/static/js/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html create mode 100644 xblocks_contrib/problem/static/js/genex/clear.cache.gif create mode 100644 xblocks_contrib/problem/static/js/genex/genex.css create mode 100644 xblocks_contrib/problem/static/js/genex/genex.nocache.js create mode 100644 xblocks_contrib/problem/static/js/genex/hosted.html create mode 100644 xblocks_contrib/problem/static/js/genex/images/circles.png create mode 100644 xblocks_contrib/problem/static/js/genex/images/circles_ie6.png create mode 100644 xblocks_contrib/problem/static/js/genex/images/corner.png create mode 100644 xblocks_contrib/problem/static/js/genex/images/corner_ie6.png create mode 100644 xblocks_contrib/problem/static/js/genex/images/hborder.png create mode 100644 xblocks_contrib/problem/static/js/genex/images/hborder_ie6.png create mode 100644 xblocks_contrib/problem/static/js/genex/images/thumb_horz.png create mode 100644 xblocks_contrib/problem/static/js/genex/images/thumb_vertical.png create mode 100644 xblocks_contrib/problem/static/js/genex/images/vborder.png create mode 100644 xblocks_contrib/problem/static/js/genex/images/vborder_ie6.png create mode 100644 xblocks_contrib/problem/static/js/jsinput/jsinput_example.css create mode 100644 xblocks_contrib/problem/static/js/jsinput/jsinput_example.html create mode 100644 xblocks_contrib/problem/static/js/jsinput/jsinput_example.js create mode 100644 xblocks_contrib/problem/static/js/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html create mode 100644 xblocks_contrib/problem/static/js/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html create mode 100644 xblocks_contrib/problem/static/js/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html create mode 100644 xblocks_contrib/problem/static/js/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html create mode 100644 xblocks_contrib/problem/static/js/protex/F275492F7098103BCB05F4F86ABF6218.cache.html create mode 100644 xblocks_contrib/problem/static/js/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html create mode 100644 xblocks_contrib/problem/static/js/protex/clear.cache.gif create mode 100644 xblocks_contrib/problem/static/js/protex/hosted.html create mode 100644 xblocks_contrib/problem/static/js/protex/protex.css create mode 100644 xblocks_contrib/problem/static/js/protex/protex.nocache.js create mode 100644 xblocks_contrib/problem/static/js/protex/scrollTableLoading.gif create mode 100644 xblocks_contrib/problem/static/js/schematicinput.js create mode 100644 xblocks_contrib/problem/static/js/spec/formula_equation_preview_spec.js create mode 100644 xblocks_contrib/problem/static/js/spec/jsinput_spec.js create mode 100644 xblocks_contrib/problem/static/js/src/formula_equation_preview.js create mode 100644 xblocks_contrib/problem/static/js/src/jschannel.js create mode 100644 xblocks_contrib/problem/static/js/src/jsinput.js delete mode 100644 xblocks_contrib/problem/static/js/src/problem.js create mode 100644 xblocks_contrib/problem/static/js/symbolic_mathjax_preprocessor.js create mode 100644 xblocks_contrib/problem/stringify.py delete mode 100644 xblocks_contrib/problem/templates/problem.html create mode 100644 xblocks_contrib/problem/tests/data/capa/prog1.py create mode 100644 xblocks_contrib/problem/tests/data/capa/prog2.py create mode 100644 xblocks_contrib/problem/tests/data/capa/prog3.py create mode 100644 xblocks_contrib/problem/tests/test_capa_block.py delete mode 100644 xblocks_contrib/problem/tests/test_problem.py create mode 100644 xblocks_contrib/problem/tests/test_stringify.py create mode 100644 xblocks_contrib/problem/xmlparser.py 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 ee7068b1..e62f048f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -8,29 +8,77 @@ appdirs==1.4.4 # via fs asgiref==3.11.1 # via django -boto3==1.42.49 +beautifulsoup4==4.14.3 + # via -r requirements/base.in +boto3==1.42.52 # via fs-s3fs -botocore==1.42.49 +botocore==1.42.52 # 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.11 # 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.3 # 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.1 + # 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.3 + # 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 c8cd6815..98798672 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -24,17 +24,21 @@ astroid==4.0.4 # -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.49 +boto3==1.42.52 # via # -r requirements/quality.txt # -r requirements/test.txt # fs-s3fs -botocore==1.42.49 +botocore==1.42.52 # 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.11 # -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,7 +215,15 @@ edx-opaque-keys==3.0.0 # via # -r requirements/quality.txt # -r requirements/test.txt -filelock==3.24.1 +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.24.3 # via # -r requirements/quality.txt # -r requirements/test.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.3 # 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,9 +374,13 @@ path==16.16.0 # -r requirements/quality.txt # -r requirements/test.txt # edx-i18n-tools +pillow==12.1.1 + # via + # -r requirements/quality.txt + # -r requirements/test.txt pip-tools==7.5.3 # via -r requirements/pip-tools.txt -platformdirs==4.9.1 +platformdirs==4.9.2 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -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,8 +511,18 @@ 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 @@ -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 @@ -429,14 +600,20 @@ tomlkit==0.14.0 # via # -r requirements/quality.txt # pylint -tox==4.36.0 +tox==4.40.0 # via # -r requirements/quality.txt # -r requirements/test.txt +tqdm==4.67.3 + # 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 30ce6cc9..c27e8659 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -27,16 +27,18 @@ babel==2.18.0 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.49 +boto3==1.42.52 # via # -r requirements/test.txt # fs-s3fs -botocore==1.42.49 +botocore==1.42.52 # via # -r requirements/test.txt # boto3 @@ -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.4 # via # -r requirements/test.txt # pytest-cov -cryptography==46.0.5 - # 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.11 # -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,11 +154,25 @@ 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 -filelock==3.24.1 +edx-submissions==3.12.2 + # via -r requirements/test.txt +edx-toggles==5.4.1 + # via -r requirements/test.txt +filelock==3.24.3 # via # -r requirements/test.txt # tox @@ -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.6.1 # 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.3 # 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,7 +306,9 @@ path==16.16.0 # via # -r requirements/test.txt # edx-i18n-tools -platformdirs==4.9.1 +pillow==12.1.1 + # via -r requirements/test.txt +platformdirs==4.9.2 # via # -r requirements/test.txt # tox @@ -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 @@ -330,8 +432,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 @@ -340,13 +446,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 @@ -375,13 +486,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.36.0 +tox==4.40.0 # via -r requirements/test.txt +tqdm==4.67.3 + # 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 3e934a17..01fc97fa 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -20,15 +20,17 @@ astroid==4.0.4 # 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.49 +boto3==1.42.52 # via # -r requirements/test.txt # fs-s3fs -botocore==1.42.49 +botocore==1.42.52 # via # -r requirements/test.txt # boto3 @@ -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.4 # 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,27 +101,68 @@ django==5.2.11 # -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 -filelock==3.24.1 +edx-submissions==3.12.2 + # via -r requirements/test.txt +edx-toggles==5.4.1 + # via -r requirements/test.txt +filelock==3.24.3 # via # -r requirements/test.txt # tox @@ -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.3 # 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,7 +277,9 @@ path==16.16.0 # via # -r requirements/test.txt # edx-i18n-tools -platformdirs==4.9.1 +pillow==12.1.1 + # via -r requirements/test.txt +platformdirs==4.9.2 # via # -r requirements/test.txt # pylint @@ -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,14 +370,22 @@ 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 @@ -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,18 +434,28 @@ 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 # python-slugify tomlkit==0.14.0 # via pylint -tox==4.36.0 +tox==4.40.0 # via -r requirements/test.txt +tqdm==4.67.3 + # 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 bab0e9a3..c579059a 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -14,13 +14,15 @@ asgiref==3.11.1 # via # -r requirements/base.txt # django +beautifulsoup4==4.14.3 + # via -r requirements/base.txt binaryornot==0.4.4 # via cookiecutter -boto3==1.42.49 +boto3==1.42.52 # via # -r requirements/base.txt # fs-s3fs -botocore==1.42.49 +botocore==1.42.52 # via # -r requirements/base.txt # boto3 @@ -29,18 +31,30 @@ cachetools==7.0.1 # 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,34 +62,79 @@ cookiecutter==2.6.0 coverage[toml]==7.13.4 # 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 -filelock==3.24.1 +edx-submissions==3.12.2 + # via -r requirements/base.txt +edx-toggles==5.4.1 + # via -r requirements/base.txt +filelock==3.24.3 # via # tox # virtualenv @@ -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.3 # 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,7 +233,9 @@ path==16.16.0 # via # -r requirements/base.txt # edx-i18n-tools -platformdirs==4.9.1 +pillow==12.1.1 + # via -r requirements/base.txt +platformdirs==4.9.2 # via # tox # virtualenv @@ -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,19 +293,28 @@ 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 @@ -207,6 +325,12 @@ 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 -tox==4.36.0 + # via + # -r requirements/base.txt + # python-slugify +tox==4.40.0 # via -r requirements/test.in +tqdm==4.67.3 + # 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 %} + + +
+ +
+ +
+ + +
+
+
diff --git a/xblocks_contrib/problem/static/js/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html b/xblocks_contrib/problem/static/js/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html new file mode 100644 index 00000000..ec8170eb --- /dev/null +++ b/xblocks_contrib/problem/static/js/genex/21B31BA00E7CE7B6BD63DD13A8586A45.cache.html @@ -0,0 +1,652 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/genex/63308EE54E8033A708B414CAC05B0C32.cache.html b/xblocks_contrib/problem/static/js/genex/63308EE54E8033A708B414CAC05B0C32.cache.html new file mode 100644 index 00000000..952e3b5f --- /dev/null +++ b/xblocks_contrib/problem/static/js/genex/63308EE54E8033A708B414CAC05B0C32.cache.html @@ -0,0 +1,642 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html b/xblocks_contrib/problem/static/js/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html new file mode 100644 index 00000000..95cb9628 --- /dev/null +++ b/xblocks_contrib/problem/static/js/genex/7AC57DC6EC8C1D8672DDF6E6D4EF57CC.cache.html @@ -0,0 +1,628 @@ + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html b/xblocks_contrib/problem/static/js/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html new file mode 100644 index 00000000..5c828c12 --- /dev/null +++ b/xblocks_contrib/problem/static/js/genex/9B4F4D4EFA24CDE2E4287CC07897F249.cache.html @@ -0,0 +1,654 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/genex/A069AC107D79C29D6237614AC340F0C0.cache.html b/xblocks_contrib/problem/static/js/genex/A069AC107D79C29D6237614AC340F0C0.cache.html new file mode 100644 index 00000000..bcf15330 --- /dev/null +++ b/xblocks_contrib/problem/static/js/genex/A069AC107D79C29D6237614AC340F0C0.cache.html @@ -0,0 +1,652 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html b/xblocks_contrib/problem/static/js/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html new file mode 100644 index 00000000..5ab12af7 --- /dev/null +++ b/xblocks_contrib/problem/static/js/genex/C6220FCC8B9234FEAD8D826A73C6D2A4.cache.html @@ -0,0 +1,642 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/genex/clear.cache.gif b/xblocks_contrib/problem/static/js/genex/clear.cache.gif new file mode 100644 index 0000000000000000000000000000000000000000..e565824aafafe632011b281cba976baf8b3ba89a GIT binary patch literal 43 qcmZ?wbhEHbWMp7uXkcLY4+e@qSs1y10y+#p0Fq%~V)9{Rum%7ZWeN!Z literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/genex/genex.css b/xblocks_contrib/problem/static/js/genex/genex.css new file mode 100644 index 00000000..459c854f --- /dev/null +++ b/xblocks_contrib/problem/static/js/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/static/js/genex/genex.nocache.js b/xblocks_contrib/problem/static/js/genex/genex.nocache.js new file mode 100644 index 00000000..11f9714a --- /dev/null +++ b/xblocks_contrib/problem/static/js/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. + diff --git a/xblocks_contrib/problem/static/js/genex/images/circles.png b/xblocks_contrib/problem/static/js/genex/images/circles.png new file mode 100644 index 0000000000000000000000000000000000000000..ba580a3acf7f66bd473c78a55832c0a4475d1a73 GIT binary patch literal 753 zcmVi9G?VSYr2bDi3f^NZoDf?(O{f($tc|C37bD z9PmO-piJ3#So@3yD!sRVl!;rpcG1SS=uEzp2L5ahb`HK8Du-@(I^mcv;+UR?%d(b(2KnUov2b~TBZCLN}peg844$4{a1*Y|}Km~t~sRO zvgK4yhdA~8iOEo=DWy<0<#DnpMSq$G+#{)>f%ix%Xy84P3L1Eiq=E+CBdMT)_ed&e z;5|}EsJF2}L+&Vbbmm7xN*%T2aNMT-u%t5D-f72~rqi%t_o}1EEZOlLb`tHdz4e2K}`9;e%MUDy+?J*)6`k@qvu+?zqLr)z0WT&S*^UFFq%Z^l*=bp^653c zRC8&G^3sPLHiruN^ip3+xHLz3k;V?2!l!0ZmM@uHs{Zc;0b@{u8q}Z$HK;)iYEXk3 j)Sw16s6h?-cc5PZmo*7u>_Phb00000NkvXXu0mjfUQ}wx literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/genex/images/circles_ie6.png b/xblocks_contrib/problem/static/js/genex/images/circles_ie6.png new file mode 100644 index 0000000000000000000000000000000000000000..466f11923f25e67e65db2996da5a94780012965a GIT binary patch literal 316 zcmV-C0mJ@@P)6BCH z+e70idW64K2y!< z^Lt9|g<49VtfsVy!m8wbWlR=pMqwf66!vg{oKnOvqMQo5%;Nrl@0 literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/genex/images/corner.png b/xblocks_contrib/problem/static/js/genex/images/corner.png new file mode 100644 index 0000000000000000000000000000000000000000..d0b8aad882a9cab0a27a2ea3fda6103998ad26c8 GIT binary patch literal 615 zcmeAS@N?(olHy`uVBq!ia0vp^Qb6p?!3-p~iatFIq!tDEgt!9f3s1gnL<5^PK?ERD zKyuQ-4{#A6d+*IJKrT>x_l+<6E%MdMfP#l_zJu#J_3$fD z3a9`m4p9$b?Ax~=A`T>hBKPh;y7%DGr?0<0fBp64+pqus|8Lk8?g(_HZb^_|Fax88 znT4~LcTjXpYHCJ7O+!;#XJ^;+8M9|EUbbxc>b1Le@7{ap%=x?bp1t_+wZCm`Hv@Su?=yG$;srn7e-SwSMUOc=qg`={nVy@Rm%|l(&KG;(^M|FuHJ<~TXuP0 zU+_^E4I@T|g|lCM=Df~7sZV%YP+=2L*!6hkrv62LSqz+<%`IZrFFzn_RJ3@tPLZZ> z(`v050=MM#S3RCLq2BCg3&SerLW>T^sqq#!4;)b)FVdQ&MBb@ E0MKkjI{*Lx literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/genex/images/corner_ie6.png b/xblocks_contrib/problem/static/js/genex/images/corner_ie6.png new file mode 100644 index 0000000000000000000000000000000000000000..853d5b038d3b3bbd503cde46b833851925deb82b GIT binary patch literal 325 zcmV-L0lNN)P)0{{R3wH4*V0000yP)t-s0002H zySvQH%)Pz6v9Ym|!SJTb@WR~nzP`WS-{SQ4`pV(;-r(ZA(&y0R_VV@m^Y!|^)93&H z|HIktI=ZH#00001bW%=J06^y0W&i*Hw@E}nRCod1)Q5`0Kn#T8+2)@9{_lE1*aJvt zujT4b9lpdZ1;AN@=7tsy$&QF>hf0Ss)|k#Qf#M02_fUMmGwV?8Q0Y((XpQOQ=&(oi zq_*BfJfTV|o=~)$;R#uL?}Lho>;Dj+uH>mON0}K)41l5gUXNt`IbvV`cqKkv7|JDW z%d(}E9i_BrDH)=u8Je$qv!&_6CPPyAr#BvJbaZBxIHaeL)HFzjhPF32YRnQrdIr4! Xt(royiGTJl00000NkvXXu0mjfLx-WX literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/genex/images/hborder.png b/xblocks_contrib/problem/static/js/genex/images/hborder.png new file mode 100644 index 0000000000000000000000000000000000000000..bbc37366ff865eff8e79b9236dc06c8e1a83fc86 GIT binary patch literal 1135 zcmb8udrVVT90%}g>6!{dAUZU-p)OmZsEH3^R0J*bLhBqDU=I^@WYJ_u6tv>(U<)Y5 zs6+{g8Z|SFxTt|t7&1(Yw51@(2!rx02)%vKCw<-9d+wvO_3(!Ye;ACPoaB5@e&?6- z$2sTP;q0vF$b?7)L88SXVJ@84BS_RuegsS>TV?eKvdnb&P(BKmi#5Nq_27kV-BB@IBn?_vLEiQW{F7K{Ydv9xe_qu(5_TY7|@kjl{lL4Y( z(Em(Jwv3Q%V?eG03Wefj`^%0gN~u(;)#}b^s;jH3TTl1)^t?9EeSLlXMtWdiV9*SP zhK96S?eGj3fo8y{9*kmO3SiX?HUY$ip}xH*mt>W#%9cnDa6&$V<6=j2rUNZk+W%IPHOfV7&zEg9ygwXK^2n z`x%^M36k|gfCUr)RDhxbG|kY=Jj2ejfdCr_&d&#f!B7Z}P$(P@uUYr>GkC}R^SRl1 zQL*t~Y&&^P(OWoyAqZ!uSSZN9xXd-(*1Sp(%fnJ#waA7VWJf%|0y)nAzih{8X}%8s68O-?<=ecV@v|S&8WCKC~|9vLPJW%v=2MK9I2tdFK`mQGR7p1m!U0%}jrd#*(B literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/genex/images/hborder_ie6.png b/xblocks_contrib/problem/static/js/genex/images/hborder_ie6.png new file mode 100644 index 0000000000000000000000000000000000000000..2add622a58817d794dddc3ee051a47e43b2f5f13 GIT binary patch literal 595 zcmeAS@N?(olHy`uVBq!ia0vp^3JeS!xg5+u){)-3!a%Anz$e62*Q?CHx7sAM-6Cd^ zP0~E;>~)UWYi#p2xfX48F4^r-dBC&!=%j-m_U_p`_3+1;M?W4pa%9Pw&#NzdIdkUB z_G{mE-Tb!i_V?@8uOGetHo9!-1w)h4hrxGHl@NSP*Qb2@Q}%KY@llCS-QS1?bh lyw3s*)MFl=OyVb<_b_}l^gH7-an%-(`JS$RF6*2UngB4K)L8%k literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/genex/images/thumb_horz.png b/xblocks_contrib/problem/static/js/genex/images/thumb_horz.png new file mode 100644 index 0000000000000000000000000000000000000000..6c9baa03c7bf18af5d33d7c029801abc0a066693 GIT binary patch literal 78 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)I0VEjG*H$eAQlg$Njv*C{$p&RS9p~4+O!~pk a_@ah^`?#?DTR|2hkU~#aKbLh*2~7ZMe-g?7 literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/genex/images/thumb_vertical.png b/xblocks_contrib/problem/static/js/genex/images/thumb_vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..5ea57dbd77a976a40376d3c122e058d5f899dab7 GIT binary patch literal 85 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c80VEi>_%`zZDH%@}$B+uf~HMv hY@FXH@6)i8LyRGECbQ!GnJJ|p^`5SNF6*2Ung9`E7OVgO literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/genex/images/vborder.png b/xblocks_contrib/problem/static/js/genex/images/vborder.png new file mode 100644 index 0000000000000000000000000000000000000000..070d25184ff6ab8880a7af038cf35d09eabeb177 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^xF!Z!a(8Z7>jUd-z#G zr0=6dTcO{5p(Vx>LaskpaL1EliDRPNN#Wf3kCG{o+i!V=o}In-=_y*ww37A3S() z;mNn>&!4}4|Ni60k6*rg`Tzg_GwB63K$R+s^>;Bd?7sZpakI%8YoK}tPgg&ebxsLQ098RcY5)KL literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/jsinput/jsinput_example.css b/xblocks_contrib/problem/static/js/jsinput/jsinput_example.css new file mode 100644 index 00000000..8f1144ac --- /dev/null +++ b/xblocks_contrib/problem/static/js/jsinput/jsinput_example.css @@ -0,0 +1,9 @@ +.directions { + font-size: large +} + +.feedback { + font-size: medium; + border: 2px solid cornflowerblue; + padding: 5px; +} diff --git a/xblocks_contrib/problem/static/js/jsinput/jsinput_example.html b/xblocks_contrib/problem/static/js/jsinput/jsinput_example.html new file mode 100644 index 00000000..caa034ff --- /dev/null +++ b/xblocks_contrib/problem/static/js/jsinput/jsinput_example.html @@ -0,0 +1,15 @@ + + + + Dropdown with Dynamic Text + + + + + + + + + diff --git a/xblocks_contrib/problem/static/js/jsinput/jsinput_example.js b/xblocks_contrib/problem/static/js/jsinput/jsinput_example.js new file mode 100644 index 00000000..f3ee144a --- /dev/null +++ b/xblocks_contrib/problem/static/js/jsinput/jsinput_example.js @@ -0,0 +1,86 @@ +/* globals Channel */ + +(function() { + 'use strict'; + + // state will be populated via initial_state via the `setState` method. Defining dummy values here + // to make the expected structure clear. + var state = { + availableChoices: [], + selectedChoice: '' + }, + channel, + select = document.getElementsByClassName('choices')[0], + feedback = document.getElementsByClassName('feedback')[0]; + + function populateSelect() { + // Populate the select from `state.availableChoices`. + var i, option; + + // Clear out any pre-existing options. + while (select.firstChild) { + select.removeChild(select.firstChild); + } + + // Populate the select with the available choices. + for (i = 0; i < state.availableChoices.length; i++) { + option = document.createElement('option'); + option.value = i; + option.innerHTML = state.availableChoices[i]; + if (state.availableChoices[i] === state.selectedChoice) { + option.selected = true; + } + select.appendChild(option); + } + feedback.innerText = "The currently selected answer is '" + state.selectedChoice + "'."; + } + + function getGrade() { + // The following return value may or may not be used to grade server-side. + // If getState and setState are used, then the Python grader also gets access + // to the return value of getState and can choose it instead to grade. + return JSON.stringify(state.selectedChoice); + } + + function getState() { + // Returns the current state (which can be used for grading). + return JSON.stringify(state); + } + + // This function will be called with 1 argument when JSChannel is not used, + // 2 otherwise. In the latter case, the first argument is a transaction + // object that will not be used here + // (see http://mozilla.github.io/jschannel/docs/) + function setState() { + var stateString = arguments.length === 1 ? arguments[0] : arguments[1]; + state = JSON.parse(stateString); + populateSelect(); + } + + // Establish a channel only if this application is embedded in an iframe. + // This will let the parent window communicate with this application using + // RPC and bypass SOP restrictions. + if (window.parent !== window) { + channel = Channel.build({ + window: window.parent, + origin: '*', + scope: 'JSInput' + }); + + channel.bind('getGrade', getGrade); + channel.bind('getState', getState); + channel.bind('setState', setState); + } + + select.addEventListener('change', function() { + state.selectedChoice = select.options[select.selectedIndex].text; + feedback.innerText = "You have selected '" + state.selectedChoice + + "'. Click Submit to grade your answer."; + }); + + return { + getState: getState, + setState: setState, + getGrade: getGrade + }; +}()); diff --git a/xblocks_contrib/problem/static/js/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html b/xblocks_contrib/problem/static/js/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html new file mode 100644 index 00000000..e5db692a --- /dev/null +++ b/xblocks_contrib/problem/static/js/protex/39CC89519B0E1FCB47B935AC9FE13D7B.cache.html @@ -0,0 +1,743 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html b/xblocks_contrib/problem/static/js/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html new file mode 100644 index 00000000..ce6b4448 --- /dev/null +++ b/xblocks_contrib/problem/static/js/protex/6E05B1CD5BFCAF7D53C7C64D84318178.cache.html @@ -0,0 +1,750 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html b/xblocks_contrib/problem/static/js/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html new file mode 100644 index 00000000..f84a8b98 --- /dev/null +++ b/xblocks_contrib/problem/static/js/protex/C824A958AB642DC2213DFFDAC640BEAA.cache.html @@ -0,0 +1,760 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html b/xblocks_contrib/problem/static/js/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html new file mode 100644 index 00000000..b6fc4f6a --- /dev/null +++ b/xblocks_contrib/problem/static/js/protex/D9267DE8FB02F8B995B4A58C66C76E29.cache.html @@ -0,0 +1,758 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/protex/F275492F7098103BCB05F4F86ABF6218.cache.html b/xblocks_contrib/problem/static/js/protex/F275492F7098103BCB05F4F86ABF6218.cache.html new file mode 100644 index 00000000..311c82b3 --- /dev/null +++ b/xblocks_contrib/problem/static/js/protex/F275492F7098103BCB05F4F86ABF6218.cache.html @@ -0,0 +1,750 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html b/xblocks_contrib/problem/static/js/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html new file mode 100644 index 00000000..f39f0d68 --- /dev/null +++ b/xblocks_contrib/problem/static/js/protex/F3301B0E65F38C7FCF2EF3764B3BB0B6.cache.html @@ -0,0 +1,754 @@ + + + + \ No newline at end of file diff --git a/xblocks_contrib/problem/static/js/protex/clear.cache.gif b/xblocks_contrib/problem/static/js/protex/clear.cache.gif new file mode 100644 index 0000000000000000000000000000000000000000..e565824aafafe632011b281cba976baf8b3ba89a GIT binary patch literal 43 qcmZ?wbhEHbWMp7uXkcLY4+e@qSs1y10y+#p0Fq%~V)9{Rum%7ZWeN!Z literal 0 HcmV?d00001 diff --git a/xblocks_contrib/problem/static/js/protex/hosted.html b/xblocks_contrib/problem/static/js/protex/hosted.html new file mode 100644 index 00000000..48b87f39 --- /dev/null +++ b/xblocks_contrib/problem/static/js/protex/hosted.html @@ -0,0 +1,365 @@ + + + +This html file is for Development Mode support. + diff --git a/xblocks_contrib/problem/static/js/protex/protex.css b/xblocks_contrib/problem/static/js/protex/protex.css new file mode 100644 index 00000000..b0300c33 --- /dev/null +++ b/xblocks_contrib/problem/static/js/protex/protex.css @@ -0,0 +1,57 @@ +.protex-absolute-panel { + background-color: #BBBBBB; +} + +.protex-canvas { + outline: none; +} + +.protex-caption-panel { + padding: 10px 10px 10px 10px; + margin: 0px 0px 0px 0px; + border:2px solid #4E4E4E; + font: normal normal normal 14pt/normal 'Open Sans', sans-serif; + background-color: #B2B2FF; +} + +.protex-textbox { + font: normal normal normal 12pt/normal 'Open Sans', sans-serif !important; + padding: 2px 2px 2px 2px !important; +} + +.protex-caption-panel-ss-bonds-on { + padding: 10px 10px 10px 10px; + margin: 0px 0px 0px 0px; + border:2px solid #4E4E4E; + font: normal normal normal 14pt/normal 'Open Sans', sans-serif; + background-color: #B2FFFF; +} + +.protex-button { + margin-right: 15px; +} + +.protex-listbox { +} + +.gwt-ProgressBar-shell { + border: 2px solid #faf9f7; + border-right: 2px solid #848280; + border-bottom: 2px solid #848280; + background-color: #AAAAAA; + height: 14pt; + width: 50%; + margin-left: 15px; +} +.gwt-ProgressBar-shell .gwt-ProgressBar-bar { + background-color: #67A7E3; +} +.gwt-ProgressBar-shell .gwt-ProgressBar-text { + padding: 0px; + margin: 0px; + color: white; +} + +div#protex-panel[style] { + overflow: scroll !important; +} diff --git a/xblocks_contrib/problem/static/js/protex/protex.nocache.js b/xblocks_contrib/problem/static/js/protex/protex.nocache.js new file mode 100644 index 00000000..77f770bf --- /dev/null +++ b/xblocks_contrib/problem/static/js/protex/protex.nocache.js @@ -0,0 +1,18 @@ +function protex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='39CC89519B0E1FCB47B935AC9FE13D7B',Rb='6E05B1CD5BFCAF7D53C7C64D84318178',Wb=':',pb='::',dc=' + + """) + factory = self.capa_factory_for_problem_xml(xml_str) + + # When codejail safe_exec fails upon problem creation, a LoncapaProblemError should be raised. + with pytest.raises(LoncapaProblemError): + with patch("xblocks_contrib.problem.capa.capa_problem.safe_exec") as mock_safe_exec: + mock_safe_exec.side_effect = SafeExecException() + factory.create() + + def _rescore_problem_error_helper(self, exception_class): + """Helper to allow testing all errors that rescoring might return.""" + # Create the block + block = CapaFactory.create(attempts=0) + CapaFactory.answer_key() + + # Check the problem + get_request_dict = {CapaFactory.input_key(): "1"} + block.submit_problem(get_request_dict) + + # Simulate answering a problem that raises the exception + with patch( + "xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.get_grade_from_current_answers" + ) as mock_rescore: + mock_rescore.side_effect = exception_class("test error \u03a9") + with pytest.raises(exception_class): + block.rescore(only_if_higher=False) + + # Expect that the number of attempts is NOT incremented + assert block.attempts == 1 + # and that this was considered the first attempt for grading purposes + assert block.lcp.context["attempt"] == 1 + + def test_rescore_problem_student_input_error(self): + """Ensure StudentInputError during rescore is handled correctly.""" + self._rescore_problem_error_helper(StudentInputError) + + def test_rescore_problem_problem_error(self): + """Ensure LoncapaProblemError during rescore is handled correctly.""" + self._rescore_problem_error_helper(LoncapaProblemError) + + def test_rescore_problem_response_error(self): + """Ensure ResponseError during rescore is handled correctly.""" + self._rescore_problem_error_helper(ResponseError) + + def test_save_problem(self): + """Verify saving a problem persists answers and returns success.""" + block = CapaFactory.create(done=False) + + # Save the problem + get_request_dict = {CapaFactory.input_key(): "3.14"} + result = block.save_problem(get_request_dict) + + # Expect that answers are saved to the problem + expected_answers = {CapaFactory.answer_key(): "3.14"} + assert block.lcp.student_answers == expected_answers + + # Expect that the result is success + assert ("success" in result) and result["success"] + + def test_save_problem_closed(self): + """Ensure saving a closed problem fails.""" + block = CapaFactory.create(done=False) + + # Simulate that the problem is closed + with patch("xblocks_contrib.problem.capa_block.ProblemBlock.closed") as mock_closed: + mock_closed.return_value = True + + # Try to save the problem + get_request_dict = {CapaFactory.input_key(): "3.14"} + result = block.save_problem(get_request_dict) + + # Expect that the result is failure + assert ("success" in result) and (not result["success"]) + + @ddt.data(RANDOMIZATION.ALWAYS, "true") + def test_save_problem_submitted_with_randomize(self, rerandomize): + """Verify saving fails when problem is submitted and rerandomization is enabled.""" + # Capa XModule treats 'always' and 'true' equivalently + block = CapaFactory.create(rerandomize=rerandomize, done=True) + + # Try to save + get_request_dict = {CapaFactory.input_key(): "3.14"} + result = block.save_problem(get_request_dict) + + # Expect that we cannot save + assert ("success" in result) and (not result["success"]) + + @ddt.data(RANDOMIZATION.NEVER, "false", RANDOMIZATION.PER_STUDENT) + def test_save_problem_submitted_no_randomize(self, rerandomize): + """Verify saving succeeds when problem is submitted without rerandomization.""" + # Capa XBlock treats 'false' and 'per_student' equivalently + block = CapaFactory.create(rerandomize=rerandomize, done=True) + + # Try to save + get_request_dict = {CapaFactory.input_key(): "3.14"} + result = block.save_problem(get_request_dict) + + # Expect that we succeed + assert ("success" in result) and result["success"] + + def test_submit_button_name(self): + """Verify the submit button label is correct.""" + block = CapaFactory.create(attempts=0) + assert block.submit_button_name() == "Submit" + + def test_submit_button_submitting_name(self): + """Verify the submitting button label is correct.""" + block = CapaFactory.create(attempts=1, max_attempts=10) + assert block.submit_button_submitting_name() == "Submitting" + + def test_should_enable_submit_button(self): + """Verify submit button enablement logic across deadlines, attempts, and states.""" + + attempts = random.randint(1, 10) + + # If we're after the deadline, disable the submit button + block = CapaFactory.create(due=self.yesterday_str) + assert not block.should_enable_submit_button() + + # If user is out of attempts, disable the submit button + block = CapaFactory.create(attempts=attempts, max_attempts=attempts) + assert not block.should_enable_submit_button() + + # If survey question (max_attempts = 0), disable the submit button + block = CapaFactory.create(max_attempts=0) + assert not block.should_enable_submit_button() + + # If user submitted a problem but hasn't reset, + # disable the submit button + # Note: we can only reset when rerandomize="always" or "true" + block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True) + assert not block.should_enable_submit_button() + + block = CapaFactory.create(rerandomize="true", done=True) + assert not block.should_enable_submit_button() + + # Otherwise, enable the submit button + block = CapaFactory.create() + assert block.should_enable_submit_button() + + # If the user has submitted the problem + # and we do NOT have a reset button, then we can enable the submit button + # Setting rerandomize to "never" or "false" ensures that the reset button + # is not shown + block = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, done=True) + assert block.should_enable_submit_button() + + block = CapaFactory.create(rerandomize="false", done=True) + assert block.should_enable_submit_button() + + block = CapaFactory.create(rerandomize=RANDOMIZATION.PER_STUDENT, done=True) + assert block.should_enable_submit_button() + + def test_should_show_reset_button(self): + """Verify reset button visibility logic across problem states and settings.""" + + attempts = random.randint(1, 10) + + # If we're after the deadline, do NOT show the reset button + block = CapaFactory.create(due=self.yesterday_str, done=True) + assert not block.should_show_reset_button() + + # If the user is out of attempts, do NOT show the reset button + block = CapaFactory.create(attempts=attempts, max_attempts=attempts, done=True) + assert not block.should_show_reset_button() + + # pre studio default value, DO show the reset button + block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True) + assert block.should_show_reset_button() + + # If survey question for capa (max_attempts = 0), + # DO show the reset button + block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True) + assert block.should_show_reset_button() + + # If the question is not correct + # DO show the reset button + block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True, correct=False) + assert block.should_show_reset_button() + + # If the question is correct and randomization is never + # DO not show the reset button + block = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, max_attempts=0, done=True, correct=True) + assert not block.should_show_reset_button() + + # If the question is correct and randomization is always + # Show the reset button + block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True, correct=True) + assert block.should_show_reset_button() + + # Don't show reset button if randomization is turned on and the question is not done + block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, show_reset_button=False, done=False) + assert not block.should_show_reset_button() + + # Show reset button if randomization is turned on and the problem is done + block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, show_reset_button=False, done=True) + assert block.should_show_reset_button() + + def test_should_show_save_button(self): + """Verify save button visibility logic across attempts, deadlines, and randomization.""" + + attempts = random.randint(1, 10) + + # If we're after the deadline, do NOT show the save button + block = CapaFactory.create(due=self.yesterday_str, done=True) + assert not block.should_show_save_button() + + # If the user is out of attempts, do NOT show the save button + block = CapaFactory.create(attempts=attempts, max_attempts=attempts, done=True) + assert not block.should_show_save_button() + + # If user submitted a problem but hasn't reset, do NOT show the save button + block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True) + assert not block.should_show_save_button() + + block = CapaFactory.create(rerandomize="true", done=True) + assert not block.should_show_save_button() + + # If the user has unlimited attempts and we are not randomizing, + # then do NOT show a save button + # because they can keep using "Check" + block = CapaFactory.create(max_attempts=None, rerandomize=RANDOMIZATION.NEVER, done=False) + assert not block.should_show_save_button() + + block = CapaFactory.create(max_attempts=None, rerandomize="false", done=True) + assert not block.should_show_save_button() + + block = CapaFactory.create(max_attempts=None, rerandomize=RANDOMIZATION.PER_STUDENT, done=True) + assert not block.should_show_save_button() + + # pre-studio default, DO show the save button + block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=False) + assert block.should_show_save_button() + + # If we're not randomizing and we have limited attempts, then we can save + block = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, max_attempts=2, done=True) + assert block.should_show_save_button() + + block = CapaFactory.create(rerandomize="false", max_attempts=2, done=True) + assert block.should_show_save_button() + + block = CapaFactory.create(rerandomize=RANDOMIZATION.PER_STUDENT, max_attempts=2, done=True) + assert block.should_show_save_button() + + # If survey question for capa (max_attempts = 0), + # DO show the save button + block = CapaFactory.create(max_attempts=0, done=False) + assert block.should_show_save_button() + + def test_should_show_save_button_force_save_button(self): + """Verify force_save_button overrides normal save button visibility rules.""" + # If we're after the deadline, do NOT show the save button + # even though we're forcing a save + block = CapaFactory.create(due=self.yesterday_str, force_save_button="true", done=True) + assert not block.should_show_save_button() + + # If the user is out of attempts, do NOT show the save button + attempts = random.randint(1, 10) + block = CapaFactory.create(attempts=attempts, max_attempts=attempts, force_save_button="true", done=True) + assert not block.should_show_save_button() + + # Otherwise, if we force the save button, + # then show it even if we would ordinarily + # require a reset first + block = CapaFactory.create(force_save_button="true", rerandomize=RANDOMIZATION.ALWAYS, done=True) + assert block.should_show_save_button() + + block = CapaFactory.create(force_save_button="true", rerandomize="true", done=True) + assert block.should_show_save_button() + + def test_no_max_attempts(self): + """Ensure problems with empty max_attempts render without errors.""" + block = CapaFactory.create(max_attempts="") + html = block.get_problem_html() + assert html is not None + # assert that we got here without exploding + + @patch("xblocks_contrib.problem.capa_block.render_to_string") + def test_get_problem_html(self, render_template): + """Verify problem HTML rendering uses correct template context and encapsulation.""" + render_template.return_value = "
Test Template HTML
" + block = CapaFactory.create() + + # We've tested the show/hide button logic in other tests, + # so here we hard-wire the values + enable_submit_button = bool(random.randint(0, 1) % 2) + show_reset_button = bool(random.randint(0, 1) % 2) + show_save_button = bool(random.randint(0, 1) % 2) + + block.should_enable_submit_button = Mock(return_value=enable_submit_button) + block.should_show_reset_button = Mock(return_value=show_reset_button) + block.should_show_save_button = Mock(return_value=show_save_button) + + # Patch the capa problem's HTML rendering + with patch("xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.get_html") as mock_html: + mock_html.return_value = "
Test Problem HTML
" + + # Render the problem HTML + html = block.get_problem_html(encapsulate=False) + + # Also render the problem encapsulated in a
+ html_encapsulated = block.get_problem_html(encapsulate=True) + + # Expect that we get the rendered template back + assert html == "
Test Template HTML
" + + # Check the rendering context + render_args, _ = render_template.call_args + assert len(render_args) == 2 + + template_name = render_args[0] + assert template_name == "problem.html" + + context = render_args[1] + assert context["problem"]["html"] == "
Test Problem HTML
" + assert bool(context["should_enable_submit_button"]) == enable_submit_button + assert bool(context["reset_button"]) == show_reset_button + assert bool(context["save_button"]) == show_save_button + assert not context["demand_hint_possible"] + + # Assert that the encapsulated html contains the original html + assert html in html_encapsulated + + demand_xml = """ + +

That is the question

+ + + Alpha A hint + + Beta + + + + Demand 1 + Demand 2 + +
""" + + @patch("xblocks_contrib.problem.capa_block.render_to_string") + def test_demand_hint(self, render_template): + """Verify image-based demand hints render correctly without static URL issues.""" + # HTML generation is mocked out to be meaningless here, so instead we check + # the context dict passed into HTML generation. + render_template.return_value = "
Test Template HTML
" + block = CapaFactory.create(xml=self.demand_xml) + block.get_problem_html() # ignoring html result + context = render_template.call_args[0][1] + assert context["demand_hint_possible"] + assert context["should_enable_next_hint"] + + # Check the AJAX call that gets the hint by index + result = block.get_demand_hint(0) + assert result["hint_index"] == 0 + assert result["should_enable_next_hint"] + + result = block.get_demand_hint(1) + assert result["hint_index"] == 1 + assert not result["should_enable_next_hint"] + + result = block.get_demand_hint(2) # here the server wraps around to index 0 + assert result["hint_index"] == 0 + assert result["should_enable_next_hint"] + + @patch("xblocks_contrib.problem.capa_block.render_to_string") + def test_single_demand_hint(self, render_template): + """ + Test the hint button enabled state when there is just a single hint. + """ + test_xml = """ + +

That is the question

+ + + Alpha A hint + + Beta + + + + Only demand hint + +
""" + render_template.return_value = "
Test Template HTML
" + block = CapaFactory.create(xml=test_xml) + block.get_problem_html() # ignoring html result + context = render_template.call_args[0][1] + assert context["demand_hint_possible"] + assert context["should_enable_next_hint"] + + # Check the AJAX call that gets the hint by index + result = block.get_demand_hint(0) + assert result["hint_index"] == 0 + assert not result["should_enable_next_hint"] + + @patch("xblocks_contrib.problem.capa_block.render_to_string") + def test_image_hint(self, render_template): + """ + Test the hint button shows an image without the static url. + """ + test_xml = """ + +

That is the question

+ + + Alpha A hint + + Beta + + + + + + You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button. + +
""" + render_template.return_value = "
Test Template HTML
" + block = CapaFactory.create(xml=test_xml) + block.get_problem_html() # ignoring html result + context = render_template.call_args[0][1] + assert context["demand_hint_possible"] + assert context["should_enable_next_hint"] + + # Check the AJAX call that gets the hint by index + result = block.get_demand_hint(0) + assert result["hint_index"] == 0 + assert not result["should_enable_next_hint"] + + def test_demand_hint_logging(self): + """ + Test calling get_demand_hunt() results in an event being published. + """ + block = CapaFactory.create(xml=self.demand_xml) + with patch.object(block.runtime, "publish") as mock_publish: + block.get_problem_html() + block.get_demand_hint(0) + mock_publish.assert_called_with( + block, + "edx.problem.hint.demandhint_displayed", + {"hint_index": 0, "module_id": str(block.location), "hint_text": "Demand 1", "hint_len": 2}, + ) + + def test_input_state_consistency(self): + """Verify input_state keys remain consistent and isolated across block instances.""" + block1 = CapaFactory.create() + block2 = CapaFactory.create() + + # check to make sure that the input_state and the keys have the same values + block1.set_state_from_lcp() + assert list(block1.lcp.inputs.keys()) == list(block1.input_state.keys()) + + block2.set_state_from_lcp() + + intersection = set(block2.input_state.keys()).intersection(set(block1.input_state.keys())) + assert len(intersection) == 0 + + @patch("xblocks_contrib.problem.capa_block.render_to_string") + def test_get_problem_html_error(self, render_template): + """ + In production, when an error occurs with the problem HTML + rendering, a "dummy" problem is created with an error + message to display to the user. + """ + render_template.return_value = "
Test Template HTML
" + block = CapaFactory.create() + + # Save the original problem so we can compare it later + original_problem = block.lcp + + # Simulate throwing an exception when the capa problem + # is asked to render itself as HTML + block.lcp.get_html = Mock(side_effect=Exception("Test")) + + # Try to render the block with DEBUG turned off + html = block.get_problem_html() + + assert html is not None + + # Check the rendering context + render_args, _ = render_template.call_args + context = render_args[1] + assert "error" in context["problem"]["html"] + + # Expect that the block has created a new dummy problem with the error + assert original_problem != block.lcp + + @patch("xblocks_contrib.problem.capa_block.render_to_string") + def test_get_problem_html_error_preview(self, render_template): + """ + Test the html response when an error occurs with DEBUG off in Studio. + """ + render_template.return_value = "
Test Template HTML
" + block = CapaFactory.create() + + # Simulate throwing an exception when the capa problem + # is asked to render itself as HTML + error_msg = "Superterrible error happened: ☠" + block.lcp.get_html = Mock(side_effect=Exception(error_msg)) + + block.runtime.is_author_mode = True + + # Try to render the block with the author mode turned on + html = block.get_problem_html() + + assert html is not None + + # Check the rendering context + render_args, _ = render_template.call_args + context = render_args[1] + assert error_msg in context["problem"]["html"] + + @override_settings(DEBUG=True) + @patch("xblocks_contrib.problem.capa_block.render_to_string") + def test_get_problem_html_error_w_debug(self, render_template): + """ + Test the html response when an error occurs with DEBUG on + """ + render_template.return_value = "
Test Template HTML
" + block = CapaFactory.create() + block.runtime.is_author_mode = True + + # Simulate throwing an exception when the capa problem + # is asked to render itself as HTML + error_msg = "Superterrible error happened: ☠" + block.lcp.get_html = Mock(side_effect=Exception(error_msg)) + + # Try to render the block with DEBUG turned on + html = block.get_problem_html() + + assert html is not None + + # Check the rendering context + render_args, _ = render_template.call_args + context = render_args[1] + assert error_msg in context["problem"]["html"] + + @ddt.data( + "false", "true", RANDOMIZATION.NEVER, RANDOMIZATION.PER_STUDENT, RANDOMIZATION.ALWAYS, RANDOMIZATION.ONRESET + ) + def test_random_seed_no_change(self, rerandomize): + """Verify problem seed remains stable when rerandomization does not apply.""" + + # Run the test for each possible rerandomize value + + block = CapaFactory.create(rerandomize=rerandomize) + + # Get the seed + # By this point, the block should have persisted the seed + seed = block.seed + assert seed is not None + + # If we're not rerandomizing, the seed is always set + # to the same value (1) + if rerandomize == RANDOMIZATION.NEVER: + assert seed == 1, f"Seed should always be 1 when rerandomize='{rerandomize}'" + + # Check the problem + get_request_dict = {CapaFactory.input_key(): "3.14"} + block.submit_problem(get_request_dict) + + # Expect that the seed is the same + assert seed == block.seed + + # Save the problem + block.save_problem(get_request_dict) + + # Expect that the seed is the same + assert seed == block.seed + + @ddt.data( + "false", "true", RANDOMIZATION.NEVER, RANDOMIZATION.PER_STUDENT, RANDOMIZATION.ALWAYS, RANDOMIZATION.ONRESET + ) + def test_random_seed_with_reset(self, rerandomize): + """ + Run the test for each possible rerandomize value + """ + + def _reset_and_get_seed(block): + """ + Reset the XBlock and return the block's seed + """ + + # Simulate submitting an attempt + # We need to do this, or reset_problem() will + # fail because it won't re-randomize until the problem has been submitted + # the problem yet. + block.done = True + + # Reset the problem + block.reset_problem({}) + + # Return the seed + return block.seed + + def _retry_and_check(num_tries, test_func): + """ + Returns True if *test_func* was successful + (returned True) within *num_tries* attempts + + *test_func* must be a function + of the form test_func() -> bool + """ + success = False + for __ in range(num_tries): + if test_func() is True: + success = True + break + return success + + block = CapaFactory.create(rerandomize=rerandomize, done=True) + + # Get the seed + # By this point, the block should have persisted the seed + seed = block.seed + assert seed is not None + + # We do NOT want the seed to reset if rerandomize + # is set to 'never' -- it should still be 1 + # The seed also stays the same if we're randomizing + # 'per_student': the same student should see the same problem + if rerandomize in [RANDOMIZATION.NEVER, "false", RANDOMIZATION.PER_STUDENT]: + assert seed == _reset_and_get_seed(block) + + # Otherwise, we expect the seed to change + # to another valid seed + else: + + # Since there's a small chance (expected) we might get the + # same seed again, give it 60 chances + # to generate a different seed + success = _retry_and_check(60, lambda: _reset_and_get_seed(block) != seed) + + assert block.seed is not None + msg = "Could not get a new seed from reset after 60 tries" + assert success, msg + + @ddt.data( + "false", "true", RANDOMIZATION.NEVER, RANDOMIZATION.PER_STUDENT, RANDOMIZATION.ALWAYS, RANDOMIZATION.ONRESET + ) + def test_random_seed_with_reset_question_unsubmitted(self, rerandomize): + """ + Run the test for each possible rerandomize value + """ + + def _reset_and_get_seed(block): + """ + Reset the XBlock and return the block's seed + """ + + # Reset the problem + # By default, the problem is instantiated as unsubmitted + block.reset_problem({}) + + # Return the seed + return block.seed + + block = CapaFactory.create(rerandomize=rerandomize, done=False) + + # Get the seed + # By this point, the block should have persisted the seed + seed = block.seed + assert seed is not None + + # the seed should never change because the student hasn't finished the problem + assert seed == _reset_and_get_seed(block) + + @ddt.data(RANDOMIZATION.ALWAYS, RANDOMIZATION.PER_STUDENT, "true", RANDOMIZATION.ONRESET) + def test_random_seed_bins(self, rerandomize): + """Ensure generated random seeds fall within the expected numeric range.""" + # Assert that we are limiting the number of possible seeds. + # Get a bunch of seeds, they should all be in 0-999. + i = 200 + while i > 0: + block = CapaFactory.create(rerandomize=rerandomize) + assert 0 <= block.seed < 1000 + i -= 1 + + @patch("xblocks_contrib.problem.capa_block.log") + @patch("xblocks_contrib.problem.capa_block.Progress") + def test_get_progress_error(self, mock_progress, mock_log): + """ + Check that an exception given in `Progress` produces a `log.exception` call. + """ + error_types = [TypeError, ValueError] + for error_type in error_types: + mock_progress.side_effect = error_type + block = CapaFactory.create() + assert block.get_progress() is None + mock_log.exception.assert_called_once_with("Got bad progress") + mock_log.reset_mock() + + @patch("xblocks_contrib.problem.capa_block.Progress") + def test_get_progress_no_error_if_weight_zero(self, mock_progress): + """ + Check that if the weight is 0 get_progress does not try to create a Progress object. + """ + mock_progress.return_value = True + block = CapaFactory.create() + block.weight = 0 + progress = block.get_progress() + assert progress is None + assert not mock_progress.called + + @patch("xblocks_contrib.problem.capa_block.Progress") + def test_get_progress_calculate_progress_fraction(self, mock_progress): + """ + Check that score and total are calculated correctly for the progress fraction. + """ + block = CapaFactory.create() + block.weight = 1 + block.get_progress() + mock_progress.assert_called_with(0, 1) + + other_block = CapaFactory.create(correct=True) + other_block.weight = 1 + other_block.get_progress() + mock_progress.assert_called_with(1, 1) + + @ddt.data( + ("never", True, None), + ("never", False, None), + ("past_due", True, None), + ("past_due", False, None), + ("always", True, 1), + ("always", False, 0), + ) + @ddt.unpack + def test_get_display_progress_show_correctness(self, show_correctness, is_correct, expected_score): + """ + Check that score and total are calculated correctly for the progress fraction. + """ + block = CapaFactory.create(correct=is_correct, show_correctness=show_correctness, due=self.tomorrow_str) + block.weight = 1 + score, total = block.get_display_progress() + assert score == expected_score + assert total == 1 + + def test_get_html(self): + """ + Check that get_html() calls get_progress() with no arguments. + """ + block = CapaFactory.create() + block.get_progress = Mock(wraps=block.get_progress) + block.get_html() + block.get_progress.assert_called_with() + + def test_get_problem(self): + """ + Check that get_problem() returns the expected dictionary. + """ + block = CapaFactory.create() + assert block.get_problem("data") == {"html": block.get_problem_html(encapsulate=False)} + + # Standard question with shuffle="true" used by a few tests + common_shuffle_xml = textwrap.dedent(""" + + + + Apple + Banana + Chocolate + Donut + + + + """) + + def test_check_unmask(self): + """ + Check that shuffle unmasking is plumbed through: when submit_problem is called, + unmasked names should appear in the publish event_info. + """ + block = CapaFactory.create(xml=self.common_shuffle_xml) + with patch.object(block.runtime, "publish") as mock_publish: + get_request_dict = {CapaFactory.input_key(): "choice_3"} # the correct choice + block.submit_problem(get_request_dict) + mock_call = mock_publish.mock_calls[1] + event_info = mock_call[1][2] + assert event_info["answers"][CapaFactory.answer_key()] == "choice_3" + # 'permutation' key added to record how problem was shown + assert event_info["permutation"][CapaFactory.answer_key()] == ( + "shuffle", + ["choice_3", "choice_1", "choice_2", "choice_0"], + ) + assert event_info["success"] == "correct" + + def test_check_unmask_answerpool(self): + """Check answer-pool question publish uses unmasked names""" + xml = textwrap.dedent(""" + + + + Apple + Banana + Chocolate + Donut + + + + """) + block = CapaFactory.create(xml=xml) + with patch.object(block.runtime, "publish") as mock_publish: + get_request_dict = {CapaFactory.input_key(): "choice_2"} # mask_X form when masking enabled + block.submit_problem(get_request_dict) + mock_call = mock_publish.mock_calls[1] + event_info = mock_call[1][2] + assert event_info["answers"][CapaFactory.answer_key()] == "choice_2" + # 'permutation' key added to record how problem was shown + assert event_info["permutation"][CapaFactory.answer_key()] == ( + "answerpool", + ["choice_1", "choice_3", "choice_2", "choice_0"], + ) + assert event_info["success"] == "incorrect" + + @ddt.unpack + @ddt.data( + {"display_name": None, "expected_display_name": "problem"}, + {"display_name": "", "expected_display_name": "problem"}, + {"display_name": " ", "expected_display_name": "problem"}, + {"display_name": "CAPA 101", "expected_display_name": "CAPA 101"}, + ) + def test_problem_display_name_with_default(self, display_name, expected_display_name): + """ + Verify that display_name_with_default works as expected. + """ + block = CapaFactory.create(display_name=display_name) + assert block.display_name_with_default == expected_display_name + + @ddt.data( + "", + " ", + ) + @patch("xblocks_contrib.problem.capa_block.render_to_string") + def test_problem_no_display_name(self, display_name, render_template): + """ + Verify that if problem display name is not provided then a default name is used. + """ + render_template.return_value = "
Test Template HTML
" + block = CapaFactory.create(display_name=display_name) + block.get_problem_html() + render_args, _ = render_template.call_args + context = render_args[1] + assert context["problem"]["name"] == block.location.block_type + + +@ddt.ddt +@pytest.mark.django_db +class ProblemBlockXMLTest(unittest.TestCase): + """Tests XML strings for various problem types in XBlocks.""" + + sample_checkbox_problem_xml = textwrap.dedent(""" + +

Title

+ +

Description

+ +

Example

+ +

The following languages are in the Indo-European family:

+ + + Urdu + Finnish + Marathi + French + Hungarian + + + +

Note: Make sure you select all of the correct options—there may be more than one!

+ + +
+

Explanation

+ +

Solution for CAPA problem

+ +
+
+ +
+ """) + + sample_dropdown_problem_xml = textwrap.dedent(""" + +

Dropdown problems allow learners to select only one option from a list of options.

+ +

Description

+ +

You can use the following example problem as a model.

+ +

Which of the following countries celebrates its independence on August 15?

+ + + + + + + +
+

Explanation

+ +

India became an independent nation on August 15, 1947.

+ +
+
+ +
+ """) + + sample_multichoice_problem_xml = textwrap.dedent(""" + +

Multiple choice problems allow learners to select only one option.

+ +

When you add the problem, be sure to select Settings to specify a Display Name and other values.

+ +

You can use the following example problem as a model.

+ +

Which of the following countries has the largest population?

+ + + Brazil + timely feedback -- explain why an almost correct answer is wrong + + Germany + Indonesia + Russia + + + + +
+

Explanation

+ +

According to September 2014 estimates:

+

The population of Indonesia is approximately 250 million.

+

The population of Brazil is approximately 200 million.

+

The population of Russia is approximately 146 million.

+

The population of Germany is approximately 81 million.

+ +
+
+ +
+ """) + + sample_numerical_input_problem_xml = textwrap.dedent(""" + +

In a numerical input problem, learners enter numbers or a specific and relatively simple mathematical + expression. Learners enter the response in plain text, and the system then converts the text to a symbolic + expression that learners can see below the response field.

+ +

The system can handle several types of characters, including basic operators, fractions, exponents, and + common constants such as "i". You can refer learners to "Entering Mathematical and Scientific Expressions" + in the edX Guide for Students for more information.

+ +

When you add the problem, be sure to select Settings to specify a Display Name and other values that + apply.

+ +

You can use the following example problems as models.

+ +

How many miles away from Earth is the sun? Use scientific notation to answer.

+ + + + + +

The square of what number is -100?

+ + + + + + +
+

Explanation

+ +

The sun is 93,000,000, or 9.3*10^7, miles away from Earth.

+

-100 is the square of 10 times the imaginary number, i.

+ +
+
+ +
+ """) + + sample_text_input_problem_xml = textwrap.dedent(""" + +

In text input problems, also known as "fill-in-the-blank" problems, learners enter text into a response + field. The text can include letters and characters such as punctuation marks. The text that the learner + enters must match your specified answer text exactly. You can specify more than one correct answer. + Learners must enter a response that matches one of the correct answers exactly.

+ +

When you add the problem, be sure to select Settings to specify a Display Name and other values that + apply.

+ +

You can use the following example problem as a model.

+ +

What was the first post-secondary school in China to allow both male and female students?

+ + + + + + + + +
+

Explanation

+ +

Nanjing Higher Normal Institute first admitted female students in 1920.

+ +
+
+ +
+ """) + + sample_checkboxes_with_hints_and_feedback_problem_xml = textwrap.dedent(""" + +

You can provide feedback for each option in a checkbox problem, with distinct feedback depending on + whether or not the learner selects that option.

+ +

You can also provide compound feedback for a specific combination of answers. For example, if you have + three possible answers in the problem, you can configure specific feedback for when a learner selects each + combination of possible answers.

+ +

You can also add hints for learners.

+ +

Be sure to select Settings to specify a Display Name and other values that apply.

+ +

Use the following example problem as a model.

+ +

Which of the following is a fruit? Check all that apply.

+ + + apple + You are correct that an apple is a fruit because it is the fertilized + ovary that comes from an apple tree and contains seeds. + Remember that an apple is also a fruit. + pumpkin + You are correct that a pumpkin is a fruit because it is the fertilized + ovary of a squash plant and contains seeds. + Remember that a pumpkin is also a fruit. + potato + A potato is a vegetable, not a fruit, because it does not come from a + flower and does not contain seeds. + You are correct that a potato is a vegetable because it is an edible + part of a plant in tuber form. + tomato + You are correct that a tomato is a fruit because it is the fertilized + ovary of a tomato plant and contains seeds. + Many people mistakenly think a tomato is a vegetable. However, because + a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit. + + An apple, pumpkin, and tomato are all fruits as they all are fertilized + ovaries of a plant and contain seeds. + You are correct that an apple, pumpkin, and tomato are all fruits as they + all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an + edible part of a plant in tuber form and is a vegetable. + + + + + + A fruit is the fertilized ovary from a flower. + A fruit contains seeds of the plant. + +
+ """) + + sample_dropdown_with_hints_and_feedback_problem_xml = textwrap.dedent(""" + +

You can provide feedback for each available option in a dropdown problem.

+ +

You can also add hints for learners.

+ +

Be sure to select Settings to specify a Display Name and other values that apply.

+ +

Use the following example problem as a model.

+ +

A/an ________ is a vegetable.

+ + + + + + + + + + + A fruit is the fertilized ovary from a flower. + A fruit contains seeds of the plant. + +
+ """) + + sample_multichoice_with_hints_and_feedback_problem_xml = textwrap.dedent(""" + +

You can provide feedback for each option in a multiple choice problem.

+ +

You can also add hints for learners.

+ +

Be sure to select Settings to specify a Display Name and other values that apply.

+ +

Use the following example problem as a model.

+ +

Which of the following is a vegetable?

+ + + apple An apple is the fertilized ovary that comes from an apple + tree and contains seeds, meaning it is a fruit. + pumpkin A pumpkin is the fertilized ovary of a squash plant and + contains seeds, meaning it is a fruit. + potato A potato is an edible part of a plant in tuber form and is a + vegetable. + tomato Many people mistakenly think a tomato is a vegetable. + However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit. + + + + + + + A fruit is the fertilized ovary from a flower. + A fruit contains seeds of the plant. + +
+ """) + + sample_numerical_input_with_hints_and_feedback_problem_xml = textwrap.dedent(""" + +

You can provide feedback for correct answers in numerical input problems. You cannot provide feedback + for incorrect answers.

+ +

Use feedback for the correct answer to reinforce the process for arriving at the numerical value.

+ +

You can also add hints for learners.

+ +

Be sure to select Settings to specify a Display Name and other values that apply.

+ +

Use the following example problem as a model.

+ +

What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)

+ + + + The mean for this set of numbers is 20 / 5, which equals 4. + + +
+

Explanation

+ +

The mean is calculated by summing the set of numbers and dividing by n. In this case: + (1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4.

+ +
+
+ + + The mean is calculated by summing the set of numbers and dividing by n. + n is the count of items in the set. + +
+ """) + + sample_text_input_with_hints_and_feedback_problem_xml = textwrap.dedent(""" + +

You can provide feedback for the correct answer in text input problems, as well as for specific + incorrect answers.

+ +

Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on + how to arrive at the correct answer.

+ +

Be sure to select Settings to specify a Display Name and other values that apply.

+ +

Use the following example problem as a model.

+ +

Which U.S. state has the largest land area?

+ + + Alaska is 576,400 square miles, more than double the land area of the second largest state, + Texas. + While many people think Texas is the largest state, it is actually the + second largest, with 261,797 square miles. + California is the third largest state, with 155,959 square miles. + + + + + + Consider the square miles, not population. + Consider all 50 states, not just the continental United States. + +
+ """) + + def _create_block(self, xml, name=None): + """Creates a ProblemBlock to run test against""" + block = CapaFactory.create() + block.data = xml + if name: + block.display_name = name + return block + + @ddt.data(*sorted(responsetypes.registry.registered_tags())) + def test_all_response_types(self, response_tag): + """Tests that every registered response tag is correctly returned""" + xml = "<{response_tag}>".format(response_tag=response_tag) + name = "Some Capa Problem" + block = self._create_block(xml, name=name) + assert block.problem_types == {response_tag} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": [response_tag], + "content": {"display_name": name, "capa_content": ""}, + } + + def test_response_types_ignores_non_response_tags(self): + """Ensure non-response XML tags are ignored when determining problem response types.""" + xml = textwrap.dedent(""" + +

Label

+
Some comment
+ + + Apple + Banana + Chocolate + Donut + + +
+ """) + name = "Test Capa Problem" + block = self._create_block(xml, name=name) + assert block.problem_types == {"multiplechoiceresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["multiplechoiceresponse"], + "content": {"display_name": name, "capa_content": "Label Some comment Apple Banana Chocolate Donut"}, + } + + def test_response_types_multiple_tags(self): + """Verify indexing behavior when multiple response types are present in a single problem.""" + xml = textwrap.dedent(""" + +

Label

+
Some comment
+ + + Donut + + + + + Buggy + + + + + +
+ """) + name = "Other Test Capa Problem" + block = self._create_block(xml, name=name) + assert block.problem_types == {"multiplechoiceresponse", "optionresponse"} + + # We are converting problem_types to a set to compare it later without taking into account the order + # the reasoning behind is that the problem_types (property) is represented by dict and when it is converted + # to list its ordering is different everytime. + + indexing_result = block.index_dictionary() + indexing_result["problem_types"] = set(indexing_result["problem_types"]) + self.assertDictEqual( + indexing_result, + { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": {"optionresponse", "multiplechoiceresponse"}, + "content": {"display_name": name, "capa_content": "Label Some comment Donut Buggy '1','2'"}, + }, + ) + + def test_solutions_not_indexed(self): + """Confirm that solutions, scripts, styles, answers, and hints are excluded from indexing.""" + xml = textwrap.dedent(""" + + Test solution. + Test solution with attribute. + + Test solutionset. + Test solution within solutionset. + + + Test feedback. + Test feedback with attribute. + + Test FeedbackSet. + Test feedback within feedbackset. + + + Test answer. + Test answer with attribute. + + + + + + + + Test choicehint. + Test hint. + Test hintpart. + + """) + name = "Blank Common Capa Problem" + block = self._create_block(xml, name=name) + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": [], + "content": {"display_name": name, "capa_content": ""}, + } + + def test_indexing_checkboxes(self): + """Verify correct indexing of checkbox-based problems and extracted content.""" + name = "Checkboxes" + block = self._create_block(self.sample_checkbox_problem_xml, name=name) + capa_content = textwrap.dedent(""" + Title + Description + Example + The following languages are in the Indo-European family: + Urdu + Finnish + Marathi + French + Hungarian + Note: Make sure you select all of the correct options—there may be more than one! + """) + assert block.problem_types == {"choiceresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["choiceresponse"], + "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()}, + } + + def test_indexing_dropdown(self): + """Verify correct indexing of dropdown-based problems and extracted content.""" + name = "Dropdown" + block = self._create_block(self.sample_dropdown_problem_xml, name=name) + capa_content = textwrap.dedent(""" + Dropdown problems allow learners to select only one option from a list of options. + Description + You can use the following example problem as a model. + Which of the following countries celebrates its independence on August 15? 'India','Spain','China','Bermuda' + """) + assert block.problem_types == {"optionresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["optionresponse"], + "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()}, + } + + def test_indexing_multiple_choice(self): + """Verify correct indexing of multiple-choice problems and extracted content.""" + name = "Multiple Choice" + block = self._create_block(self.sample_multichoice_problem_xml, name=name) + capa_content = textwrap.dedent(""" + Multiple choice problems allow learners to select only one option. + When you add the problem, be sure to select Settings to specify a Display Name and other values. + You can use the following example problem as a model. + Which of the following countries has the largest population? + Brazil + Germany + Indonesia + Russia + """) + assert block.problem_types == {"multiplechoiceresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["multiplechoiceresponse"], + "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()}, + } + + def test_indexing_numerical_input(self): + """Verify correct indexing of numerical input problems and extracted content.""" + name = "Numerical Input" + block = self._create_block(self.sample_numerical_input_problem_xml, name=name) + capa_content = textwrap.dedent(""" + In a numerical input problem, learners enter numbers or a specific and relatively simple mathematical + expression. Learners enter the response in plain text, and the system then converts the text to a symbolic + expression that learners can see below the response field. + The system can handle several types of characters, including basic operators, fractions, exponents, and + common constants such as "i". You can refer learners to "Entering Mathematical and Scientific Expressions" + in the edX Guide for Students for more information. + When you add the problem, be sure to select Settings to specify a Display Name and other values that + apply. + You can use the following example problems as models. + How many miles away from Earth is the sun? Use scientific notation to answer. + The square of what number is -100? + """) + assert block.problem_types == {"numericalresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["numericalresponse"], + "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()}, + } + + def test_indexing_text_input(self): + """Verify correct indexing of text input problems and extracted content.""" + name = "Text Input" + block = self._create_block(self.sample_text_input_problem_xml, name=name) + capa_content = textwrap.dedent(""" + In text input problems, also known as "fill-in-the-blank" problems, learners enter text into a response + field. The text can include letters and characters such as punctuation marks. The text that the learner + enters must match your specified answer text exactly. You can specify more than one correct answer. + Learners must enter a response that matches one of the correct answers exactly. + When you add the problem, be sure to select Settings to specify a Display Name and other values that + apply. + You can use the following example problem as a model. + What was the first post-secondary school in China to allow both male and female students? + """) + assert block.problem_types == {"stringresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["stringresponse"], + "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()}, + } + + def test_indexing_non_latin_problem(self): + """Ensure non-Latin characters are preserved correctly in indexed problem content.""" + sample_text_input_problem_xml = textwrap.dedent(""" + + +

Δοκιμή με μεταβλητές με Ελληνικούς χαρακτήρες μέσα σε python: $FX1_VAL

+
+ """) + name = "Non latin Input" + block = self._create_block(sample_text_input_problem_xml, name=name) + capa_content = "Δοκιμή με μεταβλητές με Ελληνικούς χαρακτήρες μέσα σε python: $FX1_VAL" + + block_dict = block.index_dictionary() + assert block_dict["content"]["capa_content"] == smart_str(capa_content) + + def test_indexing_checkboxes_with_hints_and_feedback(self): + """Verify indexing of checkbox problems containing hints and feedback.""" + name = "Checkboxes with Hints and Feedback" + block = self._create_block(self.sample_checkboxes_with_hints_and_feedback_problem_xml, name=name) + capa_content = textwrap.dedent(""" + You can provide feedback for each option in a checkbox problem, with distinct feedback depending on + whether or not the learner selects that option. + You can also provide compound feedback for a specific combination of answers. For example, if you have + three possible answers in the problem, you can configure specific feedback for when a learner selects each + combination of possible answers. + You can also add hints for learners. + Be sure to select Settings to specify a Display Name and other values that apply. + Use the following example problem as a model. + Which of the following is a fruit? Check all that apply. + apple + pumpkin + potato + tomato + """) + assert block.problem_types == {"choiceresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["choiceresponse"], + "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()}, + } + + def test_indexing_dropdown_with_hints_and_feedback(self): + """Verify indexing of dropdown problems containing hints and feedback.""" + name = "Dropdown with Hints and Feedback" + block = self._create_block(self.sample_dropdown_with_hints_and_feedback_problem_xml, name=name) + capa_content = textwrap.dedent(""" + You can provide feedback for each available option in a dropdown problem. + You can also add hints for learners. + Be sure to select Settings to specify a Display Name and other values that apply. + Use the following example problem as a model. + A/an ________ is a vegetable. + apple + pumpkin + potato + tomato + """) + assert block.problem_types == {"optionresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["optionresponse"], + "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()}, + } + + def test_indexing_multiple_choice_with_hints_and_feedback(self): + """Verify indexing of multiple-choice problems containing hints and feedback.""" + name = "Multiple Choice with Hints and Feedback" + block = self._create_block(self.sample_multichoice_with_hints_and_feedback_problem_xml, name=name) + capa_content = textwrap.dedent(""" + You can provide feedback for each option in a multiple choice problem. + You can also add hints for learners. + Be sure to select Settings to specify a Display Name and other values that apply. + Use the following example problem as a model. + Which of the following is a vegetable? + apple + pumpkin + potato + tomato + """) + assert block.problem_types == {"multiplechoiceresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["multiplechoiceresponse"], + "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()}, + } + + def test_indexing_numerical_input_with_hints_and_feedback(self): + """Verify indexing of numerical input problems containing hints and feedback.""" + name = "Numerical Input with Hints and Feedback" + block = self._create_block(self.sample_numerical_input_with_hints_and_feedback_problem_xml, name=name) + capa_content = textwrap.dedent(""" + You can provide feedback for correct answers in numerical input problems. You cannot provide feedback + for incorrect answers. + Use feedback for the correct answer to reinforce the process for arriving at the numerical value. + You can also add hints for learners. + Be sure to select Settings to specify a Display Name and other values that apply. + Use the following example problem as a model. + What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5) + """) + assert block.problem_types == {"numericalresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["numericalresponse"], + "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()}, + } + + def test_indexing_text_input_with_hints_and_feedback(self): + """Verify indexing of text input problems containing hints and feedback.""" + name = "Text Input with Hints and Feedback" + block = self._create_block(self.sample_text_input_with_hints_and_feedback_problem_xml, name=name) + capa_content = textwrap.dedent(""" + You can provide feedback for the correct answer in text input problems, as well as for specific + incorrect answers. + Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on + how to arrive at the correct answer. + Be sure to select Settings to specify a Display Name and other values that apply. + Use the following example problem as a model. + Which U.S. state has the largest land area? + """) + assert block.problem_types == {"stringresponse"} + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["stringresponse"], + "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()}, + } + + def test_indexing_problem_with_html_tags(self): + """Ensure HTML tags, comments, scripts, and styles are safely ignored during indexing.""" + sample_problem_xml = textwrap.dedent(""" + + + +

This has HTML comment in it.

+ + +

HTML end.

+ + +
+ """) + name = "Mixed business" + block = self._create_block(sample_problem_xml, name=name) + capa_content = "This has HTML comment in it. HTML end." + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": [], + "content": {"display_name": name, "capa_content": capa_content}, + } + + def test_indexing_problem_with_no_whitespace_between_tags(self): + """ + The new (MFE) visual editor for capa problems renders the OLX without spaces between the tags. + We want to make sure the index description is still readable and has whitespace. + """ + sample_problem_xml = ( + '' + "
Question text here.
" + '
Option A
' + '
Option B
' + "
" + "
" + ) + name = "No spaces" + block = self._create_block(sample_problem_xml, name=name) + capa_content = "Question text here. Option A Option B" + assert block.index_dictionary() == { + "content_type": ProblemBlock.INDEX_CONTENT_TYPE, + "problem_types": ["choiceresponse"], + "content": {"display_name": name, "capa_content": capa_content}, + } + + def test_invalid_xml_handling(self): + """ + Tests to confirm that invalid XML throws errors during xblock creation, + so as not to allow bad data into modulestore. + """ + sample_invalid_xml = textwrap.dedent(""" + + + +

You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown + problems. Edit this component to replace this template with your own assessment.

+ + You can add an optional tip or note related to the prompt like this. + + + + + +
+
+ """) + with pytest.raises(Exception): + CapaFactory.create(xml=problem_xml) + + +class ComplexEncoderTest(unittest.TestCase): + """Tests JSON encoding of complex numbers.""" + + def test_default(self): + """ + Check that complex numbers can be encoded into JSON. + """ + complex_num = 1 - 1j + expected_str = "1-1*j" + json_str = json.dumps(complex_num, cls=ComplexEncoder) + assert expected_str == json_str[1:(-1)] + # ignore quotes + + +@skip_unless_lms +@UseUnsafeCodejail() +@pytest.mark.django_db +class ProblemCheckTrackingTest(unittest.TestCase): + """ + Ensure correct tracking information is included in events emitted during problem checks. + """ + + def setUp(self): + super().setUp() + self.maxDiff = None # pylint: disable=invalid-name + + def test_choice_answer_text(self): + """Verify tracked submission data for multiple choice, option, and checkbox responses.""" + xml = """\ + + + + + + + + + + a table + a desk + a chair + a bookshelf + + + + + + + a piano + a tree + a guitar + a window + + + + """ + + # Whitespace screws up comparisons + xml = "".join(line.strip() for line in xml.split("\n")) + factory = self.capa_factory_for_problem_xml(xml) + block = factory.create() + + answer_input_dict = { + factory.input_key(2): "blue", + factory.input_key(3): "choice_0", + factory.input_key(4): ["choice_0", "choice_1"], + } + event = self.get_event_for_answers(block, answer_input_dict) + + assert event["submission"] == { + factory.answer_key(2): { + "question": "What color is the open ocean on a sunny day?", + "answer": "blue", + "response_type": "optionresponse", + "input_type": "optioninput", + "correct": True, + "group_label": "", + "variant": "", + }, + factory.answer_key(3): { + "question": "Which piece of furniture is built for sitting?", + "answer": "a table", + "response_type": "multiplechoiceresponse", + "input_type": "choicegroup", + "correct": False, + "group_label": "", + "variant": "", + }, + factory.answer_key(4): { + "question": "Which of the following are musical instruments?", + "answer": ["a piano", "a tree"], + "response_type": "choiceresponse", + "input_type": "checkboxgroup", + "correct": False, + "group_label": "", + "variant": "", + }, + } + + def capa_factory_for_problem_xml(self, xml): + """Create a custom CapaFactory for a given problem XML string.""" + + class CustomCapaFactory(CapaFactory): + """ + A factory for creating a Capa problem with arbitrary xml. + """ + + sample_problem_xml = textwrap.dedent(xml) + + return CustomCapaFactory + + def get_event_for_answers(self, block, answer_input_dict): + """Submit answers and return the emitted tracking event payload.""" + with patch.object(block.runtime, "publish") as mock_publish: + block.submit_problem(answer_input_dict) + + assert len(mock_publish.mock_calls) >= 2 + # There are potentially 2 track logs: answers and hint. [-1]=answers. + mock_call = mock_publish.mock_calls[-1] + event = mock_call[1][2] + + return event + + def test_numerical_textline(self): + """Verify tracking data for numerical textline responses.""" + factory = CapaFactory + block = factory.create() + + answer_input_dict = {factory.input_key(2): "3.14"} + + event = self.get_event_for_answers(block, answer_input_dict) + assert event["submission"] == { + factory.answer_key(2): { + "question": "", + "answer": "3.14", + "response_type": "numericalresponse", + "input_type": "textline", + "correct": True, + "group_label": "", + "variant": "", + } + } + + def test_multiple_inputs(self): + """Verify tracking data for multiple inputs within a single response group.""" + group_label = "Choose the correct color" + input1_label = "What color is the sky?" + input2_label = "What color are pine needles?" + factory = self.capa_factory_for_problem_xml(f"""\ + + + + + + + + """) + block = factory.create() + answer_input_dict = { + factory.input_key(2, 1): "blue", + factory.input_key(2, 2): "yellow", + } + + event = self.get_event_for_answers(block, answer_input_dict) + assert event["submission"] == { + factory.answer_key(2, 1): { + "group_label": group_label, + "question": input1_label, + "answer": "blue", + "response_type": "optionresponse", + "input_type": "optioninput", + "correct": True, + "variant": "", + }, + factory.answer_key(2, 2): { + "group_label": group_label, + "question": input2_label, + "answer": "yellow", + "response_type": "optionresponse", + "input_type": "optioninput", + "correct": False, + "variant": "", + }, + } + + def test_optioninput_extended_xml(self): + """Test the new XML form of writing with