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