Commit 2232813d by Will Daly

Add tests for dynamic update of student training examples based on rubric changes.

Use dependency injection for event notification; moved some classes around.
Move the container JS classes into the Studio minified JS.
Fix a bug that broke student training updates in Firefox and PhantomJS.
Move fixture path installation into a common file shared by all tests.

Add tests that verify the correct notifications
are sent by the rubric view.
parent a05d51fd
......@@ -27,6 +27,7 @@ module.exports = function(config) {
'src/*.js',
'src/lms/*.js',
'src/studio/*.js',
'spec/test_shared.js',
'spec/*.js',
'spec/lms/*.js',
'spec/studio/*.js',
......
......@@ -391,7 +391,7 @@
"submission_due": "2014-10-1T10:00:00",
"criteria": [
{
"name": "52bfbd0eb3044212b809564866e77079",
"name": "criterion_1",
"label": "Criterion with two options",
"prompt": "Prompt for criterion with two options",
"order_num": 0,
......@@ -400,14 +400,14 @@
{
"order_num": 0,
"points": 1,
"name": "85bbbecbb6a343f8a2146cde0e609ad0",
"name": "option_1",
"label": "Fair",
"explanation": "Fair explanation"
},
{
"order_num": 1,
"points": 2,
"name": "5936d5b9e281403ca123964055d4719a",
"name": "option_2",
"label": "Good",
"explanation": "Good explanation"
}
......@@ -415,7 +415,7 @@
"points_possible": 2
},
{
"name": "d96bb68a69ee4ccb8f86c753b6924f75",
"name": "criterion_2",
"label": "Criterion with no options",
"prompt": "Prompt for criterion with no options",
"order_num": 0,
......@@ -424,7 +424,7 @@
"points_possible": 0
},
{
"name": "2ca052403b06424da714f7a80dfb954d",
"name": "criterion_3",
"label": "Criterion with optional feedback",
"prompt": "Prompt for criterion with optional feedback",
"order_num": 2,
......@@ -433,7 +433,7 @@
{
"order_num": 0,
"points": 2,
"name": "d7445661a89b4b339b9788cb7225a603",
"name": "option_1",
"label": "Good",
"explanation": "Good explanation"
}
......@@ -462,6 +462,148 @@
"output": "oa_edit.html"
},
{
"template": "openassessmentblock/edit/oa_edit.html",
"context": {
"prompt": "Test prompt",
"title": "Test title",
"submission_due": "2014-10-1T10:00:00",
"criteria": [
{
"name": "criterion_with_two_options",
"label": "Criterion with two options",
"prompt": "Criterion with two options prompt",
"order_num": 0,
"feedback": "disabled",
"options": [
{
"order_num": 0,
"points": 1,
"name": "option_1",
"label": "Fair",
"explanation": "Fair explanation"
},
{
"order_num": 1,
"points": 2,
"name": "option_2",
"label": "Good",
"explanation": "Good explanation"
}
],
"points_possible": 2
},
{
"name": "criterion_no_options",
"label": "Criterion with no options",
"prompt": "Criterion with no options prompt",
"order_num": 0,
"options": [],
"feedback": "required",
"points_possible": 0
}
],
"assessments": {
"training": {
"examples": [
{
"answer": "Test answer",
"criteria": [
{
"name": "criterion_with_two_options",
"label": "Criterion with two options",
"prompt": "Criterion with two options prompt",
"order_num": 0,
"feedback": "disabled",
"options": [
{
"order_num": 0,
"points": 1,
"name": "option_1",
"label": "Fair",
"explanation": "Fair explanation"
},
{
"order_num": 1,
"points": 2,
"name": "option_2",
"label": "Good",
"explanation": "Good explanation"
}
],
"points_possible": 2,
"option_selected": "option_1"
},
{
"name": "criterion_no_options",
"label": "Criterion with no options",
"prompt": "Criterion with no options prompt",
"order_num": 0,
"options": [],
"feedback": "required",
"points_possible": 0,
"option_selected": ""
}
]
}
],
"template": {
"answer": "",
"criteria": [
{
"name": "criterion_with_two_options",
"label": "Criterion with two options",
"prompt": "Criterion with two options prompt",
"order_num": 0,
"feedback": "disabled",
"options": [
{
"order_num": 0,
"points": 1,
"name": "option_1",
"label": "Fair",
"explanation": "Fair explanation"
},
{
"order_num": 1,
"points": 2,
"name": "option_2",
"label": "Good",
"explanation": "Good explanation"
}
],
"points_possible": 2,
"option_selected": ""
},
{
"name": "criterion_no_options",
"label": "Criterion with no options",
"prompt": "Criterion with no options prompt",
"order_num": 0,
"options": [],
"feedback": "required",
"points_possible": 0,
"option_selected": ""
}
]
}
},
"peer_assessment": {
"start": "",
"due": "",
"must_grade": 5,
"must_be_graded_by": 3
}
},
"editor_assessments_order": [
"student_training",
"peer_assessment",
"self_assessment",
"example_based_assessment"
]
},
"output": "oa_edit_student_training.html"
},
{
"template": "openassessmentblock/staff_debug/staff_debug.html",
"context": {
"status_counts": {
......
......@@ -56,7 +56,6 @@ describe("OpenAssessment.BaseView", function() {
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_base.html');
// Create a new stub server
......
......@@ -42,7 +42,6 @@ describe("OpenAssessment.GradeView", function() {
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_grade_complete.html');
// Create the stub server
......
......@@ -44,7 +44,6 @@ describe("OpenAssessment.PeerView", function() {
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_peer_assessment.html');
// Create a new stub server
......
......@@ -97,7 +97,6 @@ describe("OpenAssessment.ResponseView", function() {
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_response.html');
// Create stub objects
......
......@@ -39,7 +39,6 @@ describe("OpenAssessment.SelfView", function() {
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_self_assessment.html');
// Create a new stub server
......
......@@ -69,9 +69,6 @@ describe("OpenAssessment.StaffInfoView", function() {
};
beforeEach(function() {
// Configure the Jasmine fixtures path
jasmine.getFixtures().fixturesPath = 'base/fixtures';
// Create a new stub server
server = new StubServer();
......
......@@ -45,7 +45,6 @@ describe("OpenAssessment.StudentTrainingView", function() {
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_student_training.html');
// Create a new stub server
......
......@@ -7,7 +7,6 @@ describe("OpenAssessment.Rubric", function() {
var rubric = null;
beforeEach(function() {
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_rubric.html');
var el = $("#peer-assessment--001__assessment").get(0);
......
......@@ -52,28 +52,28 @@ describe("OpenAssessment.StudioView", function() {
{
order_num: 0,
label: "Criterion with two options",
name: "52bfbd0eb3044212b809564866e77079",
name: "criterion_1",
prompt: "Prompt for criterion with two options",
feedback: "disabled",
options: [
{
order_num: 0,
points: 1,
name: "85bbbecbb6a343f8a2146cde0e609ad0",
name: "option_1",
label: "Fair",
explanation: "Fair explanation"
},
{
order_num: 1,
points: 2,
name: "5936d5b9e281403ca123964055d4719a",
name: "option_2",
label: "Good",
explanation: "Good explanation"
}
]
},
{
name: "d96bb68a69ee4ccb8f86c753b6924f75",
name: "criterion_2",
label: "Criterion with no options",
prompt: "Prompt for criterion with no options",
order_num: 1,
......@@ -81,7 +81,7 @@ describe("OpenAssessment.StudioView", function() {
feedback: "required",
},
{
name: "2ca052403b06424da714f7a80dfb954d",
name: "criterion_3",
label: "Criterion with optional feedback",
prompt: "Prompt for criterion with optional feedback",
order_num: 2,
......@@ -90,7 +90,7 @@ describe("OpenAssessment.StudioView", function() {
{
order_num: 0,
points: 2,
name: "d7445661a89b4b339b9788cb7225a603",
name: "option_1",
label: "Good",
explanation: "Good explanation"
}
......@@ -121,7 +121,6 @@ describe("OpenAssessment.StudioView", function() {
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_edit.html');
// Create the stub server
......@@ -190,7 +189,7 @@ describe("OpenAssessment.StudioView", function() {
expect(runtime.notify).toHaveBeenCalledWith('cancel', {});
});
it("displays an error when server reports an update XML error", function() {
it("displays an error when server reports an error", function() {
server.updateError = true;
view.save();
expect(runtime.notify).toHaveBeenCalledWith('error', {msg: 'Test error'});
......
......@@ -18,7 +18,6 @@ describe("OpenAssessment edit assessment views", function() {
};
beforeEach(function() {
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_edit.html');
});
......@@ -126,4 +125,4 @@ describe("OpenAssessment edit assessment views", function() {
it("Enables and disables", function() { testEnableAndDisable(view); });
it("Loads a description", function() { testLoadXMLExamples(view); });
});
});
\ No newline at end of file
});
/**
Tests for the student training listener,
which dynamically updates student training examples
based on rubric changes.
**/
describe("OpenAssessment.StudentTrainingListener", function() {
var listener = null;
/**
Check that all student training examples have the expected
criteria or option labels.
Args:
actual (array): A list of example criteria or option labels
(object literals) retrieved from the DOM.
expected (object literal): The expected value for each example.
numExamples (int, optional): The number of student training examples
(defaults to 1).
**/
var assertExampleLabels = function(actual, expected, numExamples) {
// The most common case is one example, so use that as a default.
if (typeof(numExamples) == "undefined") {
numExamples = 1;
}
// Add one to the number of examples to include the client-side template.
expect(actual.length).toEqual(numExamples + 1);
// Verify that each example matches what we expect.
// Since there is only one rubric for the problem,
// the training examples should always match that rubric.
for (var index in actual) {
for (var criterionName in expected) {
expect(actual[index][criterionName]).toEqual(expected[criterionName]);
}
}
};
beforeEach(function() {
loadFixtures('oa_edit_student_training.html');
listener = new OpenAssessment.StudentTrainingListener();
});
it("updates the label of an option", function() {
// Initial state, set by the fixture
assertExampleLabels(
listener.examplesOptionsLabels(),
{
criterion_with_two_options: {
"": "Not Scored",
option_1: "Fair - 1 points",
option_2: "Good - 2 points"
}
}
);
// Update the option label and points,
listener.optionUpdated({
criterionName: "criterion_with_two_options",
name: "option_1",
label: "This is a new label!",
points: 42
});
// Verify the new state
assertExampleLabels(
listener.examplesOptionsLabels(),
{
criterion_with_two_options: {
"": "Not Scored",
option_1: "This is a new label! - 42 points",
option_2: "Good - 2 points"
}
}
);
});
it("removes an option and displays an alert", function() {
// Initial state, set by the fixture
assertExampleLabels(
listener.examplesOptionsLabels(),
{
criterion_with_two_options: {
"": "Not Scored",
option_1: "Fair - 1 points",
option_2: "Good - 2 points"
}
}
);
expect(listener.alert.isVisible()).toBe(false);
// Remove an option
listener.optionRemove({
criterionName: "criterion_with_two_options",
name: "option_1"
});
// Verify the new state
assertExampleLabels(
listener.examplesOptionsLabels(),
{
criterion_with_two_options: {
"": "Not Scored",
option_2: "Good - 2 points"
}
}
);
// The alert should be displayed
expect(listener.alert.isVisible()).toBe(true);
});
it("removes a criterion if the criterion has no options", function() {
// Initial state, set by the fixture
assertExampleLabels(
listener.examplesCriteriaLabels(),
{ criterion_with_two_options: "Criterion with two options" }
);
expect(listener.alert.isVisible()).toBe(false);
// Remove all options for the criterion
listener.removeAllOptions({
criterionName: "criterion_with_two_options"
});
// Since the criterion has no options, it should no longer
// be available in student training
assertExampleLabels(listener.examplesCriteriaLabels(), {}, 1);
// The alert should be displayed
expect(listener.alert.isVisible()).toBe(true);
});
it("updates the label of a criterion", function() {
// Initial state, set by the fixture
assertExampleLabels(
listener.examplesCriteriaLabels(),
{ criterion_with_two_options: "Criterion with two options" }
);
// Update a label
listener.criterionUpdated({
criterionName: "criterion_with_two_options",
criterionLabel: "This is a new label!",
});
// Verify the new state
assertExampleLabels(
listener.examplesCriteriaLabels(),
{ criterion_with_two_options: "This is a new label!" }
);
});
it("adds a criterion and options", function() {
// Initial state, set by the fixture
assertExampleLabels(
listener.examplesCriteriaLabels(),
{ criterion_with_two_options: "Criterion with two options" }
);
// Add the criterion, which has no options
listener.criterionAdd({
criterionName: "new_criterion",
label: "This is a new criterion!"
});
// Since the criterion has no options, it should not
// be displayed.
assertExampleLabels(
listener.examplesCriteriaLabels(),
{ criterion_with_two_options: "Criterion with two options" }
);
// Add an option to the criterion
listener.optionAdd({
criterionName: "new_criterion",
name: "new_option",
label: "This is a new option!",
points: 56
});
// Now the criterion should be visible in student training
assertExampleLabels(
listener.examplesCriteriaLabels(),
{
criterion_with_two_options: "Criterion with two options",
new_criterion: "This is a new criterion!"
}
);
assertExampleLabels(
listener.examplesOptionsLabels(),
{
criterion_with_two_options: {
"": "Not Scored",
option_1: "Fair - 1 points",
option_2: "Good - 2 points",
},
new_criterion: {
"": "Not Scored",
new_option: "This is a new option! - 56 points"
}
}
);
// Add another option to the criterion
listener.optionAdd({
criterionName: "new_criterion",
name: "yet_another_option",
label: "This is yet another option!",
points: 27
});
assertExampleLabels(
listener.examplesOptionsLabels(),
{
criterion_with_two_options: {
"": "Not Scored",
option_1: "Fair - 1 points",
option_2: "Good - 2 points",
},
new_criterion: {
"": "Not Scored",
new_option: "This is a new option! - 56 points",
yet_another_option: "This is yet another option! - 27 points"
}
}
);
});
it("removes a criterion and displays an alert", function() {
// Initial state, set by the fixture
assertExampleLabels(
listener.examplesCriteriaLabels(),
{ criterion_with_two_options: "Criterion with two options" }
);
expect(listener.alert.isVisible()).toBe(false);
// Remove the criterion
listener.criterionRemove({
criterionName: "criterion_with_two_options"
});
// The criterion should no longer be displayed
assertExampleLabels(listener.examplesCriteriaLabels(), {}, 1);
// The alert should be displayed
expect(listener.alert.isVisible()).toBe(true);
});
});
describe("OpenAssessment.Notifier", function() {
var notifier = null;
var listeners = [];
var StubListener = function() {
this.receivedData = null;
this.testNotification = function(data) {
this.receivedData = data;
};
};
beforeEach(function() {
listeners = [ new StubListener(), new StubListener() ];
notifier = new OpenAssessment.Notifier(listeners);
});
it("notifies listeners when a notification fires", function() {
// Fire a notification that the listeners don't respond to
notifier.notificationFired("ignore this!", {});
expect(listeners[0].receivedData).toBe(null);
expect(listeners[1].receivedData).toBe(null);
// Fire a notification that the listeners care about
var testData = { foo: "bar" };
notifier.notificationFired("testNotification", testData);
// Check that the listeners were notified
expect(listeners[0].receivedData).toBe(testData);
expect(listeners[1].receivedData).toBe(testData);
});
});
......@@ -8,7 +8,6 @@ describe("OpenAssessment.EditPromptView", function() {
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_edit.html');
// Create the view
......@@ -22,4 +21,4 @@ describe("OpenAssessment.EditPromptView", function() {
view.promptText("This is a test prompt!");
expect(view.promptText()).toEqual("This is a test prompt!");
});
});
\ No newline at end of file
});
......@@ -3,13 +3,25 @@ Tests for the rubric editing view.
**/
describe("OpenAssessment.EditRubricView", function() {
// Use a stub notifier implementation that simply stores
// the notifications it receives.
var notifier = null;
var StubNotifier = function() {
this.notifications = [];
this.notificationFired = function(name, data) {
this.notifications.push({
name: name,
data: data
});
};
};
var view = null;
beforeEach(function() {
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_edit.html');
var el = $("#oa_rubric_editor_wrapper").get(0);
view = new OpenAssessment.EditRubricView(el);
notifier = new StubNotifier();
view = new OpenAssessment.EditRubricView(el, notifier);
});
it("reads a criteria definition from the editor", function() {
......@@ -20,7 +32,7 @@ describe("OpenAssessment.EditRubricView", function() {
// Criterion with two options, feedback disabled
expect(criteria[0]).toEqual({
name: "52bfbd0eb3044212b809564866e77079",
name: "criterion_1",
label: "Criterion with two options",
prompt: "Prompt for criterion with two options",
order_num: 0,
......@@ -29,14 +41,14 @@ describe("OpenAssessment.EditRubricView", function() {
{
order_num: 0,
points: 1,
name: "85bbbecbb6a343f8a2146cde0e609ad0",
name: "option_1",
label: "Fair",
explanation: "Fair explanation"
},
{
order_num: 1,
points: 2,
name: "5936d5b9e281403ca123964055d4719a",
name: "option_2",
label: "Good",
explanation: "Good explanation"
}
......@@ -45,7 +57,7 @@ describe("OpenAssessment.EditRubricView", function() {
// Criterion with no options, feedback required
expect(criteria[1]).toEqual({
name: "d96bb68a69ee4ccb8f86c753b6924f75",
name: "criterion_2",
label: "Criterion with no options",
prompt: "Prompt for criterion with no options",
order_num: 1,
......@@ -55,7 +67,7 @@ describe("OpenAssessment.EditRubricView", function() {
// Criterion with one option, feeback optional
expect(criteria[2]).toEqual({
name: "2ca052403b06424da714f7a80dfb954d",
name: "criterion_3",
label: "Criterion with optional feedback",
prompt: "Prompt for criterion with optional feedback",
order_num: 2,
......@@ -64,7 +76,7 @@ describe("OpenAssessment.EditRubricView", function() {
{
order_num: 0,
points: 2,
name: "d7445661a89b4b339b9788cb7225a603",
name: "option_1",
label: "Good",
explanation: "Good explanation"
}
......@@ -75,14 +87,13 @@ describe("OpenAssessment.EditRubricView", function() {
it("creates new criteria and options", function() {
// Delete all existing criteria from the rubric
// Then add new criteria (created from a client-side template)
view.removeAllCriteria();
$.each(view.getAllCriteria(), function() { view.removeCriterion(this); });
view.addCriterion();
view.addCriterion();
// Add an option to the second criterion
view.getCriterionItem(1).addOption();
view.addOption(1);
// Check the definition
// Since no criteria/option names are set, leave them out of the description.
// This will cause the server to assign them unique names.
var criteria = view.criteriaDefinition();
......@@ -124,4 +135,77 @@ describe("OpenAssessment.EditRubricView", function() {
expect(view.feedbackPrompt()).toEqual(prompt);
});
});
\ No newline at end of file
it("fires a notification when an option is added", function() {
view.addOption();
expect(notifier.notifications).toContain({
name: "optionAdd",
data: {
criterionName: 'criterion_1',
criterionLabel: 'Criterion with two options',
name:'0',
label: '',
points : 1
}
});
// Add a second option and ensure that it is given a unique name
view.addOption();
expect(notifier.notifications).toContain({
name: "optionAdd",
data: {
criterionName: 'criterion_1',
criterionLabel: 'Criterion with two options',
name:'1',
label: '',
points : 1
}
});
});
it("fires a notification when an option is removed", function() {
view.removeOption(0, view.getOptionItem(0, 0));
expect(notifier.notifications).toContain({
name: "optionRemove",
data: {
criterionName: 'criterion_1',
name: 'option_1'
}
});
});
it("fires a notification when an option's label or points are updated", function() {
// Simulate what happens when the options label or points are updated
view.getOptionItem(0, 0).updateHandler();
expect(notifier.notifications).toContain({
name: "optionUpdated",
data: {
criterionName: 'criterion_1',
name: 'option_1',
label: 'Fair',
points: 1
}
});
});
it("fires a notification when a criterion's label is updated", function() {
// Simulate what happens when a criterion label is updated
view.getCriterionItem(0).updateHandler();
expect(notifier.notifications).toContain({
name: "criterionUpdated",
data: {
criterionName: 'criterion_1',
criterionLabel: 'Criterion with two options'
}
});
});
it("fires a notification when a criterion is removed", function() {
view.criteriaContainer.remove(view.getCriterionItem(0));
expect(notifier.notifications).toContain({
name: "criterionRemove",
data: {criterionName : 'criterion_1'}
});
});
});
......@@ -22,7 +22,6 @@ describe("OpenAssessment.EditSettingsView", function() {
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_edit.html');
// Create the stub assessment views
......
describe("OpenAssessment.ValidationAlert", function() {
var alert = null;
beforeEach(function() {
loadFixtures('oa_edit.html');
alert = new OpenAssessment.ValidationAlert(
$("#openassessment_rubric_validation_alert")
);
});
it("shows and hides an alert", function() {
// Initially, the alert should be hidden
expect(alert.isVisible()).toBe(false);
// Show the alert
alert.show();
expect(alert.isVisible()).toBe(true);
// Hide the alert
alert.hide();
expect(alert.isVisible()).toBe(false);
});
it("sets the alert title and message", function() {
// Set the title and message
alert.setMessage("new title", "new message");
expect(alert.getTitle()).toEqual("new title");
expect(alert.getMessage()).toEqual("new message");
});
});
\ No newline at end of file
/**
Common test configuration, loaded before any of the spec files.
**/
// Set the fixture path
jasmine.getFixtures().fixturesPath = 'base/fixtures';
......@@ -44,8 +44,8 @@ including for pre-existing items in the container element.
Templates elements are never deleted, so they should be hidden using CSS styles.
Args:
containerItem (object): The container item object used to access
the contents of items in the container.
containerItem (constructor): The constructor of the container item object
used to access the contents of items in the container.
Kwargs:
containerElement (DOM element): The element representing the container.
......@@ -55,25 +55,33 @@ Kwargs:
There may be one of these for each item in the container.
containerItemClass (string): The CSS class of items in the container.
New items will be assigned this class.
notifier (OpenAssessment.Notifier): Used to send notifications of updates to container items.
**/
OpenAssessment.Container = function(containerItem, kwargs) {
this.containerItem = containerItem;
this.containerElement = kwargs.containerElement;
this.templateElement = kwargs.templateElement;
this.addButtonElement = kwargs.addButtonElement;
this.removeButtonClass = kwargs.removeButtonClass;
this.containerItemClass = kwargs.containerItemClass;
this.notifier = kwargs.notifier;
// Since every container item should be instantiated with
// the notifier we were given, create a helper method
// that does this automatically.
var container = this;
this.createContainerItem = function(element) {
return new containerItem(element, container.notifier);
};
// Install a click handler for the add button
$(this.addButtonElement).click($.proxy(this.add, this));
// Find items already in the container and install click
// handlers for the delete buttons.
var container = this;
$("." + this.removeButtonClass, this.containerElement).click(
function(eventData) {
var item = new container.containerItem(eventData.target);
var item = container.createContainerItem(eventData.target);
container.remove(item);
}
);
......@@ -82,7 +90,7 @@ OpenAssessment.Container = function(containerItem, kwargs) {
// own event handlers.
$("." + this.containerItemClass, this.containerElement).each(
function(index, element) {
new container.containerItem(element);
container.createContainerItem(element);
}
);
};
......@@ -112,13 +120,13 @@ OpenAssessment.Container.prototype = {
var containerItem = $("." + this.containerItemClass, this.containerElement).last();
containerItem.find('.' + this.removeButtonClass)
.click(function(eventData) {
var containerItem = new container.containerItem(eventData.target);
var containerItem = container.createContainerItem(eventData.target);
container.remove(containerItem);
} );
// Initialize the item, allowing it to install event handlers.
// Fire event handler for adding a new element
var handlerItem = new this.containerItem(containerItem);
var handlerItem = container.createContainerItem(containerItem);
handlerItem.addHandler();
},
......@@ -133,7 +141,7 @@ OpenAssessment.Container.prototype = {
**/
remove: function(item) {
var itemElement = $(item.element).closest("." + this.containerItemClass);
var containerItem = new this.containerItem(itemElement);
var containerItem = this.createContainerItem(itemElement);
containerItem.removeHandler();
itemElement.remove();
},
......@@ -152,7 +160,7 @@ OpenAssessment.Container.prototype = {
$("." + this.containerItemClass, this.containerElement).each(
function(index, element) {
var containerItem = new container.containerItem(element);
var containerItem = container.createContainerItem(element);
var fieldValues = containerItem.getFieldValues();
values.push(fieldValues);
}
......@@ -173,7 +181,7 @@ OpenAssessment.Container.prototype = {
**/
getItem: function(index) {
var element = $("." + this.containerItemClass, this.containerElement).get(index);
return (element !== undefined) ? new this.containerItem(element) : null;
return (element !== undefined) ? this.createContainerItem(element) : null;
},
/**
......@@ -186,6 +194,6 @@ OpenAssessment.Container.prototype = {
getAllItems: function() {
var container = this;
return $("." + this.containerItemClass, this.containerElement)
.map(function() { return new container.containerItem(this); });
.map(function() { return container.createContainerItem(this); });
}
};
......@@ -15,7 +15,7 @@ OpenAssessment.ItemUtilities = {
createUniqueName: function(selector, nameAttribute) {
var index = 0;
while (index <= selector.length) {
if (selector.parent().find("*[" + nameAttribute + "='" + index + "']").length == 0) {
if (selector.parent().find("*[" + nameAttribute + "='" + index + "']").length === 0) {
return index.toString();
}
index++;
......@@ -30,13 +30,14 @@ container object. Constructs a new RubricOption element.
Args:
element (OpenAssessment.Container): The container that the option is a member of.
notifier (OpenAssessment.Notifier): Used to send notifications of updates to rubric options.
Returns:
OpenAssessment.RubricOption
**/
OpenAssessment.RubricOption = function(element) {
OpenAssessment.RubricOption = function(element, notifier) {
this.element = element;
this.modificationHandler = new OpenAssessment.RubricEventHandler();
this.notifier = notifier;
$(this.element).focusout($.proxy(this.updateHandler, this));
};
......@@ -78,7 +79,7 @@ OpenAssessment.RubricOption.prototype = {
},
/**
Hook into the event handler for removal of a criterion option.
Hook into the event handler for addition of a criterion option.
*/
addHandler: function (){
......@@ -97,7 +98,7 @@ OpenAssessment.RubricOption.prototype = {
$(".openassessment_criterion_option_name", this.element).attr("value", name);
var fields = this.getFieldValues();
this.modificationHandler.notificationFired(
this.notifier.notificationFired(
"optionAdd",
{
"criterionName": criterionName,
......@@ -116,7 +117,7 @@ OpenAssessment.RubricOption.prototype = {
removeHandler: function (){
var criterionName = $(this.element).data('criterion');
var optionName = $(this.element).data('option');
this.modificationHandler.notificationFired(
this.notifier.notificationFired(
"optionRemove",
{
"criterionName": criterionName,
......@@ -136,7 +137,7 @@ OpenAssessment.RubricOption.prototype = {
var optionName = $(this.element).data('option');
var optionLabel = fields.label;
var optionPoints = fields.points;
this.modificationHandler.notificationFired(
this.notifier.notificationFired(
"optionUpdated",
{
"criterionName": criterionName,
......@@ -154,20 +155,22 @@ the DOM.
Args:
element (JQuery Object): The selection which describes the scope of the criterion.
notifier (OpenAssessment.Notifier): Used to send notifications of updates to rubric criteria.
Returns:
OpenAssessment.RubricCriterion
**/
OpenAssessment.RubricCriterion = function(element) {
OpenAssessment.RubricCriterion = function(element, notifier) {
this.element = element;
this.modificationHandler = new OpenAssessment.RubricEventHandler();
this.notifier = notifier;
this.optionContainer = new OpenAssessment.Container(
OpenAssessment.RubricOption, {
containerElement: $(".openassessment_criterion_option_list", this.element).get(0),
templateElement: $("#openassessment_option_template").get(0),
addButtonElement: $(".openassessment_criterion_add_option", this.element).get(0),
removeButtonClass: "openassessment_criterion_option_remove_button",
containerItemClass: "openassessment_criterion_option"
containerItemClass: "openassessment_criterion_option",
notifier: this.notifier
}
);
......@@ -247,7 +250,7 @@ OpenAssessment.RubricCriterion.prototype = {
*/
removeHandler: function(){
var criterionName = $(this.element).data('criterion');
this.modificationHandler.notificationFired("criterionRemove", {'criterionName': criterionName});
this.notifier.notificationFired("criterionRemove", {'criterionName': criterionName});
},
/**
......@@ -258,7 +261,7 @@ OpenAssessment.RubricCriterion.prototype = {
var fields = this.getFieldValues();
var criterionName = fields.name;
var criterionLabel = fields.label;
this.modificationHandler.notificationFired(
this.notifier.notificationFired(
"criterionUpdated",
{'criterionName': criterionName, 'criterionLabel': criterionLabel}
);
......@@ -270,10 +273,11 @@ OpenAssessment.RubricCriterion.prototype = {
The TrainingExample class is used to construct and retrieve information from its element within the DOM
Args:
element (JQuery Object): the selection which identifies the scope of the training example.
element (JQuery Object): the selection which identifies the scope of the training example.
Returns:
OpenAssessment.TrainingExample
OpenAssessment.TrainingExample
**/
OpenAssessment.TrainingExample = function(element){
this.element = element;
......@@ -304,5 +308,4 @@ OpenAssessment.TrainingExample.prototype = {
addHandler: function() {},
removeHandler: function() {},
updateHandler: function() {}
};
\ No newline at end of file
......@@ -52,7 +52,10 @@ OpenAssessment.StudioView = function(runtime, element, server) {
// Initialize the rubric tab view
this.rubricView = new OpenAssessment.EditRubricView(
$("#oa_rubric_editor_wrapper", this.element).get(0)
$("#oa_rubric_editor_wrapper", this.element).get(0),
new OpenAssessment.Notifier([
new OpenAssessment.StudentTrainingListener()
])
);
// Install the save and cancel buttons
......
......@@ -280,7 +280,7 @@ OpenAssessment.EditStudentTrainingView = function(element) {
removeButtonClass: "openassessment_training_example_remove",
containerItemClass: "openassessment_training_example"
}
)
);
};
OpenAssessment.EditStudentTrainingView.prototype = {
......@@ -333,21 +333,6 @@ OpenAssessment.EditStudentTrainingView.prototype = {
},
/**
Get or set the XML defining the training examples.
Args:
xml (string, optional): The XML of the training example definitions.
Returns:
string
**/
exampleDefinitions: function(xml) {
var sel = $("#student_training_examples", this.element);
return OpenAssessment.Fields.stringField(sel, xml);
},
/**
Gets the ID of the assessment
Returns:
......
/**
Notify multiple listeners that an event has occurred.
A listener is any object that implements a notification method.
For example, a listener for the notification "foo" might look like:
>>> var fooListener = {
>>> foo: function(data) {};
>>> };
Since `fooListener` implements `foo`, it will be notified when
a "foo" notification fires.
All notification methods must take a single argument, "data",
which is contains arbitrary information associated with the notification.
If a notification is fired that the listener does not respond to,
the listener will ignore the notification.
Args:
listeners (array): List of objects
**/
OpenAssessment.Notifier = function(listeners) {
this.listeners = listeners;
};
OpenAssessment.Notifier.prototype = {
/**
Fire a notification, which will be received
Args:
name (string): The name of the notification. This should
be the same as the name of the method implemented
by the listeners.
data (object literal): Arbitrary data to include with the notification.
**/
notificationFired: function(name, data) {
for (var i = 0; i < this.listeners.length; i++) {
if (typeof(this.listeners[i][name]) === 'function') {
this.listeners[i][name](data);
}
}
}
};
\ No newline at end of file
/**
Interface for editing rubric definitions.
Args:
element (DOM element): The DOM element representing the rubric.
notifier (OpenAssessment.Notifier): Used to notify other views about updates to the rubric.
This view fires the following notification events:
* optionAdd: An option was added to the rubric.
* optionRemove: An option was removed from the rubric.
* optionUpdated: An option's label and/or points were updated in the rubric.
* criterionRemove: A criterion was removed from the rubric.
* criterionUpdated: A criterion's label was updated in the rubric.
**/
OpenAssessment.EditRubricView = function(element) {
OpenAssessment.EditRubricView = function(element, notifier) {
this.element = element;
this.criteriaContainer = new OpenAssessment.Container(
OpenAssessment.RubricCriterion, {
containerElement: $("#openassessment_criterion_list", this.element).get(0),
templateElement: $("#openassessment_criterion_template", this.element).get(0),
addButtonElement: $("#openassessment_rubric_add_criterion", this.element).get(0),
removeButtonClass: "openassessment_criterion_remove_button",
containerItemClass: "openassessment_criterion"
containerItemClass: "openassessment_criterion",
notifier: notifier
}
);
this.alert = new OpenAssessment.ValidationAlert($('#openassessment_rubric_validation_alert', this.element));
......@@ -78,16 +92,6 @@ OpenAssessment.EditRubricView.prototype = {
},
/**
Remove all criteria in this rubric.
Mainly useful for testing.
**/
removeAllCriteria: function() {
var items = this.criteriaContainer.getAllItems();
var view = this;
$.each(items, function() { view.criteriaContainer.remove(this); });
},
/**
Add a new criterion to the rubric.
Uses a client-side template to create the new criterion.
**/
......@@ -96,64 +100,94 @@ OpenAssessment.EditRubricView.prototype = {
},
/**
Retrieve a criterion item (a container item) from the rubric
at a particular index.
Remove a criterion from the rubric.
Args:
item (OpenAssessment.RubricCriterion): The criterion item to remove.
**/
removeCriterion: function(item) {
this.criteriaContainer.remove(item);
},
/**
Retrieve all criteria from the rubric.
Returns:
Array of OpenAssessment.RubricCriterion objects.
**/
getAllCriteria: function() {
return this.criteriaContainer.getAllItems();
},
/**
Retrieve a criterion item from the rubric.
Args:
index (int): The index of the criterion, starting from 0.
Returns:
OpenAssessment.RubricCriterion
OpenAssessment.RubricCriterion or null
**/
getCriterionItem: function(index) {
return this.criteriaContainer.getItem(index);
}
};
/**
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.
/**
Add a new option to the rubric.
Returns:
Openassessment.ValidationAlert
*/
OpenAssessment.ValidationAlert = function (element) {
this.element = element;
this.title = $(".openassessment_alert_title", this.element);
this.message = $(".openassessment_alert_message", this.element);
};
Args:
criterionIndex (int): The index of the criterion to which
the option will be added (starts from 0).
OpenAssessment.ValidationAlert.prototype = {
**/
addOption: function(criterionIndex) {
var criterionItem = this.getCriterionItem(criterionIndex);
criterionItem.optionContainer.add();
},
/**
Hides the alert.
*/
hide: function () {
this.element.addClass('is--hidden');
Remove an option from the rubric.
Args:
criterionIndex (int): The index of the criterion, starting from 0.
item (OpenAssessment.RubricOption): The option item to remove.
**/
removeOption: function(criterionIndex, item) {
var criterionItem = this.getCriterionItem(criterionIndex);
criterionItem.optionContainer.remove(item);
},
/**
Displays the alert.
*/
show : function () {
this.element.removeClass('is--hidden');
Retrieve all options for a particular criterion.
Args:
criterionIndex (int): The index of the criterion, starting from 0.
Returns:
Array of OpenAssessment.RubricOption
**/
getAllOptions: function(criterionIndex) {
var criterionItem = this.getCriterionItem(criterionIndex);
return criterionItem.optionContainer.getAllItems();
},
/**
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
*/
setMessage: function (newTitle, newMessage){
this.title.text(newTitle);
this.message.text(newMessage);
Retrieve a particular option from the rubric.
Args:
criterionIndex (int): The index of the criterion, starting from 0.
optionIndex (int): The index of the option within the criterion,
starting from 0.
Returns:
OpenAssessment.RubricOption
**/
getOptionItem: function(criterionIndex, optionIndex) {
var criterionItem = this.getCriterionItem(criterionIndex);
return criterionItem.optionContainer.getItem(optionIndex);
}
};
......@@ -3,7 +3,7 @@ Editing interface for OpenAssessment settings (including assessments).
Args:
element (DOM element): The DOM element representing this view.
assessmentViews (array): List of assessment view objects.
assessmentViews (object literal): Mapping of CSS IDs to view objects.
Returns:
OpenAssessment.EditSettingsView
......
/**
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) {
this.element = element;
this.title = $(".openassessment_alert_title", this.element);
this.message = $(".openassessment_alert_message", this.element);
};
OpenAssessment.ValidationAlert.prototype = {
/**
Hides the alert.
*/
hide: function() {
this.element.addClass('is--hidden');
},
/**
Displays the alert.
*/
show : function() {
this.element.removeClass('is--hidden');
},
/**
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
*/
setMessage: function(newTitle, newMessage) {
this.title.text(newTitle);
this.message.text(newMessage);
},
/**
Check whether the alert is currently visible.
Returns:
boolean
**/
isVisible: function() {
return !this.element.hasClass('is--hidden');
},
/**
Retrieve the title of the alert.
Returns:
string
**/
getTitle: function() {
return this.title.text();
},
/**
Retrieve the message of the alert.
Returns:
string
**/
getMessage: function() {
return this.message.text();
}
};
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