diff --git a/problem_builder/mcq.py b/problem_builder/mcq.py index c2f935cf..3234bf4f 100644 --- a/problem_builder/mcq.py +++ b/problem_builder/mcq.py @@ -185,7 +185,7 @@ class RatingBlock(MCQBlock): list_values_provider=QuestionnaireAbstractBlock.choice_values_provider, list_style='set', # Underered, unique items. Affects the UI editor. ) - editable_fields = MCQBlock.editable_fields + ('low', 'high', 'name') + editable_fields = MCQBlock.editable_fields + ('low', 'high') @property def all_choice_values(self): @@ -212,3 +212,25 @@ def get_author_edit_view_fragment(self, context): })) self.render_children(context, fragment, can_reorder=True, can_add=False) return fragment + + @property + def url_name(self): + """ + Get the url_name for this block. In Studio/LMS it is provided by a mixin, so we just + defer to super(). In the workbench or any other platform, we use the name. + """ + try: + return super(RatingBlock, self).url_name + except AttributeError: + return self.name + + def student_view(self, context): + fragment = super(RatingBlock, self).student_view(context) + rendering_for_studio = None + if context: # Workbench does not provide context + rendering_for_studio = context.get('author_edit_view') + if rendering_for_studio: + fragment.add_content(loader.render_template('templates/html/rating_edit_footer.html', { + "url_name": self.url_name + })) + return fragment diff --git a/problem_builder/plot.py b/problem_builder/plot.py index d332c3a1..e7aa1ea9 100644 --- a/problem_builder/plot.py +++ b/problem_builder/plot.py @@ -59,7 +59,7 @@ def _normalize_id(key): @XBlock.wants('user') class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, XBlock): """ - XBlock that displays plot that summarizes answers to scale questions. + XBlock that displays plot that summarizes answers to scale and/or rating questions. """ CATEGORY = 'sb-plot' @@ -139,7 +139,7 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin 'Claims and questions that should be included in the plot. ' 'Each line defines a triple of the form "claim, q1, q2", ' 'where "claim" is arbitrary text that represents a claim, ' - 'and "q1" and "q2" are IDs of scale questions. ' + 'and "q1" and "q2" are IDs of scale or rating questions. ' ), default="", multiline_editor=True, @@ -175,9 +175,9 @@ def _get_claims(self, response_function): claim, q1, q2 = line.split(', ') r1, r2 = None, None for question_id, question in zip(question_ids, questions): - if question.name == q1: + if question.url_name == q1: r1 = response_function(question, question_id) - if question.name == q2: + if question.url_name == q2: r2 = response_function(question, question_id) if r1 is not None and r2 is not None: break diff --git a/problem_builder/slider.py b/problem_builder/slider.py index 4097d768..99e0d72b 100644 --- a/problem_builder/slider.py +++ b/problem_builder/slider.py @@ -87,6 +87,17 @@ class SliderBlock( editable_fields = ('min_label', 'max_label', 'display_name', 'question', 'show_title') + @property + def url_name(self): + """ + Get the url_name for this block. In Studio/LMS it is provided by a mixin, so we just + defer to super(). In the workbench or any other platform, we use the name. + """ + try: + return super(SliderBlock, self).url_name + except AttributeError: + return self.name + def mentoring_view(self, context): """ Main view of this block """ context = context.copy() if context else {} @@ -145,11 +156,17 @@ def submit(self, value): self.student_value = value if sub_api: # Also send to the submissions API: - sub_api.create_submission(self.student_item_key, {'value': value}) + sub_api.create_submission(self.student_item_key, value) result = self.get_last_result() log.debug(u'Slider submission result: %s', result) return result + def get_submission_display(self, submission): + """ + Get the human-readable version of a submission value + """ + return submission * 100 + def validate_field_data(self, validation, data): """ Validate this block's field data. diff --git a/problem_builder/step.py b/problem_builder/step.py index 9372f63b..ecad83a2 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -217,6 +217,7 @@ def author_edit_view(self, context): 'head': u'
', 'tail': u'
' } + local_context['author_edit_view'] = True fragment = super(MentoringStepBlock, self).author_edit_view(local_context) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css')) diff --git a/problem_builder/templates/html/rating_edit_footer.html b/problem_builder/templates/html/rating_edit_footer.html new file mode 100644 index 00000000..11b3a119 --- /dev/null +++ b/problem_builder/templates/html/rating_edit_footer.html @@ -0,0 +1,5 @@ +{% load i18n %} + diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index e912f80a..6f038674 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -14,8 +14,23 @@ def get_all_submissions(self, course_key_str, block_id, block_type): ) +class MultipleSliderBlocksTestMixins(): + """ Mixins for testing slider blocks. Allows multiple slider blocks on the page. """ + + def get_slider_value(self, slider_number): + print('SLIDER NUMBER: {}'.format(slider_number)) + return int( + self.browser.execute_script("return $('.pb-slider-range').eq(arguments[0]).val()", slider_number-1) + ) + + def set_slider_value(self, slider_number, val): + self.browser.execute_script( + "$('.pb-slider-range').eq(arguments[0]).val(arguments[1]).change()", slider_number-1, val + ) + + @ddt -class StepBuilderTest(MentoringAssessmentBaseTest): +class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixins): def setUp(self): super(StepBuilderTest, self).setUp() @@ -34,6 +49,10 @@ def setUp(self): "problem_builder.mcq.sub_api", mock_submissions_api ), + ( + "problem_builder.slider.sub_api", + mock_submissions_api + ), ( "problem_builder.sub_api.SubmittingXBlockMixin.student_item_key", property( @@ -41,7 +60,7 @@ def setUp(self): student_id="student_id", course_id="course_id", item_id=block.scope_ids.usage_id, - item_type="pb-rating" + item_type=block.scope_ids.block_type ) ) ), @@ -725,8 +744,8 @@ def test_plot(self): 'point_color': 'rgb(255, 165, 0)', # 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) + ('20', '396'), # Values computed according to xScale and yScale (cf. plot.js) ], } average_overlay = { @@ -735,8 +754,85 @@ def test_plot(self): 'point_color': 'rgb(128, 0, 128)', # 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) ('4', '380'), # Values computed according to xScale and yScale (cf. plot.js) + ('20', '396'), # Values computed according to xScale and yScale (cf. plot.js) + ], + } + # Check if plot shows correct overlay(s) initially (default overlay on, average overlay off) + 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.check_overlays(step_builder, 4, overlays=[default_overlay, average_overlay]) + + # Check if plot shows correct overlay(s) (default overlay off, average overlay on) + self.click_default_button(plot_controls, overlay_on=False) + self.check_overlays(step_builder, 2, overlays=[average_overlay]) + + # Check if plot shows correct overlay(s) (default overlay off, average overlay off) + self.click_average_button(plot_controls, overlay_on=False) + 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.check_overlays(step_builder, 2, overlays=[default_overlay]) + + # Check quadrant labels + self.check_quadrant_labels(step_builder, plot_controls, hidden=True) + plot_controls.quadrants_button.click() + self.check_quadrant_labels( + step_builder, plot_controls, hidden=False, + labels=['Custom Q1 label', 'Custom Q2 label', 'Custom Q3 label', 'Custom Q4 label'] + ) + + def answer_scale_question(self, question_number, step_builder, question, value): + self.assertEqual(self.get_slider_value(question_number), 50) + question_text = self.question_text(question_number) + self.wait_until_text_in(question_text, step_builder) + self.assertIn(question, step_builder.text) + self.set_slider_value(question_number, value) + + def test_plot_with_scale_questions(self): + step_builder, controls = self.load_assessment_scenario("step_builder_plot_scale_questions.xml", {}) + + # Step 1: Questions + # Provide first rating + self.answer_scale_question(1, step_builder, "How much do you agree?", 17) + # Provide second rating + self.answer_scale_question(2, step_builder, "How important do you think this is?", 83) + # 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 + plot_controls = self.plot_controls(step_builder) + # Overlay data + default_overlay = { + 'selector': '.claim-default', + 'num_points': 2, + 'point_color': 'rgb(255, 165, 0)', # orange + 'titles': ['2 + 2 = 5: 17, 83', 'The answer to everything is 42: 5, 1'], + 'positions': [ + ('68', '68'), # Values computed according to xScale and yScale (cf. plot.js) + ('20', '396'), # Values computed according to xScale and yScale (cf. plot.js) + ], + } + average_overlay = { + 'selector': '.claim-average', + 'num_points': 2, + 'point_color': 'rgb(128, 0, 128)', # purple + 'titles': ['2 + 2 = 5: 17, 83', 'The answer to everything is 42: 5, 1'], + 'positions': [ + ('68', '68'), # Values computed according to xScale and yScale (cf. plot.js) + ('20', '396'), # Values computed according to xScale and yScale (cf. plot.js) ], } # Check if plot shows correct overlay(s) initially (default overlay on, average overlay off) diff --git a/problem_builder/tests/integration/xml_templates/step_builder_plot_scale_questions.xml b/problem_builder/tests/integration/xml_templates/step_builder_plot_scale_questions.xml new file mode 100644 index 00000000..3b38608a --- /dev/null +++ b/problem_builder/tests/integration/xml_templates/step_builder_plot_scale_questions.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + +