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 @@
- + diff --git a/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html b/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html index ab4d9cfb95..93b0e7bacd 100644 --- a/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html +++ b/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html @@ -19,6 +19,7 @@
${initial_rubric}
+