Commit f4e54aa0 by Will Daly

Students can save their response before submitting

parent 93544bbd
......@@ -31,11 +31,13 @@
</div>
<div class="step__content">
<!-- @talbs: This is some more ugly placeholder stuff that you can replace -->
<div id="response__save_status">{{ save_status }}</div>
<form id="response__submission" class="response__submission">
<ol class="list list--fields response__submission__content">
<li class="field field--textarea submission__answer" id="submission__answer">
<label class="sr" for="submission__answer__value">Please provide your response to the above question</label>
<textarea id="submission__answer__value" placeholder=""></textarea>
<textarea id="submission__answer__value" placeholder="">{{ saved_response }}</textarea>
</li>
</ol>
......
......@@ -191,6 +191,12 @@ class OpenAssessmentBlock(XBlock, SubmissionMixin, PeerAssessmentMixin, SelfAsse
help="The course_id associated with this prompt (until we can get it from runtime).",
)
saved_response = String(
default=u"",
scope=Scope.user_state,
help="Saved response submission for the current user."
)
def get_xblock_trace(self):
"""Uniquely identify this XBlock by context.
......
......@@ -28,7 +28,7 @@ describe("OpenAssessment.StudioUI", function() {
else {
return this.errorPromise;
}
}
};
this.updateXml = function(xml) {
if (!this.updateError) {
......@@ -40,7 +40,7 @@ describe("OpenAssessment.StudioUI", function() {
else {
return this.errorPromise;
}
}
};
};
var server = null;
......@@ -49,7 +49,7 @@ describe("OpenAssessment.StudioUI", function() {
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures'
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_edit.html');
// Create the stub server
......@@ -65,7 +65,7 @@ describe("OpenAssessment.StudioUI", function() {
it("loads the XML definition", function() {
// Initialize the UI
ui.load()
ui.load();
// Expect that the XML definition was loaded
var contents = ui.codeBox.getValue();
......
......@@ -6,8 +6,8 @@ describe("OpenAssessment.Server", function() {
// Stub runtime implementation that returns the handler as the URL
var runtime = {
handlerUrl: function(element, handler) { return "/" + handler }
}
handlerUrl: function(element, handler) { return "/" + handler; }
};
var server = null;
......@@ -25,10 +25,10 @@ describe("OpenAssessment.Server", function() {
spyOn($, 'ajax').andReturn(
$.Deferred(function(defer) {
if (success) { defer.resolveWith(this, [responseData]); }
else { defer.reject() }
else { defer.reject(); }
}).promise()
);
}
};
beforeEach(function() {
// Create the server
......@@ -37,7 +37,7 @@ describe("OpenAssessment.Server", function() {
server = new OpenAssessment.Server(runtime, null);
});
it("Renders the XBlock as HTML", function() {
it("renders the XBlock as HTML", function() {
stubAjax(true, "<div>Open Assessment</div>");
var loadedHtml = "";
......@@ -51,7 +51,7 @@ describe("OpenAssessment.Server", function() {
});
});
it("Sends a submission to the XBlock", function() {
it("sends a submission to the XBlock", function() {
// Status, student ID, attempt number
stubAjax(true, [true, 1, 2]);
......@@ -73,11 +73,23 @@ describe("OpenAssessment.Server", function() {
});
});
it("Sends an assessment to the XBlock", function() {
stubAjax(true, {success: true, msg:''});
it("saves a response submission", function() {
stubAjax(true, {'success': true, 'msg': ''});
var success = false;
server.save("Test").done(function() { success = true; });
expect(success).toBe(true);
expect($.ajax).toHaveBeenCalledWith({
url: "/save_submission",
type: "POST",
data: JSON.stringify({submission: "Test"})
});
});
it("sends an assessment to the XBlock", function() {
stubAjax(true, {success: true, msg: ''});
var success = false;
var options = {clarity: "Very clear", precision: "Somewhat precise"}
var options = {clarity: "Very clear", precision: "Somewhat precise"};
server.assess("abc1234", options, "Excellent job!").done(function() {
success = true;
});
......@@ -161,6 +173,20 @@ describe("OpenAssessment.Server", function() {
expect(receivedErrorMsg).toEqual("Error occurred!");
});
it("informs the caller of an AJAX error when sending a submission", function() {
stubAjax(false, null);
var receivedMsg = null;
server.save("Test").fail(function(errorMsg) { receivedMsg = errorMsg; });
expect(receivedMsg).toEqual('Could not contact server.');
});
it("informs the caller of a server error when sending a submission", function() {
stubAjax(true, {'success': false, 'msg': 'test error'});
var receivedMsg = null;
server.save("Test").fail(function(errorMsg) { receivedMsg = errorMsg; });
expect(receivedMsg).toEqual('test error');
});
it("informs the caller of an Ajax error when loading XML", function() {
stubAjax(false, null);
......@@ -205,11 +231,11 @@ describe("OpenAssessment.Server", function() {
expect(receivedMsg).toEqual("Test error");
});
it("informs the caller of a server error when sending an assessment", function() {
it("informs the caller of a server error when sending an assessment", function() {
stubAjax(true, {success:false, msg:'Test error!'});
var receivedMsg = null;
var options = {clarity: "Very clear", precision: "Somewhat precise"}
var options = {clarity: "Very clear", precision: "Somewhat precise"};
server.assess("abc1234", options, "Excellent job!").fail(function(msg) {
receivedMsg = msg;
});
......@@ -217,11 +243,11 @@ describe("OpenAssessment.Server", function() {
expect(receivedMsg).toEqual("Test error!");
});
it("informs the caller of an AJAX error when sending an assessment", function() {
it("informs the caller of an AJAX error when sending an assessment", function() {
stubAjax(false, null);
var receivedMsg = null;
var options = {clarity: "Very clear", precision: "Somewhat precise"}
var options = {clarity: "Very clear", precision: "Somewhat precise"};
server.assess("abc1234", options, "Excellent job!").fail(function(msg) {
receivedMsg = msg;
});
......
......@@ -21,7 +21,7 @@ OpenAssessment.BaseUI = function(runtime, element, server) {
this.runtime = runtime;
this.element = element;
this.server = server;
}
};
OpenAssessment.BaseUI.prototype = {
......@@ -71,8 +71,15 @@ OpenAssessment.BaseUI.prototype = {
// Install a click handler for submission
$('#step--response__submit', ui.element).click(
function(eventObject) { ui.submit(); }
);
// Install a click handler for the save button
$('#submission__save', ui.element).click(
function(eventObject) {
ui.submit();
// Override default form submission
eventObject.preventDefault();
ui.save();
}
);
}
......@@ -153,6 +160,23 @@ OpenAssessment.BaseUI.prototype = {
},
/**
Save a response without submitting it.
**/
save: function() {
// Retrieve the student's response from the DOM
var submission = $('#submission__answer__value', this.element).val();
var ui = this;
this.server.save(submission).done(function() {
// Update the "saved" icon
$('#response__save_status', this.element).replaceWith("Saved");
}).fail(function(errMsg) {
// TODO: display to the user
console.log(errMsg);
});
},
/**
Send a submission to the server and update the UI.
**/
submit: function() {
......@@ -199,7 +223,7 @@ OpenAssessment.BaseUI.prototype = {
console.log(errMsg);
});
}
}
};
/* XBlock JavaScript entry point for OpenAssessmentXBlock. */
function OpenAssessmentBlock(runtime, element) {
......
......@@ -100,7 +100,33 @@ OpenAssessment.Server.prototype = {
}
}).fail(function(data) {
defer.rejectWith(this, ["AJAX", "Could not contact server."]);
})
});
}).promise();
},
/**
Save a response without submitting it.
Args:
submission (string): The text of the student's response.
Returns:
A JQuery promise, which resolves with no arguments on success and
fails with an error message.
**/
save: function(submission) {
var url = this.url('save_submission');
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({submission: submission})
}).done(function(data) {
if (data.success) { defer.resolve(); }
else { defer.rejectWith(this, [data.msg]); }
}).fail(function(data) {
defer.rejectWith(this, ["Could not contact server."]);
});
}).promise();
},
......
from xblock.core import XBlock
from submissions import api
from django.utils.translation import ugettext as _
from openassessment.peer import api as peer_api
......@@ -68,6 +69,30 @@ class SubmissionMixin(object):
status_text = status_text if status_text else self.submit_errors[status_tag]
return status, status_tag, status_text
@XBlock.json_handler
def save_submission(self, data, suffix=''):
"""
Save the current student's response submission.
If the student already has a response saved, this will overwrite it.
Args:
data (dict): Data should have a single key 'submission' that contains
the text of the student's response.
suffix (str): Not used.
Returns:
dict: Contains a bool 'success' and unicode string 'msg'.
"""
if 'submission' in data:
try:
self.saved_response = unicode(data['submission'])
except:
return {'success': False, 'msg': _(u"Could not save response submission")}
else:
return {'success': True, 'msg': u''}
else:
return {'success': False, 'msg': _(u"Missing required key 'submission'")}
@staticmethod
def _get_submission_score(student_item_dict):
"""Return the most recent score, if any, for student item
......@@ -145,6 +170,8 @@ class SubmissionMixin(object):
"student_submission": student_submission,
"student_score": student_score,
"step_status": step_status,
"saved_response": self.saved_response,
"save_status": _('Saved but not submitted') if len(self.saved_response) > 0 else _("Not saved"),
}
path = "openassessmentblock/response/oa_response.html"
......
{
"empty": [""],
"unicode": ["Ѕраѓтаиѕ! ГоиіБЂт, Щэ ↁіиэ іи Нэll!"],
"long": [
"Lorem ipsum dolor sit amet,",
"consectetur adipiscing elit. Etiam luctus dapibus ante, vel luctus nibh bibendum et.",
"Praesent in commodo quam. Morbi lobortis at felis ac mollis.",
"Maecenas placerat nisl sed imperdiet posuere.",
"Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.",
"Vivamus convallis augue et tincidunt iaculis.",
"Pellentesque fermentum mauris mauris, in adipiscing justo venenatis vel.",
"Aenean ligula lorem, aliquet in arcu sit amet, porta pellentesque urna. Aliquam a porttitor libero.",
"Aliquam mattis, ligula et pellentesque porta, nibh urna vulputate ante, in blandit elit nisl et massa.",
"Morbi et porttitor elit. Proin interdum nisi vitae diam feugiat, eget vestibulum nisi tincidunt.",
"Suspendisse at mi non ipsum sollicitudin euismod nec sit amet dolor.",
"Etiam rutrum, tellus ut ullamcorper elementum, nulla sem lobortis mauris,",
"quis laoreet diam nunc sed tellus. Pellentesque vitae enim convallis, euismod lectus vel, tristique nibh.",
"Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;",
"Quisque quis fermentum arcu. Pellentesque a hendrerit eros, sed pellentesque justo.",
"Cras volutpat neque ut nisl tristique, in pellentesque diam faucibus.",
"Sed at egestas eros, at ornare felis. Donec a diam non magna sollicitudin lacinia sit amet at eros.",
"Quisque ac nisl et tellus gravida viverra fermentum eget massa.",
"Maecenas eleifend iaculis libero a dignissim.",
"Quisque vel viverra massa. Etiam eget arcu dignissim, semper lectus ac, aliquet massa.",
"Nulla vulputate enim ut ligula dictum, ac dapibus risus laoreet.",
"Donec at purus a tellus dapibus condimentum non sit amet justo.",
"Etiam fringilla nisl eu justo venenatis, ut egestas nibh auctor.",
"Cras fermentum felis massa, ut porta tortor malesuada a.",
"Vestibulum eget felis lacus. Donec lectus elit, aliquam eu ligula non, consectetur elementum nibh.",
"Suspendisse vehicula ornare erat, a adipiscing justo consequat nec.",
"Nam erat nulla, suscipit vel tortor non, consequat hendrerit velit.",
"Sed quis condimentum tortor, ut dictum enim. Sed tristique euismod magna in luctus.",
"Phasellus sodales nunc sit amet suscipit imperdiet. Suspendisse tincidunt erat urna, ac iaculis risus tincidunt vitae.",
"Aliquam id convallis est. Morbi enim neque, egestas vel diam ut, faucibus aliquam lectus.",
"Nulla congue egestas quam, lacinia pretium diam blandit sit amet.",
"Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;",
"Cras consequat feugiat ultrices. Nam tortor massa, pharetra non consectetur ac, tincidunt sit amet nisl.",
"Maecenas pellentesque, nibh vitae dapibus dapibus, diam mauris lacinia mauris,",
"ullamcorper gravida erat tellus congue est.",
"Aenean non mauris ligula. Curabitur eu mattis leo.",
"Proin non elit eget felis tincidunt porta ut sed lorem.",
"Integer quis consequat libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra,",
"per inceptos himenaeos.",
"Morbi aliquam mi eu lectus fermentum ornare.",
"Morbi sagittis mollis mi, sit amet convallis sapien elementum et.",
"Integer aliquam justo elit, ornare faucibus quam lobortis id. Duis eu diam augue.",
"Maecenas eros erat, molestie quis justo id, mattis ultricies ligula.",
"Morbi turpis justo, rhoncus ac leo non, iaculis blandit erat.",
"Cum sociis natoque penatibus et magnis dis parturient montes,",
"nascetur ridiculus mus. Mauris at dapibus mauris, sed pharetra tortor.",
"Pellentesque purus sem, congue sed elementum non, pretium in mi. Cras cursus gravida commodo.",
"Aenean eu massa rhoncus, faucibus tortor id, sollicitudin tortor."
]
}
\ No newline at end of file
<openassessment>
<title>Open Assessment Test</title>
<prompt>
Given the state of the world today, what do you think should be done to
combat poverty? Please answer in a short essay of 200-300 words.
</prompt>
<rubric>
<prompt>Read for conciseness, clarity of thought, and form.</prompt>
<criterion>
<name>Concise</name>
<prompt>How concise is it?</prompt>
<option points="0">
<name>Neal Stephenson (late)</name>
<explanation>Neal Stephenson explanation</explanation>
</option>
<option points="1">
<name>HP Lovecraft</name>
<explanation>HP Lovecraft explanation</explanation>
</option>
<option points="3">
<name>Robert Heinlein</name>
<explanation>Robert Heinlein explanation</explanation>
</option>
<option points="4">
<name>Neal Stephenson (early)</name>
<explanation>Neal Stephenson (early) explanation</explanation>
</option>
<option points="5">
<name>Earnest Hemingway</name>
<explanation>Earnest Hemingway</explanation>
</option>
</criterion>
<criterion>
<name>Clear-headed</name>
<prompt>How clear is the thinking?</prompt>
<option points="0">
<name>Yogi Berra</name>
<explanation>Yogi Berra explanation</explanation>
</option>
<option points="1">
<name>Hunter S. Thompson</name>
<explanation>Hunter S. Thompson explanation</explanation>
</option>
<option points="2">
<name>Robert Heinlein</name>
<explanation>Robert Heinlein explanation</explanation>
</option>
<option points="3">
<name>Isaac Asimov</name>
<explanation>Isaac Asimov explanation</explanation>
</option>
<option points="10">
<name>Spock</name>
<explanation>Spock explanation</explanation>
</option>
</criterion>
<criterion>
<name>Form</name>
<prompt>Lastly, how is it's form? Punctuation, grammar, and spelling all count.</prompt>
<option points="0">
<name>lolcats</name>
<explanation>lolcats explanation</explanation>
</option>
<option points="1">
<name>Facebook</name>
<explanation>Facebook explanation</explanation>
</option>
<option points="2">
<name>Reddit</name>
<explanation>Reddit explanation</explanation>
</option>
<option points="3">
<name>metafilter</name>
<explanation>metafilter explanation</explanation>
</option>
<option points="4">
<name>Usenet, 1996</name>
<explanation>Usenet, 1996 explanation</explanation>
</option>
<option points="5">
<name>The Elements of Style</name>
<explanation>The Elements of Style explanation</explanation>
</option>
</criterion>
</rubric>
<assessments>
<assessment name="peer-assessment"
start="2014-12-20T19:00"
due="2014-12-21T22:22"
must_grade="5"
must_be_graded_by="3" />
</assessments>
</openassessment>
# -*- coding: utf-8 -*-
"""
Test that the student can save a response.
"""
import json
import ddt
from .base import XBlockHandlerTestCase, scenario
@ddt.ddt
class SaveResponseTest(XBlockHandlerTestCase):
@scenario('data/save_scenario.xml', user_id="Daniels")
def test_default_saved_response_blank(self, xblock):
resp = self.request(xblock, 'render_submission', json.dumps({}))
self.assertIn('<textarea id="submission__answer__value" placeholder=""></textarea>', resp)
@ddt.file_data('data/save_responses.json')
@scenario('data/save_scenario.xml', user_id="Perleman")
def test_save_response(self, xblock, data):
# Save the response
submission_text = " ".join(data)
payload = json.dumps({'submission': submission_text })
resp = self.request(xblock, 'save_submission', payload, response_format="json")
self.assertTrue(resp['success'])
self.assertEqual(resp['msg'], u'')
# Reload the submission UI
resp = self.request(xblock, 'render_submission', json.dumps({}))
expected_html = u'<textarea id="submission__answer__value" placeholder="">{submitted}</textarea>'.format(
submitted=submission_text
)
self.assertIn(expected_html, resp.decode('utf-8'))
@scenario('data/save_scenario.xml', user_id="Valchek")
def test_overwrite_saved_response(self, xblock):
# XBlock has a saved response already
xblock.saved_response = (
u"THAT'ꙅ likɘ A 40-bɘgᴙɘɘ bAY."
u"Aiᴎ'T ᴎodobY goT ᴎoTHiᴎg To ꙅAY AdoUT A 40-bɘgᴙɘɘ bAY."
u"ꟻiꟻTY. dᴙiᴎg A ꙅmilɘ To YoUᴙ ꟻAↄɘ."
)
# Save another response
submission_text = u"ГЂіи lіиэ ъэтшээи Ђэаvэи аиↁ Ђэѓэ."
payload = json.dumps({'submission': submission_text })
resp = self.request(xblock, 'save_submission', payload, response_format="json")
self.assertTrue(resp['success'])
# Verify that the saved response was overwritten
self.assertEqual(xblock.saved_response, submission_text)
@scenario('data/save_scenario.xml', user_id="Bubbles")
def test_missing_submission_key(self, xblock):
resp = self.request(xblock, 'save_submission', json.dumps({}), response_format="json")
self.assertFalse(resp['success'])
self.assertIn('submission', resp['msg'])
\ No newline at end of file
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