Commit addea00c by Tim Krones

Merge pull request #82 from open-craft/plot-overlays

Step Builder: Support for additional overlays for plot blocks
parents 21326b8c 8af9fa2b
...@@ -26,6 +26,8 @@ from lazy.lazy import lazy ...@@ -26,6 +26,8 @@ from lazy.lazy import lazy
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import String, Scope from xblock.fields import String, Scope
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import ( from xblockutils.studio_editable import (
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin
...@@ -244,12 +246,89 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin ...@@ -244,12 +246,89 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
'average_claims': self.average_claims, 'average_claims': self.average_claims,
} }
@property
def allowed_nested_blocks(self):
"""
Returns a list of allowed nested XBlocks. Each item can be either
* An XBlock class
* A NestedXBlockSpec
If XBlock class is used it is assumed that this XBlock is enabled and allows multiple instances.
NestedXBlockSpec allows explicitly setting disabled/enabled state,
disabled reason (if any) and single/multiple instances.
"""
return [PlotOverlayBlock]
@lazy
def overlay_ids(self):
"""
Get the usage_ids of all of this XBlock's children that are overlays.
"""
return [
_normalize_id(child_id) for child_id in self.children if
child_isinstance(self, child_id, PlotOverlayBlock)
]
@lazy
def overlays(self):
"""
Get the overlay children of this block.
"""
return [self.runtime.get_block(overlay_id) for overlay_id in self.overlay_ids]
@lazy
def overlay_data(self):
if not self.claims:
return []
overlay_data = []
claims = self.claims.split('\n')
for index, overlay in enumerate(self.overlays):
claims_json = []
if overlay.claim_data:
claim_data = overlay.claim_data.split('\n')
for claim, data in zip(claims, claim_data):
claim = claim.split(', ')[0]
r1, r2 = data.split(', ')
claims_json.append([claim, int(r1), int(r2)])
claims_json = json.dumps(claims_json)
overlay_data.append({
'plot_label': overlay.plot_label,
'point_color': overlay.point_color,
'description': overlay.description,
'citation': overlay.citation,
'claims_json': claims_json,
'position': index,
})
return overlay_data
@lazy
def claims_display(self):
if not self.claims:
return []
claims = []
for claim in self.claims.split('\n'):
claim, q1, q2 = claim.split(', ')
claims.append([claim, q1, q2])
return claims
def author_preview_view(self, context): def author_preview_view(self, context):
return Fragment( context['self'] = self
u"<p>{}</p>".format( fragment = Fragment()
_(u"This block displays a plot that summarizes answers to scale questions.") fragment.add_content(loader.render_template('templates/html/plot_preview.html', context))
) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/plot-preview.css'))
) if self.overlay_ids:
fragment.add_content(
u"<p>{}</p>".format(
_(u"In addition to the default and average overlays the plot includes the following overlays:")
))
for overlay in self.overlays:
overlay_fragment = self._render_child_fragment(overlay, context, view='mentoring_view')
fragment.add_frag_resources(overlay_fragment)
fragment.add_content(overlay_fragment.content)
return fragment
def mentoring_view(self, context): def mentoring_view(self, context):
return self.student_view(context) return self.student_view(context)
...@@ -266,3 +345,115 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin ...@@ -266,3 +345,115 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/plot.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/plot.js'))
fragment.initialize_js('PlotBlock') fragment.initialize_js('PlotBlock')
return fragment return fragment
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add child blocks.
"""
context['wrap_children'] = {
'head': u'<div class="mentoring">',
'tail': u'</div>'
}
fragment = super(PlotBlock, self).author_edit_view(context)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/plot_edit.js'))
fragment.initialize_js('PlotEdit')
return fragment
@XBlock.needs('i18n')
class PlotOverlayBlock(StudioEditableXBlockMixin, XBlockWithPreviewMixin, XBlock):
"""
XBlock that represents a user-defined overlay for a plot block.
"""
CATEGORY = 'sb-plot-overlay'
STUDIO_LABEL = _(u"Plot Overlay")
# Settings
display_name = String(
display_name=_("Overlay title"),
default="Overlay",
scope=Scope.content
)
plot_label = String(
display_name=_("Plot label"),
help=_("Label for button that allows to toggle visibility of this overlay"),
default="",
scope=Scope.content
)
point_color = String(
display_name=_("Point color"),
help=_("Point color to use for this overlay"),
default="",
scope=Scope.content
)
description = String(
display_name=_("Description"),
help=_("Description of this overlay (optional)"),
default="",
scope=Scope.content
)
citation = String(
display_name=_("Citation"),
help=_("Source of data belonging to this overlay (optional)"),
default="",
scope=Scope.content
)
claim_data = String(
display_name=_("Claim data"),
help=_(
'Claim data to include in this overlay. '
'Each line defines a tuple of the form "q1, q2", '
'where "q1" is the value associated with the first scale or rating question, '
'and "q2" is the value associated with the second scale or rating question. '
'Note that data will be associated with claims in the order that they are defined in the parent plot.'
),
default="",
multiline_editor=True,
resettable_editor=False
)
editable_fields = (
"plot_label", "point_color", "description", "citation", "claim_data"
)
def validate_field_data(self, validation, data):
"""
Validate this block's field data.
"""
super(PlotOverlayBlock, self).validate_field_data(validation, data)
def add_error(msg):
validation.add(ValidationMessage(ValidationMessage.ERROR, msg))
if not data.plot_label.strip():
add_error(_(u"No plot label set. Button for toggling visibility of this overlay will not have a label."))
if not data.point_color.strip():
add_error(_(u"No point color set. This overlay will not work correctly."))
# If parent plot is associated with one or more claims, prompt user to add claim data
parent = self.get_parent()
if parent.claims.strip() and not data.claim_data.strip():
add_error(_(u"No claim data provided. This overlay will not work correctly."))
def author_preview_view(self, context):
return self.student_view(context)
def mentoring_view(self, context):
context = context.copy() if context else {}
context['hide_header'] = True
return self.author_preview_view(context)
def student_view(self, context):
context['self'] = self
fragment = Fragment()
fragment.add_content(loader.render_template('templates/html/overlay.html', context))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/overlay.css'))
return fragment
.sb-plot-overlay {
margin-bottom: 10px;
}
.italic {
font-style: italic;
}
.sb-plot table {
width: 100%;
margin-top: 1em;
margin-bottom: 1em;
border: 2px solid #999;
}
.sb-plot thead {
border-bottom: 2px solid #999;
background-color: #ddd;
font-weight: bold;
}
.sb-plot tr:nth-child(even) {
background-color: #eee;
}
.sb-plot td {
border-left: 1px solid #999;
padding: 5px;
}
.sb-plot {
overflow: auto;
}
.quadrants label { .quadrants label {
font-weight: bold; font-weight: bold;
} }
.overlays { .overlays {
float: right; float: right;
width: 40%;
}
.overlays input {
margin-top: 10px;
margin-right: 5px;
} }
.quadrants input, .overlays input { .quadrants input, .overlays input {
background-color: rgb(204, 204, 204); background-color: rgb(204, 204, 204);
} }
.plot-info {
margin-top: 15px;
}
.plot-info-header {
font-weight: bold;
}
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
} }
/* Custom appearance for our "Add" buttons */ /* Custom appearance for our "Add" buttons */
.xblock[data-block-type=sb-plot] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=sb-review-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button, .xblock[data-block-type=sb-review-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button, .xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=step-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button, .xblock[data-block-type=step-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
...@@ -25,6 +26,8 @@ ...@@ -25,6 +26,8 @@
line-height: 30px; line-height: 30px;
} }
.xblock[data-block-type=sb-plot] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=sb-plot] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover,
.xblock[data-block-type=sb-review-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled, .xblock[data-block-type=sb-review-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=sb-review-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover, .xblock[data-block-type=sb-review-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover,
.xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled, .xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
......
function PlotBlock(runtime, element) { function PlotBlock(runtime, element) {
// jQuery helpers
jQuery.fn.isEmpty = function() {
return !$.trim($(this).html());
};
jQuery.fn.isHidden = function() {
// Don't use jQuery :hidden selector here;
// this is necessary to ensure that result is independent of parent visibility
return $(this).css('display') === 'none';
};
jQuery.fn.isVisible = function() {
// Don't use jQuery :visible selector here;
// this is necessary to ensure that result is independent of parent visibility
return $(this).css('display') !== 'none';
};
// Plot // Plot
// Define margins // Define margins
...@@ -58,7 +76,8 @@ function PlotBlock(runtime, element) { ...@@ -58,7 +76,8 @@ function PlotBlock(runtime, element) {
var defaultButton = $('.plot-default', element), var defaultButton = $('.plot-default', element),
averageButton = $('.plot-average', element), averageButton = $('.plot-average', element),
quadrantsButton = $('.plot-quadrants', element); quadrantsButton = $('.plot-quadrants', element),
overlayButtons = $('input.plot-overlay', element);
// Claims // Claims
...@@ -80,7 +99,7 @@ function PlotBlock(runtime, element) { ...@@ -80,7 +99,7 @@ function PlotBlock(runtime, element) {
// Event handlers // Event handlers
function toggleOverlay(claims, color, klass, refresh) { function toggleOverlay(claims, color, klass, refresh) {
var selector = "." + klass, var selector = buildSelector(klass),
selection = svgContainer.selectAll(selector); selection = svgContainer.selectAll(selector);
if (selection.empty()) { if (selection.empty()) {
showOverlay(selection, claims, color, klass); showOverlay(selection, claims, color, klass);
...@@ -92,6 +111,14 @@ function PlotBlock(runtime, element) { ...@@ -92,6 +111,14 @@ function PlotBlock(runtime, element) {
} }
} }
function buildSelector(klass) {
var classes = klass.split(' ');
if (classes.length === 1) {
return "." + klass;
}
return '.' + classes.join('.');
}
function showOverlay(selection, claims, color, klass) { function showOverlay(selection, claims, color, klass) {
selection selection
.data(claims) .data(claims)
...@@ -127,6 +154,29 @@ function PlotBlock(runtime, element) { ...@@ -127,6 +154,29 @@ function PlotBlock(runtime, element) {
} }
} }
function toggleOverlayInfo(klass, hide) {
var plotInfo = $('.plot-info', element),
selector = buildSelector(klass),
overlayInfo = plotInfo.children(selector);
if (hide || overlayInfo.isVisible()) {
overlayInfo.hide();
var overlayInfos = plotInfo.children('.plot-overlay'),
hidePlotInfo = true;
overlayInfos.each(function() {
var overlayInfo = $(this);
hidePlotInfo = hidePlotInfo && (overlayInfo.isHidden() || overlayInfo.isEmpty());
});
if (hidePlotInfo) {
plotInfo.hide();
}
} else {
overlayInfo.show();
if (!overlayInfo.isEmpty() && !plotInfo.isVisible()) {
plotInfo.show();
}
}
}
function toggleQuadrantLabels() { function toggleQuadrantLabels() {
var selection = svgContainer.selectAll(".quadrant-label"), var selection = svgContainer.selectAll(".quadrant-label"),
quadrantLabelsOn = quadrantsButton.val() === 'On'; quadrantLabelsOn = quadrantsButton.val() === 'On';
...@@ -168,7 +218,7 @@ function PlotBlock(runtime, element) { ...@@ -168,7 +218,7 @@ function PlotBlock(runtime, element) {
toggleBorderColor(this, defaultColor, refresh); toggleBorderColor(this, defaultColor, refresh);
}); });
averageButton.on('click', function(event) { averageButton.on('click', function() {
toggleOverlay(averageClaims, averageColor, 'claim-average'); toggleOverlay(averageClaims, averageColor, 'claim-average');
toggleBorderColor(this, averageColor); toggleBorderColor(this, averageColor);
}); });
...@@ -177,9 +227,30 @@ function PlotBlock(runtime, element) { ...@@ -177,9 +227,30 @@ function PlotBlock(runtime, element) {
toggleQuadrantLabels(); toggleQuadrantLabels();
}); });
overlayButtons.each(function(index) {
var overlayButton = $(this),
claims = overlayButton.data('claims'),
color = overlayButton.data('point-color'),
klass = overlayButton.attr('class');
overlayButton.on('click', function() {
toggleOverlay(claims, color, klass);
toggleBorderColor(this, color);
toggleOverlayInfo(klass);
});
// Hide overlay info initially
toggleOverlayInfo(klass, 'hide');
});
// Quadrant labels are off initially; color of button for toggling them should reflect this // Quadrant labels are off initially; color of button for toggling them should reflect this
quadrantsButton.css("border-color", "red"); quadrantsButton.css("border-color", "red");
// Hide plot info initially
$('.plot-info', element).hide();
// API // API
var dataXHR; var dataXHR;
......
function PlotEdit(runtime, element) {
'use strict';
StudioContainerXBlockWithNestedXBlocksMixin(runtime, element);
ProblemBuilderUtil.transformClarifications(element);
}
{% load i18n %}
<div class="sb-plot-overlay">
{% if self.plot_label and self.point_color %}
<h3 style="color: {{ self.point_color }};">{{ self.plot_label }} {% trans "Overlay" %}</h3>
{% endif %}
<p>
<strong>{% trans "Description:" %}</strong>
{% if self.description %}
{{ self.description }}
{% else %}
<span class="italic">{% trans "No description provided" %}</span>
{% endif %}
</p>
<p>
<strong>{% trans "Source:" %}</strong>
{% if self.citation %}
{{ self.citation }}
{% else %}
<span class="italic">{% trans "No citation provided" %}</span>
{% endif %}
</p>
<p>
<strong>{% trans "Data:" %}</strong>
{% if self.claim_data %}
{{ self.claim_data }}
{% else %}
<span class="italic">{% trans "No data provided" %}</span>
{% endif %}
</p>
</div>
{% load i18n %}
<div class="sb-plot"> <div class="sb-plot">
<div class="quadrants"> <div class="quadrants">
<label> <label>
Quadrant labels {% trans "Quadrant labels" %}
<input type="button" <input type="button"
class="plot-quadrants" class="plot-quadrants"
data-q1-label="{{ self.q1_label }}" data-q1-label="{{ self.q1_label }}"
...@@ -14,7 +15,7 @@ ...@@ -14,7 +15,7 @@
</div> </div>
<div class="overlays"> <div class="overlays">
<h3>Compare your plot to others!</h3> <h3>{% trans "Compare your plot to others!" %}</h3>
<input type="button" <input type="button"
class="plot-default" class="plot-default"
...@@ -30,6 +31,40 @@ ...@@ -30,6 +31,40 @@
data-overlay-on="false" data-overlay-on="false"
value="Average" value="Average"
/> />
{% for overlay in self.overlay_data %}
<input type="button"
class="plot-overlay plot-overlay-{{ overlay.position }}"
data-claims="{{ overlay.claims_json }}"
data-point-color="{{ overlay.point_color }}"
data-overlay-on="false"
value="{{ overlay.plot_label }}"
/>
{% endfor %}
<div class="plot-info">
<p class="plot-info-header">{% trans "Plot info" %}</p>
{% for overlay in self.overlay_data %}
<div class="plot-overlay plot-overlay-{{ overlay.position }}">
{% if overlay.description or overlay.citation %}
<p class="overlay-plot-label" style="color: {{ overlay.point_color }};">{{ overlay.plot_label }}</p>
{% if overlay.description %}
<p class="overlay-description">
<strong>{% trans "Description:" %}</strong> {{ overlay.description }}
</p>
{% endif %}
{% if overlay.citation %}
<p class="overlay-citation">
<strong>{% trans "Source:" %}</strong> {{ overlay.citation }}
</p>
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
</div> </div>
</div> </div>
{% load i18n %}
<div class="sb-plot">
<p>{{ self.display_name }}</p>
{% if self.claims %}
<p>{% trans "This block displays a plot that summarizes responses to the following claims:" %}</p>
<table>
<thead>
<tr>
<td>{% trans "Claim" %}</td>
<td>{% trans "Question 1" %}</td>
<td>{% trans "Question 2" %}</td>
</tr>
</thead>
<tbody>
{% for claim in self.claims_display %}
<tr>
<td>{{ claim.0 }}</td>
<td>{{ claim.1 }}</td>
<td>{{ claim.2 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>{% trans "This block displays a plot that summarizes responses to a set of claims." %}</p>
{% endif %}
</div>
...@@ -6,6 +6,28 @@ from .base_test import CORRECT, INCORRECT, PARTIAL, MentoringAssessmentBaseTest, ...@@ -6,6 +6,28 @@ from .base_test import CORRECT, INCORRECT, PARTIAL, MentoringAssessmentBaseTest,
from .test_dashboard import MockSubmissionsAPI from .test_dashboard import MockSubmissionsAPI
class HTMLColors(object):
GREEN = 'rgba(0, 128, 0, 1)'
BLUE = 'rgba(0, 0, 255, 1)'
RED = 'rgba(255, 0, 0, 1)'
GREY = 'rgba(237, 237, 237, 1)'
PURPLE = 'rgba(128, 0, 128, 1)'
ORANGE = 'rgba(255, 165, 0, 1)'
CORAL = 'rgba(255, 127, 80, 1)'
CORNFLOWERBLUE = 'rgba(100, 149, 237, 1)'
OLIVE = 'rgba(128, 128, 0, 1)'
CRIMSON = 'rgba(220, 20, 60, 1)'
class PointColors(object):
ORANGE = 'rgb(255, 165, 0)'
PURPLE = 'rgb(128, 0, 128)'
CORAL = 'rgb(255, 127, 80)'
CORNFLOWERBLUE = 'rgb(100, 149, 237)'
OLIVE = 'rgb(128, 128, 0)'
CRIMSON = 'rgb(220, 20, 60)'
class ExtendedMockSubmissionsAPI(MockSubmissionsAPI): class ExtendedMockSubmissionsAPI(MockSubmissionsAPI):
def get_all_submissions(self, course_key_str, block_id, block_type): def get_all_submissions(self, course_key_str, block_id, block_type):
return ( return (
...@@ -587,6 +609,27 @@ class StepBuilderTest(MentoringAssessmentBaseTest): ...@@ -587,6 +609,27 @@ class StepBuilderTest(MentoringAssessmentBaseTest):
return plot_controls return plot_controls
def additional_plot_controls(self, step_builder):
class Namespace(object):
pass
additional_plot_controls = Namespace()
additional_plot_controls.teacher_button = step_builder.find_element_by_css_selector(
"input.plot-overlay.plot-overlay-0"
)
additional_plot_controls.researchers_button = step_builder.find_element_by_css_selector(
"input.plot-overlay.plot-overlay-1"
)
additional_plot_controls.sheldon_button = step_builder.find_element_by_css_selector(
"input.plot-overlay.plot-overlay-2"
)
additional_plot_controls.yoda_button = step_builder.find_element_by_css_selector(
"input.plot-overlay.plot-overlay-3"
)
return additional_plot_controls
def plot_empty(self, step_builder): def plot_empty(self, step_builder):
points = step_builder.find_elements_by_css_selector("circle") points = step_builder.find_elements_by_css_selector("circle")
self.assertEquals(points, []) self.assertEquals(points, [])
...@@ -601,43 +644,33 @@ class StepBuilderTest(MentoringAssessmentBaseTest): ...@@ -601,43 +644,33 @@ class StepBuilderTest(MentoringAssessmentBaseTest):
] ]
if hidden: if hidden:
self.assertEquals(quadrant_labels, []) self.assertEquals(quadrant_labels, [])
# rgba(255, 0, 0, 1): "red" self.assertTrue(all(bc == HTMLColors.RED for bc in quadrants_button_border_colors))
self.assertTrue(all(bc == 'rgba(255, 0, 0, 1)' for bc in quadrants_button_border_colors))
else: else:
self.assertEquals(len(quadrant_labels), 4) self.assertEquals(len(quadrant_labels), 4)
self.assertEquals(set(label.text for label in quadrant_labels), set(labels)) self.assertEquals(set(label.text for label in quadrant_labels), set(labels))
# rgba(0, 128, 0, 1): "green" self.assertTrue(all(bc == HTMLColors.GREEN for bc in quadrants_button_border_colors))
self.assertTrue(all(bc == 'rgba(0, 128, 0, 1)' for bc in quadrants_button_border_colors))
def click_overlay_button(self, overlay_button, overlay_on, color_on=None, color_off=HTMLColors.GREY):
def click_default_button( overlay_button.click()
self, plot_controls, overlay_on, color_on='rgba(0, 128, 0, 1)', color_off='rgba(237, 237, 237, 1)' button_border_colors = [
): overlay_button.value_of_css_property('border-top-color'),
plot_controls.default_button.click() overlay_button.value_of_css_property('border-right-color'),
default_button_border_colors = [ overlay_button.value_of_css_property('border-bottom-color'),
plot_controls.default_button.value_of_css_property('border-top-color'), overlay_button.value_of_css_property('border-left-color'),
plot_controls.default_button.value_of_css_property('border-right-color'),
plot_controls.default_button.value_of_css_property('border-bottom-color'),
plot_controls.default_button.value_of_css_property('border-left-color'),
] ]
if overlay_on: if overlay_on:
self.assertTrue(all(bc == color_on for bc in default_button_border_colors)) self.assertTrue(all(bc == color_on for bc in button_border_colors))
else: else:
self.assertTrue(all(bc == color_off for bc in default_button_border_colors)) self.assertTrue(all(bc == color_off for bc in button_border_colors))
def click_average_button( def click_default_button(self, plot_controls, overlay_on, color_on=HTMLColors.GREEN):
self, plot_controls, overlay_on, color_on='rgba(0, 0, 255, 1)', color_off='rgba(237, 237, 237, 1)' self.click_overlay_button(plot_controls.default_button, overlay_on, color_on)
):
plot_controls.average_button.click() def click_average_button(self, plot_controls, overlay_on, color_on=HTMLColors.BLUE):
average_button_border_colors = [ self.click_overlay_button(plot_controls.average_button, overlay_on, color_on)
plot_controls.average_button.value_of_css_property('border-top-color'),
plot_controls.average_button.value_of_css_property('border-right-color'), def check_button_label(self, button, expected_value):
plot_controls.average_button.value_of_css_property('border-bottom-color'), self.assertEquals(button.get_attribute('value'), expected_value)
plot_controls.average_button.value_of_css_property('border-left-color'),
]
if overlay_on:
self.assertTrue(all(bc == color_on for bc in average_button_border_colors))
else:
self.assertTrue(all(bc == color_off for bc in average_button_border_colors))
def test_empty_plot(self): def test_empty_plot(self):
step_builder, controls = self.load_assessment_scenario("step_builder_plot_defaults.xml", {}) step_builder, controls = self.load_assessment_scenario("step_builder_plot_defaults.xml", {})
...@@ -655,6 +688,9 @@ class StepBuilderTest(MentoringAssessmentBaseTest): ...@@ -655,6 +688,9 @@ class StepBuilderTest(MentoringAssessmentBaseTest):
self.plot_empty(step_builder) self.plot_empty(step_builder)
# Obtain references to plot controls # Obtain references to plot controls
plot_controls = self.plot_controls(step_builder) plot_controls = self.plot_controls(step_builder)
# Check button labels
self.check_button_label(plot_controls.default_button, "yours")
self.check_button_label(plot_controls.average_button, "Average")
# Check if plot is empty (default overlay off, average overlay off) # Check if plot is empty (default overlay off, average overlay off)
self.click_default_button(plot_controls, overlay_on=False) self.click_default_button(plot_controls, overlay_on=False)
self.plot_empty(step_builder) self.plot_empty(step_builder)
...@@ -678,7 +714,8 @@ class StepBuilderTest(MentoringAssessmentBaseTest): ...@@ -678,7 +714,8 @@ class StepBuilderTest(MentoringAssessmentBaseTest):
for overlay in overlays: for overlay in overlays:
# Check if correct number of points is present # Check if correct number of points is present
points = step_builder.find_elements_by_css_selector(overlay['selector']) selector = 'circle' + overlay['selector']
points = step_builder.find_elements_by_css_selector(selector)
self.assertEquals(len(points), overlay['num_points']) self.assertEquals(len(points), overlay['num_points'])
# Check point colors # Check point colors
point_colors = [ point_colors = [
...@@ -718,11 +755,14 @@ class StepBuilderTest(MentoringAssessmentBaseTest): ...@@ -718,11 +755,14 @@ class StepBuilderTest(MentoringAssessmentBaseTest):
# Step 2: Plot # Step 2: Plot
# Obtain references to plot controls # Obtain references to plot controls
plot_controls = self.plot_controls(step_builder) plot_controls = self.plot_controls(step_builder)
# Check button labels
self.check_button_label(plot_controls.default_button, "Custom plot label")
self.check_button_label(plot_controls.average_button, "Average")
# Overlay data # Overlay data
default_overlay = { default_overlay = {
'selector': '.claim-default', 'selector': '.claim-default',
'num_points': 2, 'num_points': 2,
'point_color': 'rgb(255, 165, 0)', # orange 'point_color': PointColors.ORANGE,
'titles': ['2 + 2 = 5: 1, 5', 'The answer to everything is 42: 5, 1'], 'titles': ['2 + 2 = 5: 1, 5', 'The answer to everything is 42: 5, 1'],
'positions': [ 'positions': [
('20', '396'), # Values computed according to xScale and yScale (cf. plot.js) ('20', '396'), # Values computed according to xScale and yScale (cf. plot.js)
...@@ -732,7 +772,7 @@ class StepBuilderTest(MentoringAssessmentBaseTest): ...@@ -732,7 +772,7 @@ class StepBuilderTest(MentoringAssessmentBaseTest):
average_overlay = { average_overlay = {
'selector': '.claim-average', 'selector': '.claim-average',
'num_points': 2, 'num_points': 2,
'point_color': 'rgb(128, 0, 128)', # purple 'point_color': PointColors.PURPLE,
'titles': ['2 + 2 = 5: 1, 5', 'The answer to everything is 42: 5, 1'], 'titles': ['2 + 2 = 5: 1, 5', 'The answer to everything is 42: 5, 1'],
'positions': [ 'positions': [
('20', '396'), # Values computed according to xScale and yScale (cf. plot.js) ('20', '396'), # Values computed according to xScale and yScale (cf. plot.js)
...@@ -743,7 +783,7 @@ class StepBuilderTest(MentoringAssessmentBaseTest): ...@@ -743,7 +783,7 @@ class StepBuilderTest(MentoringAssessmentBaseTest):
self.check_overlays(step_builder, total_num_points=2, overlays=[default_overlay]) self.check_overlays(step_builder, total_num_points=2, overlays=[default_overlay])
# Check if plot shows correct overlay(s) (default overlay on, average overlay on) # Check if plot shows correct overlay(s) (default overlay on, average overlay on)
self.click_average_button(plot_controls, overlay_on=True, color_on='rgba(128, 0, 128, 1)') # purple self.click_average_button(plot_controls, overlay_on=True, color_on=HTMLColors.PURPLE)
self.check_overlays(step_builder, 4, overlays=[default_overlay, average_overlay]) self.check_overlays(step_builder, 4, overlays=[default_overlay, average_overlay])
# Check if plot shows correct overlay(s) (default overlay off, average overlay on) # Check if plot shows correct overlay(s) (default overlay off, average overlay on)
...@@ -755,7 +795,7 @@ class StepBuilderTest(MentoringAssessmentBaseTest): ...@@ -755,7 +795,7 @@ class StepBuilderTest(MentoringAssessmentBaseTest):
self.plot_empty(step_builder) self.plot_empty(step_builder)
# Check if plot shows correct overlay(s) (default overlay on, average overlay off) # Check if plot shows correct overlay(s) (default overlay on, average overlay off)
self.click_default_button(plot_controls, overlay_on=True, color_on='rgba(255, 165, 0, 1)') # orange self.click_default_button(plot_controls, overlay_on=True, color_on=HTMLColors.ORANGE)
self.check_overlays(step_builder, 2, overlays=[default_overlay]) self.check_overlays(step_builder, 2, overlays=[default_overlay])
# Check quadrant labels # Check quadrant labels
...@@ -765,3 +805,319 @@ class StepBuilderTest(MentoringAssessmentBaseTest): ...@@ -765,3 +805,319 @@ class StepBuilderTest(MentoringAssessmentBaseTest):
step_builder, plot_controls, hidden=False, step_builder, plot_controls, hidden=False,
labels=['Custom Q1 label', 'Custom Q2 label', 'Custom Q3 label', 'Custom Q4 label'] labels=['Custom Q1 label', 'Custom Q2 label', 'Custom Q3 label', 'Custom Q4 label']
) )
def check_display_status(self, element, hidden):
if hidden:
display_status = element.value_of_css_property('display')
self.assertEquals(display_status, 'none')
else:
# self.wait_until_visible(element)
display_status = element.value_of_css_property('display')
self.assertEquals(display_status, 'block')
def check_plot_info(self, step_builder, hidden, visible_overlays=[], hidden_overlays=[]):
# Check if plot info is present and visible
plot_info = step_builder.find_element_by_css_selector(".plot-info")
self.check_display_status(plot_info, hidden)
# Check if info about visible overlays is present and visible
for overlay in visible_overlays:
overlay_info = plot_info.find_element_by_css_selector(overlay['selector'])
self.check_display_status(overlay_info, hidden=False)
description = overlay['description']
citation = overlay['citation']
if description is not None or citation is not None:
overlay_plot_label = overlay_info.find_element_by_css_selector('.overlay-plot-label')
self.assertEquals(overlay_plot_label.text, overlay['plot_label'])
text_color = overlay_plot_label.value_of_css_property('color')
self.assertEquals(text_color, overlay['plot_label_color'])
if description is not None:
overlay_description = overlay_info.find_element_by_css_selector('.overlay-description')
self.assertEquals(overlay_description.text, 'Description: ' + description)
if citation is not None:
overlay_citation = overlay_info.find_element_by_css_selector('.overlay-citation')
self.assertEquals(overlay_citation.text, 'Source: ' + citation)
# Check if info about hidden overlays is hidden
for overlay in hidden_overlays:
overlay_info = plot_info.find_element_by_css_selector(overlay['selector'])
self.check_display_status(overlay_info, hidden=True)
def test_plot_overlays(self):
step_builder, controls = self.load_assessment_scenario("step_builder_plot_overlays.xml", {})
# Step 1: Questions
# Provide first rating
self.answer_rating_question(1, 1, step_builder, "How much do you agree?", "1 - Disagree")
# Provide second rating
self.answer_rating_question(1, 2, step_builder, "How important do you think this is?", "5 - Very important")
# Advance
self.submit_and_go_to_next_step(controls)
# Step 2: Questions
# Provide first rating
self.answer_rating_question(2, 1, step_builder, "How much do you agree?", "5 - Agree")
# Provide second rating
self.answer_rating_question(2, 2, step_builder, "How important do you think this is?", "1 - Not important")
# Advance
self.submit_and_go_to_next_step(controls, last=True)
# Step 2: Plot
# Obtain references to plot controls
additional_plot_controls = self.additional_plot_controls(step_builder)
# Check button labels
self.check_button_label(additional_plot_controls.teacher_button, "Teacher")
self.check_button_label(additional_plot_controls.researchers_button, "Researchers")
self.check_button_label(additional_plot_controls.sheldon_button, "Sheldon Cooper")
self.check_button_label(additional_plot_controls.yoda_button, "Yoda")
# Overlay data
default_overlay = {
'selector': '.claim-default',
'num_points': 2,
'point_color': PointColors.ORANGE,
'titles': ['2 + 2 = 5: 1, 5', 'The answer to everything is 42: 5, 1'],
'positions': [
('4', '380'), # Values computed according to xScale and yScale (cf. plot.js)
('20', '396'), # Values computed according to xScale and yScale (cf. plot.js)
],
}
teacher_overlay = {
'selector': '.plot-overlay.plot-overlay-0',
'num_points': 2,
'point_color': PointColors.CORAL,
'titles': ['2 + 2 = 5: 2, 3', 'The answer to everything is 42: 4, 2'],
'positions': [
('8', '388'), # Values computed according to xScale and yScale (cf. plot.js)
('16', '392'), # Values computed according to xScale and yScale (cf. plot.js)
],
'plot_label': 'Teacher',
'plot_label_color': HTMLColors.CORAL,
'description': None,
'citation': None,
}
researchers_overlay = {
'selector': '.plot-overlay.plot-overlay-1',
'num_points': 2,
'point_color': PointColors.CORNFLOWERBLUE,
'titles': ['2 + 2 = 5: 4, 4', 'The answer to everything is 42: 1, 5'],
'positions': [
('16', '384'), # Values computed according to xScale and yScale (cf. plot.js)
('4', '380'), # Values computed according to xScale and yScale (cf. plot.js)
],
'plot_label': 'Researchers',
'plot_label_color': HTMLColors.CORNFLOWERBLUE,
'description': 'Responses of leading researchers in the field',
'citation': None,
}
sheldon_overlay = {
'selector': '.plot-overlay.plot-overlay-2',
'num_points': 2,
'point_color': PointColors.OLIVE,
'titles': ['2 + 2 = 5: 3, 5', 'The answer to everything is 42: 2, 4'],
'positions': [
('12', '380'), # Values computed according to xScale and yScale (cf. plot.js)
('8', '384'), # Values computed according to xScale and yScale (cf. plot.js)
],
'plot_label': 'Sheldon Cooper',
'plot_label_color': HTMLColors.OLIVE,
'description': None,
'citation': 'The Big Bang Theory',
}
yoda_overlay = {
'selector': '.plot-overlay.plot-overlay-3',
'num_points': 2,
'point_color': PointColors.CRIMSON,
'titles': ['2 + 2 = 5: 1, 2', 'The answer to everything is 42: 3, 3'],
'positions': [
('4', '392'), # Values computed according to xScale and yScale (cf. plot.js)
('12', '388'), # Values computed according to xScale and yScale (cf. plot.js)
],
'plot_label': 'Yoda',
'plot_label_color': HTMLColors.CRIMSON,
'description': 'Powerful you have become, the dark side I sense in you.',
'citation': 'Star Wars',
}
# Check if plot shows correct overlay(s) initially (default overlay on, additional overlays off)
self.check_overlays(
step_builder, 2, overlays=[default_overlay]
)
self.check_plot_info(
step_builder,
hidden=True,
visible_overlays=[],
hidden_overlays=[teacher_overlay, researchers_overlay, sheldon_overlay, yoda_overlay]
)
# Turn on additional overlays one by one.
# - Check if plot shows correct overlay(s)
# - Check if block displays correct info about plot
# "Teacher" on
self.click_overlay_button(
additional_plot_controls.teacher_button, overlay_on=True, color_on=HTMLColors.CORAL
)
self.check_overlays(
step_builder, 4, overlays=[default_overlay, teacher_overlay]
)
self.check_plot_info(
step_builder,
hidden=True, # "Teacher" overlay has no description/citation,
# so plot info as a whole should stay hidden
visible_overlays=[teacher_overlay],
hidden_overlays=[researchers_overlay, sheldon_overlay, yoda_overlay]
)
# "Researchers" on
self.click_overlay_button(
additional_plot_controls.researchers_button, overlay_on=True, color_on=HTMLColors.CORNFLOWERBLUE
)
self.check_overlays(
step_builder, 6, overlays=[default_overlay, teacher_overlay, researchers_overlay]
)
self.check_plot_info(
step_builder,
hidden=False,
visible_overlays=[teacher_overlay, researchers_overlay],
hidden_overlays=[sheldon_overlay, yoda_overlay]
)
# "Sheldon Cooper" on
self.click_overlay_button(
additional_plot_controls.sheldon_button, overlay_on=True, color_on=HTMLColors.OLIVE
)
self.check_overlays(
step_builder, 8, overlays=[default_overlay, teacher_overlay, researchers_overlay, sheldon_overlay]
)
self.check_plot_info(
step_builder,
hidden=False,
visible_overlays=[teacher_overlay, researchers_overlay, sheldon_overlay],
hidden_overlays=[yoda_overlay]
)
# "Yoda" on
self.click_overlay_button(
additional_plot_controls.yoda_button, overlay_on=True, color_on=HTMLColors.CRIMSON
)
self.check_overlays(
step_builder,
10,
overlays=[default_overlay, teacher_overlay, researchers_overlay, sheldon_overlay, yoda_overlay]
)
self.check_plot_info(
step_builder,
hidden=False,
visible_overlays=[teacher_overlay, researchers_overlay, sheldon_overlay, yoda_overlay],
hidden_overlays=[]
)
# Turn off additional overlays one by one.
# - Check if plot shows correct overlay(s)
# - Check if block displays correct info about plot
# "Yoda" off
self.click_overlay_button(additional_plot_controls.yoda_button, overlay_on=False)
self.check_overlays(
step_builder, 8, overlays=[default_overlay, teacher_overlay, researchers_overlay, sheldon_overlay]
)
self.check_plot_info(
step_builder,
hidden=False,
visible_overlays=[teacher_overlay, researchers_overlay, sheldon_overlay],
hidden_overlays=[yoda_overlay]
)
# "Sheldon Cooper" off
self.click_overlay_button(additional_plot_controls.sheldon_button, overlay_on=False)
self.check_overlays(
step_builder, 6, overlays=[default_overlay, teacher_overlay, researchers_overlay]
)
self.check_plot_info(
step_builder,
hidden=False,
visible_overlays=[teacher_overlay, researchers_overlay],
hidden_overlays=[sheldon_overlay, yoda_overlay]
)
# "Researchers" off
self.click_overlay_button(additional_plot_controls.researchers_button, overlay_on=False)
self.check_overlays(
step_builder, 4, overlays=[default_overlay, teacher_overlay]
)
self.check_plot_info(
step_builder,
hidden=True, # "Teacher" overlay has no description/citation,
# so plot info should be hidden at this point
visible_overlays=[teacher_overlay],
hidden_overlays=[researchers_overlay, sheldon_overlay, yoda_overlay]
)
# "Teacher" off
self.click_overlay_button(additional_plot_controls.teacher_button, overlay_on=False)
self.check_overlays(
step_builder, 2, overlays=[default_overlay]
)
self.check_plot_info(
step_builder,
hidden=True,
visible_overlays=[],
hidden_overlays=[teacher_overlay, researchers_overlay, sheldon_overlay, yoda_overlay]
)
# When deactivating an overlay that has no description/citation,
# visibility of information about remaining overlays that are currently active
# should not be affected:
# "Yoda" on:
self.click_overlay_button(
additional_plot_controls.yoda_button, overlay_on=True, color_on=HTMLColors.CRIMSON
)
self.check_overlays(
step_builder, 4, overlays=[default_overlay, yoda_overlay]
)
self.check_plot_info(
step_builder,
hidden=False, # Plot info becomes visible
visible_overlays=[yoda_overlay],
hidden_overlays=[teacher_overlay, researchers_overlay, sheldon_overlay]
)
# "Teacher" on:
self.click_overlay_button(
additional_plot_controls.teacher_button, overlay_on=True, color_on=HTMLColors.CORAL
)
self.check_overlays(
step_builder, 6, overlays=[default_overlay, yoda_overlay, teacher_overlay]
)
self.check_plot_info(
step_builder,
hidden=False,
visible_overlays=[yoda_overlay, teacher_overlay],
hidden_overlays=[researchers_overlay, sheldon_overlay]
)
# "Teacher" off:
self.click_overlay_button(additional_plot_controls.teacher_button, overlay_on=False)
self.check_overlays(
step_builder, 4, overlays=[default_overlay, yoda_overlay]
)
self.check_plot_info(
step_builder,
hidden=False, # Plot info stays visible
visible_overlays=[yoda_overlay],
hidden_overlays=[teacher_overlay, researchers_overlay, sheldon_overlay],
)
# "Yoda" off:
self.click_overlay_button(additional_plot_controls.yoda_button, overlay_on=False)
self.check_overlays(
step_builder, 2, overlays=[default_overlay]
)
self.check_plot_info(
step_builder,
hidden=True, # Last remaining overlay with description/citation deactivated;
# plot info now hidden
visible_overlays=[],
hidden_overlays=[teacher_overlay, researchers_overlay, sheldon_overlay, yoda_overlay]
)
<step-builder url_name="step-builder" display_name="Step Builder">
<sb-step display_name="First step">
<pb-rating name="rating_1_1"
low="Disagree"
high="Agree"
question="How much do you agree?"
correct_choices='["1", "2", "3", "4","5"]'>
</pb-rating>
<pb-rating name="rating_1_2"
low="Not important"
high="Very important"
question="How important do you think this is?"
correct_choices='["1", "2", "3", "4","5"]'>
</pb-rating>
</sb-step>
<sb-step display_name="Second step">
<pb-rating name="rating_2_1"
low="Disagree"
high="Agree"
question="How much do you agree?"
correct_choices='["1", "2", "3", "4","5"]'>
</pb-rating>
<pb-rating name="rating_2_2"
low="Not important"
high="Very important"
question="How important do you think this is?"
correct_choices='["1", "2", "3", "4","5"]'>
</pb-rating>
</sb-step>
<sb-step display_name="Last step">
<sb-plot plot_label="Custom plot label"
point_color_default="orange"
point_color_average="purple"
q1_label="Custom Q1 label"
q2_label="Custom Q2 label"
q3_label="Custom Q3 label"
q4_label="Custom Q4 label"
claims="2 + 2 = 5, rating_1_1, rating_1_2&#10;The answer to everything is 42, rating_2_1, rating_2_2">
<sb-plot-overlay plot_label="Teacher"
point_color="coral"
claim_data="2, 3&#10;4, 2">
</sb-plot-overlay>
<sb-plot-overlay plot_label="Researchers"
point_color="cornflowerblue"
description="Responses of leading researchers in the field"
claim_data="4, 4&#10;1, 5">
</sb-plot-overlay>
<sb-plot-overlay plot_label="Sheldon Cooper"
point_color="rgb(128, 128, 0)"
citation="The Big Bang Theory"
claim_data="3, 5&#10;2, 4">
</sb-plot-overlay>
<sb-plot-overlay plot_label="Yoda"
point_color="#dc143c"
description="Powerful you have become, the dark side I sense in you."
citation="Star Wars"
claim_data="1, 2&#10;3, 3">
</sb-plot-overlay>
</sb-plot>
</sb-step>
<sb-review-step></sb-review-step>
</step-builder>
...@@ -46,6 +46,7 @@ BLOCKS = [ ...@@ -46,6 +46,7 @@ BLOCKS = [
'sb-review-step = problem_builder.step:ReviewStepBlock', 'sb-review-step = problem_builder.step:ReviewStepBlock',
'sb-plot = problem_builder.plot:PlotBlock', 'sb-plot = problem_builder.plot:PlotBlock',
'sb-plot-overlay = problem_builder.plot:PlotOverlayBlock',
'pb-table = problem_builder.table:MentoringTableBlock', 'pb-table = problem_builder.table:MentoringTableBlock',
'pb-column = problem_builder.table:MentoringTableColumn', 'pb-column = problem_builder.table:MentoringTableColumn',
......
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