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
a0028a7b
Commit
a0028a7b
authored
Aug 02, 2016
by
E. Kolpakov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[SOL-1944] Submitting answers in assessment mode
parent
630a8d7f
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
551 additions
and
162 deletions
+551
-162
.travis.yml
+2
-1
README.md
+19
-10
drag_and_drop_v2/__init__.py
+1
-0
drag_and_drop_v2/default_data.py
+86
-95
drag_and_drop_v2/drag_and_drop_v2.py
+0
-0
drag_and_drop_v2/public/css/drag_and_drop.css
+51
-0
drag_and_drop_v2/public/js/drag_and_drop.js
+65
-11
drag_and_drop_v2/translations/en/LC_MESSAGES/text.po
+31
-5
drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po
+36
-18
drag_and_drop_v2/utils.py
+75
-2
pylintrc
+8
-7
tests/integration/test_interaction.py
+112
-1
tests/pylintrc
+23
-0
tests/unit/data/assessment/config_out.json
+8
-2
tests/unit/data/assessment/data.json
+12
-2
tests/unit/test_advanced.py
+0
-0
tests/unit/test_basics.py
+8
-8
tests/utils.py
+14
-0
No files found.
.travis.yml
View file @
a0028a7b
...
...
@@ -12,7 +12,8 @@ install:
-
"
pip
install
dist/xblock-drag-and-drop-v2-2.0.7.tar.gz"
script
:
-
pep8 drag_and_drop_v2 tests --max-line-length=120
-
pylint drag_and_drop_v2 tests
-
pylint drag_and_drop_v2
-
pylint tests --rcfile=tests/pylintrc
-
python run_tests.py
notifications
:
email
:
false
...
...
README.md
View file @
a0028a7b
...
...
@@ -96,11 +96,19 @@ and Drop component to a lesson, then click the `EDIT` button.

In the first step, you can set some basic properties of the component,
such as the title, the
mode, the maximum number of attempts, the maximum score,
In the first step, you can set some basic properties of the component,
such as
the title, the problem
mode, the maximum number of attempts, the maximum score,
the problem text to render above the background image, the introductory feedback
(shown initially), and the final feedback (shown after the learner successfully
completes the drag and drop problem).
completes the drag and drop problem, or when the learner runs out of attempts).
There are two problem modes available:
*
**Standard**
: In this mode, the learner gets immediate feedback on each
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.

...
...
@@ -126,13 +134,14 @@ potentially, overlap the zones below.

In the final step, you define the background and text color for drag
items, as well as the drag items themselves. A drag item can contain
either text or an image. You can define custom success and error
feedback for each item. The feedback text is displayed in a popup
after the learner drops the item on a zone - the success feedback is
shown if the item is dropped on a correct zone, while the error
feedback is shown when dropping the item on an incorrect drop zone.
In the final step, you define the background and text color for drag items, as
well as the drag items themselves. A drag item can contain either text or an
image. You can define custom success and error feedback for each item. In
standard mode, the feedback text is displayed in a popup after the learner drops
the item on a zone - the success feedback is shown if the item is dropped on a
correct zone, while the error feedback is shown when dropping the item on an
incorrect drop zone. In assessment mode, the success and error feedback texts
are not used.
You can select any number of zones for an item to belong to using
the checkboxes; all zones defined in the previous step are available.
...
...
drag_and_drop_v2/__init__.py
View file @
a0028a7b
""" Drag and Drop v2 XBlock """
from
.drag_and_drop_v2
import
DragAndDropBlock
drag_and_drop_v2/default_data.py
View file @
a0028a7b
""" Default data for Drag and Drop v2 XBlock """
from
.utils
import
_
TARGET_IMG_DESCRIPTION
=
_
(
...
...
@@ -27,100 +28,90 @@ START_FEEDBACK = _("Drag the items onto the image above.")
FINISH_FEEDBACK
=
_
(
"Good work! You have completed this drag and drop problem."
)
DEFAULT_DATA
=
{
"targetImgDescription"
:
TARGET_IMG_DESCRIPTION
,
"zones"
:
[
{
"uid"
:
TOP_ZONE_ID
,
"title"
:
TOP_ZONE_TITLE
,
"description"
:
TOP_ZONE_DESCRIPTION
,
"x"
:
160
,
"y"
:
30
,
"width"
:
196
,
"height"
:
178
,
"targetImgDescription"
:
TARGET_IMG_DESCRIPTION
,
"zones"
:
[
{
"uid"
:
TOP_ZONE_ID
,
"title"
:
TOP_ZONE_TITLE
,
"description"
:
TOP_ZONE_DESCRIPTION
,
"x"
:
160
,
"y"
:
30
,
"width"
:
196
,
"height"
:
178
,
},
{
"uid"
:
MIDDLE_ZONE_ID
,
"title"
:
MIDDLE_ZONE_TITLE
,
"description"
:
MIDDLE_ZONE_DESCRIPTION
,
"x"
:
86
,
"y"
:
210
,
"width"
:
340
,
"height"
:
138
,
},
{
"uid"
:
BOTTOM_ZONE_ID
,
"title"
:
BOTTOM_ZONE_TITLE
,
"description"
:
BOTTOM_ZONE_DESCRIPTION
,
"x"
:
15
,
"y"
:
350
,
"width"
:
485
,
"height"
:
135
,
}
],
"items"
:
[
{
"displayName"
:
_
(
"Goes to the top"
),
"feedback"
:
{
"incorrect"
:
ITEM_INCORRECT_FEEDBACK
,
"correct"
:
ITEM_CORRECT_FEEDBACK
.
format
(
zone
=
TOP_ZONE_TITLE
)
},
"zones"
:
[
TOP_ZONE_ID
],
"imageURL"
:
""
,
"id"
:
0
,
},
{
"displayName"
:
_
(
"Goes to the middle"
),
"feedback"
:
{
"incorrect"
:
ITEM_INCORRECT_FEEDBACK
,
"correct"
:
ITEM_CORRECT_FEEDBACK
.
format
(
zone
=
MIDDLE_ZONE_TITLE
)
},
"zones"
:
[
MIDDLE_ZONE_ID
],
"imageURL"
:
""
,
"id"
:
1
,
},
{
"displayName"
:
_
(
"Goes to the bottom"
),
"feedback"
:
{
"incorrect"
:
ITEM_INCORRECT_FEEDBACK
,
"correct"
:
ITEM_CORRECT_FEEDBACK
.
format
(
zone
=
BOTTOM_ZONE_TITLE
)
},
"zones"
:
[
BOTTOM_ZONE_ID
],
"imageURL"
:
""
,
"id"
:
2
,
},
{
"displayName"
:
_
(
"Goes anywhere"
),
"feedback"
:
{
"incorrect"
:
""
,
"correct"
:
ITEM_ANY_ZONE_FEEDBACK
},
"zones"
:
[
TOP_ZONE_ID
,
BOTTOM_ZONE_ID
,
MIDDLE_ZONE_ID
],
"imageURL"
:
""
,
"id"
:
3
},
{
"displayName"
:
_
(
"I don't belong anywhere"
),
"feedback"
:
{
"incorrect"
:
ITEM_NO_ZONE_FEEDBACK
,
"correct"
:
""
},
"zones"
:
[],
"imageURL"
:
""
,
"id"
:
4
,
},
],
"feedback"
:
{
"start"
:
START_FEEDBACK
,
"finish"
:
FINISH_FEEDBACK
,
},
{
"uid"
:
MIDDLE_ZONE_ID
,
"title"
:
MIDDLE_ZONE_TITLE
,
"description"
:
MIDDLE_ZONE_DESCRIPTION
,
"x"
:
86
,
"y"
:
210
,
"width"
:
340
,
"height"
:
138
,
},
{
"uid"
:
BOTTOM_ZONE_ID
,
"title"
:
BOTTOM_ZONE_TITLE
,
"description"
:
BOTTOM_ZONE_DESCRIPTION
,
"x"
:
15
,
"y"
:
350
,
"width"
:
485
,
"height"
:
135
,
}
],
"items"
:
[
{
"displayName"
:
_
(
"Goes to the top"
),
"feedback"
:
{
"incorrect"
:
ITEM_INCORRECT_FEEDBACK
,
"correct"
:
ITEM_CORRECT_FEEDBACK
.
format
(
zone
=
TOP_ZONE_TITLE
)
},
"zones"
:
[
TOP_ZONE_ID
],
"imageURL"
:
""
,
"id"
:
0
,
},
{
"displayName"
:
_
(
"Goes to the middle"
),
"feedback"
:
{
"incorrect"
:
ITEM_INCORRECT_FEEDBACK
,
"correct"
:
ITEM_CORRECT_FEEDBACK
.
format
(
zone
=
MIDDLE_ZONE_TITLE
)
},
"zones"
:
[
MIDDLE_ZONE_ID
],
"imageURL"
:
""
,
"id"
:
1
,
},
{
"displayName"
:
_
(
"Goes to the bottom"
),
"feedback"
:
{
"incorrect"
:
ITEM_INCORRECT_FEEDBACK
,
"correct"
:
ITEM_CORRECT_FEEDBACK
.
format
(
zone
=
BOTTOM_ZONE_TITLE
)
},
"zones"
:
[
BOTTOM_ZONE_ID
],
"imageURL"
:
""
,
"id"
:
2
,
},
{
"displayName"
:
_
(
"Goes anywhere"
),
"feedback"
:
{
"incorrect"
:
""
,
"correct"
:
ITEM_ANY_ZONE_FEEDBACK
},
"zones"
:
[
TOP_ZONE_ID
,
BOTTOM_ZONE_ID
,
MIDDLE_ZONE_ID
],
"imageURL"
:
""
,
"id"
:
3
},
{
"displayName"
:
_
(
"I don't belong anywhere"
),
"feedback"
:
{
"incorrect"
:
ITEM_NO_ZONE_FEEDBACK
,
"correct"
:
""
},
"zones"
:
[],
"imageURL"
:
""
,
"id"
:
4
,
},
],
"feedback"
:
{
"start"
:
START_FEEDBACK
,
"finish"
:
FINISH_FEEDBACK
,
},
}
drag_and_drop_v2/drag_and_drop_v2.py
View file @
a0028a7b
This diff is collapsed.
Click to expand it.
drag_and_drop_v2/public/css/drag_and_drop.css
View file @
a0028a7b
...
...
@@ -284,6 +284,57 @@
border-top
:
solid
1px
#bdbdbd
;
}
.xblock--drag-and-drop
.feedback
p
{
margin
:
2px
0
;
padding
:
0.5em
;
border
:
2px
solid
#999
;
}
.xblock--drag-and-drop
.feedback
p
.correct
{
color
:
#166e36
;
border
:
2px
solid
#166e36
;
}
.xblock--drag-and-drop
.feedback
p
.partial
{
color
:
#166e36
;
border
:
2px
solid
#166e36
;
}
.xblock--drag-and-drop
.feedback
p
.incorrect
{
color
:
#b20610
;
border
:
2px
solid
#b20610
;
}
/* Font Awesome icons have different width - margin-right tries to compensate it */
.xblock--drag-and-drop
.feedback
p
:before
{
content
:
"\f129"
;
font-family
:
FontAwesome
;
margin-right
:
0.7em
;
margin-left
:
0.3em
;
}
.xblock--drag-and-drop
.feedback
p
.correct
:before
{
content
:
"\f00c"
;
font-family
:
FontAwesome
;
margin-right
:
0.3em
;
margin-left
:
0
;
}
.xblock--drag-and-drop
.feedback
p
.partial
:before
{
content
:
"\f069"
;
font-family
:
FontAwesome
;
margin-right
:
0.3em
;
margin-left
:
0
;
}
.xblock--drag-and-drop
.feedback
p
.incorrect
:before
{
content
:
"\f00d"
;
font-family
:
FontAwesome
;
margin-right
:
0.45em
;
margin-left
:
0.1em
;
}
.xblock--drag-and-drop
.popup
{
position
:
absolute
;
display
:
none
;
...
...
drag_and_drop_v2/public/js/drag_and_drop.js
View file @
a0028a7b
...
...
@@ -227,12 +227,21 @@ function DragAndDropTemplates(configuration) {
};
var
feedbackTemplate
=
function
(
ctx
)
{
var
feedback_display
=
ctx
.
feedback_html
?
'block'
:
'none'
;
var
properties
=
{
attributes
:
{
'aria-live'
:
'polite'
}
};
var
messages
=
ctx
.
overall_feedback_messages
||
[];
var
feedback_display
=
messages
.
length
>
0
?
'block'
:
'none'
;
var
feedback_messages
=
messages
.
map
(
function
(
message
)
{
var
selector
=
"p.message"
;
if
(
message
.
message_class
)
{
selector
+=
"."
+
message
.
message_class
;
}
return
h
(
selector
,
{
innerHTML
:
message
.
message
},
[]);
});
return
(
h
(
'section.feedback'
,
properties
,
[
h
(
'h3.title1'
,
{
style
:
{
display
:
feedback_display
}
},
gettext
(
'Feedback'
)),
h
(
'
p.message'
,
{
style
:
{
display
:
feedback_display
},
innerHTML
:
ctx
.
feedback_html
}
)
h
(
'
div.messages'
,
{
style
:
{
display
:
feedback_display
}
},
feedback_messages
)
])
);
};
...
...
@@ -266,20 +275,23 @@ function DragAndDropTemplates(configuration) {
var
submitAnswerTemplate
=
function
(
ctx
)
{
var
attemptsUsedId
=
"attempts-used-"
+
configuration
.
url_name
;
var
attemptsUsedDisplay
=
(
ctx
.
max_attempts
&&
ctx
.
max_attempts
>
0
)
?
'inline'
:
'none'
;
var
button_enabled
=
ctx
.
items
.
some
(
function
(
item
)
{
return
item
.
is_placed
;})
&&
(
ctx
.
max_attempts
===
null
||
ctx
.
max_attempts
>
ctx
.
num_attempts
);
return
(
h
(
"section.action-toolbar-item.submit-answer"
,
{},
[
h
(
"button.btn-brand.submit-answer-button"
,
{
disabled
:
!
button_enabled
,
attributes
:
{
"aria-describedby"
:
attemptsUsedId
}},
gettext
(
"Submit"
)
{
disabled
:
ctx
.
disable_submit_button
||
ctx
.
submit_spinner
,
attributes
:
{
"aria-describedby"
:
attemptsUsedId
}},
[
(
ctx
.
submit_spinner
?
h
(
"span.fa.fa-spin.fa-spinner"
)
:
null
),
gettext
(
"Submit"
)
]
),
h
(
"span.attempts-used#"
+
attemptsUsedId
,
{
style
:
{
display
:
attemptsUsedDisplay
}},
gettext
(
"You have used {used} of {total} attempts."
)
.
replace
(
"{used}"
,
ctx
.
num_
attempts
).
replace
(
"{total}"
,
ctx
.
max_attempts
)
.
replace
(
"{used}"
,
ctx
.
attempts
).
replace
(
"{total}"
,
ctx
.
max_attempts
)
)
])
);
...
...
@@ -444,6 +456,7 @@ function DragAndDropBlock(runtime, element, configuration) {
// Set up event handlers:
$
(
document
).
on
(
'keydown mousedown touchstart'
,
closePopup
);
$element
.
on
(
'click'
,
'.submit-answer-button'
,
doAttempt
);
$element
.
on
(
'click'
,
'.keyboard-help-button'
,
showKeyboardHelp
);
$element
.
on
(
'keydown'
,
'.keyboard-help-button'
,
function
(
evt
)
{
runOnKey
(
evt
,
RET
,
showKeyboardHelp
);
...
...
@@ -897,7 +910,7 @@ function DragAndDropBlock(runtime, element, configuration) {
if
(
!
zone
)
{
return
;
}
var
url
=
runtime
.
handlerUrl
(
element
,
'd
o_attempt
'
);
var
url
=
runtime
.
handlerUrl
(
element
,
'd
rop_item
'
);
var
data
=
{
val
:
item_id
,
zone
:
zone
,
...
...
@@ -969,6 +982,45 @@ function DragAndDropBlock(runtime, element, configuration) {
});
};
var
doAttempt
=
function
(
evt
)
{
evt
.
preventDefault
();
state
.
submit_spinner
=
true
;
applyState
();
$
.
ajax
({
type
:
'POST'
,
url
:
runtime
.
handlerUrl
(
element
,
"do_attempt"
),
data
:
'{}'
}).
done
(
function
(
data
){
state
.
attempts
=
data
.
attempts
;
state
.
overall_feedback
=
data
.
overall_feedback
;
if
(
attemptsRemain
())
{
data
.
misplaced_items
.
forEach
(
function
(
misplaced_item_id
)
{
delete
state
.
items
[
misplaced_item_id
]
});
}
else
{
state
.
finished
=
true
;
}
focusFirstDraggable
();
}).
always
(
function
()
{
state
.
submit_spinner
=
false
;
applyState
();
});
};
var
canSubmitAttempt
=
function
()
{
return
Object
.
keys
(
state
.
items
).
length
>
0
&&
attemptsRemain
();
};
var
canReset
=
function
()
{
return
Object
.
keys
(
state
.
items
).
length
>
0
&&
(
configuration
.
mode
!==
DragAndDropBlock
.
ASSESSMENT_MODE
||
attemptsRemain
())
};
var
attemptsRemain
=
function
()
{
return
!
configuration
.
max_attempts
||
configuration
.
max_attempts
>
state
.
attempts
;
};
var
render
=
function
()
{
var
items
=
configuration
.
items
.
map
(
function
(
item
)
{
var
item_user_state
=
state
.
items
[
item
.
id
];
...
...
@@ -1028,7 +1080,7 @@ function DragAndDropBlock(runtime, element, configuration) {
show_title
:
configuration
.
show_title
,
mode
:
configuration
.
mode
,
max_attempts
:
configuration
.
max_attempts
,
num_attempts
:
state
.
num_
attempts
,
attempts
:
state
.
attempts
,
problem_html
:
configuration
.
problem_text
,
show_problem_header
:
configuration
.
show_problem_header
,
show_submit_answer
:
configuration
.
mode
==
DragAndDropBlock
.
ASSESSMENT_MODE
,
...
...
@@ -1042,8 +1094,10 @@ function DragAndDropBlock(runtime, element, configuration) {
last_action_correct
:
state
.
last_action_correct
,
item_bank_focusable
:
item_bank_focusable
,
popup_html
:
state
.
feedback
||
''
,
feedback_html
:
$
.
trim
(
state
.
overall_feedback
),
disable_reset_button
:
Object
.
keys
(
state
.
items
).
length
==
0
,
overall_feedback_messages
:
state
.
overall_feedback
,
disable_reset_button
:
!
canReset
(),
disable_submit_button
:
!
canSubmitAttempt
(),
submit_spinner
:
state
.
submit_spinner
};
return
renderView
(
context
);
...
...
drag_and_drop_v2/translations/en/LC_MESSAGES/text.po
View file @
a0028a7b
...
...
@@ -286,10 +286,6 @@ msgid ""
msgstr ""
#: templates/html/js_templates.html
msgid "Zones"
msgstr ""
#: templates/html/js_templates.html
msgid "Use text that is clear and descriptive of the item to be placed"
msgstr ""
...
...
@@ -323,6 +319,7 @@ msgstr ""
msgid "Final Feedback"
msgstr ""
#: templates/html/js_templates.html
#: templates/html/drag_and_drop_edit.html
msgid "Zones"
msgstr ""
...
...
@@ -421,7 +418,10 @@ msgid "Correctly placed in: {zone_title}"
msgstr ""
#: public/js/drag_and_drop.js
msgid "Reset problem"
msgid "Reset"
msgstr ""
msgid "Submit"
msgstr ""
#: public/js/drag_and_drop.js
...
...
@@ -485,3 +485,29 @@ msgstr ""
#: public/js/drag_and_drop_edit.js
msgid "Error: "
msgstr ""
#: utils.py:18
msgid "Final attempt was used, highest score is {score}"
msgstr ""
#: utils.py:19
msgid "Misplaced items were returned to item bank."
msgstr ""
#: utils.py:24
msgid "Correctly placed {correct_count} item."
msgid_plural "Correctly placed {correct_count} items."
msgstr[0] ""
msgstr[1] ""
#: utils.py:32
msgid "Misplaced {misplaced_count} item."
msgid_plural "Misplaced {misplaced_count} items."
msgstr[0] ""
msgstr[1] ""
#: utils.py:40
msgid "Did not place {missing_count} required item."
msgid_plural "Did not place {missing_count} required items."
msgstr[0] ""
msgstr[1] ""
drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po
View file @
a0028a7b
...
...
@@ -164,6 +164,7 @@ msgstr ""
"Ìf thé välüé ïs nöt sét, ïnfïnïté ättémpts äré ällöwéd. Ⱡ'σяєм #"
#: drag_and_drop_v2.py
#: templates/html/drag_and_drop_edit.html
msgid "Show title"
msgstr "Shöw tïtlé Ⱡ'σяєм ιρѕυм ∂σłσ#"
...
...
@@ -173,6 +174,7 @@ msgstr ""
"Dïspläý thé tïtlé tö thé léärnér? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#"
#: drag_and_drop_v2.py
#: templates/html/drag_and_drop_edit.html
msgid "Problem text"
msgstr "Prößlém téxt Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
...
...
@@ -349,6 +351,7 @@ msgstr ""
"äütömätïç wïdth): Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#"
#: templates/html/js_templates.html
#: templates/html/drag_and_drop_edit.html
msgid "Zones"
msgstr "Zönés Ⱡ'σяєм ιρѕ#"
...
...
@@ -379,22 +382,10 @@ msgid "Problem mode"
msgstr "Prößlém mödé Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: templates/html/drag_and_drop_edit.html
msgid "Show title"
msgstr "Shöw tïtlé Ⱡ'σяєм ιρѕυм ∂σłσ#"
#: templates/html/drag_and_drop_edit.html
msgid "Maximum score"
msgstr "Mäxïmüm sçöré Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
#: templates/html/drag_and_drop_edit.html
msgid "Problem text"
msgstr "Prößlém téxt Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: templates/html/drag_and_drop_edit.html
msgid "Show \"Problem\" heading"
msgstr "Shöw \"Prößlém\" héädïng Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#"
#: templates/html/drag_and_drop_edit.html
msgid "Introductory Feedback"
msgstr "Ìntrödüçtörý Féédßäçk Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
...
...
@@ -403,10 +394,6 @@ msgid "Final Feedback"
msgstr "Fïnäl Féédßäçk Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
#: templates/html/drag_and_drop_edit.html
msgid "Zones"
msgstr "Zönés Ⱡ'σяєм ιρѕ#"
#: templates/html/drag_and_drop_edit.html
msgid "Background URL"
msgstr "Bäçkgröünd ÛRL Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
...
...
@@ -506,8 +493,11 @@ msgid "Correctly placed in: {zone_title}"
msgstr "Çörréçtlý pläçéd ïn: {zone_title} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: public/js/drag_and_drop.js
msgid "Reset problem"
msgstr "Rését prößlém Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
msgid "Reset"
msgstr "Rését Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
msgid "Submit"
msgstr "Süßmït Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
#: public/js/drag_and_drop.js
msgid "Feedback"
...
...
@@ -584,3 +574,30 @@ msgstr "Nöné Ⱡ'σяєм ι#"
#: public/js/drag_and_drop_edit.js
msgid "Error: "
msgstr "Érrör: Ⱡ'σяєм ιρѕυм #"
#: utils.py:18
msgid "Fïnäl ättémpt wäs üséd, hïghést sçöré ïs {score} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
msgstr ""
#: utils.py:19
msgid "Mïspläçéd ïtéms wéré rétürnéd tö ïtém ßänk. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
msgstr ""
#: utils.py:24
msgid "Çörréçtlý pläçéd {correct_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
msgid_plural "Çörréçtlý pläçéd {correct_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
msgstr[0] ""
msgstr[1] ""
#: utils.py:32
msgid "Mïspläçéd {misplaced_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
msgid_plural "Mïspläçéd {misplaced_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
msgstr[0] ""
msgstr[1] ""
#: utils.py:40
msgid "Dïd nöt pläçé {missing_count} réqüïréd ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
msgid_plural "Dïd nöt pläçé {missing_count} réqüïréd ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
msgstr[0] ""
msgstr[1] ""
\ No newline at end of file
drag_and_drop_v2/utils.py
View file @
a0028a7b
# -*- coding: utf-8 -*-
#
""" Drag and Drop v2 XBlock - Utils """
from
collections
import
namedtuple
# Make '_' a no-op so we can scrape strings
def
_
(
text
):
""" Dummy `gettext` replacement to make string extraction tools scrape strings marked for translation """
return
text
def
ngettext_fallback
(
text_singular
,
text_plural
,
number
):
""" Dummy `ngettext` replacement to make string extraction tools scrape strings marked for translation """
if
number
==
1
:
return
text_singular
else
:
return
text_plural
class
DummyTranslationService
(
object
):
"""
Dummy drop-in replacement for i18n XBlock service
"""
gettext
=
_
ngettext
=
ngettext_fallback
class
FeedbackMessages
(
object
):
"""
Feedback messages collection
"""
class
MessageClasses
(
object
):
"""
Namespace for message classes
"""
CORRECT_SOLUTION
=
"correct"
PARTIAL_SOLUTION
=
"partial"
INCORRECT_SOLUTION
=
"incorrect"
CORRECTLY_PLACED
=
CORRECT_SOLUTION
MISPLACED
=
INCORRECT_SOLUTION
NOT_PLACED
=
INCORRECT_SOLUTION
FINAL_ATTEMPT_TPL
=
_
(
'Final attempt was used, highest score is {score}'
)
MISPLACED_ITEMS_RETURNED
=
_
(
'Misplaced item(s) were returned to item bank.'
)
@staticmethod
def
correctly_placed
(
number
,
ngettext
=
ngettext_fallback
):
"""
Formats "correctly placed items" message
"""
return
ngettext
(
'Correctly placed {correct_count} item.'
,
'Correctly placed {correct_count} items.'
,
number
)
.
format
(
correct_count
=
number
)
@staticmethod
def
misplaced
(
number
,
ngettext
=
ngettext_fallback
):
"""
Formats "misplaced items" 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
)
.
format
(
misplaced_count
=
number
)
@staticmethod
def
not_placed
(
number
,
ngettext
=
ngettext_fallback
):
"""
Formats "did not place required items" message
"""
return
ngettext
(
'Did not place {missing_count} required item.'
,
'Did not place {missing_count} required items.'
,
number
)
.
format
(
missing_count
=
number
)
FeedbackMessage
=
namedtuple
(
"FeedbackMessage"
,
[
"message"
,
"message_class"
])
# pylint: disable=invalid-name
pylintrc
View file @
a0028a7b
...
...
@@ -6,18 +6,19 @@ max-line-length=120
[MESSAGES CONTROL]
disable=
attribute-defined-outside-init,
locally-disabled,
missing-docstring,
too-many-ancestors,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
too-few-public-methods,
too-many-public-methods,
unused-argument,
invalid-name,
no-member
unused-argument
[SIMILARITIES]
min-similarity-lines=4
[OPTIONS]
good-names=_,__,log,loader
method-rgx=_?[a-z_][a-z0-9_]{2,40}$
function-rgx=_?[a-z_][a-z0-9_]{2,40}$
method-name-hint=_?[a-z_][a-z0-9_]{2,40}$
function-name-hint=_?[a-z_][a-z0-9_]{2,40}$
tests/integration/test_interaction.py
View file @
a0028a7b
...
...
@@ -17,6 +17,7 @@ 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
...
...
@@ -481,6 +482,23 @@ class DefaultAssessmentDataTestMixin(DefaultDataTestMixin):
"""
.
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
()
@ddt
class
StandardInteractionTest
(
DefaultDataTestMixin
,
InteractionTestBase
,
BaseIntegrationTest
):
"""
...
...
@@ -512,7 +530,9 @@ class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseInt
@ddt
class
AssessmentInteractionTest
(
DefaultAssessmentDataTestMixin
,
InteractionTestBase
,
BaseIntegrationTest
):
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.
...
...
@@ -562,6 +582,97 @@ class AssessmentInteractionTest(DefaultAssessmentDataTestMixin, InteractionTestB
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
):
...
...
tests/pylintrc
0 → 100644
View file @
a0028a7b
[REPORTS]
reports=no
[FORMAT]
max-line-length=120
[MESSAGES CONTROL]
disable=
attribute-defined-outside-init,
locally-disabled,
missing-docstring,
abstract-class-little-used,
too-many-ancestors,
too-few-public-methods,
too-many-public-methods,
invalid-name,
no-member
[SIMILARITIES]
min-similarity-lines=4
[OPTIONS]
max-args=6
tests/unit/data/assessment/config_out.json
View file @
a0028a7b
...
...
@@ -47,16 +47,22 @@
"id"
:
1
},
{
"displayName"
:
"3"
,
"imageURL"
:
""
,
"expandedImageURL"
:
""
,
"id"
:
2
},
{
"displayName"
:
"X"
,
"imageURL"
:
"/static/test_url_expansion"
,
"expandedImageURL"
:
"/course/test-course/assets/test_url_expansion"
,
"id"
:
2
"id"
:
3
},
{
"displayName"
:
""
,
"imageURL"
:
"http://placehold.it/200x100"
,
"expandedImageURL"
:
"http://placehold.it/200x100"
,
"id"
:
3
"id"
:
4
}
]
}
tests/unit/data/assessment/data.json
View file @
a0028a7b
...
...
@@ -41,6 +41,16 @@
"id"
:
1
},
{
"displayName"
:
"3"
,
"feedback"
:
{
"incorrect"
:
"No 3"
,
"correct"
:
"Yes 3"
},
"zone"
:
"zone-2"
,
"imageURL"
:
""
,
"id"
:
2
},
{
"displayName"
:
"X"
,
"feedback"
:
{
"incorrect"
:
""
,
...
...
@@ -48,7 +58,7 @@
},
"zone"
:
"none"
,
"imageURL"
:
"/static/test_url_expansion"
,
"id"
:
2
"id"
:
3
},
{
"displayName"
:
""
,
...
...
@@ -58,7 +68,7 @@
},
"zone"
:
"none"
,
"imageURL"
:
"http://placehold.it/200x100"
,
"id"
:
3
"id"
:
4
}
],
...
...
tests/unit/test_advanced.py
View file @
a0028a7b
This diff is collapsed.
Click to expand it.
tests/unit/test_basics.py
View file @
a0028a7b
...
...
@@ -69,20 +69,20 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self
.
assertEqual
(
self
.
call_handler
(
"get_user_state"
),
{
'items'
:
{},
'finished'
:
False
,
"
num_
attempts"
:
0
,
'overall_feedback'
:
START_FEEDBACK
,
"attempts"
:
0
,
'overall_feedback'
:
[{
"message"
:
START_FEEDBACK
,
"message_class"
:
None
}]
})
assert_user_state_empty
()
# Drag three items into the correct spot:
data
=
{
"val"
:
0
,
"zone"
:
TOP_ZONE_ID
,
"x_percent"
:
"33
%
"
,
"y_percent"
:
"11
%
"
}
self
.
call_handler
(
'do_attempt'
,
data
)
self
.
call_handler
(
self
.
DROP_ITEM_HANDLER
,
data
)
data
=
{
"val"
:
1
,
"zone"
:
MIDDLE_ZONE_ID
,
"x_percent"
:
"67
%
"
,
"y_percent"
:
"80
%
"
}
self
.
call_handler
(
'do_attempt'
,
data
)
self
.
call_handler
(
self
.
DROP_ITEM_HANDLER
,
data
)
data
=
{
"val"
:
2
,
"zone"
:
BOTTOM_ZONE_ID
,
"x_percent"
:
"99
%
"
,
"y_percent"
:
"95
%
"
}
self
.
call_handler
(
'do_attempt'
,
data
)
self
.
call_handler
(
self
.
DROP_ITEM_HANDLER
,
data
)
data
=
{
"val"
:
3
,
"zone"
:
MIDDLE_ZONE_ID
,
"x_percent"
:
"67
%
"
,
"y_percent"
:
"80
%
"
}
self
.
call_handler
(
'do_attempt'
,
data
)
self
.
call_handler
(
self
.
DROP_ITEM_HANDLER
,
data
)
# Check the result:
self
.
assertTrue
(
self
.
block
.
completed
)
...
...
@@ -100,8 +100,8 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
'3'
:
{
'x_percent'
:
'67
%
'
,
'y_percent'
:
'80
%
'
,
'correct'
:
True
,
"zone"
:
MIDDLE_ZONE_ID
},
},
'finished'
:
True
,
"
num_
attempts"
:
0
,
'overall_feedback'
:
FINISH_FEEDBACK
,
"attempts"
:
0
,
'overall_feedback'
:
[{
"message"
:
FINISH_FEEDBACK
,
"message_class"
:
None
}]
,
})
# Reset to initial conditions
...
...
tests/utils.py
View file @
a0028a7b
import
json
import
random
import
re
from
mock
import
patch
...
...
@@ -31,10 +32,23 @@ def make_block():
return
drag_and_drop_v2
.
DragAndDropBlock
(
runtime
,
field_data
,
scope_ids
=
scope_ids
)
def
generate_max_and_attempts
(
count
=
100
):
for
_
in
xrange
(
count
):
max_attempts
=
random
.
randint
(
1
,
100
)
attempts
=
random
.
randint
(
0
,
100
)
expect_validation_error
=
max_attempts
<=
attempts
yield
max_attempts
,
attempts
,
expect_validation_error
class
TestCaseMixin
(
object
):
""" Helpful mixins for unittest TestCase subclasses """
maxDiff
=
None
DROP_ITEM_HANDLER
=
'drop_item'
DO_ATTEMPT_HANDLER
=
'do_attempt'
RESET_HANDLER
=
'reset'
USER_STATE_HANDLER
=
'get_user_state'
def
patch_workbench
(
self
):
self
.
apply_patch
(
'workbench.runtime.WorkbenchRuntime.local_resource_url'
,
...
...
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