Commit 36414f2c by Will Daly

Refactor JavaScript so self- and peer-assessment use OpenAssessment.Rubric

parent d24adc15
......@@ -47,7 +47,76 @@
{
"template": "openassessmentblock/self/oa_self_assessment.html",
"context": {
"rubric_criteria": [],
"rubric_criteria": [
{
"name": "Criterion 1",
"prompt": "Prompt 1",
"order_num": 0,
"feedback": "optional",
"options": [
{
"order_num": 0,
"points": 0,
"name": "Poor"
},
{
"order_num": 1,
"points": 1,
"name": "Fair"
},
{
"order_num": 2,
"points": 2,
"name": "Good"
}
]
},
{
"name": "Criterion 2",
"prompt": "Prompt 2",
"order_num": 1,
"options": [
{
"order_num": 0,
"points": 0,
"name": "Poor"
},
{
"order_num": 1,
"points": 1,
"name": "Fair"
},
{
"order_num": 2,
"points": 2,
"name": "Good"
}
]
},
{
"name": "Criterion 3",
"prompt": "Prompt 3",
"order_num": 2,
"feedback": "optional",
"options": [
{
"order_num": 0,
"points": 0,
"name": "Poor"
},
{
"order_num": 1,
"points": 1,
"name": "Fair"
},
{
"order_num": 2,
"points": 2,
"name": "Good"
}
]
}
],
"self_submission": {}
},
"output": "oa_self_assessment.html"
......@@ -198,4 +267,4 @@
"context": {},
"output": "oa_edit.html"
}
]
\ No newline at end of file
]
......@@ -15,12 +15,12 @@ describe("OpenAssessment.BaseView", function() {
grade: readFixtures("oa_grade_complete.html")
};
this.selfAssess = function(optionsSelected) {
return $.Deferred(function(defer) { defer.resolve(); }).promise();
};
// Remember which fragments were requested
this.fragmentsLoaded = [];
this.render = function(component) {
var server = this;
this.fragmentsLoaded.push(component);
return $.Deferred(function(defer) {
defer.resolveWith(this, [server.fragments[component]]);
}).promise();
......@@ -66,24 +66,12 @@ describe("OpenAssessment.BaseView", function() {
view = new OpenAssessment.BaseView(runtime, el, server);
});
it("Sends a self assessment to the server", function() {
loadSubviews(function() {
spyOn(server, 'selfAssess').andCallThrough();
view.selfAssess();
expect(server.selfAssess).toHaveBeenCalled();
});
});
it("Displays error messages from self assessment to the user", function() {
var testError = 'Test failure contacting server message';
it("Loads each step", function() {
loadSubviews(function() {
/* stub our selfAssess to fail */
spyOn(server, 'selfAssess').andCallFake(function(optionsSelected) {
return $.Deferred(function(defer) { defer.rejectWith(server, [testError]); }).promise();
});
view.selfAssess();
expect(server.selfAssess).toHaveBeenCalled();
expect(view.getStepActionsErrorMessage()).toContain(testError);
expect(server.fragmentsLoaded).toContain("submission");
expect(server.fragmentsLoaded).toContain("self_assessment");
expect(server.fragmentsLoaded).toContain("peer_assessment");
expect(server.fragmentsLoaded).toContain("grade");
});
});
......
......@@ -62,13 +62,13 @@ describe("OpenAssessment.PeerView", function() {
optionsSelected['Criterion 1'] = 'Poor';
optionsSelected['Criterion 2'] = 'Fair';
optionsSelected['Criterion 3'] = 'Good';
view.optionsSelected(optionsSelected);
view.rubric.optionsSelected(optionsSelected);
// Provide per-criterion feedback
var criterionFeedback = {};
criterionFeedback['Criterion 1'] = "You did a fair job";
criterionFeedback['Criterion 3'] = "You did a good job";
view.criterionFeedback(criterionFeedback);
view.rubric.criterionFeedback(criterionFeedback);
// Provide overall feedback
var overallFeedback = "Good job!";
......
......@@ -26,12 +26,12 @@ describe("OpenAssessment.SelfView", function() {
this.showLoadError = function(msg) {};
this.toggleActionError = function(msg, step) {};
this.setUpCollapseExpand = function(sel) {};
this.loadAssessmentModules = function() {};
this.scrollToTop = function() {};
};
// Stub runtime
var runtime = {};
// Stubs
var baseView = null;
var server = null;
// View under test
......@@ -45,12 +45,22 @@ describe("OpenAssessment.SelfView", function() {
// Create a new stub server
server = new StubServer();
// Create the stub base view
baseView = new StubBaseView();
// Create the object under test
var el = $("#openassessment").get(0);
view = new OpenAssessment.BaseView(runtime, el, server);
view = new OpenAssessment.SelfView(el, server, baseView);
view.installHandlers();
});
it("Sends a self assessment to the server", function() {
spyOn(server, 'selfAssess').andCallThrough();
view.selfAssess();
expect(server.selfAssess).toHaveBeenCalled();
});
it("re-enables the self assess button on error", function() {
it("Re-enables the self assess button on error", function() {
// Simulate a server error
spyOn(server, 'selfAssess').andCallFake(function() {
expect(view.selfSubmitEnabled()).toBe(false);
......@@ -59,6 +69,7 @@ describe("OpenAssessment.SelfView", function() {
}).promise();
});
view.selfAssess();
// Expect the submit button to have been re-enabled
expect(view.selfSubmitEnabled()).toBe(true);
});
......
......@@ -15,8 +15,10 @@ OpenAssessment.BaseView = function(runtime, element, server) {
this.server = server;
this.responseView = new OpenAssessment.ResponseView(this.element, this.server, this);
this.selfView = new OpenAssessment.SelfView(this.element, this.server, this);
this.peerView = new OpenAssessment.PeerView(this.element, this.server, this);
this.gradeView = new OpenAssessment.GradeView(this.element, this.server, this);
// Staff only information about student progress.
this.staffInfoView = new OpenAssessment.StaffInfoView(this.element, this.server, this);
};
......@@ -74,106 +76,11 @@ OpenAssessment.BaseView.prototype = {
**/
loadAssessmentModules: function() {
this.peerView.load();
this.renderSelfAssessmentStep();
this.selfView.load();
this.gradeView.load();
},
/**
Render the self-assessment step.
**/
renderSelfAssessmentStep: function() {
var view = this;
this.server.render('self_assessment').done(
function(html) {
// Load the HTML
$('#openassessment__self-assessment', view.element).replaceWith(html);
var sel = $('#openassessment__self-assessment', view.element);
// Install a click handler for collapse/expand
view.setUpCollapseExpand(sel);
// Install a change handler for rubric options to enable/disable the submit button
$("#self-assessment--001__assessment", view.element).change(
function() {
var numChecked = $('input[type=radio]:checked', this).length;
var numAvailable = $('.field--radio.assessment__rubric__question', this).length;
$("#self-assessment--001__assessment__submit", view.element).toggleClass(
'is--disabled', numChecked != numAvailable
);
}
);
// Install a click handler for the submit button
sel.find('#self-assessment--001__assessment__submit').click(
function(eventObject) {
// Override default form submission
eventObject.preventDefault();
// Handle the click
view.selfAssess();
}
);
}
).fail(function(errMsg) {
view.showLoadError('self-assessment');
});
},
/**
Enable/disable the self assess button.
Check that whether the self assess button is enabled.
Args:
enabled (bool): If specified, set the state of the button.
Returns:
bool: Whether the button is enabled.
Examples:
>> view.selfSubmitEnabled(true); // enable the button
>> view.selfSubmitEnabled(); // check whether the button is enabled
>> true
**/
selfSubmitEnabled: function(enabled) {
var button = $('#self-assessment--001__assessment__submit', this.element);
if (typeof enabled === 'undefined') {
return !button.hasClass('is--disabled');
} else {
button.toggleClass('is--disabled', !enabled);
}
},
/**
Send a self-assessment to the server and update the view.
**/
selfAssess: function() {
// Retrieve self-assessment info from the DOM
var optionsSelected = {};
$("#self-assessment--001__assessment input[type=radio]:checked", this.element).each(
function(index, sel) {
optionsSelected[sel.name] = sel.value;
}
);
// Send the assessment to the server
var view = this;
view.toggleActionError('self', null);
view.selfSubmitEnabled(false);
this.server.selfAssess(optionsSelected).done(
function() {
view.loadAssessmentModules();
view.scrollToTop();
}
).fail(function(errMsg) {
view.toggleActionError('self', errMsg);
view.selfSubmitEnabled(true);
});
},
/**
Report an error to the user.
Args:
......@@ -220,20 +127,6 @@ OpenAssessment.BaseView.prototype = {
$(container).toggleClass('has--error', true);
$(container + ' .step__status__value i').removeClass().addClass('ico icon-warning-sign');
$(container + ' .step__status__value .copy').html(gettext('Unable to Load'));
},
/**
* Get the contents of the Step Actions error message box, for unit test validation.
*
* Step Actions are the UX-level parts of the student interaction flow -
* Submission, Peer Assessment, and Self Assessment. Since steps are mutually
* exclusive, only one error box should be rendered on screen at a time.
*
* Returns:
* One HTML string
*/
getStepActionsErrorMessage: function() {
return $('.step__actions .message__content').html();
}
};
......
......@@ -13,6 +13,7 @@ OpenAssessment.PeerView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
this.rubric = null;
};
......@@ -27,7 +28,7 @@ OpenAssessment.PeerView.prototype = {
function(html) {
// Load the HTML and install event handlers
$('#openassessment__peer-assessment', view.element).replaceWith(html);
view.installHandlers();
view.installHandlers(false);
}
).fail(function(errMsg) {
view.showLoadError('peer-assessment');
......@@ -46,7 +47,7 @@ OpenAssessment.PeerView.prototype = {
function(html) {
// Load the HTML and install event handlers
$('#openassessment__peer-assessment', view.element).replaceWith(html);
view.installHandlersForContinuedAssessment();
view.installHandlers(true);
}
).fail(function(errMsg) {
view.showLoadError('peer-assessment');
......@@ -55,22 +56,30 @@ OpenAssessment.PeerView.prototype = {
/**
Install event handlers for the view.
Args:
isContinuedAssessment (boolean): If true, we are in "continued grading" mode,
meaning that the user is continuing to grade even though she has met
the requirements.
**/
installHandlers: function() {
installHandlers: function(isContinuedAssessment) {
var sel = $('#openassessment__peer-assessment', this.element);
var view = this;
// Install a click handler for collapse/expand
this.baseView.setUpCollapseExpand(sel, $.proxy(view.loadContinuedAssessment, view));
// Initialize the rubric
var rubricSelector = $("#peer-assessment--001__assessment", this.element);
if (rubricSelector.size() > 0) {
var rubricElement = rubricSelector.get(0);
this.rubric = new OpenAssessment.Rubric(rubricElement);
}
// Install a change handler for rubric options to enable/disable the submit button
sel.find("#peer-assessment--001__assessment").change(
function() {
var numChecked = $('input[type=radio]:checked', this).length;
var numAvailable = $('.field--radio.assessment__rubric__question', this).length;
view.peerSubmitEnabled(numChecked == numAvailable);
}
);
if (this.rubric !== null) {
this.rubric.canSubmitCallback($.proxy(view.peerSubmitEnabled, view));
}
// Install a click handler for assessment
sel.find('#peer-assessment--001__assessment__submit').click(
......@@ -79,38 +88,8 @@ OpenAssessment.PeerView.prototype = {
eventObject.preventDefault();
// Handle the click
view.peerAssess();
}
);
},
/**
Install event handlers for the continued grading version of the view.
**/
installHandlersForContinuedAssessment: function() {
var sel = $('#openassessment__peer-assessment', this.element);
var view = this;
// Install a click handler for collapse/expand
this.baseView.setUpCollapseExpand(sel);
// Install a click handler for assessment
sel.find('#peer-assessment--001__assessment__submit').click(
function(eventObject) {
// Override default form submission
eventObject.preventDefault();
// Handle the click
view.continuedPeerAssess();
}
);
// Install a change handler for rubric options to enable/disable the submit button
sel.find("#peer-assessment--001__assessment").change(
function() {
var numChecked = $('input[type=radio]:checked', this).length;
var numAvailable = $('.field--radio.assessment__rubric__question', this).length;
view.peerSubmitEnabled(numChecked == numAvailable);
if (!isContinuedAssessment) { view.peerAssess(); }
else { view.continuedPeerAssess(); }
}
);
},
......@@ -168,106 +147,6 @@ OpenAssessment.PeerView.prototype = {
},
/**
Get or set overall feedback on the submission.
Args:
overallFeedback (string or undefined): The overall feedback text (optional).
Returns:
string or undefined
Example usage:
>>> view.overallFeedback('Good job!'); // Set the feedback text
>>> view.overallFeedback(); // Retrieve the feedback text
'Good job!'
**/
overallFeedback: function(overallFeedback) {
var selector = '#assessment__rubric__question--feedback__value';
if (typeof overallFeedback === 'undefined') {
return $(selector, this.element).val();
}
else {
$(selector, this.element).val(overallFeedback);
}
},
/**
Get or set per-criterion feedback.
Args:
criterionFeedback (object literal or undefined):
Map of criterion names to feedback strings.
Returns:
object literal or undefined
Example usage:
>>> view.criterionFeedback({'ideas': 'Good ideas'}); // Set per-criterion feedback
>>> view.criterionFeedback(); // Retrieve criterion feedback
{'ideas': 'Good ideas'}
**/
criterionFeedback: function(criterionFeedback) {
var selector = '#peer-assessment--001__assessment textarea.answer__value';
var feedback = {};
$(selector, this.element).each(
function(index, sel) {
if (typeof criterionFeedback !== 'undefined') {
$(sel).val(criterionFeedback[sel.name]);
feedback[sel.name] = criterionFeedback[sel.name];
}
else {
feedback[sel.name] = $(sel).val();
}
}
);
return feedback;
},
/**
Get or set the options selected in the rubric.
Args:
optionsSelected (object literal or undefined):
Map of criterion names to option values.
Returns:
object literal or undefined
Example usage:
>>> view.optionsSelected({'ideas': 'Good'}); // Set the criterion option
>>> view.optionsSelected(); // Retrieve the options selected
{'ideas': 'Good'}
**/
optionsSelected: function(optionsSelected) {
var selector = "#peer-assessment--001__assessment input[type=radio]";
if (typeof optionsSelected === 'undefined') {
var options = {};
$(selector + ":checked", this.element).each(
function(index, sel) {
options[sel.name] = sel.value;
}
);
return options;
}
else {
// Uncheck all the options
$(selector, this.element).prop('checked', false);
// Check the selected options
$(selector, this.element).each(function(index, sel) {
if (optionsSelected.hasOwnProperty(sel.name)) {
if (sel.value == optionsSelected[sel.name]) {
$(sel).prop('checked', true);
}
}
});
}
},
/**
Common peer assessment request building, used for all types of peer assessments.
Args:
......@@ -283,8 +162,8 @@ OpenAssessment.PeerView.prototype = {
// Pull the assessment info from the DOM and send it to the server
this.server.peerAssess(
this.optionsSelected(),
this.criterionFeedback(),
this.rubric.optionsSelected(),
this.rubric.criterionFeedback(),
this.overallFeedback()
).done(
successFunction
......@@ -293,4 +172,29 @@ OpenAssessment.PeerView.prototype = {
view.peerSubmitEnabled(true);
});
},
/**
Get or set overall feedback on the submission.
Args:
overallFeedback (string or undefined): The overall feedback text (optional).
Returns:
string or undefined
Example usage:
>>> view.overallFeedback('Good job!'); // Set the feedback text
>>> view.overallFeedback(); // Retrieve the feedback text
'Good job!'
**/
overallFeedback: function(overallFeedback) {
var selector = '#assessment__rubric__question--feedback__value';
if (typeof overallFeedback === 'undefined') {
return $(selector, this.element).val();
}
else {
$(selector, this.element).val(overallFeedback);
}
}
};
/**
Interface for reading and modifying a rubric.
Args:
element (DOM element): The DOM element representing the rubric.
Returns:
OpenAssessment.Rubric
**/
OpenAssessment.Rubric = function(element) {
this.element = element;
};
OpenAssessment.Rubric.prototype = {
/**
Get or set per-criterion feedback.
Args:
criterionFeedback (object literal or undefined):
Map of criterion names to feedback strings.
Returns:
object literal or undefined
Example usage:
>>> view.criterionFeedback({'ideas': 'Good ideas'}); // Set per-criterion feedback
>>> view.criterionFeedback(); // Retrieve criterion feedback
{'ideas': 'Good ideas'}
**/
criterionFeedback: function(criterionFeedback) {
var selector = 'textarea.answer__value';
var feedback = {};
$(selector, this.element).each(
function(index, sel) {
if (typeof criterionFeedback !== 'undefined') {
$(sel).val(criterionFeedback[sel.name]);
feedback[sel.name] = criterionFeedback[sel.name];
}
else {
feedback[sel.name] = $(sel).val();
}
}
);
return feedback;
},
/**
Get or set the options selected in the rubric.
Args:
optionsSelected (object literal or undefined):
Map of criterion names to option values.
Returns:
object literal or undefined
Example usage:
>>> view.optionsSelected({'ideas': 'Good'}); // Set the criterion option
>>> view.optionsSelected(); // Retrieve the options selected
{'ideas': 'Good'}
**/
optionsSelected: function(optionsSelected) {
var selector = "input[type=radio]";
if (typeof optionsSelected === 'undefined') {
var options = {};
$(selector + ":checked", this.element).each(
function(index, sel) {
options[sel.name] = sel.value;
}
);
return options;
}
else {
// Uncheck all the options
$(selector, this.element).prop('checked', false);
// Check the selected options
$(selector, this.element).each(function(index, sel) {
if (optionsSelected.hasOwnProperty(sel.name)) {
if (sel.value == optionsSelected[sel.name]) {
$(sel).prop('checked', true);
}
}
});
}
},
/**
Install a callback handler to be notified when
the the user has selected options for all criteria and can submit the assessment.
Args:
callback (function): Callback function that accepts one argument, a boolean indicating
whether the user is allowed to submit the rubric.
**/
canSubmitCallback: function(callback) {
$(this.element).change(
function() {
var numChecked = $('input[type=radio]:checked', this).length;
var numAvailable = $('.field--radio.assessment__rubric__question', this).length;
var canSubmit = numChecked == numAvailable;
callback(canSubmit);
}
);
}
};
/**
Interface for self assessment 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.SelfView
**/
OpenAssessment.SelfView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
this.rubric = null;
};
OpenAssessment.SelfView.prototype = {
/**
Load the self assessment view.
**/
load: function() {
var view = this;
this.server.render('self_assessment').done(
function(html) {
// Load the HTML and install event handlers
$('#openassessment__self-assessment', view.element).replaceWith(html);
view.installHandlers();
}
).fail(function(errMsg) {
view.showLoadError('self-assessment');
});
},
/**
Install event handlers for the view.
**/
installHandlers: function() {
var view = this;
var sel = $('#openassessment__self-assessment', view.element);
// Install a click handler for collapse/expand
this.baseView.setUpCollapseExpand(sel);
// Initialize the rubric
var rubricSelector = $("#self-assessment--001__assessment", this.element);
if (rubricSelector.size() > 0) {
var rubricElement = rubricSelector.get(0);
this.rubric = new OpenAssessment.Rubric(rubricElement);
}
// Install a change handler for rubric options to enable/disable the submit button
if (this.rubric !== null) {
this.rubric.canSubmitCallback($.proxy(this.selfSubmitEnabled, this));
}
// Install a click handler for the submit button
sel.find('#self-assessment--001__assessment__submit').click(
function(eventObject) {
// Override default form submission
eventObject.preventDefault();
// Handle the click
view.selfAssess();
}
);
},
/**
Enable/disable the self assess button.
Check that whether the self assess button is enabled.
Args:
enabled (bool): If specified, set the state of the button.
Returns:
bool: Whether the button is enabled.
Examples:
>> view.selfSubmitEnabled(true); // enable the button
>> view.selfSubmitEnabled(); // check whether the button is enabled
>> true
**/
selfSubmitEnabled: function(enabled) {
var button = $('#self-assessment--001__assessment__submit', this.element);
if (typeof enabled === 'undefined') {
return !button.hasClass('is--disabled');
} else {
button.toggleClass('is--disabled', !enabled);
}
},
/**
Send a self-assessment to the server and update the view.
**/
selfAssess: function() {
// Send the assessment to the server
var view = this;
var baseView = this.baseView;
baseView.toggleActionError('self', null);
view.selfSubmitEnabled(false);
var options = this.rubric.optionsSelected();
this.server.selfAssess(options).done(
function() {
baseView.loadAssessmentModules();
baseView.scrollToTop();
}
).fail(function(errMsg) {
baseView.toggleActionError('self', errMsg);
view.selfSubmitEnabled(true);
});
}
};
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