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
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
304 additions
and
201 deletions
+304
-201
README.md
+26
-0
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
You can define an arbitrary number of drag items, each of which may
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
-----------
...
...
drag_and_drop_v2/drag_and_drop_v2.py
View file @
e2600bc5
...
...
@@ -16,7 +16,7 @@ from xblock.fragment import Fragment
from
xblockutils.resources
import
ResourceLoader
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
...
...
@@ -456,9 +456,9 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
feedback_key
=
'finish'
if
is_correct
else
'start'
return
[
FeedbackMessage
(
self
.
data
[
'feedback'
][
feedback_key
],
None
)],
set
()
required_ids
,
placed_ids
,
correct_id
s
=
self
.
_get_item_raw_stats
()
missing_ids
=
required_ids
-
placed_ids
misplaced_ids
=
placed_ids
-
correct_ids
item
s
=
self
.
_get_item_raw_stats
()
missing_ids
=
items
.
required
-
items
.
placed
misplaced_ids
=
items
.
placed
-
items
.
correctly_placed
feedback_msgs
=
[]
...
...
@@ -469,7 +469,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
feedback_msgs
.
append
(
FeedbackMessage
(
message
,
message_class
))
_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
(
missing_ids
,
FeedbackMessages
.
not_placed
,
FeedbackMessages
.
MessageClasses
.
NOT_PLACED
)
...
...
@@ -740,31 +740,39 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
def
_get_item_stats
(
self
):
"""
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).
Returns a tuple representing the number of correctly
placed 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
):
"""
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:
tuple: (required_items, placed_items, correct_items)
* required_items - IDs of items that must be placed on the board
* placed_items - IDs of items actually placed on the board
* correct_items - IDs of items that were placed correctly
namedtuple: (required, placed, correctly_placed, decoy, decoy_in_bank)
* required - IDs of items that must be placed on the board
* placed - IDs of items actually placed on the board
* 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
()
required_items
=
set
(
item_id
for
item_id
in
all_items
if
self
.
_get_item_zones
(
int
(
item_id
))
!=
[])
placed_items
=
set
(
item_id
for
item_id
in
all_items
if
item_id
in
item_state
)
correct_items
=
set
(
item_id
for
item_id
in
placed_items
if
item_state
[
item_id
][
'correct'
])
all_items
=
set
(
str
(
item
[
'id'
])
for
item
in
self
.
data
[
'items'
])
required
=
set
(
item_id
for
item_id
in
all_items
if
self
.
_get_item_zones
(
int
(
item_id
))
!=
[])
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
):
"""
...
...
drag_and_drop_v2/utils.py
View file @
e2600bc5
...
...
@@ -78,3 +78,7 @@ class FeedbackMessages(object):
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 (
ITEM_CORRECT_FEEDBACK
,
ITEM_INCORRECT_FEEDBACK
,
ITEM_NO_ZONE_FEEDBACK
,
ITEM_ANY_ZONE_FEEDBACK
,
START_FEEDBACK
,
FINISH_FEEDBACK
)
from
drag_and_drop_v2.utils
import
FeedbackMessages
from
.test_base
import
BaseIntegrationTest
...
...
@@ -478,36 +477,6 @@ class DefaultDataTestMixin(object):
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
class
StandardInteractionTest
(
DefaultDataTestMixin
,
InteractionTestBase
,
BaseIntegrationTest
):
"""
...
...
@@ -577,151 +546,6 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseInt
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
):
items_map
=
{
...
...
@@ -764,7 +588,7 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration
},
{
'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'
,
...
...
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):
})
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
,
{
"val"
:
1
,
"zone"
:
self
.
ZONE_2
,
"y_percent"
:
"90
%
"
,
"x_percent"
:
"42
%
"
...
...
@@ -446,7 +446,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
def
_submit_partial_solution
(
self
):
self
.
_submit_solution
({
0
:
self
.
ZONE_1
})
return
1.0
/
3
.0
return
3.0
/
5
.0
def
_submit_incorrect_solution
(
self
):
self
.
_submit_solution
({
0
:
self
.
ZONE_2
,
1
:
self
.
ZONE_1
})
...
...
@@ -518,9 +518,9 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
def
test_do_attempt_keeps_highest_score
(
self
):
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
.
assertEqual
(
self
.
block
.
grade
,
expected_score
)
...
...
@@ -528,7 +528,7 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
# make it a last attempt so we can check feedback
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
=
{})
self
.
assertEqual
(
self
.
block
.
grade
,
expected_score
)
...
...
@@ -537,3 +537,25 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
FeedbackMessages
.
MessageClasses
.
PARTIAL_SOLUTION
)
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