Commit dd914278 by Matjaz Gregoric Committed by GitHub

Merge pull request #136 from edx-solutions/mtyaka/autozones

[MCKIN-5776] Support for dynamic generation of target zones and image.
parents dac6ba40 01034db2
...@@ -335,12 +335,12 @@ class DragAndDropBlock( ...@@ -335,12 +335,12 @@ class DragAndDropBlock(
""" """
js_templates = loader.load_unicode('/templates/html/js_templates.html') js_templates = loader.load_unicode('/templates/html/js_templates.html')
# Get a 'html_id' string that is unique for this block. # Get an 'id_suffix' string that is unique for this block.
# We append it to HTML element ID attributes to ensure multiple instances of the DnDv2 block # We append it to HTML element ID attributes to ensure multiple instances of the DnDv2 block
# on the same page don't share the same ID value. # on the same page don't share the same ID value.
# We avoid using ID attributes in preference to classes, but sometimes we still need IDs to # We avoid using ID attributes in preference to classes, but sometimes we still need IDs to
# connect 'for' and 'aria-describedby' attributes to the associated elements. # connect 'for' and 'aria-describedby' attributes to the associated elements.
id_suffix = self.location.html_id() # pylint: disable=no-member id_suffix = self._get_block_id()
js_templates = js_templates.replace('{{id_suffix}}', id_suffix) js_templates = js_templates.replace('{{id_suffix}}', id_suffix)
context = { context = {
'js_templates': js_templates, 'js_templates': js_templates,
...@@ -407,6 +407,18 @@ class DragAndDropBlock( ...@@ -407,6 +407,18 @@ class DragAndDropBlock(
'result': 'success', 'result': 'success',
} }
def _get_block_id(self):
"""
Return unique ID of this block. Useful for HTML ID attributes.
Works both in LMS/Studio and workbench runtimes:
- In LMS/Studio, use the location.html_id method.
- In the workbench, use the usage_id.
"""
if hasattr(self, 'location'):
return self.location.html_id() # pylint: disable=no-member
else:
return unicode(self.scope_ids.usage_id)
@staticmethod @staticmethod
def _get_max_items_per_zone(submissions): def _get_max_items_per_zone(submissions):
""" """
......
...@@ -286,13 +286,20 @@ ...@@ -286,13 +286,20 @@
} }
.xblock--drag-and-drop .drag-container .target .zone p { .xblock--drag-and-drop .drag-container .target .zone p {
width: 100%;
font-family: Arial; font-family: Arial;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
padding: 10px;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
text-align: center; text-align: center;
margin-top: auto;
margin-bottom: auto;
} }
/*** FEEDBACK ***/ /*** FEEDBACK ***/
......
...@@ -93,7 +93,16 @@ ...@@ -93,7 +93,16 @@
margin: 10px 0 0 0; margin: 10px 0 0 0;
} }
.xblock--drag-and-drop--editor .target-image-form input { .xblock--drag-and-drop--editor .target-image-form .background-image-type {
display: block;
margin-bottom: 8px;
}
.xblock--drag-and-drop--editor .target-image-form .background-auto {
margin-top: 20px;
}
.xblock--drag-and-drop--editor .target-image-form input[type="text"] {
width: 50%; width: 50%;
} }
.xblock--drag-and-drop--editor .target-image-form textarea { .xblock--drag-and-drop--editor .target-image-form textarea {
...@@ -104,6 +113,11 @@ ...@@ -104,6 +113,11 @@
display: block; display: block;
} }
.xblock--drag-and-drop--editor .target-image-form .background-auto .autozone-layout,
.xblock--drag-and-drop--editor .target-image-form .background-auto .autozone-size {
width: 4em;
}
.xblock--drag-and-drop--editor input, .xblock--drag-and-drop--editor input,
.xblock--drag-and-drop--editor textarea { .xblock--drag-and-drop--editor textarea {
box-sizing: border-box; box-sizing: border-box;
......
...@@ -33,6 +33,7 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -33,6 +33,7 @@ function DragAndDropEditBlock(runtime, element, params) {
zoneElement: Handlebars.compile($(".zone-element-tpl", element).html()), zoneElement: Handlebars.compile($(".zone-element-tpl", element).html()),
zoneCheckbox: Handlebars.compile($(".zone-checkbox-tpl", element).html()), zoneCheckbox: Handlebars.compile($(".zone-checkbox-tpl", element).html()),
itemInput: Handlebars.compile($(".item-input-tpl", element).html()), itemInput: Handlebars.compile($(".item-input-tpl", element).html()),
autozoneSvg: Handlebars.compile($(".autozone-tpl", element).html())
}; };
} }
}, },
...@@ -68,6 +69,9 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -68,6 +69,9 @@ function DragAndDropEditBlock(runtime, element, params) {
// Hide settings that are specific to assessment mode // Hide settings that are specific to assessment mode
_fn.build.$el.feedback.form.find('.problem-mode').trigger('change'); _fn.build.$el.feedback.form.find('.problem-mode').trigger('change');
// Check whether we're using an autogenerated image and fill related input values accordingly.
_fn.build.initAutozoneInputs();
// Set focus on first input field. // Set focus on first input field.
$element.find('input:first').select(); $element.find('input:first').select();
}, },
...@@ -93,6 +97,53 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -93,6 +97,53 @@ function DragAndDropEditBlock(runtime, element, params) {
return success return success
}, },
initAutozoneInputs: function() {
var image_params = _fn.build.parseDataUriParams(params.target_img_expanded_url);
var image_type = image_params.producer === 'dndv2' ? 'auto' : 'manual';
var zone_tab = _fn.build.$el.zones.tab;
var radio_selector = '.background-image-type input[value=' + image_type + ']';
zone_tab.find(radio_selector).prop('checked', true).trigger('change');
zone_tab.find('.autozone-layout-cols').val(image_params.cols || 3);
zone_tab.find('.autozone-layout-rows').val(image_params.rows || 1);
zone_tab.find('.autozone-size-width').val(image_params.zone_width || 200);
zone_tab.find('.autozone-size-height').val(image_params.zone_height || 200);
},
// Examine the image src to determine whether the background image has been auto generated.
isAutogeneratedImage: function(src) {
if (src && src.match(/^data:image\/svg\+xml/)) {
if (_fn.build.parseDataUriParams(src).producer === 'dndv2') {
return true;
}
}
return false;
},
// When we auto generate a background image, we embed some parameters such as zone size and position
// into the generated data URI.
encodeDataUriParams: function(params) {
var encoded = [];
Object.keys(params).forEach(function(key) {
var json = JSON.stringify(params[key]);
encoded.push(encodeURIComponent(key) + '=' + encodeURIComponent(json));
});
return encoded.join(';');
},
parseDataUriParams: function(data_uri) {
var params = {};
var match = data_uri.match(/^data:image\/svg\+xml;([^,]+),/);
if (match) {
match[1].split(';').forEach(function(str) {
var pair = str.split('=');
if (pair.length === 2) {
params[decodeURIComponent(pair[0])] = JSON.parse(decodeURIComponent(pair[1]));
}
});
}
return params;
},
scrollToTop: function() { scrollToTop: function() {
$('.drag-builder', element).scrollTop(0); $('.drag-builder', element).scrollTop(0);
}, },
...@@ -123,7 +174,9 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -123,7 +174,9 @@ function DragAndDropEditBlock(runtime, element, params) {
} }
// Set the target image and bind its event handler: // Set the target image and bind its event handler:
if (!_fn.build.isAutogeneratedImage(_fn.data.targetImg)) {
$('.target-image-form .background-url', element).val(_fn.data.targetImg); $('.target-image-form .background-url', element).val(_fn.data.targetImg);
}
$('.target-image-form .background-description', element).val(_fn.data.targetImgDescription); $('.target-image-form .background-description', element).val(_fn.data.targetImgDescription);
_fn.build.$el.targetImage.load(_fn.build.form.zone.imageLoaded); _fn.build.$el.targetImage.load(_fn.build.form.zone.imageLoaded);
_fn.build.$el.targetImage.attr('src', params.target_img_expanded_url); _fn.build.$el.targetImage.attr('src', params.target_img_expanded_url);
...@@ -179,6 +232,7 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -179,6 +232,7 @@ function DragAndDropEditBlock(runtime, element, params) {
.on('change', '.problem-mode', _fn.build.form.problem.toggleAssessmentSettings); .on('change', '.problem-mode', _fn.build.form.problem.toggleAssessmentSettings);
$zoneTab $zoneTab
.on('change', '.background-image-type input', _fn.build.form.zone.toggleAutozoneSettings)
.on('click', '.add-zone', function(e) { .on('click', '.add-zone', function(e) {
_fn.build.form.zone.add(); _fn.build.form.zone.add();
// Set focus to first field of the new zone. // Set focus to first field of the new zone.
...@@ -187,7 +241,7 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -187,7 +241,7 @@ function DragAndDropEditBlock(runtime, element, params) {
.on('click', '.remove-zone', _fn.build.form.zone.remove) .on('click', '.remove-zone', _fn.build.form.zone.remove)
.on('input', '.zone-row input', _fn.build.form.zone.changedInputHandler) .on('input', '.zone-row input', _fn.build.form.zone.changedInputHandler)
.on('change', '.zone-align-select', _fn.build.form.zone.changedInputHandler) .on('change', '.zone-align-select', _fn.build.form.zone.changedInputHandler)
.on('click', '.target-image-form button', function(e) { .on('click', '.target-image-form .background-manual button', function(e) {
var new_img_url = $.trim($('.target-image-form .background-url', element).val()); var new_img_url = $.trim($('.target-image-form .background-url', element).val());
if (new_img_url) { if (new_img_url) {
// We may need to 'expand' the URL before it will be valid. // We may need to 'expand' the URL before it will be valid.
...@@ -202,6 +256,11 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -202,6 +256,11 @@ function DragAndDropEditBlock(runtime, element, params) {
} }
_fn.data.targetImg = new_img_url; _fn.data.targetImg = new_img_url;
}) })
.on(
'click',
'.target-image-form .background-auto button',
_fn.build.form.zone.generateBackgroundAndZones
)
.on('input', '.target-image-form .background-description', function(e) { .on('input', '.target-image-form .background-description', function(e) {
var new_description = $.trim( var new_description = $.trim(
$('.target-image-form .background-description', element).val() $('.target-image-form .background-description', element).val()
...@@ -243,6 +302,17 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -243,6 +302,17 @@ function DragAndDropEditBlock(runtime, element, params) {
zone: { zone: {
totalZonesCreated: 0, // This counter is used for HTML IDs. Never decremented. totalZonesCreated: 0, // This counter is used for HTML IDs. Never decremented.
zoneObjects: [], zoneObjects: [],
toggleAutozoneSettings: function(e) {
var element = _fn.build.$el.zones.tab;
var value = element.find('.background-image-type input:checked').val();
if (value === 'manual') {
element.find('.background-manual').show();
element.find('.background-auto').hide();
} else {
element.find('.background-auto').show();
element.find('.background-manual').hide();
}
},
getZoneObjByUID: function(uid) { getZoneObjByUID: function(uid) {
for (var i = 0; i < _fn.build.form.zone.zoneObjects.length; i++) { for (var i = 0; i < _fn.build.form.zone.zoneObjects.length; i++) {
if (_fn.build.form.zone.zoneObjects[i].uid == uid) { if (_fn.build.form.zone.zoneObjects[i].uid == uid) {
...@@ -348,7 +418,6 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -348,7 +418,6 @@ function DragAndDropEditBlock(runtime, element, params) {
); );
}); });
}, },
changedInputHandler: function(ev) { changedInputHandler: function(ev) {
// Called when any of the inputs have changed. // Called when any of the inputs have changed.
var $changedInput = $(ev.currentTarget); var $changedInput = $(ev.currentTarget);
...@@ -375,6 +444,115 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -375,6 +444,115 @@ function DragAndDropEditBlock(runtime, element, params) {
// The target background image has loaded (or reloaded, if changed). // The target background image has loaded (or reloaded, if changed).
_fn.build.form.zone.renderZonesPreview(); _fn.build.form.zone.renderZonesPreview();
}, },
getAutozoneParams: function() {
var element = _fn.build.$el.zones.tab.find('.background-auto');
return {
rows: parseInt(element.find('.autozone-layout-rows').val(), 10),
cols: parseInt(element.find('.autozone-layout-cols').val(), 10),
zone_width: parseInt(element.find('.autozone-size-width').val(), 10),
zone_height: parseInt(element.find('.autozone-size-height').val(), 10),
padding: 20
};
},
validateAutozoneParams: function(params) {
var fields = [
'.autozone-layout-cols',
'.autozone-layout-rows',
'.autozone-size-width',
'.autozone-size-height'
];
var success = true;
fields.forEach(function(field) {
var element = _fn.build.$el.zones.tab.find(field);
var val = element.val();
var number = parseInt(element.val(), 10);
// Make sure the user entered a positive integer number.
if (number && number > 0 && String(number) === val) {
element.removeClass('field-error');
} else {
element.addClass('field-error');
success = false;
}
});
if (!success) {
runtime.notify('error', {
'title': window.gettext("There was an error with your form."),
'message': window.gettext("Please check the values you entered.")
});
}
return success;
},
calculateAutozoneData: function(params) {
var rows = params.rows;
var cols = params.cols;
var zone_width = params.zone_width;
var zone_height = params.zone_height;
var padding = params.padding;
var width = (zone_width * cols) + (padding * (cols + 1));
var height = (zone_height * rows) + (padding * (rows + 1));
var zones = [];
for (var row = 0; row < rows; row++) {
for (var col = 0; col < cols; col++) {
zones.push({
width: zone_width,
height: zone_height,
x: (padding * (col + 1) + (col * zone_width)),
y: (padding * (row + 1) + (row * zone_height))
});
}
}
return {
width: width,
height: height,
zones: zones
};
},
generateBackgroundDataUri: function(autozone_data, params) {
var autozone_data = _fn.build.form.zone.calculateAutozoneData(params);
var svg = _fn.tpl.autozoneSvg(autozone_data).trim();
var data_uri_params = _fn.build.encodeDataUriParams({
producer: 'dndv2',
cols: params.cols,
rows: params.rows,
zone_width: params.zone_width,
zone_height: params.zone_height
});
return 'data:image/svg+xml;' + data_uri_params + ',' + encodeURIComponent(svg);
},
generateZones: function(zones) {
// First remove all existing zones.
_fn.build.$el.zones.form.find('.zone-row').remove();
_fn.build.form.zone.zoneObjects = [];
// Now generate new zones.
zones.forEach(function(zone) {
_fn.build.form.zone.add({
width: zone.width,
height: zone.height,
x: zone.x,
y: zone.y,
align: 'center'
});
});
},
generateBackgroundAndZones: function() {
var params = _fn.build.form.zone.getAutozoneParams();
if (!_fn.build.form.zone.validateAutozoneParams(params)) {
return;
}
var autozone_data = _fn.build.form.zone.calculateAutozoneData(params);
// Generate zones.
_fn.build.form.zone.generateZones(autozone_data.zones);
// Set background img src.
var data_uri = _fn.build.form.zone.generateBackgroundDataUri(autozone_data, params);
_fn.data.targetImg = data_uri;
_fn.build.$el.targetImage.attr('src', data_uri);
// Make sure "Display label names on the image" is checked.
_fn.data.displayLabels = true;
$('.display-labels-form input', element).prop('checked', true);
}
}, },
createCheckboxes: function(selectedZones) { createCheckboxes: function(selectedZones) {
var template = _fn.tpl.zoneCheckbox; var template = _fn.tpl.zoneCheckbox;
......
...@@ -91,18 +91,76 @@ ...@@ -91,18 +91,76 @@
</header> </header>
<div class="tab-content"> <div class="tab-content">
<form class="target-image-form"> <form class="target-image-form">
<fieldset>
<legend class="h4">{% trans "Background Image" %}</legend>
<label class="background-image-type">
<input type="radio" name="background-image-type-{{id_suffix}}" value="manual" />
{% trans "Provide custom image" %}
</label>
<label class="background-image-type">
<input type="radio" name="background-image-type-{{id_suffix}}" value="auto" />
{% trans "Generate image automatically" %}
</label>
</fieldset>
<fieldset class="background-manual">
<label class="h4" for="background-url-{{id_suffix}}"> <label class="h4" for="background-url-{{id_suffix}}">
<span>{% trans "Background URL" %}</span> <span>{% trans "Background URL" %}</span>
</label> </label>
<input class="background-url" <input class="background-url"
id="background-url-{{id_suffix}}" id="background-url-{{id_suffix}}"
type="text" type="text"
aria-describedby="background-url-{{id_suffix}}" /> aria-describedby="background-url-description-{{id_suffix}}" />
</label> </label>
<button type="button" class="btn">{% trans "Change background" %}</button> <button type="button" class="btn">{% trans "Change background" %}</button>
<div id="background-url-description-{{id_suffix}}" class="form-help"> <div id="background-url-description-{{id_suffix}}" class="form-help">
{% trans "For example, 'http://example.com/background.png' or '/static/background.png'." %} {% trans "For example, 'http://example.com/background.png' or '/static/background.png'." %}
</div> </div>
</fieldset>
<fieldset class="background-auto">
<fieldset>
<legend class="h4">{% trans "Zone Layout" %}</legend>
<label class="sr" for="autozone-cols-{{id_suffix}}">
{% trans "Number of columns" %}
</label>
<input id="autozone-cols-{{id_suffix}}"
class="autozone-layout autozone-layout-cols"
type="text"
aria-describedby="autozone-layout-description-{{id_suffix}}" />
<span>&times;</span>
<label class="sr" for="autozone-rows-{{id_suffix}}">
{% trans "Number of rows" %}
</label>
<input id="autozone-rows-{{id_suffix}}"
class="autozone-layout autozone-layout-rows"
type="text"
aria-describedby="autozone-layout-description-{{id_suffix}}" />
<div id="autozone-layout-description-{{id_suffix}}" class="form-help">
{% trans "Number of columns and rows." %}
</div>
</fieldset>
<fieldset>
<legend class="h4">{% trans "Zone Size" %}</legend>
<label class="sr" for="autozone-zone-width-{{id_suffix}}">
{% trans "Zone width" %}
</label>
<input id="autozone-zone-width-{{id_suffix}}"
class="autozone-size autozone-size-width"
type="text"
aria-describedby="autozone-size-description-{{id_suffix}}" />
<span>&times;</span>
<label class="sr" for="autozone-zone-height-{{id_suffix}}">
{% trans "Zone height" %}
</label>
<input id="autozone-zone-height-{{id_suffix}}"
class="autozone-size autozone-size-height"
type="text"
aria-describedby="autozone-size-description-{{id_suffix}}" />
<div id="autozone-size-description-{{id_suffix}}" class="form-help">
{% trans "Size of a single zone in pixels." %}
</div>
</fieldset>
<button type="button" class="btn">{% trans "Generate image and zones" %}</button>
</fieldset>
<label class="h4"> <label class="h4">
<span>{% trans "Background description" %}</span> <span>{% trans "Background description" %}</span>
<textarea required class="background-description" <textarea required class="background-description"
......
...@@ -172,3 +172,21 @@ ...@@ -172,3 +172,21 @@
</div> </div>
</fieldset> </fieldset>
</script> </script>
<script class="autozone-tpl" type="text/html">
<svg xmlns="http://www.w3.org/2000/svg" width="{{width}}" height="{{height}}">
<rect x="0" y="0" width="100%" height="100%" fill="#fff" />
{{#each zones}}
<rect x="{{x}}"
y="{{y}}"
width="{{width}}"
height="{{height}}"
fill="#f7f7f7"
rx="10"
ry="10"
stroke="#d6d6d6"
stroke-width="2"
stroke-dasharray="3,3" />
{{/each}}
</svg>
</script>
from xblockutils.studio_editable_test import StudioEditableBaseTest
class TestStudio(StudioEditableBaseTest):
"""
Tests that cover the editing interface in the Studio.
"""
def load_scenario(self, xml='<drag-and-drop-v2 url_name="defaults" />'):
self.set_scenario_xml(xml)
self.element = self.go_to_view('studio_view')
self.fix_js_environment()
def click_continue(self):
continue_button = self.element.find_element_by_css_selector('.continue-button')
self.scroll_into_view(continue_button)
continue_button.click()
def scroll_into_view(self, element):
"""
Scrolls to the element and places cursor above it.
Useful when you want to click an element that is scrolled off
the visible area of the screen.
"""
# We have to use block: 'end' rather than the default 'start' because there's a fixed
# title bar in the studio view in the workbench that can obstruct the element.
script = "arguments[0].scrollIntoView({behavior: 'instant', block: 'end'})"
self.browser.execute_script(script, element)
@property
def feedback_tab(self):
return self.element.find_element_by_css_selector('.feedback-tab')
@property
def zones_tab(self):
return self.element.find_element_by_css_selector('.zones-tab')
@property
def items_tab(self):
return self.element.find_element_by_css_selector('.items-tab')
@property
def background_image_type_radio_buttons(self):
radio_buttons = self.zones_tab.find_elements_by_css_selector('.background-image-type input[type="radio"]')
self.assertEqual(len(radio_buttons), 2)
self.assertEqual(radio_buttons[0].get_attribute('value'), 'manual')
self.assertEqual(radio_buttons[1].get_attribute('value'), 'auto')
return {'manual': radio_buttons[0], 'auto': radio_buttons[1]}
@property
def display_labels_checkbox(self):
return self.zones_tab.find_element_by_css_selector('.display-labels')
@property
def background_image_url_field(self):
return self.zones_tab.find_element_by_css_selector('.background-manual .background-url')
@property
def background_image_url_button(self):
return self.zones_tab.find_element_by_css_selector('.background-manual button')
@property
def autozone_cols_field(self):
return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-layout-cols')
@property
def autozone_rows_field(self):
return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-layout-rows')
@property
def autozone_width_field(self):
return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-size-width')
@property
def autozone_height_field(self):
return self.zones_tab.find_element_by_css_selector('.background-auto .autozone-size-height')
@property
def autozone_generate_button(self):
return self.zones_tab.find_element_by_css_selector('.background-auto button')
@property
def target_preview_img(self):
return self.zones_tab.find_element_by_css_selector('.target-img')
@property
def zones(self):
return self.zones_tab.find_elements_by_css_selector('.zone-row')
def test_defaults(self):
"""
Basic test to verify stepping through the editor steps and saving works.
"""
self.load_scenario()
# We start on the feedback tab.
self.assertTrue(self.feedback_tab.is_displayed())
self.assertFalse(self.zones_tab.is_displayed())
self.assertFalse(self.items_tab.is_displayed())
# Continue to the zones tab.
self.click_continue()
self.assertFalse(self.feedback_tab.is_displayed())
self.assertTrue(self.zones_tab.is_displayed())
self.assertFalse(self.items_tab.is_displayed())
# And finally to the items tab.
self.click_continue()
self.assertFalse(self.feedback_tab.is_displayed())
self.assertFalse(self.zones_tab.is_displayed())
self.assertTrue(self.items_tab.is_displayed())
# Save the block and expect success.
self.click_save(expect_success=True)
def test_custom_image(self):
""""
Verify user can provide a custom background image URL.
"""
default_bg_img_src = 'http://localhost:8081/resource/drag-and-drop-v2/public/img/triangle.png'
# In order to use a working image and avoid load errors, we use the default image with a custom
# query string
custom_bg_img_src = '{}?my-custom-image=true'.format(default_bg_img_src)
self.load_scenario()
# Go to zones tab.
self.click_continue()
radio_buttons = self.background_image_type_radio_buttons
# Manual mode should be selected by default.
self.assertTrue(radio_buttons['manual'].is_selected())
self.assertFalse(radio_buttons['auto'].is_selected())
url_field = self.background_image_url_field
self.assertEqual(url_field.get_attribute('value'), '')
self.assertEqual(self.target_preview_img.get_attribute('src'), default_bg_img_src)
url_field.send_keys(custom_bg_img_src)
self.scroll_into_view(self.background_image_url_button)
self.background_image_url_button.click()
self.assertEqual(self.target_preview_img.get_attribute('src'), custom_bg_img_src)
self.click_continue()
self.click_save(expect_success=True)
# Verify the custom image src was saved successfully.
self.element = self.go_to_view('student_view')
target_img = self.element.find_element_by_css_selector('.target-img')
self.assertEqual(target_img.get_attribute('src'), custom_bg_img_src)
# Verify the background image URL field is set to custom image src when we go back to studio view.
self.element = self.go_to_view('studio_view')
self.click_continue()
self.assertEqual(self.background_image_url_field.get_attribute('value'), custom_bg_img_src)
def _verify_autogenerated_zones(self, cols, rows, zone_width, zone_height, padding):
zones = self.zones
self.assertEqual(len(zones), rows * cols)
for col in range(cols):
for row in range(rows):
idx = col + (row * cols)
zone = zones[idx]
expected_values = {
'zone-title': 'Zone {}'.format(idx + 1),
'zone-width': zone_width,
'zone-height': zone_height,
'zone-x': (zone_width * col) + (padding * (col + 1)),
'zone-y': (zone_height * row) + (padding * (row + 1)),
}
for name, expected_value in expected_values.iteritems():
field = zone.find_element_by_css_selector('.' + name)
self.assertEqual(field.get_attribute('value'), str(expected_value))
def test_auto_generated_image(self):
"""
Verify that background image and zones get generated successfully.
"""
cols = 3
rows = 2
zone_width = 150
zone_height = 100
padding = 20
self.load_scenario()
# Go to zones tab.
self.click_continue()
radio_buttons = self.background_image_type_radio_buttons
self.scroll_into_view(radio_buttons['auto'])
radio_buttons['auto'].click()
# Manual background controls should be hidden.
self.assertFalse(self.background_image_url_field.is_displayed())
self.assertFalse(self.background_image_url_button.is_displayed())
# Display labels checkbox should be unchecked by default.
self.assertFalse(self.display_labels_checkbox.is_selected())
# Enter zone properties for automatic generation.
self.autozone_cols_field.clear()
self.autozone_cols_field.send_keys(cols)
self.autozone_rows_field.clear()
self.autozone_rows_field.send_keys(rows)
self.autozone_width_field.clear()
self.autozone_width_field.send_keys(zone_width)
self.autozone_height_field.clear()
self.autozone_height_field.send_keys(zone_height)
# Click the generate button.
self.scroll_into_view(self.autozone_generate_button)
self.autozone_generate_button.click()
# Verify generated data-uri was set successfully.
generated_url = self.target_preview_img.get_attribute('src')
self.assertTrue(generated_url.startswith('data:image/svg+xml;'))
expected_width = (zone_width * cols) + (padding * (cols + 1))
expected_height = (zone_height * rows) + (padding * (rows + 1))
self.assertEqual(self.target_preview_img.get_attribute('naturalWidth'), str(expected_width))
self.assertEqual(self.target_preview_img.get_attribute('naturalHeight'), str(expected_height))
# Display labels checkbox should be automatically selected.
self.assertTrue(self.display_labels_checkbox.is_selected())
# Verify there are exactly 6 zones, and their properties are correct.
self._verify_autogenerated_zones(cols, rows, zone_width, zone_height, padding)
# Fill in zone descriptions to make the form valid (zone descriptions are required).
for zone in self.zones:
zone.find_element_by_css_selector('.zone-description').send_keys('Description')
# Save the block.
self.click_continue()
self.click_save(expect_success=True)
# Verify the custom image src was saved successfully.
self.element = self.go_to_view('student_view')
target_img = self.element.find_element_by_css_selector('.target-img')
self.assertTrue(target_img.get_attribute('src').startswith('data:image/svg+xml'))
self.assertEqual(target_img.get_attribute('naturalWidth'), str(expected_width))
self.assertEqual(target_img.get_attribute('naturalHeight'), str(expected_height))
# Verify the background image URL field is set to custom image src when we go back to studio view.
self.element = self.go_to_view('studio_view')
self.click_continue()
radio_buttons = self.background_image_type_radio_buttons
self.assertFalse(radio_buttons['manual'].is_selected())
self.assertTrue(radio_buttons['auto'].is_selected())
self.assertEqual(self.autozone_cols_field.get_attribute('value'), str(cols))
self.assertEqual(self.autozone_rows_field.get_attribute('value'), str(rows))
self.assertEqual(self.autozone_width_field.get_attribute('value'), str(zone_width))
self.assertEqual(self.autozone_height_field.get_attribute('value'), str(zone_height))
def test_autozone_parameter_validation(self):
"""
Test that autozone parameters are verified to be valid.
"""
self.load_scenario()
# Go to zones tab.
self.click_continue()
radio_buttons = self.background_image_type_radio_buttons
self.scroll_into_view(radio_buttons['auto'])
radio_buttons['auto'].click()
# All fields are valid initially.
self.assertFalse('field-error' in self.autozone_cols_field.get_attribute('class'))
self.assertFalse('field-error' in self.autozone_rows_field.get_attribute('class'))
self.assertFalse('field-error' in self.autozone_width_field.get_attribute('class'))
self.assertFalse('field-error' in self.autozone_height_field.get_attribute('class'))
# Set two of the fields to invalid values.
self.autozone_cols_field.clear()
self.autozone_cols_field.send_keys('2.5')
self.autozone_height_field.clear()
self.autozone_height_field.send_keys('100A')
# Try to generate the image.
self.scroll_into_view(self.autozone_generate_button)
self.autozone_generate_button.click()
# The two bad fields should show errors.
self.assertTrue('field-error' in self.autozone_cols_field.get_attribute('class'))
self.assertFalse('field-error' in self.autozone_rows_field.get_attribute('class'))
self.assertFalse('field-error' in self.autozone_width_field.get_attribute('class'))
self.assertTrue('field-error' in self.autozone_height_field.get_attribute('class'))
# Fix the faulty values.
self.autozone_cols_field.clear()
self.autozone_cols_field.send_keys('2')
self.autozone_height_field.clear()
self.autozone_height_field.send_keys('100')
self.scroll_into_view(self.autozone_generate_button)
self.autozone_generate_button.click()
# All good now.
self.assertFalse('field-error' in self.autozone_cols_field.get_attribute('class'))
self.assertFalse('field-error' in self.autozone_rows_field.get_attribute('class'))
self.assertFalse('field-error' in self.autozone_width_field.get_attribute('class'))
self.assertFalse('field-error' in self.autozone_height_field.get_attribute('class'))
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