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
Hide 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:
...
@@ -108,7 +108,8 @@ There are two problem modes available:
attempt to place an item, and the number of attempts is not limited.
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
*
**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
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):
...
@@ -438,6 +438,28 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
return
self
.
_get_user_state
()
return
self
.
_get_user_state
()
@XBlock.json_handler
@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
=
''
):
def
expand_static_url
(
self
,
url
,
suffix
=
''
):
""" AJAX-accessible handler for expanding URLs to static [image] files """
""" AJAX-accessible handler for expanding URLs to static [image] files """
return
{
'url'
:
self
.
_expand_static_url
(
url
)}
return
{
'url'
:
self
.
_expand_static_url
(
url
)}
...
@@ -527,7 +549,14 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
...
@@ -527,7 +549,14 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
FeedbackMessages
.
correctly_placed
,
FeedbackMessages
.
correctly_placed
,
FeedbackMessages
.
MessageClasses
.
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
)
_add_msg_if_exists
(
missing_ids
,
FeedbackMessages
.
not_placed
,
FeedbackMessages
.
MessageClasses
.
NOT_PLACED
)
if
self
.
attempts_remain
and
(
misplaced_ids
or
missing_ids
):
if
self
.
attempts_remain
and
(
misplaced_ids
or
missing_ids
):
...
@@ -723,6 +752,31 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
...
@@ -723,6 +752,31 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
'overall_feedback'
:
self
.
_present_feedback
(
overall_feedback_msgs
)
'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
):
def
_get_item_state
(
self
):
"""
"""
Returns a copy of the user item state.
Returns a copy of the user item state.
...
@@ -855,4 +909,13 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
...
@@ -855,4 +909,13 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
"""
A canned scenario for display in the workbench.
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 @@
...
@@ -568,6 +568,7 @@
.ltr
.xblock--drag-and-drop
.actions-toolbar
.action-toolbar-item.sidebar-buttons
{
.ltr
.xblock--drag-and-drop
.actions-toolbar
.action-toolbar-item.sidebar-buttons
{
float
:
right
;
float
:
right
;
padding-right
:
-5px
;
padding-right
:
-5px
;
padding-top
:
5px
;
}
}
.rtl
.xblock--drag-and-drop
.actions-toolbar
.action-toolbar-item.sidebar-buttons
{
.rtl
.xblock--drag-and-drop
.actions-toolbar
.action-toolbar-item.sidebar-buttons
{
...
@@ -623,10 +624,6 @@
...
@@ -623,10 +624,6 @@
display
:
block
;
display
:
block
;
}
}
.xblock--drag-and-drop
.reset-button
{
margin-top
:
3px
;
}
/*** ACTIONS TOOLBAR END ***/
/*** ACTIONS TOOLBAR END ***/
/*** KEYBOARD HELP ***/
/*** KEYBOARD HELP ***/
...
...
drag_and_drop_v2/public/js/drag_and_drop.js
View file @
85c6143c
...
@@ -110,7 +110,7 @@ function DragAndDropTemplates(configuration) {
...
@@ -110,7 +110,7 @@ function DragAndDropTemplates(configuration) {
if
(
item
.
is_placed
)
{
if
(
item
.
is_placed
)
{
var
zone_title
=
(
zone
.
title
||
"Unknown Zone"
);
// This "Unknown" text should never be seen, so does not need i18n
var
zone_title
=
(
zone
.
title
||
"Unknown Zone"
);
// This "Unknown" text should never be seen, so does not need i18n
var
description_content
;
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.
// 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
);
description_content
=
gettext
(
'Placed in: {zone_title}'
).
replace
(
'{zone_title}'
,
zone_title
);
}
else
{
}
else
{
...
@@ -180,9 +180,8 @@ function DragAndDropTemplates(configuration) {
...
@@ -180,9 +180,8 @@ function DragAndDropTemplates(configuration) {
var
zoneTemplate
=
function
(
zone
,
ctx
)
{
var
zoneTemplate
=
function
(
zone
,
ctx
)
{
var
className
=
ctx
.
display_zone_labels
?
'zone-name'
:
'zone-name sr'
;
var
className
=
ctx
.
display_zone_labels
?
'zone-name'
:
'zone-name sr'
;
var
selector
=
ctx
.
display_zone_borders
?
'div.zone.zone-with-borders'
:
'div.zone'
;
var
selector
=
ctx
.
display_zone_borders
?
'div.zone.zone-with-borders'
:
'div.zone'
;
// If zone is aligned, mark its item alignment
// Mark item alignment and render its placed items as children
// and render its placed items as children
var
item_wrapper
=
'div.item-wrapper.item-align.item-align-'
+
zone
.
align
;
var
item_wrapper
=
'div.item-wrapper'
;
var
is_item_in_zone
=
function
(
i
)
{
return
i
.
is_placed
&&
(
i
.
zone
===
zone
.
uid
);
};
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
items_in_zone
=
$
.
grep
(
ctx
.
items
,
is_item_in_zone
);
var
zone_description_id
=
'zone-'
+
zone
.
uid
+
'-description'
;
var
zone_description_id
=
'zone-'
+
zone
.
uid
+
'-description'
;
...
@@ -199,12 +198,7 @@ function DragAndDropTemplates(configuration) {
...
@@ -199,12 +198,7 @@ function DragAndDropTemplates(configuration) {
gettext
(
'Items placed here: '
)
+
items_in_zone
.
map
(
function
(
item
)
{
return
item
.
displayName
;
}).
join
(
", "
)
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
(
return
(
h
(
h
(
selector
,
selector
,
...
@@ -343,14 +337,21 @@ function DragAndDropTemplates(configuration) {
...
@@ -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
(
return
(
h
(
'span.sidebar-button-wrapper'
,
{},
[
h
(
'span.sidebar-button-wrapper'
,
{},
[
h
(
h
(
'button.unbutton.btn-default.btn-small.'
+
buttonClass
,
'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
buttonText
]
]
)
)
...
@@ -359,10 +360,21 @@ function DragAndDropTemplates(configuration) {
...
@@ -359,10 +360,21 @@ function DragAndDropTemplates(configuration) {
};
};
var
sidebarTemplate
=
function
(
ctx
)
{
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
(
return
(
h
(
"section.action-toolbar-item.sidebar-buttons"
,
{},
[
h
(
"section.action-toolbar-item.sidebar-buttons"
,
{},
[
sidebarButtonTemplate
(
"keyboard-help-button"
,
"fa-question"
,
gettext
(
'Keyboard Help'
)),
sidebarButtonTemplate
(
"keyboard-help-button"
,
"fa-question"
,
gettext
(
'Keyboard Help'
)),
sidebarButtonTemplate
(
"reset-button"
,
"fa-refresh"
,
gettext
(
'Reset'
),
ctx
.
disable_reset_button
),
sidebarButtonTemplate
(
"reset-button"
,
"fa-refresh"
,
gettext
(
'Reset'
),
ctx
.
disable_reset_button
),
showAnswerButton
,
])
])
)
)
};
};
...
@@ -434,9 +446,8 @@ function DragAndDropTemplates(configuration) {
...
@@ -434,9 +446,8 @@ function DragAndDropTemplates(configuration) {
var
mainTemplate
=
function
(
ctx
)
{
var
mainTemplate
=
function
(
ctx
)
{
var
problemTitle
=
ctx
.
show_title
?
h
(
'h3.problem-title'
,
{
innerHTML
:
ctx
.
title_html
})
:
null
;
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
;
var
problemHeader
=
ctx
.
show_problem_header
?
h
(
'h4.title1'
,
gettext
(
'Problem'
))
:
null
;
// Render only items in the bank here, including placeholders. Placed
// Render only items_in_bank and items_placed_unaligned here;
// items will be rendered by zoneTemplate.
// items placed in aligned zones will be rendered by zoneTemplate.
var
is_item_placed
=
function
(
i
)
{
return
i
.
is_placed
;
};
var
is_item_placed
=
function
(
i
)
{
return
i
.
is_placed
;
};
var
items_placed
=
$
.
grep
(
ctx
.
items
,
is_item_placed
);
var
items_placed
=
$
.
grep
(
ctx
.
items
,
is_item_placed
);
var
items_in_bank
=
$
.
grep
(
ctx
.
items
,
is_item_placed
,
true
);
var
items_in_bank
=
$
.
grep
(
ctx
.
items
,
is_item_placed
,
true
);
...
@@ -561,6 +572,10 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -561,6 +572,10 @@ function DragAndDropBlock(runtime, element, configuration) {
$element
.
on
(
'keydown'
,
'.reset-button'
,
function
(
evt
)
{
$element
.
on
(
'keydown'
,
'.reset-button'
,
function
(
evt
)
{
runOnKey
(
evt
,
RET
,
resetProblem
);
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
// 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.
// to watch for load events on any child element, since load events do not bubble.
...
@@ -1098,6 +1113,26 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -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
)
{
var
doAttempt
=
function
(
evt
)
{
evt
.
preventDefault
();
evt
.
preventDefault
();
state
.
submit_spinner
=
true
;
state
.
submit_spinner
=
true
;
...
@@ -1147,6 +1182,10 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -1147,6 +1182,10 @@ function DragAndDropBlock(runtime, element, configuration) {
return
any_items_placed
&&
(
configuration
.
mode
!==
DragAndDropBlock
.
ASSESSMENT_MODE
||
attemptsRemain
());
return
any_items_placed
&&
(
configuration
.
mode
!==
DragAndDropBlock
.
ASSESSMENT_MODE
||
attemptsRemain
());
};
};
var
canShowAnswer
=
function
()
{
return
configuration
.
mode
===
DragAndDropBlock
.
ASSESSMENT_MODE
&&
!
attemptsRemain
();
};
var
attemptsRemain
=
function
()
{
var
attemptsRemain
=
function
()
{
return
!
configuration
.
max_attempts
||
configuration
.
max_attempts
>
state
.
attempts
;
return
!
configuration
.
max_attempts
||
configuration
.
max_attempts
>
state
.
attempts
;
};
};
...
@@ -1207,7 +1246,7 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -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
// 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.
// 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
;
configuration
.
mode
===
DragAndDropBlock
.
ASSESSMENT_MODE
;
var
context
=
{
var
context
=
{
...
@@ -1220,6 +1259,7 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -1220,6 +1259,7 @@ function DragAndDropBlock(runtime, element, configuration) {
problem_html
:
configuration
.
problem_text
,
problem_html
:
configuration
.
problem_text
,
show_problem_header
:
configuration
.
show_problem_header
,
show_problem_header
:
configuration
.
show_problem_header
,
show_submit_answer
:
configuration
.
mode
==
DragAndDropBlock
.
ASSESSMENT_MODE
,
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_src
:
configuration
.
target_img_expanded_url
,
target_img_description
:
configuration
.
target_img_description
,
target_img_description
:
configuration
.
target_img_description
,
display_zone_labels
:
configuration
.
display_zone_labels
,
display_zone_labels
:
configuration
.
display_zone_labels
,
...
@@ -1233,8 +1273,11 @@ function DragAndDropBlock(runtime, element, configuration) {
...
@@ -1233,8 +1273,11 @@ function DragAndDropBlock(runtime, element, configuration) {
feedback_messages
:
state
.
feedback
,
feedback_messages
:
state
.
feedback
,
overall_feedback_messages
:
state
.
overall_feedback
,
overall_feedback_messages
:
state
.
overall_feedback
,
disable_reset_button
:
!
canReset
(),
disable_reset_button
:
!
canReset
(),
disable_show_answer_button
:
!
canShowAnswer
(),
disable_submit_button
:
!
canSubmitAttempt
(),
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
);
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"
...
@@ -217,6 +217,14 @@ msgid "Max number of attempts reached"
msgstr ""
msgstr ""
#: drag_and_drop_v2.py
#: 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"
msgid "Unknown DnDv2 mode {mode} - course is misconfigured"
msgstr ""
msgstr ""
...
@@ -446,6 +454,14 @@ msgid "Reset"
...
@@ -446,6 +454,14 @@ msgid "Reset"
msgstr ""
msgstr ""
#: public/js/drag_and_drop.js
#: 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"
msgid "Submit"
msgstr ""
msgstr ""
...
@@ -529,7 +545,13 @@ msgid_plural "Correctly placed {correct_count} items."
...
@@ -529,7 +545,13 @@ msgid_plural "Correctly placed {correct_count} items."
msgstr[0] ""
msgstr[0] ""
msgstr[1] ""
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 "Misplaced {misplaced_count} item. Misplaced item was returned to item bank."
msgid_plural "Misplaced {misplaced_count} items. Misplaced items were returned to item bank."
msgid_plural "Misplaced {misplaced_count} items. Misplaced items were returned to item bank."
msgstr[0] ""
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"
...
@@ -272,6 +272,14 @@ msgid "Max number of attempts reached"
msgstr "Mäx nümßér öf ättémpts réäçhéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
msgstr "Mäx nümßér öf ättémpts réäçhéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
#: drag_and_drop_v2.py
#: 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"
msgid "Unknown DnDv2 mode {mode} - course is misconfigured"
msgstr "Ûnknöwn DnDv2 mödé {mode} - çöürsé ïs mïsçönfïgüréd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
msgstr "Ûnknöwn DnDv2 mödé {mode} - çöürsé ïs mïsçönfïgüréd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
...
@@ -520,6 +528,14 @@ msgid "Reset"
...
@@ -520,6 +528,14 @@ msgid "Reset"
msgstr "Rését Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
msgstr "Rését Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
#: public/js/drag_and_drop.js
#: 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"
msgid "Submit"
msgstr "Süßmït Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
msgstr "Süßmït Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
...
@@ -617,12 +633,18 @@ msgid_plural "Correctly placed {correct_count} items."
...
@@ -617,12 +633,18 @@ msgid_plural "Correctly placed {correct_count} items."
msgstr[0] "Çörréçtlý pläçéd {correct_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
msgstr[0] "Çörréçtlý pläçéd {correct_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
msgstr[1] "Çörréçtlý pläçéd {correct_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
msgstr[1] "Çörréçtlý pläçéd {correct_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
#: utils.py:
3
2
#: utils.py:
6
2
msgid "Misplaced {misplaced_count} item.
Misplaced item was returned to item bank.
"
msgid "Misplaced {misplaced_count} item."
msgid_plural "Misplaced {misplaced_count} items.
Misplaced items were returned to item bank.
"
msgid_plural "Misplaced {misplaced_count} items."
msgstr[0] "Mïspläçéd {misplaced_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
msgstr[0] "Mïspläçéd {misplaced_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
msgstr[1] "Mïspläçéd {misplaced_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
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
#: utils.py:40
msgid "Did not place {missing_count} required item."
msgid "Did not place {missing_count} required item."
msgid_plural "Did not place {missing_count} required items."
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):
...
@@ -60,6 +60,17 @@ class FeedbackMessages(object):
Formats "misplaced items" message
Formats "misplaced items" message
"""
"""
return
ngettext
(
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} item. Misplaced item was returned to item bank.'
,
'Misplaced {misplaced_count} items. Misplaced items were returned to item bank.'
,
'Misplaced {misplaced_count} items. Misplaced items were returned to item bank.'
,
number
number
...
...
tests/integration/test_base.py
View file @
85c6143c
...
@@ -126,6 +126,9 @@ class BaseIntegrationTest(SeleniumBaseTest):
...
@@ -126,6 +126,9 @@ class BaseIntegrationTest(SeleniumBaseTest):
def
_get_reset_button
(
self
):
def
_get_reset_button
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
'.reset-button'
)
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
):
def
_get_submit_button
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
'.submit-answer-button'
)
return
self
.
_page
.
find_element_by_css_selector
(
'.submit-answer-button'
)
...
@@ -392,12 +395,21 @@ class InteractionTestBase(object):
...
@@ -392,12 +395,21 @@ class InteractionTestBase(object):
self
.
assertDraggable
(
item_value
)
self
.
assertDraggable
(
item_value
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option'
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option'
)
self
.
assertEqual
(
item
.
get_attribute
(
'tabindex'
),
'0'
)
self
.
assertEqual
(
item
.
get_attribute
(
'tabindex'
),
'0'
)
self
.
assertEqual
(
item_description
.
text
,
'Placed in: {}'
.
format
(
zone_title
))
description
=
'Placed in: {}'
else
:
else
:
self
.
assertNotDraggable
(
item_value
)
self
.
assertNotDraggable
(
item_value
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option fade'
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option fade'
)
self
.
assertIsNone
(
item
.
get_attribute
(
'tabindex'
))
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
):
def
assert_reverted_item
(
self
,
item_value
):
item
=
self
.
_get_item_by_value
(
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
...
@@ -5,7 +5,8 @@ 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
,
BOTTOM_ZONE_ID
,
ITEM_CORRECT_FEEDBACK
,
ITEM_INCORRECT_FEEDBACK
,
ITEM_TOP_ZONE_NAME
,
ITEM_MIDDLE_ZONE_NAME
,
ITEM_TOP_ZONE_NAME
,
ITEM_MIDDLE_ZONE_NAME
,
)
)
from
tests.integration.test_base
import
BaseIntegrationTest
,
DefaultDataTestMixin
,
InteractionTestBase
,
ItemDefinition
from
tests.integration.test_base
import
BaseIntegrationTest
,
DefaultDataTestMixin
,
InteractionTestBase
,
ItemDefinition
...
@@ -146,6 +147,20 @@ class AssessmentEventsFiredTest(
...
@@ -146,6 +147,20 @@ class AssessmentEventsFiredTest(
self
.
assertEqual
(
name
,
event
[
'name'
])
self
.
assertEqual
(
name
,
event
[
'name'
])
self
.
assertEqual
(
published_data
,
event
[
'data'
])
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
@ddt
class
ItemDroppedEventTest
(
DefaultDataTestMixin
,
BaseEventsTests
):
class
ItemDroppedEventTest
(
DefaultDataTestMixin
,
BaseEventsTests
):
...
...
tests/integration/test_interaction.py
View file @
85c6143c
...
@@ -442,7 +442,7 @@ class MultipleBlocksDataInteraction(ParameterizedTestsMixin, InteractionTestBase
...
@@ -442,7 +442,7 @@ class MultipleBlocksDataInteraction(ParameterizedTestsMixin, InteractionTestBase
self
.
_switch_to_block
(
1
)
self
.
_switch_to_block
(
1
)
# Test mouse and keyboard interaction
# 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
)
self
.
interact_with_keyboard_help
(
scroll_down
=
0
,
use_keyboard
=
True
)
...
...
tests/integration/test_interaction_assessment.py
View file @
85c6143c
# -*- coding: utf-8 -*-
# Imports ###########################################################
# Imports ###########################################################
from
ddt
import
ddt
,
data
from
ddt
import
ddt
,
data
...
@@ -7,7 +9,6 @@ import time
...
@@ -7,7 +9,6 @@ import time
from
selenium.webdriver.support.ui
import
WebDriverWait
from
selenium.webdriver.support.ui
import
WebDriverWait
from
selenium.webdriver.common.keys
import
Keys
from
selenium.webdriver.common.keys
import
Keys
from
workbench.runtime
import
WorkbenchRuntime
from
xblockutils.resources
import
ResourceLoader
from
xblockutils.resources
import
ResourceLoader
from
drag_and_drop_v2.default_data
import
(
from
drag_and_drop_v2.default_data
import
(
...
@@ -55,6 +56,14 @@ class AssessmentTestMixin(object):
...
@@ -55,6 +56,14 @@ class AssessmentTestMixin(object):
submit_button
.
click
()
submit_button
.
click
()
self
.
wait_for_ajax
()
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
@ddt
class
AssessmentInteractionTest
(
class
AssessmentInteractionTest
(
...
@@ -150,6 +159,58 @@ class AssessmentInteractionTest(
...
@@ -150,6 +159,58 @@ class AssessmentInteractionTest(
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
'true'
)
self
.
assertEqual
(
submit_button
.
get_attribute
(
'disabled'
),
'true'
)
self
.
assertEqual
(
reset_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
):
def
test_do_attempt_feedback_is_updated
(
self
):
"""
"""
Test updating overall feedback after submitting solution in assessment mode
Test updating overall feedback after submitting solution in assessment mode
...
@@ -174,7 +235,7 @@ class AssessmentInteractionTest(
...
@@ -174,7 +235,7 @@ class AssessmentInteractionTest(
feedback_lines
=
[
feedback_lines
=
[
"FEEDBACK"
,
"FEEDBACK"
,
FeedbackMessages
.
correctly_placed
(
1
),
FeedbackMessages
.
correctly_placed
(
1
),
FeedbackMessages
.
misplaced
(
1
),
FeedbackMessages
.
misplaced
_returned
(
1
),
FeedbackMessages
.
not_placed
(
2
),
FeedbackMessages
.
not_placed
(
2
),
START_FEEDBACK
START_FEEDBACK
]
]
...
@@ -199,26 +260,6 @@ class AssessmentInteractionTest(
...
@@ -199,26 +260,6 @@ class AssessmentInteractionTest(
expected_feedback
=
"
\n
"
.
join
(
feedback_lines
)
expected_feedback
=
"
\n
"
.
join
(
feedback_lines
)
self
.
assertEqual
(
self
.
_get_feedback
()
.
text
,
expected_feedback
)
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
):
def
test_per_item_feedback_multiple_misplaced
(
self
):
self
.
place_item
(
0
,
MIDDLE_ZONE_ID
,
Keys
.
RETURN
)
self
.
place_item
(
0
,
MIDDLE_ZONE_ID
,
Keys
.
RETURN
)
self
.
place_item
(
1
,
BOTTOM_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):
...
@@ -190,6 +190,14 @@ class StandardModeFixture(BaseDragAndDropAjaxFixture):
self
.
assertEqual
(
res
.
status_code
,
400
)
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
@ddt.ddt
class
AssessmentModeFixture
(
BaseDragAndDropAjaxFixture
):
class
AssessmentModeFixture
(
BaseDragAndDropAjaxFixture
):
...
@@ -205,6 +213,12 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
...
@@ -205,6 +213,12 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
data
=
self
.
_make_submission
(
item_id
,
zone_id
)
data
=
self
.
_make_submission
(
item_id
,
zone_id
)
self
.
call_handler
(
self
.
DROP_ITEM_HANDLER
,
data
)
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
def
_submit_complete_solution
(
self
):
# pylint: disable=no-self-use
raise
NotImplementedError
()
raise
NotImplementedError
()
...
@@ -402,6 +416,51 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
...
@@ -402,6 +416,51 @@ class AssessmentModeFixture(BaseDragAndDropAjaxFixture):
self
.
assertEqual
(
self
.
block
.
item_state
,
original_item_state
)
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
):
class
TestDragAndDropHtmlData
(
StandardModeFixture
,
unittest
.
TestCase
):
FOLDER
=
"html"
FOLDER
=
"html"
...
@@ -468,6 +527,12 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
...
@@ -468,6 +527,12 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
self
.
assertEqual
(
res
[
self
.
FEEDBACK_KEY
],
expected_item_feedback
)
self
.
assertEqual
(
res
[
self
.
FEEDBACK_KEY
],
expected_item_feedback
)
self
.
assertEqual
(
res
[
self
.
OVERALL_FEEDBACK_KEY
],
expected_overall_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
):
def
_submit_complete_solution
(
self
):
self
.
_submit_solution
({
0
:
self
.
ZONE_1
,
1
:
self
.
ZONE_2
,
2
:
self
.
ZONE_2
})
self
.
_submit_solution
({
0
:
self
.
ZONE_1
,
1
:
self
.
ZONE_2
,
2
:
self
.
ZONE_2
})
...
@@ -486,15 +551,35 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
...
@@ -486,15 +551,35 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
expected_item_feedback
=
[
self
.
_make_feedback_message
(
self
.
FEEDBACK
[
0
][
'incorrect'
])]
expected_item_feedback
=
[
self
.
_make_feedback_message
(
self
.
FEEDBACK
[
0
][
'incorrect'
])]
expected_overall_feedback
=
[
expected_overall_feedback
=
[
self
.
_make_feedback_message
(
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
)
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
):
def
test_do_attempt_no_item_state
(
self
):
"""
"""
Test do_attempt overall feedback when no item state is saved - no items were ever dropped.
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):
...
@@ -517,11 +602,21 @@ class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase):
expected_item_feedback
=
[]
expected_item_feedback
=
[]
expected_overall_feedback
=
[
expected_overall_feedback
=
[
self
.
_make_feedback_message
(
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
)
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):
...
@@ -47,6 +47,7 @@ class TestCaseMixin(object):
DROP_ITEM_HANDLER
=
'drop_item'
DROP_ITEM_HANDLER
=
'drop_item'
DO_ATTEMPT_HANDLER
=
'do_attempt'
DO_ATTEMPT_HANDLER
=
'do_attempt'
RESET_HANDLER
=
'reset'
RESET_HANDLER
=
'reset'
SHOW_ANSWER_HANDLER
=
'show_answer'
USER_STATE_HANDLER
=
'get_user_state'
USER_STATE_HANDLER
=
'get_user_state'
def
patch_workbench
(
self
):
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