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>
Sylvia Pearce <spearce@edx.org>
Ned Batchelder <ned@nedbatchelder.com>
David Baumgold <david@davidbaumgold.com>
Grady Ward <gward@brandeis.edu>
\ No newline at end of file
{% load i18n %}
<div id="openassessment-edit" class="editor-with-buttons">
<textarea class="openassessment-editor"></textarea>
<div id="openassessment-editor" class="editor-with-buttons editor-with-tabs">
<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">
<h3 class="sr">Actions</h3>
<ul>
......
......@@ -334,7 +334,7 @@ class OpenAssessmentBlock(
xblock_validator = validator(block, strict_post_release=False)
xblock_validator(
rubric,
{'due': config['submission_due']},
{ 'due': config['submission_due'], 'start': config['submission_start']},
config['rubric_assessments']
)
......
......@@ -2133,6 +2133,48 @@ 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; }
.modal-content {
height: 500px !important; }
.openassessment .self-assessment__display__header, .openassessment .peer-assessment__display__header, .openassessment .step__header {
margin-bottom: 0 !important;
border-radius: 0 !important;
......
......@@ -12,18 +12,32 @@ describe("OpenAssessment.StudioView", function() {
var StubServer = function() {
this.loadError = false;
this.updateError = false;
this.xml = '<openassessment></openassessment>';
this.promptBox = "";
this.rubricXmlBox = "";
this.titleField = "";
this.submissionStartField = "";
this.submissionDueField = "";
this.assessmentsXmlBox = "";
this.isReleased = false;
this.errorPromise = $.Deferred(function(defer) {
defer.rejectWith(this, ['Test error']);
}).promise();
this.loadXml = function() {
var xml = this.xml;
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
};
if (!this.loadError) {
return $.Deferred(function(defer) {
defer.resolveWith(this, [xml]);
defer.resolveWith(this, [prompt, rubric, settings]);
}).promise();
}
else {
......@@ -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) {
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) {
defer.resolve();
}).promise();
......@@ -67,32 +86,37 @@ describe("OpenAssessment.StudioView", function() {
spyOn(runtime, 'notify');
// Create the object under test
var el = $('#openassessment-edit').get(0);
var el = $('#openassessment-editor').get(0);
view = new OpenAssessment.StudioView(runtime, el, server);
});
it("loads the XML definition", function() {
it("loads the editor context definition", function() {
// Initialize the view
view.load();
// Expect that the XML definition was loaded
var contents = view.codeBox.getValue();
expect(contents).toEqual('<openassessment></openassessment>');
// 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 XML definition", function() {
// Update the XML
view.codeBox.setValue('<openassessment>test!</openassessment>');
it("saves the Editor Context definition", function() {
// Update the Context
view.titleField.value = 'THIS IS THE NEW TITLE';
// Save the updated XML
// Save the updated editor definition
view.save();
// Expect the saving notification to start/end
expect(runtime.notify).toHaveBeenCalledWith('save', {state: 'start'});
expect(runtime.notify).toHaveBeenCalledWith('save', {state: 'end'});
// Expect the server's XML to have been updated
expect(server.xml).toEqual('<openassessment>test!</openassessment>');
// Expect the server's context to have been updated
expect(server.titleField).toEqual('THIS IS THE NEW TITLE');
});
it("confirms changes for a released problem", function() {
......@@ -104,7 +128,7 @@ describe("OpenAssessment.StudioView", function() {
function(onConfirm) { onConfirm(); }
);
// Save the updated XML
// Save the updated context
view.save();
// Verify that the user was asked to confirm the changes
......@@ -124,8 +148,7 @@ describe("OpenAssessment.StudioView", function() {
it("displays an error when server reports an update XML error", function() {
server.updateError = true;
view.save('<openassessment>test!</openassessment>');
view.save();
expect(runtime.notify).toHaveBeenCalledWith('error', {msg: 'Test error'});
});
});
......@@ -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() {
// Create the server
// Since the runtime is a stub implementation that ignores the element passed to it,
......@@ -150,27 +183,35 @@ describe("OpenAssessment.Server", function() {
});
});
it("loads the XBlock's XML definition", function() {
stubAjax(true, { success: true, xml: "<openassessment />" });
it("loads the XBlock's Context definition", function() {
stubAjax(true, { success: true, prompt: PROMPT, rubric: RUBRIC, settings: SETTINGS});
var loadedXml = "";
server.loadXml().done(function(xml) {
loadedXml = xml;
var loadedPrompt = "";
var loadedRubric = "";
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({
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 });
server.updateXml('<openassessment />');
server.updateEditorContext(
PROMPT, RUBRIC, SETTINGS.title, SETTINGS.submission_start, SETTINGS.submission_due, SETTINGS.assessments
);
expect($.ajax).toHaveBeenCalledWith({
url: '/update_xml', type: "POST",
data: JSON.stringify({xml: '<openassessment />'})
type: "POST", url: '/update_editor_context',
data: JSON.stringify({prompt: PROMPT, rubric: RUBRIC, settings: SETTINGS})
});
});
......@@ -252,44 +293,45 @@ describe("OpenAssessment.Server", function() {
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);
var receivedMsg = null;
server.loadXml().fail(function(msg) {
server.loadEditorContext().fail(function(msg) {
receivedMsg = msg;
});
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);
var receivedMsg = null;
server.updateXml('test').fail(function(msg) {
server.updateEditorContext('prompt', 'rubric', 'title', 'start', 'due', 'assessments').fail(function(msg) {
receivedMsg = msg;
});
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" });
var receivedMsg = null;
server.updateXml('test').fail(function(msg) {
server.updateEditorContext('prompt', 'rubric', 'title', 'start', 'due', 'assessments').fail(function(msg) {
receivedMsg = msg;
});
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" });
var receivedMsg = null;
server.loadXml().fail(function(msg) {
server.loadEditorContext().fail(function(msg) {
receivedMsg = msg;
});
......
/**
Interface for editing view in Studio.
The constructor initializes the DOM for editing.
Interface for editing view in Studio.
The constructor initializes the DOM for editing.
Args:
Args:
runtime (Runtime): an XBlock runtime instance.
element (DOM element): The DOM element representing this XBlock.
server (OpenAssessment.Server): The interface to the XBlock server.
Returns:
Returns:
OpenAssessment.StudioView
**/
**/
OpenAssessment.StudioView = function(runtime, element, server) {
this.runtime = runtime;
this.server = server;
// 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}
);
// Install click handlers
var view = this;
$(element).find('.openassessment-save-button').click(
function(eventData) {
live_element.find('.openassessment-save-button').click(
function (eventData) {
view.save();
});
$(element).find('.openassessment-cancel-button').click(
function(eventData) {
live_element.find('.openassessment-cancel-button').click(
function (eventData) {
view.cancel();
});
};
live_element.find('.openassessment-editor-content-and-tabs').tabs({
activate: function (event, ui){
view.rubricXmlBox.refresh();
view.assessmentsXmlBox.refresh();
}
});
};
OpenAssessment.StudioView.prototype = {
/**
Load the XBlock XML definition from the server and display it in the view.
**/
load: function() {
load: function () {
var view = this;
this.server.loadXml().done(
function(xml) {
view.codeBox.setValue(xml);
}).fail(function(msg) {
this.server.loadEditorContext().done(
function (prompt, rubricXml, settings) {
view.rubricXmlBox.setValue(rubricXml);
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);
}
);
......@@ -54,17 +82,21 @@ OpenAssessment.StudioView.prototype = {
Save the problem's XML definition to the server.
If the problem has been released, make the user confirm the save.
**/
save: function() {
save: function () {
var view = this;
// Check whether the problem has been released; if not,
// warn the user and allow them to cancel.
this.server.checkReleased().done(
function(isReleased) {
if (isReleased) { view.confirmPostReleaseUpdate($.proxy(view.updateXml, view)); }
else { view.updateXml(); }
function (isReleased) {
if (isReleased) {
view.confirmPostReleaseUpdate($.proxy(view.updateEditorContext, view));
}
).fail(function(errMsg) {
else {
view.updateEditorContext();
}
}
).fail(function (errMsg) {
view.showError(msg);
});
},
......@@ -77,31 +109,39 @@ OpenAssessment.StudioView.prototype = {
onConfirm (function): A function that accepts no arguments,
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.");
// TODO: classier confirm dialog
if (confirm(msg)) { onConfirm(); }
if (confirm(msg)) {
onConfirm();
}
},
/**
Save the updated XML definition to the server.
**/
updateXml: function() {
updateEditorContext: function () {
// Notify the client-side runtime that we are starting
// to save so it can show the "Saving..." notification
this.runtime.notify('save', {state: 'start'});
// 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;
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
// so it can hide the "Saving..." notification.
view.runtime.notify('save', {state: 'end'});
// Reload the XML definition in the editor
view.load();
}).fail(function(msg) {
}).fail(function (msg) {
view.showError(msg);
});
},
......@@ -109,7 +149,7 @@ OpenAssessment.StudioView.prototype = {
/**
Cancel editing.
**/
cancel: function() {
cancel: function () {
// Notify the client-side runtime so it will close the editing modal.
this.runtime.notify('cancel', {});
},
......@@ -120,7 +160,7 @@ OpenAssessment.StudioView.prototype = {
Args:
errorMsg (string): The error message to display.
**/
showError: function(errorMsg) {
showError: function (errorMsg) {
this.runtime.notify('error', {msg: errorMsg});
}
};
......@@ -132,9 +172,9 @@ function OpenAssessmentEditor(runtime, element) {
/**
Initialize the editing interface on page load.
**/
$(function($) {
$(function ($) {
var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.StudioView(runtime, element, server);
view.load();
});
}
};
\ No newline at end of file
......@@ -350,13 +350,13 @@ OpenAssessment.Server.prototype = {
function(err) { console.log(err); }
);
**/
loadXml: function() {
var url = this.url('xml');
loadEditorContext: function() {
var url = this.url('editor_context');
return $.Deferred(function(defer) {
$.ajax({
type: "POST", url: url, 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]); }
}).fail(function(data) {
defer.rejectWith(this, [gettext('This problem could not be loaded.')]);
......@@ -378,9 +378,15 @@ OpenAssessment.Server.prototype = {
function(err) { console.log(err); }
);
**/
updateXml: function(xml) {
var url = this.url('update_xml');
var payload = JSON.stringify({xml: xml});
updateEditorContext: function(prompt, rubricXml, title, sub_start, sub_due, assessmentsXml) {
var url = this.url('update_editor_context');
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) {
$.ajax({
type: "POST", url: url, data: payload
......
......@@ -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):
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 = settings["submission_due"]
submission_due = xml.parse_date(settings["submission_due"])
submission_start = xml.parse_date(settings["submission_start"])
except xml.UpdateFromXmlError as ex:
return {'success': False, 'msg': _('An error occurred while saving: {error}').format(error=ex)}
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:
return {'success': False, 'msg': _('Validation error: {error}').format(error=msg)}
......@@ -79,8 +79,8 @@ class StudioMixin(object):
rubric['criteria'],
rubric['feedbackprompt'],
assessments,
settings["submission_due"],
settings["submission_start"],
submission_due,
submission_start,
settings["title"],
data["prompt"]
)
......@@ -117,9 +117,14 @@ class StudioMixin(object):
# Populates the context for the assessments section of the editing
# panel. This will adjust according to the fields laid out in this
# section.
submission_due = self.submission_due if self.submission_due else ''
submission_start = self.submission_start if self.submission_start else ''
settings = {
'submission_due': self.submission_due,
'submission_start': self.submission_start,
'submission_due': submission_due,
'submission_start': submission_start,
'title': self.title,
'assessments': assessments
}
......
......@@ -78,6 +78,6 @@
"submission_due": "2012-02-27T09: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):
}
SUBMISSION = {
"start": None,
"due": None
}
......
......@@ -294,7 +294,7 @@ def validator(oa_block, strict_post_release=True):
return (False, msg)
# 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]
success, msg = validate_dates(oa_block.start, oa_block.due, submission_dates + assessment_dates)
if not success:
......
......@@ -171,14 +171,22 @@ def parse_date(date_str):
Returns:
unicode in ISO format (without milliseconds) if the date string is
parse-able. None if parsing fails.
Raises:
UpdateFromXmlError
"""
if date_str == "":
return None
try:
# Get the date into ISO format
parsed_date = dateutil.parser.parse(unicode(date_str)).replace(tzinfo=pytz.utc)
formatted_date = parsed_date.strftime("%Y-%m-%dT%H:%M:%S")
return unicode(formatted_date)
except (TypeError, ValueError):
return None
except (ValueError, TypeError):
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):
......@@ -642,16 +650,12 @@ def parse_from_xml(root):
submission_start = None
if 'submission_start' in root.attrib:
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
# Set it to None by default; we will update it to the earliest deadline later on
submission_due = None
if 'submission_due' in root.attrib:
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
title_el = root.find('title')
......@@ -760,6 +764,7 @@ def parse_examples_xml_str(xml):
UpdateFromXmlError: The XML definition is invalid.
"""
xml = u"<data>" + xml + u"</data>"
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