From 769192e7b57b043ebd3c9d5449133fea0fc2b842 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 21 Oct 2015 18:26:09 +0200 Subject: [PATCH 1/3] Add support for additional overlays for plot blocks. --- problem_builder/plot.py | 201 +++++++++++++++++- problem_builder/public/css/overlay.css | 7 + problem_builder/public/css/plot-preview.css | 21 ++ problem_builder/public/css/plot.css | 18 ++ .../public/css/problem-builder-edit.css | 3 + problem_builder/public/js/plot.js | 78 ++++++- problem_builder/public/js/plot_edit.js | 5 + problem_builder/templates/html/overlay.html | 30 +++ problem_builder/templates/html/plot.html | 34 +++ .../templates/html/plot_preview.html | 27 +++ setup.py | 1 + 11 files changed, 417 insertions(+), 8 deletions(-) create mode 100644 problem_builder/public/css/overlay.css create mode 100644 problem_builder/public/css/plot-preview.css create mode 100644 problem_builder/public/js/plot_edit.js create mode 100644 problem_builder/templates/html/overlay.html create mode 100644 problem_builder/templates/html/plot_preview.html diff --git a/problem_builder/plot.py b/problem_builder/plot.py index d332c3a1..b3fd3c27 100644 --- a/problem_builder/plot.py +++ b/problem_builder/plot.py @@ -26,6 +26,8 @@ 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 @@ def get_data(self, data, suffix): '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( - u"

{}

".format( - _(u"This block displays a plot that summarizes answers to scale questions.") - ) - ) + 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"

{}

".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): return self.student_view(context) @@ -266,3 +345,115 @@ def student_view(self, context=None): 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'
', + 'tail': u'
' + } + 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 question, ' + 'and "q2" is the value associated with the second scale 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 diff --git a/problem_builder/public/css/overlay.css b/problem_builder/public/css/overlay.css new file mode 100644 index 00000000..5e584848 --- /dev/null +++ b/problem_builder/public/css/overlay.css @@ -0,0 +1,7 @@ +.sb-plot-overlay { + margin-bottom: 10px; +} + +.italic { + font-style: italic; +} diff --git a/problem_builder/public/css/plot-preview.css b/problem_builder/public/css/plot-preview.css new file mode 100644 index 00000000..e636f276 --- /dev/null +++ b/problem_builder/public/css/plot-preview.css @@ -0,0 +1,21 @@ +.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; +} diff --git a/problem_builder/public/css/plot.css b/problem_builder/public/css/plot.css index 569d0c78..674d1d5b 100644 --- a/problem_builder/public/css/plot.css +++ b/problem_builder/public/css/plot.css @@ -1,11 +1,29 @@ +.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; +} diff --git a/problem_builder/public/css/problem-builder-edit.css b/problem_builder/public/css/problem-builder-edit.css index c1a0b3a3..e78f6103 100644 --- a/problem_builder/public/css/problem-builder-edit.css +++ b/problem_builder/public/css/problem-builder-edit.css @@ -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, diff --git a/problem_builder/public/js/plot.js b/problem_builder/public/js/plot.js index 0ee66e18..e38aeb9d 100644 --- a/problem_builder/public/js/plot.js +++ b/problem_builder/public/js/plot.js @@ -1,5 +1,23 @@ 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.is(':visible')) { + 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,31 @@ 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; diff --git a/problem_builder/public/js/plot_edit.js b/problem_builder/public/js/plot_edit.js new file mode 100644 index 00000000..74e56904 --- /dev/null +++ b/problem_builder/public/js/plot_edit.js @@ -0,0 +1,5 @@ +function PlotEdit(runtime, element) { + 'use strict'; + StudioContainerXBlockWithNestedXBlocksMixin(runtime, element); + ProblemBuilderUtil.transformClarifications(element); +} diff --git a/problem_builder/templates/html/overlay.html b/problem_builder/templates/html/overlay.html new file mode 100644 index 00000000..36af9623 --- /dev/null +++ b/problem_builder/templates/html/overlay.html @@ -0,0 +1,30 @@ +{% load i18n %} +
+ {% if self.plot_label and self.point_color %} +

{{ self.plot_label }} {% trans "Overlay" %}

+ {% endif %} +

+ {% trans "Description:" %} + {% if self.description %} + {{ self.description }} + {% else %} + {% trans "No description provided" %} + {% endif %} +

+

+ {% trans "Source:" %} + {% if self.citation %} + {{ self.citation }} + {% else %} + {% trans "No citation provided" %} + {% endif %} +

+

+ {% trans "Data:" %} + {% if self.claim_data %} + {{ self.claim_data }} + {% else %} + {% trans "No data provided" %} + {% endif %} +

+
diff --git a/problem_builder/templates/html/plot.html b/problem_builder/templates/html/plot.html index c1bb7b36..a6cabfa7 100644 --- a/problem_builder/templates/html/plot.html +++ b/problem_builder/templates/html/plot.html @@ -30,6 +30,40 @@

Compare your plot to others!

data-overlay-on="false" value="Average" /> + + {% for overlay in self.overlay_data %} + + + + {% endfor %} + +
+

Plot info

+ {% for overlay in self.overlay_data %} +
+ {% if overlay.description or overlay.citation %} +

{{ overlay.plot_label }}

+ {% if overlay.description %} +

+ Description: {{ overlay.description }} +

+ {% endif %} + {% if overlay.citation %} +

+ Source: {{ overlay.citation }} +

+ {% endif %} + {% endif %} +
+ {% endfor %} +
+ diff --git a/problem_builder/templates/html/plot_preview.html b/problem_builder/templates/html/plot_preview.html new file mode 100644 index 00000000..908263d7 --- /dev/null +++ b/problem_builder/templates/html/plot_preview.html @@ -0,0 +1,27 @@ +{% load i18n %} +
+

{{ self.display_name }}

+ {% if self.claims %} +

{% trans "This block displays a plot that summarizes responses to the following claims:" %}

+ + + + + + + + + + {% for claim in self.claims_display %} + + + + + + {% endfor %} + +
{% trans "Claim" %}{% trans "Question 1" %}{% trans "Question 2" %}
{{ claim.0 }}{{ claim.1 }}{{ claim.2 }}
+ {% else %} +

{% trans "This block displays a plot that summarizes responses to a set of claims." %}

+ {% endif %} +
diff --git a/setup.py b/setup.py index 80fd6be8..b5e90a9d 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ def package_data(pkg, root_list): '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', From 4ee4532bccdd7f67a8d0f44b4aa3c2ec3d320ba7 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Thu, 22 Oct 2015 15:13:11 +0200 Subject: [PATCH 2/3] Add tests. --- problem_builder/public/js/plot.js | 2 +- problem_builder/templates/html/plot.html | 6 +- .../tests/integration/test_step_builder.py | 426 ++++++++++++++++-- .../step_builder_plot_overlays.xml | 67 +++ 4 files changed, 462 insertions(+), 39 deletions(-) create mode 100644 problem_builder/tests/integration/xml_templates/step_builder_plot_overlays.xml diff --git a/problem_builder/public/js/plot.js b/problem_builder/public/js/plot.js index e38aeb9d..de1169aa 100644 --- a/problem_builder/public/js/plot.js +++ b/problem_builder/public/js/plot.js @@ -171,7 +171,7 @@ function PlotBlock(runtime, element) { } } else { overlayInfo.show(); - if (!overlayInfo.isEmpty() && !plotInfo.is(':visible')) { + if (!overlayInfo.isEmpty() && !plotInfo.isVisible()) { plotInfo.show(); } } diff --git a/problem_builder/templates/html/plot.html b/problem_builder/templates/html/plot.html index a6cabfa7..fa01fcca 100644 --- a/problem_builder/templates/html/plot.html +++ b/problem_builder/templates/html/plot.html @@ -48,14 +48,14 @@

Compare your plot to others!

{% for overlay in self.overlay_data %}
{% if overlay.description or overlay.citation %} -

{{ overlay.plot_label }}

+

{{ overlay.plot_label }}

{% if overlay.description %} -

+

Description: {{ overlay.description }}

{% endif %} {% if overlay.citation %} -

+

Source: {{ overlay.citation }}

{% endif %} diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index e912f80a..ac962e4f 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -6,6 +6,28 @@ 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): def get_all_submissions(self, course_key_str, block_id, block_type): return ( @@ -587,6 +609,27 @@ class Namespace(object): 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): points = step_builder.find_elements_by_css_selector("circle") self.assertEquals(points, []) @@ -601,43 +644,33 @@ def check_quadrant_labels(self, step_builder, plot_controls, hidden, labels=['Q1 ] if hidden: self.assertEquals(quadrant_labels, []) - # rgba(255, 0, 0, 1): "red" - self.assertTrue(all(bc == 'rgba(255, 0, 0, 1)' for bc in quadrants_button_border_colors)) + self.assertTrue(all(bc == HTMLColors.RED for bc in quadrants_button_border_colors)) else: self.assertEquals(len(quadrant_labels), 4) self.assertEquals(set(label.text for label in quadrant_labels), set(labels)) - # rgba(0, 128, 0, 1): "green" - self.assertTrue(all(bc == 'rgba(0, 128, 0, 1)' for bc in quadrants_button_border_colors)) - - def click_default_button( - self, plot_controls, overlay_on, color_on='rgba(0, 128, 0, 1)', color_off='rgba(237, 237, 237, 1)' - ): - plot_controls.default_button.click() - default_button_border_colors = [ - plot_controls.default_button.value_of_css_property('border-top-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'), + self.assertTrue(all(bc == HTMLColors.GREEN for bc in quadrants_button_border_colors)) + + def click_overlay_button(self, overlay_button, overlay_on, color_on=None, color_off=HTMLColors.GREY): + overlay_button.click() + button_border_colors = [ + overlay_button.value_of_css_property('border-top-color'), + overlay_button.value_of_css_property('border-right-color'), + overlay_button.value_of_css_property('border-bottom-color'), + overlay_button.value_of_css_property('border-left-color'), ] 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: - 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( - self, plot_controls, overlay_on, color_on='rgba(0, 0, 255, 1)', color_off='rgba(237, 237, 237, 1)' - ): - plot_controls.average_button.click() - average_button_border_colors = [ - plot_controls.average_button.value_of_css_property('border-top-color'), - plot_controls.average_button.value_of_css_property('border-right-color'), - plot_controls.average_button.value_of_css_property('border-bottom-color'), - 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 click_default_button(self, plot_controls, overlay_on, color_on=HTMLColors.GREEN): + self.click_overlay_button(plot_controls.default_button, overlay_on, color_on) + + def click_average_button(self, plot_controls, overlay_on, color_on=HTMLColors.BLUE): + self.click_overlay_button(plot_controls.average_button, overlay_on, color_on) + + def check_button_label(self, button, expected_value): + self.assertEquals(button.get_attribute('value'), expected_value) def test_empty_plot(self): step_builder, controls = self.load_assessment_scenario("step_builder_plot_defaults.xml", {}) @@ -655,6 +688,9 @@ def test_empty_plot(self): self.plot_empty(step_builder) # Obtain references to plot controls 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) self.click_default_button(plot_controls, overlay_on=False) self.plot_empty(step_builder) @@ -678,7 +714,8 @@ def check_overlays(self, step_builder, total_num_points, overlays): for overlay in overlays: # 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']) # Check point colors point_colors = [ @@ -718,11 +755,14 @@ def test_plot(self): # Step 2: Plot # Obtain references to plot controls 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 default_overlay = { 'selector': '.claim-default', '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'], 'positions': [ ('20', '396'), # Values computed according to xScale and yScale (cf. plot.js) @@ -732,7 +772,7 @@ def test_plot(self): average_overlay = { 'selector': '.claim-average', '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'], 'positions': [ ('20', '396'), # Values computed according to xScale and yScale (cf. plot.js) @@ -743,7 +783,7 @@ def test_plot(self): 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) - 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]) # Check if plot shows correct overlay(s) (default overlay off, average overlay on) @@ -755,7 +795,7 @@ def test_plot(self): self.plot_empty(step_builder) # 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]) # Check quadrant labels @@ -765,3 +805,319 @@ def test_plot(self): step_builder, plot_controls, hidden=False, 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': [ + ('20', '396'), # Values computed according to xScale and yScale (cf. plot.js) + ('4', '380'), # 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] + ) diff --git a/problem_builder/tests/integration/xml_templates/step_builder_plot_overlays.xml b/problem_builder/tests/integration/xml_templates/step_builder_plot_overlays.xml new file mode 100644 index 00000000..1ce76e6e --- /dev/null +++ b/problem_builder/tests/integration/xml_templates/step_builder_plot_overlays.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 8af9fa2be82638eecf07fa399e9c5656766d03b3 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Fri, 23 Oct 2015 12:22:00 +0200 Subject: [PATCH 3/3] Minor clean up and i18n. --- problem_builder/plot.py | 4 ++-- problem_builder/public/js/plot.js | 1 - problem_builder/templates/html/plot.html | 11 ++++++----- .../tests/integration/test_step_builder.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/problem_builder/plot.py b/problem_builder/plot.py index b3fd3c27..dac55170 100644 --- a/problem_builder/plot.py +++ b/problem_builder/plot.py @@ -411,8 +411,8 @@ class PlotOverlayBlock(StudioEditableXBlockMixin, XBlockWithPreviewMixin, XBlock 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 question, ' - 'and "q2" is the value associated with the second scale question. ' + '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="", diff --git a/problem_builder/public/js/plot.js b/problem_builder/public/js/plot.js index de1169aa..4e042815 100644 --- a/problem_builder/public/js/plot.js +++ b/problem_builder/public/js/plot.js @@ -241,7 +241,6 @@ function PlotBlock(runtime, element) { }); // Hide overlay info initially - toggleOverlayInfo(klass, 'hide'); }); diff --git a/problem_builder/templates/html/plot.html b/problem_builder/templates/html/plot.html index fa01fcca..89ebe16b 100644 --- a/problem_builder/templates/html/plot.html +++ b/problem_builder/templates/html/plot.html @@ -1,8 +1,9 @@ +{% load i18n %}