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>
<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