Commit fa6df96d by Stephen Sanchez

Wiring in validation JS

parent 32f0c4ff
{% load i18n %} {% load i18n %}
{% spaceless %} {% spaceless %}
<li class="openassessment_criterion is-collapsible"> <li class="openassessment_criterion is-collapsible" data-criterion="{{ criterion_name }}">
<div class="openassessment_criterion_header view-outline"> <div class="openassessment_criterion_header view-outline">
<a class="action expand-collapse collapse"><i class="icon-caret-down ui-toggle-expansion"></i></a> <a class="action expand-collapse collapse"><i class="icon-caret-down ui-toggle-expansion"></i></a>
<h6 class="openassessment_criterion_header_title">{% trans "Criterion" %}</h6> <h6 class="openassessment_criterion_header_title">{% trans "Criterion" %}</h6>
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
</ul> </ul>
<ul class="openassessment_criterion_option_list"> <ul class="openassessment_criterion_option_list">
{% for option in criterion_options %} {% for option in criterion_options %}
{% include "openassessmentblock/edit/oa_edit_option.html" with option_name=option.name option_label=option.label option_points=option.points option_explanation=option.explanation %} {% include "openassessmentblock/edit/oa_edit_option.html" with criterion_name=criterion_name option_name=option.name option_label=option.label option_points=option.points option_explanation=option.explanation %}
{% endfor %} {% endfor %}
</ul> </ul>
......
{% load i18n %} {% load i18n %}
{% spaceless %} {% spaceless %}
<li class="openassessment_criterion_option"> <li class="openassessment_criterion_option" data-criterion="{{ criterion_name }}" data-option="{{ option_name }}">
<div class="openassessment_option_header"> <div class="openassessment_option_header">
<div class="openassessment_option_header_title">{% trans "Option" %}</div> <div class="openassessment_option_header_title">{% trans "Option" %}</div>
<div class="openassessment_criterion_option_remove_button"> <div class="openassessment_criterion_option_remove_button">
......
...@@ -30,6 +30,9 @@ ...@@ -30,6 +30,9 @@
<ol id="openassessment_training_example_template" class="is--hidden"> <ol id="openassessment_training_example_template" class="is--hidden">
{% include "openassessmentblock/edit/oa_training_example.html" with example=assessments.training.template %} {% include "openassessmentblock/edit/oa_training_example.html" with example=assessments.training.template %}
</ol> </ol>
<ol id="openassessment_training_example_criterion_template" class="is--hidden">
{% include "openassessmentblock/edit/oa_training_example_criterion.html" %}
</ol>
</div> </div>
</li> </li>
......
...@@ -19,30 +19,14 @@ ...@@ -19,30 +19,14 @@
<h2>{% trans "Scored Rubric" %}</h2> <h2>{% trans "Scored Rubric" %}</h2>
<ol class="openassessment_training_example_criteria_selections list-input settings-list"> <ol class="openassessment_training_example_criteria_selections list-input settings-list">
{% for criterion in example.criteria %} {% for criterion in example.criteria %}
<li class="field comp-setting-entry openassessment_training_example_criterion" data-criterion={{criterion.name}}> {% include "openassessmentblock/edit/oa_training_example_criterion.html" with criterion=criterion %}
<div class="wrapper-comp-setting">
<label class="openassessment_training_example_criterion_name setting-label">
{{criterion.label}}
<select class="openassessment_training_example_criterion_option setting-input" data-criterion={{criterion.name}}>
<option value="Not Scored">{% trans "Not Scored" %}</option>
{% for option in criterion.options %}
<option value={{option.name}}
{% if criterion.option_selected == option.name %} selected {% endif %}
>
{{option.label}} - {{option.points}} {% trans "points" %}
</option>
{% endfor %}
</select>
</label>
</div>
</li>
{% endfor %} {% endfor %}
</ol> </ol>
</div> </div>
<div class="openassessment_training_example_essay_wrapper"> <div class="openassessment_training_example_essay_wrapper">
<h2>{% trans "Response" %}</h2> <h2>{% trans "Response" %}</h2>
<textarea class="openassessment_training_example_essay">{{example.answer}}</textarea> <textarea class="openassessment_training_example_essay">{{ example.answer }}</textarea>
</div> </div>
</div> </div>
</li> </li>
......
{% load i18n %}
{% spaceless %}
<li class="field comp-setting-entry openassessment_training_example_criterion" data-criterion="{{ criterion.name }}">
<div class="wrapper-comp-setting">
<label class="openassessment_training_example_criterion_name setting-label">
<div class="openassessment_training_example_criterion_name_wrapper">
{{ criterion.label }}
</div>
<select class="openassessment_training_example_criterion_option setting-input" data-criterion="{{ criterion.name }}" data-option="{{ option.name }}">
<option value="">{% trans "Not Scored" %}</option>
{% for option in criterion.options %}
<option value={{ option.name }}
{% if criterion.option_selected == option.name %} selected {% endif %}
>
{{ option.label }} - {{ option.points }} {% trans "points" %}
</option>
{% endfor %}
</select>
</label>
</div>
</li>
{% endspaceless %}
\ No newline at end of file
...@@ -100,7 +100,7 @@ EDITOR_UPDATE_SCHEMA = Schema({ ...@@ -100,7 +100,7 @@ EDITOR_UPDATE_SCHEMA = Schema({
Required('criteria'): [ Required('criteria'): [
Schema({ Schema({
Required('order_num'): All(int, Range(min=0)), Required('order_num'): All(int, Range(min=0)),
'name': utf8_validator, Required('name'): utf8_validator,
Required('label'): utf8_validator, Required('label'): utf8_validator,
Required('prompt'): utf8_validator, Required('prompt'): utf8_validator,
Required('feedback'): All( Required('feedback'): All(
...@@ -114,7 +114,7 @@ EDITOR_UPDATE_SCHEMA = Schema({ ...@@ -114,7 +114,7 @@ EDITOR_UPDATE_SCHEMA = Schema({
Required('options'): [ Required('options'): [
Schema({ Schema({
Required('order_num'): All(int, Range(min=0)), Required('order_num'): All(int, Range(min=0)),
'name': utf8_validator, Required('name'): utf8_validator,
Required('label'): utf8_validator, Required('label'): utf8_validator,
Required('explanation'): utf8_validator, Required('explanation'): utf8_validator,
Required('points'): All(int, Range(min=0)), Required('points'): All(int, Range(min=0)),
......
...@@ -18,6 +18,10 @@ describe("OpenAssessment.Container", function () { ...@@ -18,6 +18,10 @@ describe("OpenAssessment.Container", function () {
var testIdNum = parseInt($(element).attr("test_id"), 10); var testIdNum = parseInt($(element).attr("test_id"), 10);
return { id: testIdNum }; return { id: testIdNum };
}; };
this.addHandler = function() {};
this.removeHandler = function() {};
this.updateHandler = function() {};
}; };
var container = null; var container = null;
......
...@@ -90,23 +90,26 @@ describe("OpenAssessment.EditRubricView", function() { ...@@ -90,23 +90,26 @@ describe("OpenAssessment.EditRubricView", function() {
expect(criteria[0]).toEqual({ expect(criteria[0]).toEqual({
order_num: 0, order_num: 0,
name: "0",
label: "", label: "",
prompt: "", prompt: "",
feedback: "disabled", feedback: "disabled",
options: [], options: []
}); });
expect(criteria[1]).toEqual({ expect(criteria[1]).toEqual({
name: "1",
order_num: 1, order_num: 1,
label: "", label: "",
prompt: "", prompt: "",
feedback: "disabled", feedback: "disabled",
options: [ options: [
{ {
order_num: 0,
label: "", label: "",
points: 1, points: 1,
explanation: "" explanation: "",
name: "0",
order_num: 0
} }
] ]
}); });
......
...@@ -81,10 +81,10 @@ OpenAssessment.Container = function(containerItem, kwargs) { ...@@ -81,10 +81,10 @@ OpenAssessment.Container = function(containerItem, kwargs) {
// Initialize existing items, in case they need to install their // Initialize existing items, in case they need to install their
// own event handlers. // own event handlers.
$("." + this.containerItemClass, this.containerElement).each( $("." + this.containerItemClass, this.containerElement).each(
function(index, element) { new container.containerItem(element); } function(index, element) {
new container.containerItem(element);
}
); );
this.modificationHandler = new OpenAssessment.RubricValidationEventHandler('#oa_student_training_editor');
}; };
OpenAssessment.Container.prototype = { OpenAssessment.Container.prototype = {
...@@ -117,7 +117,9 @@ OpenAssessment.Container.prototype = { ...@@ -117,7 +117,9 @@ OpenAssessment.Container.prototype = {
} ); } );
// Initialize the item, allowing it to install event handlers. // Initialize the item, allowing it to install event handlers.
new this.containerItem(containerItem.get(0)); // Fire event handler for adding a new element
var handlerItem = new this.containerItem(containerItem);
handlerItem.addHandler();
}, },
/** /**
...@@ -130,8 +132,10 @@ OpenAssessment.Container.prototype = { ...@@ -130,8 +132,10 @@ OpenAssessment.Container.prototype = {
**/ **/
remove: function(item) { remove: function(item) {
$(item.element).closest("." + this.containerItemClass).remove(); var itemElement = $(item.element).closest("." + this.containerItemClass);
this.containerItem.removeHandler(this.modificationHandler); var containerItem = new this.containerItem(itemElement);
containerItem.removeHandler();
itemElement.remove();
}, },
/** /**
...@@ -183,5 +187,5 @@ OpenAssessment.Container.prototype = { ...@@ -183,5 +187,5 @@ OpenAssessment.Container.prototype = {
var container = this; var container = this;
return $("." + this.containerItemClass, this.containerElement) return $("." + this.containerItemClass, this.containerElement)
.map(function() { return new container.containerItem(this); }); .map(function() { return new container.containerItem(this); });
}, }
}; };
OpenAssessment.ItemUtilities = {
/**
Utility method for creating a unique name given a set of
options.
Args:
selector (JQuery selector): Selector used to find the relative attribute
for the name.
nameAttribute (str): The name of the attribute that stores the unique
names for a particular set.
Returns:
A unique name for an object in the collection.
*/
createUniqueName: function(selector, nameAttribute) {
var index = 0;
while (index <= selector.length) {
if (selector.parent().find("*[" + nameAttribute + "='" + index + "']").length == 0) {
return index.toString();
}
index++;
}
return null;
}
};
/** /**
The RubricOption Class used to construct and maintain references to rubric options from within an options The RubricOption Class used to construct and maintain references to rubric options from within an options
container object. Constructs a new RubricOption element. container object. Constructs a new RubricOption element.
...@@ -10,6 +36,8 @@ Returns: ...@@ -10,6 +36,8 @@ Returns:
**/ **/
OpenAssessment.RubricOption = function(element) { OpenAssessment.RubricOption = function(element) {
this.element = element; this.element = element;
this.modificationHandler = new OpenAssessment.RubricEventHandler();
$(this.element).focusout($.proxy(this.updateHandler, this));
}; };
OpenAssessment.RubricOption.prototype = { OpenAssessment.RubricOption.prototype = {
...@@ -49,10 +77,74 @@ OpenAssessment.RubricOption.prototype = { ...@@ -49,10 +77,74 @@ OpenAssessment.RubricOption.prototype = {
return fields; return fields;
}, },
removeHandler: function (rubricValidationHandler){ /**
Hook into the event handler for removal of a criterion option.
*/
addHandler: function (){
var criterionElement = $(this.element).closest(".openassessment_criterion");
var criterionName = $(criterionElement).data('criterion');
var criterionLabel = $(".openassessment_criterion_label", criterionElement).val();
var options = $(".openassessment_criterion_option", this.element.parent());
// Create the unique name for this option.
var name = OpenAssessment.ItemUtilities.createUniqueName(options, "data-option");
// Set the criterion name and option name in the new rubric element.
$(this.element)
.attr("data-criterion", criterionName)
.attr("data-option", name);
$(".openassessment_criterion_option_name", this.element).attr("value", name);
var fields = this.getFieldValues();
this.modificationHandler.notificationFired(
"optionAdd",
{
"criterionName": criterionName,
"criterionLabel": criterionLabel,
"name": name,
"label": fields.label,
"points": fields.points
}
);
},
/**
Hook into the event handler for removal of a criterion option.
*/
removeHandler: function (){
var criterionName = $(this.element).data('criterion'); var criterionName = $(this.element).data('criterion');
var optionName = $(this.element).data('option'); var optionName = $(this.element).data('option');
rubricValidationHandler.optionRemove(criterionName, optionName); this.modificationHandler.notificationFired(
"optionRemove",
{
"criterionName": criterionName,
"name": optionName
}
);
},
/**
Hook into the event handler when a rubric criterion option is
modified.
*/
updateHandler: function(){
var fields = this.getFieldValues();
var criterionName = $(this.element).data('criterion');
var optionName = $(this.element).data('option');
var optionLabel = fields.label;
var optionPoints = fields.points;
this.modificationHandler.notificationFired(
"optionUpdated",
{
"criterionName": criterionName,
"name": optionName,
"label": optionLabel,
"points": optionPoints
}
);
} }
}; };
...@@ -68,15 +160,18 @@ Returns: ...@@ -68,15 +160,18 @@ Returns:
**/ **/
OpenAssessment.RubricCriterion = function(element) { OpenAssessment.RubricCriterion = function(element) {
this.element = element; this.element = element;
this.modificationHandler = new OpenAssessment.RubricEventHandler();
this.optionContainer = new OpenAssessment.Container( this.optionContainer = new OpenAssessment.Container(
OpenAssessment.RubricOption, { OpenAssessment.RubricOption, {
containerElement: $(".openassessment_criterion_option_list", this.element).get(0), containerElement: $(".openassessment_criterion_option_list", this.element).get(0),
templateElement: $("#openassessment_option_template").get(0), templateElement: $("#openassessment_option_template").get(0),
addButtonElement: $(".openassessment_criterion_add_option", this.element).get(0), addButtonElement: $(".openassessment_criterion_add_option", this.element).get(0),
removeButtonClass: "openassessment_criterion_option_remove_button", removeButtonClass: "openassessment_criterion_option_remove_button",
containerItemClass: "openassessment_criterion_option", containerItemClass: "openassessment_criterion_option"
} }
); );
$(this.element).focusout($.proxy(this.updateHandler, this));
}; };
...@@ -131,6 +226,42 @@ OpenAssessment.RubricCriterion.prototype = { ...@@ -131,6 +226,42 @@ OpenAssessment.RubricCriterion.prototype = {
**/ **/
addOption: function() { addOption: function() {
this.optionContainer.add(); this.optionContainer.add();
},
/**
Hook into the event handler for addition of a criterion.
*/
addHandler: function (){
var criteria = $(".openassessment_criterion", this.element.parent());
// Create the unique name for this option.
var name = OpenAssessment.ItemUtilities.createUniqueName(criteria, "data-criterion");
// Set the criterion name in the new rubric element.
$(this.element).attr("data-criterion", name);
$(".openassessment_criterion_name", this.element).attr("value", name);
},
/**
Hook into the event handler for removal of a criterion.
*/
removeHandler: function(){
var criterionName = $(this.element).data('criterion');
this.modificationHandler.notificationFired("criterionRemove", {'criterionName': criterionName});
},
/**
Hook into the event handler when a rubric criterion is modified.
*/
updateHandler: function(){
var fields = this.getFieldValues();
var criterionName = fields.name;
var criterionLabel = fields.label;
this.modificationHandler.notificationFired(
"criterionUpdated",
{'criterionName': criterionName, 'criterionLabel': criterionLabel}
);
} }
}; };
...@@ -168,6 +299,10 @@ OpenAssessment.TrainingExample.prototype = { ...@@ -168,6 +299,10 @@ OpenAssessment.TrainingExample.prototype = {
answer: $('.openassessment_training_example_essay', this.element).first().prop('value'), answer: $('.openassessment_training_example_essay', this.element).first().prop('value'),
options_selected: optionsSelected options_selected: optionsSelected
}; };
} },
addHandler: function() {},
removeHandler: function() {},
updateHandler: function() {}
}; };
\ No newline at end of file
...@@ -9,7 +9,7 @@ OpenAssessment.EditRubricView = function(element) { ...@@ -9,7 +9,7 @@ OpenAssessment.EditRubricView = function(element) {
templateElement: $("#openassessment_criterion_template", this.element).get(0), templateElement: $("#openassessment_criterion_template", this.element).get(0),
addButtonElement: $("#openassessment_rubric_add_criterion", this.element).get(0), addButtonElement: $("#openassessment_rubric_add_criterion", this.element).get(0),
removeButtonClass: "openassessment_criterion_remove_button", removeButtonClass: "openassessment_criterion_remove_button",
containerItemClass: "openassessment_criterion", containerItemClass: "openassessment_criterion"
} }
); );
this.alert = new OpenAssessment.ValidationAlert($('#openassessment_rubric_validation_alert', this.element)); this.alert = new OpenAssessment.ValidationAlert($('#openassessment_rubric_validation_alert', this.element));
...@@ -123,7 +123,6 @@ Returns: ...@@ -123,7 +123,6 @@ Returns:
*/ */
OpenAssessment.ValidationAlert = function (element) { OpenAssessment.ValidationAlert = function (element) {
this.element = element; this.element = element;
this.visible = false;
this.title = $(".openassessment_alert_title", this.element); this.title = $(".openassessment_alert_title", this.element);
this.message = $(".openassessment_alert_message", this.element); this.message = $(".openassessment_alert_message", this.element);
}; };
...@@ -131,29 +130,27 @@ OpenAssessment.ValidationAlert = function (element) { ...@@ -131,29 +130,27 @@ OpenAssessment.ValidationAlert = function (element) {
OpenAssessment.ValidationAlert.prototype = { OpenAssessment.ValidationAlert.prototype = {
/** /**
Hides the alert. Hides the alert.
**/ */
hide: function () { hide: function () {
this.visible = false;
this.element.addClass('is--hidden'); this.element.addClass('is--hidden');
}, },
/** /**
Displays the alert. Displays the alert.
**/ */
show : function () { show : function () {
this.visible = true;
this.element.removeClass('is--hidden'); this.element.removeClass('is--hidden');
}, },
/** /**
Sets the message of the alert. Sets the message of the alert.
How will this work with internationalization? How will this work with internationalization?
Args: Args:
newTitle (str): the new title that the message will have newTitle (str): the new title that the message will have
newMessage (str): the new text that the message's body will contain newMessage (str): the new text that the message's body will contain
**/ */
setMessage: function (newTitle, newMessage){ setMessage: function (newTitle, newMessage){
this.title.text(newTitle); this.title.text(newTitle);
this.message.text(newMessage); this.message.text(newMessage);
......
OpenAssessment.RubricEventHandler = function () {
this.listeners = [
new OpenAssessment.StudentTrainingListener()
];
};
OpenAssessment.RubricEventHandler.prototype = {
notificationFired: function(name, data) {
for (var i = 0; i < this.listeners.length; i++) {
if (typeof(this.listeners[i][name]) === 'function') {
this.listeners[i][name](data);
}
}
}
};
OpenAssessment.StudentTrainingListener = function () {
this.element = $('#oa_student_training_editor');
this.alert = new OpenAssessment.ValidationAlert($('#openassessment_rubric_validation_alert'));
};
OpenAssessment.StudentTrainingListener.prototype = {
generateOptionString: function(name, points){
return name + ' - ' + points + gettext(' points')
},
/**
Event handler for updating training examples when a criterion option has
been updated.
Args:
criterionName (str): The name of the criterion that contains the updated
option.
name (str): The name of the option.
label (str): The label for the option.
points (int): The point value for the option.
*/
optionUpdated: function(data){
var view = this;
$('.openassessment_training_example_criterion[data-criterion="'
+ data.criterionName + '"', this.element).each(function(){
var criterion = this;
var option = $("option[value='" + data.name + "'", criterion);
$(option).text(view.generateOptionString(data.label, data.points));
});
},
/**
Update the available options for a particular criterion. If this is the
first option for a criterion, the criterion needs to be created for each
example as well.
Since names are unique, and set server side for new criteria and options,
this will use the label for but the name and label attributes in the
HTML. This will be resolved server-side.
Args:
criterionName (str): The name of the criterion that will have an
additional option.
criterionLabel (str): The label for the criterion that will have an
additional option.
name (str): The name of the new option.
label (str): The new option label.
points (int): The point value of the new option.
*/
optionAdd: function(data){
// First, check to see if the criterion exists on the training examples
var options = $('.openassessment_training_example_criterion_option[data-criterion="' + data.criterionName + '"]');
var view = this;
var criterionAdded = false;
var examplesUpdated = false;
if (options.length == 0) {
this.criterionAdd(data);
criterionAdded = true;
}
$('.openassessment_training_example_criterion_option', this.element).each(function(){
if ($(this).data('criterion') === data.criterionName) {
var criterion = this;
// Risky; making an assumption that options will remain simple.
// updates could cause this to get out of sync with templates,
// but this avoids overly complex templating code.
$(criterion).append($("<option></option>")
.attr("value", data.name)
.text(view.generateOptionString(data.label, data.points)));
examplesUpdated = true;
}
});
if (criterionAdded && examplesUpdated) {
this.displayAlertMsg(
gettext("Criterion Addition requires Training Example Updates"),
gettext("Because you added a criterion, student training examples " +
"will have to be updated.")
);
}
},
/**
Event handler for when an option is removed from a criterion on the rubric.
Training examples will be updated accordingly when this occurs, and the
user is notified that these changes have effected other sections of the
configuration.
If this is the last option in the criterion, removeAllOptions should be
invoked.
Args:
criterionName (str): The name of the criterion where the option is
being removed.
name (str): The option being removed.
*/
optionRemove: function(data){
var validator = this;
var changed = false;
$('.openassessment_training_example_criterion_option', this.element).each(function(){
var criterionOption = this;
if ($(criterionOption).data('criterion') === data.criterionName) {
if ($(criterionOption).val() == data.name) {
$(criterionOption).val("");
$(criterionOption).addClass("openassessment_highlighted_field");
changed = true;
}
$("option[value='" + data.name + "'", criterionOption).remove();
// If all options have been removed from the Criterion, remove
// the criterion entirely.
if ($("option", criterionOption).length == 1) {
validator.removeAllOptions(data);
return;
}
}
});
if (changed) {
this.displayAlertMsg(
gettext("Option Deletion Led to Invalidation"),
gettext("Because you deleted an option, some student training " +
"examples had to be reset.")
);
}
},
/**
Event handler for when all options are removed from a criterion. Right now,
the logic is the same as if the criterion was removed. When all options are
removed, training examples should be updated and the user should be
notified.
Args:
criterionName (str): The criterion where all options have been removed.
*/
removeAllOptions: function(data){
var changed = false;
$('.openassessment_training_example_criterion', this.element).each(function(){
var criterion = this;
if ($(criterion).data('criterion') == data.criterionName) {
$(criterion).remove();
changed = true;
}
});
if (changed) {
this.displayAlertMsg(
gettext("Option Deletion Led to Invalidation"),
gettext("The deletion of the last criterion option caused the " +
"criterion to be removed in the student training examples.")
);
}
},
/**
Event handler for when a criterion is removed from the rubric. If a
criterion is removed, we should ensure that the traing examples are updated
to reflect this change.
Args:
criterionName (str): The name of the criterion removed.
*/
criterionRemove: function(data){
var changed = false;
$('.openassessment_training_example_criterion[data-criterion="'
+ data.criterionName + '"', this.element).each(function(){
$(this).remove();
changed = true;
});
if (changed) {
this.displayAlertMsg(
gettext("Criterion Deletion Led to Invalidation"),
gettext("Because you deleted a criterion, there were student " +
"training examples where the criterion had to be removed.")
);
}
},
/**
Sets up the alert window based on a change message.
Args:
title (str): Title of the alert message.
msg (str): Message body for the alert.
*/
displayAlertMsg: function(title, msg){
this.alert.setMessage(title, msg);
this.alert.show();
},
/**
Handler for modifying the criterion label on every example, when the label
has changed in the rubric.
Args:
criterionName (str): Name of the criterion
criterionLabel (str): New label to replace on the training examples.
*/
criterionUpdated: function(data){
$('.openassessment_training_example_criterion[data-criterion="' +
data.criterionName + '"', this.element).each(function(){
$(".openassessment_training_example_criterion_name_wrapper", this).text(data.criterionLabel);
});
},
/**
Event handler used to generate a new criterion on each training example
when a criterion is created, and the first option is added. This should
update all examples as well as the template used to create new training
examples.
Args:
name (str): The name of the criterion being added.
label (str): The label for the new criterion.
*/
criterionAdd: function(data) {
var view = this.element;
var criterion = $("#openassessment_training_example_criterion_template")
.children().first()
.clone()
.removeAttr('id')
.attr('data-criterion', data.name)
.toggleClass('is--hidden', false)
.appendTo(".openassessment_training_example_criteria_selections", view);
criterion.find(".openassessment_training_example_criterion_option").attr('data-criterion', data.name);
criterion.find(".openassessment_training_example_criterion_name_wrapper").text(data.label);
}
};
var generateOptionString = function(name, points){
return gettext(name + ' - ' + points + ' points')
};
OpenAssessment.RubricValidationEventHandler = function () {
this.element = $('#oa_student_training_editor');
this.alert = new OpenAssessment.ValidationAlert($('#openassessment_rubric_validation_alert', this.element));
};
OpenAssessment.RubricValidationEventHandler.prototype = {
optionRefresh: function(criterionName, oldName, newName, newPoints){
$('.openassessment_training_example_criterion', this.element).each(function(){
if ($(this).data('criterion') == criterionName && $(this).val() == oldName) {
$(this).val(newName);
$(this).text(generateOptionString(newName, newPoints));
}
});
},
optionAdd: function(criterionName){
$('.openassessment_training_example_criterion_option', this.element).each(function(){
if ($(this).data('criterion') == criterionName) {
$(this).append(
"<option value=''> </option>"
);
}
});
},
optionRemove: function(criterionName, optionName){
var removed = 0;
$('.openassessment_training_example_criterion_option', this.element).each(function(){
if ($(this).data('criterion') == criterionName && $(this).val() == optionName) {
$(this).val("");
$(this).addClass("openassessment_highlighted_field");
removed++;
}
});
if (removed > 0){
var title = "Option Deletion Led to Invalidation";
var msg = "Because you deleted an option, there were " + removed + " instance(s) of training examples" +
"where the choice had to be reset.";
this.alert.setMessage(title, msg);
}
},
criterionRename: function(criterionName, newValue){
$('.openassessment_training_example_criterion', this.element).each(function(){
if ($(this).data('criterion') == criterionName){
$(".openassessment_training_example_criterion_name_wrapper", this).text(newValue);
}
});
},
criterionAdd: function() {
$(".openassessment_training_example_criterion", this.element).each(function(){
$(this).append(
'<li class="field comp-setting-entry openassessment_training_example_criterion" data-criterion=APPLES>' +
'<div class="wrapper-comp-setting">' +
'<label class="openassessment_training_example_criterion_name setting-label">' +
'<div class="openassessment_training_example_criterion_name_wrapper">' +
'Banannas!' +
'</div>' +
'<select class="openassessment_training_example_criterion_option setting-input" data-criterion=APPLES>' +
'</select>' +
'</label>'+
'</div>'+
'</li>'
);
});
}
};
...@@ -425,6 +425,46 @@ ...@@ -425,6 +425,46 @@
#oa_rubric_editor_wrapper{ #oa_rubric_editor_wrapper{
#openassessment_rubric_validation_alert{
-webkit-animation: notificationSlideDown 1s ease-in-out 1;
-moz-animation: notificationSlideDown 1s ease-in-out 1;
animation: notificationSlideDown 1s ease-in-out 1;
-webkit-animation-fill-mode: forwards;
-moz-animation-fill-mode: forwards;
animation-fill-mode: forwards;
background-color: #323232;
height: auto;
border-bottom: 3px solid rgb(192, 172, 0);
padding: 10px;
position: absolute;
z-index: 10;
.openassessment_alert_icon:before{
font-family: FontAwesome;
content: "\f071";
display: inline-block;
color: rgb(192, 172, 0);
float: left;
font-size: 200%;
margin: 20px 0px 0px 25px;
}
.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;
}
}
}
.wrapper-comp-settings{ .wrapper-comp-settings{
display: initial; display: initial;
} }
...@@ -470,7 +510,7 @@ ...@@ -470,7 +510,7 @@
} }
.openassessment_criterion_header_remove { .openassessment_criterion_header_remove {
@extend .openassessment_rubric_remove_button @extend .openassessment_rubric_remove_button;
} }
} }
...@@ -587,7 +627,7 @@ ...@@ -587,7 +627,7 @@
} }
.openassessment_option_header_remove{ .openassessment_option_header_remove{
@extend .openassessment_rubric_remove_button @extend .openassessment_rubric_remove_button;
} }
} }
...@@ -711,6 +751,7 @@ ...@@ -711,6 +751,7 @@
#openassessment_rubric_feedback_wrapper{ #openassessment_rubric_feedback_wrapper{
padding: 0; padding: 0;
#openassessment_rubric_feedback_header{ #openassessment_rubric_feedback_header{
background-color: $edx-gray-t1; background-color: $edx-gray-t1;
padding: 7.5px 7.5px 7.5px 15px; padding: 7.5px 7.5px 7.5px 15px;
...@@ -788,6 +829,10 @@ ...@@ -788,6 +829,10 @@
#student_training_settings_editor { #student_training_settings_editor {
.openassessment_highlighted_field{
background-color: $edx-pink-l4;
}
.openassessment_training_example { .openassessment_training_example {
padding: 5px; padding: 5px;
.openassessment_training_example_header { .openassessment_training_example_header {
......
...@@ -4,7 +4,6 @@ Studio editing view for OpenAssessment XBlock. ...@@ -4,7 +4,6 @@ Studio editing view for OpenAssessment XBlock.
import pkg_resources import pkg_resources
import copy import copy
import logging import logging
from uuid import uuid4
from django.template import Context from django.template import Context
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -172,7 +171,7 @@ class StudioMixin(object): ...@@ -172,7 +171,7 @@ class StudioMixin(object):
return {'success': False, 'msg': _( return {'success': False, 'msg': _(
u'Validation error: No examples were provided for example based assessment.' u'Validation error: No examples were provided for example based assessment.'
)} )}
# This is where we default to EASE for problems which are edited in the GUI # This is where we default to EASE for problems which are edited in the GUI
assessment['algorithm_id'] = 'ease' assessment['algorithm_id'] = 'ease'
xblock_validator = validator(self) xblock_validator = validator(self)
......
...@@ -3,18 +3,21 @@ ...@@ -3,18 +3,21 @@
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
"name": "0",
"label": "Test criterion", "label": "Test criterion",
"prompt": "Test criterion prompt", "prompt": "Test criterion prompt",
"options": [ "options": [
{ {
"order_num": 0, "order_num": 0,
"points": 0, "points": 0,
"name": "0",
"label": "No", "label": "No",
"explanation": "No explanation" "explanation": "No explanation"
}, },
{ {
"order_num": 1, "order_num": 1,
"points": 2, "points": 2,
"name": "1",
"label": "Yes", "label": "Yes",
"explanation": "Yes explanation" "explanation": "Yes explanation"
} }
...@@ -49,18 +52,21 @@ ...@@ -49,18 +52,21 @@
"criteria": [ "criteria": [
{ {
"order_num": 0, "order_num": 0,
"name": "0",
"label": "Ṫëṡẗ ċṛïẗëïṛöṅ", "label": "Ṫëṡẗ ċṛïẗëïṛöṅ",
"prompt": "Téśt ćŕítéíŕőń ṕŕőḿṕt", "prompt": "Téśt ćŕítéíŕőń ṕŕőḿṕt",
"options": [ "options": [
{ {
"order_num": 0, "order_num": 0,
"points": 0, "points": 0,
"name": "0",
"label": "Ṅö", "label": "Ṅö",
"explanation": "Ńő éxṕĺáńátíőń" "explanation": "Ńő éxṕĺáńátíőń"
}, },
{ {
"order_num": 1, "order_num": 1,
"points": 2, "points": 2,
"name": "1",
"label": "sǝʎ", "label": "sǝʎ",
"explanation": "Чэѕ эхрlаиатіои" "explanation": "Чэѕ эхрlаиатіои"
} }
......
...@@ -48,9 +48,10 @@ class StudioViewTest(XBlockHandlerTestCase): ...@@ -48,9 +48,10 @@ class StudioViewTest(XBlockHandlerTestCase):
] ]
} }
RUBRIC_CRITERIA_WITH_AND_WITHOUT_NAMES = [ RUBRIC_CRITERIA = [
{ {
"order_num": 0, "order_num": 0,
"name": "0",
"label": "Test criterion with no name", "label": "Test criterion with no name",
"prompt": "Test criterion prompt", "prompt": "Test criterion prompt",
"feedback": "disabled", "feedback": "disabled",
...@@ -58,6 +59,7 @@ class StudioViewTest(XBlockHandlerTestCase): ...@@ -58,6 +59,7 @@ class StudioViewTest(XBlockHandlerTestCase):
{ {
"order_num": 0, "order_num": 0,
"points": 0, "points": 0,
"name": "0",
"label": "Test option with no name", "label": "Test option with no name",
"explanation": "Test explanation" "explanation": "Test explanation"
} }
...@@ -66,13 +68,14 @@ class StudioViewTest(XBlockHandlerTestCase): ...@@ -66,13 +68,14 @@ class StudioViewTest(XBlockHandlerTestCase):
{ {
"order_num": 1, "order_num": 1,
"label": "Test criterion that already has a name", "label": "Test criterion that already has a name",
"name": "cd316c145cb14e06b377db65719ed41c", "name": "1",
"prompt": "Test criterion prompt", "prompt": "Test criterion prompt",
"feedback": "disabled", "feedback": "disabled",
"options": [ "options": [
{ {
"order_num": 0, "order_num": 0,
"points": 0, "points": 0,
"name": "0",
"label": "Test option with no name", "label": "Test option with no name",
"explanation": "Test explanation" "explanation": "Test explanation"
}, },
...@@ -80,7 +83,7 @@ class StudioViewTest(XBlockHandlerTestCase): ...@@ -80,7 +83,7 @@ class StudioViewTest(XBlockHandlerTestCase):
"order_num": 1, "order_num": 1,
"points": 0, "points": 0,
"label": "Test option that already has a name", "label": "Test option that already has a name",
"name": "8bcdb0769b15482d9b2c3791d22e8ad2", "name": "1",
"explanation": "Test explanation" "explanation": "Test explanation"
}, },
] ]
...@@ -156,30 +159,6 @@ class StudioViewTest(XBlockHandlerTestCase): ...@@ -156,30 +159,6 @@ class StudioViewTest(XBlockHandlerTestCase):
self.assertTrue(resp['success'], msg=resp.get('msg')) self.assertTrue(resp['success'], msg=resp.get('msg'))
self.assertEqual(xblock.editor_assessments_order, data['editor_assessments_order']) self.assertEqual(xblock.editor_assessments_order, data['editor_assessments_order'])
@scenario('data/basic_scenario.xml')
def test_update_editor_context_assign_unique_names(self, xblock):
# Update the XBlock with a rubric that is missing
# some of the (unique) names for rubric criteria/options.
data = copy.deepcopy(self.UPDATE_EDITOR_DATA)
data['criteria'] = self.RUBRIC_CRITERIA_WITH_AND_WITHOUT_NAMES
xblock.published_date = None
resp = self.request(xblock, 'update_editor_context', json.dumps(data), response_format='json')
self.assertTrue(resp['success'], msg=resp.get('msg'))
# Check that the XBlock has assigned unique names for all criteria
criteria_names = set([criterion.get('name') for criterion in xblock.rubric_criteria])
self.assertEqual(len(criteria_names), 2)
self.assertNotIn(None, criteria_names)
# Check that the XBlock has assigned unique names for all options
option_names = set()
for criterion in xblock.rubric_criteria:
for option in criterion['options']:
option_names.add(option.get('name'))
self.assertEqual(len(option_names), 3)
self.assertNotIn(None, option_names)
@file_data('data/invalid_update_xblock.json') @file_data('data/invalid_update_xblock.json')
@scenario('data/basic_scenario.xml') @scenario('data/basic_scenario.xml')
def test_update_context_invalid_request_data(self, xblock, data): def test_update_context_invalid_request_data(self, xblock, data):
......
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