Commit 4027ea3c by Will Daly

Merge pull request #570 from edx/will/js-field-validation

Will/js field validation
parents f76313c7 d022da89
......@@ -13,6 +13,18 @@
</ul>
</div>
<div id="openassessment_validation_alert" class="covered">
<i class="openassessment_alert_icon"></i>
<div class="openassessment_alert_header">
<h2 class="openassessment_alert_title">{% trans "Rubric Change Impacts Settings Section" %}</h2>
<p class="openassessment_alert_message">{% trans "A change that you made to this assessment's rubric has an impact on some examples laid out in the settings tab. For more information, go to the Settings section and fix areas highlighted in red." %}</p>
</div>
<a href="" rel="view" class="action openassessment_alert_close">
<i class="icon-remove-sign"></i>
<span class="label is--hidden">{% trans "close alert" %}</span>
</a>
</div>
<div id="oa_prompt_editor_wrapper" class="oa_editor_content_wrapper">
<textarea id="openassessment_prompt_editor" maxlength="10000">{{ prompt }}</textarea>
</div>
......
......@@ -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 %}
......@@ -20,14 +20,14 @@
<li class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="peer_assessment_must_grade" class="setting-label">{% trans "Must Grade" %}</label>
<input id="peer_assessment_must_grade" class="input setting-input" type="number" value="{{ assessments.peer_assessment.must_grade }}">
<input id="peer_assessment_must_grade" class="input setting-input" type="number" value="{{ assessments.peer_assessment.must_grade }}" min="0" max="99">
</div>
<p class="setting-help">{% trans "Each student must assess this number of peer responses in order to recieve a grade."%}</p>
</li>
<li class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="peer_assessment_graded_by" class="setting-label"> {% trans "Graded By" %}</label>
<input id="peer_assessment_graded_by" class="input setting-input" type="number" value="{{ assessments.peer_assessment.must_be_graded_by }}">
<input id="peer_assessment_graded_by" class="input setting-input" type="number" value="{{ assessments.peer_assessment.must_be_graded_by }}" min="0" max="99">
</div>
<p class="setting-help">{% trans "Each response must be assessed by at least this many students in order to tabulate a score."%}</p>
</li>
......
......@@ -9,19 +9,6 @@
{% include "openassessmentblock/edit/oa_edit_option.html" with option_name="" option_label="" option_points=1 option_explanation="" %}
</div>
<div id="openassessment_rubric_validation_alert" class="is--hidden">
<i class="openassessment_alert_icon"></i>
<div class="openassessment_alert_header">
<h2 class="openassessment_alert_title">{% trans "Rubric Change Impacts Settings Section" %}</h2>
<p class="openassessment_alert_message">{% trans "A change that you made to this assessment's rubric has an impact on some examples laid out in the settings tab. For more information, go to the Settings section and fix areas highlighted in red." %}</p>
</div>
<a href="" rel="view" class="action openassessment_alert_close">
<i class="icon-remove-sign"></i>
<span class="label is--hidden">{% trans "close alert" %}</span>
</a>
</div>
<div id="openassessment_rubric_content_editor">
<div id="openassessment_rubric_instructions">
<p class="openassessment_description">
{% trans "For open response problems, assessment is rubric-based. Rubric criterion have point breakdowns and explanations to help students with peer and self assessment steps. For more information on how to build your rubric, see our online help documentation."%}
......@@ -57,6 +44,5 @@
</ul>
</div>
</div>
</div>
{% endspaceless %}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -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,8 +591,8 @@
}
},
"peer_assessment": {
"start": "",
"due": "",
"start": "2014-01-02T00:00",
"due": "2020-01-01T12:34",
"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,46 @@ 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"
);
});
it("validates the must grade field", function() {
// Invalid value (not a number)
view.mustGradeNum("123abc");
expect(view.validate()).toBe(false);
expect(view.validationErrors()).toContain("Peer assessment must grade is invalid");
view.clearValidationErrors();
// Valid value
view.mustGradeNum("34");
expect(view.validate()).toBe(true);
expect(view.validationErrors()).toEqual([]);
});
it("validates the must be graded by field", function() {
// Invalid value (not a number)
view.mustBeGradedByNum("123abc");
expect(view.validate()).toBe(false);
expect(view.validationErrors()).toContain("Peer assessment must be graded by is invalid");
view.clearValidationErrors();
// Valid value
view.mustBeGradedByNum("34");
expect(view.validate()).toBe(true);
expect(view.validationErrors()).toEqual([]);
});
});
......@@ -59,11 +111,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 +126,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 +149,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
......@@ -79,6 +79,29 @@ describe("OpenAssessment.StudentTrainingListener", function() {
);
});
it("updates the label of an option with invalid points", function() {
// If an option has invalid points, the points will be set to NaN
listener.optionUpdated({
criterionName: "criterion_with_two_options",
name: "option_1",
label: "This is a new label!",
points: NaN
});
// Invalid points should be labeled as such
assertExampleLabels(
listener.examplesOptionsLabels(),
{
criterion_with_two_options: {
"": "Not Scored",
option_1: "This is a new label!",
option_2: "Good - 2 points"
}
}
);
});
it("removes an option and displays an alert", function() {
// Initial state, set by the fixture
assertExampleLabels(
......
......@@ -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");
});
});
......@@ -39,19 +39,26 @@ OpenAssessment.ItemUtilities = {
}
var singularString = label + " - " + points + " point";
var multipleString = label + " - " + points + " points";
// If the option doesn't have a data points value, that indicates to us that it is not a user-specified option,
// but represents the "Not Selected" option which all criterion drop-downs have.
if (typeof points === 'undefined') {
$(element).text(
gettext('Not Selected')
);
var finalLabel = "";
if (points === undefined) {
finalLabel = gettext('Not Selected');
}
// If the points are invalid, we'll be given NaN
// Don't show this to the user.
else if (isNaN(points)) {
finalLabel = label;
}
// Otherwise, set the text of the option element to be the properly conjugated, translated string.
else {
$(element).text(
ngettext(singularString, multipleString, points)
);
finalLabel = ngettext(singularString, multipleString, points);
}
$(element).text(finalLabel);
}
};
......@@ -69,9 +76,20 @@ Returns:
OpenAssessment.RubricOption = function(element, notifier) {
this.element = element;
this.notifier = notifier;
this.pointsField = new OpenAssessment.IntField(
$(".openassessment_criterion_option_points", this.element),
{ min: 0, max: 999 }
);
};
OpenAssessment.RubricOption.prototype = {
/**
Adds event listeners specific to this container item.
**/
addEventListeners: function() {
// Install a focus out handler for container changes.
$(this.element).focusout($.proxy(this.updateHandler, this));
},
/**
Finds the values currently entered in the Option's fields, and returns them.
......@@ -86,15 +104,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.
......@@ -109,6 +121,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) {
if (points !== undefined) { this.pointsField.set(points); }
return this.pointsField.get();
},
/**
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.
*/
......@@ -179,11 +236,34 @@ OpenAssessment.RubricOption.prototype = {
},
/**
Adds event listeners specific to this container item.
*/
addEventListeners: function() {
// Install a focus out handler for container changes.
$(this.element).focusout($.proxy(this.updateHandler, this));
Mark validation errors.
Returns:
Boolean indicating whether the option is valid.
**/
validate: function() {
return this.pointsField.validate();
},
/**
Return a list of validation errors visible in the UI.
Mainly useful for testing.
Returns:
list of string
**/
validationErrors: function() {
var hasError = (this.pointsField.validationErrors().length > 0);
return hasError ? ["Option points are invalid"] : [];
},
/**
Clear all validation errors from the UI.
**/
clearValidationErrors: function() {
this.pointsField.clearValidationErrors();
}
};
......@@ -215,6 +295,17 @@ OpenAssessment.RubricCriterion = function(element, notifier) {
OpenAssessment.RubricCriterion.prototype = {
/**
Invoked by the container to add event listeners to all child containers
of this item, and add event listeners specific to this container item.
**/
addEventListeners: function() {
this.optionContainer.addEventListeners();
// Install a focus out handler for container changes.
$(this.element).focusout($.proxy(this.updateHandler, this));
},
/**
Finds the values currently entered in the Criterion's fields, and returns them.
......@@ -236,15 +327,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()
};
......@@ -260,14 +345,45 @@ OpenAssessment.RubricCriterion.prototype = {
},
/**
Invoked by the container to add event listeners to all child containers
of this item, and add event listeners specific to this container item.
Get or set the label of the criterion.
*/
addEventListeners: function() {
this.optionContainer.addEventListeners();
// Install a focus out handler for container changes.
$(this.element).focusout($.proxy(this.updateHandler, this));
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();
},
/**
......@@ -312,6 +428,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 = (this.validate() && isValid);
});
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();
});
}
};
......@@ -361,5 +517,9 @@ OpenAssessment.TrainingExample.prototype = {
addHandler: function() {},
addEventListeners: 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,42 @@ 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:
// * 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.clearValidationErrors();
if (!this.validate()) {
this.validationAlert.setMessage(
gettext("Couldn't Save This Assignment"),
gettext("Please correct the outlined fields.")
).show();
}
else {
// At this point, we know that all fields are valid,
// so we can dismiss the validation alert.
this.validationAlert.hide();
// 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 +223,41 @@ 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() {
var settingsValid = this.settingsView.validate();
var rubricValid = this.rubricView.validate();
return settingsValid && rubricValid;
},
/**
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();
},
};
......
......@@ -11,6 +11,14 @@ Returns:
OpenAssessment.EditPeerAssessmentView = function(element) {
this.element = element;
this.name = "peer-assessment";
this.mustGradeField = new OpenAssessment.IntField(
$("#peer_assessment_must_grade", this.element),
{ min: 0, max: 99 }
);
this.mustBeGradedByField = new OpenAssessment.IntField(
$("#peer_assessment_graded_by", this.element),
{ min: 0, max: 99 }
);
// Configure the toggle checkbox to enable/disable this assessment
new OpenAssessment.ToggleControl(
......@@ -83,8 +91,8 @@ OpenAssessment.EditPeerAssessmentView.prototype = {
int
**/
mustGradeNum: function(num) {
var sel = $("#peer_assessment_must_grade", this.element);
return OpenAssessment.Fields.intField(sel, num);
if (num !== undefined) { this.mustGradeField.set(num); }
return this.mustGradeField.get();
},
/**
......@@ -97,8 +105,8 @@ OpenAssessment.EditPeerAssessmentView.prototype = {
int
**/
mustBeGradedByNum: function(num) {
var sel = $("#peer_assessment_graded_by", this.element);
return OpenAssessment.Fields.intField(sel, num);
if (num !== undefined) { this.mustBeGradedByField.set(num); }
return this.mustBeGradedByField.get();
},
/**
......@@ -137,7 +145,57 @@ OpenAssessment.EditPeerAssessmentView.prototype = {
**/
getID: function() {
return $(this.element).attr('id');
}
},
/**
Mark validation errors.
Returns:
Boolean indicating whether the view is valid.
**/
validate: function() {
var startValid = this.startDatetimeControl.validate();
var dueValid = this.dueDatetimeControl.validate();
var mustGradeValid = this.mustGradeField.validate();
var mustBeGradedByValid = this.mustBeGradedByField.validate();
return startValid && dueValid && mustGradeValid && mustBeGradedByValid;
},
/**
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");
}
if (this.mustGradeField.validationErrors().length > 0) {
errors.push("Peer assessment must grade is invalid");
}
if(this.mustBeGradedByField.validationErrors().length > 0) {
errors.push("Peer assessment must be graded by is invalid");
}
return errors;
},
/**
Clear all validation errors from the UI.
**/
clearValidationErrors: function() {
this.startDatetimeControl.clearValidationErrors();
this.dueDatetimeControl.clearValidationErrors();
this.mustGradeField.clearValidationErrors();
this.mustBeGradedByField.clearValidationErrors();
},
};
......@@ -242,14 +300,54 @@ 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() {
var startValid = this.startDatetimeControl.validate();
var dueValid = this.dueDatetimeControl.validate();
return startValid && dueValid;
},
/**
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();
},
};
/**
......@@ -342,7 +440,11 @@ OpenAssessment.EditStudentTrainingView.prototype = {
**/
getID: function() {
return $(this.element).attr('id');
}
},
validate: function() { return true; },
validationErrors: function() { return []; },
clearValidationErrors: function() {},
};
/**
......@@ -417,12 +519,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
......@@ -3,23 +3,108 @@ Utilities for reading / writing fields.
**/
OpenAssessment.Fields = {
stringField: function(sel, value) {
if (typeof(value) !== "undefined") { sel.val(value); }
if (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); }
if (value !== undefined) { sel.prop("checked", value); }
return sel.prop("checked");
},
};
/**
Integer input.
Args:
inputSel (JQuery selector or DOM element): The input field.
Keyword args:
min (int): The minimum value allowed in the input.
max (int): The maximum value allowed in the input.
**/
OpenAssessment.IntField = function(inputSel, restrictions) {
this.max = restrictions.max;
this.min = restrictions.min;
this.input = $(inputSel);
};
OpenAssessment.IntField.prototype = {
/**
Retrieve the integer value from the input.
Decimal values will be truncated, and non-numeric
values will become NaN.
Returns:
integer or NaN
**/
get: function() {
return parseInt(this.input.val().trim(), 10);
},
/**
Set the input value.
Args:
val (int or string)
**/
set: function(val) {
this.input.val(val);
},
/**
Mark validation errors if the field does not satisfy the restrictions.
Fractional values are not considered valid integers.
This will trim whitespace from the field, so " 34 " would be considered
a valid input.
Returns:
Boolean indicating whether the field's value is valid.
**/
validate: function() {
var value = this.get();
var isValid = !isNaN(value) && value >= this.min && value <= this.max;
// Decimal values not allowed
if (this.input.val().indexOf(".") !== -1) {
isValid = false;
}
if (!isValid) {
this.input.addClass("openassessment_highlighted_field");
}
return isValid;
},
/**
Clear any validation errors from the UI.
**/
clearValidationErrors: function() {
this.input.removeClass("openassessment_highlighted_field");
},
/**
Return a list of validation errors currently displayed
in the UI. Mainly useful for testing.
Returns:
list of strings
**/
validationErrors: function() {
var hasError = this.input.hasClass("openassessment_highlighted_field");
return hasError ? ["Int field is invalid"] : [];
},
};
/**
Show and hide elements based on a checkbox.
Args:
......@@ -108,7 +193,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 +203,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
......@@ -4,7 +4,7 @@ changes to the rubric.
**/
OpenAssessment.StudentTrainingListener = function() {
this.element = $('#oa_student_training_editor');
this.alert = new OpenAssessment.ValidationAlert($('#openassessment_rubric_validation_alert'));
this.alert = new OpenAssessment.ValidationAlert();
};
OpenAssessment.StudentTrainingListener.prototype = {
......@@ -22,6 +22,7 @@ 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;
......
......@@ -27,7 +27,6 @@ OpenAssessment.EditRubricView = function(element, notifier) {
}
);
this.criteriaContainer.addEventListeners();
this.alert = new OpenAssessment.ValidationAlert($('#openassessment_rubric_validation_alert', this.element));
};
OpenAssessment.EditRubricView.prototype = {
......@@ -190,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 = (this.validate() && isValid);
});
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 = true;
isValid = (this.startDatetimeControl.validate() && isValid);
isValid = (this.dueDatetimeControl.validate() && isValid);
// Validate each of the assessment views
$.each(this.assessmentViews, function() {
isValid = (this.validate() && isValid);
});
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,105 @@
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;
this.rubricContentElement = $('#openassessment_rubric_content_editor');
OpenAssessment.ValidationAlert = function() {
this.element = $('#openassessment_validation_alert');
this.editorElement = $(this.element).parent();
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();
}
);
this.ALERT_YELLOW = 'rgb(192, 172, 0)';
this.DARK_GREY = '#323232';
};
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');
// Finds the height of all other elements in the editor_and_tabs (the Header) and sets the height
// of the editing area to be 100% of that element minus those constraints.
var headerHeight = $('#openassessment_editor_header', this.editorElement).outerHeight();
this.element.addClass('covered');
var styles = {
'height': 'Calc(100% - ' + headerHeight + 'px)',
'border-top-right-radius': '3px',
'border-top-left-radius': '3px'
};
$('.oa_editor_content_wrapper', this.editorElement).each( function () {
$(this).css(styles);
});
return this;
},
/**
Displays the alert.
*/
Displays the alert.
Returns:
OpenAssessment.ValidationAlert
*/
show : function() {
this.element.removeClass('is--hidden');
this.rubricContentElement.addClass('openassessment_alert_shown');
var view = this;
if (this.isVisible()){
$(this.element).animate(
{'background-color': view.ALERT_YELLOW}, 300, 'swing', function () {
$(this).animate({'background-color': view.DARK_GREY}, 700, 'swing');
}
);
} else {
// Finds the height of all other elements in the editor_and_tabs (the Header and Alert) and sets
// the height of the editing area to be 100% of that element minus those constraints.
this.element.removeClass('covered');
var alertHeight = this.element.outerHeight();
var headerHeight = $('#openassessment_editor_header', this.editorElement).outerHeight();
var heightString = 'Calc(100% - ' + (alertHeight + headerHeight) + 'px)';
var styles = {
'height': heightString,
'border-top-right-radius': '0px',
'border-top-left-radius': '0px'
};
$('.oa_editor_content_wrapper', this.editorElement).each(function () {
$(this).css(styles);
});
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;
},
/**
......@@ -60,7 +111,7 @@ OpenAssessment.ValidationAlert.prototype = {
**/
isVisible: function() {
return !this.element.hasClass('is--hidden');
return !this.element.hasClass('covered');
},
/**
......
......@@ -184,7 +184,7 @@
.openassessment_editor_content_and_tabs {
width: 100%;
height: Calc(100% - 97px);
height: Calc(100% - 55px);
}
#oa_editor_window_title{
......@@ -250,14 +250,84 @@
}
}
#openassessment_validation_alert{
width: 100%;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
background-color: #323232;
border-bottom: 3px solid rgb(192, 172, 0);
padding: 10px;
position: absolute;
z-index: 10;
max-height: 200px;
.openassessment_alert_icon{
position: absolute;
top: 35%;
font-size: 200%;
left: 3%;
}
.openassessment_alert_icon:before{
font-family: FontAwesome;
content: "\f071";
display: inline-block;
color: rgb(192, 172, 0);
float: left;
height: 0;
width: 0;
}
.openassessment_alert_header {
width: 85%;
margin: 0 5% 0 10%;
.openassessment_alert_title {
width: auto;
color: white;
}
.openassessment_alert_message {
font-size: 80%;
color: darkgray;
}
}
// with cancel
.openassessment_alert_close {
display: inline-block;
position: absolute;
top: 0px;
right: 0px;
color: #e9e9e9;
text-align: center;
margin: 5px 10px;
[class^="icon"] {
width: auto;
margin: 0;
padding: 2px;
}
&:hover {
color: $blue;
}
}
}
.oa_editor_content_wrapper {
height: Calc(100% - 1px);
height: Calc(100% - 42px);
width: 100%;
border-radius: 3px;
border: 1px solid $edx-gray-d1;
background-color: white;
overflow-y: scroll;
position: absolute;
bottom: 0;
z-index: 11;
transition: height 1s ease-in-out 0;
-webkit-transition: height 1s ease-in-out 0;
-moz-transition: height 1s ease-in-out 0;
}
#openassessment_prompt_editor {
......@@ -424,78 +494,6 @@
#oa_rubric_editor_wrapper{
#openassessment_rubric_validation_alert{
height: auto;
width: 100%;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
background-color: #323232;
border-bottom: 3px solid rgb(192, 172, 0);
padding: 10px;
position: absolute;
z-index: 10;
min-height: 70px;
@include transition (color 0.50s ease-in-out 0s);
.openassessment_alert_icon:before{
font-family: FontAwesome;
content: "\f071";
display: inline-block;
color: rgb(192, 172, 0);
float: left;
font-size: 200%;
margin: 1.5% 0px 0px 2%;
}
.openassessment_alert_header {
width: 85%;
margin: 0 5% 0 10%;
.openassessment_alert_title {
width: auto;
color: white;
}
.openassessment_alert_message {
font-size: 80%;
color: darkgray;
}
}
// with cancel
.openassessment_alert_close {
display: inline-block;
position: absolute;
top: 0px;
right: 0px;
color: #e9e9e9;
background-color: #323232;
text-align: center;
margin: 5px 10px;
[class^="icon"] {
width: auto;
margin: 0;
padding: 2px;
}
&:hover {
color: $blue;
}
}
}
#openassessment_rubric_content_editor{
height: 100%;
width: 100%;
overflow-y: scroll;
}
#openassessment_rubric_content_editor.openassessment_alert_shown{
height: Calc(100% - 70px);
position: absolute;
bottom: 0;
}
.wrapper-comp-settings{
display: block;
}
......@@ -858,11 +856,14 @@
height: 100%;
}
#student_training_settings_editor {
#openassessment-editor {
.openassessment_highlighted_field{
background-color: $edx-pink-l4;
border-color: red;
border-width: 2px;
}
}
#student_training_settings_editor {
.openassessment_training_example {
padding: 5px;
......
......@@ -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