Commit d6b36969 by Jillian Vogel Committed by GitHub

Merge pull request #97 from arbrandes/SOL-1805

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