Commit 7f341c34 by Tim Krones

Merge pull request #41 from open-craft/a11y

Make DnDv2 accessible
parents c90e8579 5f21dbc8
......@@ -8,29 +8,30 @@ The editor is fully guided. Features include:
* custom target image
* free target zone positioning and sizing
* custom size items
* custom zone labels
* custom text and background colors for items
* image items
* decoy items that don't have a zone
* feedback popups for both correct and incorrect attempts
* introductory and final feedback
It supports progressive grading and keeps progress across
The XBlock supports progressive grading and keeps progress across
refreshes. All checking and record keeping is done on the server side.
The screenshot shows the Drag and Drop XBlock rendered inside the edX
LMS before starting before the user starts solving the problem:
The following screenshot shows the Drag and Drop XBlock rendered
inside the edX LMS before the user starts solving the problem:
![Student view start](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/student-view-start.png)
This screenshot shows the XBlock after the student successfully
completed the drag and drop problem:
completed the Drag and Drop problem:
![Student view finish](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/student-view-finish.png)
Installation
------------
Install the requirements into the python virtual environment of your
Install the requirements into the Python virtual environment of your
`edx-platform` installation by running the following command from the
root folder:
......@@ -41,12 +42,12 @@ $ pip install -e .
Enabling in Studio
------------------
You can enable the Drag and Drop XBlock in studio through the advanced
settings.
You can enable the Drag and Drop XBlock in Studio through the Advanced
Settings.
1. From the main page of a specific course, navigate to `Settings ->
Advanced Settings` from the top menu.
2. Check for the `advanced_modules` policy key, and add
2. Check for the `Advanced Module List` policy key, and add
`"drag-and-drop-v2"` to the policy value list.
3. Click the "Save changes" button.
......@@ -54,13 +55,13 @@ Usage
-----
The Drag and Drop XBlock features an interactive editor. Add the Drag
and Drop component to a lesson, then click the 'Edit' button.
and Drop component to a lesson, then click the `EDIT` button.
![Edit view](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/edit-view.png)
In the first step, you can set some basic properties of the component,
such as the title, question text that rendered above the background
image, the introduction feedback (shown initially) and the final
such as the title, the question text to render above the background
image, the introductory feedback (shown initially) and the final
feedback (shown after the student successfully completes the drag and
drop problem).
......@@ -69,21 +70,31 @@ drop problem).
In the next step, you set the background image URL and define the
properties of the drop zones. The properties include the title/text
rendered in the drop zone, the zone's dimensions and position
coordinates. You can define an arbitrary number of drop zones as long
as their titles are unique.
coordinates. In this step you can also specify whether you would like
zone labels to be shown to students or not. It is possible to define
an arbitrary number of drop zones as long as their titles are unique.
![Drag item edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/edit-view-items.png)
In the final step, you define the drag items. A drag item can contain
either text or an image. You can define the success and error feedback
texts. The feedback text is displayed in a popup after the student
drops the item into a zone - the success feedback is shown if the item
is dropped into the correct zone, while the error feedback is shown
when dropping the item into a wrong drop zone.
either text or an image. You can define custom success and error feedback
for each item. The feedback text is displayed in a popup after the student
drops the item on a zone - the success feedback is shown if the item
is dropped on the correct zone, while the error feedback is shown
when dropping the item on an incorrect drop zone.
Additionally, items can have a numerical value (and an optional error
margin) associated with them. When a student drops an item that has a
numerical value on the correct zone, an input field for entering a
value is shown next to the item. The value that the student submits is
checked against the expected value for the item. If you also specify a
margin, the value entered by the student will be considered correct if
it does not differ from the expected value by more than that margin
(and incorrect otherwise).
![Zone dropdown](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/edit-view-zone-dropdown.png)
The zone that the item belongs is selected from a dropdown that
The zone that an item belongs to is selected from a dropdown that
includes all drop zones defined in the previous step and a `none`
option that can be used for "decoy" items - items that don't belong to
any zone.
......
from .utils import _
TARGET_IMG_DESCRIPTION = _(
"An isosceles triangle with three layers of similar height. "
"It is shown upright, so the widest layer is located at the bottom, "
"and the narrowest layer is located at the top."
)
TOP_ZONE_TITLE = _("The Top Zone")
MIDDLE_ZONE_TITLE = _("The Middle Zone")
BOTTOM_ZONE_TITLE = _("The Bottom Zone")
TOP_ZONE_DESCRIPTION = _("Use this zone to associate an item with the top layer of the triangle.")
MIDDLE_ZONE_DESCRIPTION = _("Use this zone to associate an item with the middle layer of the triangle.")
BOTTOM_ZONE_DESCRIPTION = _("Use this zone to associate an item with the bottom layer of the triangle.")
ITEM_INCORRECT_FEEDBACK = _("No, this item does not belong here. Try again.")
ITEM_CORRECT_FEEDBACK = _("Correct! This one belongs to {zone}.")
START_FEEDBACK = _("Drag the items onto the image above.")
FINISH_FEEDBACK = _("Good work! You have completed this drag and drop exercise.")
DEFAULT_DATA = {
"targetImgDescription": TARGET_IMG_DESCRIPTION,
"zones": [
{
"index": 1,
"id": "zone-1",
"title": _("Zone 1"),
"title": TOP_ZONE_TITLE,
"description": TOP_ZONE_DESCRIPTION,
"x": 160,
"y": 30,
"width": 196,
......@@ -14,47 +36,68 @@ DEFAULT_DATA = {
{
"index": 2,
"id": "zone-2",
"title": _("Zone 2"),
"title": MIDDLE_ZONE_TITLE,
"description": MIDDLE_ZONE_DESCRIPTION,
"x": 86,
"y": 210,
"width": 340,
"height": 140,
"height": 138,
},
{
"index": 3,
"id": "zone-3",
"title": BOTTOM_ZONE_TITLE,
"description": BOTTOM_ZONE_DESCRIPTION,
"x": 15,
"y": 350,
"width": 485,
"height": 135,
}
],
"items": [
{
"displayName": "1",
"displayName": _("Goes to the top"),
"feedback": {
"incorrect": _("No, 1 does not belong here"),
"correct": _("Yes, it's a 1")
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE)
},
"zone": "Zone 1",
"backgroundImage": "",
"zone": TOP_ZONE_TITLE,
"imageURL": "",
"id": 0,
},
{
"displayName": "2",
"displayName": _("Goes to the middle"),
"feedback": {
"incorrect": _("No, 2 does not belong here"),
"correct": _("Yes, it's a 2")
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=MIDDLE_ZONE_TITLE)
},
"zone": "Zone 2",
"backgroundImage": "",
"zone": MIDDLE_ZONE_TITLE,
"imageURL": "",
"id": 1,
},
{
"displayName": "X",
"displayName": _("Goes to the bottom"),
"feedback": {
"incorrect": _("You silly, there are no zones for X"),
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=BOTTOM_ZONE_TITLE)
},
"zone": BOTTOM_ZONE_TITLE,
"imageURL": "",
"id": 2,
},
{
"displayName": _("I don't belong anywhere"),
"feedback": {
"incorrect": _("You silly, there are no zones for this one."),
"correct": ""
},
"zone": "none",
"backgroundImage": "",
"id": 2,
"imageURL": "",
"id": 3,
},
],
"feedback": {
"start": _("Drag the items onto the image above."),
"finish": _("Good work! You have completed this drag and drop exercise.")
"start": START_FEEDBACK,
"finish": FINISH_FEEDBACK,
},
}
......@@ -153,6 +153,7 @@ class DragAndDropBlock(XBlock):
"question_text": self.question_text,
"show_question_header": self.show_question_header,
"target_img_expanded_url": self.target_img_expanded_url,
"target_img_description": self.target_img_description,
"item_background_color": self.item_background_color or None,
"item_text_color": self.item_text_color or None,
"initial_feedback": self.data['feedback']['start'],
......@@ -329,6 +330,11 @@ class DragAndDropBlock(XBlock):
return self.default_background_image_url
@property
def target_img_description(self):
""" Get the description for the target image (the image items are dragged onto). """
return self.data.get("targetImgDescription", "")
@property
def default_background_image_url(self):
""" The URL to the default background image, shown when no custom background is used """
return self.runtime.local_resource_url(self, "public/img/triangle.png")
......
......@@ -23,7 +23,7 @@
/* Shared styles used in header and footer */
.xblock--drag-and-drop .title1 {
color: rgb(85, 85, 85);
color: #555555;
text-transform: uppercase;
font-weight: bold;
font-style: normal;
......@@ -58,7 +58,7 @@
align-items: center;
position: relative;
border: 1px solid rgba(0,0,0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
padding: 5px;
}
......@@ -72,10 +72,12 @@
border-radius: 3px;
margin: 5px;
padding: 10px;
background-color: #2e83cd;
background-color: #1d5280;
font-size: 14px;
color: #fff;
opacity: 1;
outline-color: #fff;
outline-style: none;
/* Some versions of the drag and drop library try to fiddle with this */
z-index: 10 !important;
}
......@@ -112,8 +114,16 @@
transform: translate(-50%, -50%); /* These blocks are to be centered on their absolute x,y position */
}
/* Focused option */
.xblock--drag-and-drop .drag-container .item-bank .option:focus,
.xblock--drag-and-drop .drag-container .item-bank .option:hover {
outline-width: 2px;
outline-style: solid;
outline-offset: -4px;
}
.xblock--drag-and-drop .drag-container .ui-draggable-dragging {
box-shadow: 0 16px 32px 0 rgba(0,0,0,.3);
box-shadow: 0 16px 32px 0 rgba(0, 0, 0, 0.3);
border: 1px solid #ccc;
opacity: .65;
z-index: 20 !important;
......@@ -159,7 +169,7 @@
.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input {
background: #ceffce;
color: #0dad0d;
color: #087108;
}
.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input {
......@@ -224,8 +234,12 @@
}
/* Focused zone */
.xblock--drag-and-drop .zone:focus {
border: 2px solid #a5a5a5;
}
.xblock--drag-and-drop .zone p {
visibility: hidden;
width: 100%;
font-family: Arial;
font-size: 16px;
......@@ -250,7 +264,7 @@
/*** FEEDBACK ***/
.xblock--drag-and-drop .feedback {
margin-top: 20px;
border-top: solid 1px rgb(189, 189, 189);
border-top: solid 1px #bdbdbd;
}
.xblock--drag-and-drop .popup {
......@@ -258,8 +272,8 @@
display: none;
top: 5%;
right: 5%;
border: 1px solid white;
background: none repeat scroll 0 0 rgba(0,0,0,.8);
border: 1px solid #fff;
background: none repeat scroll 0 0 rgba(0, 0, 0, 0.8);
width: 500px;
max-width: 90%;
min-height: 50px;
......@@ -269,7 +283,7 @@
}
.xblock--drag-and-drop .popup .popup-content {
color: #FFFFFF;
color: #ffffff;
margin-left: 15px;
margin-top: 35px;
margin-bottom: 15px;
......@@ -282,7 +296,7 @@
margin-right: 8px;
margin-top: 8px;
margin-left: 20px;
color: #FFFFFF;
color: #ffffff;
font-family: "fontawesome";
font-size: 18pt;
}
......@@ -290,6 +304,20 @@
.xblock--drag-and-drop .reset-button {
cursor: pointer;
float: right;
color: #3384CA;
color: #2d74b3;
margin-top: 3px;
}
/* Make sure screen-reader content is hidden in the workbench: */
.xblock--drag-and-drop .sr {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
background: #ffffff;
color: #000000;
}
......@@ -103,7 +103,8 @@
margin: 10px 0 0 0;
}
.xblock--drag-and-drop--editor .target-image-form input {
.xblock--drag-and-drop--editor .target-image-form input,
.xblock--drag-and-drop--editor .target-image-form textarea {
width: 50%;
}
......@@ -141,9 +142,11 @@
width: 18%;
}
.xblock--drag-and-drop--editor .zones-form .zone-row .title {
.xblock--drag-and-drop--editor .zones-form .zone-row > input {
width: 60%;
margin: 0 0 5px;
line-height: 2.664rem; /* .title gets line-height from a Studio rule that does not apply to .description;
here we make sure that both input fields get the same value for line-height */
}
.xblock--drag-and-drop--editor .zones-form .zone-row .layout {
......@@ -162,6 +165,7 @@
height: 128px;
}
.xblock--drag-and-drop--editor .target-image-form .target-image-form-help,
.xblock--drag-and-drop--editor .item-styles-form .item-styles-form-help {
margin-top: 5px;
font-size: small;
......@@ -173,7 +177,7 @@
}
.xblock--drag-and-drop--editor .items-form .item {
background: #73bde7;
background: #8fcaec;
padding: 10px 0 1px;
margin: 15px 0;
}
......@@ -187,6 +191,11 @@
width: 35%;
}
.xblock--drag-and-drop--editor .items-form .item-image-url {
width: 81%;
margin-right: 1%;
}
.xblock--drag-and-drop--editor .items-form .item-width,
.xblock--drag-and-drop--editor .items-form .item-height {
width: 40px;
......@@ -194,7 +203,8 @@
.xblock--drag-and-drop--editor .items-form .item-numerical-value,
.xblock--drag-and-drop--editor .items-form .item-numerical-margin {
width: 60px;
margin: 0 1%;
width: 50%;
}
.xblock--drag-and-drop--editor .items-form textarea {
......@@ -209,11 +219,12 @@
/** Buttons **/
.xblock--drag-and-drop--editor .btn {
background: #2e83cd;
background: #1d5280;
color: #fff;
border: 1px solid #156ab4;
border-radius: 6px;
padding: 5px 10px;
margin-top: 15px;
}
.xblock--drag-and-drop--editor .btn:hover {
......@@ -228,7 +239,7 @@
.xblock--drag-and-drop--editor .add-element {
text-decoration: none;
color: #2e83cd;
color: #1d5280;
}
.xblock--drag-and-drop--editor .remove-zone {
......@@ -246,7 +257,7 @@
width: 14px;
height: 14px;
border-radius: 7px;
background: #2e83cd;
background: #1d5280;
position: relative;
float: left;
margin: 0 5px 0 0;
......@@ -315,8 +326,9 @@
.xblock--drag-and-drop--editor .remove-item .icon.remove {
background: #fff;
color: #0072a7; /* Override default color from Studio to ensure contrast is large enough */
}
.xblock--drag-and-drop--editor .remove-item .icon.remove:before,
.xblock--drag-and-drop--editor .remove-item .icon.remove:after {
background: #2e83cd;
background: #1d5280;
}
......@@ -52,10 +52,11 @@ function DragAndDropBlock(runtime, element, configuration) {
promise.reject();
}
}, false);
img.addEventListener("error", function() { promise.reject() });
img.addEventListener("error", function() { promise.reject(); });
img.src = configuration.target_img_expanded_url;
img.alt = configuration.target_img_description;
return promise;
}
};
/** Zones are specified in the configuration via pixel values - convert to percentages */
var computeZoneDimension = function(zone, bg_image_width, bg_image_height) {
......@@ -167,10 +168,17 @@ function DragAndDropBlock(runtime, element, configuration) {
revertDuration: 150,
start: function(evt, ui) {
var item_id = $(this).data('value');
setGrabbedState(item_id, true);
updateDOM();
publishEvent({
event_type: 'xblock.drag-and-drop-v2.item.picked-up',
item_id: item_id
});
},
stop: function(evt, ui) {
var item_id = $(this).data('value');
setGrabbedState(item_id, false);
updateDOM();
}
});
} catch (e) {
......@@ -180,6 +188,14 @@ function DragAndDropBlock(runtime, element, configuration) {
});
};
var setGrabbedState = function(item_id, grabbed) {
for (var i = 0; i < configuration.items.length; i++) {
if (configuration.items[i].id === item_id) {
configuration.items[i].grabbed = grabbed;
}
}
};
var destroyDraggable = function() {
$root.find('.item-bank .option[data-drag-disabled=true]').each(function() {
try {
......@@ -310,14 +326,22 @@ function DragAndDropBlock(runtime, element, configuration) {
input.class_name = item_user_state.correct_input ? 'correct' : 'incorrect';
}
}
var imageURL = item.imageURL || item.backgroundImage; // Fall back on "backgroundImage" to be backward-compatible
var grabbed = false;
if (item.grabbed !== undefined) {
grabbed = item.grabbed;
}
var itemProperties = {
value: item.id,
drag_disabled: Boolean(item_user_state || state.finished),
class_name: item_user_state && ('input' in item_user_state || item_user_state.correct_input) ? 'fade': undefined,
xhr_active: (item_user_state && item_user_state.submitting_location),
input: input,
content_html: item.backgroundImage ? '<img src="' + item.backgroundImage + '"/>' : item.displayName,
has_image: !!item.backgroundImage
displayName: item.displayName,
imageURL: imageURL,
imageDescription: item.imageDescription,
has_image: !!imageURL,
grabbed: grabbed,
};
if (item_user_state) {
itemProperties.is_placed = true;
......@@ -340,6 +364,7 @@ function DragAndDropBlock(runtime, element, configuration) {
question_html: configuration.question_text,
show_question_header: configuration.show_question_header,
target_img_src: configuration.target_img_expanded_url,
target_img_description: configuration.target_img_description,
display_zone_labels: configuration.display_zone_labels,
zones: configuration.zones,
items: items,
......
......@@ -70,9 +70,11 @@ function DragAndDropEditBlock(runtime, element, params) {
}
// Set the target image and bind its event handler:
$('.target-image-form input', element).val(_fn.data.targetImg);
$('.target-image-form .url-input', element).val(_fn.data.targetImg);
$('.target-image-form .description-input', element).val(_fn.data.targetImgDescription);
_fn.build.$el.targetImage.load(_fn.build.form.zone.imageLoaded);
_fn.build.$el.targetImage.attr('src', params.target_img_expanded_url);
_fn.build.$el.targetImage.attr('alt', _fn.data.targetImgDescription);
if (_fn.data.displayLabels) {
$('.display-labels-form input', element).prop('checked', true);
......@@ -122,7 +124,7 @@ function DragAndDropEditBlock(runtime, element, params) {
.on('click', '.target-image-form button', function(e) {
e.preventDefault();
var new_img_url = $.trim($('.target-image-form input', element).val());
var new_img_url = $.trim($('.target-image-form .url-input', element).val());
if (new_img_url) {
// We may need to 'expand' the URL before it will be valid.
// e.g. '/static/blah.png' becomes '/asset-v1:course+id/blah.png'
......@@ -136,6 +138,12 @@ function DragAndDropEditBlock(runtime, element, params) {
}
_fn.data.targetImg = new_img_url;
var new_description = $.trim(
$('.target-image-form .description-input', element).val()
);
_fn.build.$el.targetImage.attr('alt', new_description);
_fn.data.targetImgDescription = new_description;
// Placeholder shim for IE9
$.placeholder.shim();
})
......@@ -176,6 +184,7 @@ function DragAndDropEditBlock(runtime, element, params) {
// Update zone obj
var zoneObj = {
title: oldZone.title || 'Zone ' + num,
description: oldZone.description,
id: name,
index: num,
width: oldZone.width || 200,
......@@ -247,6 +256,7 @@ function DragAndDropEditBlock(runtime, element, params) {
_fn.tpl.zoneElement({
id: zoneObj.id,
title: zoneObj.title,
description: zoneObj.description,
x_percent: (+zoneObj.x) / imgWidth * 100,
y_percent: (+zoneObj.y) / imgHeight * 100,
width_percent: (+zoneObj.width) / imgWidth * 100,
......@@ -276,6 +286,8 @@ function DragAndDropEditBlock(runtime, element, params) {
record.title = $changedInput.val();
} else if ($changedInput.hasClass('width')) {
record.width = $changedInput.val();
} else if ($changedInput.hasClass('description')) {
record.description = $changedInput.val();
} else if ($changedInput.hasClass('height')) {
record.height = $changedInput.val();
} else if ($changedInput.hasClass('x')) {
......@@ -376,9 +388,10 @@ function DragAndDropEditBlock(runtime, element, params) {
$form.each(function(i, el) {
var $el = $(el),
name = $el.find('.item-text').val(),
backgroundImage = $el.find('.background-image').val();
imageURL = $el.find('.item-image-url').val(),
imageDescription = $el.find('.item-image-description').val();
if (name.length > 0 || backgroundImage.length > 0) {
if (name.length > 0 || imageURL.length > 0) {
// Item width/height are ignored, but preserve the data:
var width = $el.find('.item-width').val(),
height = $el.find('.item-height').val();
......@@ -401,7 +414,8 @@ function DragAndDropEditBlock(runtime, element, params) {
width: width,
height: height
},
backgroundImage: backgroundImage
imageURL: imageURL,
imageDescription: imageDescription,
};
var numValue = parseFloat($el.find('.item-numerical-value').val());
......@@ -410,7 +424,7 @@ function DragAndDropEditBlock(runtime, element, params) {
data.inputOptions = {
value: numValue,
margin: isFinite(numMargin) ? numMargin : 0
}
};
}
items.push(data);
......
......@@ -55,29 +55,45 @@
var itemTemplate = function(item) {
var style = {};
var className = (item.class_name) ? item.class_name : "";
var tabindex = 0;
if (item.background_color) {
style['background-color'] = item.background_color;
}
if (item.color) {
style.color = item.color;
// Ensure contrast between outline-color and background color
// matches contrast between text color and background color:
style['outline-color'] = item.color;
}
if (item.is_placed) {
style.left = item.x_percent + "%";
style.top = item.y_percent + "%";
tabindex = -1; // If an item has been placed it can no longer be interacted with,
// so remove the ability to move focus to it using the keyboard
}
if (item.has_image) {
className += " " + "option-with-image";
}
var content_html = item.displayName;
if (item.imageURL) {
content_html = '<img src="' + item.imageURL + '" alt="' + item.imageDescription + '" />';
}
return (
h('div.option',
{
key: item.value,
className: className,
attributes: {'data-value': item.value, 'data-drag-disabled': item.drag_disabled},
attributes: {
'tabindex': tabindex,
'draggable': !item.drag_disabled,
'aria-grabbed': item.grabbed,
'data-value': item.value,
'data-drag-disabled': item.drag_disabled
},
style: style
}, [
itemSpinnerTemplate(item.xhr_active),
h('div', {innerHTML: item.content_html, className: "item-content"}),
h('div', {innerHTML: content_html, className: "item-content"}),
itemInputTemplate(item.input)
]
)
......@@ -85,18 +101,27 @@
};
var zoneTemplate = function(zone, ctx) {
var className = ctx.display_zone_labels ? 'zone-name' : 'zone-name sr';
return (
h(
'div.zone',
{
id: zone.id,
attributes: {'data-zone': zone.title},
attributes: {
'tabindex': 0,
'dropzone': 'move',
'aria-dropeffect': 'move',
'data-zone': zone.title
},
style: {
top: zone.y_percent + '%', left: zone.x_percent + "%",
width: zone.width_percent + '%', height: zone.height_percent + "%",
}
},
ctx.display_zone_labels ? h('p', zone.title) : null
[
h('p', { className: className }, zone.title),
h('p', { className: 'zone-description sr' }, zone.description)
]
)
);
};
......@@ -104,8 +129,9 @@
var feedbackTemplate = function(ctx) {
var feedback_display = ctx.feedback_html ? 'block' : 'none';
var reset_button_display = ctx.display_reset_button ? 'block' : 'none';
var properties = { attributes: { 'aria-live': 'polite' } };
return (
h('section.feedback', [
h('section.feedback', properties, [
h('div.reset-button', {style: {display: reset_button_display}}, gettext('Reset exercise')),
h('h3.title1', {style: {display: feedback_display}}, gettext('Feedback')),
h('p.message', {style: {display: feedback_display},
......@@ -135,7 +161,7 @@
h('p.popup-content', {innerHTML: ctx.popup_html}),
]),
h('div.target-img-wrapper', [
h('img.target-img', {src: ctx.target_img_src, alt: "Image Description here"}),
h('img.target-img', {src: ctx.target_img_src, alt: ctx.target_img_description}),
]),
renderCollection(zoneTemplate, ctx.zones, ctx),
renderCollection(itemTemplate, items_placed, ctx),
......
......@@ -20,7 +20,7 @@
</label>
<h3>{% trans "Maximum score" %}</h3>
<input class="weight" value="1" value="{{ self.weight }}"/>
<input type="number" step="0.1" class="weight" value="1" value="{{ self.weight }}" />
<h3>{% trans "Question text" %}</h3>
<textarea class="question-text">{{ self.question_text }}</textarea>
......@@ -30,7 +30,7 @@
{% trans "Show \"Question\" heading" %}
</label>
<h3>{% trans "Introduction Feedback" %}</h3>
<h3>{% trans "Introductory Feedback" %}</h3>
<textarea class="intro-feedback">{{ self.data.feedback.start }}</textarea>
<h3>{% trans "Final Feedback" %}</h3>
......@@ -41,18 +41,41 @@
<div class="tab zones-tab hidden">
<header class="tab-header">
<h3>{% trans "Zone Positions" %}</h3>
<h3>{% trans "Zones" %}</h3>
</header>
<section class="tab-content">
<section class="tab-content target-image-form">
<label>{% trans "New background URL" %}:</label>
<input type="text">
<form class="target-image-form">
<h3 id="background-url-label">
{% trans "Background URL" %}
</h3>
<input type="text"
class="url-input"
aria-labelledby="background-url-label"
placeholder="{% trans 'e.g. http://example.com/background.png or /static/background.png' %}">
<h3 id="background-description-label">
{% trans "Background description" %}
</h3>
<textarea class="description-input"
aria-labelledby="background-description-label"
aria-describedby="background-description-description"></textarea>
<div id="background-description-description" class="target-image-form-help">
{% blocktrans %}
Please provide a description of the image for non-visual users.
The description should provide sufficient information that would allow anyone
to solve the problem even without seeing the image.
{% endblocktrans %}
</div>
<button class="btn">{% trans "Change background" %}</button>
</form>
</section>
<section class="tab-content display-labels-form">
<section class="tab-content">
<form class="display-labels-form">
<h3>{% trans "Zone labels" %}</h3>
<label for="display-labels">{% trans "Display label names on the image" %}:</label>
<input name="display-labels" id="display-labels" type="checkbox" />
</form>
</section>
<section class="tab-content">
<div class="zone-editor">
<div class="controls">
<form class="zones-form"></form>
......
......@@ -5,26 +5,48 @@
width:{{ width_percent }}%;
height:{{ height_percent }}%;">
<p>{{{ title }}}</p>
<p class="sr">{{{ description }}}</p>
</div>
</script>
<script id="zone-input-tpl" type="text/html">
<div class="zone-row {{ id }}" data-index="{{index}}">
<label>{{i18n "Text"}}</label>
<input type="text" class="title" value="{{ title }}" />
<label for="zone-{{index}}-title">{{i18n "Text"}}</label>
<input type="text"
id="zone-{{index}}-title"
class="title"
value="{{ title }}" />
<a href="#" class="remove-zone hidden">
<div class="icon remove"></div>
</a>
<label for="zone-{{index}}-description">{{i18n "Description"}}</label>
<input type="text"
id="zone-{{index}}-description"
class="description"
value="{{ description }}"
placeholder="{{i18n 'Describe this zone to non-visual users'}}" />
<div class="layout">
<label>{{i18n "width"}}</label>
<input type="text" class="size width" value="{{ width }}" />
<label>{{i18n "height"}}</label>
<input type="text" class="size height" value="{{ height }}" />
<label for="zone-{{index}}-width">{{i18n "width"}}</label>
<input type="text"
id="zone-{{index}}-width"
class="size width"
value="{{ width }}" />
<label for="zone-{{index}}-height">{{i18n "height"}}</label>
<input type="text"
id="zone-{{index}}-height"
class="size height"
value="{{ height }}" />
<br />
<label>x</label>
<input type="text" class="coord x" value="{{ x }}" />
<label>y</label>
<input type="text" class="coord y" value="{{ y }}" />
<label for="zone-{{index}}-x">x</label>
<input type="text"
id="zone-{{index}}-x"
class="coord x"
value="{{ x }}" />
<label for="zone-{{index}}-y">y</label>
<input type="text"
id="zone-{{index}}-y"
class="coord y"
value="{{ y }}" />
</div>
</div>
</script>
......@@ -36,25 +58,39 @@
<script id="item-input-tpl" type="text/html">
<div class="item">
<div class="row">
<label>{{i18n "Text"}}</label>
<input type="text" class="item-text" value="{{ displayName }}"/>
<label>{{i18n "Zone"}}</label>
<select class="zone-select">{{ dropdown }}</select>
<label for="item-{{id}}-text">{{i18n "Text"}}</label>
<input type="text"
id="item-{{id}}-text"
class="item-text"
value="{{ displayName }}" />
<label for="item-{{id}}-zone">{{i18n "Zone"}}</label>
<select id="item-{{id}}-zone"
class="zone-select">{{ dropdown }}</select>
<a href="#" class="remove-item hidden">
<div class="icon remove"></div>
</a>
</div>
<div class="row">
<label>{{i18n "Background image URL (alternative to the text)"}}</label>
<textarea class="background-image">{{ backgroundImage }}</textarea>
<label for="item-{{id}}-image-url">{{i18n "Image URL (alternative to the text)"}}</label>
<input type="text"
id="item-{{id}}-image-url"
class="item-image-url"
value="{{ imageURL }}" />
</div>
<div class="row">
<label>{{i18n "Success Feedback"}}</label>
<textarea class="success-feedback">{{ feedback.correct }}</textarea>
<label for="item-{{id}}-image-description">{{i18n "Image description (should provide sufficient information to place the item even if the image did not load)"}}</label>
<textarea id="item-{{id}}-image-description"
class="item-image-description">{{ imageDescription }}</textarea>
</div>
<div class="row">
<label>{{i18n "Error Feedback"}}</label>
<textarea class="error-feedback">{{ feedback.incorrect }}</textarea>
<label for="item-{{id}}-success-feedback">{{i18n "Success Feedback"}}</label>
<textarea id="item-{{id}}-success-feedback"
class="success-feedback">{{ feedback.correct }}</textarea>
</div>
<div class="row">
<label for="item-{{id}}-error-feedback">{{i18n "Error Feedback"}}</label>
<textarea id="item-{{id}}-error-feedback"
class="error-feedback">{{ feedback.incorrect }}</textarea>
</div>
<div class="row" style="display: none;">
<!--
......@@ -65,16 +101,34 @@
If we do add them back in, we should only allow setting "width". Height will be
detected automatically based on font/image size requirements.
-->
<label>{{i18n "Width in pixels (0 for auto)"}}</label>
<input type="text" class="item-width" value="{{ width }}"></input>
<label>{{i18n "Height in pixels (0 for auto)"}}</label>
<input type="text" class="item-height" value="{{ height }}"></input>
<label for="item-{{id}}-width">{{i18n "Width in pixels (0 for auto)"}}</label>
<input type="text"
id="item-{{id}}-width"
class="item-width"
value="{{ width }}" />
<label for="item-{{id}}-height">{{i18n "Height in pixels (0 for auto)"}}</label>
<input type="text"
id="item-{{id}}-height"
class="item-height"
value="{{ height }}" />
</div>
<div class="row">
<label for="item-{{id}}-numerical-value">
{{i18n "Optional numerical value (if you set this, students will be prompted for this value after dropping this item)"}}
</label>
<input type="number"
step="0.1"
id="item-{{id}}-numerical-value"
class="item-numerical-value" value="{{ numericalValue }}" />
</div>
<div class="row">
<label>{{i18n "Optional numerical value"}}</label>
<input type="text" class="item-numerical-value" value="{{ numericalValue }}"></input>
<label>{{i18n "Margin ±"}}</label>
<input type="text" class="item-numerical-margin" value="{{ numericalMargin }}"></input>
<label for="item-{{id}}-numerical-margin">
{{i18n "Margin ± (when a numerical value is required, values entered by students must not differ from the expected value by more than this margin; default is zero)"}}
</label>
<input type="number"
step="0.1"
id="item-{{id}}-numerical-margin"
class="item-numerical-margin" value="{{ numericalMargin }}" />
</div>
</div>
</script>
......@@ -27,7 +27,7 @@
"correct": "Yes 1"
},
"zone": "Zone 1",
"backgroundImage": "",
"imageURL": "",
"id": 0
},
{
......@@ -37,7 +37,7 @@
"correct": "Yes 2"
},
"zone": "Zone 2",
"backgroundImage": "",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
......@@ -51,7 +51,7 @@
"correct": ""
},
"zone": "none",
"backgroundImage": "",
"imageURL": "",
"id": 2
}
],
......
{
"zones": [
{
"index": 1,
"title": "Zone 1",
"description": "This describes zone 1",
"height": 178,
"width": 196,
"y": "30",
"x": "160",
"id": "zone-1"
},
{
"index": 2,
"title": "Zone 2",
"description": "This describes zone 2",
"height": 140,
"width": 340,
"y": "210",
"x": "86",
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"imageURL": "https://placehold.it/100x100",
"imageDescription": "This describes the background image of item 1",
"feedback": {
"incorrect": "No, 1 does not belong here",
"correct": "Yes, 1 goes here"
},
"zone": "Zone 1",
"id": 0
},
{
"displayName": "2",
"imageURL": "https://placehold.it/100x100",
"imageDescription": "This describes the background image of item 2",
"feedback": {
"incorrect": "No, 2 does not belong here",
"correct": "Yes, 2 goes here"
},
"zone": "Zone 2",
"id": 1
},
{
"displayName": "X",
"imageURL": "",
"feedback": {
"incorrect": "You silly, there are no zones for X",
"correct": ""
},
"zone": "none",
"id": 2
}
],
"feedback": {
"start": "Drag the items onto the image above.",
"finish": "Good work! You have completed this drag and drop exercise."
},
"targetImgDescription": "This describes the target image"
}
......@@ -27,7 +27,7 @@
"correct": "Correct 1"
},
"zone": "Zone 51",
"backgroundImage": "",
"imageURL": "",
"id": 10
},
{
......@@ -37,7 +37,7 @@
"correct": "Correct 2"
},
"zone": "Zone 52",
"backgroundImage": "",
"imageURL": "",
"id": 20,
"inputOptions": {
"value": 100,
......@@ -51,7 +51,7 @@
"correct": ""
},
"zone": "none",
"backgroundImage": "",
"imageURL": "",
"id": 30
}
],
......
......@@ -27,7 +27,7 @@
"correct": "Yes <b>1</b>"
},
"zone": "Zone <i>1</i>",
"backgroundImage": "",
"imageURL": "",
"id": 0
},
{
......@@ -37,7 +37,7 @@
"correct": "Yes <i>2</i>"
},
"zone": "Zone <b>2</b>",
"backgroundImage": "",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
......@@ -51,7 +51,7 @@
"correct": ""
},
"zone": "none",
"backgroundImage": "",
"imageURL": "",
"id": 2
}
],
......@@ -59,5 +59,6 @@
"start": "Intro <i>Feed</i>",
"finish": "Final <b>Feed</b>"
},
"targetImg": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg=="
"targetImg": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==",
"targetImgDescription": "This describes the target image"
}
......@@ -57,6 +57,9 @@ class BaseIntegrationTest(SeleniumBaseTest):
def _get_zones(self):
return self._page.find_elements_by_css_selector(".drag-container .zone")
def _get_feedback(self):
return self._page.find_element_by_css_selector(".feedback")
def _get_feedback_message(self):
return self._page.find_element_by_css_selector(".feedback .message")
......
......@@ -31,4 +31,6 @@ class TestCustomDataDragAndDropRendering(BaseIntegrationTest):
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciI"
"HdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg=="
)
custom_image_description = "This describes the target image"
self.assertEqual(bg_image.get_attribute("src"), custom_image_url)
self.assertEqual(bg_image.get_attribute("alt"), custom_image_description)
from selenium.webdriver import ActionChains
from drag_and_drop_v2.default_data import START_FEEDBACK, FINISH_FEEDBACK
from .test_base import BaseIntegrationTest
from ..utils import load_resource
......@@ -145,7 +146,7 @@ class InteractionTestBase(object):
self.assertDictEqual(locations_after_reset[item_key], initial_locations[item_key])
class InteractionTestFixture(InteractionTestBase):
class BasicInteractionTest(InteractionTestBase):
"""
Verifying Drag and Drop XBlock rendering against default data - if default data changes this will probably break.
"""
......@@ -161,8 +162,8 @@ class InteractionTestFixture(InteractionTestBase):
all_zones = ['Zone 1', 'Zone 2']
feedback = {
"intro": "Drag the items onto the image above.",
"final": "Good work! You have completed this drag and drop exercise."
"intro": START_FEEDBACK,
"final": FINISH_FEEDBACK,
}
def _get_scenario_xml(self): # pylint: disable=no-self-use
......@@ -184,7 +185,7 @@ class InteractionTestFixture(InteractionTestBase):
self.parameterized_final_feedback_and_reset(self.items_map, self.feedback)
class CustomDataInteractionTest(InteractionTestFixture, BaseIntegrationTest):
class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
items_map = {
0: ItemDefinition(0, 'Zone 1', "Yes 1", "No 1"),
1: ItemDefinition(1, 'Zone 2', "Yes 2", "No 2", "102"),
......@@ -202,7 +203,7 @@ class CustomDataInteractionTest(InteractionTestFixture, BaseIntegrationTest):
return self._get_custom_scenario_xml("integration/data/test_data.json")
class CustomHtmlDataInteractionTest(InteractionTestFixture, BaseIntegrationTest):
class CustomHtmlDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
items_map = {
0: ItemDefinition(0, 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"),
1: ItemDefinition(1, 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>", "95"),
......
from ddt import ddt, unpack, data
from selenium.common.exceptions import NoSuchElementException
from drag_and_drop_v2.default_data import START_FEEDBACK
from ..utils import load_resource
from .test_base import BaseIntegrationTest
class Colors(object):
WHITE = 'rgb(255, 255, 255)'
BLUE = 'rgb(46, 131, 205)'
BLUE = 'rgb(29, 82, 128)'
GREY = 'rgb(237, 237, 237)'
CORAL = '#ff7f50'
CORNFLOWERBLUE = 'cornflowerblue'
......@@ -32,10 +35,16 @@ class TestDragAndDropRender(BaseIntegrationTest):
def load_scenario(self, item_background_color="", item_text_color=""):
scenario_xml = """
<vertical_demo>
<drag-and-drop-v2 item_background_color='{item_background_color}' item_text_color='{item_text_color}' />
</vertical_demo>
""".format(item_background_color=item_background_color, item_text_color=item_text_color)
<vertical_demo>
<drag-and-drop-v2 item_background_color='{item_background_color}'
item_text_color='{item_text_color}'
data='{data}' />
</vertical_demo>
""".format(
item_background_color=item_background_color,
item_text_color=item_text_color,
data=load_resource("integration/data/test_data_a11y.json")
)
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml)
self.browser.get(self.live_server_url)
......@@ -61,6 +70,7 @@ class TestDragAndDropRender(BaseIntegrationTest):
def _test_item_style(self, item_element, style_settings):
item_val = item_element.get_attribute('data-value')
item_selector = '.item-bank .option[data-value=' + item_val + ']'
style = item_element.get_attribute('style')
# Check background color
background_color_property = 'background-color'
......@@ -69,7 +79,7 @@ class TestDragAndDropRender(BaseIntegrationTest):
expected_background_color = Colors.BLUE
else:
expected_background_color = Colors.rgb(style_settings['background-color'])
background_color = self._get_style('.item-bank .option[data-value='+item_val+']', 'backgroundColor')
background_color = self._get_style(item_selector, 'backgroundColor')
self.assertEquals(background_color, expected_background_color)
# Check text color
......@@ -80,21 +90,21 @@ class TestDragAndDropRender(BaseIntegrationTest):
expected_color = Colors.WHITE
else:
expected_color = Colors.rgb(style_settings['color'])
color = self._get_style('.item-bank .option[data-value='+item_val+']', 'color')
color = self._get_style(item_selector, 'color')
self.assertEquals(color, expected_color)
def test_items(self):
self.load_scenario()
items = self._get_items()
# Check outline color
outline_color_property = 'outline-color'
if outline_color_property not in style_settings:
self.assertNotIn(outline_color_property, style)
# Outline color should match text color to ensure it does not meld into background color:
expected_outline_color = expected_color
outline_color = self._get_style(item_selector, 'outlineColor')
self.assertEquals(outline_color, expected_outline_color)
self.assertEqual(len(items), 3)
for index, item in enumerate(items):
self.assertEqual(item.get_attribute('data-value'), str(index))
self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text'])
self.assertIn('ui-draggable', self.get_element_classes(item))
self._test_item_style(item, {})
def test_items_default_colors(self):
self.load_scenario()
self._test_items()
@unpack
@data(
......@@ -105,21 +115,39 @@ class TestDragAndDropRender(BaseIntegrationTest):
def test_items_custom_colors(self, item_background_color, item_text_color):
self.load_scenario(item_background_color, item_text_color)
items = self._get_items()
self.assertEqual(len(items), 3)
color_settings = {}
if item_background_color:
color_settings['background-color'] = item_background_color
if item_text_color:
color_settings['color'] = item_text_color
color_settings['outline-color'] = item_text_color
self._test_items(color_settings=color_settings)
def _test_items(self, color_settings=None):
color_settings = color_settings or {}
items = self._get_items()
self.assertEqual(len(items), 3)
for index, item in enumerate(items):
item_number = index + 1
self.assertEqual(item.get_attribute('tabindex'), '0')
self.assertEqual(item.get_attribute('draggable'), 'true')
self.assertEqual(item.get_attribute('aria-grabbed'), 'false')
self.assertEqual(item.get_attribute('data-value'), str(index))
self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text'])
self.assertIn('ui-draggable', self.get_element_classes(item))
self._test_item_style(item, color_settings)
try:
background_image = item.find_element_by_css_selector('img')
except NoSuchElementException:
self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text'])
else:
self.assertEqual(
background_image.get_attribute('alt'),
'This describes the background image of item {}'.format(item_number)
)
def test_zones(self):
self.load_scenario()
......@@ -128,20 +156,36 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.assertEqual(len(zones), 2)
self.assertEqual(zones[0].get_attribute('data-zone'), 'Zone 1')
self.assertIn('ui-droppable', self.get_element_classes(zones[0]))
self._assert_box_percentages('#zone-1', left=31.1284, top=6.17284, width=38.1323, height=36.6255)
self.assertEqual(zones[1].get_attribute('data-zone'), 'Zone 2')
self.assertIn('ui-droppable', self.get_element_classes(zones[1]))
self._assert_box_percentages('#zone-2', left=16.7315, top=43.2099, width=66.1479, height=28.8066)
box_percentages = [
{"left": 31.1284, "top": 6.17284, "width": 38.1323, "height": 36.6255},
{"left": 16.7315, "top": 43.2099, "width": 66.1479, "height": 28.8066}
]
for index, zone in enumerate(zones):
zone_number = index + 1
self.assertEqual(zone.get_attribute('tabindex'), '0')
self.assertEqual(zone.get_attribute('dropzone'), 'move')
self.assertEqual(zone.get_attribute('aria-dropeffect'), 'move')
self.assertEqual(zone.get_attribute('data-zone'), 'Zone {}'.format(zone_number))
self.assertIn('ui-droppable', self.get_element_classes(zone))
zone_box_percentages = box_percentages[index]
self._assert_box_percentages( # pylint: disable=star-args
'#zone-{}'.format(zone_number), **zone_box_percentages
)
zone_name = zone.find_element_by_css_selector('p.zone-name')
self.assertEqual(zone_name.text, 'ZONE {}'.format(zone_number))
zone_description = zone.find_element_by_css_selector('p.zone-description')
self.assertEqual(zone_description.text, 'THIS DESCRIBES ZONE {}'.format(zone_number))
# Zone description should only be visible to screen readers:
self.assertEqual(zone_description.get_attribute('class'), 'zone-description sr')
def test_feedback(self):
self.load_scenario()
feedback = self._get_feedback()
feedback_message = self._get_feedback_message()
self.assertEqual(feedback_message.text, "Drag the items onto the image above.")
self.assertEqual(feedback.get_attribute('aria-live'), 'polite')
self.assertEqual(feedback_message.text, START_FEEDBACK)
def test_background_image(self):
self.load_scenario()
......@@ -149,3 +193,4 @@ class TestDragAndDropRender(BaseIntegrationTest):
bg_image = self.browser.find_element_by_css_selector(".xblock--drag-and-drop .target-img")
image_path = '/resource/drag-and-drop-v2/public/img/triangle.png'
self.assertTrue(bg_image.get_attribute("src").endswith(image_path))
self.assertEqual(bg_image.get_attribute("alt"), 'This describes the target image')
......@@ -4,6 +4,7 @@
"question_text": "Solve this <strong>drag-and-drop</strong> problem.",
"show_question_header": false,
"target_img_expanded_url": "/expanded/url/to/drag_and_drop_v2/public/img/triangle.png",
"target_img_description": "This describes the target image",
"item_background_color": "white",
"item_text_color": "#000080",
"initial_feedback": "HTML <strong>Intro</strong> Feed",
......@@ -33,25 +34,25 @@
"items": [
{
"displayName": "<b>1</b>",
"backgroundImage": "",
"imageURL": "",
"id": 0,
"inputOptions": false
},
{
"displayName": "<i>2</i>",
"backgroundImage": "",
"imageURL": "",
"id": 1,
"inputOptions": true
},
{
"displayName": "X",
"backgroundImage": "",
"imageURL": "",
"id": 2,
"inputOptions": false
},
{
"displayName": "",
"backgroundImage": "http://placehold.it/100x300",
"imageURL": "http://placehold.it/100x300",
"id": 3,
"inputOptions": false
}
......
......@@ -28,7 +28,7 @@
"correct": "Yes <b>1</b>"
},
"zone": "Zone <i>1</i>",
"backgroundImage": "",
"imageURL": "",
"id": 0
},
{
......@@ -38,7 +38,7 @@
"correct": "Yes <i>2</i>"
},
"zone": "Zone <b>2</b>",
"backgroundImage": "",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
......@@ -52,7 +52,7 @@
"correct": ""
},
"zone": "none",
"backgroundImage": "",
"imageURL": "",
"id": 2
},
{
......@@ -62,12 +62,13 @@
"correct": ""
},
"zone": "none",
"backgroundImage": "http://placehold.it/100x300",
"imageURL": "http://placehold.it/100x300",
"id": 3
}
],
"feedback": {
"start": "HTML <strong>Intro</strong> Feed",
"finish": "Final <strong>feedback</strong>!"
}
},
"targetImgDescription": "This describes the target image"
}
......@@ -4,6 +4,7 @@
"question_text": "",
"show_question_header": true,
"target_img_expanded_url": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"target_img_description": "This describes the target image",
"item_background_color": null,
"item_text_color": null,
"initial_feedback": "Intro Feed",
......@@ -33,28 +34,28 @@
"items": [
{
"displayName": "1",
"backgroundImage": "",
"imageURL": "",
"id": 0,
"inputOptions": false,
"size": {"height": "auto", "width": "190px"}
},
{
"displayName": "2",
"backgroundImage": "",
"imageURL": "",
"id": 1,
"inputOptions": true,
"size": {"height": "auto", "width": "190px"}
},
{
"displayName": "X",
"backgroundImage": "",
"imageURL": "",
"id": 2,
"inputOptions": false,
"size": {"height": "100px", "width": "100px"}
},
{
"displayName": "",
"backgroundImage": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"imageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"id": 3,
"inputOptions": false,
"size": {"height": "auto", "width": "190px"}
......
......@@ -27,7 +27,7 @@
"correct": "Yes 1"
},
"zone": "Zone 1",
"backgroundImage": "",
"imageURL": "",
"id": 0,
"size": {
"width": "190px",
......@@ -41,7 +41,7 @@
"correct": "Yes 2"
},
"zone": "Zone 2",
"backgroundImage": "",
"imageURL": "",
"id": 1,
"size": {
"width": "190px",
......@@ -59,7 +59,7 @@
"correct": ""
},
"zone": "none",
"backgroundImage": "",
"imageURL": "",
"id": 2,
"size": {
"width": "100px",
......@@ -73,7 +73,7 @@
"correct": ""
},
"zone": "none",
"backgroundImage": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"imageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"id": 3,
"size": {
"width": "190px",
......@@ -89,5 +89,6 @@
"start": "Intro Feed",
"finish": "Final Feed"
},
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png"
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"targetImgDescription": "This describes the target image"
}
......@@ -4,6 +4,7 @@
"question_text": "Can you solve this drag-and-drop problem?",
"show_question_header": true,
"target_img_expanded_url": "http://placehold.it/800x600",
"target_img_description": "This describes the target image",
"item_background_color": null,
"item_text_color": null,
"initial_feedback": "This is the initial feedback.",
......@@ -33,25 +34,25 @@
"items": [
{
"displayName": "1",
"backgroundImage": "",
"imageURL": "",
"id": 0,
"inputOptions": false
},
{
"displayName": "2",
"backgroundImage": "",
"imageURL": "",
"id": 1,
"inputOptions": true
},
{
"displayName": "X",
"backgroundImage": "",
"imageURL": "",
"id": 2,
"inputOptions": false
},
{
"displayName": "",
"backgroundImage": "http://placehold.it/200x100",
"imageURL": "http://placehold.it/200x100",
"id": 3,
"inputOptions": false
}
......
......@@ -29,7 +29,7 @@
"correct": "Yes 1"
},
"zone": "Zone 1",
"backgroundImage": "",
"imageURL": "",
"id": 0
},
{
......@@ -39,7 +39,7 @@
"correct": "Yes 2"
},
"zone": "Zone 2",
"backgroundImage": "",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
......@@ -53,7 +53,7 @@
"correct": ""
},
"zone": "none",
"backgroundImage": "",
"imageURL": "",
"id": 2
},
{
......@@ -63,7 +63,7 @@
"correct": ""
},
"zone": "none",
"backgroundImage": "http://placehold.it/200x100",
"imageURL": "http://placehold.it/200x100",
"id": 3
}
],
......@@ -76,5 +76,6 @@
"targetImg": "http://placehold.it/800x600",
"targetImgDescription": "This describes the target image",
"displayLabels": false
}
import unittest
from ..utils import (
DEFAULT_START_FEEDBACK,
DEFAULT_FINISH_FEEDBACK,
make_block,
TestCaseMixin,
from drag_and_drop_v2.default_data import (
TARGET_IMG_DESCRIPTION, START_FEEDBACK, FINISH_FEEDBACK, DEFAULT_DATA
)
from ..utils import make_block, TestCaseMixin
class BasicTests(TestCaseMixin, unittest.TestCase):
......@@ -36,35 +34,18 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
"question_text": "",
"show_question_header": True,
"target_img_expanded_url": '/expanded/url/to/drag_and_drop_v2/public/img/triangle.png',
"target_img_description": TARGET_IMG_DESCRIPTION,
"item_background_color": None,
"item_text_color": None,
"initial_feedback": DEFAULT_START_FEEDBACK,
"initial_feedback": START_FEEDBACK,
})
self.assertEqual(zones, [
{
"index": 1,
"title": "Zone 1",
"id": "zone-1",
"x": 160,
"y": 30,
"width": 196,
"height": 178,
},
{
"index": 2,
"title": "Zone 2",
"id": "zone-2",
"x": 86,
"y": 210,
"width": 340,
"height": 140,
}
])
self.assertEqual(zones, DEFAULT_DATA["zones"])
# Items should contain no answer data:
self.assertEqual(items, [
{"id": 0, "displayName": "1", "backgroundImage": "", "inputOptions": False},
{"id": 1, "displayName": "2", "backgroundImage": "", "inputOptions": False},
{"id": 2, "displayName": "X", "backgroundImage": "", "inputOptions": False},
{"id": 0, "displayName": "Goes to the top", "imageURL": "", "inputOptions": False},
{"id": 1, "displayName": "Goes to the middle", "imageURL": "", "inputOptions": False},
{"id": 2, "displayName": "Goes to the bottom", "imageURL": "", "inputOptions": False},
{"id": 3, "displayName": "I don't belong anywhere", "imageURL": "", "inputOptions": False},
])
def test_ajax_solve_and_reset(self):
......@@ -76,14 +57,16 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self.assertEqual(self.call_handler("get_user_state"), {
'items': {},
'finished': False,
'overall_feedback': DEFAULT_START_FEEDBACK,
'overall_feedback': START_FEEDBACK,
})
assert_user_state_empty()
# Drag both items into the correct spot:
data = {"val": 0, "zone": "Zone 1", "x_percent": "33%", "y_percent": "11%"}
# Drag three items into the correct spot:
data = {"val": 0, "zone": "The Top Zone", "x_percent": "33%", "y_percent": "11%"}
self.call_handler('do_attempt', data)
data = {"val": 1, "zone": "The Middle Zone", "x_percent": "67%", "y_percent": "80%"}
self.call_handler('do_attempt', data)
data = {"val": 1, "zone": "Zone 2", "x_percent": "67%", "y_percent": "80%"}
data = {"val": 2, "zone": "The Bottom Zone", "x_percent": "99%", "y_percent": "95%"}
self.call_handler('do_attempt', data)
# Check the result:
......@@ -91,14 +74,16 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self.assertEqual(self.block.item_state, {
'0': {'x_percent': '33%', 'y_percent': '11%'},
'1': {'x_percent': '67%', 'y_percent': '80%'},
'2': {'x_percent': '99%', 'y_percent': '95%'},
})
self.assertEqual(self.call_handler('get_user_state'), {
'items': {
'0': {'x_percent': '33%', 'y_percent': '11%', 'correct_input': True},
'1': {'x_percent': '67%', 'y_percent': '80%', 'correct_input': True},
'2': {'x_percent': '99%', 'y_percent': '95%', 'correct_input': True},
},
'finished': True,
'overall_feedback': DEFAULT_FINISH_FEEDBACK,
'overall_feedback': FINISH_FEEDBACK,
})
# Reset to initial conditions
......
......@@ -10,9 +10,6 @@ from xblock.runtime import KvsFieldData, DictKeyValueStore
import drag_and_drop_v2
DEFAULT_START_FEEDBACK = "Drag the items onto the image above."
DEFAULT_FINISH_FEEDBACK = "Good work! You have completed this drag and drop exercise."
def make_request(data, method='POST'):
""" Make a webob JSON Request """
......
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