Commit 6a28f277 by Will Daly

Rename JavaScript UI classes "views"

Move JavaScript response step code into separate view class.
Implement unsaved draft and save button enable/disable
parent c3c120d2
......@@ -71,7 +71,8 @@
<ul class="list list--actions">
<li class="list--actions__item">
<a aria-role="button" href="#" id="step--response__submit" class="action action--submit step--response__submit is--disabled">
<a aria-role="button" href="#" id="step--response__submit"
class="action action--submit step--response__submit {{ submit_enabled|yesno:",is--disabled" }}">
<span class="copy">Submit your response and move to the next step</span>
<i class="ico icon-caret-right"></i>
</a>
......
<div id="openassessment-base">
<ol>
<li id="openassessment__response" class="openassessment__steps__step step--response ui-toggle-visibility">
<div class="ui-toggle-visibility__content">
<div class="wrapper--step__content">
<div class="step__content">
<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">Provide your response to the question.</label>
<textarea id="submission__answer__value" placeholder=""></textarea>
<span class="tip">You may continue to work on your response until you submit it.</span>
</li>
</ol>
<div class="response__submission__actions">
<div class="message message--inline message--error message--error-server">
<h3 class="message__title">We could not save your progress</h3>
</div>
<ul class="list list--actions">
<li class="list--actions__item">
<button type="submit" id="submission__save" class="action action--save submission__save is--disabled">Save Your Progress</button>
<div id="response__save_status" class="response__submission__status">
<h3 class="response__submission__status__title">
<span class="sr">Your Working Submission Status:</span>
Unsaved draft
</h3>
</div>
</li>
</ul>
</div>
</form>
</div>
<div class="step__actions">
<div class="message message--inline message--error message--error-server">
<h3 class="message__title">We could not submit your response</h3>
</div>
<ul class="list list--actions">
<li class="list--actions__item">
<a aria-role="button" href="#" id="step--response__submit" class="action action--submit step--response__submit is--disabled">
<span class="copy">Submit your response and move to the next step</span>
<i class="ico icon-caret-right"></i>
</a>
</li>
</ul>
</div>
</div>
</div>
</li>
</ol>
</div>
......@@ -2,7 +2,7 @@
Tests for OA student-facing views.
**/
describe("OpenAssessment.BaseUI", function() {
describe("OpenAssessment.BaseView", function() {
// Stub server that returns dummy data
var StubServer = function() {
......@@ -15,12 +15,6 @@ describe("OpenAssessment.BaseUI", function() {
grade: "Test fragment"
};
this.submit = function(submission) {
return $.Deferred(function(defer) {
defer.resolveWith(this, ['student', 0]);
}).promise();
};
this.peerAssess = function(submissionId, optionsSelected, feedback) {
return $.Deferred(function(defer) { defer.resolve(); }).promise();
};
......@@ -42,7 +36,7 @@ describe("OpenAssessment.BaseUI", function() {
this.feedbackOptions = options;
// Return a promise that always resolves successfully
return $.Deferred(function(defer) { defer.resolve() }).promise();
return $.Deferred(function(defer) { defer.resolve(); }).promise();
};
};
......@@ -50,7 +44,7 @@ describe("OpenAssessment.BaseUI", function() {
var runtime = {};
var server = null;
var ui = null;
var view = null;
/**
Wait for subviews to load before executing callback.
......@@ -60,7 +54,7 @@ describe("OpenAssessment.BaseUI", function() {
**/
var loadSubviews = function(callback) {
runs(function() {
ui.load();
view.load();
});
waitsFor(function() {
......@@ -85,21 +79,13 @@ describe("OpenAssessment.BaseUI", function() {
// Create the object under test
var el = $("#openassessment-base").get(0);
ui = new OpenAssessment.BaseUI(runtime, el, server);
});
it("Sends a submission to the server", function() {
loadSubviews(function() {
spyOn(server, 'submit').andCallThrough();
ui.submit();
expect(server.submit).toHaveBeenCalled();
});
view = new OpenAssessment.BaseView(runtime, el, server);
});
it("Sends a peer assessment to the server", function() {
loadSubviews(function() {
spyOn(server, 'peerAssess').andCallThrough();
ui.peerAssess();
view.peerAssess();
expect(server.peerAssess).toHaveBeenCalled();
});
});
......@@ -107,7 +93,7 @@ describe("OpenAssessment.BaseUI", function() {
it("Sends a self assessment to the server", function() {
loadSubviews(function() {
spyOn(server, 'selfAssess').andCallThrough();
ui.selfAssess();
view.selfAssess();
expect(server.selfAssess).toHaveBeenCalled();
});
});
......@@ -126,10 +112,10 @@ describe("OpenAssessment.BaseUI", function() {
// Create the object under test
var el = $("#openassessment-base").get(0);
ui = new OpenAssessment.BaseUI(runtime, el, server);
view = new OpenAssessment.BaseView(runtime, el, server);
// Submit feedback on an assessment
ui.submitFeedbackOnAssessment();
view.submitFeedbackOnAssessment();
// Expect that the feedback was retrieved from the DOM and sent to the server
expect(server.feedbackText).toEqual('I disliked the feedback I received.');
......
......@@ -2,7 +2,7 @@
Tests for OA XBlock editing.
**/
describe("OpenAssessment.StudioUI", function() {
describe("OpenAssessment.StudioView", function() {
var runtime = {
notify: function(type, data) {}
......@@ -52,7 +52,7 @@ describe("OpenAssessment.StudioUI", function() {
};
var server = null;
var ui = null;
var view = null;
beforeEach(function() {
......@@ -68,24 +68,24 @@ describe("OpenAssessment.StudioUI", function() {
// Create the object under test
var el = $('#openassessment-edit').get(0);
ui = new OpenAssessment.StudioUI(runtime, el, server);
view = new OpenAssessment.StudioView(runtime, el, server);
});
it("loads the XML definition", function() {
// Initialize the UI
ui.load();
// Initialize the view
view.load();
// Expect that the XML definition was loaded
var contents = ui.codeBox.getValue();
var contents = view.codeBox.getValue();
expect(contents).toEqual('<openassessment></openassessment>');
});
it("saves the XML definition", function() {
// Update the XML
ui.codeBox.setValue('<openassessment>test!</openassessment>');
view.codeBox.setValue('<openassessment>test!</openassessment>');
// Save the updated XML
ui.save();
view.save();
// Expect the saving notification to start/end
expect(runtime.notify).toHaveBeenCalledWith('save', {state: 'start'});
......@@ -100,31 +100,31 @@ describe("OpenAssessment.StudioUI", function() {
server.isReleased = true;
// Stub the confirmation step (avoid showing the dialog)
spyOn(ui, 'confirmPostReleaseUpdate').andCallFake(
spyOn(view, 'confirmPostReleaseUpdate').andCallFake(
function(onConfirm) { onConfirm(); }
);
// Save the updated XML
ui.save();
view.save();
// Verify that the user was asked to confirm the changes
expect(ui.confirmPostReleaseUpdate).toHaveBeenCalled();
expect(view.confirmPostReleaseUpdate).toHaveBeenCalled();
});
it("cancels editing", function() {
ui.cancel();
view.cancel();
expect(runtime.notify).toHaveBeenCalledWith('cancel', {});
});
it("displays an error when server reports a load XML error", function() {
server.loadError = true;
ui.load();
view.load();
expect(runtime.notify).toHaveBeenCalledWith('error', {msg: 'Test error'});
});
it("displays an error when server reports an update XML error", function() {
server.updateError = true;
ui.save('<openassessment>test!</openassessment>');
view.save('<openassessment>test!</openassessment>');
expect(runtime.notify).toHaveBeenCalledWith('error', {msg: 'Test error'});
});
......
/**
Tests for OpenAssessment response (submission) step.
**/
describe("OpenAssessment.ResponseView", function() {
// Stub server
var StubServer = function() {
var successPromise = $.Deferred(
function(defer) {
defer.resolve();
}
).promise();
this.save = function(submission) {
return successPromise;
};
this.submit = function(submission) {
return successPromise;
};
this.render = function(step) {
return successPromise;
};
};
// Stub base view
var StubBaseView = function() {
this.showLoadError = function(msg) {};
this.toggleActionError = function(msg, step) {};
this.setUpCollapseExpand = function(sel) {};
this.renderPeerAssessmentStep = function() {};
};
// Stubs
var baseView = null;
var server = null;
// View under test
var view = null;
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_response.html');
// Create the stub server
server = new StubServer();
// Create the stub base view
baseView = new StubBaseView();
// Create and install the view
var el = $('#openassessment-base').get(0);
view = new OpenAssessment.ResponseView(el, server, baseView);
view.installHandlers();
});
it("updates submit/save buttons and save status when response text changes", function() {
// Response is blank --> save/submit buttons disabled
view.response('');
view.responseChanged();
expect(view.submitEnabled()).toBe(false);
expect(view.saveEnabled()).toBe(false);
expect(view.saveStatus()).toContain('Unsaved draft');
// Response is not blank --> submit button enabled
view.response('Test response');
view.responseChanged();
expect(view.submitEnabled()).toBe(true);
expect(view.saveEnabled()).toBe(true);
expect(view.saveStatus()).toContain('Unsaved draft');
});
it("updates submit/save buttons and save status when the user saves a response", function() {
// Response is blank --> save/submit button is disabled
view.response('');
view.save();
expect(view.submitEnabled()).toBe(false);
expect(view.saveEnabled()).toBe(false);
expect(view.saveStatus()).toContain('Saved but not submitted');
// Response is not blank --> submit button enabled
view.response('Test response');
view.save();
expect(view.submitEnabled()).toBe(true);
expect(view.saveEnabled()).toBe(false);
expect(view.saveStatus()).toContain('Saved but not submitted');
});
it("shows unsaved draft only when response text has changed", function() {
// Save the initial response
view.response('Lorem ipsum');
view.save();
expect(view.saveEnabled()).toBe(false);
expect(view.saveStatus()).toContain('Saved but not submitted');
// Keep the text the same, but trigger an update
// Should still be saved
view.response('Lorem ipsum');
view.responseChanged();
expect(view.saveEnabled()).toBe(false);
expect(view.saveStatus()).toContain('Saved but not submitted');
// Change the text
// This should cause it to change to unsaved draft
view.response('changed ');
view.responseChanged();
expect(view.saveEnabled()).toBe(true);
expect(view.saveStatus()).toContain('Unsaved draft');
});
it("sends the saved submission to the server", function() {
spyOn(server, 'save').andCallThrough();
view.response('Test response');
view.save();
expect(server.save).toHaveBeenCalledWith('Test response');
});
it("submits a response to the server", function() {
spyOn(server, 'submit').andCallThrough();
view.response('Test response');
view.submit();
expect(server.submit).toHaveBeenCalledWith('Test response');
});
});
......@@ -8,7 +8,7 @@ if (typeof OpenAssessment == "undefined" || !OpenAssessment) {
/**
Interface for editing UI in Studio.
Interface for editing view in Studio.
The constructor initializes the DOM for editing.
Args:
......@@ -17,9 +17,9 @@ Args:
server (OpenAssessment.Server): The interface to the XBlock server.
Returns:
OpenAssessment.StudioUI
OpenAssessment.StudioView
**/
OpenAssessment.StudioUI = function(runtime, element, server) {
OpenAssessment.StudioView = function(runtime, element, server) {
this.runtime = runtime;
this.server = server;
......@@ -30,31 +30,31 @@ OpenAssessment.StudioUI = function(runtime, element, server) {
);
// Install click handlers
var ui = this;
var view = this;
$(element).find('.openassessment-save-button').click(
function(eventData) {
ui.save();
view.save();
});
$(element).find('.openassessment-cancel-button').click(
function(eventData) {
ui.cancel();
view.cancel();
});
};
OpenAssessment.StudioUI.prototype = {
OpenAssessment.StudioView.prototype = {
/**
Load the XBlock XML definition from the server and display it in the UI.
Load the XBlock XML definition from the server and display it in the view.
**/
load: function() {
var ui = this;
var view = this;
this.server.loadXml().done(
function(xml) {
ui.codeBox.setValue(xml);
view.codeBox.setValue(xml);
}).fail(function(msg) {
ui.showError(msg);
view.showError(msg);
}
);
},
......@@ -64,17 +64,17 @@ OpenAssessment.StudioUI.prototype = {
If the problem has been released, make the user confirm the save.
**/
save: function() {
var ui = this;
var view = this;
// Check whether the problem has been released; if not,
// warn the user and allow them to cancel.
this.server.checkReleased().done(
function(isReleased) {
if (isReleased) { ui.confirmPostReleaseUpdate($.proxy(ui.updateXml, ui)); }
else { ui.updateXml(); }
if (isReleased) { view.confirmPostReleaseUpdate($.proxy(view.updateXml, view)); }
else { view.updateXml(); }
}
).fail(function(errMsg) {
ui.showError(msg);
view.showError(msg);
});
},
......@@ -102,16 +102,16 @@ OpenAssessment.StudioUI.prototype = {
// Send the updated XML to the server
var xml = this.codeBox.getValue();
var ui = this;
var view = this;
this.server.updateXml(xml).done(function() {
// Notify the client-side runtime that we finished saving
// so it can hide the "Saving..." notification.
ui.runtime.notify('save', {state: 'end'});
view.runtime.notify('save', {state: 'end'});
// Reload the XML definition in the editor
ui.load();
view.load();
}).fail(function(msg) {
ui.showError(msg);
view.showError(msg);
});
},
......@@ -143,7 +143,7 @@ function OpenAssessmentEditor(runtime, element) {
**/
$(function($) {
var server = new OpenAssessment.Server(runtime, element);
var ui = new OpenAssessment.StudioUI(runtime, element, server);
ui.load();
var view = new OpenAssessment.StudioView(runtime, element, server);
view.load();
});
}
/* JavaScript for response (submission) view */
/* Namespace for open assessment */
if (typeof OpenAssessment == "undefined" || !OpenAssessment) {
OpenAssessment = {};
}
/**
Interface for response (submission) view.
Args:
element (DOM element): The DOM element representing the XBlock.
server (OpenAssessment.Server): The interface to the XBlock server.
baseView (OpenAssessment.BaseView): Container view.
Returns:
OpenAssessment.ResponseView
**/
OpenAssessment.ResponseView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
this.savedResponse = "";
};
OpenAssessment.ResponseView.prototype = {
/**
Load the response (submission) view.
**/
load: function() {
var view = this;
this.server.render('submission').done(
function(html) {
// Load the HTML and install event handlers
$('#openassessment__response', view.element).replaceWith(html);
view.installHandlers();
}
).fail(function(errMsg) {
view.baseView.showLoadError('response');
});
},
/**
Install event handlers for the view.
**/
installHandlers: function() {
var sel = $('#openassessment__response', this.element);
var view = this;
// Install a click handler for collapse/expand
this.baseView.setUpCollapseExpand(sel);
// Install change handler for textarea (to enable submission button)
this.savedResponse = this.response();
var handleChange = function(eventData) { view.responseChanged(); };
sel.find('#submission__answer__value').on('change keyup drop paste', handleChange);
// Install a click handler for submission
sel.find('#step--response__submit').click(
function(eventObject) {
// Override default form submission
eventObject.preventDefault();
view.submit();
}
);
// Install a click handler for the save button
sel.find('#submission__save').click(
function(eventObject) {
// Override default form submission
eventObject.preventDefault();
view.save();
}
);
},
/**
Enable/disable the submit button.
Check that whether the submit button is enabled.
Args:
enabled (bool): If specified, set the state of the button.
Returns:
bool: Whether the button is enabled.
Examples:
>> view.submitEnabled(true); // enable the button
>> view.submitEnabled(); // check whether the button is enabled
>> true
**/
submitEnabled: function(enabled) {
var sel = $('#step--response__submit', this.element);
if (typeof enabled === 'undefined') {
return !sel.hasClass('is--disabled');
} else {
sel.toggleClass('is--disabled', !enabled)
}
},
/**
Enable/disable the save button.
Check that whether the save button is enabled.
Args:
enabled (bool): If specified, set the state of the button.
Returns:
bool: Whether the button is enabled.
Examples:
>> view.submitEnabled(true); // enable the button
>> view.submitEnabled(); // check whether the button is enabled
>> true
**/
saveEnabled: function(enabled) {
var sel = $('#submission__save', this.element);
if (typeof enabled === 'undefined') {
return !sel.hasClass('is--disabled');
} else {
sel.toggleClass('is--disabled', !enabled);
}
},
/**
Set the save status message.
Retrieve the save status message.
Args:
msg (string): If specified, the message to display.
Returns:
string: The current status message.
**/
saveStatus: function(msg) {
var sel = $('#response__save_status h3', this.element);
if (typeof msg === 'undefined') {
return sel.text();
} else {
// Setting the HTML will overwrite the screen reader tag,
// so prepend it to the message.
sel.html('<span class="sr">Your Working Submission Status:</span>\n' + msg);
}
},
/**
Set the response text.
Retrieve the response text.
Args:
text (string): If specified, the text to set for the response.
Returns:
string: The current response text.
**/
response: function(text) {
var sel = $('#submission__answer__value', this.element);
if (typeof text === 'undefined') {
return sel.val();
} else {
sel.val(text);
}
},
/**
Enable/disable the submission and save buttons based on whether
the user has entered a response.
**/
responseChanged: function() {
// Enable the save/submit button only for non-blank responses
var currentResponse = this.response();
var isBlank = (currentResponse !== '');
this.submitEnabled(isBlank);
// Update the save button and status only if the response has changed
if (this.savedResponse !== currentResponse) {
this.saveEnabled(isBlank);
this.saveStatus('Unsaved draft');
}
},
/**
Save a response without submitting it.
**/
save: function() {
// Update the save status and error notifications
this.saveStatus('Saving...');
this.baseView.toggleActionError('save', null);
var view = this;
var savedResponse = this.response();
this.server.save(savedResponse).done(function() {
// Remember which response we saved, once the server confirms that it's been saved...
view.savedResponse = savedResponse;
// ... but update the UI based on what the user may have entered
// since hitting the save button.
var currentResponse = view.response();
view.submitEnabled(currentResponse !== '');
if (currentResponse == savedResponse) {
view.saveEnabled(false);
view.saveStatus("Saved but not submitted");
}
}).fail(function(errMsg) {
view.saveStatus('Error');
view.baseView.toggleActionError('save', errMsg);
});
},
/**
Send a response submission to the server and update the view.
**/
submit: function() {
// Send the submission to the server
var submission = $('#submission__answer__value', this.element).val();
this.baseView.toggleActionError('response', null);
var view = this;
var baseView = this.baseView;
this.server.submit(submission).done(
// When we have successfully sent the submission, move on to the next step
function(studentId, attemptNum) {
view.load();
baseView.renderPeerAssessmentStep();
}
).fail(function(errCode, errMsg) {
baseView.toggleActionError('submit', errMsg);
});
}
};
......@@ -155,7 +155,7 @@ class SubmissionMixin(object):
Returns:
unicode
"""
return _(u'Saved but not submitted') if self.has_saved else _(u'Not saved')
return _(u'Saved but not submitted') if self.has_saved else _(u'Unsaved draft')
@XBlock.handler
def render_submission(self, data, suffix=''):
......@@ -185,6 +185,7 @@ class SubmissionMixin(object):
context = {
"saved_response": self.saved_response,
"save_status": self.save_status,
"submit_enabled": self.saved_response != '',
"submission_due": sub_due,
}
......
......@@ -14,7 +14,7 @@ class SaveResponseTest(XBlockHandlerTestCase):
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)
self.assertIn('Not saved', resp)
self.assertIn('Unsaved draft', resp)
@ddt.file_data('data/save_responses.json')
@scenario('data/save_scenario.xml', user_id="Perleman")
......@@ -57,4 +57,4 @@ class SaveResponseTest(XBlockHandlerTestCase):
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
self.assertIn('submission', resp['msg'])
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