Commit 602d0212 by Tim Krones

WYSIWYG functionality for Studio, Part 2:

- Add support for setting names and labels for vectors.
- Add support for defining expected result.

Add support for setting name and label of a vector.

Add WYSIWYG functionality for defining expected result.
parent 224391aa
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
background-color: #f7f7f7; background-color: #f7f7f7;
} }
.vectordraw_block .menu .vector-properties h3 { .vectordraw_block h3 {
font-size: 16px; font-size: 16px;
margin: 0 0 5px; margin: 0 0 5px;
} }
......
.vectordraw_edit_block,
.vectordraw_edit_block .jxgboard {
display: block;
}
.vectordraw_edit_block { .vectordraw_edit_block {
border-top: 1px solid #e5e5e5; border-top: 1px solid #e5e5e5;
margin-left: 20px; margin-left: 20px;
...@@ -15,12 +20,37 @@ ...@@ -15,12 +20,37 @@
} }
.vectordraw_edit_block .jxgboard { .vectordraw_edit_block .jxgboard {
float: none; float: left;
margin-bottom: 1em; margin-bottom: 1em;
} }
/* Menu */ /* Menu */
.vectordraw_edit_block .menu .controls .result-mode {
float: right;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-name,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-tail,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-angle {
padding-right: 0px;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-select,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-label,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-length {
padding-right: 5px;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row select,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row input {
height: 2em;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row select {
padding: 0px;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-remove { .vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-remove {
vertical-align: bottom; vertical-align: bottom;
} }
...@@ -28,3 +58,44 @@ ...@@ -28,3 +58,44 @@
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-remove button { .vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-remove button {
float: right; float: right;
} }
.vectordraw_edit_block h3 {
margin-top: 5px;
margin-bottom: 5px;
}
.vectordraw_edit_block .checks {
display: none;
width: 220px;
float: right;
border-top: 2px solid #1f628d;
border-right: 2px solid #1f628d;
border-bottom: 2px solid #1f628d;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
padding-left: 10px;
background-color: #f7f7f7;
overflow: auto;
}
.vectordraw_edit_block .checks .check .row {
height: 2em;
}
.vectordraw_edit_block .checks .check span,
.vectordraw_edit_block .checks .check input[type="text"] {
height: 1.5em;
margin-right: 12px;
}
.vectordraw_edit_block .checks .check input[type="checkbox"] {
height: 1.2em;
width: 20%;
min-width: 0px;
margin-top: 2px;
}
.vectordraw_edit_block .checks .check input {
float: right;
vertical-align: middle;
}
...@@ -91,6 +91,10 @@ function StudioEditableXBlockMixin(runtime, element) { ...@@ -91,6 +91,10 @@ function StudioEditableXBlockMixin(runtime, element) {
return { return {
getContents: function(fieldName) {
return _.findWhere(fields, {name: fieldName}).val();
},
save: function(data) { save: function(data) {
var values = {}; var values = {};
var notSet = []; // List of field names that should be set to default values var notSet = []; // List of field names that should be set to default values
...@@ -107,9 +111,12 @@ function StudioEditableXBlockMixin(runtime, element) { ...@@ -107,9 +111,12 @@ function StudioEditableXBlockMixin(runtime, element) {
field.removeEditor(); field.removeEditor();
} }
} }
// If WYSIWYG editor was used, prefer its data over value of "vectors" field: // If WYSIWYG editor was used,
if (data.vectors) { // prefer its data over values of "Vectors" and "Expected result" fields:
if (!_.isEmpty(data)) {
values.vectors = JSON.stringify(data.vectors, undefined, 4); values.vectors = JSON.stringify(data.vectors, undefined, 4);
values.expected_result_positions = data.expected_result_positions;
values.expected_result = JSON.stringify(data.expected_result, undefined, 4);
} }
studio_submit({values: values, defaults: notSet}); studio_submit({values: values, defaults: notSet});
......
...@@ -6,22 +6,42 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -6,22 +6,42 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
this.dragged_vector = null; this.dragged_vector = null;
this.drawMode = false; this.drawMode = false;
this.wasUsed = false; this.wasUsed = false;
this.resultMode = false;
this.settings = settings; this.settings = settings;
this.numberOfVectors = this.settings.vectors.length; this.numberOfVectors = this.settings.vectors.length;
this.element = $('#' + element_id, element); this.element = $('#' + element_id, element);
this.element.on('click', '.add-vector', this.onAddVector.bind(this)); this.element.on('click', '.controls .add-vector', this.onAddVector.bind(this));
this.element.on('click', '.controls .result-mode', this.onEditResult.bind(this));
this.element.on('change', '.menu .element-list-edit', this.onEditStart.bind(this)); this.element.on('change', '.menu .element-list-edit', this.onEditStart.bind(this));
this.element.on('click', '.menu .vector-prop-update', this.onEditSubmit.bind(this)); this.element.on('click', '.menu .update', this.onEditSubmit.bind(this));
this.element.on('click', '.vector-remove', this.onRemoveVector.bind(this)); this.element.on('click', '.menu .remove', this.onRemoveVector.bind(this));
// Prevents default image drag and drop actions in some browsers. // Prevents default image drag and drop actions in some browsers.
this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); }); this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); });
this.discardStaleData();
this.render(); this.render();
}; };
VectorDraw.prototype.discardStaleData = function() {
// If author removed or renamed vectors via the "Vectors" field
// (without making necessary adjustments in "Expected results" field)
// discard stale information about expected positions and checks
var vectorData = JSON.parse(fieldEditor.getContents('vectors')),
vectorNames = _.pluck(vectorData, 'name');
_.each(_.keys(this.settings.expected_result_positions), function(key) {
if (!_.contains(vectorNames, key)) {
delete this.settings.expected_result_positions[key];
}
}, this);
_.each(_.keys(this.settings.expected_result), function(key) {
if (!_.contains(vectorNames, key)) {
delete this.settings.expected_result[key];
}
}, this);
};
VectorDraw.prototype.render = function() { VectorDraw.prototype.render = function() {
$('.vector-prop-slope', this.element).hide();
// Assign the jxgboard element a random unique ID, // Assign the jxgboard element a random unique ID,
// because JXG.JSXGraph.initBoard needs it. // because JXG.JSXGraph.initBoard needs it.
this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard')); this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard'));
...@@ -218,10 +238,56 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -218,10 +238,56 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
var idx = _.indexOf(this.settings.vectors, vectorSettings), var idx = _.indexOf(this.settings.vectors, vectorSettings),
editOption = this.getEditMenuOption("vector", idx); editOption = this.getEditMenuOption("vector", idx);
editOption.remove(); editOption.remove();
// 4. Reset input fields for vector properties to default values // 4. Discard information about expected position
var expectedPositions = this.settings.expected_result_positions;
if (expectedPositions[vectorName]) {
delete expectedPositions[vectorName];
this.settings.expected_result_positions = expectedPositions;
}
// 5. Discard information about expected result
var expectedResults = this.settings.expected_result;
if (expectedResults[vectorName]) {
delete expectedResults[vectorName];
this.settings.expected_result = expectedResults;
}
// 6. Reset input fields for vector properties to default values
this.resetVectorProperties(); this.resetVectorProperties();
}; };
VectorDraw.prototype.onEditResult = function(evt) {
// Switch to result mode
this.resultMode = true;
// Save vector positions
this.settings.vectors = this.getState(); // Discards vectors that were removed from board
// Update vector positions using positions from expected result
var expectedResultPositions = this.settings.expected_result_positions;
if (!_.isEmpty(expectedResultPositions)) {
// Loop over vectors and update their positions based on expected result positions
_.each(this.settings.vectors, function(vec) {
var vectorName = vec.name,
resultPosition = expectedResultPositions[vectorName];
if (resultPosition) {
var resultTail = resultPosition.tail,
resultTip = resultPosition.tip,
boardObject = this.board.elementsByName[vectorName];
boardObject.point1.setPosition(JXG.COORDS_BY_USER, resultTail);
boardObject.point2.setPosition(JXG.COORDS_BY_USER, resultTip);
}
}, this);
this.board.update();
}
// Hide or disable operations that are specific to defining initial state
$(evt.currentTarget).prop('disabled', true);
$('.add-vector', element).css('visibility', 'hidden');
$('.vector-prop-name input', element).prop('disabled', true);
$('.vector-prop-label input', element).prop('disabled', true);
$('.vector-remove button').hide();
// Reset vector properties to ensure a clean slate
this.resetVectorProperties();
// Show controls for opting in and out of checks
$('.checks', element).show();
};
VectorDraw.prototype.resetVectorProperties = function(vector) { VectorDraw.prototype.resetVectorProperties = function(vector) {
// Select default value // Select default value
$('.menu .element-list-edit option[value="-"]', element).attr('selected', true); $('.menu .element-list-edit option[value="-"]', element).attr('selected', true);
...@@ -269,13 +335,14 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -269,13 +335,14 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
if (angle < 0) { if (angle < 0) {
angle += 360; angle += 360;
} }
var slope = (y2-y1)/(x2-x1);
// Update menu for selecting vector to edit // Update menu for selecting vector to edit
this.element.find('.menu .element-list-edit option').attr('selected', false); this.element.find('.menu .element-list-edit option').attr('selected', false);
var idx = _.indexOf(this.settings.vectors, vec_settings), var idx = _.indexOf(this.settings.vectors, vec_settings),
editOption = this.getEditMenuOption("vector", idx); editOption = this.getEditMenuOption("vector", idx);
editOption.attr('selected', true); editOption.attr('selected', true);
// Update properties // Update properties
$('.vector-prop-name input', this.element).val(vector.name);
$('.vector-prop-label input', this.element).val(vec_settings.style.label || '-');
$('.vector-prop-angle input', this.element).val(angle.toFixed(2)); $('.vector-prop-angle input', this.element).val(angle.toFixed(2));
if (vector.elType !== "line") { if (vector.elType !== "line") {
var tailInput = x1.toFixed(2) + ", " + y1.toFixed(2); var tailInput = x1.toFixed(2) + ", " + y1.toFixed(2);
...@@ -286,14 +353,73 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -286,14 +353,73 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
$('.vector-prop-tail input', this.element).val(tailInput); $('.vector-prop-tail input', this.element).val(tailInput);
$('.vector-prop-length', this.element).show(); $('.vector-prop-length', this.element).show();
$('.vector-prop-length input', this.element).val(lengthInput); $('.vector-prop-length input', this.element).val(lengthInput);
$('.vector-prop-slope', this.element).hide();
} }
else { else {
$('.vector-prop-length', this.element).hide(); $('.vector-prop-length', this.element).hide();
if (this.settings.show_slope_for_lines) {
$('.vector-prop-slope', this.element).show();
$('.vector-prop-slope input', this.element).val(slope.toFixed(2));
} }
};
VectorDraw.prototype.updateChecks = function(vector) {
var expectedResult = this.settings.expected_result[vector.name] || {};
var checks = [
'tail', 'tail_x', 'tail_y', 'tip', 'tip_x', 'tip_y', 'coords', 'length', 'angle'
];
_.each(checks, function(check) {
var checkElement = $('#check-' + check, element);
// Update checkbox
if (expectedResult[check]) {
checkElement.find('input[type="checkbox"]').prop('checked', true);
} else {
checkElement.find('input[type="checkbox"]').prop('checked', false);
}
// Update tolerance
var tolerance = expectedResult[check + '_tolerance'];
if (tolerance) {
checkElement.find('input[type="text"]').val(tolerance.toFixed(1));
} else {
var defaultTolerance = check === 'angle' ? 2.0 : 1.0;
checkElement.find('input[type="text"]').val(defaultTolerance.toFixed(1));
}
});
};
VectorDraw.prototype.saveExpectedPosition = function(vectorName, coords, length, angle) {
var expectedPosition = {
coords: coords,
tail: coords[0],
tip: coords[1],
tail_x: coords[0][0],
tail_y: coords[0][1],
tip_x: coords[1][0],
tip_y: coords[1][1],
length: length,
angle: angle
};
this.settings.expected_result_positions[vectorName] = expectedPosition;
};
VectorDraw.prototype.saveChecks = function(vectorName) {
var expectedResult = {};
var checks = [
'tail', 'tail_x', 'tail_y', 'tip', 'tip_x', 'tip_y', 'coords', 'length', 'angle'
];
_.each(checks, function(check) {
var checkElement = $('#check-' + check, element);
if (checkElement.find('input[type="checkbox"]').prop('checked')) {
// Insert (or update) check: Need current position of selected vector
expectedResult[check] = this.settings.expected_result_positions[vectorName][check];
// Insert (or update) tolerance
var tolerance = checkElement.find('input[type="text"]').val();
expectedResult[check + '_tolerance'] = parseFloat(tolerance);
}
}, this);
if (_.isEmpty(expectedResult)) { // If author doesn't select any checks,
// assume they also want to skip the presence check
// (which the grader will perform automatically
// for each vector that has an entry in expected_result)
delete this.settings.expected_result[vectorName];
} else {
this.settings.expected_result[vectorName] = expectedResult;
} }
}; };
...@@ -357,6 +483,9 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -357,6 +483,9 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
var coords = this.getMouseCoords(evt); var coords = this.getMouseCoords(evt);
var targetObjects = this.objectsUnderMouse(coords); var targetObjects = this.objectsUnderMouse(coords);
if (!targetObjects || _.all(targetObjects, this.canCreateVectorOnTopOf.bind(this))) { if (!targetObjects || _.all(targetObjects, this.canCreateVectorOnTopOf.bind(this))) {
if (this.resultMode) {
return;
}
// Add vector to board // Add vector to board
var point_coords = [coords.usrCoords[1], coords.usrCoords[2]]; var point_coords = [coords.usrCoords[1], coords.usrCoords[2]];
var defaultVector = this.getDefaultVector([point_coords, point_coords]); var defaultVector = this.getDefaultVector([point_coords, point_coords]);
...@@ -364,6 +493,7 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -364,6 +493,7 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
var lastIndex = this.numberOfVectors - 1; var lastIndex = this.numberOfVectors - 1;
this.drawMode = true; this.drawMode = true;
this.dragged_vector = this.renderVector(lastIndex); this.dragged_vector = this.renderVector(lastIndex);
this.addEditMenuOption(defaultVector.name, lastIndex);
} }
else { else {
// Move existing vector around // Move existing vector around
...@@ -373,6 +503,9 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -373,6 +503,9 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
this.dragged_vector = this.getVectorForObject(vectorPoint); this.dragged_vector = this.getVectorForObject(vectorPoint);
this.dragged_vector.point1.setProperty({fixed: false}); this.dragged_vector.point1.setProperty({fixed: false});
this.updateVectorProperties(this.dragged_vector); this.updateVectorProperties(this.dragged_vector);
if (this.resultMode) {
this.updateChecks(this.dragged_vector);
}
} }
} }
}; };
...@@ -402,26 +535,66 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -402,26 +535,66 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
var vectorName = $(evt.currentTarget).find('option:selected').data('vector-name'); var vectorName = $(evt.currentTarget).find('option:selected').data('vector-name');
var vectorObject = this.board.elementsByName[vectorName]; var vectorObject = this.board.elementsByName[vectorName];
this.updateVectorProperties(vectorObject); this.updateVectorProperties(vectorObject);
if (this.resultMode) {
this.updateChecks(vectorObject);
}
}; };
VectorDraw.prototype.onEditSubmit = function(evt) { VectorDraw.prototype.onEditSubmit = function(evt) {
if (!this.wasUsed) { if (!this.wasUsed) {
this.wasUsed = true; this.wasUsed = true;
} }
// Get vector that is currently "selected" // Get name of vector that is currently "selected"
var vectorName = $('.element-list-edit', element).find('option:selected').data('vector-name'); var vectorName = "" + $('.element-list-edit', element).find('option:selected').data('vector-name');
// Get values from input fields // Get values from input fields
var newTail = $('.vector-prop-tail input', element).val(), var newName = $('.vector-prop-name input', element).val(),
newLabel = $('.vector-prop-label input', element).val(),
newTail = $('.vector-prop-tail input', element).val(),
newLength = $('.vector-prop-length input', element).val(), newLength = $('.vector-prop-length input', element).val(),
newAngle = $('.vector-prop-angle input', element).val(); newAngle = $('.vector-prop-angle input', element).val();
// Process values // Process values
newName = $.trim(newName);
newLabel = $.trim(newLabel);
newTail = _.map(newTail.split(/ *, */), function(coord) { newTail = _.map(newTail.split(/ *, */), function(coord) {
return parseFloat(coord); return parseFloat(coord);
}); });
newLength = parseFloat(newLength); newLength = parseFloat(newLength);
newAngle = parseFloat(newAngle); newAngle = parseFloat(newAngle);
// Validate and update values
var vectorSettings = this.getVectorSettingsByName(vectorName),
editOption = $('.menu .element-list-edit option[data-vector-name="' + vectorName + '"]', element),
boardObject = this.board.elementsByName[vectorName];
if (newName && newName !== vectorName) {
// Update vector settings
vectorSettings.name = newName;
// Update dropdown for selecting vector to edit
editOption.data('vector-name', newName);
editOption.text(newName);
// Update board
boardObject.name = newName;
boardObject.point2.name = newName;
this.board.elementsByName[newName] = boardObject;
delete this.board.elementsByName[vectorName];
// Update expected position
var expectedPositions = this.settings.expected_result_positions,
expectedPosition = expectedPositions[vectorName];
if (expectedPosition) {
expectedPositions[newName] = expectedPosition;
delete expectedPositions[vectorName];
}
// Update expected result
var expectedResults = this.settings.expected_result,
expectedResult = expectedResults[vectorName];
if (expectedResult) {
expectedResults[newName] = expectedResult;
delete expectedResults[vectorName];
}
}
if (newLabel && newLabel !== '-') {
vectorSettings.style.label = newLabel;
boardObject.point2.name = newLabel; // Always prefer label for labeling vector on board
}
var values = [newTail[0], newTail[1], newLength, newAngle]; var values = [newTail[0], newTail[1], newLength, newAngle];
// Validate values
if (!_.some(values, Number.isNaN)) { if (!_.some(values, Number.isNaN)) {
$('.vector-prop-update .update-error', element).css('visibility', 'hidden'); $('.vector-prop-update .update-error', element).css('visibility', 'hidden');
// Use coordinates of new tail, new length, new angle to calculate new position of tip // Use coordinates of new tail, new length, new angle to calculate new position of tip
...@@ -431,10 +604,17 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -431,10 +604,17 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
newTail[1] + Math.sin(radians) * newLength newTail[1] + Math.sin(radians) * newLength
]; ];
// Update position of vector // Update position of vector
var board_object = this.board.elementsByName[vectorName]; boardObject.point1.setPosition(JXG.COORDS_BY_USER, newTail);
board_object.point1.setPosition(JXG.COORDS_BY_USER, newTail); boardObject.point2.setPosition(JXG.COORDS_BY_USER, newTip);
board_object.point2.setPosition(JXG.COORDS_BY_USER, newTip);
this.board.update(); this.board.update();
// If board is in result mode, also save
// - expected position
// - check data
// for "selected" vector
if (this.resultMode) {
this.saveExpectedPosition(vectorName, [newTail, newTip], newLength, newAngle);
this.saveChecks(vectorName);
}
} else { } else {
$('.vector-prop-update .update-error', element).css('visibility', 'visible'); $('.vector-prop-update .update-error', element).css('visibility', 'visible');
} }
...@@ -466,7 +646,7 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -466,7 +646,7 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
vec.tail = tail; vec.tail = tail;
vec.tip = tip; vec.tip = tip;
// Update length, angle // Update length, angle
vec.length = Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2));; vec.length = Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2));
// Update angle // Update angle
vec.angle = ((Math.atan2(y2-y1, x2-x1)/Math.PI*180)) % 360; vec.angle = ((Math.atan2(y2-y1, x2-x1)/Math.PI*180)) % 360;
vectors.push(vec); vectors.push(vec);
...@@ -485,10 +665,23 @@ function VectorDrawXBlockEdit(runtime, element, init_args) { ...@@ -485,10 +665,23 @@ function VectorDrawXBlockEdit(runtime, element, init_args) {
// Set up click handlers // Set up click handlers
$('.save-button', element).on('click', function(e) { $('.save-button', element).on('click', function(e) {
e.preventDefault(); e.preventDefault();
var data = {}; var data;
if (vectordraw.wasUsed) { if (vectordraw.resultMode) { // Author edited both initial state and result
data = {
vectors: vectordraw.settings.vectors, // Corresponds to state vectors were in
// when author switched to result mode
expected_result_positions: vectordraw.settings.expected_result_positions,
expected_result: vectordraw.settings.expected_result
};
} else if (vectordraw.wasUsed) { // Author edited initial state
var state = vectordraw.getState(); var state = vectordraw.getState();
data = { vectors: state }; data = {
vectors: state,
expected_result_positions: vectordraw.settings.expected_result_positions,
expected_result: vectordraw.settings.expected_result
};
} else { // Author did not use WYSIWYG editor
data = {};
} }
fieldEditor.save(data); fieldEditor.save(data);
}); });
......
...@@ -68,25 +68,42 @@ ...@@ -68,25 +68,42 @@
<p id="wysiwyg-description"> <p id="wysiwyg-description">
{% blocktrans %} {% blocktrans %}
Instead of using the "Vectors" field above to define or modify Instead of using the "Vectors" and "Expected result" fields above
the set of working vectors for this exercise, you can also use the board below. to define or modify the set of working vectors and expected result for this exercise,
you can also use the board below.
To add a vector, left-click the board where you want the vector to originate. To add a vector, left-click the board where you want the vector to originate.
Keep holding down the left mouse button and drag your mouse pointer across the board Keep holding down the left mouse button and drag your mouse pointer across the board
to achieve the desired length and angle for the vector. to achieve the desired length and angle for the vector.
Alternatively, you can click "Create vector", which will add a new vector Alternatively, you can click "Create vector", which will add a new vector
that starts at the center of the board and has a predefined length (3) and angle (90). that starts at the center of the board and has a predefined length (3) and angle (90).
To modify an existing vector, left-click it, hold down the left mouse button, To modify the position of an existing vector, left-click it, hold down the left mouse button,
and move your mouse pointer across the board. and move your mouse pointer across the board. To modify length and/or angle,
Alternatively, you can select an existing vector from the dropdown menu left-click the tip of the vector and drag your mouse pointer across the board.
Alternatively, you can select an existing vector from the dropdown menu,
modify its tail position, length, and angle by changing the values modify its tail position, length, and angle by changing the values
in the corresponding input fields, and click "Update" to update its position on the board. in the corresponding input fields, and click "Update" to update its position on the board.
You can also modify the name and label of a vector using this technique.
To remove an existing vector, left-click it or select it from the dropdown menu, To remove an existing vector, left-click it or select it from the dropdown menu,
then click "Remove". then click "Remove".
Note that if you make changes using the board below, any changes you made via the "Vectors" field above When you are done defining the set of working vectors, click "Edit result"
will be overwritten when you save the settings for this exercise by clicking the "Save" button below. to switch the editor to a mode that will allow you to define the expected result for this exercise.
In this mode you can operate on vectors as described above but you can not add or remove vectors,
and you may not change the name and label of a selected vector.
To define the expected result for the exercise, place each vector where it would be located
in a correct solution. When a vector is selected, use the menu to the right of the board
to select the checks that you would like the grader to perform for this vector,
and click "Update". Note that if you do not select any checks for a given vector,
no checks at all will be performed for it during grading (i.e., the grader will skip a presence check).
Finally, note that if you make changes using the board below, any changes you made
via the "Vectors" and "Expected results" fields above will be overwritten
when you save the settings for this exercise by clicking the "Save" button
at the bottom of this dialog.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
...@@ -94,16 +111,17 @@ ...@@ -94,16 +111,17 @@
<div class="menu" style="width: {{ self.width }}px;"> <div class="menu" style="width: {{ self.width }}px;">
<div class="controls"> <div class="controls">
<button class="add-vector">{% trans "Create vector" %}</button> <button class="add-vector">{% trans "Create vector" %}</button>
<button class="result-mode">{% trans "Edit result" %}</button>
</div> </div>
<div class="vector-properties" aria-live="polite"> <div class="vector-properties" aria-live="polite">
<h3>{{ self.vector_properties_label }}</h3> <h3>{{ self.vector_properties_label }}</h3>
<div class="vector-prop-list"> <div class="vector-prop-list">
<div class="row"> <div class="row">
<div class="vector-prop vector-prop-name"> <div class="vector-prop vector-select">
<span id="vector-prop-name-label"> <span id="vector-select-label">
{% trans "name" %}: {% trans "vector" %}:
</span> </span>
<select class="element-list-edit" aria-labelledby="vector-prop-name-label"> <select class="element-list-edit" aria-labelledby="vector-select-label">
<option value="-" selected="selected" disabled="disabled">-</option> <option value="-" selected="selected" disabled="disabled">-</option>
{% for vector in self.get_vectors %} {% for vector in self.get_vectors %}
<option value="vector-{{ forloop.counter0 }}" data-vector-name="{{ vector.name }}"> <option value="vector-{{ forloop.counter0 }}" data-vector-name="{{ vector.name }}">
...@@ -112,34 +130,40 @@ ...@@ -112,34 +130,40 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="vector-prop vector-prop-name">
<span id="vector-prop-name-label">
{% trans "name" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-name-label">
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="vector-prop vector-prop-label">
<span id="vector-prop-label-label">
{% trans "label" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-label-label">
</div>
<div class="vector-prop vector-prop-tail"> <div class="vector-prop vector-prop-tail">
<span id="vector-prop-tail-label"> <span id="vector-prop-tail-label">
{% trans "tail position" %}: {% trans "tail position" %}:
</span> </span>
<input type="text" value="-" aria-labelledby="vector-prop-tail-label"> <input type="text" value="-" aria-labelledby="vector-prop-tail-label">
</div> </div>
</div>
<div class="row">
<div class="vector-prop vector-prop-length"> <div class="vector-prop vector-prop-length">
<span id="vector-prop-length-label"> <span id="vector-prop-length-label">
{% trans "length" %}: {% trans "length" %}:
</span> </span>
<input type="text" value="-" aria-labelledby="vector-prop-length-label"> <input type="text" value="-" aria-labelledby="vector-prop-length-label">
</div> </div>
</div>
<div class="row">
<div class="vector-prop vector-prop-angle"> <div class="vector-prop vector-prop-angle">
<span id="vector-prop-angle-label"> <span id="vector-prop-angle-label">
{% trans "angle" %}: {% trans "angle" %}:
</span> </span>
<input type="text" value="-" aria-labelledby="vector-prop-angle-label"> <input type="text" value="-" aria-labelledby="vector-prop-angle-label">
</div> </div>
<div class="vector-prop vector-prop-slope">
<span id="vector-prop-slope-label">
{% trans "slope" %}:
</span>
<input type="text" value="-" aria-labelledby="vector-prop-slope-label" disabled="disabled">
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="vector-prop vector-prop-update"> <div class="vector-prop vector-prop-update">
...@@ -163,6 +187,135 @@ ...@@ -163,6 +187,135 @@
style="width: {{ self.width }}px; height: {{ self.height }}px;" style="width: {{ self.width }}px; height: {{ self.height }}px;"
tabindex="0"> tabindex="0">
</div> </div>
<div class="checks" style="height: {{ self.height }}px;">
<h3>{% trans "Checks" %}</h3>
<div class="check" id="check-tail">
<div class="row">
<span id="tail-check-label">
{% trans "check tail" %}:
</span>
<input type="checkbox" aria-labelledby="tail-check-label">
</div>
<div class="row">
<span id="tail-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="text" value="1.0" aria-labelledby="tail-tolerance-label">
</div>
</div>
<div class="check" id="check-tip">
<div class="row">
<span id="tip-check-label">
{% trans "check tip" %}:
</span>
<input type="checkbox" aria-labelledby="tip-check-label">
</div>
<div class="row">
<span id="tip-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="text" value="1.0" aria-labelledby="tip-tolerance-label">
</div>
</div>
<div class="check" id="check-tail_x">
<div class="row">
<span id="tail-x-check-label">
{% trans "check tail(x)" %}:
</span>
<input type="checkbox" aria-labelledby="tail-x-check-label">
</div>
<div class="row">
<span id="tail-x-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="text" value="1.0" aria-labelledby="tail-x-tolerance-label">
</div>
</div>
<div class="check" id="check-tail_y">
<div class="row">
<span id="tail-y-check-label">
{% trans "check tail(y)" %}:
</span>
<input type="checkbox" aria-labelledby="tail-y-check-label">
</div>
<div class="row">
<span id="tail-y-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="text" value="1.0" aria-labelledby="tail-y-tolerance-label">
</div>
</div>
<div class="check" id="check-tip_x">
<div class="row">
<span id="tip-x-check-label">
{% trans "check tip(x)" %}:
</span>
<input type="checkbox" aria-labelledby="tip-x-check-label">
</div>
<div class="row">
<span id="tip-x-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="text" value="1.0" aria-labelledby="tip-x-tolerance-label">
</div>
</div>
<div class="check" id="check-tip_y">
<div class="row">
<span id="tip-y-check-label">
{% trans "check tip(y)" %}:
</span>
<input type="checkbox" aria-labelledby="tip-y-check-label">
</div>
<div class="row">
<span id="tip-y-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="text" value="1.0" aria-labelledby="tip-y-tolerance-label">
</div>
</div>
<div class="check" id="check-coords">
<div class="row">
<span id="coords-check-label">
{% trans "check coords" %}:
</span>
<input type="checkbox" aria-labelledby="coords-check-label">
</div>
<div class="row">
<span id="coords-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="text" value="1.0" aria-labelledby="coords-tolerance-label">
</div>
</div>
<div class="check" id="check-length">
<div class="row">
<span id="length-check-label">
{% trans "check length" %}:
</span>
<input type="checkbox" aria-labelledby="length-check-label">
</div>
<div class="row">
<span id="length-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="text" value="1.0" aria-labelledby="length-tolerance-label">
</div>
</div>
<div class="check" id="check-angle">
<div class="row">
<span id="angle-check-label">
{% trans "check angle" %}:
</span>
<input type="checkbox" aria-labelledby="angle-check-label">
</div>
<div class="row">
<span id="angle-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="text" value="2.0" aria-labelledby="angle-tolerance-label">
</div>
</div>
</div>
</div> </div>
</div> </div>
</li> </li>
......
...@@ -178,7 +178,11 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock): ...@@ -178,7 +178,11 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
display_name="Expected result", display_name="Expected result",
help=( help=(
"Defines vector properties for grading. " "Defines vector properties for grading. "
"Vectors omitted from this setting are ignored when grading." "Vectors omitted from this setting are ignored when grading. "
"Note that you can also use the WYSIWYG editor below to opt in and out of checks "
"for individual vectors. "
"If you use the WYSIWYG editor at all, any changes you make here "
"will be overwritten when saving."
), ),
default="{}", default="{}",
multiline_editor=True, multiline_editor=True,
...@@ -206,6 +210,11 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock): ...@@ -206,6 +210,11 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
enforce_type=True enforce_type=True
) )
# Dictionary that keeps track of vector positions for correct answer;
# treated as an editable field but hidden from author in Studio
# since changes to it are implicit
expected_result_positions = Dict(scope=Scope.content)
# User state # User state
# Dictionary containing vectors and points present on the board when user last clicked "Check", # Dictionary containing vectors and points present on the board when user last clicked "Check",
...@@ -234,6 +243,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock): ...@@ -234,6 +243,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
'vectors', 'vectors',
'points', 'points',
'expected_result', 'expected_result',
'expected_result_positions',
'custom_checks' 'custom_checks'
) )
...@@ -261,6 +271,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock): ...@@ -261,6 +271,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
'vectors': self.get_vectors, 'vectors': self.get_vectors,
'points': self.get_points, 'points': self.get_points,
'expected_result': self.get_expected_result, 'expected_result': self.get_expected_result,
'expected_result_positions': self.expected_result_positions,
} }
@property @property
...@@ -406,6 +417,8 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock): ...@@ -406,6 +417,8 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
context = {'fields': [], 'self': self} context = {'fields': [], 'self': self}
# Build a list of all the fields that can be edited: # Build a list of all the fields that can be edited:
for field_name in self.editable_fields: for field_name in self.editable_fields:
if field_name == "expected_result_positions":
continue
field = self.fields[field_name] field = self.fields[field_name]
assert field.scope in (Scope.content, Scope.settings), ( assert field.scope in (Scope.content, Scope.settings), (
"Only Scope.content or Scope.settings fields can be used with " "Only Scope.content or Scope.settings fields can be used with "
......
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