diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py
index 10a5e61519..a23d0ef1f0 100644
--- a/common/lib/xmodule/xmodule/combined_open_ended_module.py
+++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py
@@ -14,13 +14,28 @@ import textwrap
log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = [
- "display_name", "max_attempts", "graded", "accept_file_upload",
- "skip_spelling_checks", "due", "graceperiod", "weight", "min_to_calibrate",
- "max_to_calibrate", "peer_grader_count", "required_peer_grading",
+ "display_name",
+ "max_attempts",
+ "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",
- "student_attempts", "ready_to_reset", "old_task_states"]
+V1_STUDENT_ATTRIBUTES = [
+ "current_task_number",
+ "task_states",
+ "state",
+ "student_attempts",
+ "ready_to_reset",
+ "old_task_states",
+]
V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES
diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss
index 7b61e59dbd..9f76752bf7 100644
--- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss
+++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss
@@ -322,7 +322,7 @@ div.combined-rubric-container {
div.written-feedback {
background: #f6f6f6;
- padding: 15px;
+ padding: 5px;
}
}
diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
index 8759bd3102..d1a7b4d816 100644
--- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
@@ -119,6 +119,7 @@ class @CombinedOpenEnded
next_rubric_sel: '.rubric-next-button'
previous_rubric_sel: '.rubric-previous-button'
oe_alert_sel: '.open-ended-alert'
+ save_button_sel: '.save-button'
constructor: (el) ->
@el=el
@@ -183,6 +184,7 @@ class @CombinedOpenEnded
@hint_wrapper = @$(@oe).find(@hint_wrapper_sel)
@message_wrapper = @$(@oe).find(@message_wrapper_sel)
@submit_button = @$(@oe).find(@submit_button_sel)
+ @save_button = @$(@oe).find(@save_button_sel)
@child_state = @oe.data('state')
@child_type = @oe.data('child-type')
if @child_type=="openended"
@@ -270,6 +272,8 @@ class @CombinedOpenEnded
# rebind to the appropriate function for the current state
@submit_button.unbind('click')
@submit_button.show()
+ @save_button.unbind('click')
+ @save_button.hide()
@reset_button.hide()
@hide_file_upload()
@next_problem_button.hide()
@@ -295,6 +299,8 @@ class @CombinedOpenEnded
@submit_button.prop('value', 'Submit')
@submit_button.click @confirm_save_answer
@setup_file_upload()
+ @save_button.click @store_answer
+ @save_button.show()
else if @child_state == 'assessing'
@answer_area.attr("disabled", true)
@replace_text_inputs()
@@ -334,13 +340,26 @@ class @CombinedOpenEnded
else
@reset_button.show()
-
find_assessment_elements: ->
@assessment = @$('input[name="grade-selection"]')
find_hint_elements: ->
@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) =>
if response.success
@rubric_wrapper.html(response.rubric_html)
@@ -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) =>
+ @$el.find(@oe_alert_sel).remove()
@submit_button.attr("disabled",true)
@submit_button.hide()
event.preventDefault()
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
index 44ab492af4..083998b323 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
@@ -784,6 +784,7 @@ class CombinedOpenEndedV1Module():
self.task_states[self.current_task_number] = self.current_task.get_instance_state()
self.current_task_number = 0
self.ready_to_reset = False
+
self.setup_next_task()
return {'success': True, 'html': self.get_html_nonsystem()}
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index 66c13e7f33..d7fe8c0d26 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -605,6 +605,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'save_post_assessment': self.message_post,
'skip_post_assessment': self.skip_post_assessment,
'check_for_score': self.check_for_score,
+ 'store_answer': self.store_answer,
}
if dispatch not in handlers:
@@ -688,8 +689,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# set context variables and render template
eta_string = None
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)
score = self.latest_score()
correct = 'correct' if self.is_submission_correct(score) else 'incorrect'
@@ -698,8 +697,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
else:
post_assessment = ""
correct = ""
- previous_answer = ""
- previous_answer = previous_answer.replace("\n","
")
+ previous_answer = self.get_display_answer()
+
context = {
'prompt': self.child_prompt,
'previous_answer': previous_answer,
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
index 2f8d4fa866..d7555ce77e 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
@@ -82,6 +82,7 @@ class OpenEndedChild(object):
self.child_state = instance_state.get('child_state', self.INITIAL)
self.child_created = instance_state.get('child_created', False)
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.child_prompt = static_data['prompt']
@@ -195,6 +196,7 @@ class OpenEndedChild(object):
"""
answer = OpenEndedChild.sanitize_html(answer)
self.child_history.append({'answer': answer})
+ self.stored_answer = None
def record_latest_score(self, score):
"""Assumes that state is right, so we're adding a score to the latest
@@ -231,6 +233,7 @@ class OpenEndedChild(object):
'max_score': self._max_score,
'child_attempts': self.child_attempts,
'child_created': False,
+ 'stored_answer': self.stored_answer,
}
return json.dumps(state)
@@ -262,6 +265,33 @@ class OpenEndedChild(object):
self.change_state(self.INITIAL)
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("
","\n").replace("
", "\n")
+ else:
+ if latest is not None and len(latest) > 0:
+ previous_answer = latest
+ else:
+ previous_answer = ""
+ previous_answer = previous_answer.replace("\n","
")
+
+ 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):
'''
For now, just return last score / max_score
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
index 2485fc77ea..cc830f88c8 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
@@ -55,13 +55,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
@return: Rendered HTML
"""
# set context variables and render template
- if self.child_state != self.INITIAL:
- latest = self.latest_answer()
- previous_answer = latest if latest is not None else ''
- else:
- previous_answer = ''
+ previous_answer = self.get_display_answer()
- previous_answer = previous_answer.replace("\n","
")
context = {
'prompt': self.child_prompt,
'previous_answer': previous_answer,
@@ -91,6 +86,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
'save_answer': self.save_answer,
'save_assessment': self.save_assessment,
'save_post_assessment': self.save_hint,
+ 'store_answer': self.store_answer,
}
if dispatch not in handlers:
@@ -218,13 +214,13 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
return self.out_of_sync_error(data)
try:
- score = int(data['assessment'])
+ score = int(data.get('assessment'))
score_list = data.getlist('score_list[]')
for i in xrange(0, len(score_list)):
score_list[i] = int(score_list[i])
- except ValueError:
+ except (ValueError, TypeError):
# 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
return {'success': False, 'error': "Error saving your score. Please notify course staff."}
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 1b3fd36f45..5944910bab 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -344,6 +344,41 @@ class OpenEndedModuleTest(unittest.TestCase):
score = self.openendedmodule.latest_score()
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):
"""
diff --git a/common/lib/xmodule/xmodule/tests/test_self_assessment.py b/common/lib/xmodule/xmodule/tests/test_self_assessment.py
index c9140d643a..2dec24daa1 100644
--- a/common/lib/xmodule/xmodule/tests/test_self_assessment.py
+++ b/common/lib/xmodule/xmodule/tests/test_self_assessment.py
@@ -4,6 +4,7 @@ import unittest
from xmodule.open_ended_grading_classes.self_assessment_module import SelfAssessmentModule
from xmodule.modulestore import Location
+from xmodule.tests.test_util_open_ended import MockQueryDict
from lxml import etree
from . import get_test_system
@@ -38,7 +39,7 @@ class SelfAssessmentTest(unittest.TestCase):
'state': SelfAssessmentModule.INITIAL,
'attempts': 2})
- static_data = {
+ self.static_data = {
'max_attempts': 10,
'rubric': etree.XML(self.rubric),
'prompt': self.prompt,
@@ -60,7 +61,7 @@ class SelfAssessmentTest(unittest.TestCase):
self.module = SelfAssessmentModule(get_test_system(), self.location,
self.definition,
self.descriptor,
- static_data)
+ self.static_data)
def test_get_html(self):
html = self.module.get_html(self.module.system)
@@ -104,3 +105,49 @@ class SelfAssessmentTest(unittest.TestCase):
responses['assessment'] = '1'
self.module.save_assessment(mock_query_dict, self.module.system)
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)
+
+
diff --git a/lms/templates/combinedopenended/openended/open_ended.html b/lms/templates/combinedopenended/openended/open_ended.html
index c6a8cb2253..52d8042038 100644
--- a/lms/templates/combinedopenended/openended/open_ended.html
+++ b/lms/templates/combinedopenended/openended/open_ended.html
@@ -30,7 +30,7 @@