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
85c6143c
Commit
85c6143c
authored
Sep 22, 2016
by
Tim Krones
Committed by
GitHub
Sep 22, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #101 from arbrandes/SOL-1998
[SOL-1998] Implement Show Answer button
parents
07add130
4c6fb7d7
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
386 additions
and
63 deletions
+386
-63
README.md
+2
-1
drag_and_drop_v2/drag_and_drop_v2.py
+65
-2
drag_and_drop_v2/public/css/drag_and_drop.css
+1
-4
drag_and_drop_v2/public/js/drag_and_drop.js
+61
-18
drag_and_drop_v2/translations/en/LC_MESSAGES/text.po
+23
-1
drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po
+25
-3
drag_and_drop_v2/utils.py
+11
-0
tests/integration/test_base.py
+14
-2
tests/integration/test_events.py
+16
-1
tests/integration/test_interaction.py
+1
-1
tests/integration/test_interaction_assessment.py
+63
-22
tests/unit/test_advanced.py
+103
-8
tests/utils.py
+1
-0
No files found.
README.md
View file @
85c6143c
...
...
@@ -108,7 +108,8 @@ There are two problem modes available:
attempt to place an item, and the number of attempts is not limited.
*
**Assessment**
: In this mode, the learner places all items on the board and
then clicks a "Submit" button to get feedback. The number of attempts can be
limited.
limited. When all attempts are used, the learner can click a "Show Answer"
button to temporarily place items on their correct drop zones.

...
...
drag_and_drop_v2/drag_and_drop_v2.py
View file @
85c6143c
...
...
@@ -438,6 +438,28 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
return
self
.
_get_user_state
()
@XBlock.json_handler
def
show_answer
(
self
,
data
,
suffix
=
''
):
"""
Returns correct answer in assessment mode.
Raises:
* JsonHandlerError with 400 error code in standard mode.
* JsonHandlerError with 409 error code if there are still attempts left
"""
if
self
.
mode
!=
Constants
.
ASSESSMENT_MODE
:
raise
JsonHandlerError
(
400
,
self
.
i18n_service
.
gettext
(
"show_answer handler should only be called for assessment mode"
)
)
if
self
.
attempts_remain
:
raise
JsonHandlerError
(
409
,
self
.
i18n_service
.
gettext
(
"There are attempts remaining"
)
)
return
self
.
_get_correct_state
()
@XBlock.json_handler
def
expand_static_url
(
self
,
url
,
suffix
=
''
):
""" AJAX-accessible handler for expanding URLs to static [image] files """
return
{
'url'
:
self
.
_expand_static_url
(
url
)}
...
...
@@ -527,7 +549,14 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
FeedbackMessages
.
correctly_placed
,
FeedbackMessages
.
MessageClasses
.
CORRECTLY_PLACED
)
_add_msg_if_exists
(
misplaced_ids
,
FeedbackMessages
.
misplaced
,
FeedbackMessages
.
MessageClasses
.
MISPLACED
)
# Misplaced items are not returned to the bank on the final attempt.
if
self
.
attempts_remain
:
misplaced_template
=
FeedbackMessages
.
misplaced_returned
else
:
misplaced_template
=
FeedbackMessages
.
misplaced
_add_msg_if_exists
(
misplaced_ids
,
misplaced_template
,
FeedbackMessages
.
MessageClasses
.
MISPLACED
)
_add_msg_if_exists
(
missing_ids
,
FeedbackMessages
.
not_placed
,
FeedbackMessages
.
MessageClasses
.
NOT_PLACED
)
if
self
.
attempts_remain
and
(
misplaced_ids
or
missing_ids
):
...
...
@@ -723,6 +752,31 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
'overall_feedback'
:
self
.
_present_feedback
(
overall_feedback_msgs
)
}
def
_get_correct_state
(
self
):
"""
Returns one of the possible correct states for the configured data.
"""
state
=
{}
items
=
copy
.
deepcopy
(
self
.
data
.
get
(
'items'
,
[]))
for
item
in
items
:
zones
=
item
.
get
(
'zones'
)
# For backwards compatibility
if
zones
is
None
:
zones
=
[]
zone
=
item
.
get
(
'zone'
)
if
zone
is
not
None
and
zone
!=
'none'
:
zones
.
append
(
zone
)
if
zones
:
zone
=
zones
.
pop
()
state
[
str
(
item
[
'id'
])]
=
{
'zone'
:
zone
,
'correct'
:
True
,
}
return
{
'items'
:
state
}
def
_get_item_state
(
self
):
"""
Returns a copy of the user item state.
...
...
@@ -855,4 +909,13 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
A canned scenario for display in the workbench.
"""
return
[(
"Drag-and-drop-v2 scenario"
,
"<vertical_demo><drag-and-drop-v2/></vertical_demo>"
)]
return
[
(
"Drag-and-drop-v2 standard"
,
"<vertical_demo><drag-and-drop-v2/></vertical_demo>"
),
(
"Drag-and-drop-v2 assessment"
,
"<vertical_demo><drag-and-drop-v2 mode='assessment' max_attempts='3'/></vertical_demo>"
),
]
drag_and_drop_v2/public/css/drag_and_drop.css
View file @
85c6143c
...
...
@@ -568,6 +568,7 @@
.ltr
.xblock--drag-and-drop
.actions-toolbar
.action-toolbar-item.sidebar-buttons
{
float
:
right
;
padding-right
:
-5px
;
padding-top
:
5px
;
}
.rtl
.xblock--drag-and-drop
.actions-toolbar
.action-toolbar-item.sidebar-buttons
{
...
...
@@ -623,10 +624,6 @@
display
:
block
;
}
.xblock--drag-and-drop
.reset-button
{
margin-top
:
3px
;
}
/*** ACTIONS TOOLBAR END ***/
/*** KEYBOARD HELP ***/
...
...
drag_and_drop_v2/public/js/drag_and_drop.js
View file @
85c6143c
...
...
@@ -110,7 +110,7 @@ function DragAndDropTemplates(configuration) {
if
(
item
.
is_placed
)
{
var
zone_title
=
(
zone
.
title
||
"Unknown Zone"
);
// This "Unknown" text should never be seen, so does not need i18n
var
description_content
;
if
(
configuration
.
mode
===
DragAndDropBlock
.
ASSESSMENT_MODE
)
{
if
(
configuration
.
mode
===
DragAndDropBlock
.
ASSESSMENT_MODE
&&
!
ctx
.
showing_answer
)
{
// In assessment mode placed items will "stick" even when not in correct zone.
description_content
=
gettext
(
'Placed in: {zone_title}'
).
replace
(
'{zone_title}'
,
zone_title
);
}
else
{
...
...
@@ -180,9 +180,8 @@ function DragAndDropTemplates(configuration) {
var
zoneTemplate
=
function
(
zone
,
ctx
)
{
var
className
=
ctx
.
display_zone_labels
?
'zone-name'
:
'zone-name sr'
;
var
selector
=
ctx
.
display_zone_borders
?
'div.zone.zone-with-borders'
:
'div.zone'
;
// If zone is aligned, mark its item alignment
// and render its placed items as children
var
item_wrapper
=
'div.item-wrapper'
;
// Mark item alignment and render its placed items as children
var
item_wrapper
=
'div.item-wrapper.item-align.item-align-'
+
zone
.
align
;
var
is_item_in_zone
=
function
(
i
)
{
return
i
.
is_placed
&&
(
i
.
zone
===
zone
.
uid
);
};
var
items_in_zone
=
$
.
grep
(
ctx
.
items
,
is_item_in_zone
);
var
zone_description_id
=
'zone-'
+
zone
.
uid
+
'-description'
;
...
...
@@ -199,12 +198,7 @@ function DragAndDropTemplates(configuration) {
gettext
(
'Items placed here: '
)
+
items_in_zone
.
map
(
function
(
item
)
{
return
item
.
displayName
;
}).
join
(
", "
)
);
}
if
(
zone
.
align
!==
'none'
)
{
item_wrapper
+=
'.item-align.item-align-'
+
zone
.
align
;
//items_in_zone = $.grep(ctx.items, is_item_in_zone);
}
else
{
items_in_zone
=
[];
}
return
(
h
(
selector
,
...
...
@@ -343,14 +337,21 @@ function DragAndDropTemplates(configuration) {
);
};
var
sidebarButtonTemplate
=
function
(
buttonClass
,
iconClass
,
buttonText
,
disabled
)
{
var
sidebarButtonTemplate
=
function
(
buttonClass
,
iconClass
,
buttonText
,
disabled
,
spinner
)
{
if
(
spinner
)
{
iconClass
=
'fa-spin.fa-spinner'
;
}
return
(
h
(
'span.sidebar-button-wrapper'
,
{},
[
h
(
'button.unbutton.btn-default.btn-small.'
+
buttonClass
,
{
disabled
:
disabled
||
false
,
attributes
:
{
tabindex
:
0
}},
{
disabled
:
disabled
||
spinner
||
false
,
attributes
:
{
tabindex
:
0
}},
[
h
(
"span.btn-icon.fa."
+
iconClass
,
{
attributes
:
{
"aria-hidden"
:
true
}},
[]),
h
(
"span.btn-icon.fa."
+
iconClass
,
{
attributes
:
{
"aria-hidden"
:
true
}},
[]
),
buttonText
]
)
...
...
@@ -359,10 +360,21 @@ function DragAndDropTemplates(configuration) {
};
var
sidebarTemplate
=
function
(
ctx
)
{
var
showAnswerButton
=
null
;
if
(
ctx
.
show_show_answer
)
{
showAnswerButton
=
sidebarButtonTemplate
(
"show-answer-button"
,
"fa-info-circle"
,
gettext
(
'Show Answer'
),
ctx
.
showing_answer
?
true
:
ctx
.
disable_show_answer_button
,
ctx
.
show_answer_spinner
);
}
return
(
h
(
"section.action-toolbar-item.sidebar-buttons"
,
{},
[
sidebarButtonTemplate
(
"keyboard-help-button"
,
"fa-question"
,
gettext
(
'Keyboard Help'
)),
sidebarButtonTemplate
(
"reset-button"
,
"fa-refresh"
,
gettext
(
'Reset'
),
ctx
.
disable_reset_button
),
showAnswerButton
,
])
)
};
...
...
@@ -434,9 +446,8 @@ function DragAndDropTemplates(configuration) {
var
mainTemplate
=
function
(
ctx
)
{
var
problemTitle
=
ctx
.
show_title
?
h
(
'h3.problem-title'
,
{
innerHTML
:
ctx
.
title_html
})
:
null
;
var
problemHeader
=
ctx
.
show_problem_header
?
h
(
'h4.title1'
,
gettext
(
'Problem'
))
:
null
;
// Render only items_in_bank and items_placed_unaligned here;
// items placed in aligned zones will be rendered by zoneTemplate.
// Render only items in the bank here, including placeholders. Placed
// items will be rendered by zoneTemplate.
var
is_item_placed
=
function
(
i
)
{
return
i
.
is_placed
;
};
var
items_placed
=
$
.
grep
(
ctx
.
items
,
is_item_placed
);
var
items_in_bank
=
$
.
grep
(
ctx
.
items
,
is_item_placed
,
true
);
...
...
@@ -561,6 +572,10 @@ function DragAndDropBlock(runtime, element, configuration) {
$element
.
on
(
'keydown'
,
'.reset-button'
,
function
(
evt
)
{
runOnKey
(
evt
,
RET
,
resetProblem
);
});
$element
.
on
(
'click'
,
'.show-answer-button'
,
showAnswer
);
$element
.
on
(
'keydown'
,
'.show-answer-button'
,
function
(
evt
)
{
runOnKey
(
evt
,
RET
,
showAnswer
);
});
// For the next one, we need to use addEventListener with useCapture 'true' in order
// to watch for load events on any child element, since load events do not bubble.
...
...
@@ -1098,6 +1113,26 @@ function DragAndDropBlock(runtime, element, configuration) {
});
};
var
showAnswer
=
function
(
evt
)
{
evt
.
preventDefault
();
state
.
show_answer_spinner
=
true
;
applyState
();
$
.
ajax
({
type
:
'POST'
,
url
:
runtime
.
handlerUrl
(
element
,
'show_answer'
),
data
:
'{}'
,
}).
done
(
function
(
data
)
{
state
.
items
=
data
.
items
;
state
.
showing_answer
=
true
;
delete
state
.
feedback
;
}).
always
(
function
()
{
state
.
show_answer_spinner
=
false
;
applyState
();
$root
.
find
(
'.item-bank'
).
focus
();
});
};
var
doAttempt
=
function
(
evt
)
{
evt
.
preventDefault
();
state
.
submit_spinner
=
true
;
...
...
@@ -1147,6 +1182,10 @@ function DragAndDropBlock(runtime, element, configuration) {
return
any_items_placed
&&
(
configuration
.
mode
!==
DragAndDropBlock
.
ASSESSMENT_MODE
||
attemptsRemain
());
};
var
canShowAnswer
=
function
()
{
return
configuration
.
mode
===
DragAndDropBlock
.
ASSESSMENT_MODE
&&
!
attemptsRemain
();
};
var
attemptsRemain
=
function
()
{
return
!
configuration
.
max_attempts
||
configuration
.
max_attempts
>
state
.
attempts
;
};
...
...
@@ -1207,7 +1246,7 @@ function DragAndDropBlock(runtime, element, configuration) {
// In assessment mode, it is possible to move items back to the bank, so the bank should be able to
// gain focus while keyboard placement is in progress.
var
item_bank_focusable
=
state
.
keyboard_placement_mode
&&
var
item_bank_focusable
=
(
state
.
keyboard_placement_mode
||
state
.
showing_answer
)
&&
configuration
.
mode
===
DragAndDropBlock
.
ASSESSMENT_MODE
;
var
context
=
{
...
...
@@ -1220,6 +1259,7 @@ function DragAndDropBlock(runtime, element, configuration) {
problem_html
:
configuration
.
problem_text
,
show_problem_header
:
configuration
.
show_problem_header
,
show_submit_answer
:
configuration
.
mode
==
DragAndDropBlock
.
ASSESSMENT_MODE
,
show_show_answer
:
configuration
.
mode
==
DragAndDropBlock
.
ASSESSMENT_MODE
,
target_img_src
:
configuration
.
target_img_expanded_url
,
target_img_description
:
configuration
.
target_img_description
,
display_zone_labels
:
configuration
.
display_zone_labels
,
...
...
@@ -1233,8 +1273,11 @@ function DragAndDropBlock(runtime, element, configuration) {
feedback_messages
:
state
.
feedback
,
overall_feedback_messages
:
state
.
overall_feedback
,
disable_reset_button
:
!
canReset
(),
disable_show_answer_button
:
!
canShowAnswer
(),
disable_submit_button
:
!
canSubmitAttempt
(),
submit_spinner
:
state
.
submit_spinner
submit_spinner
:
state
.
submit_spinner
,
showing_answer
:
state
.
showing_answer
,
show_answer_spinner
:
state
.
show_answer_spinner
};
return
renderView
(
context
);
...
...
drag_and_drop_v2/translations/en/LC_MESSAGES/text.po
View file @
85c6143c
...
...
@@ -217,6 +217,14 @@ msgid "Max number of attempts reached"
msgstr ""
#: drag_and_drop_v2.py
msgid "show_answer handler should only be called for assessment mode"
msgstr ""
#: drag_and_drop_v2.py
msgid "There are attempts remaining"
msgstr ""
#: drag_and_drop_v2.py
msgid "Unknown DnDv2 mode {mode} - course is misconfigured"
msgstr ""
...
...
@@ -446,6 +454,14 @@ msgid "Reset"
msgstr ""
#: public/js/drag_and_drop.js
msgid "Show Answer"
msgstr ""
#: public/js/drag_and_drop.js
msgid "Hide Answer"
msgstr ""
#: public/js/drag_and_drop.js
msgid "Submit"
msgstr ""
...
...
@@ -529,7 +545,13 @@ msgid_plural "Correctly placed {correct_count} items."
msgstr[0] ""
msgstr[1] ""
#: utils.py:32
#: utils.py:62
msgid "Misplaced {misplaced_count} item."
msgid_plural "Misplaced {misplaced_count} items."
msgstr[0] ""
msgstr[1] ""
#: utils.py:73
msgid "Misplaced {misplaced_count} item. Misplaced item was returned to item bank."
msgid_plural "Misplaced {misplaced_count} items. Misplaced items were returned to item bank."
msgstr[0] ""
...
...
drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po
View file @
85c6143c
...
...
@@ -272,6 +272,14 @@ msgid "Max number of attempts reached"
msgstr "Mäx nümßér öf ättémpts réäçhéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
#: drag_and_drop_v2.py
msgid "show_answer handler should only be called for assessment mode"
msgstr "shöw_änswér händlér shöüld önlý ßé çälléd för ässéssmént mödé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: drag_and_drop_v2.py
msgid "There are attempts remaining"
msgstr "Théré äré ättémpts rémäïnïng Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
#: drag_and_drop_v2.py
msgid "Unknown DnDv2 mode {mode} - course is misconfigured"
msgstr "Ûnknöwn DnDv2 mödé {mode} - çöürsé ïs mïsçönfïgüréd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
...
...
@@ -520,6 +528,14 @@ msgid "Reset"
msgstr "Rését Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
#: public/js/drag_and_drop.js
msgid "Show Answer"
msgstr "Shöw Ànswér Ⱡ'σяєм ιρѕυм ∂σłσя #"
#: public/js/drag_and_drop.js
msgid "Hide Answer"
msgstr "Hïdé Ànswér Ⱡ'σяєм ιρѕυм ∂σłσя #"
#: public/js/drag_and_drop.js
msgid "Submit"
msgstr "Süßmït Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
...
...
@@ -617,12 +633,18 @@ msgid_plural "Correctly placed {correct_count} items."
msgstr[0] "Çörréçtlý pläçéd {correct_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
msgstr[1] "Çörréçtlý pläçéd {correct_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
#: utils.py:
3
2
msgid "Misplaced {misplaced_count} item.
Misplaced item was returned to item bank.
"
msgid_plural "Misplaced {misplaced_count} items.
Misplaced items were returned to item bank.
"
#: utils.py:
6
2
msgid "Misplaced {misplaced_count} item."
msgid_plural "Misplaced {misplaced_count} items."
msgstr[0] "Mïspläçéd {misplaced_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
msgstr[1] "Mïspläçéd {misplaced_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: utils.py:73
msgid "Misplaced {misplaced_count} item. Misplaced item was returned to item bank."
msgid_plural "Misplaced {misplaced_count} items. Misplaced items were returned to item bank."
msgstr[0] "Mïspläçéd {misplaced_count} ïtém. Mïspläçéd ïtém wäs rétürnéd tö ïtém ßänk. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
msgstr[1] "Mïspläçéd {misplaced_count} ïtéms. Mïspläçéd ïtéms wéré rétürnéd tö ïtém ßänk. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: utils.py:40
msgid "Did not place {missing_count} required item."
msgid_plural "Did not place {missing_count} required items."
...
...
drag_and_drop_v2/utils.py
View file @
85c6143c
...
...
@@ -60,6 +60,17 @@ class FeedbackMessages(object):
Formats "misplaced items" message
"""
return
ngettext
(
'Misplaced {misplaced_count} item.'
,
'Misplaced {misplaced_count} items.'
,
number
)
.
format
(
misplaced_count
=
number
)
@staticmethod
def
misplaced_returned
(
number
,
ngettext
=
ngettext_fallback
):
"""
Formats "misplaced items returned to bank" message
"""
return
ngettext
(
'Misplaced {misplaced_count} item. Misplaced item was returned to item bank.'
,
'Misplaced {misplaced_count} items. Misplaced items were returned to item bank.'
,
number
...
...
tests/integration/test_base.py
View file @
85c6143c
...
...
@@ -126,6 +126,9 @@ class BaseIntegrationTest(SeleniumBaseTest):
def
_get_reset_button
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
'.reset-button'
)
def
_get_show_answer_button
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
'.show-answer-button'
)
def
_get_submit_button
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
'.submit-answer-button'
)
...
...
@@ -392,12 +395,21 @@ class InteractionTestBase(object):
self
.
assertDraggable
(
item_value
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option'
)
self
.
assertEqual
(
item
.
get_attribute
(
'tabindex'
),
'0'
)
self
.
assertEqual
(
item_description
.
text
,
'Placed in: {}'
.
format
(
zone_title
))
description
=
'Placed in: {}'
else
:
self
.
assertNotDraggable
(
item_value
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option fade'
)
self
.
assertIsNone
(
item
.
get_attribute
(
'tabindex'
))
self
.
assertEqual
(
item_description
.
text
,
'Correctly placed in: {}'
.
format
(
zone_title
))
description
=
'Correctly placed in: {}'
# An item with multiple drop zones could be located in any one of these
# zones. In that case, zone_title will be a list, and we need to check
# whether the zone info in the description of the item matches any of
# the zones in that list.
if
isinstance
(
zone_title
,
list
):
self
.
assertIn
(
item_description
.
text
,
[
description
.
format
(
title
)
for
title
in
zone_title
])
else
:
self
.
assertEqual
(
item_description
.
text
,
description
.
format
(
zone_title
))
def
assert_reverted_item
(
self
,
item_value
):
item
=
self
.
_get_item_by_value
(
item_value
)
...
...
tests/integration/test_events.py
View file @
85c6143c
...
...
@@ -5,7 +5,8 @@ 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
,
BOTTOM_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
,
ItemDefinition
...
...
@@ -146,6 +147,20 @@ class AssessmentEventsFiredTest(
self
.
assertEqual
(
name
,
event
[
'name'
])
self
.
assertEqual
(
published_data
,
event
[
'data'
])
def
test_grade
(
self
):
"""
Test grading after submitting solution in assessment mode
"""
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
)
@ddt
class
ItemDroppedEventTest
(
DefaultDataTestMixin
,
BaseEventsTests
):
...
...
tests/integration/test_interaction.py
View file @
85c6143c
...
...
@@ -442,7 +442,7 @@ class MultipleBlocksDataInteraction(ParameterizedTestsMixin, InteractionTestBase
self
.
_switch_to_block
(
1
)
# Test mouse and keyboard interaction
self
.
interact_with_keyboard_help
(
scroll_down
=
9
00
)
self
.
interact_with_keyboard_help
(
scroll_down
=
12
00
)
self
.
interact_with_keyboard_help
(
scroll_down
=
0
,
use_keyboard
=
True
)
...
...
tests/integration/test_interaction_assessment.py
View file @
85c6143c
# -*- coding: utf-8 -*-
# Imports ###########################################################
from
ddt
import
ddt
,
data
...
...
@@ -7,7 +9,6 @@ import time
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
(
...
...
@@ -55,6 +56,14 @@ class AssessmentTestMixin(object):
submit_button
.
click
()
self
.
wait_for_ajax
()
def
click_show_answer
(
self
):
show_answer_button
=
self
.
_get_show_answer_button
()
self
.
_wait_until_enabled
(
show_answer_button
)
show_answer_button
.
click
()
self
.
wait_for_ajax
()
@ddt
class
AssessmentInteractionTest
(
...
...
@@ -150,6 +159,58 @@ class AssessmentInteractionTest(
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
'true'
)
self
.
assertEqual
(
reset_button
.
get_attribute
(
'disabled'
),
'true'
)
def
_assert_show_answer_item_placement
(
self
):
zones
=
dict
(
self
.
all_zones
)
for
item
in
self
.
_get_items_with_zone
(
self
.
items_map
)
.
values
():
zone_titles
=
[
zones
[
zone_id
]
for
zone_id
in
item
.
zone_ids
]
# When showing answers, correct items are placed as if assessment_mode=False
self
.
assert_placed_item
(
item
.
item_id
,
zone_titles
,
assessment_mode
=
False
)
for
item_definition
in
self
.
_get_items_without_zone
(
self
.
items_map
)
.
values
():
self
.
assertNotDraggable
(
item_definition
.
item_id
)
item
=
self
.
_get_item_by_value
(
item_definition
.
item_id
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option fade'
)
item_content
=
item
.
find_element_by_css_selector
(
'.item-content'
)
item_description_id
=
'-item-{}-description'
.
format
(
item_definition
.
item_id
)
self
.
assertEqual
(
item_content
.
get_attribute
(
'aria-describedby'
),
item_description_id
)
describedby_text
=
(
u'Press "Enter", "Space", "Ctrl-m", or "⌘-m" on an item to select it for dropping, '
'then navigate to the zone you want to drop it on.'
)
self
.
assertEqual
(
item
.
find_element_by_css_selector
(
'.sr'
)
.
text
,
describedby_text
)
def
test_show_answer
(
self
):
"""
Test "Show Answer" button is shown in assessment mode, enabled when no
more attempts remaining, is disabled and displays correct answers when
clicked.
"""
show_answer_button
=
self
.
_get_show_answer_button
()
self
.
assertTrue
(
show_answer_button
.
is_displayed
())
self
.
place_item
(
0
,
TOP_ZONE_ID
,
Keys
.
RETURN
)
for
_
in
xrange
(
self
.
MAX_ATTEMPTS
-
1
):
self
.
assertEqual
(
show_answer_button
.
get_attribute
(
'disabled'
),
'true'
)
self
.
click_submit
()
# Place an incorrect item on the final attempt.
self
.
place_item
(
1
,
TOP_ZONE_ID
,
Keys
.
RETURN
)
self
.
click_submit
()
# A feedback popup should open upon final submission.
popup
=
self
.
_get_popup
()
self
.
assertTrue
(
popup
.
is_displayed
())
self
.
assertIsNone
(
show_answer_button
.
get_attribute
(
'disabled'
))
self
.
click_show_answer
()
# The popup should be closed upon clicking Show Answer.
self
.
assertFalse
(
popup
.
is_displayed
())
self
.
assertEqual
(
show_answer_button
.
get_attribute
(
'disabled'
),
'true'
)
self
.
_assert_show_answer_item_placement
()
def
test_do_attempt_feedback_is_updated
(
self
):
"""
Test updating overall feedback after submitting solution in assessment mode
...
...
@@ -174,7 +235,7 @@ class AssessmentInteractionTest(
feedback_lines
=
[
"FEEDBACK"
,
FeedbackMessages
.
correctly_placed
(
1
),
FeedbackMessages
.
misplaced
(
1
),
FeedbackMessages
.
misplaced
_returned
(
1
),
FeedbackMessages
.
not_placed
(
2
),
START_FEEDBACK
]
...
...
@@ -199,26 +260,6 @@ class AssessmentInteractionTest(
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
)
def
test_per_item_feedback_multiple_misplaced
(
self
):
self
.
place_item
(
0
,
MIDDLE_ZONE_ID
,
Keys
.
RETURN
)
self
.
place_item
(
1
,
BOTTOM_ZONE_ID
,
Keys
.
RETURN
)
...
...
tests/unit/test_advanced.py
View file @
85c6143c
...
...
@@ -190,6 +190,14 @@ class StandardModeFixture(BaseDragAndDropAjaxFixture):
self
.
assertEqual
(
res
.
status_code
,
400
)
def
test_show_answer_not_available
(
self
):
"""
Tests that do_attempt handler returns 400 error for standard mode DnDv2
"""
res
=
self
.
call_handler
(
self
.
SHOW_ANSWER_HANDLER
,
expect_json
=
False
)
self
.
assertEqual
(
res
.
status_code
,
400
)
@ddt.ddt
class
AssessmentModeFixture
(
BaseDragAndDropAjaxFixture
):
...
...
@@ -205,6 +213,12 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
data
=
self
.
_make_submission
(
item_id
,
zone_id
)
self
.
call_handler
(
self
.
DROP_ITEM_HANDLER
,
data
)
def
_get_all_solutions
(
self
):
# pylint: disable=no-self-use
raise
NotImplementedError
()
def
_get_all_decoys
(
self
):
# pylint: disable=no-self-use
raise
NotImplementedError
()
def
_submit_complete_solution
(
self
):
# pylint: disable=no-self-use
raise
NotImplementedError
()
...
...
@@ -402,6 +416,51 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
self
.
assertEqual
(
self
.
block
.
item_state
,
original_item_state
)
@ddt.data
(
(
None
,
10
,
True
),
(
0
,
12
,
True
),
(
3
,
3
,
False
),
)
@ddt.unpack
def
test_show_answer_validation
(
self
,
max_attempts
,
attempts
,
expect_validation_error
):
"""
Test that show_answer returns a 409 when max_attempts = None, or when
there are still attempts remaining.
"""
self
.
block
.
max_attempts
=
max_attempts
self
.
block
.
attempts
=
attempts
res
=
self
.
call_handler
(
self
.
SHOW_ANSWER_HANDLER
,
data
=
{},
expect_json
=
False
)
if
expect_validation_error
:
self
.
assertEqual
(
res
.
status_code
,
409
)
else
:
self
.
assertEqual
(
res
.
status_code
,
200
)
def
test_get_correct_state
(
self
):
"""
Test that _get_correct_state returns one of the possible correct
solutions for the configuration.
"""
self
.
_set_final_attempt
()
self
.
_submit_incorrect_solution
()
self
.
call_handler
(
self
.
DO_ATTEMPT_HANDLER
,
data
=
{})
self
.
assertFalse
(
self
.
block
.
attempts_remain
)
# precondition check
res
=
self
.
call_handler
(
self
.
SHOW_ANSWER_HANDLER
,
data
=
{})
self
.
assertIn
(
'items'
,
res
)
decoys
=
self
.
_get_all_decoys
()
solution
=
{}
for
item_id
,
item_state
in
res
[
'items'
]
.
iteritems
():
self
.
assertIn
(
'correct'
,
item_state
)
self
.
assertIn
(
'zone'
,
item_state
)
self
.
assertNotIn
(
int
(
item_id
),
decoys
)
solution
[
int
(
item_id
)]
=
item_state
[
'zone'
]
self
.
assertIn
(
solution
,
self
.
_get_all_solutions
())
class
TestDragAndDropHtmlData
(
StandardModeFixture
,
unittest
.
TestCase
):
FOLDER
=
"html"
...
...
@@ -468,6 +527,12 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
self
.
assertEqual
(
res
[
self
.
FEEDBACK_KEY
],
expected_item_feedback
)
self
.
assertEqual
(
res
[
self
.
OVERALL_FEEDBACK_KEY
],
expected_overall_feedback
)
def
_get_all_solutions
(
self
):
return
[{
0
:
self
.
ZONE_1
,
1
:
self
.
ZONE_2
,
2
:
self
.
ZONE_2
}]
def
_get_all_decoys
(
self
):
return
[
3
,
4
]
def
_submit_complete_solution
(
self
):
self
.
_submit_solution
({
0
:
self
.
ZONE_1
,
1
:
self
.
ZONE_2
,
2
:
self
.
ZONE_2
})
...
...
@@ -486,15 +551,35 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
expected_item_feedback
=
[
self
.
_make_feedback_message
(
self
.
FEEDBACK
[
0
][
'incorrect'
])]
expected_overall_feedback
=
[
self
.
_make_feedback_message
(
FeedbackMessages
.
correctly_placed
(
1
),
FeedbackMessages
.
MessageClasses
.
CORRECTLY_PLACED
FeedbackMessages
.
correctly_placed
(
1
),
FeedbackMessages
.
MessageClasses
.
CORRECTLY_PLACED
),
self
.
_make_feedback_message
(
FeedbackMessages
.
misplaced_returned
(
1
),
FeedbackMessages
.
MessageClasses
.
MISPLACED
),
self
.
_make_feedback_message
(
FeedbackMessages
.
not_placed
(
1
),
FeedbackMessages
.
MessageClasses
.
NOT_PLACED
),
self
.
_make_feedback_message
(
self
.
INITIAL_FEEDBACK
,
None
),
self
.
_make_feedback_message
(
FeedbackMessages
.
misplaced
(
1
),
FeedbackMessages
.
MessageClasses
.
MISPLACED
),
self
.
_make_feedback_message
(
FeedbackMessages
.
not_placed
(
1
),
FeedbackMessages
.
MessageClasses
.
NOT_PLACED
),
self
.
_make_feedback_message
(
self
.
INITIAL_FEEDBACK
,
None
),
]
self
.
_assert_item_and_overall_feedback
(
res
,
expected_item_feedback
,
expected_overall_feedback
)
def
test_do_attempt_shows_correct_misplaced_feedback_at_last_attempt
(
self
):
self
.
_set_final_attempt
()
self
.
_submit_solution
({
0
:
self
.
ZONE_2
})
res
=
self
.
_do_attempt
()
misplaced_message
=
self
.
_make_feedback_message
(
FeedbackMessages
.
misplaced
(
1
),
FeedbackMessages
.
MessageClasses
.
MISPLACED
)
self
.
assertIn
(
misplaced_message
,
res
[
self
.
OVERALL_FEEDBACK_KEY
])
def
test_do_attempt_no_item_state
(
self
):
"""
Test do_attempt overall feedback when no item state is saved - no items were ever dropped.
...
...
@@ -517,11 +602,21 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
expected_item_feedback
=
[]
expected_overall_feedback
=
[
self
.
_make_feedback_message
(
FeedbackMessages
.
correctly_placed
(
2
),
FeedbackMessages
.
MessageClasses
.
CORRECTLY_PLACED
FeedbackMessages
.
correctly_placed
(
2
),
FeedbackMessages
.
MessageClasses
.
CORRECTLY_PLACED
),
self
.
_make_feedback_message
(
FeedbackMessages
.
misplaced_returned
(
1
),
FeedbackMessages
.
MessageClasses
.
MISPLACED
),
self
.
_make_feedback_message
(
FeedbackMessages
.
not_placed
(
1
),
FeedbackMessages
.
MessageClasses
.
NOT_PLACED
),
self
.
_make_feedback_message
(
self
.
INITIAL_FEEDBACK
,
None
),
self
.
_make_feedback_message
(
FeedbackMessages
.
misplaced
(
1
),
FeedbackMessages
.
MessageClasses
.
MISPLACED
),
self
.
_make_feedback_message
(
FeedbackMessages
.
not_placed
(
1
),
FeedbackMessages
.
MessageClasses
.
NOT_PLACED
),
self
.
_make_feedback_message
(
self
.
INITIAL_FEEDBACK
,
None
),
]
self
.
_assert_item_and_overall_feedback
(
res
,
expected_item_feedback
,
expected_overall_feedback
)
...
...
tests/utils.py
View file @
85c6143c
...
...
@@ -47,6 +47,7 @@ class TestCaseMixin(object):
DROP_ITEM_HANDLER
=
'drop_item'
DO_ATTEMPT_HANDLER
=
'do_attempt'
RESET_HANDLER
=
'reset'
SHOW_ANSWER_HANDLER
=
'show_answer'
USER_STATE_HANDLER
=
'get_user_state'
def
patch_workbench
(
self
):
...
...
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