Commit a73a0638 by gradyward

A completed first draft of the authoring changes.

parent dfb745c4
......@@ -2133,47 +2133,104 @@ hr.divider,
.step--student-training .message--incorrect.is--hidden .step__header {
border-bottom: none; }
#openassessment-editor .openassessment-editor-content-and-tabs {
width: 100%;
height: 370px; }
#openassessment-editor .openassessment-editor-header {
background-color: #e5e5e5;
width: 100%;
top: 0; }
#openassessment-editor #oa-editor-window-title {
float: left; }
#openassessment-editor .oa-editor-tab {
float: right;
padding: 2.5px 5px;
margin: 2.5px 5px; }
#openassessment-editor .oa-editor-content-wrapper {
height: 100%;
width: 100%;
padding: 5px 10px; }
#openassessment-editor .openassessment-prompt-editor {
width: 100%;
height: 100%;
resize: none; }
#openassessment-editor .openassessment-rubric-editor {
width: 100%;
height: 100%; }
#openassessment-editor .openassessment-assessments-editor {
width: 100%; }
#openassessment-editor #oa-settings-editor-text-fields {
float: left;
width: 30%; }
#openassessment-editor #oa-settings-assessments {
float: right;
width: 70%;
height: 100%; }
#openassessment-editor .xblock-actions {
background-color: #e5e5e5;
position: absolute;
width: 100%;
bottom: 0; }
#openassessment-editor {
margin-bottom: 0; }
#openassessment-editor .openassessment-editor-content-and-tabs {
width: 100%;
height: 370px; }
#openassessment-editor .openassessment-editor-header {
background-color: #e5e5e5;
width: 100%;
top: 0; }
#openassessment-editor #oa-editor-window-title {
float: left; }
#openassessment-editor .oa-editor-tab {
float: right;
padding: 2.5px 5px;
margin: 2.5px 5px;
border-radius: 2.5px;
box-shadow: none;
border: 0; }
#openassessment-editor .oa-editor-content-wrapper {
height: 100%;
width: 100%;
padding: 5px 10px; }
#openassessment-editor .openassessment-prompt-editor {
width: 100%;
height: 100%;
resize: none; }
#openassessment-editor .openassessment-rubric-editor {
width: 100%;
height: 100%; }
#openassessment-editor .openassessment-assessments-editor {
width: 100%; }
#openassessment-editor #oa-settings-editor-wrapper {
overflow-y: scroll; }
#openassessment-editor #openassessment-title-editor {
width: 300px;
margin-left: 50px; }
#openassessment-editor .openassessment-number-field {
width: 25px; }
#openassessment-editor .openassessment-date-field {
width: 130px; }
#openassessment-editor .openassessment-description {
font-size: 75%; }
#openassessment-editor .openassessment-text-field-wrapper {
width: 50%;
text-align: center; }
#openassessment-editor .right-text-field-wrapper {
float: right; }
#openassessment-editor .left-text-field-wrapper {
float: left; }
#openassessment-editor .openassessment-due-date-editor {
height: 30px; }
#openassessment-editor .openassessment-inclusion-wrapper {
background-color: #dadbdc;
padding: 2.5px 5px;
margin: 2.5px 5px;
border-radius: 2.5px; }
#openassessment-editor label {
padding-right: 10px; }
#openassessment-editor .xblock-actions {
background-color: #e5e5e5;
position: absolute;
width: 100%;
bottom: 0; }
#openassessment-editor .peer-number-constraints {
margin-bottom: 10px; }
#openassessment-editor .ui-widget-header .ui-state-default {
background: #e5e5e5; }
#openassessment-editor .ui-widget-header .ui-state-default a {
color: #202021;
text-transform: uppercase;
outline-color: transparent; }
#openassessment-editor .ui-widget-header .ui-state-active {
background: #414243;
color: whitesmoke; }
#openassessment-editor .ui-widget-header .ui-state-active a {
color: whitesmoke;
text-transform: uppercase;
outline-color: transparent; }
#openassessment-editor input[type="checkbox"] {
display: none; }
#openassessment-editor input[type="checkbox"] + label:before {
font-family: "FontAwesome";
display: inline-block;
margin-right: 10px;
width: auto;
height: auto;
content: "\f096"; }
#openassessment-editor input[type="checkbox"]:checked + label:before {
content: "\f046"; }
#openassessment-editor hr {
background-color: #d4d4d4;
color: #414243;
height: 1px;
border: 0px;
clear: both; }
.modal-content {
height: 500px !important; }
height: 470px !important; }
.openassessment .self-assessment__display__header, .openassessment .peer-assessment__display__header, .openassessment .step__header {
margin-bottom: 0 !important;
......
......@@ -17,7 +17,21 @@ describe("OpenAssessment.StudioView", function() {
this.titleField = "";
this.submissionStartField = "";
this.submissionDueField = "";
this.assessmentsXmlBox = "";
this.hasPeer = true;
this.hasSelf = true;
this.hasTraining = true;
this.hasAI = false;
this.peerMustGrade = 2;
this.peerGradedBy = 3;
this.peerStart = '';
this.peerDue = '';
this.selfStart = '';
this.selfDue = '';
this.aiTrainingExamplesCodeBox = "";
this.studentTrainingExamplesCodeBox = "";
this.isReleased = false;
......@@ -28,16 +42,27 @@ describe("OpenAssessment.StudioView", function() {
this.loadEditorContext = function() {
var prompt = this.promptBox;
var rubric = this.rubricXmlBox;
var settings = {
title: this.titleField,
submission_start: this.submissionStartField,
submission_due: this.submissionDueField,
assessments: this.assessmentsXmlBox
};
var title = this.titleField;
var submission_start = this.submissionStartField;
var submission_due = this.submissionDueField;
var assessments = [
{
name: "peer",
must_grade: this.peerMustGrade,
must_be_graded_by: this.peerGradedBy,
start: this.peerStart,
due: this.peerDue
},
{
name: "self",
start: this.selfStart,
due: this.selfDue
}
];
if (!this.loadError) {
return $.Deferred(function(defer) {
defer.resolveWith(this, [prompt, rubric, settings]);
defer.resolveWith(this, [prompt, rubric, title, submission_start, submission_due, assessments]);
}).promise();
}
else {
......@@ -45,14 +70,39 @@ describe("OpenAssessment.StudioView", function() {
}
};
this.updateEditorContext = function(prompt, rubricXml, title, sub_start, sub_due, assessmentsXml) {
this.updateEditorContext = function(prompt, rubricXml, title, sub_start, sub_due, assessments) {
if (!this.updateError) {
this.promptBox = prompt;
this.rubricXmlBox = rubricXml;
this.titleField = title;
this.submissionStartField = sub_start;
this.submissionDueField = sub_due;
this.assessmentsXmlBox = assessmentsXml;
this.hasPeer = false;
this.hasSelf = false;
this.hasAI = false;
this.hasTraining = false;
for (var i = 0; i < assessments.length; i++) {
var assessment = assessments[i];
if (assessment.name == 'peer-assessment') {
this.hasPeer = true;
this.peerMustGrade = assessment.must_grade;
this.peerGradedBy = assessment.must_be_graded_by;
this.peerStart = assessment.start;
this.peerDue = assessment.due;
} else if (assessment.name == 'self-assessment') {
this.hasSelf = true;
this.selfStart = assessment.start;
this.selfDue = assessment.due;
} else if (assessment.name == 'example-based-assessment') {
this.hasAI = true;
this.aiTrainingExamplesCodeBox = assessment.examples;
} else if (assessment.name == 'student-training') {
this.hasTraining = true;
this.studentTrainingExamplesCodeBox = assessment.examples;
}
}
return $.Deferred(function(defer) {
defer.resolve();
}).promise();
......@@ -97,11 +147,9 @@ describe("OpenAssessment.StudioView", function() {
// Expect that the XML definition(s) were loaded
var rubric = view.rubricXmlBox.getValue();
var prompt = view.promptBox.value;
var assessments = view.assessmentsXmlBox.getValue()
expect(prompt).toEqual('');
expect(rubric).toEqual('');
expect(assessments).toEqual('');
});
it("saves the Editor Context definition", function() {
......
......@@ -51,17 +51,24 @@ describe("OpenAssessment.Server", function() {
'</criterion>'+
'</rubric>';
var assessments = '<assessments>' +
'<assessment name="peer-assessment" must_grade="1" must_be_graded_by="1" due="2000-01-02"/>' +
'<assessment name="self-assessment" due="2000-01-8"/>' +
'</assessments>';
var SETTINGS = {
title: 'This is the title.',
submission_start: '2012-10-09T00:00:00',
submission_due: '2015-10-10T00:00:00',
assessments: assessments
};
var ASSESSMENTS = [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": "",
"due": "4014-03-10T00:00:00"
},
{
"name": "self-assessment",
"start": "",
"due": ""
}
];
var TITLE = 'This is the title.';
var SUBMISSION_START = '2012-10-09T00:00:00';
var SUBMISSION_DUE = '2015-10-10T00:00:00';
beforeEach(function() {
// Create the server
......@@ -184,20 +191,33 @@ describe("OpenAssessment.Server", function() {
});
it("loads the XBlock's Context definition", function() {
stubAjax(true, { success: true, prompt: PROMPT, rubric: RUBRIC, settings: SETTINGS});
stubAjax(true, {
success: true, prompt: PROMPT, rubric: RUBRIC, title: TITLE,
submission_start: SUBMISSION_START, submission_due: SUBMISSION_DUE, assessments: ASSESSMENTS
});
var loadedPrompt = "";
var loadedRubric = "";
var loadedSettings = "";
server.loadEditorContext().done(function(prompt, rubric, settings) {
var loadedAssessments = [];
var loadedTitle = "";
var loadedStart = "";
var loadedDue = "";
server.loadEditorContext().done(function(prompt, rubric, title, sub_start, sub_due, assessments) {
loadedPrompt = prompt;
loadedRubric = rubric;
loadedSettings = settings;
loadedTitle = title;
loadedStart = sub_start;
loadedDue = sub_due;
loadedAssessments = assessments;
});
expect(loadedPrompt).toEqual(PROMPT);
expect(loadedRubric).toEqual(RUBRIC);
expect(loadedSettings).toEqual(SETTINGS);
expect(loadedTitle).toEqual(TITLE);
expect(loadedStart).toEqual(SUBMISSION_START);
expect(loadedDue).toEqual(SUBMISSION_DUE);
expect(loadedAssessments).toEqual(ASSESSMENTS);
expect($.ajax).toHaveBeenCalledWith({
url: '/editor_context', type: "POST", data: '""'
});
......@@ -207,11 +227,14 @@ describe("OpenAssessment.Server", function() {
stubAjax(true, { success: true });
server.updateEditorContext(
PROMPT, RUBRIC, SETTINGS.title, SETTINGS.submission_start, SETTINGS.submission_due, SETTINGS.assessments
PROMPT, RUBRIC, TITLE, SUBMISSION_START, SUBMISSION_DUE, ASSESSMENTS
);
expect($.ajax).toHaveBeenCalledWith({
type: "POST", url: '/update_editor_context',
data: JSON.stringify({prompt: PROMPT, rubric: RUBRIC, settings: SETTINGS})
data: JSON.stringify({
prompt: PROMPT, rubric: RUBRIC, title: TITLE, submission_start: SUBMISSION_START,
submission_due: SUBMISSION_DUE, assessments: ASSESSMENTS
})
});
});
......
......@@ -157,9 +157,7 @@ function OpenAssessmentBlock(runtime, element) {
/**
Render views within the base view on page load.
**/
$(function($) {
var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.BaseView(runtime, element, server);
view.load();
});
var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.BaseView(runtime, element, server);
view.load();
}
......@@ -32,8 +32,27 @@ OpenAssessment.StudioView = function(runtime, element, server) {
this.submissionDueField = live_element.find('.openassessment-submission-due-editor').first().get(0);
this.assessmentsXmlBox = CodeMirror.fromTextArea(
live_element.find('.openassessment-assessments-editor').first().get(0),
// Finds our boolean checkboxes that indicate the assessment definition
this.hasPeer = live_element.find('#include-peer-assessment');
this.hasSelf = live_element.find('#include-self-assessment');
this.hasAI = live_element.find('#include-ai-assessment');
this.hasTraining = live_element.find('#include-student-training');
this.peerMustGrade = live_element.find('#peer-assessment-must-grade');
this.peerGradedBy = live_element.find('#peer-assessment-graded-by');
this.peerStart = live_element.find('#peer-assessment-start-date');
this.peerDue = live_element.find('#peer-assessment-due-date');
this.selfStart = live_element.find('#self-assessment-start-date');
this.selfDue = live_element.find('#self-assessment-due-date');
this.aiTrainingExamplesCodeBox = CodeMirror.fromTextArea(
live_element.find('#ai-training-examples').get(0),
{mode: "xml", lineNumbers: true, lineWrapping: true}
);
this.studentTrainingExamplesCodeBox = CodeMirror.fromTextArea(
live_element.find('#student-training-examples').get(0),
{mode: "xml", lineNumbers: true, lineWrapping: true}
);
......@@ -52,9 +71,49 @@ OpenAssessment.StudioView = function(runtime, element, server) {
live_element.find('.openassessment-editor-content-and-tabs').tabs({
activate: function (event, ui){
view.rubricXmlBox.refresh();
view.assessmentsXmlBox.refresh();
}
});
live_element.find('#include-peer-assessment').change(function () {
if (this.checked){
$("#peer-assessment-description-closed", live_element).fadeOut('fast');
$("#peer-assessment-settings-editor", live_element).fadeIn();
} else {
$("#peer-assessment-settings-editor", live_element).fadeOut('fast');
$("#peer-assessment-description-closed", live_element).fadeIn();
}
});
live_element.find('#include-self-assessment').change(function () {
if (this.checked){
$("#self-assessment-description-closed", live_element).fadeOut('fast');
$("#self-assessment-settings-editor", live_element).fadeIn();
} else {
$("#self-assessment-settings-editor", live_element).fadeOut('fast');
$("#self-assessment-description-closed", live_element).fadeIn();
}
});
live_element.find('#include-ai-assessment').change(function () {
if (this.checked){
$("#ai-assessment-description-closed", live_element).fadeOut('fast');
$("#ai-assessment-settings-editor", live_element).fadeIn();
} else {
$("#ai-assessment-settings-editor", live_element).fadeOut('fast');
$("#ai-assessment-description-closed", live_element).fadeIn();
}
});
live_element.find('#include-student-training').change(function () {
if (this.checked){
$("#student-training-description-closed", live_element).fadeOut('fast');
$("#student-training-settings-editor", live_element).fadeIn();
} else {
$("#student-training-settings-editor", live_element).fadeOut('fast');
$("#student-training-description-closed", live_element).fadeIn();
}
});
};
OpenAssessment.StudioView.prototype = {
......@@ -65,13 +124,38 @@ OpenAssessment.StudioView.prototype = {
load: function () {
var view = this;
this.server.loadEditorContext().done(
function (prompt, rubricXml, settings) {
function (prompt, rubricXml, title, sub_start, sub_due, assessments) {
view.rubricXmlBox.setValue(rubricXml);
view.assessmentsXmlBox.setValue(settings.assessments);
view.submissionStartField.value = settings.submission_start;
view.submissionDueField.value = settings.submission_due;
view.submissionStartField.value = sub_start;
view.submissionDueField.value = sub_due;
view.promptBox.value = prompt;
view.titleField.value = settings.title;
view.titleField.value = title;
view.hasPeer.prop('checked',false).change();
view.hasSelf.prop('checked',false).change();
view.hasTraining.prop('checked',false).change();
view.hasAI.prop('checked',false).change();
for (var i = 0; i < assessments.length; i++) {
var assessment = assessments[i];
if (assessment.name == 'peer-assessment') {
view.hasPeer.prop('checked', true).change();
view.peerMustGrade.prop('value', assessment.must_grade);
view.peerGradedBy.prop('value', assessment.must_be_graded_by);
view.peerStart.prop('value', assessment.start);
view.peerDue.prop('value', assessment.due);
} else if (assessment.name == 'self-assessment') {
view.hasSelf.prop('checked', true).change();
view.selfStart.prop('value', assessment.start);
view.selfDue.prop('value', assessment.due);
} else if (assessment.name == 'example-based-assessment') {
view.hasAI.prop('checked', true).change();
view.aiTrainingExamplesCodeBox.setValue(assessment.examples);
} else if (assessment.name == 'student-training') {
view.hasTraining.prop('checked', true).change();
view.studentTrainingExamplesCodeBox.setValue(assessment.examples);
} else {
}
}
}).fail(function (msg) {
view.showError(msg);
}
......@@ -98,7 +182,7 @@ OpenAssessment.StudioView.prototype = {
}
).fail(function (errMsg) {
view.showError(msg);
});
});
},
/**
......@@ -131,10 +215,58 @@ OpenAssessment.StudioView.prototype = {
var title = this.titleField.value;
var sub_start = this.submissionStartField.value;
var sub_due = this.submissionDueField.value;
var assessmentsXml = this.assessmentsXmlBox.getValue();
var assessments = [];
if (this.hasTraining.prop('checked')){
assessments[assessments.length] = {
"name": "student-training",
"examples": this.studentTrainingExamplesCodeBox.getValue()
};
}
if (this.hasPeer.prop('checked')) {
var assessment = {
"name": "peer-assessment",
"must_grade": parseInt(this.peerMustGrade.prop('value')),
"must_be_graded_by": parseInt(this.peerGradedBy.prop('value'))
};
var start_str = this.peerStart.prop('value');
var due_str = this.peerDue.prop('value');
if (start_str){
assessment = $.extend(assessment, {"start": start_str})
}
if (due_str){
assessment = $.extend(assessment, {"due": due_str})
}
assessments[assessments.length] = assessment;
}
if (this.hasSelf.prop('checked')) {
assessment = {
"name": "self-assessment"
};
start_str = this.selfStart.prop('value');
due_str = this.selfDue.prop('value');
if (start_str){
assessment = $.extend(assessment, {"start": start_str})
}
if (due_str){
assessment = $.extend(assessment, {"due": due_str})
}
assessments[assessments.length] = assessment;
}
if (this.hasAI.prop('checked')) {
assessments[assessments.length] = {
"name": "example-based-assessment",
"algorithm_id": "ease",
"examples": this.aiTrainingExamplesCodeBox.getValue()
};
}
var view = this;
this.server.updateEditorContext(prompt, rubricXml, title, sub_start, sub_due, assessmentsXml).done(function () {
this.server.updateEditorContext(prompt, rubricXml, title, sub_start, sub_due, assessments).done(function () {
// Notify the client-side runtime that we finished saving
// so it can hide the "Saving..." notification.
view.runtime.notify('save', {state: 'end'});
......@@ -172,9 +304,9 @@ function OpenAssessmentEditor(runtime, element) {
/**
Initialize the editing interface on page load.
**/
$(function ($) {
var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.StudioView(runtime, element, server);
view.load();
});
var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.StudioView(runtime, element, server);
view.load();
};
\ No newline at end of file
......@@ -356,7 +356,9 @@ OpenAssessment.Server.prototype = {
$.ajax({
type: "POST", url: url, data: "\"\""
}).done(function(data) {
if (data.success) { defer.resolveWith(this, [data.prompt, data.rubric, data.settings]); }
if (data.success) { defer.resolveWith(this, [
data.prompt, data.rubric, data.title, data.submission_start, data.submission_due, data.assessments
]); }
else { defer.rejectWith(this, [data.msg]); }
}).fail(function(data) {
defer.rejectWith(this, [gettext('This problem could not be loaded.')]);
......@@ -367,7 +369,7 @@ OpenAssessment.Server.prototype = {
/**
Update the XBlock's XML definition on the server.
Returns:
Return
A JQuery promise, which resolves with no arguments
and fails with an error message.
......@@ -378,15 +380,16 @@ OpenAssessment.Server.prototype = {
function(err) { console.log(err); }
);
**/
updateEditorContext: function(prompt, rubricXml, title, sub_start, sub_due, assessmentsXml) {
updateEditorContext: function(prompt, rubricXml, title, sub_start, sub_due, assessments) {
var url = this.url('update_editor_context');
var settings = {
var payload = JSON.stringify({
'prompt': prompt,
'rubric': rubricXml,
'title': title,
'submission_start': sub_start,
'submission_due': sub_due,
'assessments': assessmentsXml
};
var payload = JSON.stringify({'prompt': prompt, 'rubric': rubricXml, 'settings': settings});
'assessments': assessments
});
return $.Deferred(function(defer) {
$.ajax({
type: "POST", url: url, data: payload
......
......@@ -171,6 +171,7 @@
// --------------------
#openassessment-editor {
margin-bottom: 0;
.openassessment-editor-content-and-tabs {
width: 100%;
......@@ -191,6 +192,9 @@
float: right;
padding: ($baseline-v/8) ($baseline-h/8);
margin: ($baseline-v/8) ($baseline-h/8);
border-radius: ($baseline-v/8);
box-shadow: none;
border: 0;
}
.oa-editor-content-wrapper {
......@@ -215,14 +219,57 @@
}
#oa-settings-editor-text-fields {
float: left;
width: 30%;
}
#oa-settings-assessments{
#oa-settings-editor-wrapper {
overflow-y: scroll;
}
#openassessment-title-editor {
width: 300px;
margin-left: 50px;
}
.openassessment-number-field{
width: 25px;
}
.openassessment-date-field{
width: 130px;
}
.openassessment-description{
font-size: 75%;
}
.openassessment-text-field-wrapper{
width: 50%;
text-align: center;
}
.right-text-field-wrapper {
float: right;
width: 70%;
height: 100%;
}
.left-text-field-wrapper {
float: left;
}
.openassessment-due-date-editor{
height: 30px;
}
.openassessment-inclusion-wrapper{
background-color: $edx-gray-l3;
input[type="checkbox"] {
}
padding: ($baseline-v/8) ($baseline-h/8);
margin: ($baseline-v/8) ($baseline-h/8);
border-radius: ($baseline-v)/8;
}
label{
padding-right: 10px;
}
.xblock-actions {
......@@ -231,8 +278,58 @@
width: 100%;
bottom: 0;
}
.peer-number-constraints{
margin-bottom: 10px;
}
.ui-widget-header .ui-state-default{
background: #e5e5e5;
a{
color: $edx-gray-d4;
text-transform: uppercase;
outline-color: transparent;
}
}
.ui-widget-header .ui-state-active{
background: $edx-gray-d3;
color: $white;
a{
color: $white;
text-transform: uppercase;
outline-color: transparent;
}
}
input[type="checkbox"]{
display: none;
}
input[type="checkbox"] + label:before {
font-family: "FontAwesome";
display: inline-block;
margin-right: ($baseline-h/4);
width: auto;
height: auto;
content: "\f096";
}
input[type="checkbox"]:checked + label:before{
content: "\f046";
}
hr {
background-color: #d4d4d4;
color: $edx-gray-d3;
height: 1px;
border: 0px;
clear: both;
}
}
.modal-content {
height: 500px !important;
height: 470px !important;
}
......@@ -2,6 +2,7 @@
Studio editing view for OpenAssessment XBlock.
"""
import pkg_resources
import copy
import logging
from django.template.context import Context
from django.template.loader import get_template
......@@ -42,12 +43,14 @@ class StudioMixin(object):
Update the XBlock's configuration.
Args:
data (dict): Data from the request; should have a value for the keys
'rubric', 'settings' and 'prompt'. The 'rubric' should be an XML
representation of the new rubric. The 'prompt' should be a plain
text prompt. The 'settings' should be a dict of 'title',
'submission_due', 'submission_start' and the XML configuration for
all 'assessments'.
data (dict): Data from the request; should have a value for the keys: 'rubric', 'prompt',
'title', 'submission_start', 'submission_due', and 'assessments'.
-- The 'rubric' should be an XML representation of the new rubric.
-- The 'prompt' and 'title' should be plain text.
-- The dates 'submission_start' and 'submission_due' are both ISO strings
-- The 'assessments' is a list of asessment dictionaries (much like self.rubric_assessments)
with the notable exception that all examples (for Student Training and eventually AI)
are in XML string format and need to be parsed into dictionaries.
Kwargs:
suffix (str): Not used
......@@ -55,18 +58,20 @@ class StudioMixin(object):
Returns:
dict with keys 'success' (bool) and 'msg' (str)
"""
missing_keys = list({'rubric', 'settings', 'prompt'} - set(data.keys()))
missing_keys = list(
{'rubric', 'prompt', 'title', 'assessments', 'submission_start', 'submission_due'} - set(data.keys())
)
if missing_keys:
logger.warn(
'Must specify the following keys in request JSON dict: {}'.format(missing_keys)
'Must specify the following missing keys in request JSON dict: {}'.format(missing_keys)
)
return {'success': False, 'msg': _('Error updating XBlock configuration')}
settings = data['settings']
try:
rubric = xml.parse_rubric_xml_str(data['rubric'])
assessments = xml.parse_assessments_xml_str(settings['assessments'])
submission_due = xml.parse_date(settings["submission_due"])
submission_start = xml.parse_date(settings["submission_start"])
rubric = xml.parse_rubric_xml_str(data["rubric"])
submission_due = xml.parse_date(data["submission_due"])
submission_start = xml.parse_date(data["submission_start"])
assessments = xml.parse_assessment_dictionaries(data["assessments"])
except xml.UpdateFromXmlError as ex:
return {'success': False, 'msg': _('An error occurred while saving: {error}').format(error=ex)}
......@@ -81,7 +86,7 @@ class StudioMixin(object):
assessments,
submission_due,
submission_start,
settings["title"],
data["title"],
data["prompt"]
)
return {'success': True, 'msg': 'Successfully updated OpenAssessment XBlock'}
......@@ -100,15 +105,27 @@ class StudioMixin(object):
suffix (str): Not used
Returns:
dict with keys 'success' (bool), 'message' (unicode),
'rubric' (unicode), 'prompt' (unicode), and 'settings' (dict)
dict with keys
'success' (bool), 'message' (unicode), 'rubric' (unicode), 'prompt' (unicode),
'title' (unicode), 'submission_start' (unicode), 'submission_due' (unicode), 'assessments (dict)
"""
try:
assessments = xml.serialize_assessments_to_xml_str(self)
rubric = xml.serialize_rubric_to_xml_str(self)
# We do not expect serialization to raise an exception,
# but if it does, handle it gracefully.
# Copies the rubric assessments so that we can change student training examples from dict -> str without
# negatively modifying the openassessmentblock definition.
assessment_list = copy.deepcopy(self.rubric_assessments)
# Finds the student training dictionary, if it exists, and replaces the examples with their XML definition
student_training_dictionary = [d for d in assessment_list if d["name"] == "student-training"]
if student_training_dictionary:
# Our for loop will return a list. Select the first element of that list if it exists.
student_training_dictionary = student_training_dictionary[0]
examples = xml.serialize_examples_to_xml_str(student_training_dictionary)
student_training_dictionary["examples"] = examples
# We do not expect serialization to raise an exception, but if it does, handle it gracefully.
except Exception as ex:
msg = _('An unexpected error occurred while loading the problem: {error}').format(error=ex)
logger.error(msg)
......@@ -122,19 +139,15 @@ class StudioMixin(object):
submission_start = self.submission_start if self.submission_start else ''
settings = {
'submission_due': submission_due,
'submission_start': submission_start,
'title': self.title,
'assessments': assessments
}
return {
'success': True,
'msg': '',
'rubric': rubric,
'prompt': self.prompt,
'settings': settings
'submission_due': submission_due,
'submission_start': submission_start,
'title': self.title,
'assessments': assessment_list
}
@XBlock.json_handler
......@@ -157,3 +170,4 @@ class StudioMixin(object):
'success': True, 'msg': u'',
'is_released': self.is_released()
}
......@@ -11,17 +11,24 @@
"</rubric>"
],
"prompt": "My new prompt.",
"settings": {
"title": "My new title.",
"assessments": [
"<assessments>",
"<assessment name=\"peer-assessment\" must_grade=\"5\" must_be_graded_by=\"3\" />",
"<assessment name=\"self-assessment\" />",
"</assessments>"
],
"submission_due": "2014-02-27T09:46:28",
"submission_start": "2014-02-10T09:46:28"
},
"submission_due": "4014-02-27T09:46:28",
"submission_start": "4014-02-10T09:46:28",
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": "",
"due": "4014-03-10T00:00:00"
},
{
"name": "self-assessment",
"start": "",
"due": ""
}
],
"expected-assessment": "peer-assessment",
"expected-criterion-prompt": "Test criterion prompt"
}
......
{
"no_rubric": {
"prompt": "My new prompt.",
"settings": {
"title": "My new title.",
"assessments": [
"<assessments>",
"<assessment name=\"peer-assessment\" must_grade=\"5\" must_be_graded_by=\"3\" />",
"<assessment name=\"self-assessment\" />",
"</assessments>"
],
"submission_due": "2014-02-27T09:46:28",
"submission_start": "2014-02-10T09:46:28"
},
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": "",
"due": ""
},
{
"name": "self-assessment",
"start": "",
"due": ""
}
],
"submission_due": "2014-02-27T09:46:28",
"submission_start": "2014-02-10T09:46:28",
"expected_error": "error"
},
"no_prompt": {
......@@ -26,20 +32,26 @@
"</criterion>",
"</rubric>"
],
"settings": {
"title": "My new title.",
"assessments": [
"<assessments>",
"<assessment name=\"peer-assessment\" must_grade=\"5\" must_be_graded_by=\"3\" />",
"<assessment name=\"self-assessment\" />",
"</assessments>"
],
"submission_due": "2014-02-27T09:46:28",
"submission_start": "2014-02-10T09:46:28"
},
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": "",
"due": ""
},
{
"name": "self-assessment",
"start": "",
"due": ""
}
],
"submission_due": "2014-02-27T09:46:28",
"submission_start": "2014-02-10T09:46:28",
"expected_error": "error"
},
"no_settings": {
"no_submission_due": {
"rubric": [
"<rubric>",
"<prompt>Test prompt</prompt>",
......@@ -52,9 +64,59 @@
"</rubric>"
],
"prompt": "My new prompt.",
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": "",
"due": ""
},
{
"name": "self-assessment",
"start": "",
"due": ""
}
],
"submission_start": "2014-02-10T09:46:28",
"expected_error": "error"
},
"invalid_dates": {
"invalid_dates_one": {
"rubric": [
"<rubric>",
"<prompt>Test prompt</prompt>",
"<criterion>",
"<name>Test criterion</name>",
"<prompt>Test criterion prompt</prompt>",
"<option points=\"0\"><name>No</name><explanation>No explanation</explanation></option>",
"<option points=\"2\"><name>Yes</name><explanation>Yes explanation</explanation></option>",
"</criterion>",
"</rubric>"
],
"prompt": "My new prompt.",
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": "",
"due": ""
},
{
"name": "self-assessment",
"start": "",
"due": ""
}
],
"submission_due": "2012-02-27T09:46:28",
"submission_start": "2015-02-10T09:46:28",
"expected_error": "cannot be later"
},
"invalid_dates_two": {
"rubric": [
"<rubric>",
"<prompt>Test prompt</prompt>",
......@@ -67,17 +129,25 @@
"</rubric>"
],
"prompt": "My new prompt.",
"settings": {
"title": "My new title.",
"assessments": [
"<assessments>",
"<assessment name=\"peer-assessment\" must_grade=\"5\" must_be_graded_by=\"3\" />",
"<assessment name=\"self-assessment\" start=\"2010-01-01\" due=\"2003-01-01\"/>",
"</assessments>"
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": "",
"due": ""
},
{
"name": "self-assessment",
"start": "",
"due": "2003-01-02T00:00:00"
}
],
"submission_due": "2012-02-27T09:46:28",
"submission_start": "2015-02-10T09:46:28"
},
"expected_error": "cannot be earlier"
"submission_start": "",
"expected_error": "cannot be later"
}
}
\ No newline at end of file
{
"no-dates": {
"assessments_list": [
{
"name": "peer-assessment",
"start": "",
"due": "",
"must_grade": 5,
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"due": "",
"start": ""
}
],
"results": [
{
"name": "peer-assessment",
"start": null,
"due": null,
"must_grade": 5,
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"due": null,
"start": null
}
]
},
"student-training": {
"assessments_list": [
{
"name": "student-training",
"start": "",
"due": "",
"examples": "<example><answer>ẗëṡẗ äṅṡẅëṛ</answer><select criterion=\"Test criterion\" option=\"Yes\" /><select criterion=\"Another test criterion\" option=\"No\" /></example><example><answer>äṅöẗḧëṛ ẗëṡẗ äṅṡẅëṛ</answer><select criterion=\"Another test criterion\" option=\"Yes\" /><select criterion=\"Test criterion\" option=\"No\" /></example>"
},
{
"name": "peer-assessment",
"start": "",
"due": "",
"must_grade": 5,
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"due": "",
"start": ""
}
],
"results": [
{
"name": "student-training",
"due": null,
"start": null,
"examples": [
{
"answer": "ẗëṡẗ äṅṡẅëṛ",
"options_selected": [
{
"criterion": "Test criterion",
"option": "Yes"
},
{
"criterion": "Another test criterion",
"option": "No"
}
]
},
{
"answer": "äṅöẗḧëṛ ẗëṡẗ äṅṡẅëṛ",
"options_selected": [
{
"criterion": "Another test criterion",
"option": "Yes"
},
{
"criterion": "Test criterion",
"option": "No"
}
]
}
]
},
{
"name": "peer-assessment",
"start": null,
"due": null,
"must_grade": 5,
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"due": null,
"start": null
}
]
},
"date-parsing": {
"assessments_list": [
{
"name": "student-training",
"start": "2014-10-10T01:00:01",
"due": "",
"examples": "<example><answer>ẗëṡẗ äṅṡẅëṛ</answer><select criterion=\"Test criterion\" option=\"Yes\" /><select criterion=\"Another test criterion\" option=\"No\" /></example><example><answer>äṅöẗḧëṛ ẗëṡẗ äṅṡẅëṛ</answer><select criterion=\"Another test criterion\" option=\"Yes\" /><select criterion=\"Test criterion\" option=\"No\" /></example>"
},
{
"name": "peer-assessment",
"start": "",
"due": "2015-01-01T00:00:00",
"must_grade": 5,
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"due": "2015-01-01T00:00:00",
"start": ""
}
],
"results": [
{
"name": "student-training",
"due": null,
"start": "2014-10-10T01:00:01",
"examples": [
{
"answer": "ẗëṡẗ äṅṡẅëṛ",
"options_selected": [
{
"criterion": "Test criterion",
"option": "Yes"
},
{
"criterion": "Another test criterion",
"option": "No"
}
]
},
{
"answer": "äṅöẗḧëṛ ẗëṡẗ äṅṡẅëṛ",
"options_selected": [
{
"criterion": "Another test criterion",
"option": "Yes"
},
{
"criterion": "Test criterion",
"option": "No"
}
]
}
]
},
{
"name": "peer-assessment",
"start": null,
"due": "2015-01-01T00:00:00",
"must_grade": 5,
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"due": "2015-01-01T00:00:00",
"start": null
}
]
}
}
\ No newline at end of file
{
"date-parsing-due": {
"assessments_list": [
{
"name": "student-training",
"start": "2014-10-10T01:00:01",
"due": "",
"examples": "<examples><example><answer>ẗëṡẗ äṅṡẅëṛ</answer><select criterion=\"Test criterion\" option=\"Yes\" /><select criterion=\"Another test criterion\" option=\"No\" /></example><example><answer>äṅöẗḧëṛ ẗëṡẗ äṅṡẅëṛ</answer><select criterion=\"Another test criterion\" option=\"Yes\" /><select criterion=\"Test criterion\" option=\"No\" /></example></examples>"
},
{
"name": "peer-assessment",
"start": "",
"due": "2015-01-01T00:00:HI",
"must_grade": 5,
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"due": "2015-014-01",
"start": ""
}
]
},
"date-parsing-start": {
"assessments_list": [
{
"name": "peer-assessment",
"start": "2014-13-13T00:00:00",
"due": "",
"must_grade": 5,
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"due": "",
"start": ""
}
]
},
"no-answers-in-examples": {
"assessments_list": [
{
"name": "student-training",
"start": "",
"due": "",
"examples": "<example><select criterion=\"Test criterion\" option=\"Yes\" /><select criterion=\"Another test criterion\" option=\"No\" /></example><example><answer>äṅöẗḧëṛ ẗëṡẗ äṅṡẅëṛ</answer><select criterion=\"Another test criterion\" option=\"Yes\" /><select criterion=\"Test criterion\" option=\"No\" /></example>"
},
{
"name": "peer-assessment",
"start": "",
"due": "",
"must_grade": 5,
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"due": "",
"start": ""
}
]
},
"must_grade": {
"assessments_list": [
{
"name": "peer-assessment",
"start": "",
"due": "",
"must_grade": "Not a number fool!",
"must_be_graded_by": 3
},
{
"name": "self-assessment",
"due": "",
"start": ""
}
]
},
"must_be_graded_by": {
"assessments_list": [
{
"name": "peer-assessment",
"start": "",
"due": "",
"must_grade": 3,
"must_be_graded_by": "Not a number fool!"
},
{
"name": "self-assessment",
"due": "",
"start": ""
}
]
}
}
\ No newline at end of file
......@@ -12,17 +12,24 @@
"</rubric>"
],
"prompt": "My new prompt.",
"settings": {
"title": "My new title.",
"assessments": [
"<assessments>",
"<assessment name=\"peer-assessment\" must_grade=\"5\" must_be_graded_by=\"3\" />",
"<assessment name=\"self-assessment\" />",
"</assessments>"
],
"submission_due": "4014-02-27T09:46:28",
"submission_start": "4014-02-10T09:46:28"
},
"submission_due": "4014-02-27T09:46:28",
"submission_start": "4014-02-10T09:46:28",
"title": "My new title.",
"assessments": [
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": "",
"due": "4014-03-10T00:00:00"
},
{
"name": "self-assessment",
"start": "",
"due": ""
}
],
"expected-assessment": "peer-assessment",
"expected-criterion-prompt": "Test criterion prompt"
}
......
......@@ -33,8 +33,12 @@ class StudioViewTest(XBlockHandlerTestCase):
rubric = etree.fromstring(resp['rubric'])
self.assertEqual(rubric.tag, 'rubric')
assessments = etree.fromstring(resp['settings']['assessments'])
self.assertEqual(assessments.tag, 'assessments')
# Verify that every assessment in the list of assessments has a name.
for assessment_dict in resp['assessments']:
self.assertTrue(assessment_dict.get('name', False))
if assessment_dict.get('name') == 'studnet-training':
examples = etree.fromstring(assessment_dict['examples'])
self.assertEqual(examples.tag, 'examples')
@mock.patch('openassessment.xblock.xml.serialize_rubric_to_xml_str')
@scenario('data/basic_scenario.xml')
......@@ -52,7 +56,6 @@ class StudioViewTest(XBlockHandlerTestCase):
def test_update_xblock(self, xblock, data):
# First, parse XML data into a single string.
data['rubric'] = "".join(data['rubric'])
data['settings']['assessments'] = "".join(data['settings']['assessments'])
xblock.published_date = None
# Test that we can update the xblock with the expected configuration.
request = json.dumps(data)
......@@ -66,7 +69,7 @@ class StudioViewTest(XBlockHandlerTestCase):
# Check that the XBlock fields were updated
# We don't need to be exhaustive here, because we have other unit tests
# that verify this extensively.
self.assertEqual(xblock.title, data['settings']['title'])
self.assertEqual(xblock.title, data['title'])
self.assertEqual(xblock.prompt, data['prompt'])
self.assertEqual(xblock.rubric_assessments[0]['name'], data['expected-assessment'])
self.assertEqual(xblock.rubric_criteria[0]['prompt'], data['expected-criterion-prompt'])
......@@ -76,7 +79,6 @@ class StudioViewTest(XBlockHandlerTestCase):
def test_update_context_post_release(self, xblock, data):
# First, parse XML data into a single string.
data['rubric'] = "".join(data['rubric'])
data['settings']['assessments'] = "".join(data['settings']['assessments'])
# XBlock start date defaults to already open,
# so we should get an error when trying to update anything that change the number of points
......@@ -93,9 +95,6 @@ class StudioViewTest(XBlockHandlerTestCase):
if 'rubric' in data:
data['rubric'] = "".join(data['rubric'])
if 'settings' in data and 'assessments' in data['settings']:
data['settings']['assessments'] = "".join(data['settings']['assessments'])
xblock.published_date = None
resp = self.request(xblock, 'update_editor_context', json.dumps(data), response_format='json')
......@@ -107,7 +106,6 @@ class StudioViewTest(XBlockHandlerTestCase):
def test_update_rubric_invalid(self, xblock, data):
# First, parse XML data into a single string.
data['rubric'] = "".join(data['rubric'])
data['settings']['assessments'] = "".join(data['settings']['assessments'])
request = json.dumps(data)
......
......@@ -15,7 +15,7 @@ from openassessment.xblock.xml import (
serialize_content, parse_from_xml_str, parse_rubric_xml_str,
parse_examples_xml_str, parse_assessments_xml_str,
serialize_rubric_to_xml_str, serialize_examples_to_xml_str,
serialize_assessments_to_xml_str, UpdateFromXmlError
serialize_assessments_to_xml_str, UpdateFromXmlError, parse_assessment_dictionaries
)
......@@ -358,6 +358,21 @@ class TestParseAssessmentsFromXml(TestCase):
self.assertEqual(assessments, data['assessments'])
@ddt.ddt
class TestParseAssessmentsFromDictionaries(TestCase):
@ddt.file_data('data/parse_assessment_dicts.json')
def test_parse_assessments_dictionary(self, data):
config = parse_assessment_dictionaries(data['assessments_list'])
for i in range(0, len(config)):
self.assertEqual(config[i], data['results'][i])
@ddt.file_data('data/parse_assessment_dicts_error.json')
def test_parse_assessments_dictionary_error(self, data):
with self.assertRaises(UpdateFromXmlError):
parse_assessment_dictionaries(data['assessments_list'])
@ddt.ddt
class TestUpdateFromXml(TestCase):
......@@ -399,3 +414,4 @@ class TestUpdateFromXml(TestCase):
def test_parse_from_xml_error(self, data):
with self.assertRaises(UpdateFromXmlError):
parse_from_xml_str("".join(data['xml']))
......@@ -295,7 +295,7 @@ def validator(oa_block, strict_post_release=True):
# Dates
submission_dates = [(submission_dict['start'], submission_dict['due'])]
assessment_dates = [(asmnt['start'], asmnt['due']) for asmnt in assessments]
assessment_dates = [(asmnt.get('start'), asmnt.get('due')) for asmnt in assessments]
success, msg = validate_dates(oa_block.start, oa_block.due, submission_dates + assessment_dates)
if not success:
return (False, msg)
......
......@@ -374,6 +374,68 @@ def parse_examples_xml(examples):
return examples_list
def parse_assessment_dictionaries(input_assessments):
assessments_list = []
for assessment in input_assessments:
assessment_dict = dict()
# Assessment name
if assessment.get('name'):
assessment_dict['name'] = unicode(assessment.get('name'))
else:
raise UpdateFromXmlError(_('All "assessment" elements must contain a "name" element.'))
# Assessment start
if assessment.get('start'):
try:
parsed_start = parse_date(assessment.get('start'))
assessment_dict['start'] = parsed_start
except UpdateFromXmlError:
raise UpdateFromXmlError(_('The date format in the "start" attribute is invalid. Make sure the date is formatted as YYYY-MM-DDTHH:MM:SS.'))
else:
assessment_dict['start'] = None
# Assessment due
if assessment.get('due'):
try:
parsed_due = parse_date(assessment.get('due'))
assessment_dict['due'] = parsed_due
except UpdateFromXmlError:
raise UpdateFromXmlError(_('The date format in the "due" attribute is invalid. Make sure the date is formatted as YYYY-MM-DDTHH:MM:SS.'))
else:
assessment_dict['due'] = None
# Assessment must_grade
if assessment.get('must_grade'):
try:
assessment_dict['must_grade'] = int(assessment.get('must_grade'))
except ValueError:
raise UpdateFromXmlError(_('The "must_grade" value must be a positive integer.'))
# Assessment must_be_graded_by
if assessment.get('must_be_graded_by'):
try:
assessment_dict['must_be_graded_by'] = int(assessment.get('must_be_graded_by'))
except ValueError:
raise UpdateFromXmlError(_('The "must_be_graded_by" value must be a positive integer.'))
# Training examples (can be for AI or for Student Training)
if assessment.get('examples'):
try:
assessment_dict['examples'] = parse_examples_xml_str(assessment.get('examples'))
except (UpdateFromXmlError, UnicodeError) as ex:
raise UpdateFromXmlError(_(
"There was an error in parsing the {0} examples: {1}".format(assessment.get('name'), ex)
))
# Update the list of assessments
assessments_list.append(assessment_dict)
return assessments_list
def parse_assessments_xml(assessments_root):
"""
Parse the <assessments> element in the OpenAssessment XBlock's content XML.
......@@ -765,8 +827,12 @@ def parse_examples_xml_str(xml):
"""
xml = u"<data>" + xml + u"</data>"
return parse_examples_xml(list(_unicode_to_xml(xml)))
if "<examples>" not in xml:
xml = u"<data>" + xml + u"</data>"
else:
xml = unicode(xml)
return parse_examples_xml(list(_unicode_to_xml(xml).findall('example')))
def _unicode_to_xml(xml):
......
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