Commit dfb745c4 by gradyward

Merge pull request #413 from edx/grady/authoring_front_end

Changed studio editing of open assessment blocks to have three tabs and changed validation process to accommodate
parents d92cce76 700a69ce
...@@ -8,3 +8,4 @@ Mark Hoeber <hoeber@edx.org> ...@@ -8,3 +8,4 @@ Mark Hoeber <hoeber@edx.org>
Sylvia Pearce <spearce@edx.org> Sylvia Pearce <spearce@edx.org>
Ned Batchelder <ned@nedbatchelder.com> Ned Batchelder <ned@nedbatchelder.com>
David Baumgold <david@davidbaumgold.com> David Baumgold <david@davidbaumgold.com>
Grady Ward <gward@brandeis.edu>
\ No newline at end of file
{% load i18n %} {% load i18n %}
<div id="openassessment-edit" class="editor-with-buttons"> <div id="openassessment-editor" class="editor-with-buttons editor-with-tabs">
<textarea class="openassessment-editor"></textarea> <div class="openassessment-editor-content-and-tabs">
<div class="openassessment-editor-header">
<h6 id="oa-editor-window-title" class="title modal-window-title" >{% trans "Editing: Open Assessment" %}</h6>
<ul class="editor-modes action-list action-modes editor-tabs">
<li class="view-button oa-editor-tab"><a href="#oa-settings-editor-wrapper">{% trans "Settings" %}</a></li>
<li class="view-button oa-editor-tab"><a href="#oa-rubric-editor-wrapper">{% trans "Rubric" %}</a></li>
<li class="view-button oa-editor-tab"><a href="#oa-prompt-editor-wrapper">{% trans "Prompt" %}</a></li>
</ul>
</div>
<div id = "oa-prompt-editor-wrapper" class="oa-editor-content-wrapper">
<h2>{% trans "Prompt Editor" %}</h2>
<textarea class="openassessment-prompt-editor"></textarea>
</div>
<div id="oa-rubric-editor-wrapper" class="oa-editor-content-wrapper">
<h2>{% trans "Rubric Editor" %}</h2>
<div id="rubric-editor-wrapper">
<textarea class="openassessment-rubric-editor"></textarea>
</div>
</div>
<div id="oa-settings-editor-wrapper" class="oa-editor-content-wrapper">
<div id="oa-settings-editor-text-fields">
<h2>{% trans "Settings Editor" %}</h2>
<h2>{% trans "Title:" %}</h2>
<input type="text" name="title" class="openassessment-title-editor">
<h2>{% trans "Submission Start Date:" %}</h2>
<input type="text" name="start_date" class="openassessment-submission-start-editor">
<h2>{% trans "Submission Due Date:" %}</h2>
<input type="text" name="due_date" class="openassessment-submission-due-editor">
</div>
<div id="oa-settings-assessments">
<h2> {% trans "XML for Assessments" %} </h2>
<textarea class="openassessment-assessments-editor"></textarea>
</div>
</div>
</div>
<div class="xblock-actions"> <div class="xblock-actions">
<h3 class="sr">Actions</h3> <h3 class="sr">Actions</h3>
<ul> <ul>
......
...@@ -334,7 +334,7 @@ class OpenAssessmentBlock( ...@@ -334,7 +334,7 @@ class OpenAssessmentBlock(
xblock_validator = validator(block, strict_post_release=False) xblock_validator = validator(block, strict_post_release=False)
xblock_validator( xblock_validator(
rubric, rubric,
{'due': config['submission_due']}, { 'due': config['submission_due'], 'start': config['submission_start']},
config['rubric_assessments'] config['rubric_assessments']
) )
......
...@@ -2133,6 +2133,48 @@ hr.divider, ...@@ -2133,6 +2133,48 @@ hr.divider,
.step--student-training .message--incorrect.is--hidden .step__header { .step--student-training .message--incorrect.is--hidden .step__header {
border-bottom: none; } 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; }
.modal-content {
height: 500px !important; }
.openassessment .self-assessment__display__header, .openassessment .peer-assessment__display__header, .openassessment .step__header { .openassessment .self-assessment__display__header, .openassessment .peer-assessment__display__header, .openassessment .step__header {
margin-bottom: 0 !important; margin-bottom: 0 !important;
border-radius: 0 !important; border-radius: 0 !important;
......
...@@ -12,18 +12,32 @@ describe("OpenAssessment.StudioView", function() { ...@@ -12,18 +12,32 @@ describe("OpenAssessment.StudioView", function() {
var StubServer = function() { var StubServer = function() {
this.loadError = false; this.loadError = false;
this.updateError = false; this.updateError = false;
this.xml = '<openassessment></openassessment>'; this.promptBox = "";
this.rubricXmlBox = "";
this.titleField = "";
this.submissionStartField = "";
this.submissionDueField = "";
this.assessmentsXmlBox = "";
this.isReleased = false; this.isReleased = false;
this.errorPromise = $.Deferred(function(defer) { this.errorPromise = $.Deferred(function(defer) {
defer.rejectWith(this, ['Test error']); defer.rejectWith(this, ['Test error']);
}).promise(); }).promise();
this.loadXml = function() { this.loadEditorContext = function() {
var xml = this.xml; var prompt = this.promptBox;
var rubric = this.rubricXmlBox;
var settings = {
title: this.titleField,
submission_start: this.submissionStartField,
submission_due: this.submissionDueField,
assessments: this.assessmentsXmlBox
};
if (!this.loadError) { if (!this.loadError) {
return $.Deferred(function(defer) { return $.Deferred(function(defer) {
defer.resolveWith(this, [xml]); defer.resolveWith(this, [prompt, rubric, settings]);
}).promise(); }).promise();
} }
else { else {
...@@ -31,9 +45,14 @@ describe("OpenAssessment.StudioView", function() { ...@@ -31,9 +45,14 @@ describe("OpenAssessment.StudioView", function() {
} }
}; };
this.updateXml = function(xml) { this.updateEditorContext = function(prompt, rubricXml, title, sub_start, sub_due, assessmentsXml) {
if (!this.updateError) { if (!this.updateError) {
this.xml = xml; this.promptBox = prompt;
this.rubricXmlBox = rubricXml;
this.titleField = title;
this.submissionStartField = sub_start;
this.submissionDueField = sub_due;
this.assessmentsXmlBox = assessmentsXml;
return $.Deferred(function(defer) { return $.Deferred(function(defer) {
defer.resolve(); defer.resolve();
}).promise(); }).promise();
...@@ -67,32 +86,37 @@ describe("OpenAssessment.StudioView", function() { ...@@ -67,32 +86,37 @@ describe("OpenAssessment.StudioView", function() {
spyOn(runtime, 'notify'); spyOn(runtime, 'notify');
// Create the object under test // Create the object under test
var el = $('#openassessment-edit').get(0); var el = $('#openassessment-editor').get(0);
view = new OpenAssessment.StudioView(runtime, el, server); view = new OpenAssessment.StudioView(runtime, el, server);
}); });
it("loads the XML definition", function() { it("loads the editor context definition", function() {
// Initialize the view // Initialize the view
view.load(); view.load();
// Expect that the XML definition was loaded // Expect that the XML definition(s) were loaded
var contents = view.codeBox.getValue(); var rubric = view.rubricXmlBox.getValue();
expect(contents).toEqual('<openassessment></openassessment>'); var prompt = view.promptBox.value;
var assessments = view.assessmentsXmlBox.getValue()
expect(prompt).toEqual('');
expect(rubric).toEqual('');
expect(assessments).toEqual('');
}); });
it("saves the XML definition", function() { it("saves the Editor Context definition", function() {
// Update the XML // Update the Context
view.codeBox.setValue('<openassessment>test!</openassessment>'); view.titleField.value = 'THIS IS THE NEW TITLE';
// Save the updated XML // Save the updated editor definition
view.save(); view.save();
// Expect the saving notification to start/end // Expect the saving notification to start/end
expect(runtime.notify).toHaveBeenCalledWith('save', {state: 'start'}); expect(runtime.notify).toHaveBeenCalledWith('save', {state: 'start'});
expect(runtime.notify).toHaveBeenCalledWith('save', {state: 'end'}); expect(runtime.notify).toHaveBeenCalledWith('save', {state: 'end'});
// Expect the server's XML to have been updated // Expect the server's context to have been updated
expect(server.xml).toEqual('<openassessment>test!</openassessment>'); expect(server.titleField).toEqual('THIS IS THE NEW TITLE');
}); });
it("confirms changes for a released problem", function() { it("confirms changes for a released problem", function() {
...@@ -104,7 +128,7 @@ describe("OpenAssessment.StudioView", function() { ...@@ -104,7 +128,7 @@ describe("OpenAssessment.StudioView", function() {
function(onConfirm) { onConfirm(); } function(onConfirm) { onConfirm(); }
); );
// Save the updated XML // Save the updated context
view.save(); view.save();
// Verify that the user was asked to confirm the changes // Verify that the user was asked to confirm the changes
...@@ -124,8 +148,7 @@ describe("OpenAssessment.StudioView", function() { ...@@ -124,8 +148,7 @@ describe("OpenAssessment.StudioView", function() {
it("displays an error when server reports an update XML error", function() { it("displays an error when server reports an update XML error", function() {
server.updateError = true; server.updateError = true;
view.save('<openassessment>test!</openassessment>'); view.save();
expect(runtime.notify).toHaveBeenCalledWith('error', {msg: 'Test error'}); expect(runtime.notify).toHaveBeenCalledWith('error', {msg: 'Test error'});
}); });
}); });
...@@ -30,6 +30,39 @@ describe("OpenAssessment.Server", function() { ...@@ -30,6 +30,39 @@ describe("OpenAssessment.Server", function() {
); );
}; };
var PROMPT = "Hello this is the prompt yes.";
var RUBRIC = '<rubric>'+
'<criterion>'+
'<name>𝓒𝓸𝓷𝓬𝓲𝓼𝓮</name>'+
'<prompt>How concise is it?</prompt>'+
'<option points="3">'+
'<name>ﻉซƈﻉɭɭﻉกՇ</name>'+
'<explanation>Extremely concise</explanation>'+
'</option>'+
'<option points="2">'+
'<name>Ġööḋ</name>'+
'<explanation>Concise</explanation>'+
'</option>'+
'<option points="1">'+
'<name>ק๏๏г</name>'+
'<explanation>Wordy</explanation>'+
'</option>'+
'</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
};
beforeEach(function() { beforeEach(function() {
// Create the server // Create the server
// Since the runtime is a stub implementation that ignores the element passed to it, // Since the runtime is a stub implementation that ignores the element passed to it,
...@@ -150,27 +183,35 @@ describe("OpenAssessment.Server", function() { ...@@ -150,27 +183,35 @@ describe("OpenAssessment.Server", function() {
}); });
}); });
it("loads the XBlock's XML definition", function() { it("loads the XBlock's Context definition", function() {
stubAjax(true, { success: true, xml: "<openassessment />" }); stubAjax(true, { success: true, prompt: PROMPT, rubric: RUBRIC, settings: SETTINGS});
var loadedXml = ""; var loadedPrompt = "";
server.loadXml().done(function(xml) { var loadedRubric = "";
loadedXml = xml; var loadedSettings = "";
server.loadEditorContext().done(function(prompt, rubric, settings) {
loadedPrompt = prompt;
loadedRubric = rubric;
loadedSettings = settings;
}); });
expect(loadedXml).toEqual('<openassessment />'); expect(loadedPrompt).toEqual(PROMPT);
expect(loadedRubric).toEqual(RUBRIC);
expect(loadedSettings).toEqual(SETTINGS);
expect($.ajax).toHaveBeenCalledWith({ expect($.ajax).toHaveBeenCalledWith({
url: '/xml', type: "POST", data: '""' url: '/editor_context', type: "POST", data: '""'
}); });
}); });
it("updates the XBlock's XML definition", function() { it("updates the XBlock's Context definition", function() {
stubAjax(true, { success: true }); stubAjax(true, { success: true });
server.updateXml('<openassessment />'); server.updateEditorContext(
PROMPT, RUBRIC, SETTINGS.title, SETTINGS.submission_start, SETTINGS.submission_due, SETTINGS.assessments
);
expect($.ajax).toHaveBeenCalledWith({ expect($.ajax).toHaveBeenCalledWith({
url: '/update_xml', type: "POST", type: "POST", url: '/update_editor_context',
data: JSON.stringify({xml: '<openassessment />'}) data: JSON.stringify({prompt: PROMPT, rubric: RUBRIC, settings: SETTINGS})
}); });
}); });
...@@ -252,44 +293,45 @@ describe("OpenAssessment.Server", function() { ...@@ -252,44 +293,45 @@ describe("OpenAssessment.Server", function() {
expect(receivedMsg).toEqual('test error'); expect(receivedMsg).toEqual('test error');
}); });
it("informs the caller of an Ajax error when loading XML", function() {
it("informs the caller of an Ajax error when loading the editor context", function() {
stubAjax(false, null); stubAjax(false, null);
var receivedMsg = null; var receivedMsg = null;
server.loadXml().fail(function(msg) { server.loadEditorContext().fail(function(msg) {
receivedMsg = msg; receivedMsg = msg;
}); });
expect(receivedMsg).toContain("This problem could not be loaded"); expect(receivedMsg).toContain("This problem could not be loaded");
}); });
it("informs the caller of an Ajax error when updating XML", function() { it("informs the caller of an Ajax error when updating the editor context", function() {
stubAjax(false, null); stubAjax(false, null);
var receivedMsg = null; var receivedMsg = null;
server.updateXml('test').fail(function(msg) { server.updateEditorContext('prompt', 'rubric', 'title', 'start', 'due', 'assessments').fail(function(msg) {
receivedMsg = msg; receivedMsg = msg;
}); });
expect(receivedMsg).toContain("This problem could not be saved"); expect(receivedMsg).toContain("This problem could not be saved");
}); });
it("informs the caller of a server error when loading XML", function() { it("informs the caller of a server error when loading the editor context", function() {
stubAjax(true, { success: false, msg: "Test error" }); stubAjax(true, { success: false, msg: "Test error" });
var receivedMsg = null; var receivedMsg = null;
server.updateXml('test').fail(function(msg) { server.updateEditorContext('prompt', 'rubric', 'title', 'start', 'due', 'assessments').fail(function(msg) {
receivedMsg = msg; receivedMsg = msg;
}); });
expect(receivedMsg).toEqual("Test error"); expect(receivedMsg).toEqual("Test error");
}); });
it("informs the caller of a server error when updating XML", function() { it("informs the caller of a server error when updating the editor context", function() {
stubAjax(true, { success: false, msg: "Test error" }); stubAjax(true, { success: false, msg: "Test error" });
var receivedMsg = null; var receivedMsg = null;
server.loadXml().fail(function(msg) { server.loadEditorContext().fail(function(msg) {
receivedMsg = msg; receivedMsg = msg;
}); });
......
/** /**
Interface for editing view in Studio. Interface for editing view in Studio.
The constructor initializes the DOM for editing. The constructor initializes the DOM for editing.
Args: Args:
runtime (Runtime): an XBlock runtime instance. runtime (Runtime): an XBlock runtime instance.
element (DOM element): The DOM element representing this XBlock. element (DOM element): The DOM element representing this XBlock.
server (OpenAssessment.Server): The interface to the XBlock server. server (OpenAssessment.Server): The interface to the XBlock server.
Returns: Returns:
OpenAssessment.StudioView OpenAssessment.StudioView
**/ **/
OpenAssessment.StudioView = function(runtime, element, server) { OpenAssessment.StudioView = function(runtime, element, server) {
this.runtime = runtime; this.runtime = runtime;
this.server = server; this.server = server;
// Initialize the code box // Initialize the code box
this.codeBox = CodeMirror.fromTextArea(
$(element).find('.openassessment-editor').first().get(0), live_element = $(element)
this.promptBox = live_element.find('.openassessment-prompt-editor').first().get(0);
this.rubricXmlBox = CodeMirror.fromTextArea(
live_element.find('.openassessment-rubric-editor').first().get(0),
{mode: "xml", lineNumbers: true, lineWrapping: true}
);
this.titleField = live_element.find('.openassessment-title-editor').first().get(0);
this.submissionStartField = live_element.find('.openassessment-submission-start-editor').first().get(0);
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),
{mode: "xml", lineNumbers: true, lineWrapping: true} {mode: "xml", lineNumbers: true, lineWrapping: true}
); );
// Install click handlers // Install click handlers
var view = this; var view = this;
$(element).find('.openassessment-save-button').click( live_element.find('.openassessment-save-button').click(
function(eventData) { function (eventData) {
view.save(); view.save();
}); });
$(element).find('.openassessment-cancel-button').click( live_element.find('.openassessment-cancel-button').click(
function(eventData) { function (eventData) {
view.cancel(); view.cancel();
});
live_element.find('.openassessment-editor-content-and-tabs').tabs({
activate: function (event, ui){
view.rubricXmlBox.refresh();
view.assessmentsXmlBox.refresh();
}
}); });
}; };
OpenAssessment.StudioView.prototype = { OpenAssessment.StudioView.prototype = {
/** /**
Load the XBlock XML definition from the server and display it in the view. Load the XBlock XML definition from the server and display it in the view.
**/ **/
load: function() { load: function () {
var view = this; var view = this;
this.server.loadXml().done( this.server.loadEditorContext().done(
function(xml) { function (prompt, rubricXml, settings) {
view.codeBox.setValue(xml); view.rubricXmlBox.setValue(rubricXml);
}).fail(function(msg) { view.assessmentsXmlBox.setValue(settings.assessments);
view.submissionStartField.value = settings.submission_start;
view.submissionDueField.value = settings.submission_due;
view.promptBox.value = prompt;
view.titleField.value = settings.title;
}).fail(function (msg) {
view.showError(msg); view.showError(msg);
} }
); );
}, },
/** /**
Save the problem's XML definition to the server. Save the problem's XML definition to the server.
If the problem has been released, make the user confirm the save. If the problem has been released, make the user confirm the save.
**/ **/
save: function() { save: function () {
var view = this; var view = this;
// Check whether the problem has been released; if not, // Check whether the problem has been released; if not,
// warn the user and allow them to cancel. // warn the user and allow them to cancel.
this.server.checkReleased().done( this.server.checkReleased().done(
function(isReleased) { function (isReleased) {
if (isReleased) { view.confirmPostReleaseUpdate($.proxy(view.updateXml, view)); } if (isReleased) {
else { view.updateXml(); } view.confirmPostReleaseUpdate($.proxy(view.updateEditorContext, view));
}
else {
view.updateEditorContext();
}
} }
).fail(function(errMsg) { ).fail(function (errMsg) {
view.showError(msg); view.showError(msg);
}); });
}, },
/** /**
Make the user confirm that he/she wants to update a problem Make the user confirm that he/she wants to update a problem
that has already been released. that has already been released.
Args: Args:
onConfirm (function): A function that accepts no arguments, onConfirm (function): A function that accepts no arguments,
executed if the user confirms the update. executed if the user confirms the update.
**/ **/
confirmPostReleaseUpdate: function(onConfirm) { confirmPostReleaseUpdate: function (onConfirm) {
var msg = gettext("This problem has already been released. Any changes will apply only to future assessments."); var msg = gettext("This problem has already been released. Any changes will apply only to future assessments.");
// TODO: classier confirm dialog // TODO: classier confirm dialog
if (confirm(msg)) { onConfirm(); } if (confirm(msg)) {
onConfirm();
}
}, },
/** /**
Save the updated XML definition to the server. Save the updated XML definition to the server.
**/ **/
updateXml: function() { updateEditorContext: function () {
// Notify the client-side runtime that we are starting // Notify the client-side runtime that we are starting
// to save so it can show the "Saving..." notification // to save so it can show the "Saving..." notification
this.runtime.notify('save', {state: 'start'}); this.runtime.notify('save', {state: 'start'});
// Send the updated XML to the server // Send the updated XML to the server
var xml = this.codeBox.getValue(); var prompt = this.promptBox.value;
var rubricXml = this.rubricXmlBox.getValue();
var title = this.titleField.value;
var sub_start = this.submissionStartField.value;
var sub_due = this.submissionDueField.value;
var assessmentsXml = this.assessmentsXmlBox.getValue();
var view = this; var view = this;
this.server.updateXml(xml).done(function() { this.server.updateEditorContext(prompt, rubricXml, title, sub_start, sub_due, assessmentsXml).done(function () {
// Notify the client-side runtime that we finished saving // Notify the client-side runtime that we finished saving
// so it can hide the "Saving..." notification. // so it can hide the "Saving..." notification.
view.runtime.notify('save', {state: 'end'}); view.runtime.notify('save', {state: 'end'});
// Reload the XML definition in the editor // Reload the XML definition in the editor
view.load(); view.load();
}).fail(function(msg) { }).fail(function (msg) {
view.showError(msg); view.showError(msg);
}); });
}, },
/** /**
Cancel editing. Cancel editing.
**/ **/
cancel: function() { cancel: function () {
// Notify the client-side runtime so it will close the editing modal. // Notify the client-side runtime so it will close the editing modal.
this.runtime.notify('cancel', {}); this.runtime.notify('cancel', {});
}, },
/** /**
Display an error message to the user. Display an error message to the user.
Args: Args:
errorMsg (string): The error message to display. errorMsg (string): The error message to display.
**/ **/
showError: function(errorMsg) { showError: function (errorMsg) {
this.runtime.notify('error', {msg: errorMsg}); this.runtime.notify('error', {msg: errorMsg});
} }
}; };
...@@ -130,11 +170,11 @@ OpenAssessment.StudioView.prototype = { ...@@ -130,11 +170,11 @@ OpenAssessment.StudioView.prototype = {
function OpenAssessmentEditor(runtime, element) { function OpenAssessmentEditor(runtime, element) {
/** /**
Initialize the editing interface on page load. Initialize the editing interface on page load.
**/ **/
$(function($) { $(function ($) {
var server = new OpenAssessment.Server(runtime, element); var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.StudioView(runtime, element, server); var view = new OpenAssessment.StudioView(runtime, element, server);
view.load(); view.load();
}); });
} };
\ No newline at end of file
...@@ -66,15 +66,15 @@ OpenAssessment.Server.prototype = { ...@@ -66,15 +66,15 @@ OpenAssessment.Server.prototype = {
continue grading peers. continue grading peers.
Returns: Returns:
A JQuery promise, which resolves with the HTML of the rendered peer A JQuery promise, which resolves with the HTML of the rendered peer
assessment section or fails with an error message. assessment section or fails with an error message.
Example: Example:
server.render_continued_peer().done( server.render_continued_peer().done(
function(html) { console.log(html); } function(html) { console.log(html); }
).fail( ).fail(
function(err) { console.log(err); } function(err) { console.log(err); }
) )
**/ **/
renderContinuedPeer: function() { renderContinuedPeer: function() {
var url = this.url('render_peer_assessment'); var url = this.url('render_peer_assessment');
...@@ -350,13 +350,13 @@ OpenAssessment.Server.prototype = { ...@@ -350,13 +350,13 @@ OpenAssessment.Server.prototype = {
function(err) { console.log(err); } function(err) { console.log(err); }
); );
**/ **/
loadXml: function() { loadEditorContext: function() {
var url = this.url('xml'); var url = this.url('editor_context');
return $.Deferred(function(defer) { return $.Deferred(function(defer) {
$.ajax({ $.ajax({
type: "POST", url: url, data: "\"\"" type: "POST", url: url, data: "\"\""
}).done(function(data) { }).done(function(data) {
if (data.success) { defer.resolveWith(this, [data.xml]); } if (data.success) { defer.resolveWith(this, [data.prompt, data.rubric, data.settings]); }
else { defer.rejectWith(this, [data.msg]); } else { defer.rejectWith(this, [data.msg]); }
}).fail(function(data) { }).fail(function(data) {
defer.rejectWith(this, [gettext('This problem could not be loaded.')]); defer.rejectWith(this, [gettext('This problem could not be loaded.')]);
...@@ -378,9 +378,15 @@ OpenAssessment.Server.prototype = { ...@@ -378,9 +378,15 @@ OpenAssessment.Server.prototype = {
function(err) { console.log(err); } function(err) { console.log(err); }
); );
**/ **/
updateXml: function(xml) { updateEditorContext: function(prompt, rubricXml, title, sub_start, sub_due, assessmentsXml) {
var url = this.url('update_xml'); var url = this.url('update_editor_context');
var payload = JSON.stringify({xml: xml}); var settings = {
'title': title,
'submission_start': sub_start,
'submission_due': sub_due,
'assessments': assessmentsXml
};
var payload = JSON.stringify({'prompt': prompt, 'rubric': rubricXml, 'settings': settings});
return $.Deferred(function(defer) { return $.Deferred(function(defer) {
$.ajax({ $.ajax({
type: "POST", url: url, data: payload type: "POST", url: url, data: payload
......
...@@ -57,8 +57,8 @@ ...@@ -57,8 +57,8 @@
} }
.openassessment__student-info_list { .openassessment__student-info_list {
list-style-type: none; list-style-type: none;
} }
.value { .value {
width: $max-width/2; width: $max-width/2;
...@@ -165,6 +165,74 @@ ...@@ -165,6 +165,74 @@
} }
} }
} }
}
// --------------------
// Developer Styles for Studio Editing of OA problems
// --------------------
#openassessment-editor {
.openassessment-editor-content-and-tabs {
width: 100%;
height: 370px;
}
.openassessment-editor-header{
background-color: #e5e5e5;
width: 100%;
top: 0;
}
#oa-editor-window-title{
float: left;
}
.oa-editor-tab{
float: right;
padding: ($baseline-v/8) ($baseline-h/8);
margin: ($baseline-v/8) ($baseline-h/8);
}
.oa-editor-content-wrapper {
height: 100%;
width: 100%;
padding: ($baseline-v/4) ($baseline-h/4);
}
.openassessment-prompt-editor {
width: 100%;
height: 100%;
resize: none;
}
.openassessment-rubric-editor {
width: 100%;
height: 100%;
}
.openassessment-assessments-editor {
width: 100%;
}
#oa-settings-editor-text-fields {
float: left;
width: 30%;
}
#oa-settings-assessments{
float: right;
width: 70%;
height: 100%;
}
.xblock-actions {
background-color: #e5e5e5;
position: absolute;
width: 100%;
bottom: 0;
}
}
.modal-content {
height: 500px !important;
} }
...@@ -63,15 +63,15 @@ class StudioMixin(object): ...@@ -63,15 +63,15 @@ class StudioMixin(object):
return {'success': False, 'msg': _('Error updating XBlock configuration')} return {'success': False, 'msg': _('Error updating XBlock configuration')}
settings = data['settings'] settings = data['settings']
try: try:
rubric = xml.parse_rubric_xml_str(data['rubric']) rubric = xml.parse_rubric_xml_str(data['rubric'])
assessments = xml.parse_assessments_xml_str(settings['assessments']) assessments = xml.parse_assessments_xml_str(settings['assessments'])
submission_due = settings["submission_due"] submission_due = xml.parse_date(settings["submission_due"])
submission_start = xml.parse_date(settings["submission_start"])
except xml.UpdateFromXmlError as ex: except xml.UpdateFromXmlError as ex:
return {'success': False, 'msg': _('An error occurred while saving: {error}').format(error=ex)} return {'success': False, 'msg': _('An error occurred while saving: {error}').format(error=ex)}
xblock_validator = validator(self) xblock_validator = validator(self)
success, msg = xblock_validator(rubric, {'due': submission_due}, assessments) success, msg = xblock_validator(rubric, {'due': submission_due, 'start': submission_start}, assessments)
if not success: if not success:
return {'success': False, 'msg': _('Validation error: {error}').format(error=msg)} return {'success': False, 'msg': _('Validation error: {error}').format(error=msg)}
...@@ -79,8 +79,8 @@ class StudioMixin(object): ...@@ -79,8 +79,8 @@ class StudioMixin(object):
rubric['criteria'], rubric['criteria'],
rubric['feedbackprompt'], rubric['feedbackprompt'],
assessments, assessments,
settings["submission_due"], submission_due,
settings["submission_start"], submission_start,
settings["title"], settings["title"],
data["prompt"] data["prompt"]
) )
...@@ -117,9 +117,14 @@ class StudioMixin(object): ...@@ -117,9 +117,14 @@ class StudioMixin(object):
# Populates the context for the assessments section of the editing # Populates the context for the assessments section of the editing
# panel. This will adjust according to the fields laid out in this # panel. This will adjust according to the fields laid out in this
# section. # section.
submission_due = self.submission_due if self.submission_due else ''
submission_start = self.submission_start if self.submission_start else ''
settings = { settings = {
'submission_due': self.submission_due, 'submission_due': submission_due,
'submission_start': self.submission_start, 'submission_start': submission_start,
'title': self.title, 'title': self.title,
'assessments': assessments 'assessments': assessments
} }
......
...@@ -78,6 +78,6 @@ ...@@ -78,6 +78,6 @@
"submission_due": "2012-02-27T09:46:28", "submission_due": "2012-02-27T09:46:28",
"submission_start": "2015-02-10T09:46:28" "submission_start": "2015-02-10T09:46:28"
}, },
"expected_error": "cannot be later" "expected_error": "cannot be earlier"
} }
} }
\ No newline at end of file
...@@ -208,6 +208,7 @@ class ValidationIntegrationTest(TestCase): ...@@ -208,6 +208,7 @@ class ValidationIntegrationTest(TestCase):
} }
SUBMISSION = { SUBMISSION = {
"start": None,
"due": None "due": None
} }
......
...@@ -294,7 +294,7 @@ def validator(oa_block, strict_post_release=True): ...@@ -294,7 +294,7 @@ def validator(oa_block, strict_post_release=True):
return (False, msg) return (False, msg)
# Dates # Dates
submission_dates = [(oa_block.start, submission_dict['due'])] submission_dates = [(submission_dict['start'], submission_dict['due'])]
assessment_dates = [(asmnt['start'], asmnt['due']) for asmnt in assessments] assessment_dates = [(asmnt['start'], asmnt['due']) for asmnt in assessments]
success, msg = validate_dates(oa_block.start, oa_block.due, submission_dates + assessment_dates) success, msg = validate_dates(oa_block.start, oa_block.due, submission_dates + assessment_dates)
if not success: if not success:
......
...@@ -171,14 +171,22 @@ def parse_date(date_str): ...@@ -171,14 +171,22 @@ def parse_date(date_str):
Returns: Returns:
unicode in ISO format (without milliseconds) if the date string is unicode in ISO format (without milliseconds) if the date string is
parse-able. None if parsing fails. parse-able. None if parsing fails.
Raises:
UpdateFromXmlError
""" """
if date_str == "":
return None
try: try:
# Get the date into ISO format # Get the date into ISO format
parsed_date = dateutil.parser.parse(unicode(date_str)).replace(tzinfo=pytz.utc) parsed_date = dateutil.parser.parse(unicode(date_str)).replace(tzinfo=pytz.utc)
formatted_date = parsed_date.strftime("%Y-%m-%dT%H:%M:%S") formatted_date = parsed_date.strftime("%Y-%m-%dT%H:%M:%S")
return unicode(formatted_date) return unicode(formatted_date)
except (TypeError, ValueError): except (ValueError, TypeError):
return None msg = (
'The format for the given date ({}) is invalid. Make sure the date is formatted as YYYY-MM-DDTHH:MM:SS.'
).format(date_str)
raise UpdateFromXmlError(_(msg))
def _parse_options_xml(options_root): def _parse_options_xml(options_root):
...@@ -642,16 +650,12 @@ def parse_from_xml(root): ...@@ -642,16 +650,12 @@ def parse_from_xml(root):
submission_start = None submission_start = None
if 'submission_start' in root.attrib: if 'submission_start' in root.attrib:
submission_start = parse_date(unicode(root.attrib['submission_start'])) submission_start = parse_date(unicode(root.attrib['submission_start']))
if submission_start is None:
raise UpdateFromXmlError(_('The format for the submission start date is invalid. Make sure the date is formatted as YYYY-MM-DDTHH:MM:SS.'))
# Retrieve the due date for the submission # Retrieve the due date for the submission
# Set it to None by default; we will update it to the earliest deadline later on # Set it to None by default; we will update it to the earliest deadline later on
submission_due = None submission_due = None
if 'submission_due' in root.attrib: if 'submission_due' in root.attrib:
submission_due = parse_date(unicode(root.attrib['submission_due'])) submission_due = parse_date(unicode(root.attrib['submission_due']))
if submission_due is None:
raise UpdateFromXmlError(_('The format for the submission due date is invalid. Make sure the date is formatted as YYYY-MM-DDTHH:MM:SS.'))
# Retrieve the title # Retrieve the title
title_el = root.find('title') title_el = root.find('title')
...@@ -760,6 +764,7 @@ def parse_examples_xml_str(xml): ...@@ -760,6 +764,7 @@ def parse_examples_xml_str(xml):
UpdateFromXmlError: The XML definition is invalid. UpdateFromXmlError: The XML definition is invalid.
""" """
xml = u"<data>" + xml + u"</data>" xml = u"<data>" + xml + u"</data>"
return parse_examples_xml(list(_unicode_to_xml(xml))) return parse_examples_xml(list(_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