From 13e52786d84e89ed5405ce99f8a48312cc3919c8 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 28 Apr 2015 16:46:59 -0700 Subject: [PATCH 1/8] New tests for messages in standard mode --- .../tests/integration/base_test.py | 23 +++- .../tests/integration/test_messages.py | 101 ++++++++++++++++++ .../integration/xml_templates/messages.xml | 19 ++++ 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 problem_builder/tests/integration/test_messages.py create mode 100644 problem_builder/tests/integration/xml_templates/messages.xml diff --git a/problem_builder/tests/integration/base_test.py b/problem_builder/tests/integration/base_test.py index 231ef165..0983a5be 100644 --- a/problem_builder/tests/integration/base_test.py +++ b/problem_builder/tests/integration/base_test.py @@ -65,13 +65,32 @@ class MentoringBaseTest(SeleniumBaseTest, PopupCheckMixin): default_css_selector = 'div.mentoring' -class MentoringAssessmentBaseTest(SeleniumXBlockTest, PopupCheckMixin): +class MentoringBaseTemplateTest(SeleniumXBlockTest, PopupCheckMixin): """ - Base class for tests of assessment mode + Base class for mentoring tests that use templated XML. + All new tests should inherit from this rather than MentoringBaseTest """ module_name = __name__ default_css_selector = 'div.mentoring' + def load_scenario(self, xml_file, params=None): + params = params or {} + scenario = loader.render_template("xml_templates/{}".format(xml_file), params) + self.set_scenario_xml(scenario) + return self.go_to_view("student_view") + + def click_submit(self, mentoring): + """ Click the submit button and wait for the response """ + submit = mentoring.find_element_by_css_selector('.submit input.input-main') + self.assertTrue(submit.is_displayed() and submit.is_enabled()) + submit.click() + self.wait_until_disabled(submit) + + +class MentoringAssessmentBaseTest(MentoringBaseTemplateTest): + """ + Base class for tests of assessment mode + """ @staticmethod def question_text(number): if number: diff --git a/problem_builder/tests/integration/test_messages.py b/problem_builder/tests/integration/test_messages.py new file mode 100644 index 00000000..6112bfb9 --- /dev/null +++ b/problem_builder/tests/integration/test_messages.py @@ -0,0 +1,101 @@ +# -*- 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 . +# +from .base_test import MentoringBaseTemplateTest +import ddt + + +COMPLETED, INCOMPLETE, MAX_REACHED = "completed", "incomplete", "max_attempts_reached" +MESSAGES = { + COMPLETED: u"Great job! (completed message)", + INCOMPLETE: u"Not quite! You can try again, though. (incomplete message)", + MAX_REACHED: ( + u"Sorry, you have used up all of your allowed submissions. (max_attempts_reached message)" + ), +} + + +@ddt.ddt +class MessagesTest(MentoringBaseTemplateTest): + """ + Test the various types of message that can be added to a problem. + """ + def expect_message(self, msg_type, mentoring): + """ + Assert that the message of the specified type is shown to the user. + """ + messages_element = mentoring.find_element_by_css_selector('.messages') + if msg_type is None: + self.assertFalse(messages_element.is_displayed()) + else: + self.assertTrue(messages_element.is_displayed()) + message_text = messages_element.text.strip() + self.assertTrue(message_text.startswith("FEEDBACK")) + message_text = message_text[8:].lstrip() + self.assertEqual(MESSAGES[msg_type], message_text) + + def click_choice(self, container, choice_text): + """ Click on the choice label with the specified text """ + for label in container.find_elements_by_css_selector('.choices .choice label'): + if choice_text in label.text: + label.click() + break + + @ddt.data( + ("One", COMPLETED), + ("Two", COMPLETED), + ("I don't understand", MAX_REACHED), + ) + @ddt.unpack + def test_one_shot(self, choice_text, expected_message_type): + """ + Test a question that has max_attempts set to 1 + """ + mentoring = self.load_scenario("messages.xml", {"max_attempts": 1}) + self.expect_message(None, mentoring) + self.click_choice(mentoring, choice_text) + self.click_submit(mentoring) + self.expect_message(expected_message_type, mentoring) + + @ddt.data( + (2, "One", COMPLETED), + (2, "I don't understand", MAX_REACHED), + (0, "I don't understand", INCOMPLETE), + (10, "I don't understand", INCOMPLETE), + ) + @ddt.unpack + def test_retry(self, max_attempts, choice_text, expected_message_type): + """ + Test submitting a wrong answer, seeing a message, then submitting another answer. + In each case, max_attempts is not 1. + """ + mentoring = self.load_scenario("messages.xml", {"max_attempts": max_attempts}) + # First, there should be no message. + self.expect_message(None, mentoring) + # Let's get the question wrong: + self.click_choice(mentoring, "I don't understand") + self.click_submit(mentoring) + # We should now see the INCOMPLETE message: + self.expect_message(INCOMPLETE, mentoring) + + # Now, answer as directed and expect the given message: + self.click_choice(mentoring, "One") # Make sure we change our choice so we will be able to submit + self.click_choice(mentoring, choice_text) + self.click_submit(mentoring) + self.expect_message(expected_message_type, mentoring) diff --git a/problem_builder/tests/integration/xml_templates/messages.xml b/problem_builder/tests/integration/xml_templates/messages.xml new file mode 100644 index 00000000..68262239 --- /dev/null +++ b/problem_builder/tests/integration/xml_templates/messages.xml @@ -0,0 +1,19 @@ + + +

This is a test of messages

+
+ + + One + Two + I don't understand + + Yep, if you interpret 'x' as multiplication, this equals one. + You must be a programmer. If you interpret '0x' as a hexadecimal prefix, this equals two. + + + Great job! (completed message) + Not quite! You can try again, though. (incomplete message) + Sorry, you have used up all of your allowed submissions. (max_attempts_reached message) + You may try this assessment again, and only the latest score will be used. (on-assessment-review message) +
From 162c74e96bc16c23f642d4ae15f459c743e29909 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 28 Apr 2015 16:50:01 -0700 Subject: [PATCH 2/8] Fix the bug --- problem_builder/mentoring.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 4d287f61..b9acac47 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -376,15 +376,24 @@ def publish_event(self, data, suffix=''): return {'result': 'ok'} def get_message(self, completed): - if self.max_attempts_reached: - return self.get_message_html('max_attempts_reached') - elif completed: + """ + Get the message to display to a student following a submission in normal mode. + """ + if completed: + # Student has achieved a perfect score return self.get_message_html('completed') + elif self.max_attempts_reached: + # Student has not achieved a perfect score and cannot try again + return self.get_message_html('max_attempts_reached') else: + # Student did not achieve a perfect score but can try again: return self.get_message_html('incomplete') @property def assessment_message(self): + """ + Get the message to display to a student following a submission in assessment mode. + """ if not self.max_attempts_reached: return self.get_message_html('on-assessment-review') else: @@ -475,8 +484,6 @@ def submit(self, submissions, suffix=''): child.save() completed = completed and (child_result['status'] == 'correct') - message = self.get_message(completed) - # Once it has been completed once, keep completion even if user changes values if self.completed: completed = True @@ -509,6 +516,7 @@ def submit(self, submissions, suffix=''): self.completed = completed is True + message = self.get_message(completed) raw_score = self.score.raw self.runtime.publish(self, 'xblock.problem_builder.submitted', { From 2562f54ba32a24aca090c3d00211e143cc4b38e4 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 28 Apr 2015 16:51:15 -0700 Subject: [PATCH 3/8] Fix appearance of messages in Studio --- problem_builder/message.py | 5 ++++- problem_builder/public/js/mentoring_edit.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/problem_builder/message.py b/problem_builder/message.py index cebc6d07..dfce93cf 100644 --- a/problem_builder/message.py +++ b/problem_builder/message.py @@ -67,7 +67,10 @@ def _(self, text): def mentoring_view(self, context=None): """ Render this message for use by a mentoring block. """ - html = u'
{content}
'.format(msg_type=self.type, content=self.content) + html = u'
{content}
'.format( + msg_type=self.type, + content=self.content + ) return Fragment(html) def student_view(self, context=None): diff --git a/problem_builder/public/js/mentoring_edit.js b/problem_builder/public/js/mentoring_edit.js index 131a8649..693a609e 100644 --- a/problem_builder/public/js/mentoring_edit.js +++ b/problem_builder/public/js/mentoring_edit.js @@ -5,7 +5,7 @@ function MentoringEditComponents(runtime, element) { var updateButtons = function() { $buttons.each(function() { var msg_type = $(this).data('boilerplate'); - $(this).toggleClass('disabled', $('.xblock .message.'+msg_type).length > 0); + $(this).toggleClass('disabled', $('.xblock .submission-message.'+msg_type).length > 0); }); }; updateButtons(); From 8e7eec3f30cfd1ace09cc2534e6692f86a2ea3da Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 28 Apr 2015 16:52:14 -0700 Subject: [PATCH 4/8] Clarify the various messages and what they are supposed to do --- problem_builder/message.py | 87 +++++++++++++++---- problem_builder/public/css/mentoring_edit.css | 8 ++ 2 files changed, 78 insertions(+), 17 deletions(-) diff --git a/problem_builder/message.py b/problem_builder/message.py index dfce93cf..d63c9099 100644 --- a/problem_builder/message.py +++ b/problem_builder/message.py @@ -40,6 +40,52 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin): A message which can be conditionally displayed at the mentoring block level, for example upon completion of the block """ + MESSAGE_TYPES = { + "completed": { + "display_name": _(u"Completed"), + "long_display_name": _(u"Message shown when complete"), + "default": _(u"Great job!"), + "description": _( + u"In standard mode, this message will be shown when the student achieves a " + "perfect score. " + "This message is ignored in assessment mode." + ), + }, + "incomplete": { + "display_name": _(u"Incomplete"), + "long_display_name": _(u"Message shown when incomplete"), + "default": _(u"Not quite! You can try again, though."), + "description": _( + u"In standard mode, this message will be shown when the student gets at least " + "one question wrong, but is allowed to try again. " + "This message is ignored in assessment mode." + ), + }, + "max_attempts_reached": { + "display_name": _(u"Reached max. # of attempts"), + "long_display_name": _(u"Message shown when student reaches max. # of attempts"), + "default": _(u"Sorry, you have used up all of your allowed submissions."), + "description": _( + u"In standard mode, this message will be shown when the student has used up " + "all of their allowed attempts without achieving a perfect score. " + "This message is ignored in assessment mode." + ), + }, + "on-assessment-review": { + "display_name": _(u"Review with attempts left"), + "long_display_name": _(u"Message shown during review when attempts remain"), + "default": _( + u"You may try this assessment again, and only the latest score will be used." + ), + "description": _( + u"In assessment mode, this message will be shown when the student is reviewing " + "their answers to the assessment, if the student is allowed to try again. " + "This message is ignored in standard mode and is not shown if the student has " + "used up all of their allowed attempts." + ), + }, + } + content = String( display_name=_("Message"), help=_("Message to display upon completion"), @@ -53,10 +99,10 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin): scope=Scope.content, default="completed", values=( - {"display_name": "Completed", "value": "completed"}, - {"display_name": "Incompleted", "value": "incomplete"}, - {"display_name": "Reached max. # of attemps", "value": "max_attempts_reached"}, - {"display_name": "Review with attempts left", "value": "on-assessment-review"} + {"value": "completed", "display_name": MESSAGE_TYPES["completed"]["display_name"]}, + {"value": "incomplete", "display_name": MESSAGE_TYPES["incomplete"]["display_name"]}, + {"value": "max_attempts_reached", "display_name": MESSAGE_TYPES["max_attempts_reached"]["display_name"]}, + {"value": "on-assessment-review", "display_name": MESSAGE_TYPES["on-assessment-review"]["display_name"]}, ), ) editable_fields = ("content", ) @@ -77,27 +123,34 @@ def student_view(self, context=None): """ Normal view of this XBlock, identical to mentoring_view """ return self.mentoring_view(context) + def author_view(self, context=None): + fragment = self.mentoring_view(context) + fragment.content += u'

{}

'.format(self.help_text) + return fragment + @property def display_name_with_default(self): - if self.type == 'max_attempts_reached': - max_attempts = self.get_parent().max_attempts - return self._(u"Message when student reaches max. # of attempts ({limit})").format( - limit=self._(u"unlimited") if max_attempts == 0 else max_attempts - ) - if self.type == 'completed': - return self._(u"Message shown when complete") - if self.type == 'incomplete': - return self._(u"Message shown when incomplete") - if self.type == 'on-assessment-review': - return self._(u"Message shown during review when attempts remain") - return u"INVALID MESSAGE" + try: + return self._(self.MESSAGE_TYPES[self.type]["long_display_name"]) + except KeyError: + return u"INVALID MESSAGE" + + @property + def help_text(self): + try: + return self._(self.MESSAGE_TYPES[self.type]["description"]) + except KeyError: + return u"This message is not a valid message type!" @classmethod def get_template(cls, template_id): """ Used to interact with Studio's create_xblock method to instantiate pre-defined templates. """ - return {'data': {'type': template_id, 'content': "Message goes here."}} + return {'data': { + 'type': template_id, + 'content': cls.MESSAGE_TYPES[template_id]["default"], + }} @classmethod def parse_xml(cls, node, runtime, keys, id_generator): diff --git a/problem_builder/public/css/mentoring_edit.css b/problem_builder/public/css/mentoring_edit.css index 17d76e5c..c1500e27 100644 --- a/problem_builder/public/css/mentoring_edit.css +++ b/problem_builder/public/css/mentoring_edit.css @@ -26,3 +26,11 @@ border-color: #888; cursor: default; } + +.xblock[data-block-type=problem-builder] .submission-message-help p { + border-top: 1px solid #ddd; + font-size: 0.85em; + font-style: italic; + margin-top: 1em; + padding-top: 0.3em; +} From f7a0d1561a7e3a22fe60831c47a899b7722f5d0c Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 28 Apr 2015 16:54:07 -0700 Subject: [PATCH 5/8] Remove unused code --- problem_builder/mentoring.py | 8 ++++---- problem_builder/public/js/answer.js | 1 - problem_builder/public/js/mentoring_standard_view.js | 4 ---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index b9acac47..77262c07 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -148,6 +148,7 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC # Has the student attempted this mentoring step? default=False, scope=Scope.user_state + # TODO: Does anything use this 'attempted' field? May want to delete it. ) completed = Boolean( # Has the student completed this mentoring step? @@ -458,7 +459,6 @@ def get_results(self, queries, suffix=''): return { 'results': results, 'completed': completed, - 'attempted': self.attempted, 'message': message, 'step': step, 'max_attempts': self.max_attempts, @@ -468,6 +468,8 @@ def get_results(self, queries, suffix=''): @XBlock.json_handler def submit(self, submissions, suffix=''): log.info(u'Received submissions: {}'.format(submissions)) + + # This has now been attempted: self.attempted = True if self.is_assessment: @@ -528,10 +530,9 @@ def submit(self, submissions, suffix=''): return { 'results': submit_results, 'completed': self.completed, - 'attempted': self.attempted, 'message': message, 'max_attempts': self.max_attempts, - 'num_attempts': self.num_attempts + 'num_attempts': self.num_attempts, } def handle_assessment_submit(self, submissions, suffix): @@ -589,7 +590,6 @@ def handle_assessment_submit(self, submissions, suffix): return { 'completed': completed, - 'attempted': self.attempted, 'max_attempts': self.max_attempts, 'num_attempts': self.num_attempts, 'step': self.step, diff --git a/problem_builder/public/js/answer.js b/problem_builder/public/js/answer.js index 4a50a255..84d23896 100644 --- a/problem_builder/public/js/answer.js +++ b/problem_builder/public/js/answer.js @@ -24,7 +24,6 @@ function AnswerBlock(runtime, element) { handleSubmit: function(result) { var checkmark = $('.answer-checkmark', element); - $(element).find('.message').text((result || {}).error || ''); this.clearResult(); diff --git a/problem_builder/public/js/mentoring_standard_view.js b/problem_builder/public/js/mentoring_standard_view.js index 265186d6..fb08f473 100644 --- a/problem_builder/public/js/mentoring_standard_view.js +++ b/problem_builder/public/js/mentoring_standard_view.js @@ -48,10 +48,6 @@ function MentoringStandardView(runtime, element, mentoring) { submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults); } - function get_results() { - calculate_results('get_results'); - } - function submit() { calculate_results('submit'); } From cff2006960a912b2e7da7dddc2dabe5d5ebd9d9f Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 28 Apr 2015 16:54:37 -0700 Subject: [PATCH 6/8] Clean up the submit handlers --- problem_builder/mentoring.py | 55 ++++++++++--------- .../public/js/mentoring_standard_view.js | 20 ++++++- .../tests/integration/test_progression.py | 8 ++- 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 77262c07..26eb3a4d 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -26,7 +26,7 @@ from collections import namedtuple from xblock.core import XBlock -from xblock.exceptions import NoSuchViewError +from xblock.exceptions import NoSuchViewError, JsonHandlerError from xblock.fields import Boolean, Scope, String, Integer, Float, List from xblock.fragment import Fragment from xblock.validation import ValidationMessage @@ -468,6 +468,14 @@ def get_results(self, queries, suffix=''): @XBlock.json_handler def submit(self, submissions, suffix=''): log.info(u'Received submissions: {}'.format(submissions)) + # server-side check that the user is allowed to submit: + if self.max_attempts_reached: + raise JsonHandlerError(403, "Maximum number of attempts already reached.") + elif self.has_missing_dependency: + raise JsonHandlerError( + 403, + "You need to complete all previous steps before being able to complete the current one." + ) # This has now been attempted: self.attempted = True @@ -476,6 +484,7 @@ def submit(self, submissions, suffix=''): return self.handle_assessment_submit(submissions, suffix) submit_results = [] + previously_completed = self.completed completed = True for child_id in self.steps: child = self.runtime.get_block(child_id) @@ -486,37 +495,30 @@ def submit(self, submissions, suffix=''): child.save() completed = completed and (child_result['status'] == 'correct') - # Once it has been completed once, keep completion even if user changes values - if self.completed: - completed = True - - # server-side check to not set completion if the max_attempts is reached - if self.max_attempts_reached: - completed = False - - if self.has_missing_dependency: - completed = False - message = 'You need to complete all previous steps before being able to complete the current one.' - elif completed and self.next_step == self.url_name: + if completed and self.next_step == self.url_name: self.next_step = self.followed_by - # Once it was completed, lock score - if not self.completed: - # save user score and results + # Update the score and attempts, unless the user had already achieved a perfect score ("completed"): + if not previously_completed: + # Update the results while self.student_results: self.student_results.pop() for result in submit_results: self.student_results.append(result) + # Save the user's latest score self.runtime.publish(self, 'grade', { 'value': self.score.raw, 'max_value': 1, }) - if not self.completed and self.max_attempts > 0: - self.num_attempts += 1 + # Mark this as having used an attempt: + if self.max_attempts > 0: + self.num_attempts += 1 - self.completed = completed is True + # Save the completion status. + # Once it has been completed once, keep completion even if user changes values + self.completed = bool(completed) or previously_completed message = self.get_message(completed) raw_score = self.score.raw @@ -570,14 +572,13 @@ def handle_assessment_submit(self, submissions, suffix): if current_child == steps[-1]: log.info(u'Last assessment step submitted: {}'.format(submissions)) - if not self.max_attempts_reached: - self.runtime.publish(self, 'grade', { - 'value': score.raw, - 'max_value': 1, - 'score_type': 'proficiency', - }) - event_data['final_grade'] = score.raw - assessment_message = self.assessment_message + self.runtime.publish(self, 'grade', { + 'value': score.raw, + 'max_value': 1, + 'score_type': 'proficiency', + }) + event_data['final_grade'] = score.raw + assessment_message = self.assessment_message self.num_attempts += 1 self.completed = True diff --git a/problem_builder/public/js/mentoring_standard_view.js b/problem_builder/public/js/mentoring_standard_view.js index fb08f473..98509182 100644 --- a/problem_builder/public/js/mentoring_standard_view.js +++ b/problem_builder/public/js/mentoring_standard_view.js @@ -32,6 +32,24 @@ function MentoringStandardView(runtime, element, mentoring) { submitDOM.attr('disabled', 'disabled'); } + function handleSubmitError(jqXHR, textStatus, errorThrown) { + if (textStatus == "error") { + var errMsg = errorThrown; + // Check if there's a more specific JSON error message: + if (jqXHR.responseText) { + // Is there a more specific error message we can show? + try { + errMsg = JSON.parse(jqXHR.responseText).error; + } catch (error) { errMsg = jqXHR.responseText.substr(0, 300); } + } + + mentoring.setContent(messagesDOM, errMsg); + messagesDOM.show(); + + submitDOM.attr('disabled', 'disabled'); + } + } + function calculate_results(handler_name) { var data = {}; var children = mentoring.children; @@ -45,7 +63,7 @@ function MentoringStandardView(runtime, element, mentoring) { if (submitXHR) { submitXHR.abort(); } - submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults); + submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults).error(handleSubmitError); } function submit() { diff --git a/problem_builder/tests/integration/test_progression.py b/problem_builder/tests/integration/test_progression.py index 9b0d6a74..283f5787 100644 --- a/problem_builder/tests/integration/test_progression.py +++ b/problem_builder/tests/integration/test_progression.py @@ -68,6 +68,7 @@ def test_progression(self): answer = mentoring.find_element_by_css_selector('textarea') answer.send_keys('This is the answer') submit = mentoring.find_element_by_css_selector('.submit input.input-main') + self.assertTrue(submit.is_displayed() and submit.is_enabled()) submit.click() self.wait_until_disabled(submit) @@ -90,6 +91,7 @@ def test_progression(self): answer = mentoring.find_element_by_css_selector('textarea') answer.send_keys('This is the answer') submit = mentoring.find_element_by_css_selector('.submit input.input-main') + self.assertTrue(submit.is_displayed() and submit.is_enabled()) submit.click() self.wait_until_disabled(submit) self.assert_warning_is_hidden(mentoring) @@ -106,7 +108,11 @@ def test_progression(self): # Complete step 2 - no more warnings anywhere submit = mentoring.find_element_by_css_selector('.submit input.input-main') - submit.click() # Already filled the textarea in previous step + answer = mentoring.find_element_by_css_selector('textarea') + self.assertEqual(answer.text, "") # Earlier attempt to submit did not save + answer.send_keys('This is the answer') + self.assertTrue(submit.is_displayed() and submit.is_enabled()) + submit.click() self.wait_until_disabled(submit) messages = mentoring.find_element_by_css_selector('.messages') From b1db7697a2c23225122ccfb7b5202465644c2cd4 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 28 Apr 2015 21:52:51 -0700 Subject: [PATCH 7/8] Minor test cleanups --- .../tests/integration/base_test.py | 3 +- .../tests/integration/test_assessment.py | 6 +- .../tests/integration/test_dashboard.py | 72 ++----------------- .../integration/xml_templates/dashboard.xml | 58 +++++++++++++++ 4 files changed, 69 insertions(+), 70 deletions(-) create mode 100644 problem_builder/tests/integration/xml_templates/dashboard.xml diff --git a/problem_builder/tests/integration/base_test.py b/problem_builder/tests/integration/base_test.py index 0983a5be..2278caf7 100644 --- a/problem_builder/tests/integration/base_test.py +++ b/problem_builder/tests/integration/base_test.py @@ -82,7 +82,8 @@ def load_scenario(self, xml_file, params=None): def click_submit(self, mentoring): """ Click the submit button and wait for the response """ submit = mentoring.find_element_by_css_selector('.submit input.input-main') - self.assertTrue(submit.is_displayed() and submit.is_enabled()) + self.assertTrue(submit.is_displayed()) + self.assertTrue(submit.is_enabled()) submit.click() self.wait_until_disabled(submit) diff --git a/problem_builder/tests/integration/test_assessment.py b/problem_builder/tests/integration/test_assessment.py index 0eef4a49..3e22caa4 100644 --- a/problem_builder/tests/integration/test_assessment.py +++ b/problem_builder/tests/integration/test_assessment.py @@ -394,7 +394,7 @@ def test_single_question_assessment(self): self.peek_at_review(mentoring, controls, expected_results) self.assert_messages_empty(mentoring) + self.wait_until_clickable(controls.try_again) controls.try_again.click() - # this is a wait and assertion all together - it waits until expected text is in mentoring block - # and it fails with PrmoiseFailed exception if it's not - self.wait_until_text_in(self.question_text(0), mentoring) + self.wait_until_hidden(controls.try_again) + self.assertIn(self.question_text(0), mentoring.text) diff --git a/problem_builder/tests/integration/test_dashboard.py b/problem_builder/tests/integration/test_dashboard.py index 217c4370..12093314 100644 --- a/problem_builder/tests/integration/test_dashboard.py +++ b/problem_builder/tests/integration/test_dashboard.py @@ -18,7 +18,7 @@ # "AGPLv3". If not, see . # from mock import Mock, patch -from xblockutils.base_test import SeleniumXBlockTest +from .base_test import MentoringBaseTemplateTest class MockSubmissionsAPI(object): @@ -50,77 +50,20 @@ def get_submissions(self, key, limit=1): return [] -class TestDashboardBlock(SeleniumXBlockTest): +class TestDashboardBlock(MentoringBaseTemplateTest): """ Test the Student View of a dashboard XBlock linked to some problem builder blocks """ def setUp(self): super(TestDashboardBlock, self).setUp() # Set up our scenario: - self.set_scenario_xml(""" - - - - Option 1 - Option 2 - Option 3 - Option 4 - - - Option 1 - Option 2 - Option 3 - Option 4 - - - Option 1 - Option 2 - Option 3 - Option 4 - - This message here should be ignored. - - - - Option 4 - Option 5 - Option 6 - - - Option 1 - Option 2 - Option 3 - Option 4 - - - Option 1 - Option 2 - Option 3 - Option 4 - - - - - Option 1 - Option 2 - Option 3 - Option 4 - - - Option A - Option B - Option C - - - - - - """) + self.load_scenario('dashboard.xml') # Apply a whole bunch of patches that are needed in lieu of the LMS/CMS runtime and edx-submissions: + def get_mentoring_blocks(dashboard_block, mentoring_ids, ignore_errors=True): return [dashboard_block.runtime.get_block(key) for key in dashboard_block.get_parent().children[:-1]] + mock_submisisons_api = MockSubmissionsAPI() patches = ( ( @@ -174,10 +117,7 @@ def test_dashboard(self): for idx, mcq in enumerate(mcqs): choices = mcq.find_elements_by_css_selector('.choices .choice label') choices[idx].click() - submit = pb.find_element_by_css_selector('.submit input.input-main') - self.assertTrue(submit.is_enabled()) - submit.click() - self.wait_until_disabled(submit) + self.click_submit(pb) # Reload the page: self.go_to_view("student_view") diff --git a/problem_builder/tests/integration/xml_templates/dashboard.xml b/problem_builder/tests/integration/xml_templates/dashboard.xml new file mode 100644 index 00000000..01f3971a --- /dev/null +++ b/problem_builder/tests/integration/xml_templates/dashboard.xml @@ -0,0 +1,58 @@ + + + + Option 1 + Option 2 + Option 3 + Option 4 + + + Option 1 + Option 2 + Option 3 + Option 4 + + + Option 1 + Option 2 + Option 3 + Option 4 + + This message here should be ignored. + + + + Option 4 + Option 5 + Option 6 + + + Option 1 + Option 2 + Option 3 + Option 4 + + + Option 1 + Option 2 + Option 3 + Option 4 + + + + + Option 1 + Option 2 + Option 3 + Option 4 + + + Option A + Option B + Option C + + + + + From a6fce0457a1de1c296c461fca1ad555f4867a1e9 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 28 Apr 2015 21:55:39 -0700 Subject: [PATCH 8/8] Fix Travis build --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3dfd81e6..6c0587c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,3 +17,5 @@ script: - python run_tests.py --with-coverage --cover-package=problem_builder notifications: email: false +addons: + firefox: "36.0"