From a9a8dcf8826191554cc4b20d26430957dd3c267c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 18 Dec 2013 12:00:26 -0500 Subject: [PATCH 1/3] Allow acceptance test problem functions to work without hardcoded course name --- .../courseware/features/problems.py | 4 +- .../courseware/features/problems_setup.py | 109 ++++++++++-------- 2 files changed, 62 insertions(+), 51 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index ab5ed7de25..bc3c16ed8b 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -72,7 +72,7 @@ def input_problem_answer(_, problem_type, correctness): """ assert(correctness in ['correct', 'incorrect']) assert(problem_type in PROBLEM_DICT) - answer_problem(problem_type, correctness) + answer_problem(world.scenario_dict['COURSE'].number, problem_type, correctness) @step(u'I check a problem') @@ -98,7 +98,7 @@ def assert_problem_has_answer(step, problem_type, answer_class): ''' assert answer_class in ['correct', 'incorrect', 'blank'] assert problem_type in PROBLEM_DICT - problem_has_answer(problem_type, answer_class) + problem_has_answer(world.scenario_dict['COURSE'].number, problem_type, answer_class) @step(u'I reset the problem') diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py index ef9aa9972a..6a7d800b3e 100644 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ b/lms/djangoapps/courseware/features/problems_setup.py @@ -162,46 +162,48 @@ PROBLEM_DICT = { } -def answer_problem(problem_type, correctness): +def answer_problem(course, problem_type, correctness): # Make sure that the problem has been completely rendered before # starting to input an answer. world.wait_for_ajax_complete() + section_loc = section_location(course) + if problem_type == "drop down": - select_name = "input_i4x-edx-model_course-problem-drop_down_2_1" + select_name = "input_i4x-{0.org}-{0.course}-problem-drop_down_2_1".format(section_loc) option_text = 'Option 2' if correctness == 'correct' else 'Option 3' world.select_option(select_name, option_text) elif problem_type == "multiple choice": if correctness == 'correct': - world.css_check(inputfield('multiple choice', choice='choice_2')) + world.css_check(inputfield(course, 'multiple choice', choice='choice_2')) else: - world.css_check(inputfield('multiple choice', choice='choice_1')) + world.css_check(inputfield(course, 'multiple choice', choice='choice_1')) elif problem_type == "checkbox": if correctness == 'correct': - world.css_check(inputfield('checkbox', choice='choice_0')) - world.css_check(inputfield('checkbox', choice='choice_2')) + world.css_check(inputfield(course, 'checkbox', choice='choice_0')) + world.css_check(inputfield(course, 'checkbox', choice='choice_2')) else: - world.css_check(inputfield('checkbox', choice='choice_3')) + world.css_check(inputfield(course, 'checkbox', choice='choice_3')) elif problem_type == 'radio': if correctness == 'correct': - world.css_check(inputfield('radio', choice='choice_2')) + world.css_check(inputfield(course, 'radio', choice='choice_2')) else: - world.css_check(inputfield('radio', choice='choice_1')) + world.css_check(inputfield(course, 'radio', choice='choice_1')) elif problem_type == 'string': textvalue = 'correct string' if correctness == 'correct' else 'incorrect' - world.css_fill(inputfield('string'), textvalue) + world.css_fill(inputfield(course, 'string'), textvalue) elif problem_type == 'numerical': textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2)) - world.css_fill(inputfield('numerical'), textvalue) + world.css_fill(inputfield(course, 'numerical'), textvalue) elif problem_type == 'formula': textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' - world.css_fill(inputfield('formula'), textvalue) + world.css_fill(inputfield(course, 'formula'), textvalue) elif problem_type == 'script': # Correct answer is any two integers that sum to 10 @@ -213,8 +215,8 @@ def answer_problem(problem_type, correctness): if correctness == 'incorrect': second_addend += random.randint(1, 10) - world.css_fill(inputfield('script', input_num=1), str(first_addend)) - world.css_fill(inputfield('script', input_num=2), str(second_addend)) + world.css_fill(inputfield(course, 'script', input_num=1), str(first_addend)) + world.css_fill(inputfield(course, 'script', input_num=2), str(second_addend)) elif problem_type == 'code': # The fake xqueue server is configured to respond @@ -233,15 +235,16 @@ def answer_problem(problem_type, correctness): choice = "choiceinput_0bc" if correctness == 'correct' else "choiceinput_1bc" world.css_fill( inputfield( + course, problem_type, choice="choiceinput_0_numtolerance_input_0" ), input_value ) - world.css_check(inputfield(problem_type, choice=choice)) + world.css_check(inputfield(course, problem_type, choice=choice)) -def problem_has_answer(problem_type, answer_class): +def problem_has_answer(course, problem_type, answer_class): if problem_type == "drop down": if answer_class == 'blank': assert world.is_css_not_present('option[selected="true"]') @@ -252,52 +255,52 @@ def problem_has_answer(problem_type, answer_class): elif problem_type == "multiple choice": if answer_class == 'correct': - assert_checked('multiple choice', ['choice_2']) + assert_checked(course, 'multiple choice', ['choice_2']) elif answer_class == 'incorrect': - assert_checked('multiple choice', ['choice_1']) + assert_checked(course, 'multiple choice', ['choice_1']) else: - assert_checked('multiple choice', []) + assert_checked(course, 'multiple choice', []) elif problem_type == "checkbox": if answer_class == 'correct': - assert_checked('checkbox', ['choice_0', 'choice_2']) + assert_checked(course, 'checkbox', ['choice_0', 'choice_2']) elif answer_class == 'incorrect': - assert_checked('checkbox', ['choice_3']) + assert_checked(course, 'checkbox', ['choice_3']) else: - assert_checked('checkbox', []) + assert_checked(course, 'checkbox', []) elif problem_type == "radio": if answer_class == 'correct': - assert_checked('radio', ['choice_2']) + assert_checked(course, 'radio', ['choice_2']) elif answer_class == 'incorrect': - assert_checked('radio', ['choice_1']) + assert_checked(course, 'radio', ['choice_1']) else: - assert_checked('radio', []) + assert_checked(course, 'radio', []) elif problem_type == 'string': if answer_class == 'blank': expected = '' else: expected = 'correct string' if answer_class == 'correct' else 'incorrect' - assert_textfield('string', expected) + assert_textfield(course, 'string', expected) elif problem_type == 'formula': if answer_class == 'blank': expected = '' else: expected = "x^2+2*x+y" if answer_class == 'correct' else 'x^2' - assert_textfield('formula', expected) + assert_textfield(course, 'formula', expected) elif problem_type in ("radio_text", "checkbox_text"): if answer_class == 'blank': expected = ('', '') - assert_choicetext_values(problem_type, (), expected) + assert_choicetext_values(course, problem_type, (), expected) elif answer_class == 'incorrect': expected = ('5', '') - assert_choicetext_values(problem_type, ["choiceinput_1bc"], expected) + assert_choicetext_values(course, problem_type, ["choiceinput_1bc"], expected) else: expected = ('8', '') - assert_choicetext_values(problem_type, ["choiceinput_0bc"], expected) + assert_choicetext_values(course, problem_type, ["choiceinput_0bc"], expected) else: # The other response types use random data, @@ -325,14 +328,16 @@ def add_problem_to_course(course, problem_type, extra_meta=None): # We set rerandomize=always in the metadata so that the "Reset" button # will appear. category_name = "problem" - return world.ItemFactory.create(parent_location=section_location(course), - category=category_name, - display_name=str(problem_type), - data=problem_xml, - metadata=metadata) + return world.ItemFactory.create( + parent_location=section_location(course), + category=category_name, + display_name=str(problem_type), + data=problem_xml, + metadata=metadata + ) -def inputfield(problem_type, choice=None, input_num=1): +def inputfield(course, problem_type, choice=None, input_num=1): """ Return the css selector for `problem_type`. For example, if problem_type is 'string', return the text field for the string problem in the test course. @@ -340,14 +345,20 @@ def inputfield(problem_type, choice=None, input_num=1): `choice` is the name of the checkbox input in a group of checkboxes. """ - sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % - (problem_type.replace(" ", "_"), str(input_num))) + section_loc = section_location(course) - # this is necessary due to naming requirement for this problem type + # this is necessary due to naming requirement for this problem type if problem_type in ("radio_text", "checkbox_text"): - sel = "input#i4x-edx-model_course-problem-{0}_2_{1}".format( - problem_type.replace(" ", "_"), str(input_num) - ) + selector_template = "input#i4x-{org}-{course}-problem-{ptype}_2_{input}" + else: + selector_template = "input#input_i4x-{org}-{course}-problem-{ptype}_2_{input}" + + sel = selector_template.format( + org=section_loc.org, + course=section_loc.course, + ptype=problem_type.replace(" ", "_"), + input=input_num, + ) if choice is not None: base = "_choice_" if problem_type == "multiple choice" else "_" @@ -360,7 +371,7 @@ def inputfield(problem_type, choice=None, input_num=1): return sel -def assert_checked(problem_type, choices): +def assert_checked(course, problem_type, choices): ''' Assert that choice names given in *choices* are the only ones checked. @@ -371,7 +382,7 @@ def assert_checked(problem_type, choices): all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3'] for this_choice in all_choices: def check_problem(): - element = world.css_find(inputfield(problem_type, choice=this_choice)) + element = world.css_find(inputfield(course, problem_type, choice=this_choice)) if this_choice in choices: assert element.checked else: @@ -379,12 +390,12 @@ def assert_checked(problem_type, choices): world.retry_on_exception(check_problem) -def assert_textfield(problem_type, expected_text, input_num=1): - element_value = world.css_value(inputfield(problem_type, input_num=input_num)) +def assert_textfield(course, problem_type, expected_text, input_num=1): + element_value = world.css_value(inputfield(course, problem_type, input_num=input_num)) assert element_value == expected_text -def assert_choicetext_values(problem_type, choices, expected_values): +def assert_choicetext_values(course, problem_type, choices, expected_values): """ Asserts that only the given choices are checked, and given text fields have a desired value @@ -397,7 +408,7 @@ def assert_choicetext_values(problem_type, choices, expected_values): "choiceinput_1_numtolerance_input_0" ] for this_choice in all_choices: - element = world.css_find(inputfield(problem_type, choice=this_choice)) + element = world.css_find(inputfield(course, problem_type, choice=this_choice)) if this_choice in choices: assert element.checked @@ -405,6 +416,6 @@ def assert_choicetext_values(problem_type, choices, expected_values): assert not element.checked for (name, expected) in zip(all_inputs, expected_values): - element = world.css_find(inputfield(problem_type, name)) + element = world.css_find(inputfield(course, problem_type, name)) # Remove any trailing spaces that may have been added assert element.value.strip() == expected From 268c4fa01b0b6ca8d5ea6a273ea4251b8f3c4a2a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 18 Dec 2013 16:11:02 -0500 Subject: [PATCH 2/3] Add acceptance tests of the conditional module [LMS-1639] --- .../courseware/features/conditional.feature | 22 ++++ .../courseware/features/conditional.py | 119 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 lms/djangoapps/courseware/features/conditional.feature create mode 100644 lms/djangoapps/courseware/features/conditional.py diff --git a/lms/djangoapps/courseware/features/conditional.feature b/lms/djangoapps/courseware/features/conditional.feature new file mode 100644 index 0000000000..65080f76be --- /dev/null +++ b/lms/djangoapps/courseware/features/conditional.feature @@ -0,0 +1,22 @@ +@shard_2 +Feature: LMS.Conditional Module + As a student, I want to view a Conditional component in the LMS + + Scenario: A Conditional hides content when conditions aren't satisfied + Given that a course has a Conditional conditioned on problem attempted=True + And that the conditioned problem has not been attempted + When I view the conditional + Then the conditional contents are hidden + + Scenario: A Conditional shows content when conditions are satisfied + Given that a course has a Conditional conditioned on problem attempted=True + And that the conditioned problem has been attempted + When I view the conditional + Then the conditional contents are visible + + Scenario: A Conditional containing a Poll is updated when the poll is answered + Given that a course has a Conditional conditioned on poll poll_answer=yes + When I view the conditional + Then the conditional contents are hidden + When I answer the conditioned poll "yes" + Then the conditional contents are visible diff --git a/lms/djangoapps/courseware/features/conditional.py b/lms/djangoapps/courseware/features/conditional.py new file mode 100644 index 0000000000..665cfbcd07 --- /dev/null +++ b/lms/djangoapps/courseware/features/conditional.py @@ -0,0 +1,119 @@ + +from lettuce import world, steps +from nose.tools import assert_in, assert_equals, assert_true + +from common import i_am_registered_for_the_course, visit_scenario_item +from problems_setup import add_problem_to_course, answer_problem + +@steps +class ConditionalSteps(object): + COURSE_NUM = 'test_course' + + def setup_conditional(self, step, condition_type, condition, cond_value): + r'that a course has a Conditional conditioned on (?P\w+) (?P\w+)=(?P\w+)$' + + i_am_registered_for_the_course(step, self.COURSE_NUM) + + world.scenario_dict['VERTICAL'] = world.ItemFactory( + parent_location=world.scenario_dict['SECTION'].location, + category='vertical', + display_name="Test Vertical", + ) + + world.scenario_dict['WRAPPER'] = world.ItemFactory( + parent_location=world.scenario_dict['VERTICAL'].location, + category='wrapper', + display_name="Test Poll Wrapper" + ) + + if condition_type == 'problem': + world.scenario_dict['CONDITION_SOURCE'] = add_problem_to_course(self.COURSE_NUM, 'string') + elif condition_type == 'poll': + world.scenario_dict['CONDITION_SOURCE'] = world.ItemFactory( + parent_location=world.scenario_dict['WRAPPER'].location, + category='poll_question', + display_name='Conditional Poll', + data={ + 'question': 'Is this a good poll?', + 'answers': [ + {'id': 'yes', 'text': 'Yes, of course'}, + {'id': 'no', 'text': 'Of course not!'} + ], + } + ) + else: + raise Exception("Unknown condition type: {!r}".format(condition_type)) + + metadata = { + 'xml_attributes': { + 'sources': world.scenario_dict['CONDITION_SOURCE'].location.url() + } + } + metadata['xml_attributes'][condition] = cond_value + + world.scenario_dict['CONDITIONAL'] = world.ItemFactory( + parent_location=world.scenario_dict['WRAPPER'].location, + category='conditional', + display_name="Test Conditional", + metadata=metadata + ) + + world.ItemFactory( + parent_location=world.scenario_dict['CONDITIONAL'].location, + category='html', + display_name='Conditional Contents', + data='
Hidden Contents

' + ) + + + def setup_problem_attempts(self, step, not_attempted=None): + r'that the conditioned problem has (?Pnot )?been attempted$' + visit_scenario_item('CONDITION_SOURCE') + + if not_attempted is None: + answer_problem(self.COURSE_NUM, 'string', True) + world.css_click("input.check") + + def when_i_view_the_conditional(self, step): + r'I view the conditional$' + visit_scenario_item('CONDITIONAL') + world.wait_for_js_variable_truthy('$(".xblock-student_view[data-type=Conditional]").data("initialized")') + + def check_visibility(self, step, visible): + r'the conditional contents are (?P\w+)$' + world.wait_for_ajax_complete() + + assert_in(visible, ('visible', 'hidden')) + + if visible == 'visible': + world.wait_for_visible('.hidden-contents') + assert_true(world.css_visible('.hidden-contents')) + else: + assert_true(world.is_css_not_present('.hidden-contents')) + + def answer_poll(self, step, answer): + r' I answer the conditioned poll "([^"]*)"$' + visit_scenario_item('CONDITION_SOURCE') + world.wait_for_js_variable_truthy('$(".xblock-student_view[data-type=Poll]").data("initialized")') + world.wait_for_ajax_complete() + + answer_text = [ + poll_answer['text'] + for poll_answer + in world.scenario_dict['CONDITION_SOURCE'].answers + if poll_answer['id'] == answer + ][0] + + text_selector = '.poll_answer .text' + + poll_texts = world.retry_on_exception( + lambda: [elem.text for elem in world.css_find(text_selector)] + ) + + for idx, poll_text in enumerate(poll_texts): + if poll_text == answer_text: + world.css_click(text_selector, index=idx) + return + + +ConditionalSteps() \ No newline at end of file From 06fadcdc0c639d32f15c472e9e5eb48b5ba5f3a5 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 18 Dec 2013 16:18:03 -0500 Subject: [PATCH 3/3] Make conditional module and poll modules a little easier to understand --- common/lib/xmodule/xmodule/conditional_module.py | 6 +++++- common/lib/xmodule/xmodule/poll_module.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index 8248343371..48872b5393 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -96,7 +96,11 @@ class ConditionalModule(ConditionalFields, XModule): xml_value = self.descriptor.xml_attributes.get(xml_attr) if xml_value: return xml_value, attr_name - raise Exception('Error in conditional module: unknown condition "%s"' % xml_attr) + raise Exception( + 'Error in conditional module: no known conditional found in {!r}'.format( + self.descriptor.xml_attributes.keys() + ) + ) @lazy def required_modules(self): diff --git a/common/lib/xmodule/xmodule/poll_module.py b/common/lib/xmodule/xmodule/poll_module.py index 86852cd698..47b65fe892 100644 --- a/common/lib/xmodule/xmodule/poll_module.py +++ b/common/lib/xmodule/xmodule/poll_module.py @@ -32,7 +32,9 @@ class PollFields(object): poll_answer = String(help="Student answer", scope=Scope.user_state, default='') poll_answers = Dict(help="All possible answers for the poll fro other students", scope=Scope.user_state_summary) + # List of answers, in the form {'id': 'some id', 'text': 'the answer text'} answers = List(help="Poll answers from xml", scope=Scope.content, default=[]) + question = String(help="Poll question", scope=Scope.content, default='')