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):
'''