Commit c9625e18 by Will Daly

Merge pull request #578 from edx/will/authoring-validate-student-training

Client-side validation for student-training examples
parents 2dea0227 b29503ae
...@@ -165,6 +165,10 @@ describe("OpenAssessment edit assessment views", function() { ...@@ -165,6 +165,10 @@ describe("OpenAssessment edit assessment views", function() {
var view = null; var view = null;
beforeEach(function() { beforeEach(function() {
// We need to load the student-training specific editing view
// so that the student training example template is properly initialized.
loadFixtures('oa_edit_student_training.html');
var element = $("#oa_student_training_editor").get(0); var element = $("#oa_student_training_editor").get(0);
view = new OpenAssessment.EditStudentTrainingView(element); view = new OpenAssessment.EditStudentTrainingView(element);
}); });
...@@ -173,28 +177,65 @@ describe("OpenAssessment edit assessment views", function() { ...@@ -173,28 +177,65 @@ describe("OpenAssessment edit assessment views", function() {
it("loads a description", function () { it("loads a description", function () {
// This assumes a particular structure of the DOM, // This assumes a particular structure of the DOM,
// which is set by the HTML fixture. // which is set by the HTML fixture.
var examples = view.exampleContainer.getItemValues(); expect(view.description()).toEqual({
expect(examples.length).toEqual(0); examples: [
{
answer: 'Test answer',
options_selected: [
{
criterion: 'criterion_with_two_options',
option: 'option_1'
}
]
}
]
});
}); });
it("modifies a description", function () { it("modifies a description", function () {
view.exampleContainer.add(); view.exampleContainer.add();
var examples = view.exampleContainer.getItemValues(); expect(view.description()).toEqual({
expect(examples.length).toEqual(1); examples: [
}); {
it("returns the correct format", function () { answer: 'Test answer',
view.exampleContainer.add(); options_selected: [
var examples = view.exampleContainer.getItemValues(); {
expect(examples).toEqual( criterion: 'criterion_with_two_options',
[ option: 'option_1'
}
]
},
{ {
answer: "", answer: '',
options_selected: [] options_selected: [
{
criterion: 'criterion_with_two_options',
option: ''
}
]
} }
] ]
); });
}); });
it("shows an alert when disabled", function() { testAlertOnDisable(view); }); it("shows an alert when disabled", function() { testAlertOnDisable(view); });
it("validates selected options", function() {
// On page load, the examples should be valid
expect(view.validate()).toBe(true);
expect(view.validationErrors()).toEqual([]);
// Add a new training example (default no option selected)
view.exampleContainer.add();
// Now there should be a validation error
expect(view.validate()).toBe(false);
expect(view.validationErrors()).toContain("Student training example is invalid.");
// Clear validation errors
view.clearValidationErrors();
expect(view.validationErrors()).toEqual([]);
});
}); });
describe("OpenAssessment.EditExampleBasedAssessmentView", function() { describe("OpenAssessment.EditExampleBasedAssessmentView", function() {
......
...@@ -34,7 +34,7 @@ OpenAssessment.ItemUtilities = { ...@@ -34,7 +34,7 @@ OpenAssessment.ItemUtilities = {
var points = $(element).data('points'); var points = $(element).data('points');
var label = $(element).data('label'); var label = $(element).data('label');
// We don't want the lack of a label to make it look like - 1 points. // We don't want the lack of a label to make it look like - 1 points.
if (label == ""){ if (label === ""){
label = gettext('Unnamed Option'); label = gettext('Unnamed Option');
} }
var singularString = label + " - " + points + " point"; var singularString = label + " - " + points + " point";
...@@ -484,12 +484,17 @@ OpenAssessment.RubricCriterion.prototype = { ...@@ -484,12 +484,17 @@ OpenAssessment.RubricCriterion.prototype = {
**/ **/
OpenAssessment.TrainingExample = function(element){ OpenAssessment.TrainingExample = function(element){
this.element = element; this.element = element;
// Goes through and instantiates the option description in the training example for each option. this.criteria = $(".openassessment_training_example_criterion_option", this.element);
$(".openassessment_training_example_criterion_option", this.element) .each( function () { this.answer = $('.openassessment_training_example_essay', this.element).first();
$('option', this).each(function(){
OpenAssessment.ItemUtilities.refreshOptionString($(this)); // Initialize the option label in the training example for each option.
}); this.criteria.each(
}); function () {
$('option', this).each(function(){
OpenAssessment.ItemUtilities.refreshOptionString($(this));
});
}
);
}; };
OpenAssessment.TrainingExample.prototype = { OpenAssessment.TrainingExample.prototype = {
...@@ -500,16 +505,17 @@ OpenAssessment.TrainingExample.prototype = { ...@@ -500,16 +505,17 @@ OpenAssessment.TrainingExample.prototype = {
// Iterates through all of the options selected by the training example, and adds them // Iterates through all of the options selected by the training example, and adds them
// to a list. // to a list.
var optionsSelected = []; var optionsSelected = this.criteria.map(
$(".openassessment_training_example_criterion_option", this.element) .each( function () { function () {
optionsSelected.push({ return {
criterion: $(this).data('criterion'), criterion: $(this).data('criterion'),
option: $(this).prop('value') option: $(this).prop('value')
}); };
}); }
).get();
return { return {
answer: $('.openassessment_training_example_essay', this.element).first().prop('value'), answer: this.answer.prop('value'),
options_selected: optionsSelected options_selected: optionsSelected
}; };
}, },
...@@ -519,7 +525,61 @@ OpenAssessment.TrainingExample.prototype = { ...@@ -519,7 +525,61 @@ OpenAssessment.TrainingExample.prototype = {
removeHandler: function() {}, removeHandler: function() {},
updateHandler: function() {}, updateHandler: function() {},
validate: function() { return true; }, /**
validationErrors: function() { return []; }, Mark validation errors.
clearValidationErrors: function() {}
Returns:
Boolean indicating whether the criterion is valid.
**/
validate: function() {
var isValid = true;
this.criteria.each(
function() {
var isOptionValid = ($(this).prop('value') !== "");
isValid = isOptionValid && isValid;
if (!isOptionValid) {
$(this).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 errors = [];
this.criteria.each(
function() {
var hasError = $(this).hasClass("openassessment_highlighted_field");
if (hasError) {
errors.push("Student training example is invalid.");
}
}
);
return errors;
},
/**
Retrieve all elements representing items in this container.
Returns:
array of container item objects
**/
clearValidationErrors: function() {
this.criteria.each(
function() { $(this).removeClass("openassessment_highlighted_field"); }
);
}
}; };
\ No newline at end of file
...@@ -466,18 +466,56 @@ OpenAssessment.EditStudentTrainingView.prototype = { ...@@ -466,18 +466,56 @@ OpenAssessment.EditStudentTrainingView.prototype = {
}, },
/** /**
Gets the ID of the assessment Gets the ID of the assessment
Returns: Returns:
string (CSS ID of the Element object) string (CSS ID of the Element object)
**/ **/
getID: function() { getID: function() {
return $(this.element).attr('id'); return $(this.element).attr('id');
}, },
validate: function() { return true; }, /**
validationErrors: function() { return []; }, Mark validation errors.
clearValidationErrors: function() {},
Returns:
Boolean indicating whether the view is valid.
**/
validate: function() {
var isValid = true;
$.each(this.exampleContainer.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.exampleContainer.getAllItems(), function() {
errors = errors.concat(this.validationErrors());
});
return errors;
},
/**
Clear all validation errors from the UI.
**/
clearValidationErrors: function() {
$.each(this.exampleContainer.getAllItems(), function() {
this.clearValidationErrors();
});
},
}; };
/** /**
......
...@@ -955,7 +955,7 @@ ...@@ -955,7 +955,7 @@
} }
], ],
"editor_assessments_order": ["student-training", "peer-assessment", "self-assessment"], "editor_assessments_order": ["student-training", "peer-assessment", "self-assessment"],
"expected_error": "example 1 has an extra option" "expected_error": "student training example option does not match the rubric"
}, },
"missing_editor_assessments_order": { "missing_editor_assessments_order": {
......
...@@ -303,7 +303,7 @@ class ValidationIntegrationTest(TestCase): ...@@ -303,7 +303,7 @@ class ValidationIntegrationTest(TestCase):
# Expect a validation error # Expect a validation error
is_valid, msg = self.validator(self.RUBRIC, mutated_assessments) is_valid, msg = self.validator(self.RUBRIC, mutated_assessments)
self.assertFalse(is_valid) self.assertFalse(is_valid)
self.assertEqual(msg, u'Example 1 has an extra option for "Invalid criterion!"; Example 1 is missing an option for "vocabulary"') self.assertEqual(msg, u'Student training example option does not match the rubric.')
def test_student_training_examples_invalid_option(self): def test_student_training_examples_invalid_option(self):
# Mutate the assessment training examples so the option names don't match the rubric # Mutate the assessment training examples so the option names don't match the rubric
...@@ -313,7 +313,7 @@ class ValidationIntegrationTest(TestCase): ...@@ -313,7 +313,7 @@ class ValidationIntegrationTest(TestCase):
# Expect a validation error # Expect a validation error
is_valid, msg = self.validator(self.RUBRIC, mutated_assessments) is_valid, msg = self.validator(self.RUBRIC, mutated_assessments)
self.assertFalse(is_valid) self.assertFalse(is_valid)
self.assertEqual(msg, u'Example 1 has an invalid option for "vocabulary": "Invalid option!"') self.assertEqual(msg, u'Student training example option does not match the rubric.')
def test_example_based_assessment_duplicate_point_values(self): def test_example_based_assessment_duplicate_point_values(self):
# Mutate the rubric so that two options have the same point value # Mutate the rubric so that two options have the same point value
......
...@@ -294,7 +294,7 @@ def validate_assessment_examples(rubric_dict, assessments, _): ...@@ -294,7 +294,7 @@ def validate_assessment_examples(rubric_dict, assessments, _):
# examples against the rubric. # examples against the rubric.
errors = validate_training_examples(rubric_dict, examples) errors = validate_training_examples(rubric_dict, examples)
if errors: if errors:
return False, "; ".join(errors) return False, _(u"Student training example option does not match the rubric.")
return True, u'' return True, u''
......
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