Commit 57fb306c by Vik Paruchuri

Merge pull request #1068 from edx/feature/vik/oe-save

Implement a save button for open ended responses
parents 936d2e0f d9ce5e07
......@@ -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
......
......@@ -322,7 +322,7 @@ div.combined-rubric-container {
div.written-feedback {
background: #f6f6f6;
padding: 15px;
padding: 5px;
}
}
......
......@@ -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()
......
......@@ -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()}
......
......@@ -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","<br/>")
previous_answer = self.get_display_answer()
context = {
'prompt': self.child_prompt,
'previous_answer': previous_answer,
......
......@@ -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("<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):
'''
For now, just return last score / max_score
......
......@@ -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","<br/>")
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."}
......
......@@ -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):
"""
......
......@@ -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)
......@@ -30,7 +30,7 @@
</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 name="skip" class="skip-button" type="button" value="${_('Skip Post-Assessment')}"/>
......
......@@ -19,6 +19,7 @@
<div class="rubric-wrapper">${initial_rubric}</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"/>
<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