Commit 140c261f by Braden MacDonald

Merge pull request #4 from open-craft/resizable-image

Make the image resize to fit the container, make item placement relative
parents 1ace6d32 541295cc
...@@ -10,7 +10,8 @@ install: ...@@ -10,7 +10,8 @@ install:
- "python setup.py sdist" - "python setup.py sdist"
- "pip install dist/xblock-drag-and-drop-v2-0.1.tar.gz" - "pip install dist/xblock-drag-and-drop-v2-0.1.tar.gz"
script: script:
- pep8 drag_and_drop_v2 --max-line-length=120 - pep8 drag_and_drop_v2 tests --max-line-length=120
- pylint drag_and_drop_v2 tests
- python run_tests.py - python run_tests.py
notifications: notifications:
email: false email: false
......
...@@ -4,21 +4,21 @@ DEFAULT_DATA = { ...@@ -4,21 +4,21 @@ DEFAULT_DATA = {
"zones": [ "zones": [
{ {
"index": 1, "index": 1,
"width": 200, "id": "zone-1",
"title": _("Zone 1"), "title": _("Zone 1"),
"height": 100, "x": 160,
"x": "120", "y": 30,
"y": "200", "width": 196,
"id": "zone-1" "height": 178,
}, },
{ {
"index": 2, "index": 2,
"width": 200, "id": "zone-2",
"title": _("Zone 2"), "title": _("Zone 2"),
"height": 100, "x": 86,
"x": "120", "y": 210,
"y": "360", "width": 340,
"id": "zone-2" "height": 140,
} }
], ],
"items": [ "items": [
...@@ -31,10 +31,6 @@ DEFAULT_DATA = { ...@@ -31,10 +31,6 @@ DEFAULT_DATA = {
"zone": "Zone 1", "zone": "Zone 1",
"backgroundImage": "", "backgroundImage": "",
"id": 0, "id": 0,
"size": {
"width": "190px",
"height": "auto"
}
}, },
{ {
"displayName": "2", "displayName": "2",
...@@ -45,10 +41,6 @@ DEFAULT_DATA = { ...@@ -45,10 +41,6 @@ DEFAULT_DATA = {
"zone": "Zone 2", "zone": "Zone 2",
"backgroundImage": "", "backgroundImage": "",
"id": 1, "id": 1,
"size": {
"width": "190px",
"height": "auto"
}
}, },
{ {
"displayName": "X", "displayName": "X",
...@@ -59,18 +51,10 @@ DEFAULT_DATA = { ...@@ -59,18 +51,10 @@ DEFAULT_DATA = {
"zone": "none", "zone": "none",
"backgroundImage": "", "backgroundImage": "",
"id": 2, "id": 2,
"size": {
"width": "100px",
"height": "100px"
}
}, },
], ],
"state": {
"items": {},
"finished": True
},
"feedback": { "feedback": {
"start": _("Intro Feed"), "start": _("Drag the items onto the image above."),
"finish": _("Final Feed") "finish": _("Good work! You have completed this drag and drop exercise.")
}, },
} }
...@@ -12,7 +12,7 @@ from xblock.core import XBlock ...@@ -12,7 +12,7 @@ from xblock.core import XBlock
from xblock.fields import Scope, String, Dict, Float, Boolean from xblock.fields import Scope, String, Dict, Float, Boolean
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .utils import _, render_template, load_resource from .utils import _, render_template, load_resource # pylint: disable=unused-import
from .default_data import DEFAULT_DATA from .default_data import DEFAULT_DATA
...@@ -125,10 +125,40 @@ class DragAndDropBlock(XBlock): ...@@ -125,10 +125,40 @@ class DragAndDropBlock(XBlock):
for js_url in js_urls: for js_url in js_urls:
fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url)) fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url))
fragment.initialize_js('DragAndDropBlock') fragment.initialize_js('DragAndDropBlock', self.get_configuration())
return fragment return fragment
def get_configuration(self):
"""
Get the configuration data for the student_view.
The configuration is all the settings defined by the author, except for correct answers
and feedback.
"""
def items_without_answers():
items = copy.deepcopy(self.data.get('items', ''))
for item in items:
del item['feedback']
del item['zone']
item['inputOptions'] = 'inputOptions' in item
return items
return {
"zones": self.data.get('zones', []),
"display_zone_labels": self.data.get('displayLabels', False),
"items": items_without_answers(),
"title": self.display_name,
"show_title": self.show_title,
"question_text": self.question_text,
"show_question_header": self.show_question_header,
"target_img_expanded_url": self.target_img_expanded_url,
"item_background_color": self.item_background_color or None,
"item_text_color": self.item_text_color or None,
"initial_feedback": self.data['feedback']['start'],
# final feedback (data.feedback.finish) is not included - it may give away answers.
}
def studio_view(self, context): def studio_view(self, context):
""" """
Editing view in Studio Editing view in Studio
...@@ -164,7 +194,11 @@ class DragAndDropBlock(XBlock): ...@@ -164,7 +194,11 @@ class DragAndDropBlock(XBlock):
for js_url in js_urls: for js_url in js_urls:
fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url)) fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url))
fragment.initialize_js('DragAndDropEditBlock') fragment.initialize_js('DragAndDropEditBlock', {
'data': self.data,
'target_img_expanded_url': self.target_img_expanded_url,
'default_background_image_url': self.default_background_image_url,
})
return fragment return fragment
...@@ -183,18 +217,13 @@ class DragAndDropBlock(XBlock): ...@@ -183,18 +217,13 @@ class DragAndDropBlock(XBlock):
'result': 'success', 'result': 'success',
} }
@XBlock.handler
def get_data(self, request, suffix=''):
data = self._get_data()
return webob.Response(body=json.dumps(data), content_type='application/json')
@XBlock.json_handler @XBlock.json_handler
def do_attempt(self, attempt, suffix=''): def do_attempt(self, attempt, suffix=''):
item = next(i for i in self.data['items'] if i['id'] == attempt['val']) item = next(i for i in self.data['items'] if i['id'] == attempt['val'])
state = None state = None
feedback = item['feedback']['incorrect'] feedback = item['feedback']['incorrect']
final_feedback = None overall_feedback = None
is_correct = False is_correct = False
is_correct_location = False is_correct_location = False
...@@ -220,16 +249,15 @@ class DragAndDropBlock(XBlock): ...@@ -220,16 +249,15 @@ class DragAndDropBlock(XBlock):
is_correct = True is_correct = True
feedback = item['feedback']['correct'] feedback = item['feedback']['correct']
state = { state = {
'top': attempt['top'], 'x_percent': attempt['x_percent'],
'left': attempt['left'], 'y_percent': attempt['y_percent'],
'absolute': True # flag for backwards compatibility (values used to be relative)
} }
if state: if state:
self.item_state[str(item['id'])] = state self.item_state[str(item['id'])] = state
if self._is_finished(): if self._is_finished():
final_feedback = self.data['feedback']['finish'] overall_feedback = self.data['feedback']['finish']
# don't publish the grade if the student has already completed the exercise # don't publish the grade if the student has already completed the exercise
if not self.completed: if not self.completed:
...@@ -259,49 +287,72 @@ class DragAndDropBlock(XBlock): ...@@ -259,49 +287,72 @@ class DragAndDropBlock(XBlock):
'correct': is_correct, 'correct': is_correct,
'correct_location': is_correct_location, 'correct_location': is_correct_location,
'finished': self._is_finished(), 'finished': self._is_finished(),
'final_feedback': final_feedback, 'overall_feedback': overall_feedback,
'feedback': feedback 'feedback': feedback
} }
@XBlock.json_handler @XBlock.json_handler
def reset(self, data, suffix=''): def reset(self, data, suffix=''):
self.item_state = {} self.item_state = {}
return self._get_data() return self._get_user_state()
def _expand_static_url(self, url):
"""
This is required to make URLs like '/static/dnd-test-image.png' work (note: that is the
only portable URL format for static files that works across export/import and reruns).
This method is unfortunately a bit hackish since XBlock does not provide a low-level API
for this.
"""
if hasattr(self.runtime, 'replace_urls'):
url = self.runtime.replace_urls('"{}"'.format(url))[1:-1]
elif hasattr(self.runtime, 'course_id'):
# edX Studio uses a different runtime for 'studio_view' than 'student_view',
# and the 'studio_view' runtime doesn't provide the replace_urls API.
try:
from static_replace import replace_static_urls # pylint: disable=import-error
url = replace_static_urls('"{}"'.format(url), None, course_id=self.runtime.course_id)[1:-1]
except ImportError:
pass
return url
def _get_data(self): @XBlock.json_handler
data = copy.deepcopy(self.data) def expand_static_url(self, url, suffix=''):
""" AJAX-accessible handler for expanding URLs to static [image] files """
return {'url': self._expand_static_url(url)}
@property
def target_img_expanded_url(self):
""" Get the expanded URL to the target image (the image items are dragged onto). """
if self.data.get("targetImg"):
return self._expand_static_url(self.data["targetImg"])
else:
return self.default_background_image_url
for item in data['items']: @property
# Strip answers def default_background_image_url(self):
del item['feedback'] """ The URL to the default background image, shown when no custom background is used """
del item['zone'] return self.runtime.local_resource_url(self, "public/img/triangle.png")
item['inputOptions'] = 'inputOptions' in item
if not self._is_finished(): @XBlock.handler
del data['feedback']['finish'] def get_user_state(self, request, suffix=''):
""" GET all user-specific data, and any applicable feedback """
data = self._get_user_state()
return webob.Response(body=json.dumps(data), content_type='application/json')
def _get_user_state(self):
""" Get all user-specific data, and any applicable feedback """
item_state = self._get_item_state() item_state = self._get_item_state()
for item_id, item in item_state.iteritems(): for item_id, item in item_state.iteritems():
definition = next(i for i in self.data['items'] if str(i['id']) == item_id) definition = next(i for i in self.data['items'] if str(i['id']) == item_id)
item['correct_input'] = self._is_correct_input(definition, item.get('input')) item['correct_input'] = self._is_correct_input(definition, item.get('input'))
data['state'] = { is_finished = self._is_finished()
return {
'items': item_state, 'items': item_state,
'finished': self._is_finished() 'finished': is_finished,
'overall_feedback': self.data['feedback']['finish' if is_finished else 'start'],
} }
data['title'] = self.display_name
data['show_title'] = self.show_title
data['question_text'] = self.question_text
data['show_question_header'] = self.show_question_header
if self.item_background_color:
data['item_background_color'] = self.item_background_color
if self.item_text_color:
data['item_text_color'] = self.item_text_color
return data
def _get_item_state(self): def _get_item_state(self):
""" """
Returns the user item state. Returns the user item state.
......
.xblock--drag-and-drop { .xblock--drag-and-drop {
width: 770px; width: auto;
max-width: 770px;
margin: 0; margin: 0;
padding: 0; padding: 0;
background: #fff;
} }
/* Header, instruction text, etc. */
.xblock--drag-and-drop .problem-header { .xblock--drag-and-drop .problem-header {
display: inline-block; display: inline-block;
margin: 0 0 15px 0; margin: 0 0 15px 0;
} }
.xblock--drag-and-drop .problem-progress {
display: inline-block;
padding-left: 5px;
color: #666;
font-weight: 100;
font-size: 1em;
}
.xblock--drag-and-drop .problem p { .xblock--drag-and-drop .problem p {
margin-bottom: 1.41575em; margin-bottom: 1.41575em;
} }
.xblock--drag-and-drop .drag-container { /* Shared styles used in header and footer */
width: 515px;
padding: 1%;
background: #ebf0f2;
position: relative;
}
.xblock--drag-and-drop .clear {
clear: both;
}
.xblock--drag-and-drop .title1 { .xblock--drag-and-drop .title1 {
color: rgb(85, 85, 85); color: rgb(85, 85, 85);
...@@ -42,22 +27,48 @@ ...@@ -42,22 +27,48 @@
margin-top: 20px; margin-top: 20px;
} }
/* drag-container holds the .items and the .target image */
.xblock--drag-and-drop .drag-container {
width: auto;
padding: 5px;
background: #ebf0f2;
}
/*.xblock--drag-and-drop .clear {
clear: both;
}*/
/** Draggable Items **/ /** Draggable Items **/
.xblock--drag-and-drop .drag-container .items { .xblock--drag-and-drop .item-bank {
display: block; display: -ms-flexbox;
padding: 0 !important; /* LMS tries to override this */ display: flex;
-ms-flex-flow: row wrap;
flex-flow: row wrap;
-ms-justify-content: flex-start;
justify-content: flex-start;
-ms-flex-align: center;
align-items: center;
position: relative; position: relative;
border: 1px solid rgba(0,0,0, 0.1);
border-radius: 3px;
padding: 5px;
} }
.xblock--drag-and-drop .drag-container .option { .xblock--drag-and-drop .drag-container .option {
position: relative;
display: inline-block; display: inline-block;
width: auto; width: auto;
min-width: 4em;
max-width: calc(100% / 3 - 1% - 1% - 20px);
border: 1px solid transparent;
border-radius: 3px; border-radius: 3px;
margin: 1%; margin: 5px;
padding: 10px; padding: 10px;
background: #2e83cd; background-color: #2e83cd;
font-size: 14px; font-size: 14px;
color: #fff; color: #fff;
opacity: 1; opacity: 1;
...@@ -65,13 +76,22 @@ ...@@ -65,13 +76,22 @@
z-index: 10 !important; z-index: 10 !important;
} }
/* Placed option */
.xblock--drag-and-drop .drag-container .target .option {
position: absolute;
margin: 0;
transform: translate(-50%, -50%); /* These blocks are to be centered on their absolute x,y position */
}
.xblock--drag-and-drop .drag-container .ui-draggable-dragging { .xblock--drag-and-drop .drag-container .ui-draggable-dragging {
box-shadow: 0 16px 32px 0 rgba(0,0,0,.3); box-shadow: 0 16px 32px 0 rgba(0,0,0,.3);
border: 1px solid #ccc; border: 1px solid #ccc;
opacity: .65; opacity: .65;
z-index: 20 !important;
} }
.xblock--drag-and-drop .drag-container .option img { .xblock--drag-and-drop .drag-container .option img {
display: block;
max-width: 100%; max-width: 100%;
} }
...@@ -88,11 +108,10 @@ ...@@ -88,11 +108,10 @@
} }
.xblock--drag-and-drop .drag-container .option .numerical-input .submit-input { .xblock--drag-and-drop .drag-container .option .numerical-input .submit-input {
box-sizing: border-box;
position: absolute; position: absolute;
left: 150px; left: 150px;
top: 4px; top: 4px;
height: 24px; white-space: nowrap; /* Fix cross-browser issue: Without this declaration, button text wraps in Chrome/Chromium */
} }
.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input-submit, .xblock--drag-and-drop .drag-container .option .numerical-input.correct .input-submit,
...@@ -117,18 +136,27 @@ ...@@ -117,18 +136,27 @@
/*** Drop Target ***/ /*** Drop Target ***/
.xblock--drag-and-drop .target { .xblock--drag-and-drop .target {
display: block; display: table;
width: 515px; /* 'display: table' makes this have the smallest size that fits the .target-img
height: 510px; while still allowing the image to use 'max-width: 100%' and scale proportionally.
The end result is that this element has the same width and height as the image, so we
can use it as a 'position: relative' anchor for the placed elements. */
height: auto;
position: relative; position: relative;
margin-top: 1%; margin-top: 1%;
background: #fff; background: #fff;
} }
.xblock--drag-and-drop .target-img-wrapper {
/* This element is required for Firefox due to https://bugzilla.mozilla.org/show_bug.cgi?id=975632 */
display: table-row;
}
.xblock--drag-and-drop .target-img { .xblock--drag-and-drop .target-img {
background: url('../img/triangle.png') no-repeat; display: table-cell;
width: 100%; width: 100%;
height: 100%; max-width: 100%;
height: auto;
} }
.xblock--drag-and-drop .zone { .xblock--drag-and-drop .zone {
......
/*** xBlock styles ***/ .xblock--drag-and-drop--editor {
.xblock--drag-and-drop { width: 100%;
width: 100%; height: 100%;
margin: 0;
padding: 0;
background: #fff;
}
.modal-window .editor-with-buttons.xblock--drag-and-drop {
/* Fix Studio edito height */
margin-bottom: 0;
height: 380px;
}
/** Draggable Items **/
.xblock--drag-and-drop .items {
width: 210px;
margin: 10px;
padding: 0;
font-size: 14px;
position: relative;
display: inline;
float: left;
list-style-type: none;
} }
.modal-window .drag-builder {
/*** Drop Target ***/
.xblock--drag-and-drop .target {
width: 515px;
height: 510px;
position: relative;
display: inline;
float: left;
margin: 10px 0 15px 5px;
background: #fff;
z-index: 1;
}
.xblock--drag-and-drop .target-img {
background: url('../img/triangle.png') no-repeat;
width: 100%; width: 100%;
height: 100%; height: calc(100% - 60px);
position: absolute;
overflow-y: scroll;
} }
.xblock--drag-and-drop .zone { /*** Drop Target ***/
.xblock--drag-and-drop--editor .zone {
position: absolute; position: absolute;
display: -webkit-box; display: -webkit-box;
...@@ -68,9 +35,11 @@ ...@@ -68,9 +35,11 @@
box-pack:center; box-pack:center;
box-align:center; box-align:center;
border: 1px dotted #666;
box-sizing: border-box;
} }
.xblock--drag-and-drop .zone p { .xblock--drag-and-drop--editor .zone p {
width: 100%; width: 100%;
font-family: Arial; font-family: Arial;
font-size: 16px; font-size: 16px;
...@@ -82,153 +51,164 @@ ...@@ -82,153 +51,164 @@
} }
/*** IE9 alignment fix ***/ /*** IE9 alignment fix ***/
.lt-ie10 .xblock--drag-and-drop .zone { .lt-ie10 .xblock--drag-and-drop--editor .zone {
display: table; display: table;
} }
.lt-ie10 .xblock--drag-and-drop .zone p { .lt-ie10 .xblock--drag-and-drop--editor .zone p {
display: table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
} }
/** Builder **/ /** Builder **/
.xblock--drag-and-drop .hidden { .xblock--drag-and-drop--editor .hidden {
display: none !important; display: none !important;
} }
.xblock--drag-and-drop .drag-builder { .xblock--drag-and-drop--editor .tab {
height: 100%;
overflow: scroll;
}
.xblock--drag-and-drop .drag-builder .tab {
width: 100%; width: 100%;
background: #eee; background: #eee;
padding: 3px 0; padding: 3px 0;
position: relative; position: relative;
} }
.xblock--drag-and-drop .drag-builder .tab:after, .xblock--drag-and-drop--editor .tab::after,
.xblock--drag-and-drop .drag-builder .tab-footer:after, .xblock--drag-and-drop--editor .tab-footer::after {
.xblock--drag-and-drop .drag-builder .target:after {
content: ""; content: "";
display: table; display: table;
clear: both; clear: both;
} }
.xblock--drag-and-drop .drag-builder .tab h3 { .xblock--drag-and-drop--editor .tab h3 {
margin: 20px 0 8px 0; margin: 20px 0 8px 0;
} }
.xblock--drag-and-drop .drag-builder .tab-header, .xblock--drag-and-drop--editor .tab-header,
.xblock--drag-and-drop .drag-builder .tab-content, .xblock--drag-and-drop--editor .tab-content,
.xblock--drag-and-drop .drag-builder .tab-footer { .xblock--drag-and-drop--editor .tab-footer {
width: 96%; width: 96%;
margin: 2%; margin: 2%;
} }
.xblock--drag-and-drop .drag-builder .tab-footer { .xblock--drag-and-drop--editor .tab-footer {
height: 25px; height: 25px;
position: relative; position: relative;
display: block; display: block;
float: left; float: left;
} }
.xblock--drag-and-drop .drag-builder .items { .xblock--drag-and-drop--editor .items {
width: calc(100% - 515px); width: calc(100% - 515px);
margin: 10px 0 0 0; margin: 10px 0 0 0;
} }
.xblock--drag-and-drop .drag-builder .target { .xblock--drag-and-drop--editor .target-image-form input {
margin-left: 0; width: 50%;
} }
.xblock--drag-and-drop .drag-builder .target-image-form input { /* Zones Tab */
width: 50%; .xblock--drag-and-drop--editor .zones-tab .zone-editor {
position: relative;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: flex-start;
justify-content: space-between;
}
.xblock--drag-and-drop--editor .zones-tab .tab-content .controls {
width: 40%;
max-width: 50%;
min-width: 330px;
margin-right: 15px;
} }
.xblock--drag-and-drop .zones-form .zone-row label { .xblock--drag-and-drop--editor .zones-tab .tab-content .target {
position: relative;
border: 1px solid #ccc;
overflow: hidden;
}
.xblock--drag-and-drop--editor .zones-tab .tab-content .target-img {
display: block;
width: auto;
height: auto;
max-width: 100%;
}
.xblock--drag-and-drop--editor .zones-form .zone-row label {
display: inline-block; display: inline-block;
width: 18%; width: 18%;
} }
.xblock--drag-and-drop .zones-form .zone-row .title { .xblock--drag-and-drop--editor .zones-form .zone-row .title {
width: 60%; width: 60%;
margin: 0 0 5px; margin: 0 0 5px;
} }
.xblock--drag-and-drop .zones-form .zone-row .layout { .xblock--drag-and-drop--editor .zones-form .zone-row .layout {
margin-bottom: 15px; margin-bottom: 15px;
} }
.xblock--drag-and-drop .zones-form .zone-row .layout .size, .xblock--drag-and-drop--editor .zones-form .zone-row .layout .size,
.xblock--drag-and-drop .zones-form .zone-row .layout .coord { .xblock--drag-and-drop--editor .zones-form .zone-row .layout .coord {
width: 15%; width: 15%;
margin: 0 19px 5px 0; margin: 0 19px 5px 0;
} }
.xblock--drag-and-drop .drag-builder .target { .xblock--drag-and-drop--editor .feedback-form textarea {
margin-bottom: 40px;
}
.xblock--drag-and-drop .drag-builder .zone {
border: 1px dotted #666;
}
.xblock--drag-and-drop .feedback-form textarea {
width: 99%; width: 99%;
height: 128px; height: 128px;
} }
.xblock--drag-and-drop .item-styles-form .item-styles-form-help { .xblock--drag-and-drop--editor .item-styles-form .item-styles-form-help {
margin-top: 5px; margin-top: 5px;
font-size: small; font-size: small;
} }
.xblock--drag-and-drop .item-styles-form, .xblock--drag-and-drop--editor .item-styles-form,
.xblock--drag-and-drop .items-form { .xblock--drag-and-drop--editor .items-form {
margin-bottom: 30px; margin-bottom: 30px;
} }
.xblock--drag-and-drop .items-form .item { .xblock--drag-and-drop--editor .items-form .item {
background: #73bde7; background: #73bde7;
padding: 10px 0 1px; padding: 10px 0 1px;
margin: 15px 0; margin: 15px 0;
} }
.xblock--drag-and-drop .items-form label { .xblock--drag-and-drop--editor .items-form label {
margin: 0 1%; margin: 0 1%;
} }
.xblock--drag-and-drop .items-form input, .xblock--drag-and-drop--editor .items-form input,
.xblock--drag-and-drop .items-form select { .xblock--drag-and-drop--editor .items-form select {
width: 35%; width: 35%;
} }
.xblock--drag-and-drop .items-form .item-width, .xblock--drag-and-drop--editor .items-form .item-width,
.xblock--drag-and-drop .items-form .item-height { .xblock--drag-and-drop--editor .items-form .item-height {
width: 40px; width: 40px;
} }
.xblock--drag-and-drop .items-form .item-numerical-value, .xblock--drag-and-drop--editor .items-form .item-numerical-value,
.xblock--drag-and-drop .items-form .item-numerical-margin { .xblock--drag-and-drop--editor .items-form .item-numerical-margin {
width: 60px; width: 60px;
} }
.xblock--drag-and-drop .items-form textarea { .xblock--drag-and-drop--editor .items-form textarea {
width: 97%; width: 97%;
margin: 0 1%; margin: 0 1%;
} }
.xblock--drag-and-drop .items-form .row { .xblock--drag-and-drop--editor .items-form .row {
margin-bottom: 20px; margin-bottom: 20px;
} }
/** Buttons **/ /** Buttons **/
.xblock--drag-and-drop .btn { .xblock--drag-and-drop--editor .btn {
background: #2e83cd; background: #2e83cd;
color: #fff; color: #fff;
border: 1px solid #156ab4; border: 1px solid #156ab4;
...@@ -236,33 +216,33 @@ ...@@ -236,33 +216,33 @@
padding: 5px 10px; padding: 5px 10px;
} }
.xblock--drag-and-drop .btn:hover { .xblock--drag-and-drop--editor .btn:hover {
opacity: 0.8; opacity: 0.8;
cursor: pointer; cursor: pointer;
} }
.xblock--drag-and-drop .btn:focus { .xblock--drag-and-drop--editor .btn:focus {
outline: none; outline: none;
opacity: 0.5; opacity: 0.5;
} }
.xblock--drag-and-drop .add-element { .xblock--drag-and-drop--editor .add-element {
text-decoration: none; text-decoration: none;
color: #2e83cd; color: #2e83cd;
} }
.xblock--drag-and-drop .remove-zone { .xblock--drag-and-drop--editor .remove-zone {
float: right; float: right;
margin-top: 2px; margin-top: 2px;
margin-right: 16px; margin-right: 16px;
} }
.xblock--drag-and-drop .remove-item { .xblock--drag-and-drop--editor .remove-item {
display: inline-block; display: inline-block;
margin-left: 95px; margin-left: 95px;
} }
.xblock--drag-and-drop .icon { .xblock--drag-and-drop--editor .icon {
width: 14px; width: 14px;
height: 14px; height: 14px;
border-radius: 7px; border-radius: 7px;
...@@ -272,14 +252,14 @@ ...@@ -272,14 +252,14 @@
margin: 0 5px 0 0; margin: 0 5px 0 0;
} }
.xblock--drag-and-drop .add-zone:hover, .xblock--drag-and-drop--editor .add-zone:hover,
.xblock--drag-and-drop .add-zone:hover .icon, .xblock--drag-and-drop--editor .add-zone:hover .icon,
.xblock--drag-and-drop .remove-zone:hover, .xblock--drag-and-drop--editor .remove-zone:hover,
.xblock--drag-and-drop .remove-zone:hover .icon { .xblock--drag-and-drop--editor .remove-zone:hover .icon {
opacity: 0.7; opacity: 0.7;
} }
.xblock--drag-and-drop .icon.add:before { .xblock--drag-and-drop--editor .icon.add:before {
content: ''; content: '';
height: 10px; height: 10px;
width: 2px; width: 2px;
...@@ -291,7 +271,7 @@ ...@@ -291,7 +271,7 @@
left: 6px; left: 6px;
} }
.xblock--drag-and-drop .icon.add:after { .xblock--drag-and-drop--editor .icon.add:after {
content: ''; content: '';
height: 2px; height: 2px;
width: 10px; width: 10px;
...@@ -303,7 +283,7 @@ ...@@ -303,7 +283,7 @@
left: 0; left: 0;
} }
.xblock--drag-and-drop .icon.remove:before { .xblock--drag-and-drop--editor .icon.remove:before {
content: ''; content: '';
height: 10px; height: 10px;
width: 2px; width: 2px;
...@@ -318,7 +298,7 @@ ...@@ -318,7 +298,7 @@
transform: rotate(45deg); transform: rotate(45deg);
} }
.xblock--drag-and-drop .icon.remove:after { .xblock--drag-and-drop--editor .icon.remove:after {
content: ''; content: '';
height: 2px; height: 2px;
width: 10px; width: 10px;
...@@ -333,10 +313,10 @@ ...@@ -333,10 +313,10 @@
transform: rotate(45deg); transform: rotate(45deg);
} }
.xblock--drag-and-drop .remove-item .icon.remove { .xblock--drag-and-drop--editor .remove-item .icon.remove {
background: #fff; background: #fff;
} }
.xblock--drag-and-drop .remove-item .icon.remove:before, .xblock--drag-and-drop--editor .remove-item .icon.remove:before,
.xblock--drag-and-drop .remove-item .icon.remove:after { .xblock--drag-and-drop--editor .remove-item .icon.remove:after {
background: #2e83cd; background: #2e83cd;
} }
(function(h) { (function(h) {
"use strict";
// Set up a mock for gettext if it isn't available in the client runtime:
if (!window.gettext) { window.gettext = function gettext_stub(string) { return string; }; }
var FocusHook = function() { var FocusHook = function() {
if (!(this instanceof FocusHook)) { if (!(this instanceof FocusHook)) {
...@@ -14,10 +18,6 @@ ...@@ -14,10 +18,6 @@
}, 0); }, 0);
}; };
var px = function(n) {
return n + 'px';
};
var renderCollection = function(template, collection, ctx) { var renderCollection = function(template, collection, ctx) {
return collection.map(function(item) { return collection.map(function(item) {
return template(item, ctx); return template(item, ctx);
...@@ -40,35 +40,46 @@ ...@@ -40,35 +40,46 @@
}; };
var itemTemplate = function(item) { var itemTemplate = function(item) {
var style = { var style = {};
width: item.width,
height: item.height,
top: item.top,
left: item.left,
position: item.position
};
if (item.background_color) { if (item.background_color) {
style['background-color'] = item.background_color; style['background-color'] = item.background_color;
} }
if (item.color) { if (item.color) {
style.color = item.color; style.color = item.color;
} }
if (item.is_placed) {
style.left = item.x_percent + "%";
style.top = item.y_percent + "%";
}
return ( return (
h('div.option', {className: item.class_name, h('div.option',
attributes: {'data-value': item.value, 'data-drag-disabled': item.drag_disabled}, {
style: style}, [ key: item.value,
h('div', {innerHTML: item.content_html}), className: item.class_name,
itemInputTemplate(item.input) attributes: {'data-value': item.value, 'data-drag-disabled': item.drag_disabled},
]) style: style
}, [
h('div', {innerHTML: item.content_html}),
itemInputTemplate(item.input)
]
)
); );
}; };
var zoneTemplate = function(zone, ctx) { var zoneTemplate = function(zone, ctx) {
return ( return (
h('div.zone', {id: zone.id, attributes: {'data-zone': zone.title}, h(
style: {top: px(zone.y), left: px(zone.x), 'div.zone',
width: px(zone.width), height: px(zone.height)}}, {
h('p', {style: {visibility: ctx.display_zone_labels ? 'visible': 'hidden'}}, zone.title)) id: zone.id,
attributes: {'data-zone': zone.title},
style: {
top: zone.y_percent + '%', left: zone.x_percent + "%",
width: zone.width_percent + '%', height: zone.height_percent + "%",
}
},
ctx.display_zone_labels ? h('p', zone.title) : null
)
); );
}; };
...@@ -88,28 +99,31 @@ ...@@ -88,28 +99,31 @@
var mainTemplate = function(ctx) { var mainTemplate = function(ctx) {
var problemHeader = ctx.show_title ? h('h2.problem-header', {innerHTML: ctx.header_html}) : null; var problemHeader = ctx.show_title ? h('h2.problem-header', {innerHTML: ctx.header_html}) : null;
var questionHeader = ctx.show_question_header ? h('h3.title1', gettext('Question')) : null; var questionHeader = ctx.show_question_header ? h('h3.title1', gettext('Question')) : null;
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);
return ( return (
h('section.xblock--drag-and-drop', [ h('section.xblock--drag-and-drop', [
problemHeader, problemHeader,
h('section.problem', {role: 'application'}, [ h('section.problem', {role: 'application'}, [
questionHeader, questionHeader,
h('p', {innerHTML: ctx.question_html}) h('p', {innerHTML: ctx.question_html}),
]), ]),
h('section.drag-container', [ h('section.drag-container', [
h('div.items', {height: ctx.itemsHeight}, renderCollection(itemTemplate, ctx.items, ctx)), h('div.item-bank', renderCollection(itemTemplate, items_in_bank, ctx)),
h('div.target', [ h('div.target', [
h('div.popup', {style: {display: ctx.popup_html ? 'block' : 'none'}}, [ h('div.popup', {style: {display: ctx.popup_html ? 'block' : 'none'}}, [
h('div.close.icon-remove-sign.fa-times-circle'), h('div.close.icon-remove-sign.fa-times-circle'),
h('p.popup-content', {innerHTML: ctx.popup_html}) h('p.popup-content', {innerHTML: ctx.popup_html}),
]),
h('div.target-img-wrapper', [
h('img.target-img', {src: ctx.target_img_src, alt: "Image Description here"}),
]), ]),
h('div.target-img', {style: {backgroundImage: ctx.target_img_src ? renderCollection(zoneTemplate, ctx.zones, ctx),
'url(' + ctx.target_img_src + ')' : renderCollection(itemTemplate, items_placed, ctx),
undefined}},
renderCollection(zoneTemplate, ctx.zones, ctx))
]), ]),
h('div.clear')
]), ]),
feedbackTemplate(ctx) feedbackTemplate(ctx),
]) ])
); );
}; };
......
<section class="xblock--drag-and-drop"></section> {% load i18n %}
<section class="xblock--drag-and-drop">
{% trans "Loading drag and drop exercise." %}
</section>
{% load i18n %} {% load i18n %}
<div class="xblock--drag-and-drop editor-with-buttons"> <div class="xblock--drag-and-drop--editor editor-with-buttons">
{{ js_templates|safe }} {{ js_templates|safe }}
<script type="text/javascript">
var DragAndDropV2BlockPreviousData = JSON.parse(decodeURIComponent('{{ data|safe }}'));
</script>
<section class="drag-builder"> <section class="drag-builder">
<div class="tab feedback-tab"> <div class="tab feedback-tab">
<p class="tab-content"> <p class="tab-content">
...@@ -57,12 +53,16 @@ ...@@ -57,12 +53,16 @@
<label for="display-labels">{% trans "Display label names on the image" %}:</label> <label for="display-labels">{% trans "Display label names on the image" %}:</label>
<input name="display-labels" id="display-labels" type="checkbox" /> <input name="display-labels" id="display-labels" type="checkbox" />
</section> </section>
<div class="items"> <div class="zone-editor">
<form class="zones-form"></form> <div class="controls">
<a href="#" class="add-zone add-element"><div class="icon add"></div>{% trans "Add a zone" %}</a> <form class="zones-form"></form>
</div> <a href="#" class="add-zone add-element"><div class="icon add"></div>{% trans "Add a zone" %}</a>
<div class="target"> </div>
<div class="target-img"></div> <div class="target">
<img class="target-img">
<div class="zones-preview">
</div>
</div>
</div> </div>
</section> </section>
</div> </div>
......
<script id="zone-element-tpl" type="text/html"> <script id="zone-element-tpl" type="text/html">
<div id="{{ id }}" class="zone" data-zone="{{ title }}" style=" <div id="{{ id }}" class="zone" data-zone="{{ title }}" style="
top:{{ y }}px; top:{{ y_percent }}%;
left:{{ x }}px; left:{{ x_percent }}%;
width:{{ width }}px; width:{{ width_percent }}%;
height:{{ height }}px;"> height:{{ height_percent }}%;">
<p>{{{ title }}}</p> <p>{{{ title }}}</p>
</div> </div>
</script> </script>
<script id="zone-input-tpl" type="text/html"> <script id="zone-input-tpl" type="text/html">
<div class="zone-row {{ id }}"> <div class="zone-row {{ id }}" data-index="{{index}}">
<label>{{i18n "Text"}}</label> <label>{{i18n "Text"}}</label>
<input type="text" class="title" value="{{ title }}" /> <input type="text" class="title" value="{{ title }}" />
<a href="#" class="remove-zone hidden"> <a href="#" class="remove-zone hidden">
...@@ -56,7 +56,15 @@ ...@@ -56,7 +56,15 @@
<label>{{i18n "Error Feedback"}}</label> <label>{{i18n "Error Feedback"}}</label>
<textarea class="error-feedback">{{ feedback.incorrect }}</textarea> <textarea class="error-feedback">{{ feedback.incorrect }}</textarea>
</div> </div>
<div class="row"> <div class="row" style="display: none;">
<!--
width and height are no longer respected, so they are now hidden, but we are
keeping the HTML and JS code intact so that the existing data will be preserved
until we can be sure that removing the width/height is OK for all courses that use
this block.
If we do add them back in, we should only allow setting "width". Height will be
detected automatically based on font/image size requirements.
-->
<label>{{i18n "Width in pixels (0 for auto)"}}</label> <label>{{i18n "Width in pixels (0 for auto)"}}</label>
<input type="text" class="item-width" value="{{ width }}"></input> <input type="text" class="item-width" value="{{ width }}"></input>
<label>{{i18n "Height in pixels (0 for auto)"}}</label> <label>{{i18n "Height in pixels (0 for auto)"}}</label>
......
...@@ -6,11 +6,17 @@ max-line-length=120 ...@@ -6,11 +6,17 @@ max-line-length=120
[MESSAGES CONTROL] [MESSAGES CONTROL]
disable= disable=
locally-disabled, attribute-defined-outside-init,
missing-docstring, locally-disabled,
too-many-ancestors, missing-docstring,
too-many-public-methods, too-many-ancestors,
unused-argument too-many-arguments,
too-many-instance-attributes,
too-few-public-methods,
too-many-public-methods,
unused-argument,
invalid-name,
no-member
[SIMILARITIES] [SIMILARITIES]
min-similarity-lines=8 min-similarity-lines=8
...@@ -6,6 +6,7 @@ This script is required to run our selenium tests inside the xblock-sdk workbenc ...@@ -6,6 +6,7 @@ This script is required to run our selenium tests inside the xblock-sdk workbenc
because the workbench SDK's settings file is not inside any python module. because the workbench SDK's settings file is not inside any python module.
""" """
import logging
import os import os
import sys import sys
import workbench import workbench
...@@ -21,6 +22,9 @@ if __name__ == "__main__": ...@@ -21,6 +22,9 @@ if __name__ == "__main__":
# Configure a range of ports in case the default port of 8081 is in use # Configure a range of ports in case the default port of 8081 is in use
os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099") os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099")
# Silence too verbose Django logging
logging.disable(logging.DEBUG)
try: try:
os.mkdir('var') os.mkdir('var')
except OSError: except OSError:
......
...@@ -28,11 +28,7 @@ ...@@ -28,11 +28,7 @@
}, },
"zone": "Zone 1", "zone": "Zone 1",
"backgroundImage": "", "backgroundImage": "",
"id": 0, "id": 0
"size": {
"width": "190px",
"height": "auto"
}
}, },
{ {
"displayName": "2 here", "displayName": "2 here",
...@@ -43,10 +39,6 @@ ...@@ -43,10 +39,6 @@
"zone": "Zone 2", "zone": "Zone 2",
"backgroundImage": "", "backgroundImage": "",
"id": 1, "id": 1,
"size": {
"width": "190px",
"height": "auto"
},
"inputOptions": { "inputOptions": {
"value": 100, "value": 100,
"margin": 5 "margin": 5
...@@ -60,17 +52,9 @@ ...@@ -60,17 +52,9 @@
}, },
"zone": "none", "zone": "none",
"backgroundImage": "", "backgroundImage": "",
"id": 2, "id": 2
"size": {
"width": "100px",
"height": "100px"
}
} }
], ],
"state": {
"items": {},
"finished": true
},
"feedback": { "feedback": {
"start": "Other Intro Feed", "start": "Other Intro Feed",
"finish": "Other Final Feed" "finish": "Other Final Feed"
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
"width": 200, "width": 200,
"title": "Zone <i>1</i>", "title": "Zone <i>1</i>",
"height": 100, "height": 100,
"y": "200", "y": 200,
"x": "100", "x": 100,
"id": "zone-1" "id": "zone-1"
}, },
{ {
...@@ -28,11 +28,7 @@ ...@@ -28,11 +28,7 @@
}, },
"zone": "Zone <i>1</i>", "zone": "Zone <i>1</i>",
"backgroundImage": "", "backgroundImage": "",
"id": 0, "id": 0
"size": {
"width": "190px",
"height": "auto"
}
}, },
{ {
"displayName": "<i>2</i>", "displayName": "<i>2</i>",
...@@ -43,10 +39,6 @@ ...@@ -43,10 +39,6 @@
"zone": "Zone <b>2</b>", "zone": "Zone <b>2</b>",
"backgroundImage": "", "backgroundImage": "",
"id": 1, "id": 1,
"size": {
"width": "190px",
"height": "auto"
},
"inputOptions": { "inputOptions": {
"value": 100, "value": 100,
"margin": 5 "margin": 5
...@@ -60,20 +52,12 @@ ...@@ -60,20 +52,12 @@
}, },
"zone": "none", "zone": "none",
"backgroundImage": "", "backgroundImage": "",
"id": 2, "id": 2
"size": {
"width": "100px",
"height": "100px"
}
} }
], ],
"state": {
"items": {},
"finished": true
},
"feedback": { "feedback": {
"start": "Intro <i>Feed</i>", "start": "Intro <i>Feed</i>",
"finish": "Final <b>Feed</b>" "finish": "Final <b>Feed</b>"
}, },
"targetImg": "https://www.edx.org/sites/default/files/theme/edx-logo-header.png" "targetImg": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg=="
} }
# Imports ########################################################### # Imports ###########################################################
from xml.sax.saxutils import escape from xml.sax.saxutils import escape
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from tests.utils import load_resource from ..utils import load_resource
from workbench import scenarios from workbench import scenarios
...@@ -19,25 +19,26 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -19,25 +19,26 @@ class BaseIntegrationTest(SeleniumBaseTest):
"'": "&apos;" "'": "&apos;"
} }
def _make_scenario_xml(self, display_name, show_title, question_text, completed=False, show_question_header=True): @staticmethod
def _make_scenario_xml(display_name, show_title, question_text, completed=False, show_question_header=True):
return """ return """
<vertical_demo> <vertical_demo>
<drag-and-drop-v2 <drag-and-drop-v2
display_name='{display_name}' display_name='{display_name}'
show_title='{show_title}' show_title='{show_title}'
question_text='{question_text}' question_text='{question_text}'
show_question_header='{show_question_header}' show_question_header='{show_question_header}'
weight='1' weight='1'
completed='{completed}' completed='{completed}'
/> />
</vertical_demo> </vertical_demo>
""".format( """.format(
display_name=escape(display_name), display_name=escape(display_name),
show_title=show_title, show_title=show_title,
question_text=escape(question_text), question_text=escape(question_text),
show_question_header=show_question_header, show_question_header=show_question_header,
completed=completed, completed=completed,
) )
def _get_custom_scenario_xml(self, filename): def _get_custom_scenario_xml(self, filename):
data = load_resource(filename) data = load_resource(filename)
...@@ -50,7 +51,7 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -50,7 +51,7 @@ class BaseIntegrationTest(SeleniumBaseTest):
self.addCleanup(scenarios.remove_scenario, identifier) self.addCleanup(scenarios.remove_scenario, identifier)
def _get_items(self): def _get_items(self):
items_container = self._page.find_element_by_css_selector('.items') items_container = self._page.find_element_by_css_selector('.item-bank')
return items_container.find_elements_by_css_selector('.option') return items_container.find_elements_by_css_selector('.option')
def _get_zones(self): def _get_zones(self):
...@@ -62,10 +63,12 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -62,10 +63,12 @@ class BaseIntegrationTest(SeleniumBaseTest):
def scroll_down(self, pixels=50): def scroll_down(self, pixels=50):
self.browser.execute_script("$(window).scrollTop({})".format(pixels)) self.browser.execute_script("$(window).scrollTop({})".format(pixels))
def get_element_html(self, element): @staticmethod
def get_element_html(element):
return element.get_attribute('innerHTML').strip() return element.get_attribute('innerHTML').strip()
def get_element_classes(self, element): @staticmethod
def get_element_classes(element):
return element.get_attribute('class').split() return element.get_attribute('class').split()
def wait_until_html_in(self, html, elem): def wait_until_html_in(self, html, elem):
...@@ -73,7 +76,8 @@ class BaseIntegrationTest(SeleniumBaseTest): ...@@ -73,7 +76,8 @@ class BaseIntegrationTest(SeleniumBaseTest):
wait.until(lambda e: html in e.get_attribute('innerHTML'), wait.until(lambda e: html in e.get_attribute('innerHTML'),
u"{} should be in {}".format(html, elem.get_attribute('innerHTML'))) u"{} should be in {}".format(html, elem.get_attribute('innerHTML')))
def wait_until_has_class(self, class_name, elem): @staticmethod
def wait_until_has_class(class_name, elem):
wait = WebDriverWait(elem, 2) wait = WebDriverWait(elem, 2)
wait.until(lambda e: class_name in e.get_attribute('class').split(), wait.until(lambda e: class_name in e.get_attribute('class').split(),
u"Class name {} not in {}".format(class_name, elem.get_attribute('class'))) u"Class name {} not in {}".format(class_name, elem.get_attribute('class')))
from tests.integration.test_base import BaseIntegrationTest from .test_base import BaseIntegrationTest
class TestCustomDataDragAndDropRendering(BaseIntegrationTest): class TestCustomDataDragAndDropRendering(BaseIntegrationTest):
...@@ -26,6 +26,9 @@ class TestCustomDataDragAndDropRendering(BaseIntegrationTest): ...@@ -26,6 +26,9 @@ class TestCustomDataDragAndDropRendering(BaseIntegrationTest):
self.assertIn('<span style="color:red">X</span>', self.get_element_html(items[2])) self.assertIn('<span style="color:red">X</span>', self.get_element_html(items[2]))
def test_background_image(self): def test_background_image(self):
bg_image = self.browser.execute_script('return jQuery(".target-img").css("background-image")') bg_image = self.browser.find_element_by_css_selector(".xblock--drag-and-drop .target-img")
custom_image_url = 'https://www.edx.org/sites/default/files/theme/edx-logo-header.png' custom_image_url = (
self.assertEqual(bg_image, 'url("{}")'.format(custom_image_url)) "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciI"
"HdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg=="
)
self.assertEqual(bg_image.get_attribute("src"), custom_image_url)
from selenium.webdriver import ActionChains from selenium.webdriver import ActionChains
from tests.integration.test_base import BaseIntegrationTest from .test_base import BaseIntegrationTest
class ItemDefinition(object): class ItemDefinition(object):
def __init__(self, item_id, zone_id, feedback_positive, feedback_negative, input=None): def __init__(self, item_id, zone_id, feedback_positive, feedback_negative, input_value=None):
self.feedback_negative = feedback_negative self.feedback_negative = feedback_negative
self.feedback_positive = feedback_positive self.feedback_positive = feedback_positive
self.zone_id = zone_id self.zone_id = zone_id
self.item_id = item_id self.item_id = item_id
self.input = input self.input = input_value
class InteractionTestFixture(BaseIntegrationTest): class InteractionTestFixture(object):
""" """
Verifying Drag and Drop XBlock rendering against default data - if default data changes this would probably broke Verifying Drag and Drop XBlock rendering against default data - if default data changes this would probably broke
""" """
...@@ -28,11 +28,11 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -28,11 +28,11 @@ class InteractionTestFixture(BaseIntegrationTest):
all_zones = ['Zone 1', 'Zone 2'] all_zones = ['Zone 1', 'Zone 2']
feedback = { feedback = {
"intro": "Intro Feed", "intro": "Drag the items onto the image above.",
"final": "Final Feed" "final": "Good work! You have completed this drag and drop exercise."
} }
def _get_scenario_xml(self): def _get_scenario_xml(self): # pylint: disable=no-self-use
return "<vertical_demo><drag-and-drop-v2/></vertical_demo>" return "<vertical_demo><drag-and-drop-v2/></vertical_demo>"
@classmethod @classmethod
...@@ -54,7 +54,7 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -54,7 +54,7 @@ class InteractionTestFixture(BaseIntegrationTest):
self.browser.set_window_size(1024, 800) self.browser.set_window_size(1024, 800)
def _get_item_by_value(self, item_value): def _get_item_by_value(self, item_value):
items_container = self._page.find_element_by_css_selector('.items') items_container = self._page.find_element_by_css_selector('.item-bank')
return items_container.find_elements_by_xpath("//div[@data-value='{item_id}']".format(item_id=item_value))[0] return items_container.find_elements_by_xpath("//div[@data-value='{item_id}']".format(item_id=item_value))[0]
def _get_zone_by_id(self, zone_id): def _get_zone_by_id(self, zone_id):
...@@ -128,7 +128,9 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -128,7 +128,9 @@ class InteractionTestFixture(BaseIntegrationTest):
self.assertEqual(self.get_element_html(feedback_message), self.feedback['intro']) # precondition check self.assertEqual(self.get_element_html(feedback_message), self.feedback['intro']) # precondition check
items = self._get_correct_item_for_zone() items = self._get_correct_item_for_zone()
get_locations = lambda: {item_id: self._get_item_by_value(item_id).location for item_id in items.keys()}
def get_locations():
return {item_id: self._get_item_by_value(item_id).location for item_id in items.keys()}
initial_locations = get_locations() initial_locations = get_locations()
...@@ -157,7 +159,7 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -157,7 +159,7 @@ class InteractionTestFixture(BaseIntegrationTest):
self.assertDictEqual(locations_after_reset[item_key], initial_locations[item_key]) self.assertDictEqual(locations_after_reset[item_key], initial_locations[item_key])
class CustomDataInteractionTest(InteractionTestFixture): class CustomDataInteractionTest(InteractionTestFixture, BaseIntegrationTest):
items_map = { items_map = {
0: ItemDefinition(0, 'Zone 1', "Yes 1", "No 1"), 0: ItemDefinition(0, 'Zone 1', "Yes 1", "No 1"),
1: ItemDefinition(1, 'Zone 2', "Yes 2", "No 2", "102"), 1: ItemDefinition(1, 'Zone 2', "Yes 2", "No 2", "102"),
...@@ -175,7 +177,7 @@ class CustomDataInteractionTest(InteractionTestFixture): ...@@ -175,7 +177,7 @@ class CustomDataInteractionTest(InteractionTestFixture):
return self._get_custom_scenario_xml("integration/data/test_data.json") return self._get_custom_scenario_xml("integration/data/test_data.json")
class CustomHtmlDataInteractionTest(InteractionTestFixture): class CustomHtmlDataInteractionTest(InteractionTestFixture, BaseIntegrationTest):
items_map = { items_map = {
0: ItemDefinition(0, 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"), 0: ItemDefinition(0, 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"),
1: ItemDefinition(1, 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>", "95"), 1: ItemDefinition(1, 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>", "95"),
......
from ddt import ddt, unpack, data from ddt import ddt, unpack, data
from tests.integration.test_base import BaseIntegrationTest from .test_base import BaseIntegrationTest
class Colors(object): class Colors(object):
...@@ -19,18 +19,16 @@ class Colors(object): ...@@ -19,18 +19,16 @@ class Colors(object):
elif color == cls.CORNFLOWERBLUE: elif color == cls.CORNFLOWERBLUE:
return 'rgb(100, 149, 237)' return 'rgb(100, 149, 237)'
@ddt @ddt
class TestDragAndDropRender(BaseIntegrationTest): class TestDragAndDropRender(BaseIntegrationTest):
""" """
Verifying Drag and Drop XBlock rendering against default data - if default data changes this would probably broke Verifying Drag and Drop XBlock rendering against default data - if default data changes this
will probably break.
""" """
PAGE_TITLE = 'Drag and Drop v2' PAGE_TITLE = 'Drag and Drop v2'
PAGE_ID = 'drag_and_drop_v2' PAGE_ID = 'drag_and_drop_v2'
ITEM_PROPERTIES = [ ITEM_PROPERTIES = [{'text': '1'}, {'text': '2'}, {'text': 'X'}, ]
{'text': '1', 'style_settings': {'width': '190px', 'height': 'auto'}},
{'text': '2', 'style_settings': {'width': '190px', 'height': 'auto'}},
{'text': 'X', 'style_settings': {'width': '100px', 'height': '100px'}},
]
def load_scenario(self, item_background_color="", item_text_color=""): def load_scenario(self, item_background_color="", item_text_color=""):
scenario_xml = """ scenario_xml = """
...@@ -43,22 +41,27 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -43,22 +41,27 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
self._page = self.go_to_page(self.PAGE_TITLE) self._page = self.go_to_page(self.PAGE_TITLE)
def _get_style(self, selector, style): def _get_style(self, selector, style, computed=True):
return self.browser.execute_script( if computed:
'return getComputedStyle($("{selector}").get(0)).{style}'.format(selector=selector, style=style) query = 'return getComputedStyle($("{selector}").get(0)).{style}'
) else:
query = 'return $("{selector}").get(0).style.{style}'
def _test_style(self, element, style_settings, element_type): return self.browser.execute_script(query.format(selector=selector, style=style))
style = element.get_attribute('style')
for style_prop, expected_value in style_settings.items(): def _assert_box_percentages(self, selector, left, top, width, height):
if style_prop == 'color' or style_prop == 'background-color' and expected_value.startswith('#'): """ Assert that the element 'selector' has the specified position/size percentages """
expected_value = Colors.rgb(expected_value) values = {key: self._get_style(selector, key, False) for key in ['left', 'top', 'width', 'height']}
expected = u"{0}: {1}".format(style_prop, expected_value) for key in values:
self.assertIn(expected, style) self.assertTrue(values[key].endswith('%'))
if element_type == "item": values[key] = float(values[key][:-1])
self._test_item_style(element, style_settings, style) self.assertAlmostEqual(values['left'], left, places=2)
self.assertAlmostEqual(values['top'], top, places=2)
def _test_item_style(self, item, style_settings, style): self.assertAlmostEqual(values['width'], width, places=2)
self.assertAlmostEqual(values['height'], height, places=2)
def _test_item_style(self, item_element, style_settings):
item_val = item_element.get_attribute('data-value')
style = item_element.get_attribute('style')
# Check background color # Check background color
background_color_property = 'background-color' background_color_property = 'background-color'
if background_color_property not in style_settings: if background_color_property not in style_settings:
...@@ -66,18 +69,18 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -66,18 +69,18 @@ class TestDragAndDropRender(BaseIntegrationTest):
expected_background_color = Colors.BLUE expected_background_color = Colors.BLUE
else: else:
expected_background_color = Colors.rgb(style_settings['background-color']) expected_background_color = Colors.rgb(style_settings['background-color'])
background_color = self._get_style('.items .option', 'backgroundColor') background_color = self._get_style('.item-bank .option[data-value='+item_val+']', 'backgroundColor')
self.assertEquals(background_color, expected_background_color) self.assertEquals(background_color, expected_background_color)
# Check text color # Check text color
color_property = 'color' color_property = 'color'
if color_property not in style_settings: if color_property not in style_settings:
self.assertNotIn(' ' + color_property, style) # Leading space makes sure that # Leading space below ensures that test does not find "color" in "background-color"
# test does not find "color" in "background-color" self.assertNotIn(' ' + color_property, style)
expected_color = Colors.WHITE expected_color = Colors.WHITE
else: else:
expected_color = Colors.rgb(style_settings['color']) expected_color = Colors.rgb(style_settings['color'])
color = self._get_style('.items .option', 'color') color = self._get_style('.item-bank .option[data-value='+item_val+']', 'color')
self.assertEquals(color, expected_color) self.assertEquals(color, expected_color)
def test_items(self): def test_items(self):
...@@ -91,7 +94,7 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -91,7 +94,7 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.assertEqual(item.get_attribute('data-value'), str(index)) self.assertEqual(item.get_attribute('data-value'), str(index))
self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text']) self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text'])
self.assertIn('ui-draggable', self.get_element_classes(item)) self.assertIn('ui-draggable', self.get_element_classes(item))
self._test_style(item, self.ITEM_PROPERTIES[index]['style_settings'], element_type='item') self._test_item_style(item, {})
@unpack @unpack
@data( @data(
...@@ -116,9 +119,7 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -116,9 +119,7 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.assertEqual(item.get_attribute('data-value'), str(index)) self.assertEqual(item.get_attribute('data-value'), str(index))
self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text']) self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text'])
self.assertIn('ui-draggable', self.get_element_classes(item)) self.assertIn('ui-draggable', self.get_element_classes(item))
self._test_style( self._test_item_style(item, color_settings)
item, dict(self.ITEM_PROPERTIES[index]['style_settings'], **color_settings), element_type='item'
)
def test_zones(self): def test_zones(self):
self.load_scenario() self.load_scenario()
...@@ -129,26 +130,22 @@ class TestDragAndDropRender(BaseIntegrationTest): ...@@ -129,26 +130,22 @@ class TestDragAndDropRender(BaseIntegrationTest):
self.assertEqual(zones[0].get_attribute('data-zone'), 'Zone 1') self.assertEqual(zones[0].get_attribute('data-zone'), 'Zone 1')
self.assertIn('ui-droppable', self.get_element_classes(zones[0])) self.assertIn('ui-droppable', self.get_element_classes(zones[0]))
self._test_style( self._assert_box_percentages('#zone-1', left=31.1284, top=6.17284, width=38.1323, height=36.6255)
zones[0], {'top': '200px', 'left': '120px', 'width': '200px', 'height': '100px'}, element_type='zone'
)
self.assertEqual(zones[1].get_attribute('data-zone'), 'Zone 2') self.assertEqual(zones[1].get_attribute('data-zone'), 'Zone 2')
self.assertIn('ui-droppable', self.get_element_classes(zones[1])) self.assertIn('ui-droppable', self.get_element_classes(zones[1]))
self._test_style( self._assert_box_percentages('#zone-2', left=16.7315, top=43.2099, width=66.1479, height=28.8066)
zones[1], {'top': '360px', 'left': '120px', 'width': '200px', 'height': '100px'}, element_type='zone'
)
def test_feedback(self): def test_feedback(self):
self.load_scenario() self.load_scenario()
feedback_message = self._get_feedback_message() feedback_message = self._get_feedback_message()
self.assertEqual(feedback_message.text, "Intro Feed") self.assertEqual(feedback_message.text, "Drag the items onto the image above.")
def test_background_image(self): def test_background_image(self):
self.load_scenario() self.load_scenario()
bg_image = self.browser.execute_script('return jQuery(".target-img").css("background-image")') bg_image = self.browser.find_element_by_css_selector(".xblock--drag-and-drop .target-img")
image_path = '/resource/drag-and-drop-v2/public/img/triangle.png' image_path = '/resource/drag-and-drop-v2/public/img/triangle.png'
self.assertEqual(bg_image, 'url("{0}{1}")'.format(self.live_server_url, image_path)) self.assertTrue(bg_image.get_attribute("src").endswith(image_path))
from ddt import ddt, unpack, data from ddt import ddt, unpack, data
from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import NoSuchElementException
from tests.integration.test_base import BaseIntegrationTest from .test_base import BaseIntegrationTest
from workbench import scenarios from workbench import scenarios
......
{ {
"zones": [ "title": "DnDv2 XBlock with HTML instructions",
{ "show_title": false,
"index": 1, "question_text": "Solve this <strong>drag-and-drop</strong> problem.",
"width": 200, "show_question_header": false,
"title": "Zone <i>1</i>", "target_img_expanded_url": "/expanded/url/to/drag_and_drop_v2/public/img/triangle.png",
"height": 100, "item_background_color": "white",
"y": "200", "item_text_color": "#000080",
"x": "100", "initial_feedback": "HTML <strong>Intro</strong> Feed",
"id": "zone-1" "display_zone_labels": false,
},
{ "zones": [
"index": 2, {
"width": 200, "index": 1,
"title": "Zone <b>2</b>", "title": "Zone <i>1</i>",
"height": 100, "x": 100,
"y": 0, "y": 200,
"x": 0, "width": 200,
"id": "zone-2" "height": 100,
} "id": "zone-1"
], },
"items": [ {
{ "index": 2,
"displayName": "<b>1</b>", "title": "Zone <b>2</b>",
"backgroundImage": "", "x": 0,
"id": 0, "y": 0,
"size": { "width": 200,
"width": "190px", "height": 100,
"height": "auto" "id": "zone-2"
}, }
"inputOptions": false ],
},
{ "items": [
"displayName": "<i>2</i>", {
"backgroundImage": "", "displayName": "<b>1</b>",
"id": 1, "backgroundImage": "",
"size": { "id": 0,
"width": "190px", "inputOptions": false
"height": "auto" },
}, {
"inputOptions": true "displayName": "<i>2</i>",
}, "backgroundImage": "",
{ "id": 1,
"displayName": "X", "inputOptions": true
"backgroundImage": "", },
"id": 2, {
"size": { "displayName": "X",
"width": "100px", "backgroundImage": "",
"height": "100px" "id": 2,
}, "inputOptions": false
"inputOptions": false },
}, {
{ "displayName": "",
"displayName": "", "backgroundImage": "http://placehold.it/100x300",
"backgroundImage": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg", "id": 3,
"id": 3, "inputOptions": false
"size": { }
"width": "190px", ]
"height": "auto"
},
"inputOptions": false
}
],
"state": {
"items": {},
"finished": false
},
"feedback": {
"start": "Intro Feed"
},
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"title": "Drag and Drop",
"show_title": true,
"question_text": "",
"show_question_header": true
} }
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
"width": 200, "width": 200,
"title": "Zone <i>1</i>", "title": "Zone <i>1</i>",
"height": 100, "height": 100,
"y": "200", "y": 200,
"x": "100", "x": 100,
"id": "zone-1" "id": "zone-1"
}, },
{ {
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
"id": "zone-2" "id": "zone-2"
} }
], ],
"items": [ "items": [
{ {
"displayName": "<b>1</b>", "displayName": "<b>1</b>",
...@@ -28,11 +29,7 @@ ...@@ -28,11 +29,7 @@
}, },
"zone": "Zone <i>1</i>", "zone": "Zone <i>1</i>",
"backgroundImage": "", "backgroundImage": "",
"id": 0, "id": 0
"size": {
"width": "190px",
"height": "auto"
}
}, },
{ {
"displayName": "<i>2</i>", "displayName": "<i>2</i>",
...@@ -43,10 +40,6 @@ ...@@ -43,10 +40,6 @@
"zone": "Zone <b>2</b>", "zone": "Zone <b>2</b>",
"backgroundImage": "", "backgroundImage": "",
"id": 1, "id": 1,
"size": {
"width": "190px",
"height": "auto"
},
"inputOptions": { "inputOptions": {
"value": 100, "value": 100,
"margin": 5 "margin": 5
...@@ -60,11 +53,7 @@ ...@@ -60,11 +53,7 @@
}, },
"zone": "none", "zone": "none",
"backgroundImage": "", "backgroundImage": "",
"id": 2, "id": 2
"size": {
"width": "100px",
"height": "100px"
}
}, },
{ {
"displayName": "", "displayName": "",
...@@ -73,21 +62,12 @@ ...@@ -73,21 +62,12 @@
"correct": "" "correct": ""
}, },
"zone": "none", "zone": "none",
"backgroundImage": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg", "backgroundImage": "http://placehold.it/100x300",
"id": 3, "id": 3
"size": {
"width": "190px",
"height": "auto"
}
} }
], ],
"state": {
"items": {},
"finished": true
},
"feedback": { "feedback": {
"start": "Intro Feed", "start": "HTML <strong>Intro</strong> Feed",
"finish": "Final <b>Feed</b>" "finish": "Final <strong>feedback</strong>!"
}, }
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png"
} }
{
"display_name": "DnDv2 XBlock with HTML instructions",
"show_title": false,
"question_text": "Solve this <strong>drag-and-drop</strong> problem.",
"show_question_header": false,
"weight": 1,
"item_background_color": "white",
"item_text_color": "#000080"
}
{ {
"zones": [ "title": "Drag and Drop",
{ "show_title": true,
"index": 1, "question_text": "",
"title": "Zone 1", "show_question_header": true,
"id": "zone-1", "target_img_expanded_url": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"height": 100, "item_background_color": null,
"y": "200", "item_text_color": null,
"x": "100", "initial_feedback": "Intro Feed",
"width": 200 "display_zone_labels": false,
},
{ "zones": [
"index": 2, {
"title": "Zone 2", "index": 1,
"id": "zone-2", "title": "Zone 1",
"height": 100, "x": "100",
"y": 0, "y": "200",
"x": 0, "width": 200,
"width": 200 "height": 100,
} "id": "zone-1"
], },
"items": [ {
{ "index": 2,
"displayName": "1", "title": "Zone 2",
"backgroundImage": "", "x": 0,
"id": 0, "y": 0,
"size": { "width": 200,
"width": "190px", "height": 100,
"height": "auto" "id": "zone-2"
}, }
"inputOptions": false ],
},
{ "items": [
"displayName": "2", {
"backgroundImage": "", "displayName": "1",
"id": 1, "backgroundImage": "",
"size": { "id": 0,
"width": "190px", "inputOptions": false,
"height": "auto" "size": {"height": "auto", "width": "190px"}
}, },
"inputOptions": true {
}, "displayName": "2",
{ "backgroundImage": "",
"displayName": "X", "id": 1,
"backgroundImage": "", "inputOptions": true,
"id": 2, "size": {"height": "auto", "width": "190px"}
"size": { },
"width": "100px", {
"height": "100px" "displayName": "X",
}, "backgroundImage": "",
"inputOptions": false "id": 2,
}, "inputOptions": false,
{ "size": {"height": "100px", "width": "100px"}
"displayName": "", },
"backgroundImage": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg", {
"id": 3, "displayName": "",
"size": { "backgroundImage": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"width": "190px", "id": 3,
"height": "auto" "inputOptions": false,
}, "size": {"height": "auto", "width": "190px"}
"inputOptions": false }
} ]
],
"state": {
"items": {},
"finished": false
},
"feedback": {
"start": "Intro Feed"
},
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"title": "Drag and Drop",
"show_title": true,
"question_text": "",
"show_question_header": true
} }
{
"title": "DnDv2 XBlock with plain text instructions",
"show_title": true,
"question_text": "Can you solve this drag-and-drop problem?",
"show_question_header": true,
"target_img_expanded_url": "http://placehold.it/800x600",
"item_background_color": null,
"item_text_color": null,
"initial_feedback": "This is the initial feedback.",
"display_zone_labels": false,
"zones": [
{
"index": 1,
"title": "Zone 1",
"y": 123,
"x": 234,
"width": 345,
"height": 456,
"id": "zone-1"
},
{
"index": 2,
"title": "Zone 2",
"y": 20,
"x": 10,
"width": 30,
"height": 40,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"backgroundImage": "",
"id": 0,
"inputOptions": false
},
{
"displayName": "2",
"backgroundImage": "",
"id": 1,
"inputOptions": true
},
{
"displayName": "X",
"backgroundImage": "",
"id": 2,
"inputOptions": false
},
{
"displayName": "",
"backgroundImage": "http://placehold.it/200x100",
"id": 3,
"inputOptions": false
}
]
}
{
"zones": [
{
"index": 1,
"title": "Zone 1",
"y": 123,
"x": 234,
"width": 345,
"height": 456,
"id": "zone-1"
},
{
"index": 2,
"title": "Zone 2",
"y": 20,
"x": 10,
"width": 30,
"height": 40,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"feedback": {
"incorrect": "No 1",
"correct": "Yes 1"
},
"zone": "Zone 1",
"backgroundImage": "",
"id": 0
},
{
"displayName": "2",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "Zone 2",
"backgroundImage": "",
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"backgroundImage": "",
"id": 2
},
{
"displayName": "",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"backgroundImage": "http://placehold.it/200x100",
"id": 3
}
],
"feedback": {
"start": "This is the initial feedback.",
"finish": "This is the final feedback."
},
"targetImg": "http://placehold.it/800x600",
"displayLabels": false
}
{
"display_name": "DnDv2 XBlock with plain text instructions",
"show_title": true,
"question_text": "Can you solve this drag-and-drop problem?",
"show_question_header": true,
"weight": 1,
"item_background_color": "",
"item_text_color": ""
}
import json
import unittest
from ..utils import (
make_block,
load_resource,
TestCaseMixin,
)
class BaseDragAndDropAjaxFixture(TestCaseMixin):
ZONE_1 = None
ZONE_2 = None
FEEDBACK = {
0: {"correct": None, "incorrect": None},
1: {"correct": None, "incorrect": None},
2: {"correct": None, "incorrect": None}
}
FINAL_FEEDBACK = None
FOLDER = None
def setUp(self):
self.patch_workbench()
self.block = make_block()
initial_settings = self.initial_settings()
for field in initial_settings:
setattr(self.block, field, initial_settings[field])
self.block.data = self.initial_data()
@classmethod
def initial_data(cls):
return json.loads(load_resource('unit/data/{}/data.json'.format(cls.FOLDER)))
@classmethod
def initial_settings(cls):
return json.loads(load_resource('unit/data/{}/settings.json'.format(cls.FOLDER)))
@classmethod
def expected_configuration(cls):
return json.loads(load_resource('unit/data/{}/config_out.json'.format(cls.FOLDER)))
@classmethod
def initial_feedback(cls):
""" The initial overall_feedback value """
return cls.expected_configuration()["initial_feedback"]
def test_get_configuration(self):
self.assertEqual(self.expected_configuration(), self.block.get_configuration())
def test_do_attempt_wrong_with_feedback(self):
item_id, zone_id = 0, self.ZONE_2
data = {"val": item_id, "zone": zone_id, "x_percent": "33%", "y_percent": "11%"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"overall_feedback": None,
"finished": False,
"correct": False,
"correct_location": False,
"feedback": self.FEEDBACK[item_id]["incorrect"]
})
def test_do_attempt_wrong_without_feedback(self):
item_id, zone_id = 2, self.ZONE_1
data = {"val": item_id, "zone": zone_id, "x_percent": "33%", "y_percent": "11%"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"overall_feedback": None,
"finished": False,
"correct": False,
"correct_location": False,
"feedback": self.FEEDBACK[item_id]["incorrect"]
})
def test_do_attempt_correct(self):
item_id, zone_id = 0, self.ZONE_1
data = {"val": item_id, "zone": zone_id, "x_percent": "33%", "y_percent": "11%"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"overall_feedback": None,
"finished": False,
"correct": True,
"correct_location": True,
"feedback": self.FEEDBACK[item_id]["correct"]
})
def test_do_attempt_with_input(self):
data = {"val": 1, "zone": self.ZONE_2, "x_percent": "0%", "y_percent": "85%"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"finished": False,
"correct": False,
"correct_location": True,
"feedback": None,
"overall_feedback": None,
})
expected_state = {
'items': {
"1": {"x_percent": "0%", "y_percent": "85%", "correct_input": False},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
data = {"val": 1, "input": "250"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"finished": False,
"correct": False,
"correct_location": True,
"feedback": self.FEEDBACK[1]['incorrect'],
"overall_feedback": None
})
expected_state = {
'items': {
"1": {"x_percent": "0%", "y_percent": "85%", "input": "250", "correct_input": False},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
data = {"val": 1, "input": "103"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"finished": False,
"correct": True,
"correct_location": True,
"feedback": self.FEEDBACK[1]['correct'],
"overall_feedback": None,
})
expected_state = {
'items': {
"1": {"x_percent": "0%", "y_percent": "85%", "input": "103", "correct_input": True},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
def test_grading(self):
published_grades = []
def mock_publish(self, event, params):
if event == 'grade':
published_grades.append(params)
self.block.runtime.publish = mock_publish
self.call_handler('do_attempt', {
"val": 0, "zone": self.ZONE_1, "y_percent": "11%", "x_percent": "33%"
})
self.assertEqual(1, len(published_grades))
self.assertEqual({'value': 0.5, 'max_value': 1}, published_grades[-1])
self.call_handler('do_attempt', {
"val": 1, "zone": self.ZONE_2, "y_percent": "90%", "x_percent": "42%"
})
self.assertEqual(2, len(published_grades))
self.assertEqual({'value': 0.5, 'max_value': 1}, published_grades[-1])
self.call_handler('do_attempt', {"val": 1, "input": "99"})
self.assertEqual(3, len(published_grades))
self.assertEqual({'value': 1, 'max_value': 1}, published_grades[-1])
def test_do_attempt_final(self):
data = {"val": 0, "zone": self.ZONE_1, "x_percent": "33%", "y_percent": "11%"}
self.call_handler('do_attempt', data)
expected_state = {
"items": {
"0": {"x_percent": "33%", "y_percent": "11%", "correct_input": True}
},
"finished": False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
data = {"val": 1, "zone": self.ZONE_2, "x_percent": "22%", "y_percent": "22%"}
res = self.call_handler('do_attempt', data)
data = {"val": 1, "input": "99"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"overall_feedback": self.FINAL_FEEDBACK,
"finished": True,
"correct": True,
"correct_location": True,
"feedback": self.FEEDBACK[1]["correct"]
})
expected_state = {
"items": {
"0": {"x_percent": "33%", "y_percent": "11%", "correct_input": True},
"1": {"x_percent": "22%", "y_percent": "22%", "input": "99", "correct_input": True}
},
"finished": True,
'overall_feedback': self.FINAL_FEEDBACK,
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
class TestDragAndDropHtmlData(BaseDragAndDropAjaxFixture, unittest.TestCase):
FOLDER = "html"
ZONE_1 = "Zone <i>1</i>"
ZONE_2 = "Zone <b>2</b>"
FEEDBACK = {
0: {"correct": "Yes <b>1</b>", "incorrect": "No <b>1</b>"},
1: {"correct": "Yes <i>2</i>", "incorrect": "No <i>2</i>"},
2: {"correct": "", "incorrect": ""}
}
FINAL_FEEDBACK = "Final <strong>feedback</strong>!"
class TestDragAndDropPlainData(BaseDragAndDropAjaxFixture, unittest.TestCase):
FOLDER = "plain"
ZONE_1 = "Zone 1"
ZONE_2 = "Zone 2"
FEEDBACK = {
0: {"correct": "Yes 1", "incorrect": "No 1"},
1: {"correct": "Yes 2", "incorrect": "No 2"},
2: {"correct": "", "incorrect": ""}
}
FINAL_FEEDBACK = "This is the final feedback."
class TestOldDataFormat(TestDragAndDropPlainData):
"""
Make sure we can work with the slightly-older format for 'data' field values.
"""
FOLDER = "old"
FINAL_FEEDBACK = "Final Feed"
import unittest
from ..utils import (
DEFAULT_START_FEEDBACK,
DEFAULT_FINISH_FEEDBACK,
make_block,
TestCaseMixin,
)
class BasicTests(TestCaseMixin, unittest.TestCase):
""" Basic unit tests for the Drag and Drop block, using its default settings """
def setUp(self):
self.block = make_block()
self.patch_workbench()
def test_template_contents(self):
context = {}
student_fragment = self.block.runtime.render(self.block, 'student_view', context)
self.assertIn('<section class="xblock--drag-and-drop">', student_fragment.content)
self.assertIn('Loading drag and drop exercise.', student_fragment.content)
def test_get_configuration(self):
"""
Test the get_configuration() method.
The result of this method is passed to the block's JavaScript during initialization.
"""
config = self.block.get_configuration()
zones = config.pop("zones")
items = config.pop("items")
self.assertEqual(config, {
"display_zone_labels": False,
"title": "Drag and Drop",
"show_title": True,
"question_text": "",
"show_question_header": True,
"target_img_expanded_url": '/expanded/url/to/drag_and_drop_v2/public/img/triangle.png',
"item_background_color": None,
"item_text_color": None,
"initial_feedback": DEFAULT_START_FEEDBACK,
})
self.assertEqual(zones, [
{
"index": 1,
"title": "Zone 1",
"id": "zone-1",
"x": 160,
"y": 30,
"width": 196,
"height": 178,
},
{
"index": 2,
"title": "Zone 2",
"id": "zone-2",
"x": 86,
"y": 210,
"width": 340,
"height": 140,
}
])
# Items should contain no answer data:
self.assertEqual(items, [
{"id": 0, "displayName": "1", "backgroundImage": "", "inputOptions": False},
{"id": 1, "displayName": "2", "backgroundImage": "", "inputOptions": False},
{"id": 2, "displayName": "X", "backgroundImage": "", "inputOptions": False},
])
def test_ajax_solve_and_reset(self):
# Check assumptions / initial conditions:
self.assertFalse(self.block.completed)
def assert_user_state_empty():
self.assertEqual(self.block.item_state, {})
self.assertEqual(self.call_handler("get_user_state"), {
'items': {},
'finished': False,
'overall_feedback': DEFAULT_START_FEEDBACK,
})
assert_user_state_empty()
# Drag both items into the correct spot:
data = {"val": 0, "zone": "Zone 1", "x_percent": "33%", "y_percent": "11%"}
self.call_handler('do_attempt', data)
data = {"val": 1, "zone": "Zone 2", "x_percent": "67%", "y_percent": "80%"}
self.call_handler('do_attempt', data)
# Check the result:
self.assertTrue(self.block.completed)
self.assertEqual(self.block.item_state, {
'0': {'x_percent': '33%', 'y_percent': '11%'},
'1': {'x_percent': '67%', 'y_percent': '80%'},
})
self.assertEqual(self.call_handler('get_user_state'), {
'items': {
'0': {'x_percent': '33%', 'y_percent': '11%', 'correct_input': True},
'1': {'x_percent': '67%', 'y_percent': '80%', 'correct_input': True},
},
'finished': True,
'overall_feedback': DEFAULT_FINISH_FEEDBACK,
})
# Reset to initial conditions
self.call_handler('reset', {})
self.assertTrue(self.block.completed)
assert_user_state_empty()
def test_studio_submit(self):
body = {
'display_name': "Test Drag & Drop",
'show_title': False,
'question_text': "Question Drag & Drop",
'show_question_header': False,
'item_background_color': 'cornflowerblue',
'item_text_color': 'coral',
'weight': '5',
'data': {
'foo': 1
},
}
res = self.call_handler('studio_submit', body)
self.assertEqual(res, {'result': 'success'})
self.assertEqual(self.block.show_title, False)
self.assertEqual(self.block.display_name, "Test Drag & Drop")
self.assertEqual(self.block.question_text, "Question Drag & Drop")
self.assertEqual(self.block.show_question_header, False)
self.assertEqual(self.block.item_background_color, "cornflowerblue")
self.assertEqual(self.block.item_text_color, "coral")
self.assertEqual(self.block.weight, 5)
self.assertEqual(self.block.data, {'foo': 1})
def test_expand_static_url(self):
""" Test the expand_static_url handler needed in Studio when changing the image """
res = self.call_handler('expand_static_url', '/static/blah.png')
self.assertEqual(res, {'url': '/course/test-course/assets/blah.png'})
def test_image_url(self):
""" Ensure that the default image and custom URLs are both expanded by the runtime """
self.assertEqual(self.block.data.get("targetImg"), None)
self.assertEqual(
self.block.get_configuration()["target_img_expanded_url"],
'/expanded/url/to/drag_and_drop_v2/public/img/triangle.png',
)
self.block.data["targetImg"] = "/static/foo.png"
self.assertEqual(
self.block.get_configuration()["target_img_expanded_url"],
'/course/test-course/assets/foo.png',
)
import json
import pkg_resources import pkg_resources
import re
from mock import patch
from webob import Request
from workbench.runtime import WorkbenchRuntime
from xblock.fields import ScopeIds
from xblock.runtime import KvsFieldData, DictKeyValueStore
import drag_and_drop_v2
DEFAULT_START_FEEDBACK = "Drag the items onto the image above."
DEFAULT_FINISH_FEEDBACK = "Good work! You have completed this drag and drop exercise."
def make_request(data, method='POST'):
""" Make a webob JSON Request """
request = Request.blank('/')
request.method = 'POST'
request.body = json.dumps(data).encode('utf-8') if data is not None else ""
request.method = method
return request
def make_block():
""" Instantiate a DragAndDropBlock XBlock inside a WorkbenchRuntime """
block_type = 'drag_and_drop_v2'
key_store = DictKeyValueStore()
field_data = KvsFieldData(key_store)
runtime = WorkbenchRuntime()
def_id = runtime.id_generator.create_definition(block_type)
usage_id = runtime.id_generator.create_usage(def_id)
scope_ids = ScopeIds('user', block_type, def_id, usage_id)
return drag_and_drop_v2.DragAndDropBlock(runtime, field_data, scope_ids=scope_ids)
def load_resource(resource_path): def load_resource(resource_path):
...@@ -6,4 +40,33 @@ def load_resource(resource_path): ...@@ -6,4 +40,33 @@ def load_resource(resource_path):
Gets the content of a resource Gets the content of a resource
""" """
resource_content = pkg_resources.resource_string(__name__, resource_path) resource_content = pkg_resources.resource_string(__name__, resource_path)
return unicode(resource_content) return unicode(resource_content)
\ No newline at end of file
class TestCaseMixin(object):
""" Helpful mixins for unittest TestCase subclasses """
maxDiff = None
def patch_workbench(self):
self.apply_patch(
'workbench.runtime.WorkbenchRuntime.local_resource_url',
lambda _, _block, path: '/expanded/url/to/drag_and_drop_v2/' + path
)
self.apply_patch(
'workbench.runtime.WorkbenchRuntime.replace_urls',
lambda _, html: re.sub(r'"/static/([^"]*)"', r'"/course/test-course/assets/\1"', html),
create=True,
)
def apply_patch(self, *args, **kwargs):
new_patch = patch(*args, **kwargs)
mock = new_patch.start()
self.addCleanup(new_patch.stop)
return mock
def call_handler(self, handler_name, data=None, expect_json=True, method='POST'):
response = self.block.handle(handler_name, make_request(data, method=method))
if expect_json:
self.assertEqual(response.status_code, 200)
return json.loads(response.body)
return response
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment