Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
xblock-drag-and-drop-v2
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
xblock-drag-and-drop-v2
Commits
e2600bc5
Commit
e2600bc5
authored
Aug 26, 2016
by
Matjaz Gregoric
Committed by
GitHub
Aug 26, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #96 from arbrandes/SOL-1988
[SOL-1988] Account for decoy items in score
parents
6b6574ee
88f08735
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
305 additions
and
203 deletions
+305
-203
README.md
+27
-2
drag_and_drop_v2/drag_and_drop_v2.py
+27
-19
drag_and_drop_v2/utils.py
+4
-0
tests/integration/test_interaction.py
+1
-177
tests/integration/test_interaction_assessment.py
+219
-0
tests/unit/test_advanced.py
+27
-5
No files found.
README.md
View file @
e2600bc5
...
@@ -151,6 +151,32 @@ You can leave all of the checkboxes unchecked in order to create a
...
@@ -151,6 +151,32 @@ You can leave all of the checkboxes unchecked in order to create a
You can define an arbitrary number of drag items, each of which may
You can define an arbitrary number of drag items, each of which may
be attached to any number of zones.
be attached to any number of zones.
Scoring
-------
Student assessment scores for the Drag and Drop XBlock are calculated according
to the following formula:
score = (C + D) / T
Where
*C*
is the number of correctly placed regular items,
*D*
is the number of
decoy items that were correctly left unplaced, and
*T*
is the total number of
items available.
Example: consider a Drag and Drop instance configured with a total of four
items, of which three are regular items and one is a decoy. If a learner
places two of the normal items correctly and one incorrectly (
`C = 2`
), and
wrongly places the decoy item onto a drop zone (
`D = 0`
), that learner's score
will be
`50%`
, as given by:
score = (2 + 0) / 4
If the learner were to then move the decoy item back to the bank (
`D = 1`
) and
move the wrongly placed regular item to the correct dropzone (
`C = 3`
), their
score would be
`100%`
:
score = (3 + 1) / 4
Demo Course
Demo Course
-----------
-----------
...
@@ -514,4 +540,4 @@ Then start python interpreter, import `Dummy` translator and follow instructions
...
@@ -514,4 +540,4 @@ Then start python interpreter, import `Dummy` translator and follow instructions
>>> conv = Dummy()
>>> conv = Dummy()
>>> print conv.convert("String to translate")
>>> print conv.convert("String to translate")
Then copy output and paste it into
`translations/eo/LC_MESSAGES/text.po`
.
Then copy output and paste it into
`translations/eo/LC_MESSAGES/text.po`
.
\ No newline at end of file
drag_and_drop_v2/drag_and_drop_v2.py
View file @
e2600bc5
...
@@ -16,7 +16,7 @@ from xblock.fragment import Fragment
...
@@ -16,7 +16,7 @@ from xblock.fragment import Fragment
from
xblockutils.resources
import
ResourceLoader
from
xblockutils.resources
import
ResourceLoader
from
xblockutils.settings
import
XBlockWithSettingsMixin
,
ThemableXBlockMixin
from
xblockutils.settings
import
XBlockWithSettingsMixin
,
ThemableXBlockMixin
from
.utils
import
_
,
DummyTranslationService
,
FeedbackMessage
,
FeedbackMessages
from
.utils
import
_
,
DummyTranslationService
,
FeedbackMessage
,
FeedbackMessages
,
ItemStats
from
.default_data
import
DEFAULT_DATA
from
.default_data
import
DEFAULT_DATA
...
@@ -456,9 +456,9 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
...
@@ -456,9 +456,9 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
feedback_key
=
'finish'
if
is_correct
else
'start'
feedback_key
=
'finish'
if
is_correct
else
'start'
return
[
FeedbackMessage
(
self
.
data
[
'feedback'
][
feedback_key
],
None
)],
set
()
return
[
FeedbackMessage
(
self
.
data
[
'feedback'
][
feedback_key
],
None
)],
set
()
required_ids
,
placed_ids
,
correct_id
s
=
self
.
_get_item_raw_stats
()
item
s
=
self
.
_get_item_raw_stats
()
missing_ids
=
required_ids
-
placed_ids
missing_ids
=
items
.
required
-
items
.
placed
misplaced_ids
=
placed_ids
-
correct_ids
misplaced_ids
=
items
.
placed
-
items
.
correctly_placed
feedback_msgs
=
[]
feedback_msgs
=
[]
...
@@ -469,7 +469,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
...
@@ -469,7 +469,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
feedback_msgs
.
append
(
FeedbackMessage
(
message
,
message_class
))
feedback_msgs
.
append
(
FeedbackMessage
(
message
,
message_class
))
_add_msg_if_exists
(
_add_msg_if_exists
(
correct_ids
,
FeedbackMessages
.
correctly_placed
,
FeedbackMessages
.
MessageClasses
.
CORRECTLY_PLACED
items
.
correctly_placed
,
FeedbackMessages
.
correctly_placed
,
FeedbackMessages
.
MessageClasses
.
CORRECTLY_PLACED
)
)
_add_msg_if_exists
(
misplaced_ids
,
FeedbackMessages
.
misplaced
,
FeedbackMessages
.
MessageClasses
.
MISPLACED
)
_add_msg_if_exists
(
misplaced_ids
,
FeedbackMessages
.
misplaced
,
FeedbackMessages
.
MessageClasses
.
MISPLACED
)
_add_msg_if_exists
(
missing_ids
,
FeedbackMessages
.
not_placed
,
FeedbackMessages
.
MessageClasses
.
NOT_PLACED
)
_add_msg_if_exists
(
missing_ids
,
FeedbackMessages
.
not_placed
,
FeedbackMessages
.
MessageClasses
.
NOT_PLACED
)
...
@@ -740,31 +740,39 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
...
@@ -740,31 +740,39 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
def
_get_item_stats
(
self
):
def
_get_item_stats
(
self
):
"""
"""
Returns a tuple representing the number of correctly
-
placed items,
Returns a tuple representing the number of correctly
placed items,
and the total number of items
that must be placed on the board (non-
decoy items).
and the total number of items
required (including
decoy items).
"""
"""
required_items
,
__
,
correct_
items
=
self
.
_get_item_raw_stats
()
items
=
self
.
_get_item_raw_stats
()
return
len
(
correct_items
),
len
(
required_items
)
correct_count
=
len
(
items
.
correctly_placed
)
+
len
(
items
.
decoy_in_bank
)
total_count
=
len
(
items
.
required
)
+
len
(
items
.
decoy
)
return
correct_count
,
total_count
def
_get_item_raw_stats
(
self
):
def
_get_item_raw_stats
(
self
):
"""
"""
Returns a 3-tuple containing required, placed and correct items.
Returns a named tuple containing required, decoy, placed, correctly
placed, and correctly unplaced decoy items.
Returns:
Returns:
tuple: (required_items, placed_items, correct_items)
namedtuple: (required, placed, correctly_placed, decoy, decoy_in_bank)
* required_items - IDs of items that must be placed on the board
* required - IDs of items that must be placed on the board
* placed_items - IDs of items actually placed on the board
* placed - IDs of items actually placed on the board
* correct_items - IDs of items that were placed correctly
* correctly_placed - IDs of items that were placed correctly
* decoy - IDs of decoy items
* decoy_in_bank - IDs of decoy items that were unplaced
"""
"""
all_items
=
[
str
(
item
[
'id'
])
for
item
in
self
.
data
[
'items'
]]
item_state
=
self
.
_get_item_state
()
item_state
=
self
.
_get_item_state
()
required_items
=
set
(
item_id
for
item_id
in
all_items
if
self
.
_get_item_zones
(
int
(
item_id
))
!=
[])
all_items
=
set
(
str
(
item
[
'id'
])
for
item
in
self
.
data
[
'items'
])
placed_items
=
set
(
item_id
for
item_id
in
all_items
if
item_id
in
item_state
)
required
=
set
(
item_id
for
item_id
in
all_items
if
self
.
_get_item_zones
(
int
(
item_id
))
!=
[])
correct_items
=
set
(
item_id
for
item_id
in
placed_items
if
item_state
[
item_id
][
'correct'
])
placed
=
set
(
item_id
for
item_id
in
all_items
if
item_id
in
item_state
)
correctly_placed
=
set
(
item_id
for
item_id
in
placed
if
item_state
[
item_id
][
'correct'
])
decoy
=
all_items
-
required
decoy_in_bank
=
set
(
item_id
for
item_id
in
decoy
if
item_id
not
in
item_state
)
return
required_items
,
placed_items
,
correct_items
return
ItemStats
(
required
,
placed
,
correctly_placed
,
decoy
,
decoy_in_bank
)
def
_get_grade
(
self
):
def
_get_grade
(
self
):
"""
"""
...
...
drag_and_drop_v2/utils.py
View file @
e2600bc5
...
@@ -78,3 +78,7 @@ class FeedbackMessages(object):
...
@@ -78,3 +78,7 @@ class FeedbackMessages(object):
FeedbackMessage
=
namedtuple
(
"FeedbackMessage"
,
[
"message"
,
"message_class"
])
# pylint: disable=invalid-name
FeedbackMessage
=
namedtuple
(
"FeedbackMessage"
,
[
"message"
,
"message_class"
])
# pylint: disable=invalid-name
ItemStats
=
namedtuple
(
# pylint: disable=invalid-name
'ItemStats'
,
[
"required"
,
"placed"
,
"correctly_placed"
,
"decoy"
,
"decoy_in_bank"
]
)
tests/integration/test_interaction.py
View file @
e2600bc5
...
@@ -20,7 +20,6 @@ from drag_and_drop_v2.default_data import (
...
@@ -20,7 +20,6 @@ from drag_and_drop_v2.default_data import (
ITEM_CORRECT_FEEDBACK
,
ITEM_INCORRECT_FEEDBACK
,
ITEM_NO_ZONE_FEEDBACK
,
ITEM_CORRECT_FEEDBACK
,
ITEM_INCORRECT_FEEDBACK
,
ITEM_NO_ZONE_FEEDBACK
,
ITEM_ANY_ZONE_FEEDBACK
,
START_FEEDBACK
,
FINISH_FEEDBACK
ITEM_ANY_ZONE_FEEDBACK
,
START_FEEDBACK
,
FINISH_FEEDBACK
)
)
from
drag_and_drop_v2.utils
import
FeedbackMessages
from
.test_base
import
BaseIntegrationTest
from
.test_base
import
BaseIntegrationTest
...
@@ -478,36 +477,6 @@ class DefaultDataTestMixin(object):
...
@@ -478,36 +477,6 @@ class DefaultDataTestMixin(object):
return
"<vertical_demo><drag-and-drop-v2/></vertical_demo>"
return
"<vertical_demo><drag-and-drop-v2/></vertical_demo>"
class
DefaultAssessmentDataTestMixin
(
DefaultDataTestMixin
):
"""
Provides a test scenario with default options in assessment mode.
"""
MAX_ATTEMPTS
=
5
def
_get_scenario_xml
(
self
):
# pylint: disable=no-self-use
return
"""
<vertical_demo><drag-and-drop-v2 mode='assessment' max_attempts='{max_attempts}'/></vertical_demo>
"""
.
format
(
max_attempts
=
self
.
MAX_ATTEMPTS
)
class
AssessmentTestMixin
(
object
):
"""
Provides helper methods for assessment tests
"""
@staticmethod
def
_wait_until_enabled
(
element
):
wait
=
WebDriverWait
(
element
,
2
)
wait
.
until
(
lambda
e
:
e
.
is_displayed
()
and
e
.
get_attribute
(
'disabled'
)
is
None
)
def
click_submit
(
self
):
submit_button
=
self
.
_get_submit_button
()
self
.
_wait_until_enabled
(
submit_button
)
submit_button
.
click
()
self
.
wait_for_ajax
()
@ddt
@ddt
class
StandardInteractionTest
(
DefaultDataTestMixin
,
InteractionTestBase
,
BaseIntegrationTest
):
class
StandardInteractionTest
(
DefaultDataTestMixin
,
InteractionTestBase
,
BaseIntegrationTest
):
"""
"""
...
@@ -577,151 +546,6 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseInt
...
@@ -577,151 +546,6 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseInt
self
.
interact_with_keyboard_help
(
use_keyboard
=
use_keyboard
)
self
.
interact_with_keyboard_help
(
use_keyboard
=
use_keyboard
)
@ddt
class
AssessmentInteractionTest
(
DefaultAssessmentDataTestMixin
,
AssessmentTestMixin
,
InteractionTestBase
,
BaseIntegrationTest
):
"""
Testing interactions with Drag and Drop XBlock against default data in assessment mode.
All interactions are tested using mouse (action_key=None) and four different keyboard action keys.
If default data changes this will break.
"""
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_item_no_feedback_on_good_move
(
self
,
action_key
):
self
.
parameterized_item_positive_feedback_on_good_move
(
self
.
items_map
,
action_key
=
action_key
,
assessment_mode
=
True
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_item_no_feedback_on_bad_move
(
self
,
action_key
):
self
.
parameterized_item_negative_feedback_on_bad_move
(
self
.
items_map
,
self
.
all_zones
,
action_key
=
action_key
,
assessment_mode
=
True
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_move_items_between_zones
(
self
,
action_key
):
self
.
parameterized_move_items_between_zones
(
self
.
items_map
,
self
.
all_zones
,
action_key
=
action_key
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_final_feedback_and_reset
(
self
,
action_key
):
self
.
parameterized_final_feedback_and_reset
(
self
.
items_map
,
self
.
feedback
,
action_key
=
action_key
,
assessment_mode
=
True
)
@data
(
False
,
True
)
def
test_keyboard_help
(
self
,
use_keyboard
):
self
.
interact_with_keyboard_help
(
use_keyboard
=
use_keyboard
)
def
test_submit_button_shown
(
self
):
first_item_definition
=
self
.
_get_items_with_zone
(
self
.
items_map
)
.
values
()[
0
]
submit_button
=
self
.
_get_submit_button
()
self
.
assertTrue
(
submit_button
.
is_displayed
())
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
'true'
)
# no items are placed
attempts_info
=
self
.
_get_attempts_info
()
expected_text
=
"You have used {num} of {max} attempts."
.
format
(
num
=
0
,
max
=
self
.
MAX_ATTEMPTS
)
self
.
assertEqual
(
attempts_info
.
text
,
expected_text
)
self
.
assertEqual
(
attempts_info
.
is_displayed
(),
self
.
MAX_ATTEMPTS
>
0
)
self
.
place_item
(
first_item_definition
.
item_id
,
first_item_definition
.
zone_ids
[
0
],
None
)
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
None
)
def
test_misplaced_items_returned_to_bank
(
self
):
"""
Test items placed to incorrect zones are returned to item bank after submitting solution
"""
correct_items
=
{
0
:
TOP_ZONE_ID
}
misplaced_items
=
{
1
:
BOTTOM_ZONE_ID
,
2
:
MIDDLE_ZONE_ID
}
for
item_id
,
zone_id
in
correct_items
.
iteritems
():
self
.
place_item
(
item_id
,
zone_id
)
for
item_id
,
zone_id
in
misplaced_items
.
iteritems
():
self
.
place_item
(
item_id
,
zone_id
)
self
.
click_submit
()
for
item_id
in
correct_items
:
self
.
assert_placed_item
(
item_id
,
TOP_ZONE_TITLE
,
assessment_mode
=
True
)
for
item_id
in
misplaced_items
:
self
.
assert_reverted_item
(
item_id
)
def
test_max_attempts_reached_submit_and_reset_disabled
(
self
):
"""
Test "Submit" and "Reset" buttons are disabled when no more attempts remaining
"""
self
.
place_item
(
0
,
TOP_ZONE_ID
)
submit_button
,
reset_button
=
self
.
_get_submit_button
(),
self
.
_get_reset_button
()
attempts_info
=
self
.
_get_attempts_info
()
for
index
in
xrange
(
self
.
MAX_ATTEMPTS
):
expected_text
=
"You have used {num} of {max} attempts."
.
format
(
num
=
index
,
max
=
self
.
MAX_ATTEMPTS
)
self
.
assertEqual
(
attempts_info
.
text
,
expected_text
)
# precondition check
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
None
)
self
.
assertEqual
(
reset_button
.
get_attribute
(
'disabled'
),
None
)
self
.
click_submit
()
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
'true'
)
self
.
assertEqual
(
reset_button
.
get_attribute
(
'disabled'
),
'true'
)
def
test_do_attempt_feedback_is_updated
(
self
):
"""
Test updating overall feedback after submitting solution in assessment mode
"""
# used keyboard mode to avoid bug/feature with selenium "selecting" everything instead of dragging an element
self
.
place_item
(
0
,
TOP_ZONE_ID
,
Keys
.
RETURN
)
self
.
click_submit
()
feedback_lines
=
[
"FEEDBACK"
,
FeedbackMessages
.
correctly_placed
(
1
),
FeedbackMessages
.
not_placed
(
3
),
START_FEEDBACK
]
expected_feedback
=
"
\n
"
.
join
(
feedback_lines
)
self
.
assertEqual
(
self
.
_get_feedback
()
.
text
,
expected_feedback
)
self
.
place_item
(
1
,
BOTTOM_ZONE_ID
,
Keys
.
RETURN
)
self
.
click_submit
()
feedback_lines
=
[
"FEEDBACK"
,
FeedbackMessages
.
correctly_placed
(
1
),
FeedbackMessages
.
misplaced
(
1
),
FeedbackMessages
.
not_placed
(
2
),
FeedbackMessages
.
MISPLACED_ITEMS_RETURNED
,
START_FEEDBACK
]
expected_feedback
=
"
\n
"
.
join
(
feedback_lines
)
self
.
assertEqual
(
self
.
_get_feedback
()
.
text
,
expected_feedback
)
# reach final attempt
for
_
in
xrange
(
self
.
MAX_ATTEMPTS
-
3
):
self
.
click_submit
()
self
.
place_item
(
1
,
MIDDLE_ZONE_ID
,
Keys
.
RETURN
)
self
.
place_item
(
2
,
BOTTOM_ZONE_ID
,
Keys
.
RETURN
)
self
.
place_item
(
3
,
TOP_ZONE_ID
,
Keys
.
RETURN
)
self
.
click_submit
()
feedback_lines
=
[
"FEEDBACK"
,
FeedbackMessages
.
correctly_placed
(
4
),
FINISH_FEEDBACK
,
FeedbackMessages
.
FINAL_ATTEMPT_TPL
.
format
(
score
=
1.0
)
]
expected_feedback
=
"
\n
"
.
join
(
feedback_lines
)
self
.
assertEqual
(
self
.
_get_feedback
()
.
text
,
expected_feedback
)
class
MultipleValidOptionsInteractionTest
(
DefaultDataTestMixin
,
InteractionTestBase
,
BaseIntegrationTest
):
class
MultipleValidOptionsInteractionTest
(
DefaultDataTestMixin
,
InteractionTestBase
,
BaseIntegrationTest
):
items_map
=
{
items_map
=
{
...
@@ -764,7 +588,7 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration
...
@@ -764,7 +588,7 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration
},
},
{
{
'name'
:
'grade'
,
'name'
:
'grade'
,
'data'
:
{
'max_value'
:
1
,
'value'
:
(
1.0
/
4
)},
'data'
:
{
'max_value'
:
1
,
'value'
:
(
2.0
/
5
)},
},
},
{
{
'name'
:
'edx.drag_and_drop_v2.item.dropped'
,
'name'
:
'edx.drag_and_drop_v2.item.dropped'
,
...
...
tests/integration/test_interaction_assessment.py
0 → 100644
View file @
e2600bc5
# Imports ###########################################################
from
ddt
import
ddt
,
data
from
mock
import
Mock
,
patch
from
selenium.webdriver.support.ui
import
WebDriverWait
from
selenium.webdriver.common.keys
import
Keys
from
workbench.runtime
import
WorkbenchRuntime
from
xblockutils.resources
import
ResourceLoader
from
drag_and_drop_v2.default_data
import
(
TOP_ZONE_ID
,
MIDDLE_ZONE_ID
,
BOTTOM_ZONE_ID
,
TOP_ZONE_TITLE
,
START_FEEDBACK
,
FINISH_FEEDBACK
)
from
drag_and_drop_v2.utils
import
FeedbackMessages
from
.test_base
import
BaseIntegrationTest
from
.test_interaction
import
InteractionTestBase
,
DefaultDataTestMixin
# Globals ###########################################################
loader
=
ResourceLoader
(
__name__
)
# Classes ###########################################################
class
DefaultAssessmentDataTestMixin
(
DefaultDataTestMixin
):
"""
Provides a test scenario with default options in assessment mode.
"""
MAX_ATTEMPTS
=
5
def
_get_scenario_xml
(
self
):
# pylint: disable=no-self-use
return
"""
<vertical_demo><drag-and-drop-v2 mode='assessment' max_attempts='{max_attempts}'/></vertical_demo>
"""
.
format
(
max_attempts
=
self
.
MAX_ATTEMPTS
)
class
AssessmentTestMixin
(
object
):
"""
Provides helper methods for assessment tests
"""
@staticmethod
def
_wait_until_enabled
(
element
):
wait
=
WebDriverWait
(
element
,
2
)
wait
.
until
(
lambda
e
:
e
.
is_displayed
()
and
e
.
get_attribute
(
'disabled'
)
is
None
)
def
click_submit
(
self
):
submit_button
=
self
.
_get_submit_button
()
self
.
_wait_until_enabled
(
submit_button
)
submit_button
.
click
()
self
.
wait_for_ajax
()
@ddt
class
AssessmentInteractionTest
(
DefaultAssessmentDataTestMixin
,
AssessmentTestMixin
,
InteractionTestBase
,
BaseIntegrationTest
):
"""
Testing interactions with Drag and Drop XBlock against default data in assessment mode.
All interactions are tested using mouse (action_key=None) and four different keyboard action keys.
If default data changes this will break.
"""
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_item_no_feedback_on_good_move
(
self
,
action_key
):
self
.
parameterized_item_positive_feedback_on_good_move
(
self
.
items_map
,
action_key
=
action_key
,
assessment_mode
=
True
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_item_no_feedback_on_bad_move
(
self
,
action_key
):
self
.
parameterized_item_negative_feedback_on_bad_move
(
self
.
items_map
,
self
.
all_zones
,
action_key
=
action_key
,
assessment_mode
=
True
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_move_items_between_zones
(
self
,
action_key
):
self
.
parameterized_move_items_between_zones
(
self
.
items_map
,
self
.
all_zones
,
action_key
=
action_key
)
@data
(
None
,
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_final_feedback_and_reset
(
self
,
action_key
):
self
.
parameterized_final_feedback_and_reset
(
self
.
items_map
,
self
.
feedback
,
action_key
=
action_key
,
assessment_mode
=
True
)
@data
(
False
,
True
)
def
test_keyboard_help
(
self
,
use_keyboard
):
self
.
interact_with_keyboard_help
(
use_keyboard
=
use_keyboard
)
def
test_submit_button_shown
(
self
):
first_item_definition
=
self
.
_get_items_with_zone
(
self
.
items_map
)
.
values
()[
0
]
submit_button
=
self
.
_get_submit_button
()
self
.
assertTrue
(
submit_button
.
is_displayed
())
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
'true'
)
# no items are placed
attempts_info
=
self
.
_get_attempts_info
()
expected_text
=
"You have used {num} of {max} attempts."
.
format
(
num
=
0
,
max
=
self
.
MAX_ATTEMPTS
)
self
.
assertEqual
(
attempts_info
.
text
,
expected_text
)
self
.
assertEqual
(
attempts_info
.
is_displayed
(),
self
.
MAX_ATTEMPTS
>
0
)
self
.
place_item
(
first_item_definition
.
item_id
,
first_item_definition
.
zone_ids
[
0
],
None
)
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
None
)
def
test_misplaced_items_returned_to_bank
(
self
):
"""
Test items placed to incorrect zones are returned to item bank after submitting solution
"""
correct_items
=
{
0
:
TOP_ZONE_ID
}
misplaced_items
=
{
1
:
BOTTOM_ZONE_ID
,
2
:
MIDDLE_ZONE_ID
}
for
item_id
,
zone_id
in
correct_items
.
iteritems
():
self
.
place_item
(
item_id
,
zone_id
)
for
item_id
,
zone_id
in
misplaced_items
.
iteritems
():
self
.
place_item
(
item_id
,
zone_id
)
self
.
click_submit
()
for
item_id
in
correct_items
:
self
.
assert_placed_item
(
item_id
,
TOP_ZONE_TITLE
,
assessment_mode
=
True
)
for
item_id
in
misplaced_items
:
self
.
assert_reverted_item
(
item_id
)
def
test_max_attempts_reached_submit_and_reset_disabled
(
self
):
"""
Test "Submit" and "Reset" buttons are disabled when no more attempts remaining
"""
self
.
place_item
(
0
,
TOP_ZONE_ID
)
submit_button
,
reset_button
=
self
.
_get_submit_button
(),
self
.
_get_reset_button
()
attempts_info
=
self
.
_get_attempts_info
()
for
index
in
xrange
(
self
.
MAX_ATTEMPTS
):
expected_text
=
"You have used {num} of {max} attempts."
.
format
(
num
=
index
,
max
=
self
.
MAX_ATTEMPTS
)
self
.
assertEqual
(
attempts_info
.
text
,
expected_text
)
# precondition check
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
None
)
self
.
assertEqual
(
reset_button
.
get_attribute
(
'disabled'
),
None
)
self
.
click_submit
()
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
'true'
)
self
.
assertEqual
(
reset_button
.
get_attribute
(
'disabled'
),
'true'
)
def
test_do_attempt_feedback_is_updated
(
self
):
"""
Test updating overall feedback after submitting solution in assessment mode
"""
# used keyboard mode to avoid bug/feature with selenium "selecting" everything instead of dragging an element
self
.
place_item
(
0
,
TOP_ZONE_ID
,
Keys
.
RETURN
)
self
.
click_submit
()
feedback_lines
=
[
"FEEDBACK"
,
FeedbackMessages
.
correctly_placed
(
1
),
FeedbackMessages
.
not_placed
(
3
),
START_FEEDBACK
]
expected_feedback
=
"
\n
"
.
join
(
feedback_lines
)
self
.
assertEqual
(
self
.
_get_feedback
()
.
text
,
expected_feedback
)
self
.
place_item
(
1
,
BOTTOM_ZONE_ID
,
Keys
.
RETURN
)
self
.
click_submit
()
feedback_lines
=
[
"FEEDBACK"
,
FeedbackMessages
.
correctly_placed
(
1
),
FeedbackMessages
.
misplaced
(
1
),
FeedbackMessages
.
not_placed
(
2
),
FeedbackMessages
.
MISPLACED_ITEMS_RETURNED
,
START_FEEDBACK
]
expected_feedback
=
"
\n
"
.
join
(
feedback_lines
)
self
.
assertEqual
(
self
.
_get_feedback
()
.
text
,
expected_feedback
)
# reach final attempt
for
_
in
xrange
(
self
.
MAX_ATTEMPTS
-
3
):
self
.
click_submit
()
self
.
place_item
(
1
,
MIDDLE_ZONE_ID
,
Keys
.
RETURN
)
self
.
place_item
(
2
,
BOTTOM_ZONE_ID
,
Keys
.
RETURN
)
self
.
place_item
(
3
,
TOP_ZONE_ID
,
Keys
.
RETURN
)
self
.
click_submit
()
feedback_lines
=
[
"FEEDBACK"
,
FeedbackMessages
.
correctly_placed
(
4
),
FINISH_FEEDBACK
,
FeedbackMessages
.
FINAL_ATTEMPT_TPL
.
format
(
score
=
1.0
)
]
expected_feedback
=
"
\n
"
.
join
(
feedback_lines
)
self
.
assertEqual
(
self
.
_get_feedback
()
.
text
,
expected_feedback
)
def
test_grade
(
self
):
"""
Test grading after submitting solution in assessment mode
"""
mock
=
Mock
()
context
=
patch
.
object
(
WorkbenchRuntime
,
'publish'
,
mock
)
context
.
start
()
self
.
addCleanup
(
context
.
stop
)
self
.
publish
=
mock
self
.
place_item
(
0
,
TOP_ZONE_ID
,
Keys
.
RETURN
)
# Correctly placed item
self
.
place_item
(
1
,
BOTTOM_ZONE_ID
,
Keys
.
RETURN
)
# Incorrectly placed item
self
.
place_item
(
4
,
MIDDLE_ZONE_ID
,
Keys
.
RETURN
)
# Incorrectly placed decoy
self
.
click_submit
()
events
=
self
.
publish
.
call_args_list
published_grade
=
next
((
event
[
0
][
2
]
for
event
in
events
if
event
[
0
][
1
]
==
'grade'
))
expected_grade
=
{
'max_value'
:
1
,
'value'
:
(
1.0
/
5.0
)}
self
.
assertEqual
(
published_grade
,
expected_grade
)
tests/unit/test_advanced.py
View file @
e2600bc5
...
@@ -119,7 +119,7 @@ class StandardModeFixture(BaseDragAndDropAjaxFixture):
...
@@ -119,7 +119,7 @@ class StandardModeFixture(BaseDragAndDropAjaxFixture):
})
})
self
.
assertEqual
(
1
,
len
(
published_grades
))
self
.
assertEqual
(
1
,
len
(
published_grades
))
self
.
assertEqual
({
'value'
:
0.5
,
'max_value'
:
1
},
published_grades
[
-
1
])
self
.
assertEqual
({
'value'
:
0.
7
5
,
'max_value'
:
1
},
published_grades
[
-
1
])
self
.
call_handler
(
self
.
DROP_ITEM_HANDLER
,
{
self
.
call_handler
(
self
.
DROP_ITEM_HANDLER
,
{
"val"
:
1
,
"zone"
:
self
.
ZONE_2
,
"y_percent"
:
"90
%
"
,
"x_percent"
:
"42
%
"
"val"
:
1
,
"zone"
:
self
.
ZONE_2
,
"y_percent"
:
"90
%
"
,
"x_percent"
:
"42
%
"
...
@@ -446,7 +446,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
...
@@ -446,7 +446,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
def
_submit_partial_solution
(
self
):
def
_submit_partial_solution
(
self
):
self
.
_submit_solution
({
0
:
self
.
ZONE_1
})
self
.
_submit_solution
({
0
:
self
.
ZONE_1
})
return
1.0
/
3
.0
return
3.0
/
5
.0
def
_submit_incorrect_solution
(
self
):
def
_submit_incorrect_solution
(
self
):
self
.
_submit_solution
({
0
:
self
.
ZONE_2
,
1
:
self
.
ZONE_1
})
self
.
_submit_solution
({
0
:
self
.
ZONE_2
,
1
:
self
.
ZONE_1
})
...
@@ -518,9 +518,9 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
...
@@ -518,9 +518,9 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
def
test_do_attempt_keeps_highest_score
(
self
):
def
test_do_attempt_keeps_highest_score
(
self
):
self
.
assertFalse
(
self
.
block
.
completed
)
# precondition check
self
.
assertFalse
(
self
.
block
.
completed
)
# precondition check
expected_score
=
2.0
/
3
.0
expected_score
=
4.0
/
5
.0
self
.
_submit_solution
({
0
:
self
.
ZONE_1
,
1
:
self
.
ZONE_2
})
# partial solution, 0.
66
score
self
.
_submit_solution
({
0
:
self
.
ZONE_1
,
1
:
self
.
ZONE_2
})
# partial solution, 0.
8
score
self
.
call_handler
(
self
.
DO_ATTEMPT_HANDLER
,
data
=
{})
self
.
call_handler
(
self
.
DO_ATTEMPT_HANDLER
,
data
=
{})
self
.
assertEqual
(
self
.
block
.
grade
,
expected_score
)
self
.
assertEqual
(
self
.
block
.
grade
,
expected_score
)
...
@@ -528,7 +528,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
...
@@ -528,7 +528,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
# make it a last attempt so we can check feedback
# make it a last attempt so we can check feedback
self
.
_set_final_attempt
()
self
.
_set_final_attempt
()
self
.
_submit_solution
({
0
:
self
.
ZONE_1
})
# partial solution, 0.
33
score
self
.
_submit_solution
({
0
:
self
.
ZONE_1
})
# partial solution, 0.
6
score
res
=
self
.
call_handler
(
self
.
DO_ATTEMPT_HANDLER
,
data
=
{})
res
=
self
.
call_handler
(
self
.
DO_ATTEMPT_HANDLER
,
data
=
{})
self
.
assertEqual
(
self
.
block
.
grade
,
expected_score
)
self
.
assertEqual
(
self
.
block
.
grade
,
expected_score
)
...
@@ -537,3 +537,25 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
...
@@ -537,3 +537,25 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
FeedbackMessages
.
MessageClasses
.
PARTIAL_SOLUTION
FeedbackMessages
.
MessageClasses
.
PARTIAL_SOLUTION
)
)
self
.
assertIn
(
expected_feedback
,
res
[
self
.
OVERALL_FEEDBACK_KEY
])
self
.
assertIn
(
expected_feedback
,
res
[
self
.
OVERALL_FEEDBACK_KEY
])
def
test_do_attempt_check_score_with_decoy
(
self
):
self
.
assertFalse
(
self
.
block
.
completed
)
# precondition check
expected_score
=
4.0
/
5.0
self
.
_submit_solution
({
0
:
self
.
ZONE_1
,
1
:
self
.
ZONE_2
,
2
:
self
.
ZONE_2
,
3
:
self
.
ZONE_1
,
})
# incorrect solution, 0.8 score
self
.
call_handler
(
self
.
DO_ATTEMPT_HANDLER
,
data
=
{})
self
.
assertEqual
(
self
.
block
.
grade
,
expected_score
)
def
test_do_attempt_zero_score_with_all_decoys
(
self
):
self
.
assertFalse
(
self
.
block
.
completed
)
# precondition check
expected_score
=
0
self
.
_submit_solution
({
3
:
self
.
ZONE_1
,
4
:
self
.
ZONE_2
,
})
# incorrect solution, 0 score
self
.
call_handler
(
self
.
DO_ATTEMPT_HANDLER
,
data
=
{})
self
.
assertEqual
(
self
.
block
.
grade
,
expected_score
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment