From 61127b613b71b00a2380ea8b1bf546a9a226e92d Mon Sep 17 00:00:00 2001 From: Dmitry Viskov Date: Wed, 23 Dec 2015 14:58:15 +0300 Subject: [PATCH] Make it impossible to click "final check" without selecting a choice --- .../xmodule/js/spec/capa/display_spec.coffee | 89 +++++++++++++++++++ .../xmodule/js/src/capa/display.coffee | 54 +++++++++++ .../tests/lms/test_problem_types.py | 35 +++++++- .../courseware/features/problems.feature | 40 +++------ .../courseware/features/problems.py | 9 ++ 5 files changed, 200 insertions(+), 27 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee index 2edb056bd9..09f9408c84 100644 --- a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee @@ -247,6 +247,95 @@ describe 'Problem', -> runs -> expect(@problem.checkButtonLabel.text).toHaveBeenCalledWith 'Check' + describe 'check button on problems', -> + beforeEach -> + @problem = new Problem($('.xblock-student_view')) + @checkDisabled = (v) -> expect(@problem.checkButton.hasClass('is-disabled')).toBe(v) + + describe 'some basic tests for check button', -> + it 'should become enabled after a value is entered into the text box', -> + $('#input_example_1').val('test').trigger('input') + @checkDisabled false + $('#input_example_1').val('').trigger('input') + @checkDisabled true + + describe 'some advanced tests for check button', -> + it 'should become enabled after a checkbox is checked', -> + html = ''' +
+ + + +
+ ''' + $('#input_example_1').replaceWith(html) + @problem.checkAnswersAndCheckButton true + @checkDisabled true + $('#input_1_1_1').attr('checked', true).trigger('click') + @checkDisabled false + $('#input_1_1_1').attr('checked', false).trigger('click') + @checkDisabled true + + it 'should become enabled after a radiobutton is checked', -> + html = ''' +
+ + + +
+ ''' + $('#input_example_1').replaceWith(html) + @problem.checkAnswersAndCheckButton true + @checkDisabled true + $('#input_1_1_1').attr('checked', true).trigger('click') + @checkDisabled false + $('#input_1_1_1').attr('checked', false).trigger('click') + @checkDisabled true + + it 'should become enabled after a value is selected in a selector', -> + html = ''' +
+ +
+ ''' + $('#input_example_1').replaceWith(html) + @problem.checkAnswersAndCheckButton true + @checkDisabled true + $("#problem_sel select").val("val2").trigger('change') + @checkDisabled false + $("#problem_sel select").val("val0").trigger('change') + @checkDisabled true + + it 'should become enabled after a radiobutton is checked and a value is entered into the text box', -> + html = ''' +
+ + + +
+ ''' + $(html).insertAfter('#input_example_1') + @problem.checkAnswersAndCheckButton true + @checkDisabled true + $('#input_1_1_1').attr('checked', true).trigger('click') + @checkDisabled true + $('#input_example_1').val('111').trigger('input') + @checkDisabled false + $('#input_1_1_1').attr('checked', false).trigger('click') + @checkDisabled true + + it 'should become enabled if there are only hidden input fields', -> + html = ''' + + ''' + $('#input_example_1').replaceWith(html) + @problem.checkAnswersAndCheckButton true + @checkDisabled false + describe 'reset', -> beforeEach -> @problem = new Problem($('.xblock-student_view')) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index d9d1b89c77..a8e67a26fa 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -49,6 +49,8 @@ class @Problem window.globalTooltipManager.hide() @bindResetCorrectness() + if @checkButton.length + @checkAnswersAndCheckButton true # Collapsibles Collapsible.setCollapsibles(@el) @@ -452,6 +454,58 @@ class @Problem element.CodeMirror.save() if element.CodeMirror.save @answers = @inputs.serialize() + checkAnswersAndCheckButton: (bind=false) => + # Used to check available answers and if something is checked (or the answer is set in some textbox) + # "Check"/"Final check" button becomes enabled. Otherwise it is disabled by default. + # params: + # 'bind' used on the first check to attach event handlers to input fields + # to change "Check"/"Final check" enable status in case of some manipulations with answers + answered = true + + at_least_one_text_input_found = false + one_text_input_filled = false + @el.find("input:text").each (i, text_field) => + if $(text_field).is(':visible') + at_least_one_text_input_found = true + if $(text_field).val() isnt '' + one_text_input_filled = true + if bind + $(text_field).on 'input', (e) => + @checkAnswersAndCheckButton() + return + return + if at_least_one_text_input_found and not one_text_input_filled + answered = false + + @el.find(".choicegroup").each (i, choicegroup_block) => + checked = false + $(choicegroup_block).find("input[type=checkbox], input[type=radio]").each (j, checkbox_or_radio) => + if $(checkbox_or_radio).is(':checked') + checked = true + if bind + $(checkbox_or_radio).on 'click', (e) => + @checkAnswersAndCheckButton() + return + return + if not checked + answered = false + return + + @el.find("select").each (i, select_field) => + selected_option = $(select_field).find("option:selected").text().trim() + if selected_option is '' + answered = false + if bind + $(select_field).on 'change', (e) => + @checkAnswersAndCheckButton() + return + return + + if answered + @enableCheckButton true + else + @enableCheckButton false, false + bindResetCorrectness: -> # Loop through all input types # Bind the reset functions at that scope. diff --git a/common/test/acceptance/tests/lms/test_problem_types.py b/common/test/acceptance/tests/lms/test_problem_types.py index 4ff0d401a5..358c0146f3 100644 --- a/common/test/acceptance/tests/lms/test_problem_types.py +++ b/common/test/acceptance/tests/lms/test_problem_types.py @@ -6,6 +6,7 @@ See also lettuce tests in lms/djangoapps/courseware/features/problems.feature import random import textwrap +from nose import SkipTest from abc import ABCMeta, abstractmethod from nose.plugins.attrib import attr from selenium.webdriver import ActionChains @@ -135,6 +136,8 @@ class ProblemTypeTestMixin(object): """ Test cases shared amongst problem types. """ + can_submit_blank = False + @attr('shard_7') def test_answer_correctly(self): """ @@ -200,15 +203,34 @@ class ProblemTypeTestMixin(object): Then my "" answer is marked "incorrect" And The "" problem displays a "blank" answer """ + if not self.can_submit_blank: + raise SkipTest("Test incompatible with the current problem type") + self.problem_page.wait_for( lambda: self.problem_page.problem_name == self.problem_name, "Make sure the correct problem is on the page" ) - # Leave the problem unchanged and click check. + self.assertNotIn('is-disabled', self.problem_page.q(css='div.problem button.check').attrs('class')[0]) self.problem_page.click_check() self.wait_for_status('incorrect') + @attr('shard_7') + def test_cant_submit_blank_answer(self): + """ + Scenario: I can't submit a blank answer + When I try to submit blank answer + Then I can't check a problem + """ + if self.can_submit_blank: + raise SkipTest("Test incompatible with the current problem type") + + self.problem_page.wait_for( + lambda: self.problem_page.problem_name == self.problem_name, + "Make sure the correct problem is on the page" + ) + self.assertIn('is-disabled', self.problem_page.q(css='div.problem button.check').attrs('class')[0]) + @attr('a11y') def test_problem_type_a11y(self): """ @@ -236,6 +258,8 @@ class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): factory = AnnotationResponseXMLFactory() + can_submit_blank = True + factory_kwargs = { 'title': 'Annotation Problem', 'text': 'The text being annotated', @@ -686,6 +710,13 @@ class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): """ pass + def test_cant_submit_blank_answer(self): + """ + Overridden for script test because the testing grader always responds + with "correct" + """ + pass + class ChoiceTextProbelmTypeTestBase(ProblemTypeTestBase): """ @@ -801,6 +832,8 @@ class ImageProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): factory = ImageResponseXMLFactory() + can_submit_blank = True + factory_kwargs = { 'src': '/static/images/placeholder-image.png', 'rectangle': '(0,0)-(50,50)', diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index 153b20aa7b..5994fcc464 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -180,16 +180,22 @@ Feature: LMS.Answer problems Examples: | ProblemType | Points Possible | - | drop down | 1 point possible | - | multiple choice | 1 point possible | - | checkbox | 1 point possible | - | radio | 1 point possible | - #| string | 1 point possible | - | numerical | 1 point possible | - | formula | 1 point possible | - | script | 2 points possible | | image | 1 point possible | + Scenario: I can't submit a blank answer + Given I am viewing a "" problem + Then I can't check a problem + + Examples: + | ProblemType | + | drop down | + | multiple choice | + | checkbox | + | radio | + | string | + | numerical | + | formula | + | script | Scenario: I can reset the correctness of a problem after changing my answer Given I am viewing a "" problem @@ -234,21 +240,3 @@ Feature: LMS.Answer problems | multiple choice | incorrect | correct | | radio | correct | incorrect | | radio | incorrect | correct | - - - Scenario: I can reset the correctness of a problem after submitting a blank answer - Given I am viewing a "" problem - When I check a problem - And I input an answer on a "" problem "correctly" - Then my "" answer is marked "unanswered" - - Examples: - | ProblemType | - | drop down | - | multiple choice | - | checkbox | - | radio | - #| string | - | numerical | - | formula | - | script | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 730997b0a1..559a90e0af 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -92,12 +92,21 @@ def check_problem(step): # first scroll down so the loading mathjax button does not # cover up the Check button world.browser.execute_script("window.scrollTo(0,1024)") + assert world.is_css_not_present("button.check.is-disabled") world.css_click("button.check") # Wait for the problem to finish re-rendering world.wait_for_ajax_complete() +@step(u"I can't check a problem") +def assert_cant_check_problem(step): # pylint: disable=unused-argument + # first scroll down so the loading mathjax button does not + # cover up the Check button + world.browser.execute_script("window.scrollTo(0,1024)") + assert world.is_css_present("button.check.is-disabled") + + @step(u'The "([^"]*)" problem displays a "([^"]*)" answer') def assert_problem_has_answer(step, problem_type, answer_class): '''