From 1b66d3cd7413256d90b5c3e48e90e1d35596137e Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Sun, 29 Mar 2015 14:16:24 -0700 Subject: [PATCH 1/8] New show_title and custom title options --- problem_builder/answer.py | 9 +- problem_builder/mrq.py | 5 +- problem_builder/questionnaire.py | 9 +- problem_builder/step.py | 33 ++++- .../tests/integration/test_titles.py | 126 ++++++++++++++++++ 5 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 problem_builder/tests/integration/test_titles.py diff --git a/problem_builder/answer.py b/problem_builder/answer.py index c4584886..f09016d5 100644 --- a/problem_builder/answer.py +++ b/problem_builder/answer.py @@ -141,13 +141,7 @@ class AnswerBlock(AnswerMixin, StepMixin, StudioEditableXBlockMixin, XBlock): enforce_type=True ) - editable_fields = ('question', 'name', 'min_characters', 'weight', 'default_from') - - @property - def display_name_with_default(self): - if not self.lonely_step: - return self._(u"Question {number}").format(number=self.step_number) - return self._(u"Question") + editable_fields = ('question', 'name', 'min_characters', 'weight', 'default_from', 'display_name', 'show_title') @lazy def student_input(self): @@ -172,6 +166,7 @@ def mentoring_view(self, context=None): """ Render this XBlock within a mentoring block. """ context = context or {} context['self'] = self + context['hide_header'] = context.get('hide_header', False) or not self.show_title html = loader.render_template('templates/html/answer_editable.html', context) fragment = Fragment(html) diff --git a/problem_builder/mrq.py b/problem_builder/mrq.py index 44ff13c4..0fec59ca 100644 --- a/problem_builder/mrq.py +++ b/problem_builder/mrq.py @@ -69,7 +69,10 @@ class MRQBlock(QuestionnaireAbstractBlock): default=[], ) hide_results = Boolean(display_name="Hide results", scope=Scope.content, default=False) - editable_fields = ('question', 'required_choices', 'ignored_choices', 'message', 'weight', 'hide_results', ) + editable_fields = ( + 'question', 'required_choices', 'ignored_choices', 'message', 'display_name', + 'show_title', 'weight', 'hide_results', + ) def describe_choice_correctness(self, choice_value): if choice_value in self.required_choices: diff --git a/problem_builder/questionnaire.py b/problem_builder/questionnaire.py index 34feed39..f4f36f84 100644 --- a/problem_builder/questionnaire.py +++ b/problem_builder/questionnaire.py @@ -81,7 +81,7 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc scope=Scope.content, enforce_type=True ) - editable_fields = ('question', 'message', 'weight') + editable_fields = ('question', 'message', 'weight', 'display_name', 'show_title') has_children = True def _(self, text): @@ -113,12 +113,6 @@ def parse_xml(cls, node, runtime, keys, id_generator): return block - @property - def display_name_with_default(self): - if not self.lonely_step: - return self._(u"Question {number}").format(number=self.step_number) - return self._(u"Question") - def student_view(self, context=None): name = getattr(self, "unmixed_class", self.__class__).__name__ @@ -127,6 +121,7 @@ def student_view(self, context=None): context = context or {} context['self'] = self context['custom_choices'] = self.custom_choices + context['hide_header'] = context.get('hide_header', False) or not self.show_title fragment = Fragment(loader.render_template(template_path, context)) # If we use local_resource_url(self, ...) the runtime may insert many identical copies diff --git a/problem_builder/step.py b/problem_builder/step.py index 49ff2f77..1b386d97 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -19,9 +19,15 @@ # from lazy import lazy +from xblock.fields import String, Boolean, Scope from xblockutils.helpers import child_isinstance +# Make '_' a no-op so we can scrape strings +def _(text): + return text + + def _normalize_id(key): """ Helper method to normalize a key to avoid issues where some keys have version/branch and others don't. @@ -49,10 +55,26 @@ def steps(self): class StepMixin(object): """ - An XBlock mixin for a child block that is a "Step" + An XBlock mixin for a child block that is a "Step". + + A step is a question that the user can answer (as opposed to a read-only child). """ has_author_view = True + # Fields: + display_name = String( + display_name=_("Question title"), + help=_('Leave blank to use the default ("Question 1", "Question 2", etc.)'), + default="", # Blank will use 'Question x' - see display_name_with_default + scope=Scope.content + ) + show_title = Boolean( + display_name=_("Show title"), + help=_("Display the title?"), + default=True, + scope=Scope.content + ) + @lazy def step_number(self): return list(self.get_parent().steps).index(_normalize_id(self.scope_ids.usage_id)) + 1 @@ -63,6 +85,15 @@ def lonely_step(self): raise ValueError("Step's parent should contain Step", self, self.get_parent().steps) return len(self.get_parent().steps) == 1 + @property + def display_name_with_default(self): + """ Get the title/display_name of this question. """ + if self.display_name: + return self.display_name + if not self.lonely_step: + return self._(u"Question {number}").format(number=self.step_number) + return self._(u"Question") + def author_view(self, context): context = context or {} context['hide_header'] = True diff --git a/problem_builder/tests/integration/test_titles.py b/problem_builder/tests/integration/test_titles.py new file mode 100644 index 00000000..498d6a73 --- /dev/null +++ b/problem_builder/tests/integration/test_titles.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014-2015 Harvard, edX & OpenCraft +# +# This software's license gives you freedom; you can copy, convey, +# propagate, redistribute and/or modify this program under the terms of +# the GNU Affero General Public License (AGPL) as published by the Free +# Software Foundation (FSF), either version 3 of the License, or (at your +# option) any later version of the AGPL published by the FSF. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program in a file in the toplevel directory called +# "AGPLv3". If not, see . +# +""" +Test that the various title/display_name options for Answer and MCQ/MRQ/Ratings work. +""" + +# Imports ########################################################### +from mock import patch +from xblockutils.base_test import SeleniumXBlockTest + + +# Classes ########################################################### + + +class StepTitlesTest(SeleniumXBlockTest): + """ + Test that the various title/display_name options for Answer and MCQ/MRQ/Ratings work. + """ + + test_parameters = ( + # display_name, show_title?, expected_title: (None means default value) + ("Custom Title", None, "Custom Title",), + ("Custom Title", True, "Custom Title",), + ("Custom Title", False, None), + ("", None, "Question"), + ("", True, "Question"), + ("", False, None), + ) + + mcq_template = """ + + + Gaius Baltar + Admiral William Adama + Starbuck + Laura Roslin + Number Six + Lee Adama + + + """ + + mrq_template = """ + + + Lots of choices + Funny choices + Not sure + + + """ + + rating_template = """ + + + More than 5 stars + + + """ + + long_answer_template = """ + + + + """ + + def setUp(self): + super(StepTitlesTest, self).setUp() + # Disable asides for this test since the acid aside seems to cause Database errors + # When we test multiple scenarios in one test method. + patcher = patch( + 'workbench.runtime.WorkbenchRuntime.applicable_aside_types', + lambda self, block: [], create=True + ) + patcher.start() + self.addCleanup(patcher.stop) + + def test_all_the_things(self): + """ Test various permutations of our problem-builder components and title options. """ + # We use a loop within the test rather than DDT, because this is WAY faster + # since we can bypass the Selenium set-up and teardown + for display_name, show_title, expected_title in self.test_parameters: + for mode in ("standard", "assessment"): + for qtype in ("mcq", "mrq", "rating", "long_answer"): + template = getattr(self, qtype + "_template") + xml = template.format( + mode=mode, + display_name_attr='display_name="{}"'.format(display_name) if display_name is not None else "", + show_title_attr='show_title="{}"'.format(show_title) if show_title is not None else "", + ) + self.set_scenario_xml(xml) + pb_element = self.go_to_view() + if expected_title: + h3 = pb_element.find_element_by_css_selector('h3') + self.assertEqual(h3.text, expected_title) + else: + # No

element should be present: + all_h3s = pb_element.find_elements_by_css_selector('h3') + self.assertEqual(len(all_h3s), 0) From 18243ce25190caffc86ac936556f06e1d200f7dd Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Sat, 28 Mar 2015 15:52:31 -0700 Subject: [PATCH 2/8] Fwd port fix to OC-545 (Issues in scrollbars in Feedback pop up for large data) --- problem_builder/public/css/questionnaire.css | 4 ++-- problem_builder/public/js/questionnaire.js | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/problem_builder/public/css/questionnaire.css b/problem_builder/public/css/questionnaire.css index c56344d4..fafa07f0 100644 --- a/problem_builder/public/css/questionnaire.css +++ b/problem_builder/public/css/questionnaire.css @@ -32,9 +32,8 @@ background: none repeat scroll 0 0 #66A5B5; font-family: arial; font-size: 14px; - overflow-y: auto; opacity: 0.9; - padding: 10px; + padding: 22px 10px; width: 300px; } @@ -48,6 +47,7 @@ .mentoring .questionnaire .feedback .tip-choice-group, .mentoring .questionnaire .feedback .message-content { position: relative; + overflow-y: auto; } .mentoring .questionnaire .choice-tips .close, diff --git a/problem_builder/public/js/questionnaire.js b/problem_builder/public/js/questionnaire.js index 033a2b3d..901dda31 100644 --- a/problem_builder/public/js/questionnaire.js +++ b/problem_builder/public/js/questionnaire.js @@ -20,15 +20,21 @@ function MessageView(element, mentoring) { var data = $(tip).data(); if (data && data.width) { popupDOM.css('width', data.width); + popupDOM.find('.tip-choice-group').css('width', data.width); } else { popupDOM.css('width', ''); + popupDOM.find('.tip-choice-group').css('width', ''); } if (data && data.height) { popupDOM.css('height', data.height); } else { popupDOM.css('height', ''); + popupDOM.css('maxHeight', ''); } + // .tip-choice-group should always be the same height as the popup + // for scrolling to work properly. + popupDOM.find('.tip-choice-group').height(popupDOM.height()); var container = popupDOM.parent('.choice-tips-container'); if (container.length) { From 709006f9a286a6f4b7169733c3136ee5e073b5f3 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Sun, 29 Mar 2015 00:19:17 -0700 Subject: [PATCH 3/8] Fwd port fixes for popup heights --- problem_builder/public/css/questionnaire.css | 2 ++ problem_builder/public/js/questionnaire.js | 1 + problem_builder/tests/integration/test_mcq.py | 35 +++++++++++++++++++ .../xml/mcq_with_fixed_height_tips.xml | 13 +++++++ 4 files changed, 51 insertions(+) create mode 100644 problem_builder/tests/integration/xml/mcq_with_fixed_height_tips.xml diff --git a/problem_builder/public/css/questionnaire.css b/problem_builder/public/css/questionnaire.css index fafa07f0..f6f9589f 100644 --- a/problem_builder/public/css/questionnaire.css +++ b/problem_builder/public/css/questionnaire.css @@ -35,6 +35,8 @@ opacity: 0.9; padding: 22px 10px; width: 300px; + min-height: 40px; + z-index: 10000; } .mentoring .questionnaire .choice-tips .title { diff --git a/problem_builder/public/js/questionnaire.js b/problem_builder/public/js/questionnaire.js index 901dda31..a967c5b6 100644 --- a/problem_builder/public/js/questionnaire.js +++ b/problem_builder/public/js/questionnaire.js @@ -28,6 +28,7 @@ function MessageView(element, mentoring) { if (data && data.height) { popupDOM.css('height', data.height); + popupDOM.css('maxHeight', data.height); } else { popupDOM.css('height', ''); popupDOM.css('maxHeight', ''); diff --git a/problem_builder/tests/integration/test_mcq.py b/problem_builder/tests/integration/test_mcq.py index 9cfc1680..317da0e7 100644 --- a/problem_builder/tests/integration/test_mcq.py +++ b/problem_builder/tests/integration/test_mcq.py @@ -261,6 +261,41 @@ def test_questionnaire_html_choices(self, page): self.assertIn('Congratulations!', messages.text) + def _get_inner_height(self, elem): + return elem.size['height'] - \ + int(elem.value_of_css_property("padding-top").replace(u'px', u'')) - \ + int(elem.value_of_css_property("padding-bottom").replace(u'px', u'')) + + @ddt.unpack + @ddt.data( + ('yes', 40), + ('maybenot', 60), + ('understand', 600) + ) + def test_tip_height(self, choice_value, expected_height): + mentoring = self.go_to_page("Mcq With Fixed Height Tips") + choices_list = mentoring.find_element_by_css_selector(".choices-list") + submit = mentoring.find_element_by_css_selector('.submit input.input-main') + + choice_input_css_selector = ".choice input[value={}]".format(choice_value) + choice_input = choices_list.find_element_by_css_selector(choice_input_css_selector) + choice_wrapper = choice_input.find_element_by_xpath("./ancestor::div[@class='choice']") + + choice_input.click() + self.wait_until_clickable(submit) + submit.click() + self.wait_until_disabled(submit) + + item_feedback_popup = choice_wrapper.find_element_by_css_selector(".choice-tips") + self.assertTrue(item_feedback_popup.is_displayed()) + feedback_height = self._get_inner_height(item_feedback_popup) + self.assertEqual(feedback_height, expected_height) + + choice_wrapper.find_element_by_css_selector(".choice-result").click() + item_feedback_popup = choice_wrapper.find_element_by_css_selector(".choice-tips") + item_feedback_height = self._get_inner_height(item_feedback_popup) + self.assertEqual(item_feedback_height, expected_height) + @patch.object(MentoringBlock, 'get_theme', Mock(return_value={'package': 'problem_builder', 'locations': ['public/themes/lms.css']})) diff --git a/problem_builder/tests/integration/xml/mcq_with_fixed_height_tips.xml b/problem_builder/tests/integration/xml/mcq_with_fixed_height_tips.xml new file mode 100644 index 00000000..4a4dee69 --- /dev/null +++ b/problem_builder/tests/integration/xml/mcq_with_fixed_height_tips.xml @@ -0,0 +1,13 @@ + + + + Yes + Maybe not + I don't understand + + Great! + Ah, damn. + Really? + + + From c72721829f427b91c098b103c7c8f82f3e0e661f Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Sat, 28 Mar 2015 22:51:52 -0700 Subject: [PATCH 4/8] Fwd port fix to OC-89: Improve MCQ/MRQ choice HTML --- problem_builder/public/css/questionnaire.css | 31 +++++-------- problem_builder/questionnaire.py | 9 ++++ problem_builder/templates/html/mcqblock.html | 11 +++-- problem_builder/templates/html/mrqblock.html | 13 ++++-- .../templates/html/ratingblock.html | 35 ++++++++------ problem_builder/tests/integration/test_mcq.py | 46 +++++++++---------- 6 files changed, 79 insertions(+), 66 deletions(-) diff --git a/problem_builder/public/css/questionnaire.css b/problem_builder/public/css/questionnaire.css index f6f9589f..f7b1af8b 100644 --- a/problem_builder/public/css/questionnaire.css +++ b/problem_builder/public/css/questionnaire.css @@ -1,19 +1,23 @@ .mentoring .questionnaire .choices-list { + display: table; position: relative; + width: 100%; + border-spacing: 0 6px; padding-top: 10px; margin-bottom: 10px; } .mentoring .questionnaire .choice-result { - display: inline-block; + display: table-cell; width: 40px; - vertical-align: middle; + vertical-align: top; cursor: pointer; float: none; } .mentoring .questionnaire .choice { overflow-y: hidden; + display: table-row; } .mentoring .questionnaire .choice-result.checkmark-correct, @@ -71,26 +75,13 @@ } .mentoring .choices-list .choice-selector { - margin-right: 5px; + display: table-cell; + vertical-align: top; + width: 28px; } .mentoring .choice-label { - display: inline-block; - margin-top: 8px; - margin-bottom: 5px; + display: table-cell; + vertical-align: top; line-height: 1.3; } - -.mentoring .choices-list .choice-text > .xblock-light-child * { - vertical-align: middle; -} - -.mentoring .choices-list .choice-text > .xblock-light-child, -.mentoring .choices-list .choice-text > .xblock-light-child > .html_child { - /* - HTML Light Child content is wrapped in two divs: div.xblock-light-child and just div - On the other hand, choice are usually rendered inline. - Hence, we render first two divs inline, than all the actual content of HTML is rendered as is - */ - display: inline-block; -} diff --git a/problem_builder/questionnaire.py b/problem_builder/questionnaire.py index f4f36f84..bca6c10c 100644 --- a/problem_builder/questionnaire.py +++ b/problem_builder/questionnaire.py @@ -113,6 +113,15 @@ def parse_xml(cls, node, runtime, keys, id_generator): return block + @property + def html_id(self): + """ + A short, simple ID string used to uniquely identify this question. + + This is only used by templates for matching and