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;
......
if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}OpenAssessment.BaseView=function(runtime,element,server){this.runtime=runtime;this.element=element;this.server=server;this.responseView=new OpenAssessment.ResponseView(this.element,this.server,this);this.trainingView=new OpenAssessment.StudentTrainingView(this.element,this.server,this);this.selfView=new OpenAssessment.SelfView(this.element,this.server,this);this.peerView=new OpenAssessment.PeerView(this.element,this.server,this);this.gradeView=new OpenAssessment.GradeView(this.element,this.server,this);this.messageView=new OpenAssessment.MessageView(this.element,this.server,this);this.staffInfoView=new OpenAssessment.StaffInfoView(this.element,this.server,this)};OpenAssessment.BaseView.prototype={scrollToTop:function(){if($.scrollTo instanceof Function){$(window).scrollTo($("#openassessment__steps"),800,{offset:-50})}},setUpCollapseExpand:function(parentSel,onExpand){parentSel.find(".ui-toggle-visibility__control").click(function(eventData){var sel=$(eventData.target).closest(".ui-toggle-visibility");if(sel.hasClass("is--collapsed")&&onExpand!==undefined){onExpand()}sel.toggleClass("is--collapsed")})},load:function(){this.responseView.load();this.loadAssessmentModules();this.staffInfoView.load()},loadAssessmentModules:function(){this.trainingView.load();this.peerView.load();this.selfView.load();this.gradeView.load()},loadMessageView:function(){this.messageView.load()},toggleActionError:function(type,msg){var element=this.element;var container=null;if(type=="save"){container=".response__submission__actions"}else if(type=="submit"||type=="peer"||type=="self"||type=="student-training"){container=".step__actions"}else if(type=="feedback_assess"){container=".submission__feedback__actions"}if(container===null){if(msg!==null){console.log(msg)}}else{var msgHtml=msg===null?"":msg;$(container+" .message__content",element).html("<p>"+msgHtml+"</p>");$(container,element).toggleClass("has--error",msg!==null)}},showLoadError:function(step){var container="#openassessment__"+step;$(container).toggleClass("has--error",true);$(container+" .step__status__value i").removeClass().addClass("ico icon-warning-sign");$(container+" .step__status__value .copy").html(gettext("Unable to Load"))}};function OpenAssessmentBlock(runtime,element){$(function($){var server=new OpenAssessment.Server(runtime,element);var view=new OpenAssessment.BaseView(runtime,element,server);view.load()})}OpenAssessment.StudioView=function(runtime,element,server){this.runtime=runtime;this.server=server;this.codeBox=CodeMirror.fromTextArea($(element).find(".openassessment-editor").first().get(0),{mode:"xml",lineNumbers:true,lineWrapping:true});var view=this;$(element).find(".openassessment-save-button").click(function(eventData){view.save()});$(element).find(".openassessment-cancel-button").click(function(eventData){view.cancel()})};OpenAssessment.StudioView.prototype={load:function(){var view=this;this.server.loadXml().done(function(xml){view.codeBox.setValue(xml)}).fail(function(msg){view.showError(msg)})},save:function(){var view=this;this.server.checkReleased().done(function(isReleased){if(isReleased){view.confirmPostReleaseUpdate($.proxy(view.updateXml,view))}else{view.updateXml()}}).fail(function(errMsg){view.showError(msg)})},confirmPostReleaseUpdate:function(onConfirm){var msg=gettext("This problem has already been released. Any changes will apply only to future assessments.");if(confirm(msg)){onConfirm()}},updateXml:function(){this.runtime.notify("save",{state:"start"});var xml=this.codeBox.getValue();var view=this;this.server.updateXml(xml).done(function(){view.runtime.notify("save",{state:"end"});view.load()}).fail(function(msg){view.showError(msg)})},cancel:function(){this.runtime.notify("cancel",{})},showError:function(errorMsg){this.runtime.notify("error",{msg:errorMsg})}};function OpenAssessmentEditor(runtime,element){$(function($){var server=new OpenAssessment.Server(runtime,element);var view=new OpenAssessment.StudioView(runtime,element,server);view.load()})}OpenAssessment.GradeView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.GradeView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("grade").done(function(html){$("#openassessment__grade",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){baseView.showLoadError("grade",errMsg)})},installHandlers:function(){var sel=$("#openassessment__grade",this.element);this.baseView.setUpCollapseExpand(sel);var view=this;sel.find("#feedback__submit").click(function(eventObject){eventObject.preventDefault();view.submitFeedbackOnAssessment()})},feedbackText:function(text){if(typeof text==="undefined"){return $("#feedback__remarks__value",this.element).val()}else{$("#feedback__remarks__value",this.element).val(text)}},feedbackOptions:function(options){var view=this;if(typeof options==="undefined"){return $.map($(".feedback__overall__value:checked",view.element),function(element,index){return $(element).val()})}else{$(".feedback__overall__value",this.element).prop("checked",false);$.each(options,function(index,opt){$("#feedback__overall__value--"+opt,view.element).prop("checked",true)})}},setHidden:function(sel,hidden){sel.toggleClass("is--hidden",hidden);sel.attr("aria-hidden",hidden?"true":"false")},isHidden:function(sel){return sel.hasClass("is--hidden")&&sel.attr("aria-hidden")=="true"},feedbackState:function(newState){var containerSel=$(".submission__feedback__content",this.element);var instructionsSel=containerSel.find(".submission__feedback__instructions");var fieldsSel=containerSel.find(".submission__feedback__fields");var actionsSel=containerSel.find(".submission__feedback__actions");var transitionSel=containerSel.find(".transition__status");var messageSel=containerSel.find(".message--complete");if(typeof newState==="undefined"){var isSubmitting=containerSel.hasClass("is--transitioning")&&containerSel.hasClass("is--submitting")&&!this.isHidden(transitionSel)&&this.isHidden(messageSel)&&this.isHidden(instructionsSel)&&this.isHidden(fieldsSel)&&this.isHidden(actionsSel);var hasSubmitted=containerSel.hasClass("is--submitted")&&this.isHidden(transitionSel)&&!this.isHidden(messageSel)&&this.isHidden(instructionsSel)&&this.isHidden(fieldsSel)&&this.isHidden(actionsSel);var isOpen=!containerSel.hasClass("is--submitted")&&!containerSel.hasClass("is--transitioning")&&!containerSel.hasClass("is--submitting")&&this.isHidden(transitionSel)&&this.isHidden(messageSel)&&!this.isHidden(instructionsSel)&&!this.isHidden(fieldsSel)&&!this.isHidden(actionsSel);if(isOpen){return"open"}else if(isSubmitting){return"submitting"}else if(hasSubmitted){return"submitted"}else{throw"Invalid feedback state"}}else{if(newState=="open"){containerSel.toggleClass("is--transitioning",false);containerSel.toggleClass("is--submitting",false);containerSel.toggleClass("is--submitted",false);this.setHidden(instructionsSel,false);this.setHidden(fieldsSel,false);this.setHidden(actionsSel,false);this.setHidden(transitionSel,true);this.setHidden(messageSel,true)}else if(newState=="submitting"){containerSel.toggleClass("is--transitioning",true);containerSel.toggleClass("is--submitting",true);containerSel.toggleClass("is--submitted",false);this.setHidden(instructionsSel,true);this.setHidden(fieldsSel,true);this.setHidden(actionsSel,true);this.setHidden(transitionSel,false);this.setHidden(messageSel,true)}else if(newState=="submitted"){containerSel.toggleClass("is--transitioning",false);containerSel.toggleClass("is--submitting",false);containerSel.toggleClass("is--submitted",true);this.setHidden(instructionsSel,true);this.setHidden(fieldsSel,true);this.setHidden(actionsSel,true);this.setHidden(transitionSel,true);this.setHidden(messageSel,false)}}},submitFeedbackOnAssessment:function(){var view=this;var baseView=this.baseView;$("#feedback__submit",this.element).toggleClass("is--disabled",true);view.feedbackState("submitting");this.server.submitFeedbackOnAssessment(this.feedbackText(),this.feedbackOptions()).done(function(){view.feedbackState("submitted")}).fail(function(errMsg){baseView.toggleActionError("feedback_assess",errMsg)})}};OpenAssessment.MessageView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.MessageView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("message").done(function(html){$("#openassessment__message",view.element).replaceWith(html)}).fail(function(errMsg){baseView.showLoadError("message",errMsg)})}};OpenAssessment.PeerView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.PeerView.prototype={load:function(){var view=this;this.server.render("peer_assessment").done(function(html){$("#openassessment__peer-assessment",view.element).replaceWith(html);view.installHandlers(false)}).fail(function(errMsg){view.baseView.showLoadError("peer-assessment")});view.baseView.loadMessageView()},loadContinuedAssessment:function(){var view=this;this.server.renderContinuedPeer().done(function(html){$("#openassessment__peer-assessment",view.element).replaceWith(html);view.installHandlers(true)}).fail(function(errMsg){view.baseView.showLoadError("peer-assessment")})},installHandlers:function(isContinuedAssessment){var sel=$("#openassessment__peer-assessment",this.element);var view=this;this.baseView.setUpCollapseExpand(sel,$.proxy(view.loadContinuedAssessment,view));var rubricSelector=$("#peer-assessment--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(view.peerSubmitEnabled,view))}sel.find("#peer-assessment--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();if(!isContinuedAssessment){view.peerAssess()}else{view.continuedPeerAssess()}})},peerSubmitEnabled:function(enabled){var button=$("#peer-assessment--001__assessment__submit",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},peerAssess:function(){var view=this;var baseView=view.baseView;this.peerAssessRequest(function(){view.load();baseView.loadAssessmentModules();baseView.scrollToTop()})},continuedPeerAssess:function(){var view=this;var gradeView=this.baseView.gradeView;var baseView=view.baseView;view.peerAssessRequest(function(){view.loadContinuedAssessment();gradeView.load();baseView.scrollToTop()})},peerAssessRequest:function(successFunction){var view=this;view.baseView.toggleActionError("peer",null);view.peerSubmitEnabled(false);this.server.peerAssess(this.rubric.optionsSelected(),this.rubric.criterionFeedback(),this.overallFeedback()).done(successFunction).fail(function(errMsg){view.baseView.toggleActionError("peer",errMsg);view.peerSubmitEnabled(true)})},overallFeedback:function(overallFeedback){var selector="#assessment__rubric__question--feedback__value";if(typeof overallFeedback==="undefined"){return $(selector,this.element).val()}else{$(selector,this.element).val(overallFeedback)}}};OpenAssessment.ResponseView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.savedResponse="";this.lastChangeTime=Date.now();this.errorOnLastSave=false;this.autoSaveTimerId=null};OpenAssessment.ResponseView.prototype={AUTO_SAVE_POLL_INTERVAL:2e3,AUTO_SAVE_WAIT:3e4,load:function(){var view=this;this.server.render("submission").done(function(html){$("#openassessment__response",view.element).replaceWith(html);view.installHandlers();view.setAutoSaveEnabled(true)}).fail(function(errMsg){view.baseView.showLoadError("response")})},installHandlers:function(){var sel=$("#openassessment__response",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);this.savedResponse=this.response();var handleChange=function(eventData){view.handleResponseChanged()};sel.find("#submission__answer__value").on("change keyup drop paste",handleChange);sel.find("#step--response__submit").click(function(eventObject){eventObject.preventDefault();view.submit()});sel.find("#submission__save").click(function(eventObject){eventObject.preventDefault();view.save()})},setAutoSaveEnabled:function(enabled){if(enabled){if(this.autoSaveTimerId===null){this.autoSaveTimerId=setInterval($.proxy(this.autoSave,this),this.AUTO_SAVE_POLL_INTERVAL)}}else{if(this.autoSaveTimerId!==null){clearInterval(this.autoSaveTimerId)}}},submitEnabled:function(enabled){var sel=$("#step--response__submit",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},saveEnabled:function(enabled){var sel=$("#submission__save",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},saveStatus:function(msg){var sel=$("#response__save_status h3",this.element);if(typeof msg==="undefined"){return sel.text()}else{var label=gettext("Status of Your Response");sel.html('<span class="sr">'+label+":"+"</span>\n"+msg)}},unsavedWarningEnabled:function(enabled){if(typeof enabled==="undefined"){return window.onbeforeunload!==null}else{if(enabled){window.onbeforeunload=function(){return"If you leave this page without saving or submitting your response, "+"you'll lose any work you've done on the response."}}else{window.onbeforeunload=null}}},response:function(text){var sel=$("#submission__answer__value",this.element);if(typeof text==="undefined"){return sel.val()}else{sel.val(text)}},responseChanged:function(){var currentResponse=$.trim(this.response());var savedResponse=$.trim(this.savedResponse);return savedResponse!==currentResponse},autoSave:function(){var timeSinceLastChange=Date.now()-this.lastChangeTime;if(this.responseChanged()&&timeSinceLastChange>this.AUTO_SAVE_WAIT&&!this.errorOnLastSave){this.save()}},handleResponseChanged:function(){var isBlank=$.trim(this.response())!=="";this.submitEnabled(isBlank);if(this.responseChanged()){this.saveEnabled(isBlank);this.saveStatus(gettext("This response has not been saved."));this.unsavedWarningEnabled(true)}this.lastChangeTime=Date.now()},save:function(){this.errorOnLastSave=false;this.saveStatus(gettext("Saving..."));this.baseView.toggleActionError("save",null);this.unsavedWarningEnabled(false);var view=this;var savedResponse=this.response();this.server.save(savedResponse).done(function(){view.savedResponse=savedResponse;var currentResponse=view.response();view.submitEnabled(currentResponse!=="");if(currentResponse==savedResponse){view.saveEnabled(false);view.saveStatus(gettext("This response has been saved but not submitted."))}}).fail(function(errMsg){view.saveStatus(gettext("Error"));view.baseView.toggleActionError("save",errMsg);view.errorOnLastSave=true})},submit:function(){this.submitEnabled(false);var view=this;var baseView=this.baseView;this.confirmSubmission().pipe(function(){var submission=$("#submission__answer__value",view.element).val();baseView.toggleActionError("response",null);return view.server.submit(submission)}).done($.proxy(view.moveToNextStep,view)).fail(function(errCode,errMsg){if(errCode=="ENOMULTI"){view.moveToNextStep()}else{if(errMsg){baseView.toggleActionError("submit",errMsg)}view.submitEnabled(true)}})},moveToNextStep:function(){this.load();this.baseView.loadAssessmentModules();this.unsavedWarningEnabled(false)},confirmSubmission:function(){var msg="You're about to submit your response for this assignment. "+"After you submit this response, you can't change it or submit a new response.";return $.Deferred(function(defer){if(confirm(msg)){defer.resolve()}else{defer.reject()}})}};OpenAssessment.Rubric=function(element){this.element=element};OpenAssessment.Rubric.prototype={criterionFeedback:function(criterionFeedback){var selector="textarea.answer__value";var feedback={};$(selector,this.element).each(function(index,sel){if(typeof criterionFeedback!=="undefined"){$(sel).val(criterionFeedback[sel.name]);feedback[sel.name]=criterionFeedback[sel.name]}else{feedback[sel.name]=$(sel).val()}});return feedback},optionsSelected:function(optionsSelected){var selector="input[type=radio]";if(typeof optionsSelected==="undefined"){var options={};$(selector+":checked",this.element).each(function(index,sel){options[sel.name]=sel.value});return options}else{$(selector,this.element).prop("checked",false);$(selector,this.element).each(function(index,sel){if(optionsSelected.hasOwnProperty(sel.name)){if(sel.value==optionsSelected[sel.name]){$(sel).prop("checked",true)}}})}},canSubmitCallback:function(callback){$(this.element).change(function(){var numChecked=$("input[type=radio]:checked",this).length;var numAvailable=$(".field--radio.assessment__rubric__question",this).length;var canSubmit=numChecked==numAvailable;callback(canSubmit)})},showCorrections:function(corrections){var selector="input[type=radio]";var hasErrors=false;$(selector,this.element).each(function(index,sel){var listItem=$(sel).parents(".assessment__rubric__question");if(corrections.hasOwnProperty(sel.name)){hasErrors=true;listItem.find(".message--incorrect").removeClass("is--hidden");listItem.find(".message--correct").addClass("is--hidden")}else{listItem.find(".message--correct").removeClass("is--hidden");listItem.find(".message--incorrect").addClass("is--hidden")}});return hasErrors}};OpenAssessment.SelfView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.SelfView.prototype={load:function(){var view=this;this.server.render("self_assessment").done(function(html){$("#openassessment__self-assessment",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){view.showLoadError("self-assessment")})},installHandlers:function(){var view=this;var sel=$("#openassessment__self-assessment",view.element);this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#self-assessment--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(this.selfSubmitEnabled,this))}sel.find("#self-assessment--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();view.selfAssess()})},selfSubmitEnabled:function(enabled){var button=$("#self-assessment--001__assessment__submit",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},selfAssess:function(){var view=this;var baseView=this.baseView;baseView.toggleActionError("self",null);view.selfSubmitEnabled(false);var options=this.rubric.optionsSelected();this.server.selfAssess(options).done(function(){baseView.loadAssessmentModules();baseView.scrollToTop()}).fail(function(errMsg){baseView.toggleActionError("self",errMsg);view.selfSubmitEnabled(true)})}};OpenAssessment.Server=function(runtime,element){this.runtime=runtime;this.element=element};OpenAssessment.Server.prototype={url:function(handler){return this.runtime.handlerUrl(this.element,handler)},render:function(component){var url=this.url("render_"+component);return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html"}).done(function(data){defer.resolveWith(this,[data])}).fail(function(data){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},renderContinuedPeer:function(){var url=this.url("render_peer_assessment");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{continue_grading:true}}).done(function(data){defer.resolveWith(this,[data])}).fail(function(data){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},studentInfo:function(student_id){var url=this.url("render_student_info");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{student_id:student_id}}).done(function(data){defer.resolveWith(this,[data])}).fail(function(data){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},submit:function(submission){var url=this.url("submit");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({submission:submission})}).done(function(data){var success=data[0];if(success){var studentId=data[1];var attemptNum=data[2];defer.resolveWith(this,[studentId,attemptNum])}else{var errorNum=data[1];var errorMsg=data[2];defer.rejectWith(this,[errorNum,errorMsg])}}).fail(function(data){defer.rejectWith(this,["AJAX",gettext("This response could not be submitted.")])})}).promise()},save:function(submission){var url=this.url("save_submission");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({submission:submission})}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This response could not be saved.")])})}).promise()},submitFeedbackOnAssessment:function(text,options){var url=this.url("submit_feedback");var payload=JSON.stringify({feedback_text:text,feedback_options:options});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This feedback could not be submitted.")])})}).promise()},peerAssess:function(optionsSelected,criterionFeedback,overallFeedback){var url=this.url("peer_assess");var payload=JSON.stringify({options_selected:optionsSelected,criterion_feedback:criterionFeedback,overall_feedback:overallFeedback});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})}).promise()},selfAssess:function(optionsSelected){var url=this.url("self_assess");var payload=JSON.stringify({options_selected:optionsSelected});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},trainingAssess:function(optionsSelected){var url=this.url("training_assess");var payload=JSON.stringify({options_selected:optionsSelected});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolveWith(this,[data.corrections])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},loadXml:function(){var url=this.url("xml");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.xml])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This problem could not be loaded.")])})}).promise()},updateXml:function(xml){var url=this.url("update_xml");var payload=JSON.stringify({xml:xml});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This problem could not be saved.")])})}).promise()},checkReleased:function(){var url=this.url("check_released");var payload='""';return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolveWith(this,[data.is_released])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("The server could not be contacted.")])})}).promise()}};if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}OpenAssessment.StaffInfoView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.StaffInfoView.prototype={load:function(){var view=this;if($("#openassessment__staff-info",view.element).length>0){this.server.render("staff_info").done(function(html){$("#openassessment__staff-info",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){view.baseView.showLoadError("staff_info")})}},loadStudentInfo:function(){var view=this;var sel=$("#openassessment__staff-info",this.element);var student_id=sel.find("#openassessment__student_id").val();this.server.studentInfo(student_id).done(function(html){$("#openassessment__student-info",view.element).replaceWith(html)}).fail(function(errMsg){view.showLoadError("student_info")})},installHandlers:function(){var sel=$("#openassessment__staff-info",this.element);var view=this;if(sel.length<=0){return}this.baseView.setUpCollapseExpand(sel,function(){});sel.find("#openassessment_student_info_form").submit(function(eventObject){eventObject.preventDefault();view.loadStudentInfo()});sel.find("#submit_student_id").click(function(eventObject){eventObject.preventDefault();view.loadStudentInfo()})}};OpenAssessment.StudentTrainingView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.StudentTrainingView.prototype={load:function(){var view=this;this.server.render("student_training").done(function(html){$("#openassessment__student-training",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){view.baseView.showLoadError("student-training")})},installHandlers:function(){var sel=$("#openassessment__student-training",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#student-training--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(this.assessButtonEnabled,this))}sel.find("#student-training--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();view.assess()})},assess:function(){this.assessButtonEnabled(false);var options={};if(this.rubric!==null){options=this.rubric.optionsSelected()}var view=this;var baseView=this.baseView;this.server.trainingAssess(options).done(function(corrections){var incorrect=$("#openassessment__student-training--incorrect",this.element);var instructions=$("#openassessment__student-training--instructions",this.element);if(!view.rubric.showCorrections(corrections)){view.load();baseView.loadAssessmentModules();incorrect.addClass("is--hidden");instructions.removeClass("is--hidden")}else{instructions.addClass("is--hidden");incorrect.removeClass("is--hidden")}baseView.scrollToTop()}).fail(function(errMsg){baseView.toggleActionError("student-training",errMsg);view.assessButtonEnabled(true)})},assessButtonEnabled:function(isEnabled){var button=$("#student-training--001__assessment__submit",this.element);if(typeof isEnabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!isEnabled)}}}; if (typeof OpenAssessment == "undefined" || !OpenAssessment) {
\ No newline at end of file OpenAssessment = {};
}
if (typeof window.gettext === "undefined") {
window.gettext = function(text) {
return text;
};
}
OpenAssessment.BaseView = function(runtime, element, server) {
this.runtime = runtime;
this.element = element;
this.server = server;
this.responseView = new OpenAssessment.ResponseView(this.element, this.server, this);
this.trainingView = new OpenAssessment.StudentTrainingView(this.element, this.server, this);
this.selfView = new OpenAssessment.SelfView(this.element, this.server, this);
this.peerView = new OpenAssessment.PeerView(this.element, this.server, this);
this.gradeView = new OpenAssessment.GradeView(this.element, this.server, this);
this.messageView = new OpenAssessment.MessageView(this.element, this.server, this);
this.staffInfoView = new OpenAssessment.StaffInfoView(this.element, this.server, this);
};
OpenAssessment.BaseView.prototype = {
scrollToTop: function() {
if ($.scrollTo instanceof Function) {
$(window).scrollTo($("#openassessment__steps"), 800, {
offset: -50
});
}
},
setUpCollapseExpand: function(parentSel, onExpand) {
parentSel.find(".ui-toggle-visibility__control").click(function(eventData) {
var sel = $(eventData.target).closest(".ui-toggle-visibility");
if (sel.hasClass("is--collapsed") && onExpand !== undefined) {
onExpand();
}
sel.toggleClass("is--collapsed");
});
},
load: function() {
this.responseView.load();
this.loadAssessmentModules();
this.staffInfoView.load();
},
loadAssessmentModules: function() {
this.trainingView.load();
this.peerView.load();
this.selfView.load();
this.gradeView.load();
},
loadMessageView: function() {
this.messageView.load();
},
toggleActionError: function(type, msg) {
var element = this.element;
var container = null;
if (type == "save") {
container = ".response__submission__actions";
} else if (type == "submit" || type == "peer" || type == "self" || type == "student-training") {
container = ".step__actions";
} else if (type == "feedback_assess") {
container = ".submission__feedback__actions";
}
if (container === null) {
if (msg !== null) {
console.log(msg);
}
} else {
var msgHtml = msg === null ? "" : msg;
$(container + " .message__content", element).html("<p>" + msgHtml + "</p>");
$(container, element).toggleClass("has--error", msg !== null);
}
},
showLoadError: function(step) {
var container = "#openassessment__" + step;
$(container).toggleClass("has--error", true);
$(container + " .step__status__value i").removeClass().addClass("ico icon-warning-sign");
$(container + " .step__status__value .copy").html(gettext("Unable to Load"));
}
};
function OpenAssessmentBlock(runtime, element) {
$(function($) {
var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.BaseView(runtime, element, server);
view.load();
});
}
OpenAssessment.StudioView = function(runtime, element, server) {
this.runtime = runtime;
this.server = server;
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
});
var view = this;
$(element).find(".openassessment-save-button").click(function(eventData) {
view.save();
});
$(element).find(".openassessment-cancel-button").click(function(eventData) {
view.cancel();
});
$(".openassessment-editor-content-and-tabs").tabs();
};
OpenAssessment.StudioView.prototype = {
load: function() {
var view = this;
this.server.loadXml().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);
});
},
save: function() {
var view = this;
this.server.checkReleased().done(function(isReleased) {
if (isReleased) {
view.confirmPostReleaseUpdate($.proxy(view.updateXml, view));
} else {
view.updateXml();
}
}).fail(function(errMsg) {
view.showError(msg);
});
},
confirmPostReleaseUpdate: function(onConfirm) {
var msg = gettext("This problem has already been released. Any changes will apply only to future assessments.");
if (confirm(msg)) {
onConfirm();
}
},
updateXml: function() {
this.runtime.notify("save", {
state: "start"
});
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(prompt, rubricXml, title, sub_start, sub_due, assessmentsXml).done(function() {
view.runtime.notify("save", {
state: "end"
});
view.load();
}).fail(function(msg) {
view.showError(msg);
});
},
cancel: function() {
this.runtime.notify("cancel", {});
},
showError: function(errorMsg) {
this.runtime.notify("error", {
msg: errorMsg
});
}
};
function OpenAssessmentEditor(runtime, element) {
$(function($) {
var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.StudioView(runtime, element, server);
view.load();
});
}
OpenAssessment.GradeView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
};
OpenAssessment.GradeView.prototype = {
load: function() {
var view = this;
var baseView = this.baseView;
this.server.render("grade").done(function(html) {
$("#openassessment__grade", view.element).replaceWith(html);
view.installHandlers();
}).fail(function(errMsg) {
baseView.showLoadError("grade", errMsg);
});
},
installHandlers: function() {
var sel = $("#openassessment__grade", this.element);
this.baseView.setUpCollapseExpand(sel);
var view = this;
sel.find("#feedback__submit").click(function(eventObject) {
eventObject.preventDefault();
view.submitFeedbackOnAssessment();
});
},
feedbackText: function(text) {
if (typeof text === "undefined") {
return $("#feedback__remarks__value", this.element).val();
} else {
$("#feedback__remarks__value", this.element).val(text);
}
},
feedbackOptions: function(options) {
var view = this;
if (typeof options === "undefined") {
return $.map($(".feedback__overall__value:checked", view.element), function(element, index) {
return $(element).val();
});
} else {
$(".feedback__overall__value", this.element).prop("checked", false);
$.each(options, function(index, opt) {
$("#feedback__overall__value--" + opt, view.element).prop("checked", true);
});
}
},
setHidden: function(sel, hidden) {
sel.toggleClass("is--hidden", hidden);
sel.attr("aria-hidden", hidden ? "true" : "false");
},
isHidden: function(sel) {
return sel.hasClass("is--hidden") && sel.attr("aria-hidden") == "true";
},
feedbackState: function(newState) {
var containerSel = $(".submission__feedback__content", this.element);
var instructionsSel = containerSel.find(".submission__feedback__instructions");
var fieldsSel = containerSel.find(".submission__feedback__fields");
var actionsSel = containerSel.find(".submission__feedback__actions");
var transitionSel = containerSel.find(".transition__status");
var messageSel = containerSel.find(".message--complete");
if (typeof newState === "undefined") {
var isSubmitting = containerSel.hasClass("is--transitioning") && containerSel.hasClass("is--submitting") && !this.isHidden(transitionSel) && this.isHidden(messageSel) && this.isHidden(instructionsSel) && this.isHidden(fieldsSel) && this.isHidden(actionsSel);
var hasSubmitted = containerSel.hasClass("is--submitted") && this.isHidden(transitionSel) && !this.isHidden(messageSel) && this.isHidden(instructionsSel) && this.isHidden(fieldsSel) && this.isHidden(actionsSel);
var isOpen = !containerSel.hasClass("is--submitted") && !containerSel.hasClass("is--transitioning") && !containerSel.hasClass("is--submitting") && this.isHidden(transitionSel) && this.isHidden(messageSel) && !this.isHidden(instructionsSel) && !this.isHidden(fieldsSel) && !this.isHidden(actionsSel);
if (isOpen) {
return "open";
} else if (isSubmitting) {
return "submitting";
} else if (hasSubmitted) {
return "submitted";
} else {
throw "Invalid feedback state";
}
} else {
if (newState == "open") {
containerSel.toggleClass("is--transitioning", false);
containerSel.toggleClass("is--submitting", false);
containerSel.toggleClass("is--submitted", false);
this.setHidden(instructionsSel, false);
this.setHidden(fieldsSel, false);
this.setHidden(actionsSel, false);
this.setHidden(transitionSel, true);
this.setHidden(messageSel, true);
} else if (newState == "submitting") {
containerSel.toggleClass("is--transitioning", true);
containerSel.toggleClass("is--submitting", true);
containerSel.toggleClass("is--submitted", false);
this.setHidden(instructionsSel, true);
this.setHidden(fieldsSel, true);
this.setHidden(actionsSel, true);
this.setHidden(transitionSel, false);
this.setHidden(messageSel, true);
} else if (newState == "submitted") {
containerSel.toggleClass("is--transitioning", false);
containerSel.toggleClass("is--submitting", false);
containerSel.toggleClass("is--submitted", true);
this.setHidden(instructionsSel, true);
this.setHidden(fieldsSel, true);
this.setHidden(actionsSel, true);
this.setHidden(transitionSel, true);
this.setHidden(messageSel, false);
}
}
},
submitFeedbackOnAssessment: function() {
var view = this;
var baseView = this.baseView;
$("#feedback__submit", this.element).toggleClass("is--disabled", true);
view.feedbackState("submitting");
this.server.submitFeedbackOnAssessment(this.feedbackText(), this.feedbackOptions()).done(function() {
view.feedbackState("submitted");
}).fail(function(errMsg) {
baseView.toggleActionError("feedback_assess", errMsg);
});
}
};
OpenAssessment.MessageView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
};
OpenAssessment.MessageView.prototype = {
load: function() {
var view = this;
var baseView = this.baseView;
this.server.render("message").done(function(html) {
$("#openassessment__message", view.element).replaceWith(html);
}).fail(function(errMsg) {
baseView.showLoadError("message", errMsg);
});
}
};
OpenAssessment.PeerView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
this.rubric = null;
};
OpenAssessment.PeerView.prototype = {
load: function() {
var view = this;
this.server.render("peer_assessment").done(function(html) {
$("#openassessment__peer-assessment", view.element).replaceWith(html);
view.installHandlers(false);
}).fail(function(errMsg) {
view.baseView.showLoadError("peer-assessment");
});
view.baseView.loadMessageView();
},
loadContinuedAssessment: function() {
var view = this;
this.server.renderContinuedPeer().done(function(html) {
$("#openassessment__peer-assessment", view.element).replaceWith(html);
view.installHandlers(true);
}).fail(function(errMsg) {
view.baseView.showLoadError("peer-assessment");
});
},
installHandlers: function(isContinuedAssessment) {
var sel = $("#openassessment__peer-assessment", this.element);
var view = this;
this.baseView.setUpCollapseExpand(sel, $.proxy(view.loadContinuedAssessment, view));
var rubricSelector = $("#peer-assessment--001__assessment", this.element);
if (rubricSelector.size() > 0) {
var rubricElement = rubricSelector.get(0);
this.rubric = new OpenAssessment.Rubric(rubricElement);
}
if (this.rubric !== null) {
this.rubric.canSubmitCallback($.proxy(view.peerSubmitEnabled, view));
}
sel.find("#peer-assessment--001__assessment__submit").click(function(eventObject) {
eventObject.preventDefault();
if (!isContinuedAssessment) {
view.peerAssess();
} else {
view.continuedPeerAssess();
}
});
},
peerSubmitEnabled: function(enabled) {
var button = $("#peer-assessment--001__assessment__submit", this.element);
if (typeof enabled === "undefined") {
return !button.hasClass("is--disabled");
} else {
button.toggleClass("is--disabled", !enabled);
}
},
peerAssess: function() {
var view = this;
var baseView = view.baseView;
this.peerAssessRequest(function() {
view.load();
baseView.loadAssessmentModules();
baseView.scrollToTop();
});
},
continuedPeerAssess: function() {
var view = this;
var gradeView = this.baseView.gradeView;
var baseView = view.baseView;
view.peerAssessRequest(function() {
view.loadContinuedAssessment();
gradeView.load();
baseView.scrollToTop();
});
},
peerAssessRequest: function(successFunction) {
var view = this;
view.baseView.toggleActionError("peer", null);
view.peerSubmitEnabled(false);
this.server.peerAssess(this.rubric.optionsSelected(), this.rubric.criterionFeedback(), this.overallFeedback()).done(successFunction).fail(function(errMsg) {
view.baseView.toggleActionError("peer", errMsg);
view.peerSubmitEnabled(true);
});
},
overallFeedback: function(overallFeedback) {
var selector = "#assessment__rubric__question--feedback__value";
if (typeof overallFeedback === "undefined") {
return $(selector, this.element).val();
} else {
$(selector, this.element).val(overallFeedback);
}
}
};
OpenAssessment.ResponseView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
this.savedResponse = "";
this.lastChangeTime = Date.now();
this.errorOnLastSave = false;
this.autoSaveTimerId = null;
};
OpenAssessment.ResponseView.prototype = {
AUTO_SAVE_POLL_INTERVAL: 2e3,
AUTO_SAVE_WAIT: 3e4,
load: function() {
var view = this;
this.server.render("submission").done(function(html) {
$("#openassessment__response", view.element).replaceWith(html);
view.installHandlers();
view.setAutoSaveEnabled(true);
}).fail(function(errMsg) {
view.baseView.showLoadError("response");
});
},
installHandlers: function() {
var sel = $("#openassessment__response", this.element);
var view = this;
this.baseView.setUpCollapseExpand(sel);
this.savedResponse = this.response();
var handleChange = function(eventData) {
view.handleResponseChanged();
};
sel.find("#submission__answer__value").on("change keyup drop paste", handleChange);
sel.find("#step--response__submit").click(function(eventObject) {
eventObject.preventDefault();
view.submit();
});
sel.find("#submission__save").click(function(eventObject) {
eventObject.preventDefault();
view.save();
});
},
setAutoSaveEnabled: function(enabled) {
if (enabled) {
if (this.autoSaveTimerId === null) {
this.autoSaveTimerId = setInterval($.proxy(this.autoSave, this), this.AUTO_SAVE_POLL_INTERVAL);
}
} else {
if (this.autoSaveTimerId !== null) {
clearInterval(this.autoSaveTimerId);
}
}
},
submitEnabled: function(enabled) {
var sel = $("#step--response__submit", this.element);
if (typeof enabled === "undefined") {
return !sel.hasClass("is--disabled");
} else {
sel.toggleClass("is--disabled", !enabled);
}
},
saveEnabled: function(enabled) {
var sel = $("#submission__save", this.element);
if (typeof enabled === "undefined") {
return !sel.hasClass("is--disabled");
} else {
sel.toggleClass("is--disabled", !enabled);
}
},
saveStatus: function(msg) {
var sel = $("#response__save_status h3", this.element);
if (typeof msg === "undefined") {
return sel.text();
} else {
var label = gettext("Status of Your Response");
sel.html('<span class="sr">' + label + ":" + "</span>\n" + msg);
}
},
unsavedWarningEnabled: function(enabled) {
if (typeof enabled === "undefined") {
return window.onbeforeunload !== null;
} else {
if (enabled) {
window.onbeforeunload = function() {
return "If you leave this page without saving or submitting your response, " + "you'll lose any work you've done on the response.";
};
} else {
window.onbeforeunload = null;
}
}
},
response: function(text) {
var sel = $("#submission__answer__value", this.element);
if (typeof text === "undefined") {
return sel.val();
} else {
sel.val(text);
}
},
responseChanged: function() {
var currentResponse = $.trim(this.response());
var savedResponse = $.trim(this.savedResponse);
return savedResponse !== currentResponse;
},
autoSave: function() {
var timeSinceLastChange = Date.now() - this.lastChangeTime;
if (this.responseChanged() && timeSinceLastChange > this.AUTO_SAVE_WAIT && !this.errorOnLastSave) {
this.save();
}
},
handleResponseChanged: function() {
var isBlank = $.trim(this.response()) !== "";
this.submitEnabled(isBlank);
if (this.responseChanged()) {
this.saveEnabled(isBlank);
this.saveStatus(gettext("This response has not been saved."));
this.unsavedWarningEnabled(true);
}
this.lastChangeTime = Date.now();
},
save: function() {
this.errorOnLastSave = false;
this.saveStatus(gettext("Saving..."));
this.baseView.toggleActionError("save", null);
this.unsavedWarningEnabled(false);
var view = this;
var savedResponse = this.response();
this.server.save(savedResponse).done(function() {
view.savedResponse = savedResponse;
var currentResponse = view.response();
view.submitEnabled(currentResponse !== "");
if (currentResponse == savedResponse) {
view.saveEnabled(false);
view.saveStatus(gettext("This response has been saved but not submitted."));
}
}).fail(function(errMsg) {
view.saveStatus(gettext("Error"));
view.baseView.toggleActionError("save", errMsg);
view.errorOnLastSave = true;
});
},
submit: function() {
this.submitEnabled(false);
var view = this;
var baseView = this.baseView;
this.confirmSubmission().pipe(function() {
var submission = $("#submission__answer__value", view.element).val();
baseView.toggleActionError("response", null);
return view.server.submit(submission);
}).done($.proxy(view.moveToNextStep, view)).fail(function(errCode, errMsg) {
if (errCode == "ENOMULTI") {
view.moveToNextStep();
} else {
if (errMsg) {
baseView.toggleActionError("submit", errMsg);
}
view.submitEnabled(true);
}
});
},
moveToNextStep: function() {
this.load();
this.baseView.loadAssessmentModules();
this.unsavedWarningEnabled(false);
},
confirmSubmission: function() {
var msg = "You're about to submit your response for this assignment. " + "After you submit this response, you can't change it or submit a new response.";
return $.Deferred(function(defer) {
if (confirm(msg)) {
defer.resolve();
} else {
defer.reject();
}
});
}
};
OpenAssessment.Rubric = function(element) {
this.element = element;
};
OpenAssessment.Rubric.prototype = {
criterionFeedback: function(criterionFeedback) {
var selector = "textarea.answer__value";
var feedback = {};
$(selector, this.element).each(function(index, sel) {
if (typeof criterionFeedback !== "undefined") {
$(sel).val(criterionFeedback[sel.name]);
feedback[sel.name] = criterionFeedback[sel.name];
} else {
feedback[sel.name] = $(sel).val();
}
});
return feedback;
},
optionsSelected: function(optionsSelected) {
var selector = "input[type=radio]";
if (typeof optionsSelected === "undefined") {
var options = {};
$(selector + ":checked", this.element).each(function(index, sel) {
options[sel.name] = sel.value;
});
return options;
} else {
$(selector, this.element).prop("checked", false);
$(selector, this.element).each(function(index, sel) {
if (optionsSelected.hasOwnProperty(sel.name)) {
if (sel.value == optionsSelected[sel.name]) {
$(sel).prop("checked", true);
}
}
});
}
},
canSubmitCallback: function(callback) {
$(this.element).change(function() {
var numChecked = $("input[type=radio]:checked", this).length;
var numAvailable = $(".field--radio.assessment__rubric__question", this).length;
var canSubmit = numChecked == numAvailable;
callback(canSubmit);
});
},
showCorrections: function(corrections) {
var selector = "input[type=radio]";
var hasErrors = false;
$(selector, this.element).each(function(index, sel) {
var listItem = $(sel).parents(".assessment__rubric__question");
if (corrections.hasOwnProperty(sel.name)) {
hasErrors = true;
listItem.find(".message--incorrect").removeClass("is--hidden");
listItem.find(".message--correct").addClass("is--hidden");
} else {
listItem.find(".message--correct").removeClass("is--hidden");
listItem.find(".message--incorrect").addClass("is--hidden");
}
});
return hasErrors;
}
};
OpenAssessment.SelfView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
this.rubric = null;
};
OpenAssessment.SelfView.prototype = {
load: function() {
var view = this;
this.server.render("self_assessment").done(function(html) {
$("#openassessment__self-assessment", view.element).replaceWith(html);
view.installHandlers();
}).fail(function(errMsg) {
view.showLoadError("self-assessment");
});
},
installHandlers: function() {
var view = this;
var sel = $("#openassessment__self-assessment", view.element);
this.baseView.setUpCollapseExpand(sel);
var rubricSelector = $("#self-assessment--001__assessment", this.element);
if (rubricSelector.size() > 0) {
var rubricElement = rubricSelector.get(0);
this.rubric = new OpenAssessment.Rubric(rubricElement);
}
if (this.rubric !== null) {
this.rubric.canSubmitCallback($.proxy(this.selfSubmitEnabled, this));
}
sel.find("#self-assessment--001__assessment__submit").click(function(eventObject) {
eventObject.preventDefault();
view.selfAssess();
});
},
selfSubmitEnabled: function(enabled) {
var button = $("#self-assessment--001__assessment__submit", this.element);
if (typeof enabled === "undefined") {
return !button.hasClass("is--disabled");
} else {
button.toggleClass("is--disabled", !enabled);
}
},
selfAssess: function() {
var view = this;
var baseView = this.baseView;
baseView.toggleActionError("self", null);
view.selfSubmitEnabled(false);
var options = this.rubric.optionsSelected();
this.server.selfAssess(options).done(function() {
baseView.loadAssessmentModules();
baseView.scrollToTop();
}).fail(function(errMsg) {
baseView.toggleActionError("self", errMsg);
view.selfSubmitEnabled(true);
});
}
};
OpenAssessment.Server = function(runtime, element) {
this.runtime = runtime;
this.element = element;
};
OpenAssessment.Server.prototype = {
url: function(handler) {
return this.runtime.handlerUrl(this.element, handler);
},
render: function(component) {
var url = this.url("render_" + component);
return $.Deferred(function(defer) {
$.ajax({
url: url,
type: "POST",
dataType: "html"
}).done(function(data) {
defer.resolveWith(this, [ data ]);
}).fail(function(data) {
defer.rejectWith(this, [ gettext("This section could not be loaded.") ]);
});
}).promise();
},
renderContinuedPeer: function() {
var url = this.url("render_peer_assessment");
return $.Deferred(function(defer) {
$.ajax({
url: url,
type: "POST",
dataType: "html",
data: {
continue_grading: true
}
}).done(function(data) {
defer.resolveWith(this, [ data ]);
}).fail(function(data) {
defer.rejectWith(this, [ gettext("This section could not be loaded.") ]);
});
}).promise();
},
studentInfo: function(student_id) {
var url = this.url("render_student_info");
return $.Deferred(function(defer) {
$.ajax({
url: url,
type: "POST",
dataType: "html",
data: {
student_id: student_id
}
}).done(function(data) {
defer.resolveWith(this, [ data ]);
}).fail(function(data) {
defer.rejectWith(this, [ gettext("This section could not be loaded.") ]);
});
}).promise();
},
submit: function(submission) {
var url = this.url("submit");
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({
submission: submission
})
}).done(function(data) {
var success = data[0];
if (success) {
var studentId = data[1];
var attemptNum = data[2];
defer.resolveWith(this, [ studentId, attemptNum ]);
} else {
var errorNum = data[1];
var errorMsg = data[2];
defer.rejectWith(this, [ errorNum, errorMsg ]);
}
}).fail(function(data) {
defer.rejectWith(this, [ "AJAX", gettext("This response could not be submitted.") ]);
});
}).promise();
},
save: function(submission) {
var url = this.url("save_submission");
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify({
submission: submission
})
}).done(function(data) {
if (data.success) {
defer.resolve();
} else {
defer.rejectWith(this, [ data.msg ]);
}
}).fail(function(data) {
defer.rejectWith(this, [ gettext("This response could not be saved.") ]);
});
}).promise();
},
submitFeedbackOnAssessment: function(text, options) {
var url = this.url("submit_feedback");
var payload = JSON.stringify({
feedback_text: text,
feedback_options: options
});
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: payload
}).done(function(data) {
if (data.success) {
defer.resolve();
} else {
defer.rejectWith(this, [ data.msg ]);
}
}).fail(function(data) {
defer.rejectWith(this, [ gettext("This feedback could not be submitted.") ]);
});
}).promise();
},
peerAssess: function(optionsSelected, criterionFeedback, overallFeedback) {
var url = this.url("peer_assess");
var payload = JSON.stringify({
options_selected: optionsSelected,
criterion_feedback: criterionFeedback,
overall_feedback: overallFeedback
});
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: payload
}).done(function(data) {
if (data.success) {
defer.resolve();
} else {
defer.rejectWith(this, [ data.msg ]);
}
}).fail(function(data) {
defer.rejectWith(this, [ gettext("This assessment could not be submitted.") ]);
});
}).promise();
},
selfAssess: function(optionsSelected) {
var url = this.url("self_assess");
var payload = JSON.stringify({
options_selected: optionsSelected
});
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: payload
}).done(function(data) {
if (data.success) {
defer.resolve();
} else {
defer.rejectWith(this, [ data.msg ]);
}
}).fail(function(data) {
defer.rejectWith(this, [ gettext("This assessment could not be submitted.") ]);
});
});
},
trainingAssess: function(optionsSelected) {
var url = this.url("training_assess");
var payload = JSON.stringify({
options_selected: optionsSelected
});
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: payload
}).done(function(data) {
if (data.success) {
defer.resolveWith(this, [ data.corrections ]);
} else {
defer.rejectWith(this, [ data.msg ]);
}
}).fail(function(data) {
defer.rejectWith(this, [ gettext("This assessment could not be submitted.") ]);
});
});
},
loadXml: function() {
var url = this.url("xml");
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: '""'
}).done(function(data) {
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.") ]);
});
}).promise();
},
updateXml: function(prompt, rubricXml, title, sub_start, sub_due, assessmentsXml) {
var url = this.url("update_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) {
$.ajax({
type: "POST",
url: url,
data: payload
}).done(function(data) {
if (data.success) {
defer.resolve();
} else {
defer.rejectWith(this, [ data.msg ]);
}
}).fail(function(data) {
defer.rejectWith(this, [ gettext("This problem could not be saved.") ]);
});
}).promise();
},
checkReleased: function() {
var url = this.url("check_released");
var payload = '""';
return $.Deferred(function(defer) {
$.ajax({
type: "POST",
url: url,
data: payload
}).done(function(data) {
if (data.success) {
defer.resolveWith(this, [ data.is_released ]);
} else {
defer.rejectWith(this, [ data.msg ]);
}
}).fail(function(data) {
defer.rejectWith(this, [ gettext("The server could not be contacted.") ]);
});
}).promise();
}
};
if (typeof OpenAssessment == "undefined" || !OpenAssessment) {
OpenAssessment = {};
}
if (typeof window.gettext === "undefined") {
window.gettext = function(text) {
return text;
};
}
OpenAssessment.StaffInfoView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
};
OpenAssessment.StaffInfoView.prototype = {
load: function() {
var view = this;
if ($("#openassessment__staff-info", view.element).length > 0) {
this.server.render("staff_info").done(function(html) {
$("#openassessment__staff-info", view.element).replaceWith(html);
view.installHandlers();
}).fail(function(errMsg) {
view.baseView.showLoadError("staff_info");
});
}
},
loadStudentInfo: function() {
var view = this;
var sel = $("#openassessment__staff-info", this.element);
var student_id = sel.find("#openassessment__student_id").val();
this.server.studentInfo(student_id).done(function(html) {
$("#openassessment__student-info", view.element).replaceWith(html);
}).fail(function(errMsg) {
view.showLoadError("student_info");
});
},
installHandlers: function() {
var sel = $("#openassessment__staff-info", this.element);
var view = this;
if (sel.length <= 0) {
return;
}
this.baseView.setUpCollapseExpand(sel, function() {});
sel.find("#openassessment_student_info_form").submit(function(eventObject) {
eventObject.preventDefault();
view.loadStudentInfo();
});
sel.find("#submit_student_id").click(function(eventObject) {
eventObject.preventDefault();
view.loadStudentInfo();
});
}
};
OpenAssessment.StudentTrainingView = function(element, server, baseView) {
this.element = element;
this.server = server;
this.baseView = baseView;
this.rubric = null;
};
OpenAssessment.StudentTrainingView.prototype = {
load: function() {
var view = this;
this.server.render("student_training").done(function(html) {
$("#openassessment__student-training", view.element).replaceWith(html);
view.installHandlers();
}).fail(function(errMsg) {
view.baseView.showLoadError("student-training");
});
},
installHandlers: function() {
var sel = $("#openassessment__student-training", this.element);
var view = this;
this.baseView.setUpCollapseExpand(sel);
var rubricSelector = $("#student-training--001__assessment", this.element);
if (rubricSelector.size() > 0) {
var rubricElement = rubricSelector.get(0);
this.rubric = new OpenAssessment.Rubric(rubricElement);
}
if (this.rubric !== null) {
this.rubric.canSubmitCallback($.proxy(this.assessButtonEnabled, this));
}
sel.find("#student-training--001__assessment__submit").click(function(eventObject) {
eventObject.preventDefault();
view.assess();
});
},
assess: function() {
this.assessButtonEnabled(false);
var options = {};
if (this.rubric !== null) {
options = this.rubric.optionsSelected();
}
var view = this;
var baseView = this.baseView;
this.server.trainingAssess(options).done(function(corrections) {
var incorrect = $("#openassessment__student-training--incorrect", this.element);
var instructions = $("#openassessment__student-training--instructions", this.element);
if (!view.rubric.showCorrections(corrections)) {
view.load();
baseView.loadAssessmentModules();
incorrect.addClass("is--hidden");
instructions.removeClass("is--hidden");
} else {
instructions.addClass("is--hidden");
incorrect.removeClass("is--hidden");
}
baseView.scrollToTop();
}).fail(function(errMsg) {
baseView.toggleActionError("student-training", errMsg);
view.assessButtonEnabled(true);
});
},
assessButtonEnabled: function(isEnabled) {
var button = $("#student-training--001__assessment__submit", this.element);
if (typeof isEnabled === "undefined") {
return !button.hasClass("is--disabled");
} else {
button.toggleClass("is--disabled", !isEnabled);
}
}
};
\ No newline at end of file
/** /**
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), 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);
} }
); );
...@@ -54,17 +75,21 @@ OpenAssessment.StudioView.prototype = { ...@@ -54,17 +75,21 @@ OpenAssessment.StudioView.prototype = {
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);
}); });
}, },
...@@ -77,31 +102,39 @@ OpenAssessment.StudioView.prototype = { ...@@ -77,31 +102,39 @@ OpenAssessment.StudioView.prototype = {
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);
}); });
}, },
...@@ -109,7 +142,7 @@ OpenAssessment.StudioView.prototype = { ...@@ -109,7 +142,7 @@ OpenAssessment.StudioView.prototype = {
/** /**
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', {});
}, },
...@@ -120,7 +153,7 @@ OpenAssessment.StudioView.prototype = { ...@@ -120,7 +153,7 @@ OpenAssessment.StudioView.prototype = {
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});
} }
}; };
...@@ -132,9 +165,9 @@ function OpenAssessmentEditor(runtime, element) { ...@@ -132,9 +165,9 @@ 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
......
...@@ -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))
if 'settings' not in data:
return {'success': False, 'msg': _('Must specify "settings" in request JSON dict.')}
except ValidationError as ex: if 'prompt' not in data:
return {'success': False, 'msg': _('Validation error: {error}').format(error=ex)} return {'success': False, 'msg': _('Must specify "prompt" in request JSON dict.')}
settings = data['settings']
try:
rubric = parse_rubric_xml(data['rubric'])
assessments = parse_assessments_xml(settings['assessments'])
submission_due = settings["submission_due"]
except UpdateFromXmlError as ex: except 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)}
else: 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')} return {'success': True, 'msg': _('Successfully updated OpenAssessment XBlock')}
else: def update(self, rubric, assessments, submission_due, submission_start, title, prompt):
return {'success': False, 'msg': _('Must specify "xml" in request JSON dict.')} """
Given a dictionary of properties, update the XBlock
"""
# 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=''):
......
...@@ -124,7 +124,7 @@ def _serialize_criteria(criteria_root, criteria_list): ...@@ -124,7 +124,7 @@ def _serialize_criteria(criteria_root, criteria_list):
_serialize_options(criterion_el, options_list) _serialize_options(criterion_el, options_list)
def _serialize_rubric(rubric_root, oa_block): def serialize_rubric(rubric_root, oa_block):
""" """
Serialize a rubric dictionary as XML, adding children to the XML Serialize a rubric dictionary as XML, adding children to the XML
with root node `rubric_root`. with root node `rubric_root`.
...@@ -156,7 +156,8 @@ def _serialize_rubric(rubric_root, oa_block): ...@@ -156,7 +156,8 @@ def _serialize_rubric(rubric_root, oa_block):
feedback_prompt = etree.SubElement(rubric_root, 'feedbackprompt') feedback_prompt = etree.SubElement(rubric_root, 'feedbackprompt')
feedback_prompt.text = unicode(oa_block.rubric_feedback_prompt) feedback_prompt.text = unicode(oa_block.rubric_feedback_prompt)
def _parse_date(date_str):
def parse_date(date_str):
""" """
Attempt to parse a date string into ISO format (without milliseconds) Attempt to parse a date string into ISO format (without milliseconds)
Returns `None` if this cannot be done. Returns `None` if this cannot be done.
...@@ -282,7 +283,7 @@ def _parse_criteria_xml(criteria_root): ...@@ -282,7 +283,7 @@ def _parse_criteria_xml(criteria_root):
return criteria_list return criteria_list
def _parse_rubric_xml(rubric_root): def parse_rubric_xml(rubric_root):
""" """
Parse <rubric> element in the OpenAssessment XBlock's content XML. Parse <rubric> element in the OpenAssessment XBlock's content XML.
...@@ -320,7 +321,7 @@ def _parse_rubric_xml(rubric_root): ...@@ -320,7 +321,7 @@ def _parse_rubric_xml(rubric_root):
return rubric_dict return rubric_dict
def _parse_examples_xml(examples): def parse_examples_xml(examples):
""" """
Parse <example> (training examples) from the XML. Parse <example> (training examples) from the XML.
...@@ -362,7 +363,7 @@ def _parse_examples_xml(examples): ...@@ -362,7 +363,7 @@ def _parse_examples_xml(examples):
return examples_list return examples_list
def _parse_assessments_xml(assessments_root): def parse_assessments_xml(assessments_root):
""" """
Parse the <assessments> element in the OpenAssessment XBlock's content XML. Parse the <assessments> element in the OpenAssessment XBlock's content XML.
...@@ -390,7 +391,7 @@ def _parse_assessments_xml(assessments_root): ...@@ -390,7 +391,7 @@ def _parse_assessments_xml(assessments_root):
# Assessment start # Assessment start
if 'start' in assessment.attrib: if 'start' in assessment.attrib:
parsed_start = _parse_date(assessment.get('start')) parsed_start = parse_date(assessment.get('start'))
if parsed_start is not None: if parsed_start is not None:
assessment_dict['start'] = parsed_start assessment_dict['start'] = parsed_start
else: else:
...@@ -400,7 +401,7 @@ def _parse_assessments_xml(assessments_root): ...@@ -400,7 +401,7 @@ def _parse_assessments_xml(assessments_root):
# Assessment due # Assessment due
if 'due' in assessment.attrib: if 'due' in assessment.attrib:
parsed_start = _parse_date(assessment.get('due')) parsed_start = parse_date(assessment.get('due'))
if parsed_start is not None: if parsed_start is not None:
assessment_dict['due'] = parsed_start assessment_dict['due'] = parsed_start
else: else:
...@@ -431,7 +432,7 @@ def _parse_assessments_xml(assessments_root): ...@@ -431,7 +432,7 @@ def _parse_assessments_xml(assessments_root):
# Other assessment types ignore examples. # Other assessment types ignore examples.
# Later, we can add AI assessment here. # Later, we can add AI assessment here.
if assessment_dict['name'] == 'student-training': if assessment_dict['name'] == 'student-training':
assessment_dict['examples'] = _parse_examples_xml(examples) assessment_dict['examples'] = parse_examples_xml(examples)
# Update the list of assessments # Update the list of assessments
assessments_list.append(assessment_dict) assessments_list.append(assessment_dict)
...@@ -439,7 +440,7 @@ def _parse_assessments_xml(assessments_root): ...@@ -439,7 +440,7 @@ def _parse_assessments_xml(assessments_root):
return assessments_list return assessments_list
def _serialize_training_examples(examples, assessment_el): def serialize_training_examples(examples, assessment_el):
""" """
Serialize a training example to XML. Serialize a training example to XML.
...@@ -466,34 +467,19 @@ def _serialize_training_examples(examples, assessment_el): ...@@ -466,34 +467,19 @@ def _serialize_training_examples(examples, assessment_el):
select_el.set('option', unicode(selected_dict.get('option', ''))) select_el.set('option', unicode(selected_dict.get('option', '')))
def serialize_content_to_xml(oa_block, root): def serialize_assessments(assessments_root, oa_block):
""" """
Serialize the OpenAssessment XBlock's content to XML. Serialize the assessment modules for an OpenAssessment XBlock.
Args: Args:
oa_block (OpenAssessmentBlock): The open assessment block to serialize. assessments_root (lxml.etree.Element): The <assessments> XML element.
root (etree.Element): The XML root node to update. oa_block (OpenAssessmentXBlock): The XBlock with configuration to
serialize.
Returns: Returns:
etree.Element None
""" """
root.tag = 'openassessment'
# Set the submission start date
if oa_block.submission_start is not None:
root.set('submission_start', unicode(oa_block.submission_start))
# Set submission due date
if oa_block.submission_due is not None:
root.set('submission_due', unicode(oa_block.submission_due))
# Open assessment displayed title
title = etree.SubElement(root, 'title')
title.text = unicode(oa_block.title)
# Assessment list
assessments_root = etree.SubElement(root, 'assessments')
for assessment_dict in oa_block.rubric_assessments: for assessment_dict in oa_block.rubric_assessments:
assessment = etree.SubElement(assessments_root, 'assessment') assessment = etree.SubElement(assessments_root, 'assessment')
...@@ -517,11 +503,42 @@ def serialize_content_to_xml(oa_block, root): ...@@ -517,11 +503,42 @@ def serialize_content_to_xml(oa_block, root):
examples = assessment_dict.get('examples', []) examples = assessment_dict.get('examples', [])
if not isinstance(examples, list): if not isinstance(examples, list):
examples = [] examples = []
_serialize_training_examples(examples, assessment) serialize_training_examples(examples, assessment)
def serialize_content_to_xml(oa_block, root):
"""
Serialize the OpenAssessment XBlock's content to XML.
Args:
oa_block (OpenAssessmentBlock): The open assessment block to serialize.
root (etree.Element): The XML root node to update.
Returns:
etree.Element
"""
root.tag = 'openassessment'
# Set the submission start date
if oa_block.submission_start is not None:
root.set('submission_start', unicode(oa_block.submission_start))
# Set submission due date
if oa_block.submission_due is not None:
root.set('submission_due', unicode(oa_block.submission_due))
# Open assessment displayed title
title = etree.SubElement(root, 'title')
title.text = unicode(oa_block.title)
# Assessment list
assessments_root = etree.SubElement(root, 'assessments')
serialize_assessments(assessments_root, oa_block)
# Rubric # Rubric
rubric_root = etree.SubElement(root, 'rubric') rubric_root = etree.SubElement(root, 'rubric')
_serialize_rubric(rubric_root, oa_block) serialize_rubric(rubric_root, oa_block)
def serialize_content(oa_block): def serialize_content(oa_block):
...@@ -541,9 +558,59 @@ def serialize_content(oa_block): ...@@ -541,9 +558,59 @@ def serialize_content(oa_block):
return etree.tostring(root, pretty_print=True, encoding='utf-8') return etree.tostring(root, pretty_print=True, encoding='utf-8')
DEFAULT_VALIDATOR = lambda *args: (True, '') def serialize_rubric_to_xml_str(oa_block):
"""
Serialize the OpenAssessment XBlock's rubric into an XML string.
Args:
oa_block (OpenAssessmentBlock): The open assessment block to serialize
a rubric from.
Returns:
xml (unicode) representation of the Rubric.
"""
rubric_root = etree.Element('rubric')
serialize_rubric(rubric_root, oa_block)
return etree.tostring(rubric_root, pretty_print=True, encoding='utf-8')
def update_from_xml(oa_block, root, validator=DEFAULT_VALIDATOR): def serialize_examples_to_xml_str(assessment):
"""
Serializes the OpenAssessment XBlock's training examples into an XML unicode
string.
Args:
assessment (dict): Dictionary representation of an Assessment Module's
configuration. If this contains a list of examples, the examples
will be returned serialized.
Returns:
A unicode string of the XML serialized examples.
"""
examples = assessment.get('examples', [])
if not isinstance(examples, list):
examples = []
examples_root = etree.Element('examples')
serialize_training_examples(examples, examples_root)
return etree.tostring(examples_root, pretty_print=True, encoding='utf-8')
def serialize_assessments_to_xml_str(oa_block):
"""
Serializes the OpenAssessment XBlock's assessment modules into an XML
unicode string.
Args:
oa_block (OpenAssessmentBlock
"""
assessments_root = etree.Element('assessments')
serialize_assessments(assessments_root, oa_block)
return etree.tostring(assessments_root, pretty_print=True, encoding='utf-8')
def parse_from_xml(root):
""" """
Update the OpenAssessment XBlock's content from an XML definition. Update the OpenAssessment XBlock's content from an XML definition.
...@@ -551,24 +618,13 @@ def update_from_xml(oa_block, root, validator=DEFAULT_VALIDATOR): ...@@ -551,24 +618,13 @@ def update_from_xml(oa_block, root, validator=DEFAULT_VALIDATOR):
the XBlock to an invalid state (which will then be persisted). the XBlock to an invalid state (which will then be persisted).
Args: Args:
oa_block (OpenAssessmentBlock): The open assessment block to update.
root (lxml.etree.Element): The XML definition of the XBlock's content. root (lxml.etree.Element): The XML definition of the XBlock's content.
Kwargs:
validator(callable): Function of the form:
(rubric_dict, submission_dict, assessments) -> (bool, unicode)
where the returned bool indicates whether the XML is semantically valid,
and the returned unicode is an error message.
`rubric_dict` is a serialized Rubric model
`submission_dict` contains a single key "due" which is an ISO-formatted date string.
`assessments` is a list of serialized Assessment models.
Returns: Returns:
OpenAssessmentBlock A dictionary of all of the XBlock's content.
Raises: Raises:
UpdateFromXmlError: The XML definition is invalid or the XBlock could not be updated. UpdateFromXmlError: The XML definition is invalid
ValidationError: The validator indicated that the XML was not semantically valid.
""" """
# Check that the root has the correct tag # Check that the root has the correct tag
...@@ -579,7 +635,7 @@ def update_from_xml(oa_block, root, validator=DEFAULT_VALIDATOR): ...@@ -579,7 +635,7 @@ def update_from_xml(oa_block, root, validator=DEFAULT_VALIDATOR):
# Set it to None by default; we will update it to the latest start date later on # Set it to None by default; we will update it to the latest start date later on
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: 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.')) raise UpdateFromXmlError(_('The format for the submission start date is invalid. Make sure the date is formatted as YYYY-MM-DDTHH:MM:SS.'))
...@@ -587,7 +643,7 @@ def update_from_xml(oa_block, root, validator=DEFAULT_VALIDATOR): ...@@ -587,7 +643,7 @@ def update_from_xml(oa_block, root, validator=DEFAULT_VALIDATOR):
# 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: 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.')) raise UpdateFromXmlError(_('The format for the submission due date is invalid. Make sure the date is formatted as YYYY-MM-DDTHH:MM:SS.'))
...@@ -603,59 +659,119 @@ def update_from_xml(oa_block, root, validator=DEFAULT_VALIDATOR): ...@@ -603,59 +659,119 @@ def update_from_xml(oa_block, root, validator=DEFAULT_VALIDATOR):
if rubric_el is None: if rubric_el is None:
raise UpdateFromXmlError(_('Every assessment must contain a "rubric" element.')) raise UpdateFromXmlError(_('Every assessment must contain a "rubric" element.'))
else: else:
rubric = _parse_rubric_xml(rubric_el) rubric = parse_rubric_xml(rubric_el)
# Retrieve the assessments # Retrieve the assessments
assessments_el = root.find('assessments') assessments_el = root.find('assessments')
if assessments_el is None: if assessments_el is None:
raise UpdateFromXmlError(_('Every assessment must contain an "assessments" element.')) raise UpdateFromXmlError(_('Every assessment must contain an "assessments" element.'))
else: else:
assessments = _parse_assessments_xml(assessments_el) assessments = parse_assessments_xml(assessments_el)
# Validate return {
success, msg = validator(rubric, {'due': submission_due}, assessments) 'title': title,
if not success: 'prompt': rubric['prompt'],
raise ValidationError(msg) 'rubric_criteria': rubric['criteria'],
'rubric_assessments': assessments,
'rubric_feedback_prompt': rubric['feedbackprompt'],
'submission_start': submission_start,
'submission_due': submission_due,
}
# 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.
oa_block.title = title
oa_block.prompt = rubric['prompt']
oa_block.rubric_criteria = rubric['criteria']
oa_block.rubric_assessments = assessments
oa_block.rubric_feedback_prompt = rubric['feedbackprompt']
oa_block.submission_start = submission_start
oa_block.submission_due = submission_due
return oa_block def parse_from_xml_str(xml):
def update_from_xml_str(oa_block, xml, **kwargs):
""" """
Update the OpenAssessment XBlock's content from an XML string definition. Create a dictionary for the OpenAssessment XBlock's content from an XML
Parses the string using a library that avoids some known security vulnerabilities in etree. string definition. Parses the string using a library that avoids some known
security vulnerabilities in etree.
Args: Args:
oa_block (OpenAssessmentBlock): The open assessment block to update.
xml (unicode): The XML definition of the XBlock's content. xml (unicode): The XML definition of the XBlock's content.
Kwargs: Returns:
same as `update_from_xml` A dictionary of all configuration values for the XBlock.
Raises:
UpdateFromXmlError: The XML definition is invalid.
InvalidRubricError: The rubric was not semantically valid.
InvalidAssessmentsError: The assessments are not semantically valid.
"""
return parse_from_xml(_unicode_to_xml(xml))
def parse_rubric_xml_str(xml):
"""
Create a dictionary representation of the OpenAssessment XBlock rubric from
the given XML string.
Args:
xml (unicode): The XML definition of the XBlock's rubric.
Returns: Returns:
OpenAssessmentBlock A dictionary of all rubric configuration.
Raises: Raises:
UpdateFromXmlError: The XML definition is invalid or the XBlock could not be updated. UpdateFromXmlError: The XML definition is invalid.
InvalidRubricError: The rubric was not semantically valid. InvalidRubricError: The rubric was not semantically valid.
"""
return parse_rubric_xml(_unicode_to_xml(xml))
def parse_assessments_xml_str(xml):
"""
Create a dictionary representation of the OpenAssessment XBlock assessments
from the given XML string.
Args:
xml (unicode): The XML definition of the XBlock's assessments.
Returns:
A list of dictionaries representing the deserialized XBlock
configuration for each assessment module.
Raises:
UpdateFromXmlError: The XML definition is invalid.
InvalidAssessmentsError: The assessments are not semantically valid. InvalidAssessmentsError: The assessments are not semantically valid.
"""
return parse_assessments_xml(_unicode_to_xml(xml))
def parse_examples_xml_str(xml):
"""
Create a list representation of the OpenAssessment XBlock assessment
examples from the given XML string.
Args:
xml (unicode): The XML definition of the Assessment module's examples.
Returns:
A list of dictionaries representing the deserialized XBlock
configuration for each assessment example.
Raises:
UpdateFromXmlError: The XML definition is invalid.
"""
return parse_examples_xml(_unicode_to_xml(xml))
def _unicode_to_xml(xml):
"""
Converts unicode string to XML node.
Args:
xml (unicode): The XML definition of some XBlock configuration.
Raises:
UpdateFromXmlError: Raised when the XML definition is invalid.
""" """
# Parse the XML content definition # Parse the XML content definition
# Use the defusedxml library implementation to avoid known security vulnerabilities in ElementTree: # Use the defusedxml library implementation to avoid known security vulnerabilities in ElementTree:
# http://docs.python.org/2/library/xml.html#xml-vulnerabilities # http://docs.python.org/2/library/xml.html#xml-vulnerabilities
try: try:
root = safe_etree.fromstring(xml.encode('utf-8')) return safe_etree.fromstring(xml.encode('utf-8'))
except (ValueError, safe_etree.ParseError): except (ValueError, safe_etree.ParseError):
raise UpdateFromXmlError(_("An error occurred while parsing the XML content.")) raise UpdateFromXmlError(_("An error occurred while parsing the XML content."))
\ No newline at end of file
return update_from_xml(oa_block, root, **kwargs)
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