Commit d9ce5e07 by Vik Paruchuri

Implement a save button for open ended responses

parent 29710661
...@@ -14,13 +14,28 @@ import textwrap ...@@ -14,13 +14,28 @@ import textwrap
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = [ V1_SETTINGS_ATTRIBUTES = [
"display_name", "max_attempts", "graded", "accept_file_upload", "display_name",
"skip_spelling_checks", "due", "graceperiod", "weight", "min_to_calibrate", "max_attempts",
"max_to_calibrate", "peer_grader_count", "required_peer_grading", "graded",
"accept_file_upload",
"skip_spelling_checks",
"due",
"graceperiod",
"weight",
"min_to_calibrate",
"max_to_calibrate",
"peer_grader_count",
"required_peer_grading",
] ]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", V1_STUDENT_ATTRIBUTES = [
"student_attempts", "ready_to_reset", "old_task_states"] "current_task_number",
"task_states",
"state",
"student_attempts",
"ready_to_reset",
"old_task_states",
]
V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES
......
...@@ -322,7 +322,7 @@ div.combined-rubric-container { ...@@ -322,7 +322,7 @@ div.combined-rubric-container {
div.written-feedback { div.written-feedback {
background: #f6f6f6; background: #f6f6f6;
padding: 15px; padding: 5px;
} }
} }
......
...@@ -119,6 +119,7 @@ class @CombinedOpenEnded ...@@ -119,6 +119,7 @@ class @CombinedOpenEnded
next_rubric_sel: '.rubric-next-button' next_rubric_sel: '.rubric-next-button'
previous_rubric_sel: '.rubric-previous-button' previous_rubric_sel: '.rubric-previous-button'
oe_alert_sel: '.open-ended-alert' oe_alert_sel: '.open-ended-alert'
save_button_sel: '.save-button'
constructor: (el) -> constructor: (el) ->
@el=el @el=el
...@@ -183,6 +184,7 @@ class @CombinedOpenEnded ...@@ -183,6 +184,7 @@ class @CombinedOpenEnded
@hint_wrapper = @$(@oe).find(@hint_wrapper_sel) @hint_wrapper = @$(@oe).find(@hint_wrapper_sel)
@message_wrapper = @$(@oe).find(@message_wrapper_sel) @message_wrapper = @$(@oe).find(@message_wrapper_sel)
@submit_button = @$(@oe).find(@submit_button_sel) @submit_button = @$(@oe).find(@submit_button_sel)
@save_button = @$(@oe).find(@save_button_sel)
@child_state = @oe.data('state') @child_state = @oe.data('state')
@child_type = @oe.data('child-type') @child_type = @oe.data('child-type')
if @child_type=="openended" if @child_type=="openended"
...@@ -270,6 +272,8 @@ class @CombinedOpenEnded ...@@ -270,6 +272,8 @@ class @CombinedOpenEnded
# rebind to the appropriate function for the current state # rebind to the appropriate function for the current state
@submit_button.unbind('click') @submit_button.unbind('click')
@submit_button.show() @submit_button.show()
@save_button.unbind('click')
@save_button.hide()
@reset_button.hide() @reset_button.hide()
@hide_file_upload() @hide_file_upload()
@next_problem_button.hide() @next_problem_button.hide()
...@@ -295,6 +299,8 @@ class @CombinedOpenEnded ...@@ -295,6 +299,8 @@ class @CombinedOpenEnded
@submit_button.prop('value', 'Submit') @submit_button.prop('value', 'Submit')
@submit_button.click @confirm_save_answer @submit_button.click @confirm_save_answer
@setup_file_upload() @setup_file_upload()
@save_button.click @store_answer
@save_button.show()
else if @child_state == 'assessing' else if @child_state == 'assessing'
@answer_area.attr("disabled", true) @answer_area.attr("disabled", true)
@replace_text_inputs() @replace_text_inputs()
...@@ -334,13 +340,26 @@ class @CombinedOpenEnded ...@@ -334,13 +340,26 @@ class @CombinedOpenEnded
else else
@reset_button.show() @reset_button.show()
find_assessment_elements: -> find_assessment_elements: ->
@assessment = @$('input[name="grade-selection"]') @assessment = @$('input[name="grade-selection"]')
find_hint_elements: -> find_hint_elements: ->
@hint_area = @$('textarea.post_assessment') @hint_area = @$('textarea.post_assessment')
store_answer: (event) =>
event.preventDefault()
if @child_state == 'initial'
data = {'student_answer' : @answer_area.val()}
@save_button.attr("disabled",true)
$.postWithPrefix "#{@ajax_url}/store_answer", data, (response) =>
if response.success
@gentle_alert("Answer saved.")
else
@errors_area.html(response.error)
@save_button.attr("disabled",false)
else
@errors_area.html(@out_of_sync_message)
replace_answer: (response) => replace_answer: (response) =>
if response.success if response.success
@rubric_wrapper.html(response.rubric_html) @rubric_wrapper.html(response.rubric_html)
...@@ -360,6 +379,7 @@ class @CombinedOpenEnded ...@@ -360,6 +379,7 @@ class @CombinedOpenEnded
@save_answer(event) if confirm('Please confirm that you wish to submit your work. You will not be able to make any changes after submitting.') @save_answer(event) if confirm('Please confirm that you wish to submit your work. You will not be able to make any changes after submitting.')
save_answer: (event) => save_answer: (event) =>
@$el.find(@oe_alert_sel).remove()
@submit_button.attr("disabled",true) @submit_button.attr("disabled",true)
@submit_button.hide() @submit_button.hide()
event.preventDefault() event.preventDefault()
......
...@@ -784,6 +784,7 @@ class CombinedOpenEndedV1Module(): ...@@ -784,6 +784,7 @@ class CombinedOpenEndedV1Module():
self.task_states[self.current_task_number] = self.current_task.get_instance_state() self.task_states[self.current_task_number] = self.current_task.get_instance_state()
self.current_task_number = 0 self.current_task_number = 0
self.ready_to_reset = False self.ready_to_reset = False
self.setup_next_task() self.setup_next_task()
return {'success': True, 'html': self.get_html_nonsystem()} return {'success': True, 'html': self.get_html_nonsystem()}
......
...@@ -605,6 +605,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -605,6 +605,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'save_post_assessment': self.message_post, 'save_post_assessment': self.message_post,
'skip_post_assessment': self.skip_post_assessment, 'skip_post_assessment': self.skip_post_assessment,
'check_for_score': self.check_for_score, 'check_for_score': self.check_for_score,
'store_answer': self.store_answer,
} }
if dispatch not in handlers: if dispatch not in handlers:
...@@ -688,8 +689,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -688,8 +689,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# set context variables and render template # set context variables and render template
eta_string = None eta_string = None
if self.child_state != self.INITIAL: if self.child_state != self.INITIAL:
latest = self.latest_answer()
previous_answer = latest if latest is not None else self.initial_display
post_assessment = self.latest_post_assessment(system) post_assessment = self.latest_post_assessment(system)
score = self.latest_score() score = self.latest_score()
correct = 'correct' if self.is_submission_correct(score) else 'incorrect' correct = 'correct' if self.is_submission_correct(score) else 'incorrect'
...@@ -698,8 +697,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -698,8 +697,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
else: else:
post_assessment = "" post_assessment = ""
correct = "" correct = ""
previous_answer = "" previous_answer = self.get_display_answer()
previous_answer = previous_answer.replace("\n","<br/>")
context = { context = {
'prompt': self.child_prompt, 'prompt': self.child_prompt,
'previous_answer': previous_answer, 'previous_answer': previous_answer,
......
...@@ -82,6 +82,7 @@ class OpenEndedChild(object): ...@@ -82,6 +82,7 @@ class OpenEndedChild(object):
self.child_state = instance_state.get('child_state', self.INITIAL) self.child_state = instance_state.get('child_state', self.INITIAL)
self.child_created = instance_state.get('child_created', False) self.child_created = instance_state.get('child_created', False)
self.child_attempts = instance_state.get('child_attempts', 0) self.child_attempts = instance_state.get('child_attempts', 0)
self.stored_answer = instance_state.get('stored_answer', None)
self.max_attempts = static_data['max_attempts'] self.max_attempts = static_data['max_attempts']
self.child_prompt = static_data['prompt'] self.child_prompt = static_data['prompt']
...@@ -195,6 +196,7 @@ class OpenEndedChild(object): ...@@ -195,6 +196,7 @@ class OpenEndedChild(object):
""" """
answer = OpenEndedChild.sanitize_html(answer) answer = OpenEndedChild.sanitize_html(answer)
self.child_history.append({'answer': answer}) self.child_history.append({'answer': answer})
self.stored_answer = None
def record_latest_score(self, score): def record_latest_score(self, score):
"""Assumes that state is right, so we're adding a score to the latest """Assumes that state is right, so we're adding a score to the latest
...@@ -231,6 +233,7 @@ class OpenEndedChild(object): ...@@ -231,6 +233,7 @@ class OpenEndedChild(object):
'max_score': self._max_score, 'max_score': self._max_score,
'child_attempts': self.child_attempts, 'child_attempts': self.child_attempts,
'child_created': False, 'child_created': False,
'stored_answer': self.stored_answer,
} }
return json.dumps(state) return json.dumps(state)
...@@ -262,6 +265,33 @@ class OpenEndedChild(object): ...@@ -262,6 +265,33 @@ class OpenEndedChild(object):
self.change_state(self.INITIAL) self.change_state(self.INITIAL)
return {'success': True} return {'success': True}
def get_display_answer(self):
latest = self.latest_answer()
if self.child_state == self.INITIAL:
if self.stored_answer is not None:
previous_answer = self.stored_answer
elif latest is not None and len(latest) > 0:
previous_answer = latest
else:
previous_answer = ""
previous_answer = previous_answer.replace("<br/>","\n").replace("<br>", "\n")
else:
if latest is not None and len(latest) > 0:
previous_answer = latest
else:
previous_answer = ""
previous_answer = previous_answer.replace("\n","<br/>")
return previous_answer
def store_answer(self, data, system):
if self.child_state != self.INITIAL:
# We can only store an answer if the problem has not moved into the assessment phase.
return self.out_of_sync_error(data)
self.stored_answer = data['student_answer']
return {'success': True}
def get_progress(self): def get_progress(self):
''' '''
For now, just return last score / max_score For now, just return last score / max_score
......
...@@ -55,13 +55,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -55,13 +55,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
@return: Rendered HTML @return: Rendered HTML
""" """
# set context variables and render template # set context variables and render template
if self.child_state != self.INITIAL: previous_answer = self.get_display_answer()
latest = self.latest_answer()
previous_answer = latest if latest is not None else ''
else:
previous_answer = ''
previous_answer = previous_answer.replace("\n","<br/>")
context = { context = {
'prompt': self.child_prompt, 'prompt': self.child_prompt,
'previous_answer': previous_answer, 'previous_answer': previous_answer,
...@@ -91,6 +86,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -91,6 +86,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
'save_answer': self.save_answer, 'save_answer': self.save_answer,
'save_assessment': self.save_assessment, 'save_assessment': self.save_assessment,
'save_post_assessment': self.save_hint, 'save_post_assessment': self.save_hint,
'store_answer': self.store_answer,
} }
if dispatch not in handlers: if dispatch not in handlers:
...@@ -218,13 +214,13 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -218,13 +214,13 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
return self.out_of_sync_error(data) return self.out_of_sync_error(data)
try: try:
score = int(data['assessment']) score = int(data.get('assessment'))
score_list = data.getlist('score_list[]') score_list = data.getlist('score_list[]')
for i in xrange(0, len(score_list)): for i in xrange(0, len(score_list)):
score_list[i] = int(score_list[i]) score_list[i] = int(score_list[i])
except ValueError: except (ValueError, TypeError):
# This is a dev_facing_error # This is a dev_facing_error
log.error("Non-integer score value passed to save_assessment ,or no score list present.") log.error("Non-integer score value passed to save_assessment, or no score list present.")
# This is a student_facing_error # This is a student_facing_error
return {'success': False, 'error': "Error saving your score. Please notify course staff."} return {'success': False, 'error': "Error saving your score. Please notify course staff."}
......
...@@ -344,6 +344,41 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -344,6 +344,41 @@ class OpenEndedModuleTest(unittest.TestCase):
score = self.openendedmodule.latest_score() score = self.openendedmodule.latest_score()
self.assertEquals(score, 1) self.assertEquals(score, 1)
def test_open_ended_display(self):
"""
Test storing answer with the open ended module.
"""
# Create a module with no state yet. Important that this start off as a blank slate.
test_module = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
saved_response = "Saved response."
submitted_response = "Submitted response."
# Initially, there will be no stored answer.
self.assertEqual(test_module.stored_answer, None)
# And the initial answer to display will be an empty string.
self.assertEqual(test_module.get_display_answer(), "")
# Now, store an answer in the module.
test_module.handle_ajax("store_answer", {'student_answer' : saved_response}, get_test_system())
# The stored answer should now equal our response.
self.assertEqual(test_module.stored_answer, saved_response)
self.assertEqual(test_module.get_display_answer(), saved_response)
# Mock out the send_to_grader function so it doesn't try to connect to the xqueue.
test_module.send_to_grader = Mock(return_value=True)
# Submit a student response to the question.
test_module.handle_ajax(
"save_answer",
{"student_answer": submitted_response, "can_upload_files": False, "student_file": None},
get_test_system()
)
# Submitting an answer should clear the stored answer.
self.assertEqual(test_module.stored_answer, None)
# Confirm that the answer is stored properly.
self.assertEqual(test_module.latest_answer(), submitted_response)
class CombinedOpenEndedModuleTest(unittest.TestCase): class CombinedOpenEndedModuleTest(unittest.TestCase):
""" """
......
...@@ -4,6 +4,7 @@ import unittest ...@@ -4,6 +4,7 @@ import unittest
from xmodule.open_ended_grading_classes.self_assessment_module import SelfAssessmentModule from xmodule.open_ended_grading_classes.self_assessment_module import SelfAssessmentModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.tests.test_util_open_ended import MockQueryDict
from lxml import etree from lxml import etree
from . import get_test_system from . import get_test_system
...@@ -38,7 +39,7 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -38,7 +39,7 @@ class SelfAssessmentTest(unittest.TestCase):
'state': SelfAssessmentModule.INITIAL, 'state': SelfAssessmentModule.INITIAL,
'attempts': 2}) 'attempts': 2})
static_data = { self.static_data = {
'max_attempts': 10, 'max_attempts': 10,
'rubric': etree.XML(self.rubric), 'rubric': etree.XML(self.rubric),
'prompt': self.prompt, 'prompt': self.prompt,
...@@ -60,7 +61,7 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -60,7 +61,7 @@ class SelfAssessmentTest(unittest.TestCase):
self.module = SelfAssessmentModule(get_test_system(), self.location, self.module = SelfAssessmentModule(get_test_system(), self.location,
self.definition, self.definition,
self.descriptor, self.descriptor,
static_data) self.static_data)
def test_get_html(self): def test_get_html(self):
html = self.module.get_html(self.module.system) html = self.module.get_html(self.module.system)
...@@ -104,3 +105,49 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -104,3 +105,49 @@ class SelfAssessmentTest(unittest.TestCase):
responses['assessment'] = '1' responses['assessment'] = '1'
self.module.save_assessment(mock_query_dict, self.module.system) self.module.save_assessment(mock_query_dict, self.module.system)
self.assertEqual(self.module.child_state, self.module.DONE) self.assertEqual(self.module.child_state, self.module.DONE)
def test_self_assessment_display(self):
"""
Test storing an answer with the self assessment module.
"""
# Create a module with no state yet. Important that this start off as a blank slate.
test_module = SelfAssessmentModule(get_test_system(), self.location,
self.definition,
self.descriptor,
self.static_data)
saved_response = "Saved response."
submitted_response = "Submitted response."
# Initially, there will be no stored answer.
self.assertEqual(test_module.stored_answer, None)
# And the initial answer to display will be an empty string.
self.assertEqual(test_module.get_display_answer(), "")
# Now, store an answer in the module.
test_module.handle_ajax("store_answer", {'student_answer' : saved_response}, get_test_system())
# The stored answer should now equal our response.
self.assertEqual(test_module.stored_answer, saved_response)
self.assertEqual(test_module.get_display_answer(), saved_response)
# Submit a student response to the question.
test_module.handle_ajax("save_answer", {"student_answer": submitted_response, "can_upload_files": False, "student_file": None}, get_test_system())
# Submitting an answer should clear the stored answer.
self.assertEqual(test_module.stored_answer, None)
# Confirm that the answer is stored properly.
self.assertEqual(test_module.latest_answer(), submitted_response)
# Mock saving an assessment.
assessment = [0]
assessment_dict = MockQueryDict({'assessment': sum(assessment), 'score_list[]': assessment})
data = test_module.handle_ajax("save_assessment", assessment_dict, get_test_system())
self.assertTrue(json.loads(data)['success'])
# Reset the module so the student can try again.
test_module.reset(get_test_system())
# Confirm that the right response is loaded.
self.assertEqual(test_module.get_display_answer(), submitted_response)
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
</div> </div>
<div class="file-upload"></div> <div class="file-upload"></div>
<input type="button" value="${_('Save')}" class="save-button" name="save"/>
<input type="button" value="${_('Submit')}" class="submit-button" name="show"/> <input type="button" value="${_('Submit')}" class="submit-button" name="show"/>
<input name="skip" class="skip-button" type="button" value="${_('Skip Post-Assessment')}"/> <input name="skip" class="skip-button" type="button" value="${_('Skip Post-Assessment')}"/>
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
<div class="rubric-wrapper">${initial_rubric}</div> <div class="rubric-wrapper">${initial_rubric}</div>
<div class="file-upload"></div> <div class="file-upload"></div>
<input type="button" value="${_('Save')}" class="save-button" name="save"/>
<input type="button" value="${_('Submit')}" class="submit-button" name="show"/> <input type="button" value="${_('Submit')}" class="submit-button" name="show"/>
<div class="open-ended-action"></div> <div class="open-ended-action"></div>
......
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