Commit c78dbc6f by Adolfo R. Brandes

Add "item" field to item.dropped event

Adds a new "item" field to the edx.drag_and_drop_v2.item.dropped event
data. It contains the item text label when present, otherwise the item
imageURL.

This makes it consistent with the location field, which contains the
zone text label.
parent f435c5b7
......@@ -294,8 +294,10 @@ Example ("common" fields that are not interesting in this context have been left
...
"event": {
"is_correct": true, -- Whether the draggable item has been placed in the correct location.
"item": "Goes to the top", -- Name, or in the absence thereof, image URL of the draggable item.
"item_id": 0, -- ID of the draggable item.
"location": "The Top Zone", -- Name of the location the item was dragged to.
"location_id": 1, -- ID of the location the item was dragged to.
},
"event_source": "server", -- Common field, contains event source.
"event_type": "edx.drag_and_drop_v2.dropped", -- Common field, contains event name.
......@@ -316,6 +318,8 @@ Real event example (taken from a devstack):
"event": {
"is_correct": true,
"location": "The Top Zone",
"location_id": 1,
"item": "Goes to the top",
"item_id": 0,
},
"event_source": "server",
......
......@@ -24,6 +24,12 @@ ITEM_INCORRECT_FEEDBACK = _("No, this item does not belong here. Try again.")
ITEM_NO_ZONE_FEEDBACK = _("You silly, there are no zones for this one.")
ITEM_ANY_ZONE_FEEDBACK = _("Of course it goes here! It goes anywhere!")
ITEM_TOP_ZONE_NAME = _("Goes to the top")
ITEM_MIDDLE_ZONE_NAME = _("Goes to the middle")
ITEM_BOTTOM_ZONE_NAME = _("Goes to the bottom")
ITEM_ANY_ZONE_NAME = _("Goes anywhere")
ITEM_NO_ZONE_NAME = _("I don't belong anywhere")
START_FEEDBACK = _("Drag the items onto the image above.")
FINISH_FEEDBACK = _("Good work! You have completed this drag and drop problem.")
......@@ -63,7 +69,7 @@ DEFAULT_DATA = {
],
"items": [
{
"displayName": _("Goes to the top"),
"displayName": ITEM_TOP_ZONE_NAME,
"feedback": {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE)
......@@ -75,7 +81,7 @@ DEFAULT_DATA = {
"id": 0,
},
{
"displayName": _("Goes to the middle"),
"displayName": ITEM_MIDDLE_ZONE_NAME,
"feedback": {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=MIDDLE_ZONE_TITLE)
......@@ -87,7 +93,7 @@ DEFAULT_DATA = {
"id": 1,
},
{
"displayName": _("Goes to the bottom"),
"displayName": ITEM_BOTTOM_ZONE_NAME,
"feedback": {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=BOTTOM_ZONE_TITLE)
......@@ -99,7 +105,7 @@ DEFAULT_DATA = {
"id": 2,
},
{
"displayName": _("Goes anywhere"),
"displayName": ITEM_ANY_ZONE_NAME,
"feedback": {
"incorrect": "",
"correct": ITEM_ANY_ZONE_FEEDBACK
......@@ -113,7 +119,7 @@ DEFAULT_DATA = {
"id": 3
},
{
"displayName": _("I don't belong anywhere"),
"displayName": ITEM_NO_ZONE_NAME,
"feedback": {
"incorrect": ITEM_NO_ZONE_FEEDBACK,
"correct": ""
......
......@@ -663,7 +663,12 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
# attempt should already be validated here - not doing the check for existing zone again
zone = self._get_zone_by_uid(attempt['zone'])
item_label = item.get("displayName")
if not item_label:
item_label = item.get("imageURL")
self.runtime.publish(self, 'edx.drag_and_drop_v2.item.dropped', {
'item': item_label,
'item_id': item['id'],
'location': zone.get("title"),
'location_id': zone.get("uid"),
......
{
"zones": [
{
"width": 200,
"title": "Zone 1",
"height": 100,
"y": "200",
"x": "100",
"uid": "zone-1"
},
{
"width": 200,
"title": "Zone 2",
"height": 100,
"y": 0,
"x": 0,
"uid": "zone-2"
}
],
"items": [
{
"displayName": "Has name",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zones": [
"zone-1"
],
"imageURL": "",
"id": 0
},
{
"displayName": "",
"feedback": {
"incorrect": "No",
"correct": "Yes"
},
"zone": "zone-2",
"imageURL": "https://placehold.it/100x100",
"id": 1
}
],
"feedback": {
"start": "Start feedback",
"finish": "Finish feedback"
}
}
......@@ -7,6 +7,7 @@ from xml.sax.saxutils import escape
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from collections import namedtuple
from bok_choy.promise import EmptyPromise
from workbench import scenarios
......@@ -20,6 +21,8 @@ from drag_and_drop_v2.default_data import (
DEFAULT_DATA, START_FEEDBACK, FINISH_FEEDBACK,
TOP_ZONE_ID, TOP_ZONE_TITLE, MIDDLE_ZONE_ID, MIDDLE_ZONE_TITLE, BOTTOM_ZONE_ID, BOTTOM_ZONE_TITLE,
ITEM_CORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK, ITEM_ANY_ZONE_FEEDBACK, ITEM_NO_ZONE_FEEDBACK,
ITEM_TOP_ZONE_NAME, ITEM_MIDDLE_ZONE_NAME, ITEM_BOTTOM_ZONE_NAME,
ITEM_ANY_ZONE_NAME, ITEM_NO_ZONE_NAME,
)
# Globals ###########################################################
......@@ -29,13 +32,18 @@ loader = ResourceLoader(__name__)
# Classes ###########################################################
class ItemDefinition(object):
def __init__(self, item_id, zone_ids, zone_title, feedback_positive, feedback_negative):
self.feedback_negative = feedback_negative
self.feedback_positive = feedback_positive
self.zone_ids = zone_ids
self.zone_title = zone_title
self.item_id = item_id
ItemDefinition = namedtuple( # pylint: disable=invalid-name
"ItemDefinition",
[
"item_id",
"item_name",
"image_url",
"zone_ids",
"zone_title",
"feedback_positive",
"feedback_negative",
]
)
class BaseIntegrationTest(SeleniumBaseTest):
......@@ -180,22 +188,22 @@ class DefaultDataTestMixin(object):
items_map = {
0: ItemDefinition(
0, [TOP_ZONE_ID], TOP_ZONE_TITLE,
0, ITEM_TOP_ZONE_NAME, "", [TOP_ZONE_ID], TOP_ZONE_TITLE,
ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE), ITEM_INCORRECT_FEEDBACK
),
1: ItemDefinition(
1, [MIDDLE_ZONE_ID], MIDDLE_ZONE_TITLE,
1, ITEM_MIDDLE_ZONE_NAME, "", [MIDDLE_ZONE_ID], MIDDLE_ZONE_TITLE,
ITEM_CORRECT_FEEDBACK.format(zone=MIDDLE_ZONE_TITLE), ITEM_INCORRECT_FEEDBACK
),
2: ItemDefinition(
2, [BOTTOM_ZONE_ID], BOTTOM_ZONE_TITLE,
2, ITEM_BOTTOM_ZONE_NAME, "", [BOTTOM_ZONE_ID], BOTTOM_ZONE_TITLE,
ITEM_CORRECT_FEEDBACK.format(zone=BOTTOM_ZONE_TITLE), ITEM_INCORRECT_FEEDBACK
),
3: ItemDefinition(
3, [MIDDLE_ZONE_ID, TOP_ZONE_ID, BOTTOM_ZONE_ID], MIDDLE_ZONE_TITLE,
3, ITEM_ANY_ZONE_NAME, "", [MIDDLE_ZONE_ID, TOP_ZONE_ID, BOTTOM_ZONE_ID], MIDDLE_ZONE_TITLE,
ITEM_ANY_ZONE_FEEDBACK, ITEM_INCORRECT_FEEDBACK
),
4: ItemDefinition(4, [], None, "", ITEM_NO_ZONE_FEEDBACK),
4: ItemDefinition(4, ITEM_NO_ZONE_NAME, "", [], None, "", ITEM_NO_ZONE_FEEDBACK),
}
all_zones = [
......
from ddt import data, ddt, unpack
from mock import Mock, patch
from selenium.webdriver.common.keys import Keys
from workbench.runtime import WorkbenchRuntime
from drag_and_drop_v2.default_data import (
TOP_ZONE_TITLE, TOP_ZONE_ID, MIDDLE_ZONE_TITLE, MIDDLE_ZONE_ID, ITEM_CORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK
TOP_ZONE_TITLE, TOP_ZONE_ID, MIDDLE_ZONE_TITLE, MIDDLE_ZONE_ID, ITEM_CORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK,
ITEM_TOP_ZONE_NAME, ITEM_MIDDLE_ZONE_NAME,
)
from tests.integration.test_base import BaseIntegrationTest, DefaultDataTestMixin, InteractionTestBase
from tests.integration.test_base import BaseIntegrationTest, DefaultDataTestMixin, InteractionTestBase, ItemDefinition
from tests.integration.test_interaction import DefaultDataTestMixin, ParameterizedTestsMixin
from tests.integration.test_interaction_assessment import DefaultAssessmentDataTestMixin, AssessmentTestMixin
......@@ -44,6 +46,7 @@ class EventsFiredTest(DefaultDataTestMixin, ParameterizedTestsMixin, BaseEventsT
'name': 'edx.drag_and_drop_v2.item.dropped',
'data': {
'is_correct': True,
'item': ITEM_TOP_ZONE_NAME,
'item_id': 0,
'location': TOP_ZONE_TITLE,
'location_id': TOP_ZONE_ID,
......@@ -95,6 +98,7 @@ class AssessmentEventsFiredTest(
'name': 'edx.drag_and_drop_v2.item.dropped',
'data': {
'is_correct': False,
'item': ITEM_TOP_ZONE_NAME,
'item_id': 0,
'location': MIDDLE_ZONE_TITLE,
'location_id': MIDDLE_ZONE_ID,
......@@ -108,6 +112,7 @@ class AssessmentEventsFiredTest(
'name': 'edx.drag_and_drop_v2.item.dropped',
'data': {
'is_correct': False,
'item': ITEM_MIDDLE_ZONE_NAME,
'item_id': 1,
'location': TOP_ZONE_TITLE,
'location_id': TOP_ZONE_ID,
......@@ -140,3 +145,70 @@ class AssessmentEventsFiredTest(
dummy, name, published_data = self.publish.call_args_list[index][0]
self.assertEqual(name, event['name'])
self.assertEqual(published_data, event['data'])
@ddt
class ItemDroppedEventTest(DefaultDataTestMixin, BaseEventsTests):
"""
Test that the item.dropped event behaves properly.
"""
items_map = {
0: ItemDefinition(0, "Has name", "", 'zone-1', "Zone 1", "Yes", "No"),
1: ItemDefinition(1, "", "https://placehold.it/100x100", 'zone-2', "Zone 2", "Yes", "No"),
}
scenarios = (
(
['zone-1', 'zone-2'],
[
{
'is_correct': True,
'item': "Has name",
'item_id': 0,
'location': 'Zone 1',
'location_id': 'zone-1'
},
{
'is_correct': True,
'item': "https://placehold.it/100x100",
'item_id': 1,
'location': 'Zone 2',
'location_id': 'zone-2'
}
],
),
(
['zone-2', 'zone-1'],
[
{
'is_correct': False,
'item': "Has name",
'item_id': 0,
'location': 'Zone 2',
'location_id': 'zone-2'
},
{
'is_correct': False,
'item': "https://placehold.it/100x100",
'item_id': 1,
'location': 'Zone 1',
'location_id': 'zone-1'
}
],
),
)
def _get_scenario_xml(self):
return self._get_custom_scenario_xml("data/test_item_dropped.json")
@data(*scenarios) # pylint: disable=star-args
@unpack
def test_item_dropped_event(self, placement, expected_events):
for i, zone in enumerate(placement):
self.place_item(i, zone, Keys.RETURN)
events = self.publish.call_args_list
event_name = 'edx.drag_and_drop_v2.item.dropped'
published_events = [event[0][2] for event in events if event[0][1] == event_name]
self.assertEqual(published_events, expected_events)
......@@ -275,7 +275,15 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, Paramet
class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest):
items_map = {
0: ItemDefinition(0, ['zone-1', 'zone-2'], ["Zone 1", "Zone 2"], ["Yes 1", "Yes 1"], ["No 1", "No 1"]),
0: ItemDefinition(
0,
"Item",
"",
['zone-1', 'zone-2'],
["Zone 1", "Zone 2"],
["Yes 1", "Yes 1"],
["No 1", "No 1"]
),
}
def test_multiple_positive_feedback(self):
......@@ -334,9 +342,9 @@ class PreventSpaceBarScrollTest(DefaultDataTestMixin, InteractionTestBase, BaseI
class CustomDataInteractionTest(StandardInteractionTest):
items_map = {
0: ItemDefinition(0, ['zone-1'], "Zone 1", "Yes 1", "No 1"),
1: ItemDefinition(1, ['zone-2'], "Zone 2", "Yes 2", "No 2"),
2: ItemDefinition(2, [], None, "", "No Zone for this")
0: ItemDefinition(0, "Item 0", "", ['zone-1'], "Zone 1", "Yes 1", "No 1"),
1: ItemDefinition(1, "Item 1", "", ['zone-2'], "Zone 2", "Yes 2", "No 2"),
2: ItemDefinition(2, "Item 2", "", [], None, "", "No Zone for this")
}
all_zones = [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')]
......@@ -352,9 +360,9 @@ class CustomDataInteractionTest(StandardInteractionTest):
class CustomHtmlDataInteractionTest(StandardInteractionTest):
items_map = {
0: ItemDefinition(0, ['zone-1'], 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"),
1: ItemDefinition(1, ['zone-2'], 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>"),
2: ItemDefinition(2, [], None, "", "No Zone for <i>X</i>")
0: ItemDefinition(0, "Item 0", "", ['zone-1'], 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"),
1: ItemDefinition(1, "Item 1", "", ['zone-2'], 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>"),
2: ItemDefinition(2, "Item 2", "", [], None, "", "No Zone for <i>X</i>")
}
all_zones = [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')]
......@@ -377,14 +385,14 @@ class MultipleBlocksDataInteraction(ParameterizedTestsMixin, InteractionTestBase
item_maps = {
'block1': {
0: ItemDefinition(0, ['zone-1'], 'Zone 1', "Yes 1", "No 1"),
1: ItemDefinition(1, ['zone-2'], 'Zone 2', "Yes 2", "No 2"),
2: ItemDefinition(2, [], None, "", "No Zone for this")
0: ItemDefinition(0, "Item 0", "", ['zone-1'], 'Zone 1', "Yes 1", "No 1"),
1: ItemDefinition(1, "Item 1", "", ['zone-2'], 'Zone 2', "Yes 2", "No 2"),
2: ItemDefinition(2, "Item 2", "", [], None, "", "No Zone for this")
},
'block2': {
10: ItemDefinition(10, ['zone-51'], 'Zone 51', "Correct 1", "Incorrect 1"),
20: ItemDefinition(20, ['zone-52'], 'Zone 52', "Correct 2", "Incorrect 2"),
30: ItemDefinition(30, [], None, "", "No Zone for this")
10: ItemDefinition(10, "Item 10", "", ['zone-51'], 'Zone 51', "Correct 1", "Incorrect 1"),
20: ItemDefinition(20, "Item 20", "", ['zone-52'], 'Zone 52', "Correct 2", "Incorrect 2"),
30: ItemDefinition(30, "Item 30", "", [], None, "", "No Zone for this")
},
}
......
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