Commit c3161007 by gradyward

Added UI functionality to allow Three Tab OA Creation

parent 9355cc55
...@@ -7,3 +7,4 @@ Brian Talbot <btalbot@edx.org> ...@@ -7,3 +7,4 @@ Brian Talbot <btalbot@edx.org>
Mark Hoeber <hoeber@edx.org> 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>
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" >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>Prompt Editor</h2>
<textarea class="openassessment-prompt-editor"></textarea>
</div>
<div id="oa-rubric-editor-wrapper" class="oa-editor-content-wrapper">
<h2>Rubric Editor</h2>
<textarea class="openassessment-rubric-editor"></textarea>
</div>
<div id="oa-settings-editor-wrapper" class="oa-editor-content-wrapper">
<div id="oa-settings-editor-text-fields">
<h2>Settings Editor</h2>
<h2>Title:</h2>
<input type="text" name="title" class="openassessment-title-editor">
<h2>Submission Start Date: </h2>
<input type="text" name="start_date" class="openassessment-submission-start-editor">
<h2>Submission Due Date:</h2>
<input type="text" name="due_date" class="openassessment-submission-due-editor">
</div>
<div id="oa-settings-assessments">
XML For Assessments:
<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>
......
...@@ -22,7 +22,7 @@ from openassessment.xblock.lms_mixin import LmsCompatibilityMixin ...@@ -22,7 +22,7 @@ from openassessment.xblock.lms_mixin import LmsCompatibilityMixin
from openassessment.xblock.self_assessment_mixin import SelfAssessmentMixin from openassessment.xblock.self_assessment_mixin import SelfAssessmentMixin
from openassessment.xblock.submission_mixin import SubmissionMixin from openassessment.xblock.submission_mixin import SubmissionMixin
from openassessment.xblock.studio_mixin import StudioMixin from openassessment.xblock.studio_mixin import StudioMixin
from openassessment.xblock.xml import update_from_xml, serialize_content_to_xml from openassessment.xblock.xml import serialize_content_to_xml
from openassessment.xblock.staff_info_mixin import StaffInfoMixin from openassessment.xblock.staff_info_mixin import StaffInfoMixin
from openassessment.xblock.workflow_mixin import WorkflowMixin from openassessment.xblock.workflow_mixin import WorkflowMixin
from openassessment.workflow import api as workflow_api from openassessment.workflow import api as workflow_api
......
...@@ -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;
......
/** /**
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:
OpenAssessment.StudioView
**/
Returns:
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), this.promptBox = $('.openassessment-prompt-editor').first().get(0);
this.rubricXmlBox = CodeMirror.fromTextArea(
$(element).find('.openassessment-rubric-editor').first().get(0),
{mode: "xml", lineNumbers: true, lineWrapping: true}
);
this.titleField = $(element).find('.openassessment-title-editor');
this.submissionStartField = $(element).find('.openassessment-submission-start-editor').first().get(0);
this.submissionDueField = $(element).find('.openassessment-submission-due-editor').first().get(0);
this.assessmentsXmlBox = CodeMirror.fromTextArea(
$(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( $(element).find('.openassessment-save-button').click(
function(eventData) { function (eventData) {
view.save(); view.save();
}); });
$(element).find('.openassessment-cancel-button').click( $(element).find('.openassessment-cancel-button').click(
function(eventData) { function (eventData) {
view.cancel(); view.cancel();
}); });
};
$('.openassessment-editor-content-and-tabs').tabs();
};
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.loadXml().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.updateXml, view));
}
else {
view.updateXml();
}
} }
).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() { updateXml: 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.updateXml(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 +163,11 @@ OpenAssessment.StudioView.prototype = { ...@@ -130,11 +163,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
...@@ -356,7 +356,7 @@ OpenAssessment.Server.prototype = { ...@@ -356,7 +356,7 @@ OpenAssessment.Server.prototype = {
$.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) { updateXml: function(prompt, rubricXml, title, sub_start, sub_due, assessmentsXml) {
var url = this.url('update_xml'); var url = this.url('update_xml');
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;
} }
...@@ -8,7 +8,11 @@ from django.template.loader import get_template ...@@ -8,7 +8,11 @@ from django.template.loader import get_template
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fragment import Fragment from xblock.fragment import Fragment
from openassessment.xblock.xml import serialize_content, update_from_xml_str, ValidationError, UpdateFromXmlError from openassessment.xblock.xml import(
serialize_content, parse_rubric_xml, parse_assessments_xml, UpdateFromXmlError, serialize_rubric_to_xml_str,
serialize_assessments_to_xml_str
)
from openassessment.xblock.validation import validator from openassessment.xblock.validation import validator
...@@ -51,21 +55,52 @@ class StudioMixin(object): ...@@ -51,21 +55,52 @@ class StudioMixin(object):
Returns: Returns:
dict with keys 'success' (bool) and 'msg' (str) dict with keys 'success' (bool) and 'msg' (str)
""" """
if 'xml' in data: if 'rubric' not in data:
try: return {'success': False, 'msg': _('Must specify "rubric" in request JSON dict.')}
update_from_xml_str(self, data['xml'], validator=validator(self))
except ValidationError as ex: if 'settings' not in data:
return {'success': False, 'msg': _('Validation error: {error}').format(error=ex)} return {'success': False, 'msg': _('Must specify "settings" in request JSON dict.')}
except UpdateFromXmlError as ex: if 'prompt' not in data:
return {'success': False, 'msg': _('An error occurred while saving: {error}').format(error=ex)} return {'success': False, 'msg': _('Must specify "prompt" in request JSON dict.')}
else: settings = data['settings']
return {'success': True, 'msg': _('Successfully updated OpenAssessment XBlock')} try:
rubric = parse_rubric_xml(data['rubric'])
assessments = parse_assessments_xml(settings['assessments'])
submission_due = settings["submission_due"]
except 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)
if not success:
return {'success': False, 'msg': _('Validation error: {error}').format(error=msg)}
self.update(
rubric,
assessments,
settings["submission_due"],
settings["submission_start"],
settings["title"],
data['prompt']
)
return {'success': True, 'msg': _('Successfully updated OpenAssessment XBlock')}
def update(self, rubric, assessments, submission_due, submission_start, title, prompt):
"""
Given a dictionary of properties, update the XBlock
else: """
return {'success': False, 'msg': _('Must specify "xml" in request JSON dict.')} # If we've gotten this far, then we've successfully parsed the XML
# and validated the contents. At long last, we can safely update the XBlock.
self.title = title
self.prompt = prompt
self.rubric_criteria = rubric['criteria']
self.rubric_assessments = assessments
self.rubric_feedback_prompt = rubric['feedback_prompt']
self.submission_start = submission_start
self.submission_due = submission_due
@XBlock.json_handler @XBlock.json_handler
def xml(self, data, suffix=''): def xml(self, data, suffix=''):
...@@ -82,8 +117,14 @@ class StudioMixin(object): ...@@ -82,8 +117,14 @@ class StudioMixin(object):
dict with keys 'success' (bool), 'message' (unicode), and 'xml' (unicode) dict with keys 'success' (bool), 'message' (unicode), and 'xml' (unicode)
""" """
try: try:
xml = serialize_content(self) rubric = serialize_rubric_to_xml_str(self)
prompt = self.prompt
settings = {
'title': self.title,
'submission_start': self.submission_start,
'submission_due': self.submission_due,
'assessments': serialize_assessments_to_xml_str(self)
}
# We do not expect `serialize_content` to raise an exception, # We do not expect `serialize_content` to raise an exception,
# but if it does, handle it gracefully. # but if it does, handle it gracefully.
except Exception as ex: except Exception as ex:
...@@ -91,7 +132,7 @@ class StudioMixin(object): ...@@ -91,7 +132,7 @@ class StudioMixin(object):
logger.error(msg) logger.error(msg)
return {'success': False, 'msg': msg, 'xml': u''} return {'success': False, 'msg': msg, 'xml': u''}
else: else:
return {'success': True, 'msg': '', 'xml': xml} return {'success': True, 'msg': '', 'prompt': prompt, 'rubric': rubric, 'settings': settings}
@XBlock.json_handler @XBlock.json_handler
def check_released(self, data, suffix=''): def check_released(self, data, suffix=''):
...@@ -112,4 +153,4 @@ class StudioMixin(object): ...@@ -112,4 +153,4 @@ class StudioMixin(object):
return { return {
'success': True, 'msg': u'', 'success': True, 'msg': u'',
'is_released': self.is_released() 'is_released': self.is_released()
} }
\ No newline at end of file
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