Commit 2931e698 by Will Daly Committed by Stephen Sanchez

Add student training JavaScript

parent 9e3192de
{% load i18n %}
{% load tz %}
{% spaceless %}
{% block list_item %}
<li id="openassessment__student-training" class="openassessment__steps__step step--student-training ui-toggle-visibility">
{% endblock %}
{% block list_item %}
<li id="openassessment__student-training" class="openassessment__steps__step step--student-training ui-toggle-visibility">
{% endblock %}
<header class="step__header ui-toggle-visibility__control">
<h2 class="step__title">
<span class="step__counter"></span>
<span class="wrapper--copy">
<span class="step__label">{% trans "Learn to Assess" %}</span>
{% if self_start %}
{% if training_start %}
<span class="step__deadline">{% trans "available" %}
<span class="date">
{{ self_start|utc|date:"N j, Y H:i e" }}
(in {{ self_start|timeuntil }})
{{ training_start|utc|date:"N j, Y H:i e" }}
(in {{ training_start|timeuntil }})
</span>
</span>
{% elif training_due %}
......@@ -44,11 +44,11 @@
<div class="step__content">
<article class="student-training__display" id="student-training">
<header class="student-training__display__header">
<h3 class="student-training__display__title">{% trans "Your Response" %}</h3>
<h3 class="student-training__display__title">{% trans "Training Essay" %}</h3>
</header>
<div class="student-training__display__response">
{{ training_essay.text|linebreaks }}
{{ training_essay|linebreaks }}
</div>
</article>
......@@ -62,7 +62,20 @@
<span class="question__title__copy">{{ criterion.prompt }}</span>
<span class="label--required sr">* ({% trans "Required" %})</span>
</h4>
<div class="step__message message message--correct ui-toggle-visibility is--hidden">
<h3 class="message__title">{% trans "Correct Selection" %}</h3>
<div class="message__content">
<p>{% trans "Your selection matches staff." %}</p>
</div>
</div>
<div class="step__message message message--incorrect ui-toggle-visibility is--hidden">
<h3 class="message__title">{% trans "Incorrect Selection" %}</h3>
<div class="message__content">
<p>{% trans "Your selection does not match staff." %}</p>
</div>
</div>
<div class="ui-toggle-visibility__content">
<ol class="question__answers">
{% for option in criterion.options %}
......
......@@ -45,6 +45,85 @@
"output": "oa_response.html"
},
{
"template": "openassessmentblock/student_training/student_training.html",
"context": {
"training_essay": "My special essay.",
"training_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"
}
]
}
]
}
},
"output": "oa_student_training.html"
},
{
"template": "openassessmentblock/self/oa_self_assessment.html",
"context": {
"rubric_criteria": [
......
......@@ -10,6 +10,7 @@ describe("OpenAssessment.BaseView", function() {
// Dummy fragments to return from the render func
this.fragments = {
submission: readFixtures("oa_response.html"),
student_training: readFixtures("oa_student_training.html"),
self_assessment: readFixtures("oa_self_assessment.html"),
peer_assessment: readFixtures("oa_peer_assessment.html"),
grade: readFixtures("oa_grade_complete.html")
......@@ -69,10 +70,10 @@ describe("OpenAssessment.BaseView", function() {
it("Loads each step", function() {
loadSubviews(function() {
expect(server.fragmentsLoaded).toContain("submission");
expect(server.fragmentsLoaded).toContain("student_training");
expect(server.fragmentsLoaded).toContain("self_assessment");
expect(server.fragmentsLoaded).toContain("peer_assessment");
expect(server.fragmentsLoaded).toContain("grade");
});
});
});
......@@ -13,7 +13,7 @@ describe("OpenAssessment.PeerView", function() {
).promise();
this.peerAssess = function(optionsSelected, feedback) {
return $.Deferred(function(defer) { defer.resolve(); }).promise();
return successPromise;
};
this.render = function(step) {
......
......@@ -85,7 +85,7 @@ describe("OpenAssessment.Server", function() {
});
});
it("sends an assessment to the XBlock", function() {
it("sends a peer-assessment to the XBlock", function() {
stubAjax(true, {success: true, msg: ''});
var success = false;
......@@ -107,6 +107,29 @@ describe("OpenAssessment.Server", function() {
});
});
it("sends a training assessment to the XBlock", function() {
stubAjax(true, {success: true, msg: '', correct: true});
var success = false;
var corrections = null;
var options = {clarity: "Very clear", precision: "Somewhat precise"};
server.trainingAssess(options).done(
function(result) {
success = true;
corrections = result;
}
);
expect(success).toBe(true);
expect(corrections).toBeUndefined();
expect($.ajax).toHaveBeenCalledWith({
url: '/training_assess',
type: "POST",
data: JSON.stringify({
options_selected: options
})
});
});
it("Sends feedback on an assessment to the XBlock", function() {
stubAjax(true, {success: true, msg: ''});
......@@ -297,6 +320,28 @@ describe("OpenAssessment.Server", function() {
expect(receivedMsg).toContain("This assessment could not be submitted");
});
it("informs the caller of a server error when sending a training example assessment", function() {
stubAjax(true, {success: false, msg: "Test error!"});
var receivedMsg = null;
var options = {clarity: "Very clear", precision: "Somewhat precise"};
server.trainingAssess(options).fail(function(msg) {
receivedMsg = msg;
});
expect(receivedMsg).toEqual("Test error!");
});
it("informs the caller of an AJAX error when sending a training example assessment", function() {
stubAjax(false, null);
var receivedMsg = null;
var options = {clarity: "Very clear", precision: "Somewhat precise"};
server.trainingAssess(options).fail(function(msg) {
receivedMsg = msg;
});
expect(receivedMsg).toContain("This assessment could not be submitted");
});
it("informs the caller of an AJAX error when checking whether the XBlock has been released", function() {
stubAjax(false, null);
......
/**
Tests for OpenAssessment Student Training view.
**/
describe("OpenAssessment.StudentTrainingView", function() {
// Stub server
var StubServer = function() {
var successPromise = $.Deferred(
function(defer) { defer.resolve(); }
).promise();
this.render = function(step) {
return successPromise;
};
this.trainingAssess = function() {
return successPromise;
};
};
// Stub base view
var StubBaseView = function() {
this.showLoadError = function(msg) {};
this.toggleActionError = function(msg, step) {};
this.setUpCollapseExpand = function(sel) {};
this.scrollToTop = function() {};
this.loadAssessmentModules = 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_student_training.html');
// Create a new stub server
server = new StubServer();
// Create the stub base view
baseView = new StubBaseView();
// Create the object under test
var el = $("#openassessment-base").get(0);
view = new OpenAssessment.StudentTrainingView(el, server, baseView);
view.installHandlers();
});
it("submits an assessment for a training example", function() {
spyOn(server, 'trainingAssess').andCallFake(function() {
return $.Deferred(function(defer) {
return {
"Criterion 1": "Good",
"Criterion 2": "Poor",
"Criterion 3": "Fair"
};
}).promise();
});
// Select rubric options
var optionsSelected = {};
optionsSelected['Criterion 1'] = 'Poor';
optionsSelected['Criterion 2'] = 'Fair';
optionsSelected['Criterion 3'] = 'Good';
view.rubric.optionsSelected(optionsSelected);
// Submit the assessment
view.assess();
// Expect that the assessment was sent to the server
expect(server.trainingAssess).toHaveBeenCalledWith(optionsSelected);
});
it("disable the assess button when the user submits", function() {
spyOn(server, 'trainingAssess').andCallFake(function() {
return $.Deferred(function(defer) {
return {
"Criterion 1": "Good",
"Criterion 2": "Poor",
"Criterion 3": "Fair"
};
}).promise();
});
// Initially, the button should be disabled
expect(view.assessButtonEnabled()).toBe(false);
// Select options and submit an assessment
var optionsSelected = {};
optionsSelected['Criterion 1'] = 'Poor';
optionsSelected['Criterion 2'] = 'Fair';
optionsSelected['Criterion 3'] = 'Good';
view.rubric.optionsSelected(optionsSelected);
// Enable the button (we do this manually to avoid dealing with async change handlers)
view.assessButtonEnabled(true);
// Submit the assessment
view.assess();
// The button should be disabled after submission
expect(view.assessButtonEnabled()).toBe(false);
});
it("reloads the assessment steps when the user submits an assessment", function() {
spyOn(baseView, 'loadAssessmentModules').andCallThrough();
spyOn(server, 'trainingAssess').andCallFake(function() {
return $.Deferred(function(defer){return {};}).promise();
});
// Select rubric options
var optionsSelected = {};
optionsSelected['Criterion 1'] = 'Poor';
optionsSelected['Criterion 2'] = 'Fair';
optionsSelected['Criterion 3'] = 'Good';
view.rubric.optionsSelected(optionsSelected);
// Submit the assessment
view.assess();
// Expect that the assessment was sent to the server
expect(server.trainingAssess).toHaveBeenCalledWith(optionsSelected);
// Expect that the steps were reloaded
expect(baseView.loadAssessmentModules).toHaveBeenCalled();
});
});
......@@ -15,6 +15,7 @@ OpenAssessment.BaseView = function(runtime, element, server) {
this.server = server;
this.responseView = new OpenAssessment.ResponseView(this.element, this.server, this);
this.trainingView = new OpenAssessment.StudentTrainingView(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);
......@@ -75,6 +76,7 @@ OpenAssessment.BaseView.prototype = {
performed by the user.
**/
loadAssessmentModules: function() {
this.trainingView.load();
this.peerView.load();
this.selfView.load();
this.gradeView.load();
......
......@@ -31,7 +31,7 @@ OpenAssessment.PeerView.prototype = {
view.installHandlers(false);
}
).fail(function(errMsg) {
view.showLoadError('peer-assessment');
view.baseView.showLoadError('peer-assessment');
});
},
......@@ -50,7 +50,7 @@ OpenAssessment.PeerView.prototype = {
view.installHandlers(true);
}
).fail(function(errMsg) {
view.showLoadError('peer-assessment');
view.baseView.showLoadError('peer-assessment');
});
},
......
......@@ -106,5 +106,31 @@ OpenAssessment.Rubric.prototype = {
callback(canSubmit);
}
);
},
/**
Updates the rubric to display positive and negative messages on each
criterion. For each correction provided, the associated criterion will have
an appropriate message displayed.
Args: Corrections (list): A list of corrections to the rubric criteria that
did not match the expected selected options.
**/
updateRubric: function(corrections) {
var selector = "input[type=radio]";
var hasErrors = false;
// Display appropriate messages for each selection
$(selector, this.element).each(function(index, sel) {
var listItem = $(sel).parents(".assessment__rubric__question");
if (corrections.hasOwnProperty(sel.name)) {
hasErrors = true;
listItem.find('.message--incorrect').removeClass('is--hidden');
listItem.find('.message--correct').addClass('is--hidden');
} else {
listItem.find('.message--correct').removeClass('is--hidden');
listItem.find('.message--incorrect').addClass('is--hidden');
}
});
return hasErrors;
}
};
......@@ -296,6 +296,46 @@ OpenAssessment.Server.prototype = {
},
/**
Assess an instructor-provided training example.
Args:
optionsSelected (object literal): Keys are criteria names,
values are the option text the user selected for the criterion.
Returns:
A JQuery promise, which resolves with a boolean if successful
and fails with an error message otherwise.
Example:
var options = { clarity: "Very clear", precision: "Somewhat precise" };
server.trainingAssess(options).done(
function(isCorrect) { console.log("Success!"); }
).fail(
function(errorMsg) { console.log(errorMsg); }
);
**/
trainingAssess: function(optionsSelected) {
var url = this.url('training_assess');
var payload = JSON.stringify({
options_selected: optionsSelected
});
return $.Deferred(function(defer) {
$.ajax({ type: "POST", url: url, data: payload }).done(
function(data) {
if (data.success) {
defer.resolveWith(this, [data.corrections]);
}
else {
defer.rejectWith(this, [data.msg]);
}
}
).fail(function(data) {
defer.rejectWith(this, [gettext('This assessment could not be submitted.')]);
});
});
},
/**
Load the XBlock's XML definition from the server.
Returns:
......
/**
Interface for student training 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.StudentTrainingView
**/
OpenAssessment.StudentTrainingView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
this.rubric = null;
};
OpenAssessment.StudentTrainingView.prototype = {
/**
Load the student training view.
**/
load: function() {
var view = this;
this.server.render('student_training').done(
function(html) {
// Load the HTML and install event handlers
$('#openassessment__student-training', view.element).replaceWith(html);
view.installHandlers();
}
).fail(function(errMsg) {
view.baseView.showLoadError('student-training');
});
},
/**
Install event handlers for the view.
**/
installHandlers: function() {
var sel = $("#openassessment__student-training", this.element);
var view = this;
// Install a click handler for collapse/expand
this.baseView.setUpCollapseExpand(sel);
// Initialize the rubric
var rubricSelector = $("#student-training--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.assessButtonEnabled, this));
}
// Install a click handler for submitting the assessment
sel.find('#student-training--001__assessment__submit').click(
function(eventObject) {
// Override default form submission
eventObject.preventDefault();
// Handle the click
view.assess();
}
);
},
/**
Submit an assessment for the training example.
**/
assess: function() {
// Immediately disable the button to prevent resubmission
this.assessButtonEnabled(false);
var options = {};
if (this.rubric !== null) {
options = this.rubric.optionsSelected();
}
var view = this;
var baseView = this.baseView;
this.server.trainingAssess(options).done(
function(corrections) {
if (!view.rubric.updateRubric(corrections)) {
baseView.loadAssessmentModules();
}
baseView.scrollToTop();
}
).fail(function(errMsg) {
// Display the error
baseView.toggleActionError('student-training', errMsg);
// Re-enable the button to allow the user to resubmit
view.assessButtonEnabled(true);
});
},
/**
Enable/disable the submit training assessment button.
Check that whether the assessment button is enabled.
Args:
enabled (bool): If specified, set the state of the button.
Returns:
bool: Whether the button is enabled.
Examples:
>> view.assessButtonEnabled(true); // enable the button
>> view.assessButtonEnabled(); // check whether the button is enabled
>> true
**/
assessButtonEnabled: function(isEnabled) {
var button = $('#student-training--001__assessment__submit', this.element);
if (typeof isEnabled === 'undefined') {
return !button.hasClass('is--disabled');
} else {
button.toggleClass('is--disabled', !isEnabled);
}
}
};
......@@ -73,62 +73,84 @@
.student__answer__display__content p {
color: inherit;
}
}
// --------------------
// Developer Styles for Student Training
// --------------------
.step--student-training {
.step--student-training {
// submission
.student-training__display {
@extend %ui-subsection;
}
// submission
.student-training__display {
@extend %ui-subsection;
}
.student-training__display__header {
@include clearfix();
}
.student-training__display__header {
@include clearfix();
}
.student-training__display__title {
@extend %t-heading;
margin-bottom: ($baseline-v/2);
color: $heading-secondary-color;
}
.student-training__display__response {
@extend %ui-subsection-content;
@extend %copy-3;
@extend %ui-content-longanswer;
@extend %ui-well;
color: $copy-color;
}
.student-training__display__title {
@extend %t-heading;
margin-bottom: ($baseline-v/2);
color: $heading-secondary-color;
// assessment form
.student-training__assessment {
// fields
.assessment__fields {
margin-bottom: $baseline-v;
}
.student-training__display__response {
@extend %ui-subsection-content;
@extend %copy-3;
@extend %ui-content-longanswer;
@extend %ui-well;
color: $copy-color;
// rubric question
.assessment__rubric__question {
@extend %ui-rubric-question;
}
// assessment form
.student-training__assessment {
// rubric options
.question__answers {
@extend %ui-rubric-answers;
overflow: visible; // needed for ui-hints
}
// fields
.assessment__fields {
margin-bottom: $baseline-v;
}
// genereal feedback question
.assessment__rubric__question--feedback {
// rubric question
.assessment__rubric__question {
@extend %ui-rubric-question;
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*5);
}
}
}
// rubric options
.question__answers {
@extend %ui-rubric-answers;
overflow: visible; // needed for ui-hints
}
// TYPE: correct
.message--correct {
@extend .message--complete;
}
// genereal feedback question
.assessment__rubric__question--feedback {
// TYPE: incorrect
.message--incorrect {
@extend .message--incomplete;
}
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*5);
}
}
// Stolen from oa_base is--collapsed.
.is--hidden {
height: 0;
width: 0;
padding: 0 0 0 0;
.step__header {
padding-bottom: 0;
border-bottom: none;
margin-bottom: 0;
}
}
}
\ No newline at end of file
}
......@@ -169,5 +169,5 @@ class StudentTrainingMixin(object):
return {
'success': True,
'msg': u'',
'correct': len(corrections) == 0,
'corrections': corrections,
}
......@@ -38,7 +38,7 @@ class StudentTrainingAssessTest(XBlockHandlerTestCase):
# Expect that we were correct
self.assertTrue(resp['success'], msg=resp.get('msg'))
self.assertTrue(resp['correct'])
self.assertFalse(resp['corrections'])
@scenario('data/student_training.xml', user_id="Plato")
@ddt.file_data('data/student_training_mixin.json')
......@@ -58,7 +58,7 @@ class StudentTrainingAssessTest(XBlockHandlerTestCase):
# Expect that we were marked incorrect
self.assertTrue(resp['success'], msg=resp.get('msg'))
self.assertFalse(resp['correct'])
self.assertTrue(resp['corrections'])
@scenario('data/student_training.xml', user_id="Plato")
@ddt.file_data('data/student_training_mixin.json')
......@@ -80,7 +80,7 @@ class StudentTrainingAssessTest(XBlockHandlerTestCase):
# Expect that we were correct
self.assertTrue(resp['success'], msg=resp.get('msg'))
self.assertTrue(resp['correct'])
self.assertFalse(resp['corrections'])
# Agree with the course author's assessment
# (as defined in the scenario XML)
......@@ -98,7 +98,7 @@ class StudentTrainingAssessTest(XBlockHandlerTestCase):
# Expect that we were correct
self.assertTrue(resp['success'], msg=resp.get('msg'))
self.assertTrue(resp['correct'])
self.assertFalse(resp['corrections'])
expected_context = {}
expected_template = "openassessmentblock/student_training/student_training_complete.html"
self._assert_path_and_context(xblock, expected_template, expected_context)
......
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