Commit 009a80cc by Kelketek

Merge pull request #34 from edx-solutions/master

Drag and Drop v2 - Review for release on edx.org
parents 80ad8ee8 fae37c69
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
.noseids
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Integration test output:
/*.log
/tests.*.png
var/*
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IDEs
.idea
.idea/*
language: python
sudo: false
python:
- "2.7"
before_install:
- "export DISPLAY=:99"
- "sh -e /etc/init.d/xvfb start"
install:
- "sh install_test_deps.sh"
- "pip uninstall -y xblock-drag-and-drop-v2"
- "python setup.py sdist"
- "pip install dist/xblock-drag-and-drop-v2-2.0.0.tar.gz"
script:
- pep8 drag_and_drop_v2 tests --max-line-length=120
- pylint drag_and_drop_v2 tests
- python run_tests.py
notifications:
email: false
addons:
firefox: "43.0"
This diff is collapsed. Click to expand it.
from .drag_and_drop_v2 import DragAndDropBlock
from .utils import _
TARGET_IMG_DESCRIPTION = _(
"An isosceles triangle with three layers of similar height. "
"It is shown upright, so the widest layer is located at the bottom, "
"and the narrowest layer is located at the top."
)
TOP_ZONE_TITLE = _("The Top Zone")
MIDDLE_ZONE_TITLE = _("The Middle Zone")
BOTTOM_ZONE_TITLE = _("The Bottom Zone")
TOP_ZONE_DESCRIPTION = _("Use this zone to associate an item with the top layer of the triangle.")
MIDDLE_ZONE_DESCRIPTION = _("Use this zone to associate an item with the middle layer of the triangle.")
BOTTOM_ZONE_DESCRIPTION = _("Use this zone to associate an item with the bottom layer of the triangle.")
ITEM_CORRECT_FEEDBACK = _("Correct! This one belongs to {zone}.")
ITEM_INCORRECT_FEEDBACK = _("No, this item does not belong here. Try again.")
ITEM_NO_ZONE_FEEDBACK = _("You silly, there are no zones for this one.")
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": [
{
"index": 1,
"id": "zone-1",
"title": TOP_ZONE_TITLE,
"description": TOP_ZONE_DESCRIPTION,
"x": 160,
"y": 30,
"width": 196,
"height": 178,
},
{
"index": 2,
"id": "zone-2",
"title": MIDDLE_ZONE_TITLE,
"description": MIDDLE_ZONE_DESCRIPTION,
"x": 86,
"y": 210,
"width": 340,
"height": 138,
},
{
"index": 3,
"id": "zone-3",
"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)
},
"zone": TOP_ZONE_TITLE,
"imageURL": "",
"id": 0,
},
{
"displayName": _("Goes to the middle"),
"feedback": {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=MIDDLE_ZONE_TITLE)
},
"zone": MIDDLE_ZONE_TITLE,
"imageURL": "",
"id": 1,
},
{
"displayName": _("Goes to the bottom"),
"feedback": {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=BOTTOM_ZONE_TITLE)
},
"zone": BOTTOM_ZONE_TITLE,
"imageURL": "",
"id": 2,
},
{
"displayName": _("I don't belong anywhere"),
"feedback": {
"incorrect": ITEM_NO_ZONE_FEEDBACK,
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 3,
},
],
"feedback": {
"start": START_FEEDBACK,
"finish": FINISH_FEEDBACK,
},
}
.xblock--drag-and-drop--editor {
width: 100%;
height: 100%;
}
.modal-window .drag-builder {
width: 100%;
height: calc(100% - 60px);
position: absolute;
overflow-y: scroll;
}
/*** Drop Target ***/
.xblock--drag-and-drop--editor .zone {
position: absolute;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
/* Internet Explorer 10 */
-ms-flex-pack:center;
-ms-flex-align:center;
/* Firefox */
-moz-box-pack:center;
-moz-box-align:center;
/* Safari, Opera, and Chrome */
-webkit-box-pack:center;
-webkit-box-align:center;
/* W3C */
box-pack:center;
box-align:center;
border: 1px dotted #565656;
box-sizing: border-box;
}
.xblock--drag-and-drop--editor .zone p {
width: 100%;
font-family: Arial;
font-size: 16px;
font-weight: bold;
text-align: center;
margin-top: auto;
margin-bottom: auto;
}
/** Builder **/
.xblock--drag-and-drop--editor .hidden {
display: none !important;
}
.xblock--drag-and-drop--editor .tab {
width: 100%;
background-color: #eee;
padding: 3px 0;
position: relative;
}
.xblock--drag-and-drop--editor .tab::after,
.xblock--drag-and-drop--editor .tab-footer::after {
content: "";
display: table;
clear: both;
}
.xblock--drag-and-drop--editor .tab h3,
.xblock--drag-and-drop--editor .tab .h3 {
margin: 20px 0 8px 0;
}
.xblock--drag-and-drop--editor .tab .h3 {
display: block;
font: inherit;
font-size: 100%;
}
.xblock--drag-and-drop--editor .tab-header,
.xblock--drag-and-drop--editor .tab-content,
.xblock--drag-and-drop--editor .tab-footer {
width: 96%;
margin: 2%;
}
.xblock--drag-and-drop--editor .tab-footer {
height: 25px;
position: relative;
display: block;
float: left;
}
.xblock--drag-and-drop--editor .items {
width: calc(100% - 515px);
margin: 10px 0 0 0;
}
.xblock--drag-and-drop--editor .target-image-form input,
.xblock--drag-and-drop--editor .target-image-form textarea {
width: 50%;
}
/* Zones Tab */
.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--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;
width: 18%;
}
.xblock--drag-and-drop--editor .zones-form .zone-row > input {
width: 60%;
margin: 0 0 5px;
line-height: 2.664rem; /* .title gets line-height from a Studio rule that does not apply to .description;
here we make sure that both input fields get the same value for line-height */
}
.xblock--drag-and-drop--editor .zones-form .zone-row .layout {
margin-bottom: 15px;
}
.xblock--drag-and-drop--editor .zones-form .zone-row .layout .size,
.xblock--drag-and-drop--editor .zones-form .zone-row .layout .coord {
width: 15%;
margin: 0 19px 5px 0;
}
.xblock--drag-and-drop--editor .feedback-form textarea {
width: 99%;
height: 128px;
}
.xblock--drag-and-drop--editor .target-image-form .target-image-form-help,
.xblock--drag-and-drop--editor .item-styles-form .item-styles-form-help {
margin-top: 5px;
font-size: small;
}
.xblock--drag-and-drop--editor .item-styles-form,
.xblock--drag-and-drop--editor .items-form {
margin-bottom: 30px;
}
.xblock--drag-and-drop--editor .items-form .item {
background-color: #8fcaec;
padding: 10px 0 1px;
margin: 15px 0;
}
.xblock--drag-and-drop--editor .items-form label {
margin: 0 1%;
}
.xblock--drag-and-drop--editor .items-form input,
.xblock--drag-and-drop--editor .items-form select {
width: 35%;
}
.xblock--drag-and-drop--editor .items-form .item-image-url {
width: 81%;
margin-right: 1%;
}
.xblock--drag-and-drop--editor .items-form .item-width {
width: 50px;
}
.xblock--drag-and-drop--editor .items-form .item-numerical-value,
.xblock--drag-and-drop--editor .items-form .item-numerical-margin {
margin: 0 1%;
width: 50%;
}
.xblock--drag-and-drop--editor .items-form textarea {
width: 97%;
margin: 0 1%;
}
.xblock--drag-and-drop--editor .items-form .row {
margin-bottom: 20px;
}
.xblock--drag-and-drop--editor .items-form .row.advanced {
display: none;
}
.xblock--drag-and-drop--editor .items-form .row.advanced-link {
padding-left: 1em;
font-size: 80%;
}
/** Buttons **/
.xblock--drag-and-drop--editor .btn {
background-color: #1d5280;
color: #fff;
border: 1px solid #156ab4;
border-radius: 6px;
padding: 5px 10px;
margin-top: 15px;
}
.xblock--drag-and-drop--editor .btn:hover {
opacity: 0.8;
cursor: pointer;
}
.xblock--drag-and-drop--editor .btn:focus {
outline: none;
opacity: 0.5;
}
.xblock--drag-and-drop--editor .add-element {
text-decoration: none;
color: #1d5280;
}
.xblock--drag-and-drop--editor .remove-zone {
float: right;
margin-top: 2px;
margin-right: 16px;
}
.xblock--drag-and-drop--editor .remove-item {
display: inline-block;
margin-left: 95px;
}
.xblock--drag-and-drop--editor .icon {
width: 14px;
height: 14px;
border-radius: 7px;
background-color: #1d5280;
position: relative;
float: left;
margin: 0 5px 0 0;
}
.xblock--drag-and-drop--editor .add-zone:hover,
.xblock--drag-and-drop--editor .add-zone:hover .icon,
.xblock--drag-and-drop--editor .remove-zone:hover,
.xblock--drag-and-drop--editor .remove-zone:hover .icon {
opacity: 0.7;
}
.xblock--drag-and-drop--editor .tab .field-error {
outline: none;
box-shadow: 0 0 10px darkred;
}
.xblock--drag-and-drop--editor .icon.add:before {
content: '';
height: 10px;
width: 2px;
background-color: #fff;
position: relative;
display: inline;
float: left;
top: 2px;
left: 6px;
}
.xblock--drag-and-drop--editor .icon.add:after {
content: '';
height: 2px;
width: 10px;
background-color: #fff;
position: relative;
display: inline;
float: left;
top: 6px;
left: 0;
}
.xblock--drag-and-drop--editor .icon.remove:before {
content: '';
height: 10px;
width: 2px;
background-color: #fff;
position: relative;
display: inline;
float: left;
top: 2px;
left: 6px;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.xblock--drag-and-drop--editor .icon.remove:after {
content: '';
height: 2px;
width: 10px;
background-color: #fff;
position: relative;
display: inline;
float: left;
top: 6px;
left: 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.xblock--drag-and-drop--editor .remove-item .icon.remove {
background-color: #fff;
color: #0072a7; /* Override default color from Studio to ensure contrast is large enough */
}
.xblock--drag-and-drop--editor .remove-item .icon.remove:before,
.xblock--drag-and-drop--editor .remove-item .icon.remove:after {
background-color: #1d5280;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 2011–2014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
\ No newline at end of file
.themed-xblock.xblock--drag-and-drop {
background-color: #fff;
}
/* Shared styles used in header and footer */
.themed-xblock.xblock--drag-and-drop .title1 {
color: #555555;
text-transform: uppercase;
font-weight: bold;
font-style: normal;
}
/* drag-container holds the .item-bank and the .target */
.themed-xblock.xblock--drag-and-drop .drag-container {
background-color: #ebf0f2;
}
.themed-xblock.xblock--drag-and-drop .item-bank {
border-radius: 0px;
}
/*** DRAGGABLE ITEMS ***/
.themed-xblock.xblock--drag-and-drop .drag-container .option {
border-radius: 0px;
text-align: initial;
font-size: 14px;
background-color: #2e83cd;
color: #fff;
opacity: 1;
}
.themed-xblock.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input {
background-color: #ceffce;
color: #087108;
}
.themed-xblock.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input {
background-color: #ffcece;
color: #ad0d0d;
}
.themed-xblock.xblock--drag-and-drop .drag-container .option.fade {
opacity: 0.5;
}
/*** DROP TARGET ***/
.themed-xblock.xblock--drag-and-drop .target {
background-color: #fff;
}
.themed-xblock.xblock--drag-and-drop .drag-container .target .zone p {
font-family: Arial;
font-size: 16px;
font-weight: bold;
text-align: center;
text-transform: uppercase;
}
/*** FEEDBACK ***/
.themed-xblock.xblock--drag-and-drop .feedback {
border-top: solid 1px #bdbdbd;
}
.themed-xblock.xblock--drag-and-drop .popup {
background-color: #66a5b5;
}
.themed-xblock.xblock--drag-and-drop .popup .popup-content {
color: #ffffff;
font-size: 14px;
}
.themed-xblock.xblock--drag-and-drop .popup .close {
cursor: pointer;
color: #ffffff;
font-family: "fontawesome";
font-size: 18pt;
}
.themed-xblock.xblock--drag-and-drop .link-button {
cursor: pointer;
color: #3384ca;
}
\ No newline at end of file
{% load i18n %}
<section class="themed-xblock xblock--drag-and-drop">
<i class="fa fa-spin fa-spinner initial-load-spinner"></i>{% trans "Loading drag and drop problem." %}
</section>
{% load i18n %}
<div class="xblock--drag-and-drop--editor editor-with-buttons">
{{ js_templates|safe }}
<section class="drag-builder">
<div class="tab feedback-tab">
<p class="tab-content">
{% trans "Note: do not edit the problem if students have already completed it. Delete the problem and create a new one." %}
</p>
<section class="tab-content">
<form class="feedback-form">
<label class="h3" for="display-name">{% trans "Problem title" %}</label>
<input id="display-name" value="{{ self.display_name }}" />
<label title="{{ help_texts.show_title }}">
<input class="show-title" type="checkbox"
{% if self.show_title %}checked="checked"{% endif %}>
{% trans "Show title" %}
<span class="sr">{{ help_texts.show_title }}</span>
</label>
<label class="h3" for="weight">{% trans "Maximum score" %}</label>
<input id="weight" type="number" step="0.1" value="{{ self.weight }}" />
<label class="h3" for="problem-text">{% trans "Problem text" %}</label>
<textarea id="problem-text">{{ self.question_text }}</textarea>
<label title="{{ help_texts.show_question_header }}">
<input class="show-problem-header" type="checkbox"
{% if self.show_question_header %}checked="checked"{% endif %}>
{% trans 'Show "Problem" heading' %}
<span class="sr">{{ help_texts.show_question_header }}</span>
</label>
<label class="h3" for="intro-feedback">{% trans "Introductory Feedback" %}</label>
<textarea id="intro-feedback">{{ self.data.feedback.start }}</textarea>
<label class="h3" for="final-feedback">{% trans "Final Feedback" %}</label>
<textarea id="final-feedback">{{ self.data.feedback.finish }}</textarea>
</form>
</section>
</div>
<div class="tab zones-tab hidden">
<header class="tab-header">
<h3>{% trans "Zones" %}</h3>
</header>
<section class="tab-content">
<form class="target-image-form">
<label class="h3" for="background-url">{% trans "Background URL" %}</label>
<input id="background-url"
type="text"
placeholder="{% trans 'For example, http://example.com/background.png or /static/background.png' %}">
<label class="h3" for="background-description">{% trans "Background description" %}</label>
<textarea required id="background-description"
aria-describedby="background-description-description"></textarea>
<div id="background-description-description" class="target-image-form-help">
{% blocktrans %}
Please provide a description of the image for non-visual users.
The description should provide sufficient information to allow anyone
to solve the problem even without seeing the image.
{% endblocktrans %}
</div>
<button class="btn">{% trans "Change background" %}</button>
</form>
</section>
<section class="tab-content">
<form class="display-labels-form">
<h3>{% trans "Zone labels" %}</h3>
<label for="display-labels">{% trans "Display label names on the image" %}:</label>
<input name="display-labels" id="display-labels" type="checkbox" />
</form>
<form class="display-borders-form">
<h3>{% trans "Zone borders" %}</h3>
<label for="display-borders">{% trans "Display zone borders on the image" %}:</label>
<input name="display-borders" id="display-borders" type="checkbox" />
</form>
</section>
<section class="tab-content">
<div class="zone-editor">
<div class="controls">
<form class="zones-form"></form>
<a href="#" class="add-zone add-element"><div class="icon add"></div>{% trans "Add a zone" %}</a>
</div>
<div class="target">
<img class="target-img">
<div class="zones-preview">
</div>
</div>
</div>
</section>
</div>
<div class="tab items-tab hidden">
<header class="tab-header">
<h3>{% trans "Items" %}</h3>
</header>
<section class="tab-content">
<form class="item-styles-form">
<label class="h3" for="item-background-color">{% trans "Background color" %}</label>
<input id="item-background-color"
placeholder="e.g. blue or #0000ff"
value="{{ self.item_background_color}}"
aria-describedby="item-background-color-description">
<div id="item-background-color-description" class="item-styles-form-help">
{{ help_texts.item_background_color }}
</div>
<label class="h3" for="item-text-color">{% trans "Text color" %}</label>
<input id="item-text-color"
placeholder="e.g. white or #ffffff"
value="{{ self.item_text_color}}"
aria-describedby="item-text-color-description">
<div id="item-text-color-description" class="item-styles-form-help">
{{ help_texts.item_text_color }}
</div>
</form>
</section>
<section class="tab-content">
<form class="items-form"></form>
</section>
<footer class="tab-footer">
<a href="#" class="add-item add-element"><div class="icon add"></div>{% trans "Add an item" %}</a>
</footer>
</div>
</section>
<div class="xblock-actions">
<span class="xblock-editor-error-message"></span>
<ul>
<li class="action-item">
<a href="#" class="button action-primary continue-button">{% trans "Continue" %}</a>
</li>
<li class="action-item hidden">
<a href="#" class="button action-primary save-button">{% trans "Save" %}</a>
</li>
<li class="action-item">
<a href="#" class="button cancel-button">{% trans "Cancel" %}</a>
</li>
</ul>
</div>
</div>
<script id="zone-element-tpl" type="text/html">
<div id="{{ id }}" class="zone" data-zone="{{ title }}" style="
top:{{ y_percent }}%;
left:{{ x_percent }}%;
width:{{ width_percent }}%;
height:{{ height_percent }}%;">
<p>{{{ title }}}</p>
<p class="sr">{{{ description }}}</p>
</div>
</script>
<script id="zone-input-tpl" type="text/html">
<div class="zone-row {{ id }}" data-index="{{index}}">
<label for="zone-{{index}}-title">{{i18n "Text"}}</label>
<input type="text"
id="zone-{{index}}-title"
class="title"
value="{{ title }}"
required />
<a href="#" class="remove-zone hidden">
<div class="icon remove"></div>
</a>
<label for="zone-{{index}}-description">{{i18n "Description"}}</label>
<input type="text"
id="zone-{{index}}-description"
class="description"
value="{{ description }}"
placeholder="{{i18n 'Describe this zone to non-visual users'}}"
required />
<div class="layout">
<label for="zone-{{index}}-width">{{i18n "width"}}</label>
<input type="text"
id="zone-{{index}}-width"
class="size width"
value="{{ width }}" />
<label for="zone-{{index}}-height">{{i18n "height"}}</label>
<input type="text"
id="zone-{{index}}-height"
class="size height"
value="{{ height }}" />
<br />
<label for="zone-{{index}}-x">x</label>
<input type="text"
id="zone-{{index}}-x"
class="coord x"
value="{{ x }}" />
<label for="zone-{{index}}-y">y</label>
<input type="text"
id="zone-{{index}}-y"
class="coord y"
value="{{ y }}" />
</div>
</div>
</script>
<script id="zone-dropdown-tpl" type="text/html">
<option value="{{ value }}" {{ selected }}>{{ value }}</option>
</script>
<script id="item-input-tpl" type="text/html">
<div class="item">
<div class="row">
<label for="item-{{id}}-text">{{i18n "Text"}}</label>
<input type="text"
id="item-{{id}}-text"
class="item-text"
value="{{ displayName }}" />
<label for="item-{{id}}-zone">{{i18n "Zone"}}</label>
<select id="item-{{id}}-zone"
class="zone-select">{{ dropdown }}</select>
<a href="#" class="remove-item hidden">
<div class="icon remove"></div>
</a>
</div>
<div class="row">
<label for="item-{{id}}-image-url">{{i18n "Image URL (alternative to the text)"}}</label>
<input type="text"
id="item-{{id}}-image-url"
class="item-image-url"
value="{{ imageURL }}" />
</div>
<div class="row">
<label for="item-{{id}}-image-description">{{i18n "Image description (should provide sufficient information to place the item even if the image did not load)"}}</label>
<textarea required id="item-{{id}}-image-description"
class="item-image-description">{{ imageDescription }}</textarea>
</div>
<div class="row">
<label for="item-{{id}}-success-feedback">{{i18n "Success Feedback"}}</label>
<textarea id="item-{{id}}-success-feedback"
class="success-feedback">{{ feedback.correct }}</textarea>
</div>
<div class="row">
<label for="item-{{id}}-error-feedback">{{i18n "Error Feedback"}}</label>
<textarea id="item-{{id}}-error-feedback"
class="error-feedback">{{ feedback.incorrect }}</textarea>
</div>
<div class="row advanced-link">
<a href="#">{{i18n "Show advanced settings" }}</a>
</div>
<div class="row advanced">
<label for="item-{{id}}-width-percent">{{i18n "Preferred width as a percentage of the background image width (or blank for automatic width):"}}</label>
<input type="number" id="item-{{id}}-width-percent" class="item-width" value="{{ singleDecimalFloat widthPercent }}" step="0.1" min="1" max="99" />%
</div>
<div class="row advanced">
<label for="item-{{id}}-numerical-value">
{{i18n "Optional numerical value (if you set this, learners will be prompted for this value after dropping this item)"}}
</label>
<input type="number"
step="0.1"
id="item-{{id}}-numerical-value"
class="item-numerical-value" value="{{ numericalValue }}" />
</div>
<div class="row advanced">
<label for="item-{{id}}-numerical-margin">
{{i18n "Margin ± (when a numerical value is required, values entered by learners must not differ from the expected value by more than this margin; default is zero)"}}
</label>
<input type="number"
step="0.1"
id="item-{{id}}-numerical-margin"
class="item-numerical-margin" value="{{ numericalMargin }}" />
</div>
</div>
</script>
# -*- coding: utf-8 -*-
#
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Installs xblock-sdk and dependencies needed to run the tests suite.
# Run this script inside a fresh virtual environment.
pip install -e git://github.com/edx/xblock-sdk.git@4e8e713e7dd886b8d2eb66b5001216b66b9af81a#egg=xblock-sdk
cd $VIRTUAL_ENV/src/xblock-sdk/ && pip install -r requirements/base.txt \
&& pip install -r requirements/test.txt && cd -
pip install -r requirements.txt
[REPORTS]
reports=no
[FORMAT]
max-line-length=120
[MESSAGES CONTROL]
disable=
attribute-defined-outside-init,
locally-disabled,
missing-docstring,
too-many-ancestors,
too-many-arguments,
too-many-instance-attributes,
too-few-public-methods,
too-many-public-methods,
unused-argument,
invalid-name,
no-member
[SIMILARITIES]
min-similarity-lines=8
git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils==1.0.2
-e .
#!/usr/bin/env python
"""
Run tests for the Drag and Drop V2 XBlock.
This script is required to run our selenium tests inside the xblock-sdk workbench
because the workbench SDK's settings file is not inside any python module.
"""
import logging
import os
import sys
import workbench
if __name__ == "__main__":
# Find the location of the XBlock SDK. Note: it must be installed in development mode.
# ('python setup.py develop' or 'pip install -e')
xblock_sdk_dir = os.path.dirname(os.path.dirname(workbench.__file__))
sys.path.append(xblock_sdk_dir)
# Use the workbench settings file:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
# 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")
# Silence too verbose Django logging
logging.disable(logging.DEBUG)
try:
os.mkdir('var')
except OSError:
# The var dir may already exist.
pass
from django.core.management import execute_from_command_line
args = sys.argv[1:]
paths = [arg for arg in args if arg[0] != '-']
if not paths:
paths = ["tests/"]
options = [arg for arg in args if arg not in paths]
execute_from_command_line([sys.argv[0], "test"] + paths + options)
# -*- coding: utf-8 -*-
# Imports ###########################################################
import os
from setuptools import setup
# Functions #########################################################
def package_data(pkg, root_list):
"""Generic function to find package_data for `pkg` under `root`."""
data = []
for root in root_list:
for dirname, _, files in os.walk(os.path.join(pkg, root)):
for fname in files:
data.append(os.path.relpath(os.path.join(dirname, fname), pkg))
return {pkg: data}
# Main ##############################################################
setup(
name='xblock-drag-and-drop-v2',
version='2.0.0',
description='XBlock - Drag-and-Drop v2',
packages=['drag_and_drop_v2'],
install_requires=[
'XBlock',
'xblock-utils',
'ddt',
'mock',
],
entry_points={
'xblock.v1': 'drag-and-drop-v2 = drag_and_drop_v2:DragAndDropBlock',
},
package_data=package_data("drag_and_drop_v2", ["static", "templates", "public"]),
)
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 200 200" width="200px" height="200px" xml:space="preserve">
<style type="text/css">
.st0{fill:#FBB03B;stroke:#000000;stroke-miterlimit:10;}
.st1{font-family:'Helvetica';}
.st2{font-size:24px;}
</style>
<rect class="st0" width="200" height="200"/>
<text transform="matrix(1 0 0 1 34.6152 104.5322)" class="st1 st2">200 x 200px</text>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 300" width="400px" height="300px" xml:space="preserve">
<style type="text/css">
.st0{fill:#8CC63F;stroke:#000000;stroke-miterlimit:10;}
.st1{font-family:'Helvetica';}
.st2{font-size:48px;}
</style>
<rect class="st0" width="400" height="300"/>
<text transform="matrix(1 0 0 1 71 160)" class="st1 st2">400 x 300px</text>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 60 60" width="60px" height="60px" xml:space="preserve">
<style type="text/css">
.st0{fill:#BBF03B;}
.st1{font-family:'Helvetica';}
.st2{font-size:12px;}
</style>
<rect class="st0" width="60" height="60"/>
<text transform="matrix(1 0 0 1 3 35)" class="st1 st2">60 x 60px</text>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 500 500" width="500px" height="500px" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#999999;}
.st2{fill:#F2F2F2;}
.st3{font-family:'Helvetica-Bold';}
.st4{font-size:24px;}
.st5{letter-spacing:-1;}
</style>
<g id="Layer_2">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1.127" y1="250" x2="500" y2="250">
<stop offset="0" style="stop-color:#CCE0F4"/>
<stop offset="1" style="stop-color:#005B97"/>
</linearGradient>
<rect x="0" class="st0" width="500" height="500"/>
</g>
<g id="Layer_1">
<rect x="0" y="200" class="st1" width="250" height="100"/>
<rect x="0" y="400" class="st1" width="375" height="100"/>
<rect x="0" class="st1" width="166.7" height="100"/>
<text transform="matrix(1 0 0 1 36 58)" class="st2 st3 st4">&lt;- 1/3 -&gt;</text>
<text transform="matrix(1 0 0 1 74 260)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="22" y="0" class="st2 st3 st4"> </tspan><tspan x="29.2" y="0" class="st2 st3 st4">50% -</tspan><tspan x="91.9" y="0" class="st2 st3 st4 st5">&gt;</tspan></text>
<text transform="matrix(1 0 0 1 150 455)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="22" y="0" class="st2 st3 st4"> </tspan><tspan x="29.2" y="0" class="st2 st3 st4">75% -</tspan><tspan x="91.9" y="0" class="st2 st3 st4 st5">&gt;</tspan></text>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1600 900" width="1600px" height="900px" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#999999;}
.st2{fill:#F2F2F2;}
.st3{font-family:'Helvetica-Bold';}
.st4{font-size:60px;}
.st5{letter-spacing:1;}
.st6{letter-spacing:-3;}
</style>
<g id="Layer_2">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1.127" y1="450" x2="1600" y2="450">
<stop offset="0" style="stop-color:#CCE0F4"/>
<stop offset="1" style="stop-color:#005B97"/>
</linearGradient>
<rect x="0" class="st0" width="1600" height="900"/>
</g>
<g id="Layer_1">
<rect y="350" class="st1" width="800" height="200"/>
<rect x="0" y="700" class="st1" width="1200" height="200"/>
<rect x="0" class="st1" width="533.33" height="200"/>
<text transform="matrix(1 0 0 1 145 115)" class="st2 st3 st4">&lt;- 1/3 -&gt;</text>
<text transform="matrix(1 0 0 1 288 462)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="55" y="0" class="st2 st3 st4 st5"> </tspan><tspan x="72.9" y="0" class="st2 st3 st4">50% -</tspan><tspan x="229.6" y="0" class="st2 st3 st4 st6">&gt;</tspan></text>
<text transform="matrix(1 0 0 1 486 821)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="55" y="0" class="st2 st3 st4 st5"> </tspan><tspan x="72.9" y="0" class="st2 st3 st4">75% -</tspan><tspan x="229.6" y="0" class="st2 st3 st4 st6">&gt;</tspan></text>
</g>
</svg>
{
"question_text": "Note: This is an example of data in the format used by prior versions of this block. Not in particular the old `size` data.",
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone 1",
"height": 100,
"y": "200",
"x": "100",
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone 2",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"feedback": {
"incorrect": "No 1",
"correct": "Yes 1"
},
"zone": "Zone 1",
"backgroundImage": "",
"id": 0,
"size": {
"width": "190px",
"height": "auto"
}
},
{
"displayName": "2",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "Zone 2",
"backgroundImage": "",
"id": 1,
"size": {
"width": "190px",
"height": "100px"
},
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "Pic",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "Zone 1",
"backgroundImage": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==",
"id": 2,
"size": {
"width": "100px",
"height": "auto"
}
}
],
"state": {
"items": {},
"finished": true
},
"feedback": {
"start": "Intro Feedback",
"finish": "Final Feedback"
},
"title": "Drag and Drop (Old-style data)"
}
\ No newline at end of file
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone 1",
"height": 100,
"y": "200",
"x": "100",
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone 2",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1 here",
"feedback": {
"incorrect": "No 1",
"correct": "Yes 1"
},
"zone": "Zone 1",
"imageURL": "",
"id": 0
},
{
"displayName": "2 here",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "Zone 2",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "No Zone for this",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2
}
],
"feedback": {
"start": "Some Intro Feed",
"finish": "Some Final Feed"
}
}
{
"zones": [
{
"index": 1,
"title": "Zone 1",
"description": "This describes zone 1",
"height": 178,
"width": 196,
"y": "30",
"x": "160",
"id": "zone-1"
},
{
"index": 2,
"title": "Zone 2",
"description": "This describes zone 2",
"height": 140,
"width": 340,
"y": "210",
"x": "86",
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"imageURL": "https://placehold.it/100x100",
"imageDescription": "This describes the background image of item 1",
"feedback": {
"incorrect": "No, 1 does not belong here",
"correct": "Yes, 1 goes here"
},
"zone": "Zone 1",
"id": 0
},
{
"displayName": "2",
"imageURL": "https://placehold.it/100x100",
"imageDescription": "This describes the background image of item 2",
"feedback": {
"incorrect": "No, 2 does not belong here",
"correct": "Yes, 2 goes here"
},
"zone": "Zone 2",
"id": 1
},
{
"displayName": "X",
"imageURL": "",
"feedback": {
"incorrect": "You silly, there are no zones for X",
"correct": ""
},
"zone": "none",
"id": 2
}
],
"feedback": {
"start": "Drag the items onto the image above.",
"finish": "Good work! You have completed this drag and drop problem."
},
"targetImgDescription": "This describes the target image",
"displayLabels": {display_labels_value},
"displayBorders": {display_borders_value},
}
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone 51",
"height": 100,
"y": "400",
"x": "200",
"id": "zone-51"
},
{
"index": 2,
"width": 200,
"title": "Zone 52",
"height": 100,
"y": "200",
"x": "100",
"id": "zone-52"
}
],
"items": [
{
"displayName": "Item 1",
"feedback": {
"incorrect": "Incorrect 1",
"correct": "Correct 1"
},
"zone": "Zone 51",
"imageURL": "",
"id": 10
},
{
"displayName": "Item 2",
"feedback": {
"incorrect": "Incorrect 2",
"correct": "Correct 2"
},
"zone": "Zone 52",
"imageURL": "",
"id": 20,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "No Zone for this",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 30
}
],
"feedback": {
"start": "Other Intro Feed",
"finish": "Other Final Feed"
}
}
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone <i>1</i>",
"height": 100,
"y": 200,
"x": 100,
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone <b>2</b>",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "<b>1</b>",
"feedback": {
"incorrect": "No <b>1</b>",
"correct": "Yes <b>1</b>"
},
"zone": "Zone <i>1</i>",
"imageURL": "",
"id": 0
},
{
"displayName": "<i>2</i>",
"feedback": {
"incorrect": "No <i>2</i>",
"correct": "Yes <i>2</i>"
},
"zone": "Zone <b>2</b>",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "<span style='color:red'>X</span>",
"feedback": {
"incorrect": "No Zone for <i>X</i>",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2
}
],
"feedback": {
"start": "Intro <i>Feed</i>",
"finish": "Final <b>Feed</b>"
},
"targetImg": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==",
"targetImgDescription": "This describes the target image"
}
{
{% if img == "wide" %}
"targetImg": "{{img_wide_url}}",
"zones": [
{"index": 1, "title": "Zone 1/3", "width": 533, "height": 200, "x": "0", "y": "0", "id": "zone-1"},
{"index": 2, "title": "Zone 50%", "width": 800, "height": 200, "x": "0", "y": "350", "id": "zone-2"},
{"index": 3, "title": "Zone 75%", "width": 1200, "height": 200, "x": "0", "y": "700", "id": "zone-3"}
],
{% else %}
"targetImg": "{{img_square_url}}",
"zones": [
{"index": 1, "title": "Zone 1/3", "width": 166, "height": 100, "x": "0", "y": "0", "id": "zone-1"},
{"index": 2, "title": "Zone 50%", "width": 250, "height": 100, "x": "0", "y": "200", "id": "zone-2"},
{"index": 3, "title": "Zone 75%", "width": 375, "height": 100, "x": "0", "y": "400", "id": "zone-3"}
],
{% endif %}
"displayBorders": true,
"items": [
{
"displayName": "Auto",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "",
"id": 0
},
{
"displayName": "Auto with long text that should wrap because draggables are given a maximum width",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "",
"id": 1
},
{
"displayName": "33.3%",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "",
"id": 2,
"widthPercent": 33.3
},
{
"displayName": "50%",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "",
"id": 3,
"widthPercent": 50
},
{
"displayName": "75%",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 75%",
"imageURL": "",
"id": 4,
"widthPercent": 75
},
{
"displayName": "IMG 400x300",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_400x300_url}}",
"id": 5,
},
{
"displayName": "IMG 200x200",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_200x200_url}}",
"id": 6,
},
{
"displayName": "IMG 400x300",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_400x300_url}}",
"id": 7,
"widthPercent": 50
},
{
"displayName": "IMG 200x200",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_200x200_url}}",
"id": 8,
"widthPercent": 50
},
{
"displayName": "IMG 60x60",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "{{img_60x60_url}}",
"id": 9
}
],
"feedback": {"start": "Some Intro Feedback", "finish": "Some Final Feedback"}
}
# Imports ###########################################################
from xml.sax.saxutils import escape
from selenium.webdriver.support.ui import WebDriverWait
from workbench import scenarios
from xblockutils.resources import ResourceLoader
from xblockutils.base_test import SeleniumBaseTest
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
class BaseIntegrationTest(SeleniumBaseTest):
default_css_selector = 'section.themed-xblock.xblock--drag-and-drop'
module_name = __name__
_additional_escapes = {
'"': "&quot;",
"'": "&apos;"
}
@staticmethod
def _make_scenario_xml(display_name, show_title, problem_text, completed=False, show_problem_header=True):
return """
<vertical_demo>
<drag-and-drop-v2
display_name='{display_name}'
show_title='{show_title}'
question_text='{problem_text}'
show_question_header='{show_problem_header}'
weight='1'
completed='{completed}'
/>
</vertical_demo>
""".format(
display_name=escape(display_name),
show_title=show_title,
problem_text=escape(problem_text),
show_problem_header=show_problem_header,
completed=completed,
)
def _get_custom_scenario_xml(self, filename):
data = loader.load_unicode(filename)
return "<vertical_demo><drag-and-drop-v2 data='{data}'/></vertical_demo>".format(
data=escape(data, self._additional_escapes)
)
def _add_scenario(self, identifier, title, xml):
scenarios.add_xml_scenario(identifier, title, xml)
self.addCleanup(scenarios.remove_scenario, identifier)
def _get_items(self):
items_container = self._page.find_element_by_css_selector('.item-bank')
return items_container.find_elements_by_css_selector('.option')
def _get_zones(self):
return self._page.find_elements_by_css_selector(".drag-container .zone")
def _get_popup(self):
return self._page.find_element_by_css_selector(".popup")
def _get_popup_content(self):
return self._page.find_element_by_css_selector(".popup .popup-content")
def _get_keyboard_help(self):
return self._page.find_element_by_css_selector(".keyboard-help")
def _get_keyboard_help_button(self):
return self._page.find_element_by_css_selector(".keyboard-help .keyboard-help-button")
def _get_keyboard_help_dialog(self):
return self._page.find_element_by_css_selector(".keyboard-help .keyboard-help-dialog")
def _get_reset_button(self):
return self._page.find_element_by_css_selector('.reset-button')
def _get_feedback(self):
return self._page.find_element_by_css_selector(".feedback")
def _get_feedback_message(self):
return self._page.find_element_by_css_selector(".feedback .message")
def scroll_down(self, pixels=50):
self.browser.execute_script("$(window).scrollTop({})".format(pixels))
@staticmethod
def get_element_html(element):
return element.get_attribute('innerHTML').strip()
@staticmethod
def get_element_classes(element):
return element.get_attribute('class').split()
def wait_until_html_in(self, html, elem):
wait = WebDriverWait(elem, 2)
wait.until(lambda e: html in e.get_attribute('innerHTML'),
u"{} should be in {}".format(html, elem.get_attribute('innerHTML')))
@staticmethod
def wait_until_has_class(class_name, elem):
wait = WebDriverWait(elem, 2)
wait.until(lambda e: class_name in e.get_attribute('class').split(),
u"Class name {} not in {}".format(class_name, elem.get_attribute('class')))
from .test_base import BaseIntegrationTest
class TestCustomDataDragAndDropRendering(BaseIntegrationTest):
PAGE_TITLE = 'Drag and Drop v2'
PAGE_ID = 'drag_and_drop_v2'
def setUp(self):
super(TestCustomDataDragAndDropRendering, self).setUp()
scenario_xml = self._get_custom_scenario_xml("data/test_html_data.json")
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml)
self._page = self.go_to_page(self.PAGE_TITLE)
header1 = self.browser.find_element_by_css_selector('h1')
self.assertEqual(header1.text, 'XBlock: ' + self.PAGE_TITLE)
def test_items_rendering(self):
items = self._get_items()
self.assertEqual(len(items), 3)
self.assertIn('<b>1</b>', self.get_element_html(items[0]))
self.assertIn('<i>2</i>', self.get_element_html(items[1]))
self.assertIn('<input class="input" type="text">', self.get_element_html(items[1]))
self.assertIn('<span style="color:red">X</span>', self.get_element_html(items[2]))
def test_background_image(self):
bg_image = self.browser.find_element_by_css_selector(".xblock--drag-and-drop .target-img")
custom_image_url = (
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciI"
"HdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg=="
)
custom_image_description = "This describes the target image"
self.assertEqual(bg_image.get_attribute("src"), custom_image_url)
self.assertEqual(bg_image.get_attribute("alt"), custom_image_description)
from ddt import ddt, unpack, data
from selenium.common.exceptions import NoSuchElementException
from .test_base import BaseIntegrationTest
from workbench import scenarios
@ddt
class TestDragAndDropTitleAndProblem(BaseIntegrationTest):
@unpack
@data(
('Plain text problem 1, header visible.', True),
('Plain text problem 2, header hidden.', False),
('Problem/instructions with <i>HTML</i> and header.', True),
('<span style="color: red;">Span problem, no header</span>', False),
)
def test_problem_parameters(self, problem_text, show_problem_header):
const_page_name = 'Test title and problem parameters'
const_page_id = 'test_block_title_and_problem'
scenario_xml = self._make_scenario_xml(
display_name="Title",
show_title=True,
problem_text=problem_text,
show_problem_header=show_problem_header,
)
scenarios.add_xml_scenario(const_page_id, const_page_name, scenario_xml)
self.addCleanup(scenarios.remove_scenario, const_page_id)
page = self.go_to_page(const_page_name)
is_problem_header_visible = len(page.find_elements_by_css_selector('section.problem > h3')) > 0
self.assertEqual(is_problem_header_visible, show_problem_header)
problem = page.find_element_by_css_selector('section.problem > p')
self.assertEqual(self.get_element_html(problem), problem_text)
@unpack
@data(
('plain shown', 'title1', True),
('plain hidden', 'title2', False),
('html shown', 'title with <i>HTML</i>', True),
('html hidden', '<span style="color:red">Title: HTML?</span>', False)
)
def test_title_parameters(self, _, display_name, show_title):
const_page_name = 'Test show title parameter'
const_page_id = 'test_block_show_title'
scenario_xml = self._make_scenario_xml(
display_name=display_name,
show_title=show_title,
problem_text='Generic problem',
)
scenarios.add_xml_scenario(const_page_id, const_page_name, scenario_xml)
self.addCleanup(scenarios.remove_scenario, const_page_id)
page = self.go_to_page(const_page_name)
if show_title:
problem_header = page.find_element_by_css_selector('h2.problem-title')
self.assertEqual(self.get_element_html(problem_header), display_name)
else:
with self.assertRaises(NoSuchElementException):
page.find_element_by_css_selector('h2.problem-title')
{
"title": "DnDv2 XBlock with HTML instructions",
"show_title": false,
"problem_text": "Solve this <strong>drag-and-drop</strong> problem.",
"show_problem_header": false,
"target_img_expanded_url": "/expanded/url/to/drag_and_drop_v2/public/img/triangle.png",
"target_img_description": "This describes the target image",
"item_background_color": "white",
"item_text_color": "#000080",
"initial_feedback": "HTML <strong>Intro</strong> Feed",
"display_zone_borders": false,
"display_zone_labels": false,
"url_name": "unique_name",
"zones": [
{
"index": 1,
"title": "Zone <i>1</i>",
"x": 100,
"y": 200,
"width": 200,
"height": 100,
"id": "zone-1"
},
{
"index": 2,
"title": "Zone <b>2</b>",
"x": 0,
"y": 0,
"width": 200,
"height": 100,
"id": "zone-2"
}
],
"items": [
{
"displayName": "<b>1</b>",
"imageURL": "",
"id": 0,
"inputOptions": false
},
{
"displayName": "<i>2</i>",
"imageURL": "",
"id": 1,
"inputOptions": true
},
{
"displayName": "X",
"imageURL": "",
"id": 2,
"inputOptions": false
},
{
"displayName": "",
"imageURL": "http://placehold.it/100x300",
"id": 3,
"inputOptions": false
}
]
}
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone <i>1</i>",
"height": 100,
"y": 200,
"x": 100,
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone <b>2</b>",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "<b>1</b>",
"feedback": {
"incorrect": "No <b>1</b>",
"correct": "Yes <b>1</b>"
},
"zone": "Zone <i>1</i>",
"imageURL": "",
"id": 0
},
{
"displayName": "<i>2</i>",
"feedback": {
"incorrect": "No <i>2</i>",
"correct": "Yes <i>2</i>"
},
"zone": "Zone <b>2</b>",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2
},
{
"displayName": "",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "http://placehold.it/100x300",
"id": 3
}
],
"feedback": {
"start": "HTML <strong>Intro</strong> Feed",
"finish": "Final <strong>feedback</strong>!"
},
"targetImgDescription": "This describes the target image"
}
{
"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",
"url_name": "unique_name"
}
{
"title": "Drag and Drop",
"show_title": true,
"problem_text": "",
"show_problem_header": true,
"target_img_expanded_url": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"target_img_description": "This describes the target image",
"item_background_color": null,
"item_text_color": null,
"initial_feedback": "Intro Feed",
"display_zone_borders": false,
"display_zone_labels": false,
"url_name": "",
"zones": [
{
"index": 1,
"title": "Zone 1",
"x": "100",
"y": "200",
"width": 200,
"height": 100,
"id": "zone-1"
},
{
"index": 2,
"title": "Zone 2",
"x": 0,
"y": 0,
"width": 200,
"height": 100,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"imageURL": "",
"id": 0,
"inputOptions": false,
"size": {"height": "auto", "width": "190px"}
},
{
"displayName": "2",
"imageURL": "",
"id": 1,
"inputOptions": true,
"size": {"height": "auto", "width": "190px"}
},
{
"displayName": "X",
"imageURL": "",
"id": 2,
"inputOptions": false,
"size": {"height": "100px", "width": "100px"}
},
{
"displayName": "",
"imageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"id": 3,
"inputOptions": false,
"size": {"height": "auto", "width": "190px"}
}
]
}
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone 1",
"height": 100,
"y": "200",
"x": "100",
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone 2",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"feedback": {
"incorrect": "No 1",
"correct": "Yes 1"
},
"zone": "Zone 1",
"imageURL": "",
"id": 0,
"size": {
"width": "190px",
"height": "auto"
}
},
{
"displayName": "2",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "Zone 2",
"imageURL": "",
"id": 1,
"size": {
"width": "190px",
"height": "auto"
},
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2,
"size": {
"width": "100px",
"height": "100px"
}
},
{
"displayName": "",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"id": 3,
"size": {
"width": "190px",
"height": "auto"
}
}
],
"state": {
"items": {},
"finished": true
},
"feedback": {
"start": "Intro Feed",
"finish": "Final Feed"
},
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"targetImgDescription": "This describes the target image"
}
{
"title": "DnDv2 XBlock with plain text instructions",
"show_title": true,
"problem_text": "Can you solve this drag-and-drop problem?",
"show_problem_header": true,
"target_img_expanded_url": "http://placehold.it/800x600",
"target_img_description": "This describes the target image",
"item_background_color": null,
"item_text_color": null,
"initial_feedback": "This is the initial feedback.",
"display_zone_borders": false,
"display_zone_labels": false,
"url_name": "test",
"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",
"imageURL": "",
"id": 0,
"inputOptions": false
},
{
"displayName": "2",
"imageURL": "",
"id": 1,
"inputOptions": true
},
{
"displayName": "X",
"imageURL": "",
"id": 2,
"inputOptions": false
},
{
"displayName": "",
"imageURL": "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",
"imageURL": "",
"id": 0
},
{
"displayName": "2",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "Zone 2",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2
},
{
"displayName": "",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "http://placehold.it/200x100",
"id": 3
}
],
"feedback": {
"start": "This is the initial feedback.",
"finish": "This is the final feedback."
},
"targetImg": "http://placehold.it/800x600",
"targetImgDescription": "This describes the target image",
"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": "",
"url_name": "test"
}
# Imports ###########################################################
import json
import unittest
from xblockutils.resources import ResourceLoader
from ..utils import make_block, TestCaseMixin
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
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(loader.load_unicode('data/{}/data.json'.format(cls.FOLDER)))
@classmethod
def initial_settings(cls):
return json.loads(loader.load_unicode('data/{}/settings.json'.format(cls.FOLDER)))
@classmethod
def expected_configuration(cls):
return json.loads(loader.load_unicode('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):
# Drop item that requires numerical input
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, "zone": self.ZONE_2,
},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
# Submit incorrect value
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%", "correct_input": False, "zone": self.ZONE_2,
"input": "250",
},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
# Submit correct value
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%", "correct_input": True, "zone": self.ZONE_2,
"input": "103",
},
},
'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, "zone": self.ZONE_1}
},
"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, "zone": self.ZONE_1,
},
"1": {
"x_percent": "22%", "y_percent": "22%", "correct_input": True, "zone": self.ZONE_2,
"input": "99",
}
},
"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 drag_and_drop_v2.default_data import (
TARGET_IMG_DESCRIPTION, TOP_ZONE_TITLE, MIDDLE_ZONE_TITLE, BOTTOM_ZONE_TITLE,
START_FEEDBACK, FINISH_FEEDBACK, DEFAULT_DATA
)
from ..utils import 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="themed-xblock xblock--drag-and-drop">', student_fragment.content)
self.assertIn('Loading drag and drop problem.', 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_borders": False,
"display_zone_labels": False,
"title": "Drag and Drop",
"show_title": True,
"problem_text": "",
"show_problem_header": True,
"target_img_expanded_url": '/expanded/url/to/drag_and_drop_v2/public/img/triangle.png',
"target_img_description": TARGET_IMG_DESCRIPTION,
"item_background_color": None,
"item_text_color": None,
"initial_feedback": START_FEEDBACK,
"url_name": "",
})
self.assertEqual(zones, DEFAULT_DATA["zones"])
# Items should contain no answer data:
self.assertEqual(items, [
{"id": 0, "displayName": "Goes to the top", "imageURL": "", "inputOptions": False},
{"id": 1, "displayName": "Goes to the middle", "imageURL": "", "inputOptions": False},
{"id": 2, "displayName": "Goes to the bottom", "imageURL": "", "inputOptions": False},
{"id": 3, "displayName": "I don't belong anywhere", "imageURL": "", "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': START_FEEDBACK,
})
assert_user_state_empty()
# Drag three items into the correct spot:
data = {"val": 0, "zone": TOP_ZONE_TITLE, "x_percent": "33%", "y_percent": "11%"}
self.call_handler('do_attempt', data)
data = {"val": 1, "zone": MIDDLE_ZONE_TITLE, "x_percent": "67%", "y_percent": "80%"}
self.call_handler('do_attempt', data)
data = {"val": 2, "zone": BOTTOM_ZONE_TITLE, "x_percent": "99%", "y_percent": "95%"}
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%', 'zone': TOP_ZONE_TITLE},
'1': {'x_percent': '67%', 'y_percent': '80%', 'zone': MIDDLE_ZONE_TITLE},
'2': {'x_percent': '99%', 'y_percent': '95%', 'zone': BOTTOM_ZONE_TITLE},
})
self.assertEqual(self.call_handler('get_user_state'), {
'items': {
'0': {'x_percent': '33%', 'y_percent': '11%', 'correct_input': True, 'zone': TOP_ZONE_TITLE},
'1': {'x_percent': '67%', 'y_percent': '80%', 'correct_input': True, 'zone': MIDDLE_ZONE_TITLE},
'2': {'x_percent': '99%', 'y_percent': '95%', 'correct_input': True, 'zone': BOTTOM_ZONE_TITLE},
},
'finished': True,
'overall_feedback': 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,
'problem_text': "Problem Drag & Drop",
'show_problem_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, "Problem 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 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
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)
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