From a5921b621215b73042b5e8ed7c9a284c20ad61df Mon Sep 17 00:00:00 2001 From: Will Daly <will@edx.org> Date: Wed, 30 Jul 2014 08:48:38 -0400 Subject: [PATCH] Add field validation for datetime fields and option points. Remove support in Javascript for "default" (empty) dates and update the template rendering script to support this in the JS tests. --- openassessment/templates/openassessmentblock/edit/oa_edit_option.html | 4 ++-- openassessment/xblock/static/js/fixtures/templates.json | 13 ++++++++----- openassessment/xblock/static/js/openassessment-studio.min.js | 4 ++-- openassessment/xblock/static/js/spec/studio/oa_edit.js | 43 +++++++++++++++++++++++++++++++++++++------ openassessment/xblock/static/js/spec/studio/oa_edit_assessments.js | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++------------------ openassessment/xblock/static/js/spec/studio/oa_edit_fields.js | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ openassessment/xblock/static/js/spec/studio/oa_edit_rubric.js | 23 +++++++++++++++++++++++ openassessment/xblock/static/js/spec/studio/oa_edit_settings.js | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------- openassessment/xblock/static/js/src/studio/oa_container_item.js | 198 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------- openassessment/xblock/static/js/src/studio/oa_edit.js | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- openassessment/xblock/static/js/src/studio/oa_edit_assessment.js | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ openassessment/xblock/static/js/src/studio/oa_edit_fields.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++------- openassessment/xblock/static/js/src/studio/oa_edit_rubric.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ openassessment/xblock/static/js/src/studio/oa_edit_settings.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ openassessment/xblock/static/js/src/studio/oa_edit_validation_alert.js | 58 +++++++++++++++++++++++++++++++++++++--------------------- scripts/render_templates.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 16 files changed, 866 insertions(+), 149 deletions(-) create mode 100644 openassessment/xblock/static/js/spec/studio/oa_edit_fields.js diff --git a/openassessment/templates/openassessmentblock/edit/oa_edit_option.html b/openassessment/templates/openassessmentblock/edit/oa_edit_option.html index 044edd4..07ba5dd 100644 --- a/openassessment/templates/openassessmentblock/edit/oa_edit_option.html +++ b/openassessment/templates/openassessmentblock/edit/oa_edit_option.html @@ -31,7 +31,7 @@ class="openassessment_criterion_option_points input setting-input" type="number" value="{{ option_points }}" - min="0" + min="0" max="999" > </label> </div> @@ -47,4 +47,4 @@ </ul> </div> </li> -{% endspaceless %} \ No newline at end of file +{% endspaceless %} diff --git a/openassessment/xblock/static/js/fixtures/templates.json b/openassessment/xblock/static/js/fixtures/templates.json index 89cf564..4b1a163 100644 --- a/openassessment/xblock/static/js/fixtures/templates.json +++ b/openassessment/xblock/static/js/fixtures/templates.json @@ -388,7 +388,8 @@ "context": { "prompt": "How much do you like waffles?", "title": "The most important of all questions.", - "submission_due": "2014-10-1T10:00:00", + "submission_start": "2014-01-02T12:15", + "submission_due": "2014-10-01T04:53", "criteria": [ { "name": "criterion_1", @@ -443,13 +444,14 @@ ], "assessments": { "peer_assessment": { - "start": "", - "due": "", + "start": "2014-01-02T00:00", + "due": "2014-01-03T00:00", "must_grade": 5, "must_be_graded_by": 3 }, "self_assessment": { - "due": "" + "start": "2014-01-04T00:00", + "due": "2014-01-05T00:00" } }, "editor_assessments_order": [ @@ -466,6 +468,7 @@ "context": { "prompt": "Test prompt", "title": "Test title", + "submission_start": "2014-01-1T10:00:00", "submission_due": "2014-10-1T10:00:00", "criteria": [ { @@ -588,7 +591,7 @@ } }, "peer_assessment": { - "start": "", + "start": "2014-01-02T00:00", "due": "", "must_grade": 5, "must_be_graded_by": 3 diff --git a/openassessment/xblock/static/js/openassessment-studio.min.js b/openassessment/xblock/static/js/openassessment-studio.min.js index 9e84c36..1d8b783 100644 --- a/openassessment/xblock/static/js/openassessment-studio.min.js +++ b/openassessment/xblock/static/js/openassessment-studio.min.js @@ -1,2 +1,2 @@ -if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.Logger==="undefined"){window.Logger={log:function(event_type,data,kwargs){}}}if(typeof OpenAssessment.Server=="undefined"||!OpenAssessment.Server){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,criterionFeedback,overallFeedback){var url=this.url("self_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.")])})})},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.")])})})},scheduleTraining:function(){var url=this.url("schedule_training");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},rescheduleUnfinishedTasks:function(){var url=this.url("reschedule_unfinished_tasks");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("One or more rescheduling tasks failed.")])})})},updateEditorContext:function(kwargs){var url=this.url("update_editor_context");var payload=JSON.stringify({prompt:kwargs.prompt,feedback_prompt:kwargs.feedbackPrompt,title:kwargs.title,submission_start:kwargs.submissionStart,submission_due:kwargs.submissionDue,criteria:kwargs.criteria,assessments:kwargs.assessments,editor_assessments_order:kwargs.editorAssessmentsOrder,allow_file_upload:kwargs.imageSubmissionEnabled});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()},getUploadUrl:function(contentType){var url=this.url("upload_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({contentType:contentType})}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("Could not retrieve upload url.")])})}).promise()},getDownloadUrl:function(){var url=this.url("download_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({})}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("Could not retrieve download url.")])})}).promise()}}}if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.Logger==="undefined"){window.Logger={log:function(event_type,data,kwargs){}}}OpenAssessment.Container=function(containerItem,kwargs){this.containerElement=kwargs.containerElement;this.templateElement=kwargs.templateElement;this.addButtonElement=kwargs.addButtonElement;this.removeButtonClass=kwargs.removeButtonClass;this.containerItemClass=kwargs.containerItemClass;this.notifier=kwargs.notifier;var container=this;this.createContainerItem=function(element){return new containerItem(element,container.notifier)};$(this.addButtonElement).click($.proxy(this.add,this));$("."+this.removeButtonClass,this.containerElement).click(function(eventData){var item=container.createContainerItem(eventData.target);container.remove(item)});$("."+this.containerItemClass,this.containerElement).each(function(index,element){container.createContainerItem(element)})};OpenAssessment.Container.prototype={add:function(){$(this.templateElement).children().first().clone().removeAttr("id").toggleClass("is--hidden",false).toggleClass(this.containerItemClass,true).appendTo($(this.containerElement));var container=this;var containerItem=$("."+this.containerItemClass,this.containerElement).last();containerItem.find("."+this.removeButtonClass).click(function(eventData){var containerItem=container.createContainerItem(eventData.target);container.remove(containerItem)});var handlerItem=container.createContainerItem(containerItem);handlerItem.addHandler()},remove:function(item){var itemElement=$(item.element).closest("."+this.containerItemClass);var containerItem=this.createContainerItem(itemElement);containerItem.removeHandler();itemElement.remove()},getItemValues:function(){var values=[];var container=this;$("."+this.containerItemClass,this.containerElement).each(function(index,element){var containerItem=container.createContainerItem(element);var fieldValues=containerItem.getFieldValues();values.push(fieldValues)});return values},getItem:function(index){var element=$("."+this.containerItemClass,this.containerElement).get(index);return element!==undefined?this.createContainerItem(element):null},getAllItems:function(){var container=this;return $("."+this.containerItemClass,this.containerElement).map(function(){return container.createContainerItem(this)})}};OpenAssessment.ItemUtilities={createUniqueName:function(selector,nameAttribute){var index=0;while(index<=selector.length){if(selector.parent().find("*["+nameAttribute+"='"+index+"']").length===0){return index.toString()}index++}return index.toString()}};OpenAssessment.RubricOption=function(element,notifier){this.element=element;this.notifier=notifier;$(this.element).focusout($.proxy(this.updateHandler,this))};OpenAssessment.RubricOption.prototype={getFieldValues:function(){var fields={label:OpenAssessment.Fields.stringField($(".openassessment_criterion_option_label",this.element)),points:OpenAssessment.Fields.intField($(".openassessment_criterion_option_points",this.element)),explanation:OpenAssessment.Fields.stringField($(".openassessment_criterion_option_explanation",this.element))};var nameString=OpenAssessment.Fields.stringField($(".openassessment_criterion_option_name",this.element));if(nameString!==""){fields.name=nameString}return fields},addHandler:function(){var criterionElement=$(this.element).closest(".openassessment_criterion");var criterionName=$(criterionElement).data("criterion");var criterionLabel=$(".openassessment_criterion_label",criterionElement).val();var options=$(".openassessment_criterion_option",this.element.parent());var name=OpenAssessment.ItemUtilities.createUniqueName(options,"data-option");$(this.element).attr("data-criterion",criterionName).attr("data-option",name);$(".openassessment_criterion_option_name",this.element).attr("value",name);var fields=this.getFieldValues();this.notifier.notificationFired("optionAdd",{criterionName:criterionName,criterionLabel:criterionLabel,name:name,label:fields.label,points:fields.points})},removeHandler:function(){var criterionName=$(this.element).data("criterion");var optionName=$(this.element).data("option");this.notifier.notificationFired("optionRemove",{criterionName:criterionName,name:optionName})},updateHandler:function(){var fields=this.getFieldValues();var criterionName=$(this.element).data("criterion");var optionName=$(this.element).data("option");var optionLabel=fields.label;var optionPoints=fields.points;this.notifier.notificationFired("optionUpdated",{criterionName:criterionName,name:optionName,label:optionLabel,points:optionPoints})}};OpenAssessment.RubricCriterion=function(element,notifier){this.element=element;this.notifier=notifier;this.optionContainer=new OpenAssessment.Container(OpenAssessment.RubricOption,{containerElement:$(".openassessment_criterion_option_list",this.element).get(0),templateElement:$("#openassessment_option_template").get(0),addButtonElement:$(".openassessment_criterion_add_option",this.element).get(0),removeButtonClass:"openassessment_criterion_option_remove_button",containerItemClass:"openassessment_criterion_option",notifier:this.notifier});$(this.element).focusout($.proxy(this.updateHandler,this))};OpenAssessment.RubricCriterion.prototype={getFieldValues:function(){var fields={label:OpenAssessment.Fields.stringField($(".openassessment_criterion_label",this.element)),prompt:OpenAssessment.Fields.stringField($(".openassessment_criterion_prompt",this.element)),feedback:OpenAssessment.Fields.stringField($(".openassessment_criterion_feedback",this.element)),options:this.optionContainer.getItemValues()};var nameString=OpenAssessment.Fields.stringField($(".openassessment_criterion_name",this.element));if(nameString!==""){fields.name=nameString}return fields},addOption:function(){this.optionContainer.add()},addHandler:function(){var criteria=$(".openassessment_criterion",this.element.parent());var name=OpenAssessment.ItemUtilities.createUniqueName(criteria,"data-criterion");$(this.element).attr("data-criterion",name);$(".openassessment_criterion_name",this.element).attr("value",name)},removeHandler:function(){var criterionName=$(this.element).data("criterion");this.notifier.notificationFired("criterionRemove",{criterionName:criterionName})},updateHandler:function(){var fields=this.getFieldValues();var criterionName=fields.name;var criterionLabel=fields.label;this.notifier.notificationFired("criterionUpdated",{criterionName:criterionName,criterionLabel:criterionLabel})}};OpenAssessment.TrainingExample=function(element){this.element=element};OpenAssessment.TrainingExample.prototype={getFieldValues:function(){var optionsSelected=[];$(".openassessment_training_example_criterion_option",this.element).each(function(){optionsSelected.push({criterion:$(this).data("criterion"),option:$(this).prop("value")})});return{answer:$(".openassessment_training_example_essay",this.element).first().prop("value"),options_selected:optionsSelected}},addHandler:function(){},removeHandler:function(){},updateHandler:function(){}};OpenAssessment.StudioView=function(runtime,element,server){this.element=element;this.runtime=runtime;this.server=server;this.fixModalHeight();this.initializeTabs();this.promptView=new OpenAssessment.EditPromptView($("#oa_prompt_editor_wrapper",this.element).get(0));var studentTrainingView=new OpenAssessment.EditStudentTrainingView($("#oa_student_training_editor",this.element).get(0));var peerAssessmentView=new OpenAssessment.EditPeerAssessmentView($("#oa_peer_assessment_editor",this.element).get(0));var selfAssessmentView=new OpenAssessment.EditSelfAssessmentView($("#oa_self_assessment_editor",this.element).get(0));var exampleBasedAssessmentView=new OpenAssessment.EditExampleBasedAssessmentView($("#oa_ai_assessment_editor",this.element).get(0));var assessmentLookupDictionary={};assessmentLookupDictionary[studentTrainingView.getID()]=studentTrainingView;assessmentLookupDictionary[peerAssessmentView.getID()]=peerAssessmentView;assessmentLookupDictionary[selfAssessmentView.getID()]=selfAssessmentView;assessmentLookupDictionary[exampleBasedAssessmentView.getID()]=exampleBasedAssessmentView;this.settingsView=new OpenAssessment.EditSettingsView($("#oa_basic_settings_editor",this.element).get(0),assessmentLookupDictionary);this.rubricView=new OpenAssessment.EditRubricView($("#oa_rubric_editor_wrapper",this.element).get(0),new OpenAssessment.Notifier([new OpenAssessment.StudentTrainingListener]));$(".openassessment_save_button",this.element).click($.proxy(this.save,this));$(".openassessment_cancel_button",this.element).click($.proxy(this.cancel,this))};OpenAssessment.StudioView.prototype={fixModalHeight:function(){$(this.element).addClass("openassessment_full_height").parentsUntil(".modal-window").addClass("openassessment_full_height");$(this.element).closest(".modal-window").addClass("openassessment_modal_window")},initializeTabs:function(){if(typeof OpenAssessment.lastOpenEditingTab==="undefined"){OpenAssessment.lastOpenEditingTab=2}$(".openassessment_editor_content_and_tabs",this.element).tabs({active:OpenAssessment.lastOpenEditingTab})},saveTabState:function(){var tabElement=$(".openassessment_editor_content_and_tabs",this.element);OpenAssessment.lastOpenEditingTab=tabElement.tabs("option","active")},save:function(){var view=this;this.saveTabState();this.server.checkReleased().done(function(isReleased){if(isReleased){view.confirmPostReleaseUpdate($.proxy(view.updateEditorContext,view))}else{view.updateEditorContext()}}).fail(function(errMsg){view.showError(errMsg)})},confirmPostReleaseUpdate:function(onConfirm){var msg=gettext("This problem has already been released. Any changes will apply only to future assessments.");if(confirm(msg)){onConfirm()}},updateEditorContext:function(){this.runtime.notify("save",{state:"start"});var view=this;this.server.updateEditorContext({prompt:view.promptView.promptText(),feedbackPrompt:view.rubricView.feedbackPrompt(),criteria:view.rubricView.criteriaDefinition(),title:view.settingsView.displayName(),submissionStart:view.settingsView.submissionStart(),submissionDue:view.settingsView.submissionDue(),assessments:view.settingsView.assessmentsDescription(),imageSubmissionEnabled:view.settingsView.imageSubmissionEnabled(),editorAssessmentsOrder:view.settingsView.editorAssessmentsOrder()}).done(function(){view.runtime.notify("save",{state:"end"})}).fail(function(msg){view.showError(msg)})},cancel:function(){this.saveTabState();this.runtime.notify("cancel",{})},showError:function(errorMsg){this.runtime.notify("error",{msg:errorMsg})}};function OpenAssessmentEditor(runtime,element){var server=new OpenAssessment.Server(runtime,element);var view=new OpenAssessment.StudioView(runtime,element,server)}OpenAssessment.EditPeerAssessmentView=function(element){this.element=element;this.name="peer-assessment";new OpenAssessment.ToggleControl(this.element,"#peer_assessment_description_closed","#peer_assessment_settings_editor").install("#include_peer_assessment");this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#peer_assessment_start_date","#peer_assessment_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#peer_assessment_due_date","#peer_assessment_due_time").install()};OpenAssessment.EditPeerAssessmentView.prototype={description:function(){return{must_grade:this.mustGradeNum(),must_be_graded_by:this.mustBeGradedByNum(),start:this.startDatetime(),due:this.dueDatetime()}},isEnabled:function(isEnabled){var sel=$("#include_peer_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},mustGradeNum:function(num){var sel=$("#peer_assessment_must_grade",this.element);return OpenAssessment.Fields.intField(sel,num)},mustBeGradedByNum:function(num){var sel=$("#peer_assessment_graded_by",this.element);return OpenAssessment.Fields.intField(sel,num)},startDatetime:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},dueDatetime:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},getID:function(){return $(this.element).attr("id")}};OpenAssessment.EditSelfAssessmentView=function(element){this.element=element;this.name="self-assessment";new OpenAssessment.ToggleControl(this.element,"#self_assessment_description_closed","#self_assessment_settings_editor").install("#include_self_assessment");this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#self_assessment_start_date","#self_assessment_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#self_assessment_due_date","#self_assessment_due_time").install()};OpenAssessment.EditSelfAssessmentView.prototype={description:function(){return{start:this.startDatetime(),due:this.dueDatetime()}},isEnabled:function(isEnabled){var sel=$("#include_self_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},startDatetime:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},dueDatetime:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},getID:function(){return $(this.element).attr("id")}};OpenAssessment.EditStudentTrainingView=function(element){this.element=element;this.name="student-training";new OpenAssessment.ToggleControl(this.element,"#student_training_description_closed","#student_training_settings_editor").install("#include_student_training");this.exampleContainer=new OpenAssessment.Container(OpenAssessment.TrainingExample,{containerElement:$("#openassessment_training_example_list",this.element).get(0),templateElement:$("#openassessment_training_example_template",this.element).get(0),addButtonElement:$(".openassessment_add_training_example",this.element).get(0),removeButtonClass:"openassessment_training_example_remove",containerItemClass:"openassessment_training_example"})};OpenAssessment.EditStudentTrainingView.prototype={description:function(){return{examples:this.exampleContainer.getItemValues()}},isEnabled:function(isEnabled){var sel=$("#include_student_training",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},getID:function(){return $(this.element).attr("id")}};OpenAssessment.EditExampleBasedAssessmentView=function(element){this.element=element;this.name="example-based-assessment";new OpenAssessment.ToggleControl(this.element,"#ai_assessment_description_closed","#ai_assessment_settings_editor").install("#include_ai_assessment")};OpenAssessment.EditExampleBasedAssessmentView.prototype={description:function(){return{examples_xml:this.exampleDefinitions()}},isEnabled:function(isEnabled){var sel=$("#include_ai_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},exampleDefinitions:function(xml){var sel=$("#ai_training_examples",this.element);return OpenAssessment.Fields.stringField(sel,xml)},getID:function(){return $(this.element).attr("id")}};OpenAssessment.Fields={stringField:function(sel,value){if(typeof value!=="undefined"){sel.val(value)}return sel.val()},intField:function(sel,value){if(typeof value!=="undefined"){sel.val(value)}return parseInt(sel.val(),10)},booleanField:function(sel,value){if(typeof value!=="undefined"){sel.prop("checked",value)}return sel.prop("checked")}};OpenAssessment.ToggleControl=function(element,hiddenSelector,shownSelector){this.element=element;this.hiddenSelector=hiddenSelector;this.shownSelector=shownSelector};OpenAssessment.ToggleControl.prototype={install:function(checkboxSelector){$(checkboxSelector,this.element).change(this,function(event){var control=event.data;if(this.checked){control.show()}else{control.hide()}});return this},show:function(){$(this.hiddenSelector,this.element).addClass("is--hidden");$(this.shownSelector,this.element).removeClass("is--hidden")},hide:function(){$(this.hiddenSelector,this.element).removeClass("is--hidden");$(this.shownSelector,this.element).addClass("is--hidden")}};OpenAssessment.DatetimeControl=function(element,datePicker,timePicker){this.element=element;this.datePicker=datePicker;this.timePicker=timePicker};OpenAssessment.DatetimeControl.prototype={install:function(){var dateString=$(this.datePicker,this.element).val();$(this.datePicker,this.element).datepicker({showButtonPanel:true}).datepicker("option","dateFormat","yy-mm-dd").datepicker("setDate",dateString);$(this.timePicker,this.element).timepicker({timeFormat:"H:i",step:60});return this},datetime:function(dateString,timeString){var datePickerSel=$(this.datePicker,this.element);var timePickerSel=$(this.timePicker,this.element);if(typeof dateString!=="undefined"){datePickerSel.datepicker("setDate",dateString)}if(typeof timeString!=="undefined"){timePickerSel.val(timeString)}if(datePickerSel.val()===""&&timePickerSel.val()===""){return null}return datePickerSel.val()+"T"+timePickerSel.val()}};OpenAssessment.StudentTrainingListener=function(){this.element=$("#oa_student_training_editor");this.alert=new OpenAssessment.ValidationAlert($("#openassessment_rubric_validation_alert"))};OpenAssessment.StudentTrainingListener.prototype={optionUpdated:function(data){var view=this;var sel='.openassessment_training_example_criterion[data-criterion="'+data.criterionName+'"]';$(sel,this.element).each(function(){var criterion=this;var option=$('option[value="'+data.name+'"]',criterion);$(option).text(view._generateOptionString(data.label,data.points))})},optionAdd:function(data){var options=$('.openassessment_training_example_criterion_option[data-criterion="'+data.criterionName+'"]');var view=this;var criterionAdded=false;var examplesUpdated=false;if(options.length===0){this.criterionAdd(data);criterionAdded=true}$(".openassessment_training_example_criterion_option",this.element).each(function(){if($(this).data("criterion")===data.criterionName){var criterion=this;$(criterion).append($("<option></option>").attr("value",data.name).text(view._generateOptionString(data.label,data.points)));examplesUpdated=true}});if(criterionAdded&&examplesUpdated){this.displayAlertMsg(gettext("Criterion Addition requires Training Example Updates"),gettext("Because you added a criterion, student training examples will have to be updated."))}},optionRemove:function(data){var handler=this;var invalidated=false;$(".openassessment_training_example_criterion_option",this.element).each(function(){var criterionOption=this;if($(criterionOption).data("criterion")===data.criterionName){if($(criterionOption).val()===data.name.toString()){$(criterionOption).val("");$(criterionOption).addClass("openassessment_highlighted_field");$(criterionOption).click(function(){$(criterionOption).removeClass("openassessment_highlighted_field")});invalidated=true}$('option[value="'+data.name+'"]',criterionOption).remove();if($("option",criterionOption).length==1){handler.removeAllOptions(data);invalidated=false}}});if(invalidated){this.displayAlertMsg(gettext("Option Deletion Led to Invalidation"),gettext("Because you deleted an option, some student training examples had to be reset."))}},removeAllOptions:function(data){var changed=false;$(".openassessment_training_example_criterion",this.element).each(function(){var criterion=this;if($(criterion).data("criterion")==data.criterionName){$(criterion).remove();changed=true}});if(changed){this.displayAlertMsg(gettext("Option Deletion Led to Invalidation"),gettext("The deletion of the last criterion option caused the criterion to be removed in the student training examples."))}},criterionRemove:function(data){var changed=false;var sel='.openassessment_training_example_criterion[data-criterion="'+data.criterionName+'"]';$(sel,this.element).each(function(){$(this).remove();changed=true});if(changed){this.displayAlertMsg(gettext("Criterion Deletion Led to Invalidation"),gettext("Because you deleted a criterion, there were student training examples where the criterion had to be removed."))}},displayAlertMsg:function(title,msg){this.alert.setMessage(title,msg);this.alert.show()},criterionUpdated:function(data){var sel='.openassessment_training_example_criterion[data-criterion="'+data.criterionName+'"]';$(sel,this.element).each(function(){$(".openassessment_training_example_criterion_name_wrapper",this).text(data.criterionLabel)})},criterionAdd:function(data){var view=this.element;var criterion=$("#openassessment_training_example_criterion_template").children().first().clone().removeAttr("id").attr("data-criterion",data.criterionName).toggleClass("is--hidden",false).appendTo(".openassessment_training_example_criteria_selections",view);criterion.find(".openassessment_training_example_criterion_option").attr("data-criterion",data.criterionName);criterion.find(".openassessment_training_example_criterion_name_wrapper").text(data.label)},examplesCriteriaLabels:function(){var examples=[];$(".openassessment_training_example_criteria_selections",this.element).each(function(){var exampleDescription={};$(".openassessment_training_example_criterion",this).each(function(){var criterionName=$(this).data("criterion");var criterionLabel=$(".openassessment_training_example_criterion_name_wrapper",this).text().trim();exampleDescription[criterionName]=criterionLabel});examples.push(exampleDescription)});return examples},examplesOptionsLabels:function(){var examples=[];$(".openassessment_training_example_criteria_selections",this.element).each(function(){var exampleDescription={};$(".openassessment_training_example_criterion_option",this).each(function(){var criterionName=$(this).data("criterion");exampleDescription[criterionName]={};$("option",this).each(function(){var optionName=$(this).val();var optionLabel=$(this).text().trim();exampleDescription[criterionName][optionName]=optionLabel})});examples.push(exampleDescription)});return examples},_generateOptionString:function(name,points){return name+" - "+points+gettext(" points")}};OpenAssessment.Notifier=function(listeners){this.listeners=listeners};OpenAssessment.Notifier.prototype={notificationFired:function(name,data){for(var i=0;i<this.listeners.length;i++){if(typeof this.listeners[i][name]==="function"){this.listeners[i][name](data)}}}};OpenAssessment.EditPromptView=function(element){this.element=element};OpenAssessment.EditPromptView.prototype={promptText:function(text){var sel=$("#openassessment_prompt_editor",this.element);return OpenAssessment.Fields.stringField(sel,text)}};OpenAssessment.EditRubricView=function(element,notifier){this.element=element;this.criteriaContainer=new OpenAssessment.Container(OpenAssessment.RubricCriterion,{containerElement:$("#openassessment_criterion_list",this.element).get(0),templateElement:$("#openassessment_criterion_template",this.element).get(0),addButtonElement:$("#openassessment_rubric_add_criterion",this.element).get(0),removeButtonClass:"openassessment_criterion_remove_button",containerItemClass:"openassessment_criterion",notifier:notifier});this.alert=new OpenAssessment.ValidationAlert($("#openassessment_rubric_validation_alert",this.element))};OpenAssessment.EditRubricView.prototype={criteriaDefinition:function(){var criteria=this.criteriaContainer.getItemValues();for(var criterion_idx=0;criterion_idx<criteria.length;criterion_idx++){var criterion=criteria[criterion_idx];criterion.order_num=criterion_idx;for(var option_idx=0;option_idx<criterion.options.length;option_idx++){var option=criterion.options[option_idx];option.order_num=option_idx}}return criteria},feedbackPrompt:function(text){var sel=$("#openassessment_rubric_feedback",this.element);return OpenAssessment.Fields.stringField(sel,text)},addCriterion:function(){this.criteriaContainer.add()},removeCriterion:function(item){this.criteriaContainer.remove(item)},getAllCriteria:function(){return this.criteriaContainer.getAllItems()},getCriterionItem:function(index){return this.criteriaContainer.getItem(index)},addOption:function(criterionIndex){var criterionItem=this.getCriterionItem(criterionIndex);criterionItem.optionContainer.add()},removeOption:function(criterionIndex,item){var criterionItem=this.getCriterionItem(criterionIndex);criterionItem.optionContainer.remove(item)},getAllOptions:function(criterionIndex){var criterionItem=this.getCriterionItem(criterionIndex);return criterionItem.optionContainer.getAllItems()},getOptionItem:function(criterionIndex,optionIndex){var criterionItem=this.getCriterionItem(criterionIndex);return criterionItem.optionContainer.getItem(optionIndex)}};OpenAssessment.EditSettingsView=function(element,assessmentViews){this.settingsElement=element;this.assessmentsElement=$(element).siblings("#openassessment_assessment_module_settings_editors").get(0);this.assessmentViews=assessmentViews;this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#openassessment_submission_start_date","#openassessment_submission_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#openassessment_submission_due_date","#openassessment_submission_due_time").install();this.initializeSortableAssessments()};OpenAssessment.EditSettingsView.prototype={initializeSortableAssessments:function(){var view=this; -$("#openassessment_assessment_module_settings_editors",view.element).sortable({start:function(event,ui){$(".openassessment_assessment_module_editor",view.element).hide();var targetHeight="auto";ui.placeholder.height(targetHeight);ui.helper.height(targetHeight);$("#openassessment_assessment_module_settings_editors",view.element).sortable("refresh").sortable("refreshPositions")},stop:function(event,ui){$(".openassessment_assessment_module_editor",view.element).show()},snap:true,axis:"y",handle:".drag-handle",cursorAt:{top:20}});$("#openassessment_assessment_module_settings_editors .drag-handle",view.element).disableSelection()},displayName:function(name){var sel=$("#openassessment_title_editor",this.settingsElement);return OpenAssessment.Fields.stringField(sel,name)},submissionStart:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},submissionDue:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},imageSubmissionEnabled:function(isEnabled){var sel=$("#openassessment_submission_image_editor",this.settingsElement);if(typeof isEnabled!=="undefined"){if(isEnabled){sel.val(1)}else{sel.val(0)}}return sel.val()==1},assessmentsDescription:function(){var assessmentDescList=[];var view=this;$(".openassessment_assessment_module_settings_editor",this.assessmentsElement).each(function(){var asmntView=view.assessmentViews[$(this).attr("id")];if(asmntView.isEnabled()){var description=asmntView.description();description["name"]=asmntView.name;assessmentDescList.push(description)}});return assessmentDescList},editorAssessmentsOrder:function(){var editorAssessments=[];var view=this;$(".openassessment_assessment_module_settings_editor",this.assessmentsElement).each(function(){var asmntView=view.assessmentViews[$(this).attr("id")];editorAssessments.push(asmntView.name)});return editorAssessments}};OpenAssessment.ValidationAlert=function(element){var alert=this;this.element=element;this.rubricContentElement=$("#openassessment_rubric_content_editor");this.title=$(".openassessment_alert_title",this.element);this.message=$(".openassessment_alert_message",this.element);$(".openassessment_alert_close",element).click(function(eventObject){eventObject.preventDefault();alert.hide()})};OpenAssessment.ValidationAlert.prototype={hide:function(){this.element.addClass("is--hidden");this.rubricContentElement.removeClass("openassessment_alert_shown")},show:function(){this.element.removeClass("is--hidden");this.rubricContentElement.addClass("openassessment_alert_shown")},setMessage:function(newTitle,newMessage){this.title.text(newTitle);this.message.text(newMessage)},isVisible:function(){return!this.element.hasClass("is--hidden")},getTitle:function(){return this.title.text()},getMessage:function(){return this.message.text()}}; \ No newline at end of file +if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.Logger==="undefined"){window.Logger={log:function(event_type,data,kwargs){}}}if(typeof OpenAssessment.Server=="undefined"||!OpenAssessment.Server){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,criterionFeedback,overallFeedback){var url=this.url("self_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.")])})})},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.")])})})},scheduleTraining:function(){var url=this.url("schedule_training");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},rescheduleUnfinishedTasks:function(){var url=this.url("reschedule_unfinished_tasks");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("One or more rescheduling tasks failed.")])})})},updateEditorContext:function(kwargs){var url=this.url("update_editor_context");var payload=JSON.stringify({prompt:kwargs.prompt,feedback_prompt:kwargs.feedbackPrompt,title:kwargs.title,submission_start:kwargs.submissionStart,submission_due:kwargs.submissionDue,criteria:kwargs.criteria,assessments:kwargs.assessments,editor_assessments_order:kwargs.editorAssessmentsOrder,allow_file_upload:kwargs.imageSubmissionEnabled});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()},getUploadUrl:function(contentType){var url=this.url("upload_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({contentType:contentType})}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("Could not retrieve upload url.")])})}).promise()},getDownloadUrl:function(){var url=this.url("download_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({})}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("Could not retrieve download url.")])})}).promise()}}}if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.Logger==="undefined"){window.Logger={log:function(event_type,data,kwargs){}}}OpenAssessment.Container=function(containerItem,kwargs){this.containerElement=kwargs.containerElement;this.templateElement=kwargs.templateElement;this.addButtonElement=kwargs.addButtonElement;this.removeButtonClass=kwargs.removeButtonClass;this.containerItemClass=kwargs.containerItemClass;this.notifier=kwargs.notifier;var container=this;this.createContainerItem=function(element){return new containerItem(element,container.notifier)};$(this.addButtonElement).click($.proxy(this.add,this));$("."+this.removeButtonClass,this.containerElement).click(function(eventData){var item=container.createContainerItem(eventData.target);container.remove(item)});$("."+this.containerItemClass,this.containerElement).each(function(index,element){container.createContainerItem(element)})};OpenAssessment.Container.prototype={add:function(){$(this.templateElement).children().first().clone().removeAttr("id").toggleClass("is--hidden",false).toggleClass(this.containerItemClass,true).appendTo($(this.containerElement));var container=this;var containerItem=$("."+this.containerItemClass,this.containerElement).last();containerItem.find("."+this.removeButtonClass).click(function(eventData){var containerItem=container.createContainerItem(eventData.target);container.remove(containerItem)});var handlerItem=container.createContainerItem(containerItem);handlerItem.addHandler()},remove:function(item){var itemElement=$(item.element).closest("."+this.containerItemClass);var containerItem=this.createContainerItem(itemElement);containerItem.removeHandler();itemElement.remove()},getItemValues:function(){var values=[];var container=this;$("."+this.containerItemClass,this.containerElement).each(function(index,element){var containerItem=container.createContainerItem(element);var fieldValues=containerItem.getFieldValues();values.push(fieldValues)});return values},getItem:function(index){var element=$("."+this.containerItemClass,this.containerElement).get(index);return element!==undefined?this.createContainerItem(element):null},getAllItems:function(){var container=this;return $("."+this.containerItemClass,this.containerElement).map(function(){return container.createContainerItem(this)})}};OpenAssessment.ItemUtilities={createUniqueName:function(selector,nameAttribute){var index=0;while(index<=selector.length){if(selector.parent().find("*["+nameAttribute+"='"+index+"']").length===0){return index.toString()}index++}return index.toString()}};OpenAssessment.RubricOption=function(element,notifier){this.element=element;this.notifier=notifier;this.MAX_POINTS=1e3;$(this.element).focusout($.proxy(this.updateHandler,this))};OpenAssessment.RubricOption.prototype={getFieldValues:function(){var fields={label:this.label(),points:this.points(),explanation:this.explanation()};var nameString=OpenAssessment.Fields.stringField($(".openassessment_criterion_option_name",this.element));if(nameString!==""){fields.name=nameString}return fields},label:function(label){var sel=$(".openassessment_criterion_option_label",this.element);return OpenAssessment.Fields.stringField(sel,label)},points:function(points){var sel=$(".openassessment_criterion_option_points",this.element);return OpenAssessment.Fields.intField(sel,points)},explanation:function(explanation){var sel=$(".openassessment_criterion_option_explanation",this.element);return OpenAssessment.Fields.stringField(sel,explanation)},addHandler:function(){var criterionElement=$(this.element).closest(".openassessment_criterion");var criterionName=$(criterionElement).data("criterion");var criterionLabel=$(".openassessment_criterion_label",criterionElement).val();var options=$(".openassessment_criterion_option",this.element.parent());var name=OpenAssessment.ItemUtilities.createUniqueName(options,"data-option");$(this.element).attr("data-criterion",criterionName).attr("data-option",name);$(".openassessment_criterion_option_name",this.element).attr("value",name);var fields=this.getFieldValues();this.notifier.notificationFired("optionAdd",{criterionName:criterionName,criterionLabel:criterionLabel,name:name,label:fields.label,points:fields.points})},removeHandler:function(){var criterionName=$(this.element).data("criterion");var optionName=$(this.element).data("option");this.notifier.notificationFired("optionRemove",{criterionName:criterionName,name:optionName})},updateHandler:function(){var fields=this.getFieldValues();var criterionName=$(this.element).data("criterion");var optionName=$(this.element).data("option");var optionLabel=fields.label;var optionPoints=fields.points;this.notifier.notificationFired("optionUpdated",{criterionName:criterionName,name:optionName,label:optionLabel,points:optionPoints})},validate:function(){var pointString=$(".openassessment_criterion_option_points",this.element).val();var matches=pointString.trim().match(/^\d{1,3}$/g);var isValid=matches!==null;if(!isValid){$(".openassessment_criterion_option_points",this.element).addClass("openassessment_highlighted_field")}return isValid},validationErrors:function(){var sel=$(".openassessment_criterion_option_points",this.element);var hasError=sel.hasClass("openassessment_highlighted_field");return hasError?["Option points are invalid"]:[]},clearValidationErrors:function(){$(".openassessment_criterion_option_points",this.element).removeClass("openassessment_highlighted_field")}};OpenAssessment.RubricCriterion=function(element,notifier){this.element=element;this.notifier=notifier;this.optionContainer=new OpenAssessment.Container(OpenAssessment.RubricOption,{containerElement:$(".openassessment_criterion_option_list",this.element).get(0),templateElement:$("#openassessment_option_template").get(0),addButtonElement:$(".openassessment_criterion_add_option",this.element).get(0),removeButtonClass:"openassessment_criterion_option_remove_button",containerItemClass:"openassessment_criterion_option",notifier:this.notifier});$(this.element).focusout($.proxy(this.updateHandler,this))};OpenAssessment.RubricCriterion.prototype={getFieldValues:function(){var fields={label:this.label(),prompt:this.prompt(),feedback:this.feedback(),options:this.optionContainer.getItemValues()};var nameString=OpenAssessment.Fields.stringField($(".openassessment_criterion_name",this.element));if(nameString!==""){fields.name=nameString}return fields},label:function(label){var sel=$(".openassessment_criterion_label",this.element);return OpenAssessment.Fields.stringField(sel,label)},prompt:function(prompt){var sel=$(".openassessment_criterion_prompt",this.element);return OpenAssessment.Fields.stringField(sel,prompt)},feedback:function(){return $(".openassessment_criterion_feedback",this.element).val()},addOption:function(){this.optionContainer.add()},addHandler:function(){var criteria=$(".openassessment_criterion",this.element.parent());var name=OpenAssessment.ItemUtilities.createUniqueName(criteria,"data-criterion");$(this.element).attr("data-criterion",name);$(".openassessment_criterion_name",this.element).attr("value",name)},removeHandler:function(){var criterionName=$(this.element).data("criterion");this.notifier.notificationFired("criterionRemove",{criterionName:criterionName})},updateHandler:function(){var fields=this.getFieldValues();var criterionName=fields.name;var criterionLabel=fields.label;this.notifier.notificationFired("criterionUpdated",{criterionName:criterionName,criterionLabel:criterionLabel})},validate:function(){var isValid=true;$.each(this.optionContainer.getAllItems(),function(){isValid=isValid&&this.validate()});return isValid},validationErrors:function(){var errors=[];$.each(this.optionContainer.getAllItems(),function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){$.each(this.optionContainer.getAllItems(),function(){this.clearValidationErrors()})}};OpenAssessment.TrainingExample=function(element){this.element=element};OpenAssessment.TrainingExample.prototype={getFieldValues:function(){var optionsSelected=[];$(".openassessment_training_example_criterion_option",this.element).each(function(){optionsSelected.push({criterion:$(this).data("criterion"),option:$(this).prop("value")})});return{answer:$(".openassessment_training_example_essay",this.element).first().prop("value"),options_selected:optionsSelected}},addHandler:function(){},removeHandler:function(){},updateHandler:function(){},validate:function(){return true},validationErrors:function(){return[]},clearValidationErrors:function(){}};OpenAssessment.StudioView=function(runtime,element,server){this.element=element;this.runtime=runtime;this.server=server;this.fixModalHeight();this.initializeTabs();this.validationAlert=new OpenAssessment.ValidationAlert;this.validationAlert.installEventHandlers();this.promptView=new OpenAssessment.EditPromptView($("#oa_prompt_editor_wrapper",this.element).get(0));var studentTrainingView=new OpenAssessment.EditStudentTrainingView($("#oa_student_training_editor",this.element).get(0));var peerAssessmentView=new OpenAssessment.EditPeerAssessmentView($("#oa_peer_assessment_editor",this.element).get(0));var selfAssessmentView=new OpenAssessment.EditSelfAssessmentView($("#oa_self_assessment_editor",this.element).get(0));var exampleBasedAssessmentView=new OpenAssessment.EditExampleBasedAssessmentView($("#oa_ai_assessment_editor",this.element).get(0));var assessmentLookupDictionary={};assessmentLookupDictionary[studentTrainingView.getID()]=studentTrainingView;assessmentLookupDictionary[peerAssessmentView.getID()]=peerAssessmentView;assessmentLookupDictionary[selfAssessmentView.getID()]=selfAssessmentView;assessmentLookupDictionary[exampleBasedAssessmentView.getID()]=exampleBasedAssessmentView;this.settingsView=new OpenAssessment.EditSettingsView($("#oa_basic_settings_editor",this.element).get(0),assessmentLookupDictionary);this.rubricView=new OpenAssessment.EditRubricView($("#oa_rubric_editor_wrapper",this.element).get(0),new OpenAssessment.Notifier([new OpenAssessment.StudentTrainingListener]));$(".openassessment_save_button",this.element).click($.proxy(this.save,this));$(".openassessment_cancel_button",this.element).click($.proxy(this.cancel,this))};OpenAssessment.StudioView.prototype={fixModalHeight:function(){$(this.element).addClass("openassessment_full_height").parentsUntil(".modal-window").addClass("openassessment_full_height");$(this.element).closest(".modal-window").addClass("openassessment_modal_window")},initializeTabs:function(){if(typeof OpenAssessment.lastOpenEditingTab==="undefined"){OpenAssessment.lastOpenEditingTab=2}$(".openassessment_editor_content_and_tabs",this.element).tabs({active:OpenAssessment.lastOpenEditingTab})},saveTabState:function(){var tabElement=$(".openassessment_editor_content_and_tabs",this.element);OpenAssessment.lastOpenEditingTab=tabElement.tabs("option","active")},save:function(){var view=this;this.saveTabState();this.validationAlert.hide();this.clearValidationErrors();if(!this.validate()){this.validationAlert.setMessage(gettext("Validation Errors"),gettext("Some fields are not valid. Please update the fields.")).show()}else{this.server.checkReleased().done(function(isReleased){if(isReleased){view.confirmPostReleaseUpdate($.proxy(view.updateEditorContext,view))}else{view.updateEditorContext()}}).fail(function(errMsg){view.showError(errMsg)})}},confirmPostReleaseUpdate:function(onConfirm){var msg=gettext("This problem has already been released. Any changes will apply only to future assessments.");if(confirm(msg)){onConfirm()}},updateEditorContext:function(){this.runtime.notify("save",{state:"start"});var view=this;this.server.updateEditorContext({prompt:view.promptView.promptText(),feedbackPrompt:view.rubricView.feedbackPrompt(),criteria:view.rubricView.criteriaDefinition(),title:view.settingsView.displayName(),submissionStart:view.settingsView.submissionStart(),submissionDue:view.settingsView.submissionDue(),assessments:view.settingsView.assessmentsDescription(),imageSubmissionEnabled:view.settingsView.imageSubmissionEnabled(),editorAssessmentsOrder:view.settingsView.editorAssessmentsOrder()}).done(function(){view.runtime.notify("save",{state:"end"})}).fail(function(msg){view.showError(msg)})},cancel:function(){this.saveTabState();this.runtime.notify("cancel",{})},showError:function(errorMsg){this.runtime.notify("error",{msg:errorMsg})},validate:function(){return this.settingsView.validate()&&this.rubricView.validate()},validationErrors:function(){return this.settingsView.validationErrors().concat(this.rubricView.validationErrors())},clearValidationErrors:function(){this.settingsView.clearValidationErrors();this.rubricView.clearValidationErrors()}};function OpenAssessmentEditor(runtime,element){var server=new OpenAssessment.Server(runtime,element);var view=new OpenAssessment.StudioView(runtime,element,server)}OpenAssessment.EditPeerAssessmentView=function(element){this.element=element;this.name="peer-assessment";new OpenAssessment.ToggleControl(this.element,"#peer_assessment_description_closed","#peer_assessment_settings_editor").install("#include_peer_assessment");this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#peer_assessment_start_date","#peer_assessment_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#peer_assessment_due_date","#peer_assessment_due_time").install()};OpenAssessment.EditPeerAssessmentView.prototype={description:function(){return{must_grade:this.mustGradeNum(),must_be_graded_by:this.mustBeGradedByNum(),start:this.startDatetime(),due:this.dueDatetime()}},isEnabled:function(isEnabled){var sel=$("#include_peer_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},mustGradeNum:function(num){var sel=$("#peer_assessment_must_grade",this.element);return OpenAssessment.Fields.intField(sel,num)},mustBeGradedByNum:function(num){var sel=$("#peer_assessment_graded_by",this.element);return OpenAssessment.Fields.intField(sel,num)},startDatetime:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},dueDatetime:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},getID:function(){return $(this.element).attr("id")},validate:function(){return this.startDatetimeControl.validate()&&this.dueDatetimeControl.validate()},validationErrors:function(){var errors=[];if(this.startDatetimeControl.validationErrors().length>0){errors.push("Peer assessment start is invalid")}if(this.dueDatetimeControl.validationErrors().length>0){errors.push("Peer assessment due is invalid")}return errors},clearValidationErrors:function(){this.startDatetimeControl.clearValidationErrors();this.dueDatetimeControl.clearValidationErrors()}};OpenAssessment.EditSelfAssessmentView=function(element){this.element=element;this.name="self-assessment";new OpenAssessment.ToggleControl(this.element,"#self_assessment_description_closed","#self_assessment_settings_editor").install("#include_self_assessment");this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#self_assessment_start_date","#self_assessment_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#self_assessment_due_date","#self_assessment_due_time").install()};OpenAssessment.EditSelfAssessmentView.prototype={description:function(){return{start:this.startDatetime(),due:this.dueDatetime()}},isEnabled:function(isEnabled){var sel=$("#include_self_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},startDatetime:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},dueDatetime:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},getID:function(){return $(this.element).attr("id")},validate:function(){return this.startDatetimeControl.validate()&&this.dueDatetimeControl.validate()},validationErrors:function(){var errors=[];if(this.startDatetimeControl.validationErrors().length>0){errors.push("Self assessment start is invalid")}if(this.dueDatetimeControl.validationErrors().length>0){errors.push("Self assessment due is invalid")}return errors},clearValidationErrors:function(){this.startDatetimeControl.clearValidationErrors();this.dueDatetimeControl.clearValidationErrors()}};OpenAssessment.EditStudentTrainingView=function(element){this.element=element;this.name="student-training";new OpenAssessment.ToggleControl(this.element,"#student_training_description_closed","#student_training_settings_editor").install("#include_student_training");this.exampleContainer=new OpenAssessment.Container(OpenAssessment.TrainingExample,{containerElement:$("#openassessment_training_example_list",this.element).get(0),templateElement:$("#openassessment_training_example_template",this.element).get(0),addButtonElement:$(".openassessment_add_training_example",this.element).get(0),removeButtonClass:"openassessment_training_example_remove",containerItemClass:"openassessment_training_example"})};OpenAssessment.EditStudentTrainingView.prototype={description:function(){return{examples:this.exampleContainer.getItemValues()}},isEnabled:function(isEnabled){var sel=$("#include_student_training",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},getID:function(){return $(this.element).attr("id")},validate:function(){return true},validationErrors:function(){return[]},clearValidationErrors:function(){}};OpenAssessment.EditExampleBasedAssessmentView=function(element){this.element=element;this.name="example-based-assessment";new OpenAssessment.ToggleControl(this.element,"#ai_assessment_description_closed","#ai_assessment_settings_editor").install("#include_ai_assessment")};OpenAssessment.EditExampleBasedAssessmentView.prototype={description:function(){return{examples_xml:this.exampleDefinitions()}},isEnabled:function(isEnabled){var sel=$("#include_ai_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},exampleDefinitions:function(xml){var sel=$("#ai_training_examples",this.element);return OpenAssessment.Fields.stringField(sel,xml)},getID:function(){return $(this.element).attr("id")},validate:function(){return true},validationErrors:function(){return[]},clearValidationErrors:function(){}};OpenAssessment.Fields={stringField:function(sel,value){if(typeof value!=="undefined"){sel.val(value)}return sel.val()},intField:function(sel,value){if(typeof value!=="undefined"){sel.val(value)}return parseInt(sel.val(),10)},booleanField:function(sel,value){if(typeof value!=="undefined"){sel.prop("checked",value)}return sel.prop("checked")}};OpenAssessment.ToggleControl=function(element,hiddenSelector,shownSelector){this.element=element;this.hiddenSelector=hiddenSelector;this.shownSelector=shownSelector};OpenAssessment.ToggleControl.prototype={install:function(checkboxSelector){$(checkboxSelector,this.element).change(this,function(event){var control=event.data;if(this.checked){control.show()}else{control.hide()}});return this},show:function(){$(this.hiddenSelector,this.element).addClass("is--hidden");$(this.shownSelector,this.element).removeClass("is--hidden")},hide:function(){$(this.hiddenSelector,this.element).removeClass("is--hidden");$(this.shownSelector,this.element).addClass("is--hidden")}};OpenAssessment.DatetimeControl=function(element,datePicker,timePicker){this.element=element;this.datePicker=datePicker;this.timePicker=timePicker};OpenAssessment.DatetimeControl.prototype={install:function(){var dateString=$(this.datePicker,this.element).val();$(this.datePicker,this.element).datepicker({showButtonPanel:true}).datepicker("option","dateFormat","yy-mm-dd").val(dateString);$(this.timePicker,this.element).timepicker({timeFormat:"H:i",step:60});return this},datetime:function(dateString,timeString){var datePickerSel=$(this.datePicker,this.element);var timePickerSel=$(this.timePicker,this.element);if(typeof dateString!=="undefined"){datePickerSel.val(dateString)}if(typeof timeString!=="undefined"){timePickerSel.val(timeString)}return datePickerSel.val()+"T"+timePickerSel.val()},validate:function(){var datetimeString=this.datetime();var matches=datetimeString.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/g);var isValid=matches!==null;if(!isValid){$(this.datePicker,this.element).addClass("openassessment_highlighted_field");$(this.timePicker,this.element).addClass("openassessment_highlighted_field")}return isValid},clearValidationErrors:function(){$(this.datePicker,this.element).removeClass("openassessment_highlighted_field");$(this.timePicker,this.element).removeClass("openassessment_highlighted_field")},validationErrors:function(){var errors=[];var dateHasError=$(this.datePicker,this.element).hasClass("openassessment_highlighted_field");var timeHasError=$(this.timePicker,this.element).hasClass("openassessment_highlighted_field");if(dateHasError){errors.push("Date is invalid")}if(timeHasError){errors.push("Time is invalid")}return errors}};OpenAssessment.StudentTrainingListener=function(){this.element=$("#oa_student_training_editor");this.alert=new OpenAssessment.ValidationAlert($("#openassessment_rubric_validation_alert"))};OpenAssessment.StudentTrainingListener.prototype={optionUpdated:function(data){var view=this;var sel='.openassessment_training_example_criterion[data-criterion="'+data.criterionName+'"]';$(sel,this.element).each(function(){var criterion=this;var option=$('option[value="'+data.name+'"]',criterion);$(option).text(view._generateOptionString(data.label,data.points))})},optionAdd:function(data){var options=$('.openassessment_training_example_criterion_option[data-criterion="'+data.criterionName+'"]');var view=this;var criterionAdded=false;var examplesUpdated=false;if(options.length===0){this.criterionAdd(data);criterionAdded=true}$(".openassessment_training_example_criterion_option",this.element).each(function(){if($(this).data("criterion")===data.criterionName){var criterion=this;$(criterion).append($("<option></option>").attr("value",data.name).text(view._generateOptionString(data.label,data.points)));examplesUpdated=true}});if(criterionAdded&&examplesUpdated){this.displayAlertMsg(gettext("Criterion Addition requires Training Example Updates"),gettext("Because you added a criterion, student training examples will have to be updated."))}},optionRemove:function(data){var handler=this;var invalidated=false;$(".openassessment_training_example_criterion_option",this.element).each(function(){var criterionOption=this;if($(criterionOption).data("criterion")===data.criterionName){if($(criterionOption).val()===data.name.toString()){$(criterionOption).val("");$(criterionOption).addClass("openassessment_highlighted_field");$(criterionOption).click(function(){$(criterionOption).removeClass("openassessment_highlighted_field")});invalidated=true}$('option[value="'+data.name+'"]',criterionOption).remove();if($("option",criterionOption).length==1){handler.removeAllOptions(data);invalidated=false}}});if(invalidated){this.displayAlertMsg(gettext("Option Deletion Led to Invalidation"),gettext("Because you deleted an option, some student training examples had to be reset."))}},removeAllOptions:function(data){var changed=false;$(".openassessment_training_example_criterion",this.element).each(function(){var criterion=this;if($(criterion).data("criterion")==data.criterionName){$(criterion).remove();changed=true}});if(changed){this.displayAlertMsg(gettext("Option Deletion Led to Invalidation"),gettext("The deletion of the last criterion option caused the criterion to be removed in the student training examples."))}},criterionRemove:function(data){var changed=false;var sel='.openassessment_training_example_criterion[data-criterion="'+data.criterionName+'"]';$(sel,this.element).each(function(){$(this).remove();changed=true});if(changed){this.displayAlertMsg(gettext("Criterion Deletion Led to Invalidation"),gettext("Because you deleted a criterion, there were student training examples where the criterion had to be removed."))}},displayAlertMsg:function(title,msg){this.alert.setMessage(title,msg);this.alert.show()},criterionUpdated:function(data){var sel='.openassessment_training_example_criterion[data-criterion="'+data.criterionName+'"]';$(sel,this.element).each(function(){$(".openassessment_training_example_criterion_name_wrapper",this).text(data.criterionLabel)})},criterionAdd:function(data){var view=this.element;var criterion=$("#openassessment_training_example_criterion_template").children().first().clone().removeAttr("id").attr("data-criterion",data.criterionName).toggleClass("is--hidden",false).appendTo(".openassessment_training_example_criteria_selections",view);criterion.find(".openassessment_training_example_criterion_option").attr("data-criterion",data.criterionName);criterion.find(".openassessment_training_example_criterion_name_wrapper").text(data.label)},examplesCriteriaLabels:function(){var examples=[];$(".openassessment_training_example_criteria_selections",this.element).each(function(){var exampleDescription={}; +$(".openassessment_training_example_criterion",this).each(function(){var criterionName=$(this).data("criterion");var criterionLabel=$(".openassessment_training_example_criterion_name_wrapper",this).text().trim();exampleDescription[criterionName]=criterionLabel});examples.push(exampleDescription)});return examples},examplesOptionsLabels:function(){var examples=[];$(".openassessment_training_example_criteria_selections",this.element).each(function(){var exampleDescription={};$(".openassessment_training_example_criterion_option",this).each(function(){var criterionName=$(this).data("criterion");exampleDescription[criterionName]={};$("option",this).each(function(){var optionName=$(this).val();var optionLabel=$(this).text().trim();exampleDescription[criterionName][optionName]=optionLabel})});examples.push(exampleDescription)});return examples},_generateOptionString:function(name,points){return name+" - "+points+gettext(" points")}};OpenAssessment.Notifier=function(listeners){this.listeners=listeners};OpenAssessment.Notifier.prototype={notificationFired:function(name,data){for(var i=0;i<this.listeners.length;i++){if(typeof this.listeners[i][name]==="function"){this.listeners[i][name](data)}}}};OpenAssessment.EditPromptView=function(element){this.element=element};OpenAssessment.EditPromptView.prototype={promptText:function(text){var sel=$("#openassessment_prompt_editor",this.element);return OpenAssessment.Fields.stringField(sel,text)}};OpenAssessment.EditRubricView=function(element,notifier){this.element=element;this.criteriaContainer=new OpenAssessment.Container(OpenAssessment.RubricCriterion,{containerElement:$("#openassessment_criterion_list",this.element).get(0),templateElement:$("#openassessment_criterion_template",this.element).get(0),addButtonElement:$("#openassessment_rubric_add_criterion",this.element).get(0),removeButtonClass:"openassessment_criterion_remove_button",containerItemClass:"openassessment_criterion",notifier:notifier});this.alert=new OpenAssessment.ValidationAlert($("#openassessment_rubric_validation_alert",this.element))};OpenAssessment.EditRubricView.prototype={criteriaDefinition:function(){var criteria=this.criteriaContainer.getItemValues();for(var criterion_idx=0;criterion_idx<criteria.length;criterion_idx++){var criterion=criteria[criterion_idx];criterion.order_num=criterion_idx;for(var option_idx=0;option_idx<criterion.options.length;option_idx++){var option=criterion.options[option_idx];option.order_num=option_idx}}return criteria},feedbackPrompt:function(text){var sel=$("#openassessment_rubric_feedback",this.element);return OpenAssessment.Fields.stringField(sel,text)},addCriterion:function(){this.criteriaContainer.add()},removeCriterion:function(item){this.criteriaContainer.remove(item)},getAllCriteria:function(){return this.criteriaContainer.getAllItems()},getCriterionItem:function(index){return this.criteriaContainer.getItem(index)},addOption:function(criterionIndex){var criterionItem=this.getCriterionItem(criterionIndex);criterionItem.optionContainer.add()},removeOption:function(criterionIndex,item){var criterionItem=this.getCriterionItem(criterionIndex);criterionItem.optionContainer.remove(item)},getAllOptions:function(criterionIndex){var criterionItem=this.getCriterionItem(criterionIndex);return criterionItem.optionContainer.getAllItems()},getOptionItem:function(criterionIndex,optionIndex){var criterionItem=this.getCriterionItem(criterionIndex);return criterionItem.optionContainer.getItem(optionIndex)},validate:function(){var isValid=true;$.each(this.getAllCriteria(),function(){isValid=isValid&&this.validate()});return isValid},validationErrors:function(){var errors=[];$.each(this.getAllCriteria(),function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){$.each(this.getAllCriteria(),function(){this.clearValidationErrors()})}};OpenAssessment.EditSettingsView=function(element,assessmentViews){this.settingsElement=element;this.assessmentsElement=$(element).siblings("#openassessment_assessment_module_settings_editors").get(0);this.assessmentViews=assessmentViews;this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#openassessment_submission_start_date","#openassessment_submission_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#openassessment_submission_due_date","#openassessment_submission_due_time").install();this.initializeSortableAssessments()};OpenAssessment.EditSettingsView.prototype={initializeSortableAssessments:function(){var view=this;$("#openassessment_assessment_module_settings_editors",view.element).sortable({start:function(event,ui){$(".openassessment_assessment_module_editor",view.element).hide();var targetHeight="auto";ui.placeholder.height(targetHeight);ui.helper.height(targetHeight);$("#openassessment_assessment_module_settings_editors",view.element).sortable("refresh").sortable("refreshPositions")},stop:function(event,ui){$(".openassessment_assessment_module_editor",view.element).show()},snap:true,axis:"y",handle:".drag-handle",cursorAt:{top:20}});$("#openassessment_assessment_module_settings_editors .drag-handle",view.element).disableSelection()},displayName:function(name){var sel=$("#openassessment_title_editor",this.settingsElement);return OpenAssessment.Fields.stringField(sel,name)},submissionStart:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},submissionDue:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},imageSubmissionEnabled:function(isEnabled){var sel=$("#openassessment_submission_image_editor",this.settingsElement);if(typeof isEnabled!=="undefined"){if(isEnabled){sel.val(1)}else{sel.val(0)}}return sel.val()==1},assessmentsDescription:function(){var assessmentDescList=[];var view=this;$(".openassessment_assessment_module_settings_editor",this.assessmentsElement).each(function(){var asmntView=view.assessmentViews[$(this).attr("id")];if(asmntView.isEnabled()){var description=asmntView.description();description["name"]=asmntView.name;assessmentDescList.push(description)}});return assessmentDescList},editorAssessmentsOrder:function(){var editorAssessments=[];var view=this;$(".openassessment_assessment_module_settings_editor",this.assessmentsElement).each(function(){var asmntView=view.assessmentViews[$(this).attr("id")];editorAssessments.push(asmntView.name)});return editorAssessments},validate:function(){var isValid=this.startDatetimeControl.validate()&&this.dueDatetimeControl.validate();$.each(this.assessmentViews,function(){isValid=isValid&&this.validate()});return isValid},validationErrors:function(){var errors=[];if(this.startDatetimeControl.validationErrors().length>0){errors.push("Submission start is invalid")}if(this.dueDatetimeControl.validationErrors().length>0){errors.push("Submission due is invalid")}$.each(this.assessmentViews,function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){this.startDatetimeControl.clearValidationErrors();this.dueDatetimeControl.clearValidationErrors();$.each(this.assessmentViews,function(){this.clearValidationErrors()})}};OpenAssessment.ValidationAlert=function(){this.element=$("#openassessment_rubric_validation_alert");this.rubricContentElement=$("#openassessment_rubric_content_editor");this.title=$(".openassessment_alert_title",this.element);this.message=$(".openassessment_alert_message",this.element)};OpenAssessment.ValidationAlert.prototype={installEventHandlers:function(){var alert=this;$(".openassessment_alert_close",this.element).click(function(eventObject){eventObject.preventDefault();alert.hide()})},hide:function(){this.element.addClass("is--hidden");this.rubricContentElement.removeClass("openassessment_alert_shown");return this},show:function(){this.element.removeClass("is--hidden");this.rubricContentElement.addClass("openassessment_alert_shown");return this},setMessage:function(newTitle,newMessage){this.title.text(newTitle);this.message.text(newMessage);return this},isVisible:function(){return!this.element.hasClass("is--hidden")},getTitle:function(){return this.title.text()},getMessage:function(){return this.message.text()}}; \ No newline at end of file diff --git a/openassessment/xblock/static/js/spec/studio/oa_edit.js b/openassessment/xblock/static/js/spec/studio/oa_edit.js index af4ee29..90dc1ae 100644 --- a/openassessment/xblock/static/js/spec/studio/oa_edit.js +++ b/openassessment/xblock/static/js/spec/studio/oa_edit.js @@ -45,8 +45,8 @@ describe("OpenAssessment.StudioView", function() { title: "The most important of all questions.", prompt: "How much do you like waffles?", feedbackPrompt: "", - submissionStart: null, - submissionDue: null, + submissionStart: "2014-01-02T12:15", + submissionDue: "2014-10-01T04:53", imageSubmissionEnabled: false, criteria: [ { @@ -100,15 +100,15 @@ describe("OpenAssessment.StudioView", function() { assessments: [ { name: "peer-assessment", - start: null, - due: null, + start: "2014-01-02T00:00", + due: "2014-01-03T00:00", must_grade: 5, must_be_graded_by: 3 }, { name: "self-assessment", - start: null, - due: null + start: "2014-01-04T00:00", + due: "2014-01-05T00:00" } ], editorAssessmentsOrder: [ @@ -204,4 +204,35 @@ describe("OpenAssessment.StudioView", function() { } }); }); + + it("validates fields before saving", function() { + // Initially, there should not be a validation alert + expect(view.validationAlert.isVisible()).toBe(false); + + // Introduce a validation error (date field does format invalid) + view.settingsView.submissionStart("Not a valid date!", "00:00"); + + // Try to save the view + view.save(); + + // Since there was an invalid field, expect that data was NOT sent to the server. + // Also expect that an error is displayed + expect(server.receivedData).toBe(null); + expect(view.validationAlert.isVisible()).toBe(true); + + // Expect that individual fields were highlighted + expect(view.validationErrors()).toContain( + "Submission start is invalid" + ); + + // Fix the error and try to save again + view.settingsView.submissionStart("2014-04-01", "00:00"); + view.save(); + + // Expect that the validation errors were cleared + // and that data was successfully sent to the server. + expect(view.validationErrors()).toEqual([]); + expect(view.validationAlert.isVisible()).toBe(false); + expect(server.receivedData).not.toBe(null); + }); }); diff --git a/openassessment/xblock/static/js/spec/studio/oa_edit_assessments.js b/openassessment/xblock/static/js/spec/studio/oa_edit_assessments.js index dfcbd33..5e48bc7 100644 --- a/openassessment/xblock/static/js/spec/studio/oa_edit_assessments.js +++ b/openassessment/xblock/static/js/spec/studio/oa_edit_assessments.js @@ -11,6 +11,21 @@ describe("OpenAssessment edit assessment views", function() { expect(view.isEnabled()).toBe(true); }; + var testValidateDate = function(view, datetimeControl, expectedError) { + // Test an invalid datetime + datetimeControl.datetime("invalid", "invalid"); + expect(view.validate()).toBe(false); + expect(view.validationErrors()).toContain(expectedError); + + // Clear validation errors (simulate re-saving) + view.clearValidationErrors(); + + // Test a valid datetime + datetimeControl.datetime("2014-04-05", "00:00"); + expect(view.validate()).toBe(true); + expect(view.validationErrors()).toEqual([]); + }; + var testLoadXMLExamples = function(view) { var xml = "XML DEFINITIONS WOULD BE HERE"; view.exampleDefinitions(xml); @@ -28,11 +43,13 @@ describe("OpenAssessment edit assessment views", function() { beforeEach(function() { var element = $("#oa_peer_assessment_editor").get(0); view = new OpenAssessment.EditPeerAssessmentView(element); + view.startDatetime("2014-01-01", "00:00"); + view.dueDatetime("2014-01-01", "00:00"); }); - it("Enables and disables", function() { testEnableAndDisable(view); }); + it("enables and disables", function() { testEnableAndDisable(view); }); - it("Loads a description", function() { + it("loads a description", function() { view.mustGradeNum(1); view.mustBeGradedByNum(2); view.startDatetime("2014-01-01", "00:00"); @@ -45,11 +62,18 @@ describe("OpenAssessment edit assessment views", function() { }); }); - it("Handles default dates", function() { - view.startDatetime(""); - view.dueDatetime(""); - expect(view.description().start).toBe(null); - expect(view.description().due).toBe(null); + it("validates the start date and time", function() { + testValidateDate( + view, view.startDatetimeControl, + "Peer assessment start is invalid" + ); + }); + + it("validates the due date and time", function() { + testValidateDate( + view, view.dueDatetimeControl, + "Peer assessment due is invalid" + ); }); }); @@ -59,11 +83,13 @@ describe("OpenAssessment edit assessment views", function() { beforeEach(function() { var element = $("#oa_self_assessment_editor").get(0); view = new OpenAssessment.EditSelfAssessmentView(element); + view.startDatetime("2014-01-01", "00:00"); + view.dueDatetime("2014-01-01", "00:00"); }); - it("Enables and disables", function() { testEnableAndDisable(view); }); + it("enables and disables", function() { testEnableAndDisable(view); }); - it("Loads a description", function() { + it("loads a description", function() { view.startDatetime("2014-01-01", "00:00"); view.dueDatetime("2014-03-04", "00:00"); expect(view.description()).toEqual({ @@ -72,11 +98,18 @@ describe("OpenAssessment edit assessment views", function() { }); }); - it("Handles default dates", function() { - view.startDatetime("", ""); - view.dueDatetime("", ""); - expect(view.description().start).toBe(null); - expect(view.description().due).toBe(null); + it("validates the start date and time", function() { + testValidateDate( + view, view.startDatetimeControl, + "Self assessment start is invalid" + ); + }); + + it("validates the due date and time", function() { + testValidateDate( + view, view.dueDatetimeControl, + "Self assessment due is invalid" + ); }); }); @@ -88,19 +121,19 @@ describe("OpenAssessment edit assessment views", function() { view = new OpenAssessment.EditStudentTrainingView(element); }); - it("Enables and disables", function() { testEnableAndDisable(view); }); - it("Loads a description", function () { + it("enables and disables", function() { testEnableAndDisable(view); }); + it("loads a description", function () { // This assumes a particular structure of the DOM, // which is set by the HTML fixture. var examples = view.exampleContainer.getItemValues(); expect(examples.length).toEqual(0); }); - it("Modifies a description", function () { + it("modifies a description", function () { view.exampleContainer.add(); var examples = view.exampleContainer.getItemValues(); expect(examples.length).toEqual(1); }); - it("Returns the correct format", function () { + it("returns the correct format", function () { view.exampleContainer.add(); var examples = view.exampleContainer.getItemValues(); expect(examples).toEqual( diff --git a/openassessment/xblock/static/js/spec/studio/oa_edit_fields.js b/openassessment/xblock/static/js/spec/studio/oa_edit_fields.js new file mode 100644 index 0000000..54f9217 --- /dev/null +++ b/openassessment/xblock/static/js/spec/studio/oa_edit_fields.js @@ -0,0 +1,79 @@ +describe("OpenAssessment.DatetimeControl", function() { + + var datetimeControl = null; + + beforeEach(function() { + // Install a minimal HTML fixture + // containing text fields for the date and time + setFixtures( + '<div id="datetime_parent">' + + '<input type="text" class="date_field" />' + + '<input type="text" class="time_field" />' + + '</div>' + ); + + // Create the datetime control, which uses elements + // available in the fixture. + datetimeControl = new OpenAssessment.DatetimeControl( + $("#datetime_parent").get(0), + ".date_field", + ".time_field" + ); + datetimeControl.install(); + }); + + // Set the date and time values, then check whether + // the datetime control has the expected validation status + var testValidateDate = function(control, dateValue, timeValue, isValid, expectedError) { + control.datetime(dateValue, timeValue); + + var actualIsValid = control.validate(); + expect(actualIsValid).toBe(isValid); + + if (isValid) { expect(control.validationErrors()).toEqual([]); } + else { expect(control.validationErrors()).toContain(expectedError); } + }; + + it("validates invalid dates", function() { + var expectedError = "Date is invalid"; + + testValidateDate(datetimeControl, "", "00:00", false, expectedError); + testValidateDate(datetimeControl, "1", "00:00", false, expectedError); + testValidateDate(datetimeControl, "123abcd", "00:00", false, expectedError); + testValidateDate(datetimeControl, "2014-", "00:00", false, expectedError); + testValidateDate(datetimeControl, "99999999-01-01", "00:00", false, expectedError); + testValidateDate(datetimeControl, "2014-99999-01", "00:00", false, expectedError); + testValidateDate(datetimeControl, "2014-01-99999", "00:00", false, expectedError); + }); + + it("validates invalid times", function() { + var expectedError = "Time is invalid"; + + testValidateDate(datetimeControl, "2014-04-01", "", false, expectedError); + testValidateDate(datetimeControl, "2014-04-01", "00:00abcd", false, expectedError); + testValidateDate(datetimeControl, "2014-04-01", "1", false, expectedError); + testValidateDate(datetimeControl, "2014-04-01", "1.23", false, expectedError); + testValidateDate(datetimeControl, "2014-04-01", "1:1", false, expectedError); + testValidateDate(datetimeControl, "2014-04-01", "000:00", false, expectedError); + testValidateDate(datetimeControl, "2014-04-01", "00:000", false, expectedError); + }); + + it("validates valid dates and times", function() { + testValidateDate(datetimeControl, "2014-04-01", "00:00", true); + testValidateDate(datetimeControl, "9999-01-01", "00:00", true); + testValidateDate(datetimeControl, "2001-12-31", "00:00", true); + testValidateDate(datetimeControl, "2014-04-01", "12:34", true); + testValidateDate(datetimeControl, "2014-04-01", "23:59", true); + }); + + it("clears validation errors", function() { + // Set an invalid state + datetimeControl.datetime("invalid", "invalid"); + datetimeControl.validate(); + expect(datetimeControl.validationErrors().length).toEqual(2); + + // Clear validation errors + datetimeControl.clearValidationErrors(); + expect(datetimeControl.validationErrors()).toEqual([]); + }); +}); \ No newline at end of file diff --git a/openassessment/xblock/static/js/spec/studio/oa_edit_rubric.js b/openassessment/xblock/static/js/spec/studio/oa_edit_rubric.js index 28b09c4..f55c911 100644 --- a/openassessment/xblock/static/js/spec/studio/oa_edit_rubric.js +++ b/openassessment/xblock/static/js/spec/studio/oa_edit_rubric.js @@ -208,4 +208,27 @@ describe("OpenAssessment.EditRubricView", function() { data: {criterionName : 'criterion_1'} }); }); + + it("validates option points", function () { + // Test that a particular value is marked as valid/invalid + var testValidateOptionPoints = function(value, isValid) { + var option = view.getOptionItem(0, 0); + option.points(value); + expect(view.validate()).toBe(isValid); + }; + + // Invalid option point values + testValidateOptionPoints("", false); + testValidateOptionPoints("123abcd", false); + testValidateOptionPoints("-1", false); + testValidateOptionPoints("1000", false); + testValidateOptionPoints("0.5", false); + + // Valid option point values + testValidateOptionPoints("0", true); + testValidateOptionPoints("1", true); + testValidateOptionPoints("2", true); + testValidateOptionPoints("998", true); + testValidateOptionPoints("999", true); + }); }); diff --git a/openassessment/xblock/static/js/spec/studio/oa_edit_settings.js b/openassessment/xblock/static/js/spec/studio/oa_edit_settings.js index 57e07c8..40f58e0 100644 --- a/openassessment/xblock/static/js/spec/studio/oa_edit_settings.js +++ b/openassessment/xblock/static/js/spec/studio/oa_edit_settings.js @@ -5,6 +5,9 @@ describe("OpenAssessment.EditSettingsView", function() { var StubView = function(name, descriptionText) { this.name = name; + this.isValid = true; + + var validationErrors = []; this.description = function() { return { dummy: descriptionText }; @@ -15,27 +18,55 @@ describe("OpenAssessment.EditSettingsView", function() { if (typeof(isEnabled) !== "undefined") { this._enabled = isEnabled; } return this._enabled; }; + + this.validate = function() { + return this.isValid; + }; + + this.setValidationErrors = function(errors) { validationErrors = errors; }; + this.validationErrors = function() { return validationErrors; }; + this.clearValidationErrors = function() { validationErrors = []; }; + }; + + var testValidateDate = function(datetimeControl, expectedError) { + // Test an invalid datetime + datetimeControl.datetime("invalid", "invalid"); + expect(view.validate()).toBe(false); + expect(view.validationErrors()).toContain(expectedError); + + view.clearValidationErrors(); + + // Test a valid datetime + datetimeControl.datetime("2014-04-05", "00:00"); + expect(view.validate()).toBe(true); + expect(view.validationErrors()).toEqual([]); }; var view = null; var assessmentViews = null; + // The Peer and Self Editor ID's + var PEER = "oa_peer_assessment_editor"; + var SELF = "oa_self_assessment_editor"; + var AI = "oa_ai_assessment_editor"; + var TRAINING = "oa_student_training_editor"; + beforeEach(function() { // Load the DOM fixture loadFixtures('oa_edit.html'); // Create the stub assessment views - assessmentViews = { - "oa_self_assessment_editor": new StubView("self-assessment", "Self assessment description"), - "oa_peer_assessment_editor": new StubView("peer-assessment", "Peer assessment description"), - "oa_ai_assessment_editor": new StubView("ai-assessment", "Example Based assessment description"), - "oa_student_training_editor": new StubView("student-training", "Student Training description") - }; + assessmentViews = {}; + assessmentViews[SELF] = new StubView("self-assessment", "Self assessment description"); + assessmentViews[PEER] = new StubView("peer-assessment", "Peer assessment description"); + assessmentViews[AI] = new StubView("ai-assessment", "Example Based assessment description"); + assessmentViews[TRAINING] = new StubView("student-training", "Student Training description"); // Create the view var element = $("#oa_basic_settings_editor").get(0); view = new OpenAssessment.EditSettingsView(element, assessmentViews); - + view.submissionStart("2014-01-01", "00:00"); + view.submissionDue("2014-03-04", "00:00"); }); it("sets and loads display name", function() { @@ -46,17 +77,11 @@ describe("OpenAssessment.EditSettingsView", function() { }); it("sets and loads the submission start/due dates", function() { - view.submissionStart("", ""); - expect(view.submissionStart()).toBe(null); + view.submissionStart("2014-04-01", "12:34"); + expect(view.submissionStart()).toEqual("2014-04-01T12:34"); - view.submissionStart("2014-04-01", "00:00"); - expect(view.submissionStart()).toEqual("2014-04-01T00:00"); - - view.submissionDue("", ""); - expect(view.submissionDue()).toBe(null); - - view.submissionDue("2014-05-02", "00:00"); - expect(view.submissionDue()).toEqual("2014-05-02T00:00"); + view.submissionDue("2014-05-02", "12:34"); + expect(view.submissionDue()).toEqual("2014-05-02T12:34"); }); it("sets and loads the image enabled state", function() { @@ -67,29 +92,21 @@ describe("OpenAssessment.EditSettingsView", function() { }); it("builds a description of enabled assessments", function() { - // In this test we also verify that the mechansim that reads off of the DOM is correct, in that it gets - // the right order of assessments, in addition to performing the correct calls. Note that this test's - // success depends on our Template having the original order (as it does in an unconfigured ORA problem) - // of TRAINING -> PEER -> SELF -> AI - - // The Peer and Self Editor ID's - var peerID = "oa_peer_assessment_editor"; - var selfID = "oa_self_assessment_editor"; - var aiID = "oa_ai_assessment_editor"; - var studentID = "oa_student_training_editor"; + // Depends on the template having an original order + // of training --> peer --> self --> ai // Disable all assessments, and expect an empty description - assessmentViews[peerID].isEnabled(false); - assessmentViews[selfID].isEnabled(false); - assessmentViews[aiID].isEnabled(false); - assessmentViews[studentID].isEnabled(false); + assessmentViews[PEER].isEnabled(false); + assessmentViews[SELF].isEnabled(false); + assessmentViews[AI].isEnabled(false); + assessmentViews[TRAINING].isEnabled(false); expect(view.assessmentsDescription()).toEqual([]); // Enable the first assessment only - assessmentViews[peerID].isEnabled(false); - assessmentViews[selfID].isEnabled(true); - assessmentViews[aiID].isEnabled(false); - assessmentViews[studentID].isEnabled(false); + assessmentViews[PEER].isEnabled(false); + assessmentViews[SELF].isEnabled(true); + assessmentViews[AI].isEnabled(false); + assessmentViews[TRAINING].isEnabled(false); expect(view.assessmentsDescription()).toEqual([ { name: "self-assessment", @@ -98,10 +115,10 @@ describe("OpenAssessment.EditSettingsView", function() { ]); // Enable the second assessment only - assessmentViews[peerID].isEnabled(true); - assessmentViews[selfID].isEnabled(false); - assessmentViews[aiID].isEnabled(false); - assessmentViews[studentID].isEnabled(false); + assessmentViews[PEER].isEnabled(true); + assessmentViews[SELF].isEnabled(false); + assessmentViews[AI].isEnabled(false); + assessmentViews[TRAINING].isEnabled(false); expect(view.assessmentsDescription()).toEqual([ { name: "peer-assessment", @@ -110,10 +127,10 @@ describe("OpenAssessment.EditSettingsView", function() { ]); // Enable both assessments - assessmentViews[peerID].isEnabled(true); - assessmentViews[selfID].isEnabled(true); - assessmentViews[aiID].isEnabled(false); - assessmentViews[studentID].isEnabled(false); + assessmentViews[PEER].isEnabled(true); + assessmentViews[SELF].isEnabled(true); + assessmentViews[AI].isEnabled(false); + assessmentViews[TRAINING].isEnabled(false); expect(view.assessmentsDescription()).toEqual([ { name: "peer-assessment", @@ -125,4 +142,29 @@ describe("OpenAssessment.EditSettingsView", function() { } ]); }); + + it("validates submission start datetime fields", function() { + testValidateDate( + view.startDatetimeControl, + "Submission start is invalid" + ); + }); + + it("validates submission due datetime fields", function() { + testValidateDate( + view.dueDatetimeControl, + "Submission due is invalid" + ); + }); + + it("validates assessment views", function() { + // Simulate one of the assessment views being invalid + assessmentViews[PEER].isValid = false; + assessmentViews[PEER].setValidationErrors(["test error"]); + + // Expect that the parent view is also invalid + expect(view.validate()).toBe(false); + debugger; + expect(view.validationErrors()).toContain("test error"); + }); }); diff --git a/openassessment/xblock/static/js/src/studio/oa_container_item.js b/openassessment/xblock/static/js/src/studio/oa_container_item.js index aa61fd3..3ab9d26 100644 --- a/openassessment/xblock/static/js/src/studio/oa_container_item.js +++ b/openassessment/xblock/static/js/src/studio/oa_container_item.js @@ -38,6 +38,7 @@ Returns: OpenAssessment.RubricOption = function(element, notifier) { this.element = element; this.notifier = notifier; + this.MAX_POINTS = 1000; $(this.element).focusout($.proxy(this.updateHandler, this)); }; @@ -56,15 +57,9 @@ OpenAssessment.RubricOption.prototype = { **/ getFieldValues: function () { var fields = { - label: OpenAssessment.Fields.stringField( - $('.openassessment_criterion_option_label', this.element) - ), - points: OpenAssessment.Fields.intField( - $('.openassessment_criterion_option_points', this.element) - ), - explanation: OpenAssessment.Fields.stringField( - $('.openassessment_criterion_option_explanation', this.element) - ) + label: this.label(), + points: this.points(), + explanation: this.explanation() }; // New options won't have unique names assigned. @@ -79,6 +74,51 @@ OpenAssessment.RubricOption.prototype = { }, /** + Get or set the label of the option. + + Args: + label (string, optional): If provided, set the label to this string. + + Returns: + string + + **/ + label: function(label) { + var sel = $('.openassessment_criterion_option_label', this.element); + return OpenAssessment.Fields.stringField(sel, label); + }, + + /** + Get or set the point value of the option. + + Args: + points (int, optional): If provided, set the point value of the option. + + Returns: + int + + **/ + points: function(points) { + var sel = $('.openassessment_criterion_option_points', this.element); + return OpenAssessment.Fields.intField(sel, points); + }, + + /** + Get or set the explanation for the option. + + Args: + explanation (string, optional): If provided, set the explanation to this string. + + Returns: + string + + **/ + explanation: function(explanation) { + var sel = $('.openassessment_criterion_option_explanation', this.element); + return OpenAssessment.Fields.stringField(sel, explanation); + }, + + /** Hook into the event handler for addition of a criterion option. */ @@ -146,6 +186,46 @@ OpenAssessment.RubricOption.prototype = { "points": optionPoints } ); + }, + + /** + Mark validation errors. + + Returns: + Boolean indicating whether the option is valid. + + **/ + validate: function() { + var pointString = $(".openassessment_criterion_option_points", this.element).val(); + var matches = pointString.trim().match(/^\d{1,3}$/g); + var isValid = (matches !== null); + if (!isValid) { + $(".openassessment_criterion_option_points", this.element) + .addClass("openassessment_highlighted_field"); + } + return isValid; + }, + + /** + Return a list of validation errors visible in the UI. + Mainly useful for testing. + + Returns: + list of string + + **/ + validationErrors: function() { + var sel = $(".openassessment_criterion_option_points", this.element); + var hasError = sel.hasClass("openassessment_highlighted_field"); + return hasError ? ["Option points are invalid"] : []; + }, + + /** + Clear all validation errors from the UI. + **/ + clearValidationErrors: function() { + $(".openassessment_criterion_option_points", this.element) + .removeClass("openassessment_highlighted_field"); } }; @@ -200,15 +280,9 @@ OpenAssessment.RubricCriterion.prototype = { **/ getFieldValues: function () { var fields = { - label: OpenAssessment.Fields.stringField( - $('.openassessment_criterion_label', this.element) - ), - prompt: OpenAssessment.Fields.stringField( - $('.openassessment_criterion_prompt', this.element) - ), - feedback: OpenAssessment.Fields.stringField( - $('.openassessment_criterion_feedback', this.element) - ), + label: this.label(), + prompt: this.prompt(), + feedback: this.feedback(), options: this.optionContainer.getItemValues() }; @@ -224,6 +298,48 @@ OpenAssessment.RubricCriterion.prototype = { }, /** + Get or set the label of the criterion. + + Args: + label (string, optional): If provided, set the label to this string. + + Returns: + string + + **/ + label: function(label) { + var sel = $('.openassessment_criterion_label', this.element); + return OpenAssessment.Fields.stringField(sel, label); + }, + + /** + Get or set the prompt of the criterion. + + Args: + prompt (string, optional): If provided, set the prompt to this string. + + Returns: + string + + **/ + prompt: function(prompt) { + var sel = $('.openassessment_criterion_prompt', this.element); + return OpenAssessment.Fields.stringField(sel, prompt); + }, + + /** + Get the feedback value for the criterion. + This is one of: "disabled", "optional", or "required". + + Returns: + string + + **/ + feedback: function() { + return $('.openassessment_criterion_feedback', this.element).val(); + }, + + /** Add an option to the criterion. Uses the client-side template to create the new option. **/ @@ -265,6 +381,46 @@ OpenAssessment.RubricCriterion.prototype = { "criterionUpdated", {'criterionName': criterionName, 'criterionLabel': criterionLabel} ); + }, + + /** + Mark validation errors. + + Returns: + Boolean indicating whether the criterion is valid. + + **/ + validate: function() { + var isValid = true; + $.each(this.optionContainer.getAllItems(), function() { + isValid = (isValid && this.validate()); + }); + return isValid; + }, + + /** + Return a list of validation errors visible in the UI. + Mainly useful for testing. + + Returns: + list of string + + **/ + validationErrors: function() { + var errors = []; + $.each(this.optionContainer.getAllItems(), function() { + errors = errors.concat(this.validationErrors()); + }); + return errors; + }, + + /** + Clear all validation errors from the UI. + **/ + clearValidationErrors: function() { + $.each(this.optionContainer.getAllItems(), function() { + this.clearValidationErrors(); + }); } }; @@ -307,5 +463,9 @@ OpenAssessment.TrainingExample.prototype = { addHandler: function() {}, removeHandler: function() {}, - updateHandler: function() {} + updateHandler: function() {}, + + validate: function() { return true; }, + validationErrors: function() { return []; }, + clearValidationErrors: function() {} }; \ No newline at end of file diff --git a/openassessment/xblock/static/js/src/studio/oa_edit.js b/openassessment/xblock/static/js/src/studio/oa_edit.js index 3677702..6ddb749 100644 --- a/openassessment/xblock/static/js/src/studio/oa_edit.js +++ b/openassessment/xblock/static/js/src/studio/oa_edit.js @@ -22,6 +22,10 @@ OpenAssessment.StudioView = function(runtime, element, server) { // Initializes the tabbing functionality and activates the last used. this.initializeTabs(); + // Initialize the validation alert + this.validationAlert = new OpenAssessment.ValidationAlert(); + this.validationAlert.installEventHandlers(); + // Initialize the prompt tab view this.promptView = new OpenAssessment.EditPromptView( $("#oa_prompt_editor_wrapper", this.element).get(0) @@ -120,20 +124,40 @@ OpenAssessment.StudioView.prototype = { save: function () { var view = this; this.saveTabState(); - // Check whether the problem has been released; if not, - // warn the user and allow them to cancel. - this.server.checkReleased().done( - function (isReleased) { - if (isReleased) { - view.confirmPostReleaseUpdate($.proxy(view.updateEditorContext, view)); - } - else { - view.updateEditorContext(); + + // Perform client-side validation: + // * Hide the validation alert + // * Clear errors from any field marked as invalid. + // * Mark invalid fields in the UI. + // * If there are any validation errors, show an alert. + // + // The `validate()` method calls `validate()` on any subviews, + // so that each subview has the opportunity to validate + // its fields. + this.validationAlert.hide(); + this.clearValidationErrors(); + if (!this.validate()) { + this.validationAlert.setMessage( + gettext("Validation Errors"), + gettext("Some fields are not valid. Please update the fields.") + ).show(); + } + else { + // Check whether the problem has been released; if not, + // warn the user and allow them to cancel. + this.server.checkReleased().done( + function (isReleased) { + if (isReleased) { + view.confirmPostReleaseUpdate($.proxy(view.updateEditorContext, view)); + } + else { + view.updateEditorContext(); + } } - } - ).fail(function (errMsg) { - view.showError(errMsg); - }); + ).fail(function (errMsg) { + view.showError(errMsg); + }); + } }, /** @@ -197,6 +221,39 @@ OpenAssessment.StudioView.prototype = { showError: function (errorMsg) { this.runtime.notify('error', {msg: errorMsg}); }, + + /** + Mark validation errors. + + Returns: + Boolean indicating whether the view is valid. + + **/ + validate: function() { + return this.settingsView.validate() && this.rubricView.validate(); + }, + + /** + Return a list of validation errors visible in the UI. + Mainly useful for testing. + + Returns: + list of string + + **/ + validationErrors: function() { + return this.settingsView.validationErrors().concat( + this.rubricView.validationErrors() + ); + }, + + /** + Clear all validation errors from the UI. + **/ + clearValidationErrors: function() { + this.settingsView.clearValidationErrors(); + this.rubricView.clearValidationErrors(); + }, }; diff --git a/openassessment/xblock/static/js/src/studio/oa_edit_assessment.js b/openassessment/xblock/static/js/src/studio/oa_edit_assessment.js index 89c288a..2329614 100644 --- a/openassessment/xblock/static/js/src/studio/oa_edit_assessment.js +++ b/openassessment/xblock/static/js/src/studio/oa_edit_assessment.js @@ -137,7 +137,45 @@ OpenAssessment.EditPeerAssessmentView.prototype = { **/ getID: function() { return $(this.element).attr('id'); - } + }, + + /** + Mark validation errors. + + Returns: + Boolean indicating whether the view is valid. + + **/ + validate: function() { + return this.startDatetimeControl.validate() && this.dueDatetimeControl.validate(); + }, + + /** + Return a list of validation errors visible in the UI. + Mainly useful for testing. + + Returns: + list of string + + **/ + validationErrors: function() { + var errors = []; + if (this.startDatetimeControl.validationErrors().length > 0) { + errors.push("Peer assessment start is invalid"); + } + if (this.dueDatetimeControl.validationErrors().length > 0) { + errors.push("Peer assessment due is invalid"); + } + return errors; + }, + + /** + Clear all validation errors from the UI. + **/ + clearValidationErrors: function() { + this.startDatetimeControl.clearValidationErrors(); + this.dueDatetimeControl.clearValidationErrors(); + }, }; @@ -242,14 +280,52 @@ OpenAssessment.EditSelfAssessmentView.prototype = { }, /** - Gets the ID of the assessment + Gets the ID of the assessment - Returns: - string (CSS ID of the Element object) - **/ + Returns: + string (CSS ID of the Element object) + **/ getID: function() { return $(this.element).attr('id'); - } + }, + + /** + Mark validation errors. + + Returns: + Boolean indicating whether the view is valid. + + **/ + validate: function() { + return this.startDatetimeControl.validate() && this.dueDatetimeControl.validate(); + }, + + /** + Return a list of validation errors visible in the UI. + Mainly useful for testing. + + Returns: + list of string + + **/ + validationErrors: function() { + var errors = []; + if (this.startDatetimeControl.validationErrors().length > 0) { + errors.push("Self assessment start is invalid"); + } + if (this.dueDatetimeControl.validationErrors().length > 0) { + errors.push("Self assessment due is invalid"); + } + return errors; + }, + + /** + Clear all validation errors from the UI. + **/ + clearValidationErrors: function() { + this.startDatetimeControl.clearValidationErrors(); + this.dueDatetimeControl.clearValidationErrors(); + }, }; /** @@ -340,7 +416,11 @@ OpenAssessment.EditStudentTrainingView.prototype = { **/ getID: function() { return $(this.element).attr('id'); - } + }, + + validate: function() { return true; }, + validationErrors: function() { return []; }, + clearValidationErrors: function() {}, }; /** @@ -415,12 +495,16 @@ OpenAssessment.EditExampleBasedAssessmentView.prototype = { }, /** - Gets the ID of the assessment + Gets the ID of the assessment - Returns: - string (CSS ID of the Element object) - **/ + Returns: + string (CSS ID of the Element object) + **/ getID: function() { return $(this.element).attr('id'); - } + }, + + validate: function() { return true; }, + validationErrors: function() { return []; }, + clearValidationErrors: function() {}, }; \ No newline at end of file diff --git a/openassessment/xblock/static/js/src/studio/oa_edit_fields.js b/openassessment/xblock/static/js/src/studio/oa_edit_fields.js index fe25d46..60d3b56 100644 --- a/openassessment/xblock/static/js/src/studio/oa_edit_fields.js +++ b/openassessment/xblock/static/js/src/studio/oa_edit_fields.js @@ -96,7 +96,7 @@ OpenAssessment.DatetimeControl.prototype = { var dateString = $(this.datePicker, this.element).val(); $(this.datePicker, this.element).datepicker({ showButtonPanel: true }) .datepicker("option", "dateFormat", "yy-mm-dd") - .datepicker("setDate", dateString); + .val(dateString); $(this.timePicker, this.element).timepicker({ timeFormat: 'H:i', step: 60 @@ -108,7 +108,7 @@ OpenAssessment.DatetimeControl.prototype = { Get or set the date and time. Args: - dateString (string, optional): If provided, set the date (YY-MM-DD). + dateString (string, optional): If provided, set the date (YYYY-MM-DD). timeString (string, optional): If provided, set the time (HH:MM, 24-hour clock). Returns: @@ -118,12 +118,55 @@ OpenAssessment.DatetimeControl.prototype = { datetime: function(dateString, timeString) { var datePickerSel = $(this.datePicker, this.element); var timePickerSel = $(this.timePicker, this.element); - if (typeof(dateString) !== "undefined") { datePickerSel.datepicker("setDate", dateString); } + if (typeof(dateString) !== "undefined") { datePickerSel.val(dateString); } if (typeof(timeString) !== "undefined") { timePickerSel.val(timeString); } + return datePickerSel.val() + "T" + timePickerSel.val(); + }, + + /** + Mark validation errors. + + Returns: + Boolean indicating whether the fields are valid. - if (datePickerSel.val() === "" && timePickerSel.val() === "") { - return null; + **/ + validate: function() { + var datetimeString = this.datetime(); + var matches = datetimeString.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/g); + var isValid = (matches !== null); + + if (!isValid) { + $(this.datePicker, this.element).addClass("openassessment_highlighted_field"); + $(this.timePicker, this.element).addClass("openassessment_highlighted_field"); } - return datePickerSel.val() + "T" + timePickerSel.val(); - } + + return isValid; + }, + + /** + Clear all validation errors from the UI. + **/ + clearValidationErrors: function() { + $(this.datePicker, this.element).removeClass("openassessment_highlighted_field"); + $(this.timePicker, this.element).removeClass("openassessment_highlighted_field"); + }, + + /** + Return a list of validation errors visible in the UI. + Mainly useful for testing. + + Returns: + list of string + + **/ + validationErrors: function() { + var errors = []; + var dateHasError = $(this.datePicker, this.element).hasClass("openassessment_highlighted_field"); + var timeHasError = $(this.timePicker, this.element).hasClass("openassessment_highlighted_field"); + + if (dateHasError) { errors.push("Date is invalid"); } + if (timeHasError) { errors.push("Time is invalid"); } + + return errors; + }, }; \ No newline at end of file diff --git a/openassessment/xblock/static/js/src/studio/oa_edit_rubric.js b/openassessment/xblock/static/js/src/studio/oa_edit_rubric.js index 8347a0b..5074196 100644 --- a/openassessment/xblock/static/js/src/studio/oa_edit_rubric.js +++ b/openassessment/xblock/static/js/src/studio/oa_edit_rubric.js @@ -189,5 +189,49 @@ OpenAssessment.EditRubricView.prototype = { getOptionItem: function(criterionIndex, optionIndex) { var criterionItem = this.getCriterionItem(criterionIndex); return criterionItem.optionContainer.getItem(optionIndex); + }, + + /** + Mark validation errors. + + Returns: + Boolean indicating whether the view is valid. + + **/ + validate: function() { + var isValid = true; + + $.each(this.getAllCriteria(), function() { + isValid = (isValid && this.validate()); + }); + + return isValid; + }, + + /** + Return a list of validation errors visible in the UI. + Mainly useful for testing. + + Returns: + list of string + + **/ + validationErrors: function() { + var errors = []; + + $.each(this.getAllCriteria(), function() { + errors = errors.concat(this.validationErrors()); + }); + + return errors; + }, + + /** + Clear all validation errors from the UI. + **/ + clearValidationErrors: function() { + $.each(this.getAllCriteria(), function() { + this.clearValidationErrors(); + }); } }; diff --git a/openassessment/xblock/static/js/src/studio/oa_edit_settings.js b/openassessment/xblock/static/js/src/studio/oa_edit_settings.js index ad4766e..f65b4d1 100644 --- a/openassessment/xblock/static/js/src/studio/oa_edit_settings.js +++ b/openassessment/xblock/static/js/src/studio/oa_edit_settings.js @@ -198,4 +198,61 @@ OpenAssessment.EditSettingsView.prototype = { ); return editorAssessments; }, + + /** + Mark validation errors. + + Returns: + Boolean indicating whether the view is valid. + + **/ + validate: function() { + // Validate the start and due datetime controls + var isValid = ( + this.startDatetimeControl.validate() && + this.dueDatetimeControl.validate() + ); + + // Validate each of the assessment views + $.each(this.assessmentViews, function() { + isValid = (isValid && this.validate()); + }); + + return isValid; + }, + + /** + Return a list of validation errors visible in the UI. + Mainly useful for testing. + + Returns: + list of string + + **/ + validationErrors: function() { + var errors = []; + + if (this.startDatetimeControl.validationErrors().length > 0) { + errors.push("Submission start is invalid"); + } + if (this.dueDatetimeControl.validationErrors().length > 0) { + errors.push("Submission due is invalid"); + } + + $.each(this.assessmentViews, function() { + errors = errors.concat(this.validationErrors()); + }); + return errors; + }, + + /** + Clear all validation errors from the UI. + **/ + clearValidationErrors: function() { + this.startDatetimeControl.clearValidationErrors(); + this.dueDatetimeControl.clearValidationErrors(); + $.each(this.assessmentViews, function() { + this.clearValidationErrors(); + }); + }, }; \ No newline at end of file diff --git a/openassessment/xblock/static/js/src/studio/oa_edit_validation_alert.js b/openassessment/xblock/static/js/src/studio/oa_edit_validation_alert.js index 025fa6a..4a5aa63 100644 --- a/openassessment/xblock/static/js/src/studio/oa_edit_validation_alert.js +++ b/openassessment/xblock/static/js/src/studio/oa_edit_validation_alert.js @@ -2,54 +2,70 @@ A class which controls the validation alert which we place at the top of the rubric page after changes are made which will propagate to the settings section. -Args: - element (element): The element that specifies the div that the validation consists of. - Returns: Openassessment.ValidationAlert */ -OpenAssessment.ValidationAlert = function (element) { - var alert = this; - this.element = element; +OpenAssessment.ValidationAlert = function() { + this.element = $('#openassessment_rubric_validation_alert'); this.rubricContentElement = $('#openassessment_rubric_content_editor'); this.title = $(".openassessment_alert_title", this.element); this.message = $(".openassessment_alert_message", this.element); - $(".openassessment_alert_close", element).click(function(eventObject) { - eventObject.preventDefault(); - alert.hide(); - } - ); }; OpenAssessment.ValidationAlert.prototype = { /** - Hides the alert. - */ + Install the event handlers for the alert. + **/ + installEventHandlers: function() { + var alert = this; + $(".openassessment_alert_close", this.element).click( + function(eventObject) { + eventObject.preventDefault(); + alert.hide(); + } + ); + }, + + /** + Hides the alert. + + Returns: + OpenAssessment.ValidationAlert + */ hide: function() { this.element.addClass('is--hidden'); this.rubricContentElement.removeClass('openassessment_alert_shown'); + return this; }, /** - Displays the alert. - */ + Displays the alert. + + Returns: + OpenAssessment.ValidationAlert + */ show : function() { this.element.removeClass('is--hidden'); this.rubricContentElement.addClass('openassessment_alert_shown'); + return this; }, /** - Sets the message of the alert. - How will this work with internationalization? + Sets the message of the alert. + How will this work with internationalization? - Args: - newTitle (str): the new title that the message will have - newMessage (str): the new text that the message's body will contain - */ + Args: + newTitle (str): the new title that the message will have + newMessage (str): the new text that the message's body will contain + + Returns: + OpenAssessment.ValidationAlert + */ setMessage: function(newTitle, newMessage) { this.title.text(newTitle); this.message.text(newMessage); + return this; }, /** diff --git a/scripts/render_templates.py b/scripts/render_templates.py index 4be8789..993ba84 100755 --- a/scripts/render_templates.py +++ b/scripts/render_templates.py @@ -25,6 +25,9 @@ templates.json file's directory. import sys import os.path import json +import re +import dateutil.parser +import pytz # This is a bit of a hack to ensure that the root repo directory # is in the Python path, so Django can find the settings module. @@ -36,6 +39,47 @@ from django.template.loader import get_template USAGE = u"{prog} TEMPLATE_DESC" +DATETIME_REGEX = re.compile("^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$") + +def parse_dates(context): + """ + Transform datetime strings into Python datetime objects. + + JSON does not provide a standard way to serialize datetime objects, + but some of the templates expect that the context contains + Python datetime objects. + + This (somewhat hacky) solution recursively searches the context + for formatted datetime strings of the form "2014-01-02T12:34" + and converts them to Python datetime objects with the timezone + set to UTC. + + Args: + context (JSON-serializable): The context (or part of the context) + that will be passed to the template. Dictionaries and lists + will be recursively searched and transformed. + + Returns: + JSON-serializable of the same type as the `context` argument. + + """ + if isinstance(context, dict): + return { + key: parse_dates(value) + for key, value in context.iteritems() + } + elif isinstance(context, list): + return [ + parse_dates(item) + for item in context + ] + elif isinstance(context, basestring): + if DATETIME_REGEX.match(context) is not None: + return dateutil.parser.parse(context).replace(tzinfo=pytz.utc) + + return context + + def render_templates(root_dir, template_json): """ Create rendered templates. @@ -51,7 +95,8 @@ def render_templates(root_dir, template_json): """ for template_dict in template_json: template = get_template(template_dict['template']) - rendered = template.render(Context(template_dict['context'])) + context = parse_dates(template_dict['context']) + rendered = template.render(Context(context)) output_path = os.path.join(root_dir, template_dict['output']) try: -- libgit2 0.26.0