Commit 6c3c75a3 by Xavier Antoviaque

Merge pull request #2 from FiloSottile/refactor

Iteration and wide refactor
parents 0290f4ff 260d00f3
...@@ -51,3 +51,5 @@ docs/_build/ ...@@ -51,3 +51,5 @@ docs/_build/
# PyBuilder # PyBuilder
target/ target/
workbench.db
# Drag and Drop XBlock v2
This XBlock implements a friendly drag-and-drop style question, where the student has to drag items on zones on a target image.
The editor is fully guided. Features include:
* custom target image
* free target zone positioning and sizing
* custom size items
* image items
* decoy items that don't have a zone
* feedback popups for both correct and incorrect attempts
* introductory and final feedback
It supports progressive grading and keeps progress across refreshes.
All checking and record keeping is done at server side.
## Installing
Just run
```
$ pip install -e .
```
from the XBlock folder and add `drag-and-drop-v2` to your Advanced Module List.
## Testing
1. In a virtualenv, run
```bash
$ (cd .../xblock-sdk/; pip install -r requirements.txt)
$ (cd .../xblock-drag-and-drop-v2/; pip install -r tests/requirements.txt)
```
2. In the xblock-sdk repository, create the following configuration file in `workbench/settings_drag_and_drop_v2.py`
```python
from settings import *
INSTALLED_APPS += ('drag_and_drop_v2',)
DATABASES['default']['NAME'] = 'workbench.db'
```
3. Run this to sync the database before starting the workbench (answering no to the superuser question is ok):
```bash
$ .../xblock-sdk/manage.py syncdb --settings=workbench.settings_drag_and_drop_v2
```
4. To run the tests, from the xblock-drag-and-drop-v2 repository root:
```bash
$ DJANGO_SETTINGS_MODULE="workbench.settings_drag_and_drop_v2" nosetests --rednose --verbose --with-cover --cover-package=drag_and_drop_v2
```
...@@ -4,12 +4,13 @@ ...@@ -4,12 +4,13 @@
# Imports ########################################################### # Imports ###########################################################
import logging import logging
import textwrap
import json import json
import webob import webob
import copy
import urllib
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Scope, String from xblock.fields import Scope, String, Dict, Float
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .utils import render_template, load_resource from .utils import render_template, load_resource
...@@ -36,40 +37,68 @@ class DragAndDropBlock(XBlock): ...@@ -36,40 +37,68 @@ class DragAndDropBlock(XBlock):
question_text = String( question_text = String(
display_name="Question text", display_name="Question text",
help="The question text that is displayed to the user", help="The question text that is displayed to the user",
scope=Scope.settings scope=Scope.settings,
default=""
)
weight = Float(
display_name="Weight",
help="This is the maximum score that the user receives when he/she successfully completes the problem",
scope=Scope.settings,
default=1
) )
data = String( data = Dict(
display_name="Drag and Drop", display_name="Drag and Drop",
help="JSON spec as generated by the builder", help="JSON spec as generated by the builder",
scope=Scope.content, scope=Scope.content,
default='{"feedback":{"start":"Intro","finish":"Final"},"items":[{"displayName":"A","zone":"Uno","id":0,"feedback":{"correct":"Si","incorrect":"No"},"size":{"width":"190px","height":"auto"},"backgroundImage":""},{"displayName":"B","zone":"none","id":1,"feedback":{"correct":"","incorrect":""},"size":{"width":"190px","height":"auto"},"backgroundImage":""}],"zones":[{"title":"Uno","id":"zone-1","active":true,"index":1,"width":200,"height":100,"x":0,"y":0},{"title":"Due","id":"zone-2","active":true,"index":2,"width":200,"height":100,"x":"300","y":"210"}],"targetImg":"https://i.imgur.com/PoI27ox.png"}' default={
# default=textwrap.dedent(""" 'feedback': {
# { 'start': '',
# feedback: {}, 'finish': ''
# items: [], },
# zones: [], 'items': [],
# targetImg: 'img/triangle.png' 'zones': [],
# } 'targetImg': None
# """) }
)
item_state = Dict(
help="How the student has interacted with the problem",
scope=Scope.user_state,
default={}
) )
has_score = True
def student_view(self, context): def student_view(self, context):
""" """
Player view, displayed to the student Player view, displayed to the student
""" """
max_score_string = '({0} Point{1} Possible)'.format(int(self.weight),
's' if self.weight > 1 else '') if self.weight else ''
js_templates = load_resource('/templates/html/js_templates.html')
context = { context = {
'js_templates': js_templates,
'title': self.display_name, 'title': self.display_name,
'question_text': self.question_text 'question_text': self.question_text,
'max_score_string': max_score_string
} }
fragment = Fragment() fragment = Fragment()
fragment.add_content(render_template('/templates/html/drag_and_drop.html', context)) fragment.add_content(render_template('/templates/html/drag_and_drop.html', context))
fragment.add_css(load_resource('public/css/drag_and_drop.css')) fragment.add_css_url(self.runtime.local_resource_url(self,
fragment.add_javascript(load_resource('public/js/vendor/jquery.html5-placeholder-shim.js')) 'public/css/vendor/jquery-ui-1.10.4.custom.min.css'))
fragment.add_javascript(load_resource('public/js/vendor/underscore1.6.0.js')) fragment.add_css_url(self.runtime.local_resource_url(self,
fragment.add_javascript(load_resource('public/js/drag_and_drop.js')) 'public/css/drag_and_drop.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self,
'public/js/vendor/jquery.html5-placeholder-shim.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self,
'public/js/vendor/handlebars-v1.1.2.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self,
'public/js/drag_and_drop.js'))
fragment.initialize_js('DragAndDropBlock') fragment.initialize_js('DragAndDropBlock')
...@@ -80,14 +109,25 @@ class DragAndDropBlock(XBlock): ...@@ -80,14 +109,25 @@ class DragAndDropBlock(XBlock):
Editing view in Studio Editing view in Studio
""" """
context = {} js_templates = load_resource('/templates/html/js_templates.html')
context = {
'js_templates': js_templates,
'self': self,
'data': urllib.quote(json.dumps(self.data)),
}
fragment = Fragment() fragment = Fragment()
fragment.add_content(render_template('/templates/html/drag_and_drop_edit.html', context)) fragment.add_content(render_template('/templates/html/drag_and_drop_edit.html', context))
fragment.add_css(load_resource('public/css/drag_and_drop_edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self,
fragment.add_javascript(load_resource('public/js/vendor/jquery.html5-placeholder-shim.js')) 'public/css/vendor/jquery-ui-1.10.4.custom.min.css'))
fragment.add_javascript(load_resource('public/js/vendor/underscore1.6.0.js')) fragment.add_css_url(self.runtime.local_resource_url(self,
fragment.add_javascript(load_resource('public/js/drag_and_drop_edit.js')) 'public/css/drag_and_drop_edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self,
'public/js/vendor/jquery.html5-placeholder-shim.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self,
'public/js/vendor/handlebars-v1.1.2.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self,
'public/js/drag_and_drop_edit.js'))
fragment.initialize_js('DragAndDropEditBlock') fragment.initialize_js('DragAndDropEditBlock')
...@@ -97,16 +137,8 @@ class DragAndDropBlock(XBlock): ...@@ -97,16 +137,8 @@ class DragAndDropBlock(XBlock):
def studio_submit(self, submissions, suffix=''): def studio_submit(self, submissions, suffix=''):
self.display_name = submissions['display_name'] self.display_name = submissions['display_name']
self.question_text = submissions['question_text'] self.question_text = submissions['question_text']
data = submissions['data'] self.weight = float(submissions['weight'])
self.data = submissions['data']
try:
json.loads(data)
self.data = data
except ValueError as e:
return {
'result': 'error',
'message': e.message
}
return { return {
'result': 'success', 'result': 'success',
...@@ -114,4 +146,57 @@ class DragAndDropBlock(XBlock): ...@@ -114,4 +146,57 @@ class DragAndDropBlock(XBlock):
@XBlock.handler @XBlock.handler
def get_data(self, request, suffix=''): def get_data(self, request, suffix=''):
return webob.response.Response(body=self.data) data = copy.deepcopy(self.data)
for item in data['items']:
# Strip answers
del item['feedback']
del item['zone']
tot_items = sum(1 for i in self.data['items'] if i['zone'] != 'none')
if len(self.item_state) != tot_items:
del data['feedback']['finish']
data['state'] = {
'items': self.item_state,
'finished': len(self.item_state) == tot_items
}
return webob.response.Response(body=json.dumps(data))
@XBlock.json_handler
def do_attempt(self, attempt, suffix=''):
item = next(i for i in self.data['items'] if i['id'] == attempt['val'])
tot_items = sum(1 for i in self.data['items'] if i['zone'] != 'none')
if item['zone'] == attempt['zone']:
self.item_state[item['id']] = (attempt['top'], attempt['left'])
if len(self.item_state) == tot_items:
final_feedback = self.data['feedback']['finish']
else:
final_feedback = None
try:
self.runtime.publish(self, 'grade', {
'value': len(self.item_state) / float(tot_items) * self.weight,
'max_value': self.weight,
})
except NotImplementedError:
# Note, this publish method is unimplemented in Studio runtimes,
# so we have to figure that we're running in Studio for now
pass
return {
'correct': True,
'finished': len(self.item_state) == tot_items,
'final_feedback': final_feedback,
'feedback': item['feedback']['correct']
}
else:
return {
'correct': False,
'finished': len(self.item_state) == tot_items,
'final_feedback': None,
'feedback': item['feedback']['incorrect']
}
...@@ -52,12 +52,16 @@ ...@@ -52,12 +52,16 @@
position: relative; position: relative;
float: left; float: left;
display: inline; display: inline;
z-index: 100; /* Some versions of the drag and drop library try to fiddle with this */
z-index: 10 !important;
margin-bottom: 5px; margin-bottom: 5px;
padding: 10px; padding: 10px;
} }
.xblock--drag-and-drop .option.hover { background: #ccc; } .xblock--drag-and-drop .drag-container .items .option img {
max-width: 100%;
}
.xblock--drag-and-drop .option.fade { opacity: 0.6; } .xblock--drag-and-drop .option.fade { opacity: 0.6; }
...@@ -74,19 +78,19 @@ ...@@ -74,19 +78,19 @@
} }
.xblock--drag-and-drop .target-img { .xblock--drag-and-drop .target-img {
background: url('../img/triangle.png') no-repeat;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.xblock--drag-and-drop .zone { .xblock--drag-and-drop .zone {
/*border: 1px solid #000;*/
position: absolute; position: absolute;
display: -webkit-box; display: -webkit-box;
display: -moz-box; display: -moz-box;
display: -ms-flexbox; display: -ms-flexbox;
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
/* Internet Explorer 10 */ /* Internet Explorer 10 */
-ms-flex-pack:center; -ms-flex-pack:center;
...@@ -117,25 +121,6 @@ ...@@ -117,25 +121,6 @@
margin-bottom: auto; margin-bottom: auto;
} }
.xblock--drag-and-drop .zone.one {
height: 75px;
width: 115px;
top: 130px;
left: 200px;
}
.xblock--drag-and-drop .zone.two {
height: 120px;
width: 200px;
top: 220px;
left: 157px;
}
.xblock--drag-and-drop .zone.three {
height: 120px;
width: 200px;
bottom: 30px;
left: 157px;
}
/*** IE9 alignment fix ***/ /*** IE9 alignment fix ***/
.lt-ie10 .xblock--drag-and-drop .zone { .lt-ie10 .xblock--drag-and-drop .zone {
display: table; display: table;
...@@ -155,5 +140,5 @@ ...@@ -155,5 +140,5 @@
} }
.no-close .ui-dialog-titlebar-close { .no-close .ui-dialog-titlebar-close {
display: none; display: none;
} }
...@@ -6,45 +6,12 @@ ...@@ -6,45 +6,12 @@
background: #fff; background: #fff;
} }
.xblock--drag-and-drop h1, .modal-window .editor-with-buttons.xblock--drag-and-drop {
.xblock--drag-and-drop h2, /* Fix Studio edito height */
.xblock--drag-and-drop h3, margin-bottom: 0;
.xblock--drag-and-drop h4, height: 380px;
.xblock--drag-and-drop h5,
.xblock--drag-and-drop h6,
.xblock--drag-and-drop p,
.xblock--drag-and-drop li,
.xblock--drag-and-drop a {
font-family: Arial;
}
.xblock--drag-and-drop h1 {
color: #adadad;
}
.xblock--drag-and-drop h2 {
color: #333;
margin: 0;
text-transform: uppercase;
} }
.xblock--drag-and-drop header p,
.xblock--drag-and-drop footer p {
color: #adadad;
line-height: 1.5em;
}
.xblock--drag-and-drop .small {
font-size: 0.6em;
}
.xblock--drag-and-drop .drag-container {
width: 760px;
background: #ebf0f2;
position: relative;
}
/** Draggable Items **/ /** Draggable Items **/
.xblock--drag-and-drop .items { .xblock--drag-and-drop .items {
width: 210px; width: 210px;
...@@ -57,21 +24,6 @@ ...@@ -57,21 +24,6 @@
list-style-type: none; list-style-type: none;
} }
.xblock--drag-and-drop .items .option {
width: 190px;
background: #2e83cd;
color: #fff;
position: relative;
float: left;
display: inline;
z-index: 100;
margin-bottom: 5px;
padding: 10px;
}
.xblock--drag-and-drop .option.hover { background: #ccc; }
.xblock--drag-and-drop .option.fade { opacity: 0.6; }
/*** Drop Target ***/ /*** Drop Target ***/
.xblock--drag-and-drop .target { .xblock--drag-and-drop .target {
...@@ -86,20 +38,19 @@ ...@@ -86,20 +38,19 @@
} }
.xblock--drag-and-drop .target-img { .xblock--drag-and-drop .target-img {
background: url(../img/triangle.png) no-repeat; background: url('../img/triangle.png') no-repeat;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.xblock--drag-and-drop .zone { .xblock--drag-and-drop .zone {
/*border: 1px solid #000;*/
position: absolute; position: absolute;
display: -webkit-box; display: -webkit-box;
display: -moz-box; display: -moz-box;
display: -ms-flexbox; display: -ms-flexbox;
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
/* Internet Explorer 10 */ /* Internet Explorer 10 */
-ms-flex-pack:center; -ms-flex-pack:center;
...@@ -130,25 +81,6 @@ ...@@ -130,25 +81,6 @@
margin-bottom: auto; margin-bottom: auto;
} }
.xblock--drag-and-drop .zone.one {
height: 75px;
width: 115px;
top: 130px;
left: 200px;
}
.xblock--drag-and-drop .zone.two {
height: 120px;
width: 200px;
top: 220px;
left: 157px;
}
.xblock--drag-and-drop .zone.three {
height: 120px;
width: 200px;
bottom: 30px;
left: 157px;
}
/*** IE9 alignment fix ***/ /*** IE9 alignment fix ***/
.lt-ie10 .xblock--drag-and-drop .zone { .lt-ie10 .xblock--drag-and-drop .zone {
display: table; display: table;
...@@ -160,26 +92,13 @@ ...@@ -160,26 +92,13 @@
text-align: center; text-align: center;
} }
/*** FEEDBACK ***/
.xblock--drag-and-drop .feedback {
width: 740px;
border-top: #ccc 1px solid;
margin: 20px 10px;
padding-top: 10px;
}
.xblock--drag-and-drop .feedback .message {
margin: 5px 0 0;
}
/** Builder **/ /** Builder **/
.xblock--drag-and-drop .hidden { .xblock--drag-and-drop .hidden {
display: none !important; display: none !important;
} }
.xblock--drag-and-drop .drag-builder { .xblock--drag-and-drop .drag-builder {
/* TODO */ height: 100%;
height: 375px;
overflow: scroll; overflow: scroll;
} }
...@@ -216,12 +135,6 @@ ...@@ -216,12 +135,6 @@
float: left; float: left;
} }
.xblock--drag-and-drop .drag-builder .continue {
position: absolute;
right: 0;
top: -5px;
}
.xblock--drag-and-drop .drag-builder .items { .xblock--drag-and-drop .drag-builder .items {
width: calc(100% - 515px); width: calc(100% - 515px);
margin: 10px 0 0 0; margin: 10px 0 0 0;
...@@ -231,6 +144,10 @@ ...@@ -231,6 +144,10 @@
margin-left: 0; margin-left: 0;
} }
.xblock--drag-and-drop .drag-builder .target-image-form input {
width: 50%;
}
.xblock--drag-and-drop .zones-form .zone-row label { .xblock--drag-and-drop .zones-form .zone-row label {
display: inline-block; display: inline-block;
width: 18%; width: 18%;
...@@ -257,8 +174,6 @@ ...@@ -257,8 +174,6 @@
} }
.xblock--drag-and-drop .drag-builder .zone { .xblock--drag-and-drop .drag-builder .zone {
width: 200px;
height: 100px;
border: 1px dotted #666; border: 1px dotted #666;
} }
...@@ -389,8 +304,8 @@ ...@@ -389,8 +304,8 @@
top: 2px; top: 2px;
left: 6px; left: 6px;
-webkit-transform: rotate(45deg); -webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg); -ms-transform: rotate(45deg);
transform: rotate(45deg); transform: rotate(45deg);
} }
.xblock--drag-and-drop .icon.remove:after { .xblock--drag-and-drop .icon.remove:after {
...@@ -404,8 +319,8 @@ ...@@ -404,8 +319,8 @@
top: 6px; top: 6px;
left: 0; left: 0;
-webkit-transform: rotate(45deg); -webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg); -ms-transform: rotate(45deg);
transform: rotate(45deg); transform: rotate(45deg);
} }
.xblock--drag-and-drop .remove-item .icon.remove { .xblock--drag-and-drop .remove-item .icon.remove {
......
<section class="xblock--drag-and-drop"> <section class="xblock--drag-and-drop">
<link rel="stylesheet" href="https://code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css"> {{ js_templates|safe }}
<h2 class="problem-header"> <h2 class="problem-header">
{{ title }} {{ title }}
</h2> </h2>
<div class="problem-progress">(1 point possible)</div> <div class="problem-progress">{{ max_score_string }}</div>
<section class="problem" role="application"> <section class="problem" role="application">
<p> <p>
......
{% load i18n %} {% load i18n %}
<div class="xblock--drag-and-drop editor-with-buttons"> <div class="xblock--drag-and-drop editor-with-buttons">
<link rel="stylesheet" href="https://code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css"> {{ js_templates|safe }}
<section class="drag-builder"> <script type="text/javascript">
var DragAndDropV2BlockPreviousData = JSON.parse(decodeURIComponent('{{ data|safe }}'));
</script>
<section class="drag-builder">
<div class="tab feedback-tab"> <div class="tab feedback-tab">
<p class="tab-content">
Note: don't edit the question if students already answered it! Delete it and create a new one.
</p>
<section class="tab-content"> <section class="tab-content">
<form class="feedback-form"> <form class="feedback-form">
<h3>Question title</h3> <h3>Question title</h3>
<input class="display-name" /> <input class="display-name" value="{{ self.display_name }}" />
<h3>Maximum score</h3>
<input class="weight" value="1" value="{{ self.weight }}"/>
<h3>Question text</h3> <h3>Question text</h3>
<textarea class="question-text"></textarea> <textarea class="question-text">{{ self.question_text }}</textarea>
<h3>Introduction Feedback</h3> <h3>Introduction Feedback</h3>
<textarea class="intro-feedback"></textarea> <textarea class="intro-feedback">{{ self.data.feedback.start }}</textarea>
<h3>Final Feedback</h3> <h3>Final Feedback</h3>
<textarea class="final-feedback"></textarea> <textarea class="final-feedback">{{ self.data.feedback.finish }}</textarea>
</form> </form>
</section> </section>
<!-- <footer class="tab-footer">
<button class="btn continue goto-zones">Continue</button>
</footer> -->
</div> </div>
<div class="tab zones-tab hidden"> <div class="tab zones-tab hidden">
...@@ -31,6 +38,11 @@ ...@@ -31,6 +38,11 @@
<h3>Zone Positions</h3> <h3>Zone Positions</h3>
</header> </header>
<section class="tab-content"> <section class="tab-content">
<section class="tab-content target-image-form">
<label>New background URL:</label>
<input type="text">
<button class="btn">Change background</button>
</section>
<div class="items"> <div class="items">
<form class="zones-form"></form> <form class="zones-form"></form>
<a href="#" class="add-zone add-element"><div class="icon add"></div>Add a zone</a> <a href="#" class="add-zone add-element"><div class="icon add"></div>Add a zone</a>
...@@ -39,9 +51,6 @@ ...@@ -39,9 +51,6 @@
<div class="target-img"></div> <div class="target-img"></div>
</div> </div>
</section> </section>
<!-- <footer class="tab-footer">
<button class="btn continue goto-items">Continue</button>
</footer> -->
</div> </div>
<div class="tab items-tab hidden"> <div class="tab items-tab hidden">
...@@ -53,7 +62,6 @@ ...@@ -53,7 +62,6 @@
</section> </section>
<footer class="tab-footer"> <footer class="tab-footer">
<a href="#" class="add-item add-element"><div class="icon add"></div>Add an item</a> <a href="#" class="add-item add-element"><div class="icon add"></div>Add an item</a>
<!-- <button class="btn continue goto-exercise">Finish</button> -->
</footer> </footer>
</div> </div>
......
<script id="item-tpl" type="text/html">
<li class="option" data-value="{{ id }}"
style="width: {{ size.width }}; height: {{ size.height }}">
{{ displayName }}
</li>
</script>
<script id="image-item-tpl" type="text/html">
<li class="option" data-value="{{ id }}"
style="width: {{ size.width }}; height: {{ size.height }}">
<img src="{{ backgroundImage }}" />
</li>
</script>
<script id="zone-element-tpl" type="text/html">
<div id="{{ id }}" class="zone" data-zone="{{ title }}" style="
top:{{ y }}px;
left:{{ x }}px;
width:{{ width }}px;
height:{{ height }}px;">
<p>{{ title }}</p>
</div>
</script>
<script id="zone-input-tpl" type="text/html">
<div class="zone-row {{ id }}">
<label>Text</label>
<input type="text" class="title" value="{{ title }}" />
<a href="#" class="remove-zone hidden">
<div class="icon remove"></div>
</a>
<div class="layout">
<label>width</label>
<input type="text" class="size width" value="{{ width }}" />
<label>height</label>
<input type="text" class="size height" value="{{ height }}" />
<br />
<label>x</label>
<input type="text" class="coord x" value="{{ x }}" />
<label>y</label>
<input type="text" 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>Text</label>
<input type="text" class="item-text" value="{{ displayName }}"/>
<label>Zone</label>
<select class="zone-select">{{ dropdown }}</select>
<a href="#" class="remove-item hidden">
<div class="icon remove"></div>
</a>
</div>
<div class="row">
<label>Background image URL (alternative to the text)</label>
<textarea class="background-image">{{ backgroundImage }}</textarea>
</div>
<div class="row">
<label>Success Feedback</label>
<textarea class="success-feedback">{{ feedback.correct }}</textarea>
</div>
<div class="row">
<label>Error Feedback</label>
<textarea class="error-feedback">{{ feedback.incorrect }}</textarea>
</div>
<div class="row">
<label>Width (px - 0 for auto)</label>
<input type="text" class="item-width" value="{{ width }}"></input>
<label>Height (px - 0 for auto)</label>
<input type="text" class="item-height" value="{{ height }}"></input>
</div>
</div>
</script>
mock
nose
coverage
rednose
-e .
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone A",
"height": 100,
"y": "200",
"x": "100",
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone B",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "A",
"feedback": {
"incorrect": "No A",
"correct": "Yes A"
},
"zone": "Zone A",
"backgroundImage": "",
"id": 0,
"size": {
"width": "190px",
"height": "auto"
}
},
{
"displayName": "B",
"feedback": {
"incorrect": "No B",
"correct": "Yes B"
},
"zone": "Zone B",
"backgroundImage": "",
"id": 1,
"size": {
"width": "190px",
"height": "auto"
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"backgroundImage": "",
"id": 2,
"size": {
"width": "100px",
"height": "100px"
}
},
{
"displayName": "",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"backgroundImage": "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"
}
import logging
import json
import re
import datetime
import time
import json
from webob import Request
from mock import Mock, patch
from workbench.runtime import WorkbenchRuntime
from xblock.runtime import KvsFieldData, DictKeyValueStore
from nose.tools import (
assert_equals, assert_true, assert_in,
assert_regexp_matches
)
import drag_and_drop_v2
# Silence too verbose Django logging
logging.disable(logging.DEBUG)
def make_request(body):
request = Request.blank('/')
request.body = body.encode('utf-8')
return request
def make_block():
runtime = WorkbenchRuntime()
key_store = DictKeyValueStore()
db_model = KvsFieldData(key_store)
return drag_and_drop_v2.DragAndDropBlock(runtime, db_model, Mock())
def test_templates_contents():
block = make_block()
block.display_name = "Test Drag & Drop"
block.question_text = "Question Drag & Drop"
block.weight = 5
student_fragment = block.render('student_view', Mock())
assert_in('<section class="xblock--drag-and-drop">',
student_fragment.content)
assert_in('{{ value }}', student_fragment.content)
assert_in("Test Drag &amp; Drop", student_fragment.content)
assert_in("Question Drag &amp; Drop", student_fragment.content)
assert_in("(5 Points Possible)", student_fragment.content)
studio_fragment = block.render('studio_view', Mock())
assert_in('<div class="xblock--drag-and-drop editor-with-buttons">',
studio_fragment.content)
assert_in('{{ value }}', studio_fragment.content)
def test_studio_submit():
block = make_block()
body = json.dumps({
'display_name': "Test Drag & Drop",
'question_text': "Question Drag & Drop",
'weight': '5',
'data': {
'foo': 1
}
})
res = block.handle('studio_submit', make_request(body))
assert_equals(json.loads(res.body), {'result': 'success'})
assert_equals(block.display_name, "Test Drag & Drop")
assert_equals(block.question_text, "Question Drag & Drop")
assert_equals(block.weight, 5)
assert_equals(block.data, {'foo': 1})
def test_ajax():
assert_equals.__self__.maxDiff = None
block = make_block()
with open('tests/test_data.json') as f:
block.data = json.load(f)
with open('tests/test_get_data.json') as f:
get_data = json.loads(block.handle('get_data', Mock()).body)
assert_equals(json.load(f), get_data)
# Wrong with feedback
data = json.dumps({"val":0,"zone":"Zone B","top":"31px","left":"216px"})
res = json.loads(block.handle('do_attempt', make_request(data)).body)
assert_equals(res, {
"final_feedback": None,
"finished": False,
"correct": False,
"feedback": "No A"
})
with open('tests/test_get_data.json') as f:
get_data = json.loads(block.handle('get_data', Mock()).body)
assert_equals(json.load(f), get_data)
# Wrong without feedback
data = json.dumps({"val":2,"zone":"Zone B","top":"42px","left":"100px"})
res = json.loads(block.handle('do_attempt', make_request(data)).body)
assert_equals(res, {
"final_feedback": None,
"finished": False,
"correct": False,
"feedback": ""
})
with open('tests/test_get_data.json') as f:
get_data = json.loads(block.handle('get_data', Mock()).body)
assert_equals(json.load(f), get_data)
# Correct
data = json.dumps({"val":0,"zone":"Zone A","top":"11px","left":"111px"})
res = json.loads(block.handle('do_attempt', make_request(data)).body)
assert_equals(res, {
"final_feedback": None,
"finished": False,
"correct": True,
"feedback": "Yes A"
})
with open('tests/test_get_data.json') as f:
expected = json.load(f)
expected["state"] = {
"items": {
"0": ["11px", "111px"]
},
"finished": False
}
get_data = json.loads(block.handle('get_data', Mock()).body)
assert_equals(expected, get_data)
# Final
data = json.dumps({"val":1,"zone":"Zone B","top":"22px","left":"222px"})
res = json.loads(block.handle('do_attempt', make_request(data)).body)
assert_equals(res, {
"final_feedback": "Final Feed",
"finished": True,
"correct": True,
"feedback": "Yes B"
})
with open('tests/test_get_data.json') as f:
expected = json.load(f)
expected["state"] = {
"items": {
"0": ["11px", "111px"],
"1": ["22px", "222px"]
},
"finished": True
}
expected["feedback"]["finish"] = "Final Feed"
get_data = json.loads(block.handle('get_data', Mock()).body)
assert_equals(expected, get_data)
{
"zones": [
{
"index": 1,
"title": "Zone A",
"id": "zone-1",
"height": 100,
"y": "200",
"x": "100",
"width": 200
},
{
"index": 2,
"title": "Zone B",
"id": "zone-2",
"height": 100,
"y": 0,
"x": 0,
"width": 200
}
],
"items": [
{
"displayName": "A",
"backgroundImage": "",
"id": 0,
"size": {
"width": "190px",
"height": "auto"
}
},
{
"displayName": "B",
"backgroundImage": "",
"id": 1,
"size": {
"width": "190px",
"height": "auto"
}
},
{
"displayName": "X",
"backgroundImage": "",
"id": 2,
"size": {
"width": "100px",
"height": "100px"
}
},
{
"displayName": "",
"backgroundImage": "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": false
},
"feedback": {
"start": "Intro Feed"
},
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png"
}
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