Commit a5921b62 by Will Daly

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.
parent cb932343
......@@ -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 %}
......@@ -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
......
......@@ -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);
});
});
......@@ -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(
......
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
......@@ -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);
});
});
......@@ -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");
});
});
......@@ -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
......@@ -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();
},
};
......
......@@ -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
......@@ -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
......@@ -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();
});
}
};
......@@ -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
......@@ -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;
},
/**
......
......@@ -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:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment