Merge pull request #1068 from edx/feature/vik/oe-save
Implement a save button for open ended responses
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user