Commit 1a119b6e by Tim Krones

Merge pull request #48 from open-craft/add-theming

Make DnDv2 themable
parents b9708906 ad9887f1
......@@ -36,9 +36,44 @@ Install the requirements into the Python virtual environment of your
root folder:
```bash
$ pip install -e .
$ pip install -r requirements.txt
```
Theming
-------
The Drag and Drop XBlock ships with an alternate theme called "Apros"
that you can enable by adding the following entry to `XBLOCK_SETTINGS`
in `lms.env.json`:
```json
"drag-and-drop-v2": {
"theme": {
"package": "drag_and_drop_v2",
"locations": ["public/themes/apros.css"]
}
}
```
You can use the same approach to apply a custom theme:
`"package"` can refer to any Python package in your virtualenv, which
means you can develop and maintain your own theme in a separate
package. There is no need to fork or modify this repository in any way
to customize the look and feel of your Drag and Drop exercises.
`"locations"` is a list of relative paths pointing to CSS files
belonging to your theme. While the XBlock loads, files will be added
to it in the order that they appear in this list. (This means that if
there are rules with identical selectors spread out over different
files, rules in files that appear later in the list will take
precedence over those that appear earlier.)
Finally, note that the default (unthemed) appearance of the Drag and
Drop XBlock has been optimized for accessibility, so its use is
encouraged -- especially for courses targeting large and/or
potentially diverse audiences.
Enabling in Studio
------------------
......@@ -105,15 +140,27 @@ You can define an arbitrary number of drag items.
Testing
-------
Inside a fresh virtualenv, run
Inside a fresh virtualenv, `cd` into the root folder of this repository
(`xblock-drag-and-drop-v2`) and run
```bash
$ cd .../xblock-drag-and-drop-v2/
$ sh install_test_deps.sh
```
To run the tests, from the xblock-drag-and-drop-v2 repository root:
You can then run the entire test suite via
```bash
$ python run_tests.py
```
To only run the unit test suite, do
```bash
$ python run_tests.py tests/unit/
```
Similarly, you can run the integration test suite via
```bash
$ python run_tests.py tests/integration/
```
......@@ -11,14 +11,22 @@ import urllib
from xblock.core import XBlock
from xblock.fields import Scope, String, Dict, Float, Boolean
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin
from .utils import _, render_template, load_resource # pylint: disable=unused-import
from .utils import _ # pylint: disable=unused-import
from .default_data import DEFAULT_DATA
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
class DragAndDropBlock(XBlock):
@XBlock.wants('settings')
class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
XBlock providing a Drag and Drop question
"""
......@@ -96,6 +104,7 @@ class DragAndDropBlock(XBlock):
default=False,
)
block_settings_key = 'drag-and-drop-v2'
has_score = True
def _(self, text):
......@@ -108,7 +117,7 @@ class DragAndDropBlock(XBlock):
"""
fragment = Fragment()
fragment.add_content(render_template('/templates/html/drag_and_drop.html'))
fragment.add_content(loader.render_template('/templates/html/drag_and_drop.html'))
css_urls = (
'public/css/vendor/jquery-ui-1.10.4.custom.min.css',
'public/css/drag_and_drop.css'
......@@ -125,6 +134,8 @@ class DragAndDropBlock(XBlock):
for js_url in js_urls:
fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url))
self.include_theme_files(fragment)
fragment.initialize_js('DragAndDropBlock', self.get_configuration())
return fragment
......@@ -166,7 +177,7 @@ class DragAndDropBlock(XBlock):
Editing view in Studio
"""
js_templates = load_resource('/templates/html/js_templates.html')
js_templates = loader.load_unicode('/templates/html/js_templates.html')
help_texts = {
field_name: self._(field.help)
for field_name, field in self.fields.viewitems() if hasattr(field, "help")
......@@ -179,7 +190,7 @@ class DragAndDropBlock(XBlock):
}
fragment = Fragment()
fragment.add_content(render_template('/templates/html/drag_and_drop_edit.html', context))
fragment.add_content(loader.render_template('/templates/html/drag_and_drop_edit.html', context))
css_urls = (
'public/css/vendor/jquery-ui-1.10.4.custom.min.css',
......
......@@ -36,14 +36,11 @@
.xblock--drag-and-drop .drag-container {
width: auto;
padding: 1%;
background: #ebf0f2;
background-color: #ebf0f2;
}
/*.xblock--drag-and-drop .clear {
clear: both;
}*/
/*** DRAGGABLE ITEMS ***/
/** Draggable Items **/
.xblock--drag-and-drop .item-bank {
display: -ms-flexbox;
display: flex;
......@@ -105,6 +102,8 @@
.xblock--drag-and-drop .drag-container .option .item-content {
display: inline-block;
width: 100%; /* Make sure size of content never exceeds size of item */
/* (this can happen if item displays image whose width exceeds computed max-width of item) */
}
/* Placed option */
......@@ -169,12 +168,12 @@
}
.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input {
background: #ceffce;
background-color: #ceffce;
color: #087108;
}
.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input {
background: #ffcece;
background-color: #ffcece;
color: #ad0d0d;
}
......@@ -182,7 +181,8 @@
opacity: 0.5;
}
/*** Drop Target ***/
/*** DROP TARGET ***/
.xblock--drag-and-drop .target {
display: table;
/* 'display: table' makes this have the smallest size that fits the .target-img
......@@ -192,7 +192,7 @@
height: auto;
position: relative;
margin-top: 1%;
background: #fff;
background-color: #fff;
}
.xblock--drag-and-drop .target-img-wrapper {
......@@ -265,7 +265,9 @@
}
/*** FEEDBACK ***/
.xblock--drag-and-drop .feedback {
margin-bottom: 1%;
border-top: solid 1px #bdbdbd;
}
......@@ -275,7 +277,7 @@
top: 5%;
right: 5%;
border: 1px solid #fff;
background: none repeat scroll 0 0 rgba(0, 0, 0, 0.8);
background-color: rgba(0, 0, 0, 0.8);
width: 500px;
max-width: 90%;
min-height: 50px;
......@@ -303,6 +305,8 @@
font-size: 18pt;
}
/*** KEYBOARD HELP ***/
.xblock--drag-and-drop .keyboard-help {
margin-top: 3px;
margin-bottom: 6px;
......@@ -324,7 +328,7 @@
left: 0;
width: 100%;
height: 100%;
background: #000;
background-color: #000;
opacity: 0.5;
z-index: 1500;
}
......@@ -348,7 +352,7 @@
.xblock--drag-and-drop .modal-content {
border-radius: 5px;
background: white;
background-color: #ffffff;
margin-bottom: 5px;
padding: 5px;
}
......@@ -378,6 +382,6 @@
padding: 0;
position: absolute;
width: 1px;
background: #ffffff;
background-color: #ffffff;
color: #000000;
}
......@@ -68,7 +68,7 @@
.xblock--drag-and-drop--editor .tab {
width: 100%;
background: #eee;
background-color: #eee;
padding: 3px 0;
position: relative;
}
......@@ -177,7 +177,7 @@
}
.xblock--drag-and-drop--editor .items-form .item {
background: #8fcaec;
background-color: #8fcaec;
padding: 10px 0 1px;
margin: 15px 0;
}
......@@ -219,7 +219,7 @@
/** Buttons **/
.xblock--drag-and-drop--editor .btn {
background: #1d5280;
background-color: #1d5280;
color: #fff;
border: 1px solid #156ab4;
border-radius: 6px;
......@@ -257,7 +257,7 @@
width: 14px;
height: 14px;
border-radius: 7px;
background: #1d5280;
background-color: #1d5280;
position: relative;
float: left;
margin: 0 5px 0 0;
......@@ -274,7 +274,7 @@
content: '';
height: 10px;
width: 2px;
background: #fff;
background-color: #fff;
position: relative;
display: inline;
float: left;
......@@ -286,7 +286,7 @@
content: '';
height: 2px;
width: 10px;
background: #fff;
background-color: #fff;
position: relative;
display: inline;
float: left;
......@@ -298,7 +298,7 @@
content: '';
height: 10px;
width: 2px;
background: #fff;
background-color: #fff;
position: relative;
display: inline;
float: left;
......@@ -313,7 +313,7 @@
content: '';
height: 2px;
width: 10px;
background: #fff;
background-color: #fff;
position: relative;
display: inline;
float: left;
......@@ -325,10 +325,10 @@
}
.xblock--drag-and-drop--editor .remove-item .icon.remove {
background: #fff;
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: #1d5280;
background-color: #1d5280;
}
......@@ -303,7 +303,7 @@ function DragAndDropBlock(runtime, element, configuration) {
// Make zone accept items that are dropped using the mouse
$root.find('.zone').droppable({
accept: '.xblock--drag-and-drop .item-bank .option',
accept: '.item-bank .option',
tolerance: 'pointer',
drop: function(evt, ui) {
var $zone = $(this);
......@@ -331,9 +331,9 @@ function DragAndDropBlock(runtime, element, configuration) {
// Make item draggable using the mouse
try {
$item.draggable({
containment: $root.find('.xblock--drag-and-drop .drag-container'),
containment: $root.find('.drag-container'),
cursor: 'move',
stack: $root.find('.xblock--drag-and-drop .item-bank .option'),
stack: $root.find('.item-bank .option'),
revert: 'invalid',
revertDuration: 150,
start: function(evt, ui) {
......
......@@ -199,7 +199,7 @@
var items_placed = $.grep(ctx.items, is_item_placed);
var items_in_bank = $.grep(ctx.items, is_item_placed, true);
return (
h('section.xblock--drag-and-drop', [
h('section.themed-xblock.xblock--drag-and-drop', [
problemHeader,
h('section.problem', {role: 'application'}, [
questionHeader,
......
.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;
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 .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 .keyboard-help-button,
.themed-xblock.xblock--drag-and-drop .reset-button {
cursor: pointer;
color: #3384ca;
}
{% load i18n %}
<section class="xblock--drag-and-drop">
<section class="themed-xblock xblock--drag-and-drop">
<i class="fa fa-spin fa-spinner initial-load-spinner"></i>{% trans "Loading drag and drop exercise." %}
</section>
......@@ -123,7 +123,7 @@
</div>
<div class="row">
<label for="item-{{id}}-numerical-margin">
{{i18n "Margin ± (when a numerical value is required, values entered by students must not differ from the expected value by more than this margin; default is zero)"}}
{{i18n "Margin +/- (when a numerical value is required, values entered by students must not differ from the expected value by more than this margin; default is zero)"}}
</label>
<input type="number"
step="0.1"
......
# -*- coding: utf-8 -*-
#
# Imports ###########################################################
import pkg_resources
from django.template import Context, Template
# Functions #########################################################
# Make '_' a no-op so we can scrape strings
def _(text):
return text
def load_resource(resource_path):
"""
Gets the content of a resource
"""
resource_content = pkg_resources.resource_string(__name__, resource_path)
return resource_content
def render_template(template_path, context=None):
"""
Evaluate a template by resource path, applying the provided context
"""
if context is None:
context = {}
template_str = load_resource(template_path)
template = Template(template_str)
return template.render(Context(context))
......@@ -3,4 +3,4 @@
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 -
python setup.py develop
pip install -r requirements.txt
git+https://github.com/edx/xblock-utils.git@v1.0.0#egg=xblock-utils==v1.0.0
-e .
......@@ -31,7 +31,6 @@ setup(
'xblock-utils',
'ddt'
],
dependency_links = ['http://github.com/edx/xblock-utils/tarball/master#egg=xblock-utils'],
entry_points={
'xblock.v1': 'drag-and-drop-v2 = drag_and_drop_v2:DragAndDropBlock',
},
......
# Imports ###########################################################
from xml.sax.saxutils import escape
from selenium.webdriver.support.ui import WebDriverWait
from ..utils import load_resource
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.xblock--drag-and-drop'
default_css_selector = 'section.themed-xblock.xblock--drag-and-drop'
module_name = __name__
_additional_escapes = {
......@@ -41,7 +48,7 @@ class BaseIntegrationTest(SeleniumBaseTest):
)
def _get_custom_scenario_xml(self, filename):
data = load_resource(filename)
data = loader.load_unicode(filename)
return "<vertical_demo><drag-and-drop-v2 data='{data}'/></vertical_demo>".format(
data=escape(data, self._additional_escapes)
)
......
......@@ -8,7 +8,7 @@ class TestCustomDataDragAndDropRendering(BaseIntegrationTest):
def setUp(self):
super(TestCustomDataDragAndDropRendering, self).setUp()
scenario_xml = self._get_custom_scenario_xml("integration/data/test_html_data.json")
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)
......
# Imports ###########################################################
from ddt import ddt, data
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
from xblockutils.resources import ResourceLoader
from drag_and_drop_v2.default_data import (
TOP_ZONE_TITLE, MIDDLE_ZONE_TITLE, BOTTOM_ZONE_TITLE,
ITEM_CORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK, ITEM_NO_ZONE_FEEDBACK,
START_FEEDBACK, FINISH_FEEDBACK
)
from .test_base import BaseIntegrationTest
from ..utils import load_resource
# Globals ###########################################################
loader = ResourceLoader(__name__)
ZONES_MAP = {
0: TOP_ZONE_TITLE,
1: MIDDLE_ZONE_TITLE,
......@@ -20,6 +27,8 @@ ZONES_MAP = {
}
# Classes ###########################################################
class ItemDefinition(object):
def __init__(self, item_id, zone_id, feedback_positive, feedback_negative, input_value=None):
self.feedback_negative = feedback_negative
......@@ -121,6 +130,7 @@ class InteractionTestBase(object):
def assert_placed_item(self, item_value, zone_id):
item = self._get_placed_item_by_value(item_value)
self.wait_until_visible(item)
item_content = item.find_element_by_css_selector('.item-content')
item_description = item.find_element_by_css_selector('.sr')
item_description_id = 'item-{}-description'.format(item_value)
......@@ -134,6 +144,7 @@ class InteractionTestBase(object):
def assert_reverted_item(self, item_value):
item = self._get_item_by_value(item_value)
self.wait_until_visible(item)
item_content = item.find_element_by_css_selector('.item-content')
self.assertEqual(item.get_attribute('class'), 'option ui-draggable')
......@@ -377,7 +388,7 @@ class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
}
def _get_scenario_xml(self):
return self._get_custom_scenario_xml("integration/data/test_data.json")
return self._get_custom_scenario_xml("data/test_data.json")
class CustomHtmlDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
......@@ -395,15 +406,15 @@ class CustomHtmlDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
}
def _get_scenario_xml(self):
return self._get_custom_scenario_xml("integration/data/test_html_data.json")
return self._get_custom_scenario_xml("data/test_html_data.json")
class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
PAGE_TITLE = 'Drag and Drop v2 Multiple Blocks'
PAGE_ID = 'drag_and_drop_v2_multi'
BLOCK1_DATA_FILE = "integration/data/test_data.json"
BLOCK2_DATA_FILE = "integration/data/test_data_other.json"
BLOCK1_DATA_FILE = "data/test_data.json"
BLOCK2_DATA_FILE = "data/test_data_other.json"
item_maps = {
'block1': {
......@@ -430,7 +441,7 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
def _get_scenario_xml(self):
blocks_xml = "\n".join([
"<drag-and-drop-v2 data='{data}'/>".format(data=load_resource(filename))
"<drag-and-drop-v2 data='{data}'/>".format(data=loader.load_unicode(filename))
for filename in (self.BLOCK1_DATA_FILE, self.BLOCK2_DATA_FILE)
])
......
# Imports ###########################################################
from ddt import ddt, unpack, data
from selenium.common.exceptions import NoSuchElementException
from xblockutils.resources import ResourceLoader
from drag_and_drop_v2.default_data import START_FEEDBACK
from ..utils import load_resource
from .test_base import BaseIntegrationTest
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
class Colors(object):
WHITE = 'rgb(255, 255, 255)'
BLUE = 'rgb(29, 82, 128)'
......@@ -36,7 +46,7 @@ class TestDragAndDropRender(BaseIntegrationTest):
SIDES = ['Top', 'Bottom', 'Left', 'Right']
def load_scenario(self, item_background_color="", item_text_color="", zone_labels=False, zone_borders=False):
exercise_data = load_resource("integration/data/test_data_a11y.json")
exercise_data = loader.load_unicode("data/test_data_a11y.json")
exercise_data = exercise_data.replace('{display_labels_value}', 'true' if zone_labels else 'false')
exercise_data = exercise_data.replace('{display_borders_value}', 'true' if zone_borders else 'false')
scenario_xml = """
......
# Imports ###########################################################
import json
import unittest
from ..utils import (
make_block,
load_resource,
TestCaseMixin,
)
from xblockutils.resources import ResourceLoader
from ..utils import make_block, TestCaseMixin
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
class BaseDragAndDropAjaxFixture(TestCaseMixin):
ZONE_1 = None
......@@ -32,15 +39,15 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
@classmethod
def initial_data(cls):
return json.loads(load_resource('unit/data/{}/data.json'.format(cls.FOLDER)))
return json.loads(loader.load_unicode('data/{}/data.json'.format(cls.FOLDER)))
@classmethod
def initial_settings(cls):
return json.loads(load_resource('unit/data/{}/settings.json'.format(cls.FOLDER)))
return json.loads(loader.load_unicode('data/{}/settings.json'.format(cls.FOLDER)))
@classmethod
def expected_configuration(cls):
return json.loads(load_resource('unit/data/{}/config_out.json'.format(cls.FOLDER)))
return json.loads(loader.load_unicode('data/{}/config_out.json'.format(cls.FOLDER)))
@classmethod
def initial_feedback(cls):
......
......@@ -17,7 +17,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
def test_template_contents(self):
context = {}
student_fragment = self.block.runtime.render(self.block, 'student_view', context)
self.assertIn('<section class="xblock--drag-and-drop">', student_fragment.content)
self.assertIn('<section class="themed-xblock xblock--drag-and-drop">', student_fragment.content)
self.assertIn('Loading drag and drop exercise.', student_fragment.content)
def test_get_configuration(self):
......
import json
import pkg_resources
import re
from mock import patch
......@@ -32,14 +31,6 @@ def make_block():
return drag_and_drop_v2.DragAndDropBlock(runtime, field_data, scope_ids=scope_ids)
def load_resource(resource_path):
"""
Gets the content of a resource
"""
resource_content = pkg_resources.resource_string(__name__, resource_path)
return unicode(resource_content)
class TestCaseMixin(object):
""" Helpful mixins for unittest TestCase subclasses """
maxDiff = None
......
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