Commit 5e0849d2 by Graham Lowe

Temporarily disable CAPA problem "Check" button.

- update appropriate jasmine tests so that stubbed $postWithPrefix
respects a subset of jQuery promise API.
- add customization for checking state.
- display checking state while button is disabled.
- ensure checking state lasts for at least a second.
parent a7796866
...@@ -390,6 +390,24 @@ class CapaMixin(CapaFields): ...@@ -390,6 +390,24 @@ class CapaMixin(CapaFields):
else: else:
return check return check
def check_button_checking_name(self):
"""
Return the "checking..." text for the "check" button.
After the user presses the "check" button, the button will briefly
display the value returned by this function until a response is
received by the server.
The text can be customized by the text_customization setting.
"""
# Apply customizations if present
if 'custom_checking' in self.text_customization:
return self.text_customization.get('custom_checking')
_ = self.runtime.service(self, "i18n").ugettext
return _('Checking...')
def should_show_check_button(self): def should_show_check_button(self):
""" """
Return True/False to indicate whether to show the "Check" button. Return True/False to indicate whether to show the "Check" button.
...@@ -548,13 +566,16 @@ class CapaMixin(CapaFields): ...@@ -548,13 +566,16 @@ class CapaMixin(CapaFields):
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
html = self.handle_problem_html_error(err) html = self.handle_problem_html_error(err)
# The convention is to pass the name of the check button # The convention is to pass the name of the check button if we want
# if we want to show a check button, and False otherwise # to show a check button, and False otherwise This works because
# This works because non-empty strings evaluate to True # non-empty strings evaluate to True. We use the same convention
# for the "checking" state text.
if self.should_show_check_button(): if self.should_show_check_button():
check_button = self.check_button_name() check_button = self.check_button_name()
check_button_checking = self.check_button_checking_name()
else: else:
check_button = False check_button = False
check_button_checking = False
content = { content = {
'name': self.display_name_with_default, 'name': self.display_name_with_default,
...@@ -566,6 +587,7 @@ class CapaMixin(CapaFields): ...@@ -566,6 +587,7 @@ class CapaMixin(CapaFields):
'problem': content, 'problem': content,
'id': self.id, 'id': self.id,
'check_button': check_button, 'check_button': check_button,
'check_button_checking': check_button_checking,
'reset_button': self.should_show_reset_button(), 'reset_button': self.should_show_reset_button(),
'save_button': self.should_show_save_button(), 'save_button': self.should_show_save_button(),
'answer_available': self.answer_available(), 'answer_available': self.answer_available(),
......
...@@ -180,6 +180,7 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -180,6 +180,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
# Proxy to CapaModule for access to any of its attributes # Proxy to CapaModule for access to any of its attributes
answer_available = module_attr('answer_available') answer_available = module_attr('answer_available')
check_button_name = module_attr('check_button_name') check_button_name = module_attr('check_button_name')
check_button_checking_name = module_attr('check_button_checking_name')
check_problem = module_attr('check_problem') check_problem = module_attr('check_problem')
choose_new_seed = module_attr('choose_new_seed') choose_new_seed = module_attr('choose_new_seed')
closed = module_attr('closed') closed = module_attr('closed')
......
...@@ -142,6 +142,10 @@ describe 'Problem', -> ...@@ -142,6 +142,10 @@ describe 'Problem', ->
@problem.answers = 'foo=1&bar=2' @problem.answers = 'foo=1&bar=2'
it 'log the problem_check event', -> it 'log the problem_check event', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
promise =
always: (callable) -> callable()
done: (callable) -> callable()
@problem.check() @problem.check()
expect(Logger.log).toHaveBeenCalledWith 'problem_check', 'foo=1&bar=2' expect(Logger.log).toHaveBeenCalledWith 'problem_check', 'foo=1&bar=2'
...@@ -151,11 +155,17 @@ describe 'Problem', -> ...@@ -151,11 +155,17 @@ describe 'Problem', ->
success: 'correct' success: 'correct'
contents: 'mock grader response' contents: 'mock grader response'
callback(response) callback(response)
promise =
always: (callable) -> callable()
done: (callable) -> callable()
@problem.check() @problem.check()
expect(Logger.log).toHaveBeenCalledWith 'problem_graded', ['foo=1&bar=2', 'mock grader response'], @problem.id expect(Logger.log).toHaveBeenCalledWith 'problem_graded', ['foo=1&bar=2', 'mock grader response'], @problem.id
it 'submit the answer for check', -> it 'submit the answer for check', ->
spyOn $, 'postWithPrefix' spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
promise =
always: (callable) -> callable()
done: (callable) -> callable()
@problem.check() @problem.check()
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_check', expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_check',
'foo=1&bar=2', jasmine.any(Function) 'foo=1&bar=2', jasmine.any(Function)
...@@ -164,6 +174,9 @@ describe 'Problem', -> ...@@ -164,6 +174,9 @@ describe 'Problem', ->
it 'call render with returned content', -> it 'call render with returned content', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'correct', contents: 'Correct!') callback(success: 'correct', contents: 'Correct!')
promise =
always: (callable) -> callable()
done: (callable) -> callable()
@problem.check() @problem.check()
expect(@problem.el.html()).toEqual 'Correct!' expect(@problem.el.html()).toEqual 'Correct!'
expect(window.SR.readElts).toHaveBeenCalled() expect(window.SR.readElts).toHaveBeenCalled()
...@@ -172,6 +185,9 @@ describe 'Problem', -> ...@@ -172,6 +185,9 @@ describe 'Problem', ->
it 'call render with returned content', -> it 'call render with returned content', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'incorrect', contents: 'Incorrect!') callback(success: 'incorrect', contents: 'Incorrect!')
promise =
always: (callable) -> callable()
done: (callable) -> callable()
@problem.check() @problem.check()
expect(@problem.el.html()).toEqual 'Incorrect!' expect(@problem.el.html()).toEqual 'Incorrect!'
expect(window.SR.readElts).toHaveBeenCalled() expect(window.SR.readElts).toHaveBeenCalled()
......
describe 'Crowdsourced hinter', -> describe 'Crowdsourced hinter', ->
beforeEach -> beforeEach ->
window.update_schematics = -> window.update_schematics = ->
jasmine.stubRequests() jasmine.stubRequests()
...@@ -13,10 +13,13 @@ describe 'Crowdsourced hinter', -> ...@@ -13,10 +13,13 @@ describe 'Crowdsourced hinter', ->
spyOn($, 'postWithPrefix').andCallFake( -> spyOn($, 'postWithPrefix').andCallFake( ->
last_argument = arguments[arguments.length - 1] last_argument = arguments[arguments.length - 1]
if typeof last_argument == 'function' if typeof last_argument == 'function'
response = response =
success: 'incorrect' success: 'incorrect'
contents: 'mock grader response' contents: 'mock grader response'
last_argument(response) last_argument(response)
promise =
always: (callable) -> callable()
done: (callable) -> callable()
) )
@problem = new Problem($('#problem')) @problem = new Problem($('#problem'))
@problem.bind() @problem.bind()
...@@ -28,7 +31,7 @@ describe 'Crowdsourced hinter', -> ...@@ -28,7 +31,7 @@ describe 'Crowdsourced hinter', ->
it 'knows when a capa problem is graded usig check_fd.', -> it 'knows when a capa problem is graded usig check_fd.', ->
spyOn($, 'ajaxWithPrefix').andCallFake((url, settings) -> spyOn($, 'ajaxWithPrefix').andCallFake((url, settings) ->
response = response =
success: 'incorrect' success: 'incorrect'
contents: 'mock grader response' contents: 'mock grader response'
settings.success(response) settings.success(response)
...@@ -50,5 +53,3 @@ describe 'Crowdsourced hinter', -> ...@@ -50,5 +53,3 @@ describe 'Crowdsourced hinter', ->
data = ['some answers', '<thing class="correct">'] data = ['some answers', '<thing class="correct">']
@hinter.capture_problem('problem_graded', data, 'fake element') @hinter.capture_problem('problem_graded', data, 'fake element')
expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_feedback", 'some answers', jasmine.any(Function)) expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_feedback", 'some answers', jasmine.any(Function))
...@@ -5,6 +5,13 @@ class @Problem ...@@ -5,6 +5,13 @@ class @Problem
@id = @el.data('problem-id') @id = @el.data('problem-id')
@element_id = @el.attr('id') @element_id = @el.attr('id')
@url = @el.data('url') @url = @el.data('url')
# has_timed_out and has_response are used to ensure that are used to
# ensure that we wait a minimum of ~ 1s before transitioning the check
# button from disabled to enabled
@has_timed_out = false
@has_response = false
@render() @render()
$: (selector) -> $: (selector) ->
...@@ -20,7 +27,10 @@ class @Problem ...@@ -20,7 +27,10 @@ class @Problem
problem_prefix = @element_id.replace(/problem_/,'') problem_prefix = @element_id.replace(/problem_/,'')
@inputs = @$("[id^=input_#{problem_prefix}_]") @inputs = @$("[id^=input_#{problem_prefix}_]")
@$('div.action input:button').click @refreshAnswers @$('div.action input:button').click @refreshAnswers
@$('div.action input.check').click @check_fd @checkButton = @$('div.action input.check')
@checkButtonCheckText = @checkButton.val()
@checkButtonCheckingText = @checkButton.data('checking')
@checkButton.click @check_fd
@$('div.action input.reset').click @reset @$('div.action input.reset').click @reset
@$('div.action button.show').click @show @$('div.action button.show').click @show
@$('div.action input.save').click @save @$('div.action input.save').click @save
...@@ -201,10 +211,15 @@ class @Problem ...@@ -201,10 +211,15 @@ class @Problem
@check() @check()
return return
@enableCheckButton false
if not window.FormData if not window.FormData
alert "Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads." alert "Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads."
@enableCheckButton true
return return
timeout_id = @enableCheckButtonAfterTimeout()
fd = new FormData() fd = new FormData()
# Sanity checks on submission # Sanity checks on submission
...@@ -251,12 +266,17 @@ class @Problem ...@@ -251,12 +266,17 @@ class @Problem
@gentle_alert error_html @gentle_alert error_html
abort_submission = file_too_large or file_not_selected or unallowed_file_submitted or required_files_not_submitted abort_submission = file_too_large or file_not_selected or unallowed_file_submitted or required_files_not_submitted
if abort_submission
window.clearTimeout(timeout_id)
@enableCheckButton true
return
settings = settings =
type: "POST" type: "POST"
data: fd data: fd
processData: false processData: false
contentType: false contentType: false
complete: @enableCheckButtonAfterResponse
success: (response) => success: (response) =>
switch response.success switch response.success
when 'incorrect', 'correct' when 'incorrect', 'correct'
...@@ -266,14 +286,17 @@ class @Problem ...@@ -266,14 +286,17 @@ class @Problem
@gentle_alert response.success @gentle_alert response.success
Logger.log 'problem_graded', [@answers, response.contents], @id Logger.log 'problem_graded', [@answers, response.contents], @id
if not abort_submission $.ajaxWithPrefix("#{@url}/problem_check", settings)
$.ajaxWithPrefix("#{@url}/problem_check", settings)
check: => check: =>
if not @check_save_waitfor(@check_internal) if not @check_save_waitfor(@check_internal)
@check_internal() @check_internal()
check_internal: => check_internal: =>
@enableCheckButton false
timeout_id = @enableCheckButtonAfterTimeout()
Logger.log 'problem_check', @answers Logger.log 'problem_check', @answers
# Segment.io # Segment.io
...@@ -281,7 +304,7 @@ class @Problem ...@@ -281,7 +304,7 @@ class @Problem
problem_id: @id problem_id: @id
answers: @answers answers: @answers
$.postWithPrefix "#{@url}/problem_check", @answers, (response) => $.postWithPrefix("#{@url}/problem_check", @answers, (response) =>
switch response.success switch response.success
when 'incorrect', 'correct' when 'incorrect', 'correct'
window.SR.readElts($(response.contents).find('.status')) window.SR.readElts($(response.contents).find('.status'))
...@@ -289,10 +312,11 @@ class @Problem ...@@ -289,10 +312,11 @@ class @Problem
@updateProgress response @updateProgress response
if @el.hasClass 'showed' if @el.hasClass 'showed'
@el.removeClass 'showed' @el.removeClass 'showed'
@$('div.action input.check').focus() @$('div.action input.check').focus()
else else
@gentle_alert response.success @gentle_alert response.success
Logger.log 'problem_graded', [@answers, response.contents], @id Logger.log 'problem_graded', [@answers, response.contents], @id
).always(@enableCheckButtonAfterResponse)
reset: => reset: =>
Logger.log 'problem_reset', @answers Logger.log 'problem_reset', @answers
...@@ -636,3 +660,29 @@ class @Problem ...@@ -636,3 +660,29 @@ class @Problem
choicetextgroup: (element, display) => choicetextgroup: (element, display) =>
element = $(element) element = $(element)
element.find("section[id^='forinput']").removeClass('choicetextgroup_show_correct') element.find("section[id^='forinput']").removeClass('choicetextgroup_show_correct')
enableCheckButton: (enable) =>
# Used to disable check button to reduce chance of accidental double-submissions.
if enable
@checkButton.removeClass 'is-disabled'
@checkButton.val(@checkButtonCheckText)
else
@checkButton.addClass 'is-disabled'
@checkButton.val(@checkButtonCheckingText)
enableCheckButtonAfterResponse: =>
@has_response = true
if not @has_timed_out
# Server has returned response before our timeout
@enableCheckButton false
else
@enableCheckButton true
enableCheckButtonAfterTimeout: =>
@has_timed_out = false
@has_response = false
enableCheckButton = () =>
@has_timed_out = true
if @has_response
@enableCheckButton true
window.setTimeout(enableCheckButton, 750)
...@@ -933,11 +933,19 @@ class CapaModuleTest(unittest.TestCase): ...@@ -933,11 +933,19 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(attempts=0) module = CapaFactory.create(attempts=0)
self.assertEqual(module.check_button_name(), "Check") self.assertEqual(module.check_button_name(), "Check")
def test_check_button_checking_name(self):
module = CapaFactory.create(attempts=1, max_attempts=10)
self.assertEqual(module.check_button_checking_name(), "Checking...")
module = CapaFactory.create(attempts=10, max_attempts=10)
self.assertEqual(module.check_button_checking_name(), "Checking...")
def test_check_button_name_customization(self): def test_check_button_name_customization(self):
module = CapaFactory.create(attempts=1, module = CapaFactory.create(
max_attempts=10, attempts=1,
text_customization={"custom_check": "Submit", "custom_final_check": "Final Submit"} max_attempts=10,
) text_customization={"custom_check": "Submit", "custom_final_check": "Final Submit"}
)
self.assertEqual(module.check_button_name(), "Submit") self.assertEqual(module.check_button_name(), "Submit")
module = CapaFactory.create(attempts=9, module = CapaFactory.create(attempts=9,
...@@ -946,6 +954,29 @@ class CapaModuleTest(unittest.TestCase): ...@@ -946,6 +954,29 @@ class CapaModuleTest(unittest.TestCase):
) )
self.assertEqual(module.check_button_name(), "Final Submit") self.assertEqual(module.check_button_name(), "Final Submit")
def test_check_button_checking_name_customization(self):
module = CapaFactory.create(
attempts=1,
max_attempts=10,
text_customization={
"custom_check": "Submit",
"custom_final_check": "Final Submit",
"custom_checking": "Checking..."
}
)
self.assertEqual(module.check_button_checking_name(), "Checking...")
module = CapaFactory.create(
attempts=9,
max_attempts=10,
text_customization={
"custom_check": "Submit",
"custom_final_check": "Final Submit",
"custom_checking": "Checking..."
}
)
self.assertEqual(module.check_button_checking_name(), "Checking...")
def test_should_show_check_button(self): def test_should_show_check_button(self):
attempts = random.randint(1, 10) attempts = random.randint(1, 10)
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
<input type="hidden" name="problem_id" value="${ problem['name'] }" /> <input type="hidden" name="problem_id" value="${ problem['name'] }" />
% if check_button: % if check_button:
<input class="check ${ check_button }" type="button" value="${ check_button }" /> <input class="check ${ check_button }" type="button" data-checking="${ check_button_checking }" value="${ check_button }" />
% endif % endif
% if reset_button: % if reset_button:
<input class="reset" type="button" value="${_('Reset')}" /> <input class="reset" type="button" value="${_('Reset')}" />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment