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
from xblock.core import XBlock
from xblock.fields import String, Scope
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import (
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin
......@@ -244,12 +246,89 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
'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):
return Fragment(
context['self'] = self
fragment = Fragment()
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"This block displays a plot that summarizes answers to scale questions.")
)
)
_(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):
return self.student_view(context)
......@@ -266,3 +345,115 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/plot.js'))
fragment.initialize_js('PlotBlock')
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 {
font-weight: bold;
}
.overlays {
float: right;
width: 40%;
}
.overlays input {
margin-top: 10px;
margin-right: 5px;
}
.quadrants input, .overlays input {
background-color: rgb(204, 204, 204);
}
.plot-info {
margin-top: 15px;
}
.plot-info-header {
font-weight: bold;
}
......@@ -15,6 +15,7 @@
}
/* 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-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,
......@@ -25,6 +26,8 @@
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:hover,
.xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
......
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
// Define margins
......@@ -58,7 +76,8 @@ function PlotBlock(runtime, element) {
var defaultButton = $('.plot-default', element),
averageButton = $('.plot-average', element),
quadrantsButton = $('.plot-quadrants', element);
quadrantsButton = $('.plot-quadrants', element),
overlayButtons = $('input.plot-overlay', element);
// Claims
......@@ -80,7 +99,7 @@ function PlotBlock(runtime, element) {
// Event handlers
function toggleOverlay(claims, color, klass, refresh) {
var selector = "." + klass,
var selector = buildSelector(klass),
selection = svgContainer.selectAll(selector);
if (selection.empty()) {
showOverlay(selection, claims, color, klass);
......@@ -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) {
selection
.data(claims)
......@@ -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() {
var selection = svgContainer.selectAll(".quadrant-label"),
quadrantLabelsOn = quadrantsButton.val() === 'On';
......@@ -168,7 +218,7 @@ function PlotBlock(runtime, element) {
toggleBorderColor(this, defaultColor, refresh);
});
averageButton.on('click', function(event) {
averageButton.on('click', function() {
toggleOverlay(averageClaims, averageColor, 'claim-average');
toggleBorderColor(this, averageColor);
});
......@@ -177,9 +227,30 @@ function PlotBlock(runtime, element) {
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
quadrantsButton.css("border-color", "red");
// Hide plot info initially
$('.plot-info', element).hide();
// API
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="quadrants">
<label>
Quadrant labels
{% trans "Quadrant labels" %}
<input type="button"
class="plot-quadrants"
data-q1-label="{{ self.q1_label }}"
......@@ -14,7 +15,7 @@
</div>
<div class="overlays">
<h3>Compare your plot to others!</h3>
<h3>{% trans "Compare your plot to others!" %}</h3>
<input type="button"
class="plot-default"
......@@ -30,6 +31,40 @@
data-overlay-on="false"
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>
{% 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 = [
'sb-review-step = problem_builder.step:ReviewStepBlock',
'sb-plot = problem_builder.plot:PlotBlock',
'sb-plot-overlay = problem_builder.plot:PlotOverlayBlock',
'pb-table = problem_builder.table:MentoringTableBlock',
'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