Merge pull request #12124 from edx/diana/conditional-transaction
Convert conditional module test to bok choy
This commit is contained in:
@@ -106,6 +106,9 @@ def xblock_handler(request, usage_key_string):
|
||||
:children: the unicode representation of the UsageKeys of children for this xblock.
|
||||
:metadata: new values for the metadata fields. Any whose values are None will be deleted not set
|
||||
to None! Absent ones will be left alone.
|
||||
:fields: any other xblock fields to be set. Only supported by update.
|
||||
This is represented as a dictionary:
|
||||
{'field_name': 'field_value'}
|
||||
:nullout: which metadata fields to set to None
|
||||
:graderType: change how this unit is graded
|
||||
:isPrereq: Set this xblock as a prerequisite which can be used to limit access to other xblocks
|
||||
@@ -169,6 +172,7 @@ def xblock_handler(request, usage_key_string):
|
||||
prereq_usage_key=request.json.get('prereqUsageKey'),
|
||||
prereq_min_score=request.json.get('prereqMinScore'),
|
||||
publish=request.json.get('publish'),
|
||||
fields=request.json.get('fields'),
|
||||
)
|
||||
elif request.method in ('PUT', 'POST'):
|
||||
if 'duplicate_source_locator' in request.json:
|
||||
@@ -431,11 +435,13 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None):
|
||||
|
||||
|
||||
def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, nullout=None,
|
||||
grader_type=None, is_prereq=None, prereq_usage_key=None, prereq_min_score=None, publish=None):
|
||||
grader_type=None, is_prereq=None, prereq_usage_key=None, prereq_min_score=None,
|
||||
publish=None, fields=None):
|
||||
"""
|
||||
Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
|
||||
nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
|
||||
to default).
|
||||
|
||||
"""
|
||||
store = modulestore()
|
||||
# Perform all xblock changes within a (single-versioned) transaction
|
||||
@@ -457,6 +463,10 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None,
|
||||
else:
|
||||
data = old_content['data'] if 'data' in old_content else None
|
||||
|
||||
if fields:
|
||||
for field_name in fields:
|
||||
setattr(xblock, field_name, fields[field_name])
|
||||
|
||||
if children_strings is not None:
|
||||
children = []
|
||||
for child_string in children_strings:
|
||||
@@ -610,7 +620,7 @@ def _create_item(request):
|
||||
user=request.user,
|
||||
category=category,
|
||||
display_name=request.json.get('display_name'),
|
||||
boilerplate=request.json.get('boilerplate')
|
||||
boilerplate=request.json.get('boilerplate'),
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
|
||||
@@ -805,6 +805,22 @@ class TestEditItem(TestEditItemSetup):
|
||||
self.assertEqual(sequential.due, datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
|
||||
self.assertEqual(sequential.start, datetime(2010, 9, 12, 14, 0, tzinfo=UTC))
|
||||
|
||||
def test_update_generic_fields(self):
|
||||
new_display_name = 'New Display Name'
|
||||
new_max_attempts = 2
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={
|
||||
'fields': {
|
||||
'display_name': new_display_name,
|
||||
'max_attempts': new_max_attempts,
|
||||
}
|
||||
}
|
||||
)
|
||||
problem = self.get_item_from_modulestore(self.problem_usage_key, verify_is_draft=True)
|
||||
self.assertEqual(problem.display_name, new_display_name)
|
||||
self.assertEqual(problem.max_attempts, new_max_attempts)
|
||||
|
||||
def test_delete_child(self):
|
||||
"""
|
||||
Test deleting a child.
|
||||
|
||||
@@ -22,7 +22,8 @@ class XBlockFixtureDesc(object):
|
||||
Description of an XBlock, used to configure a course fixture.
|
||||
"""
|
||||
|
||||
def __init__(self, category, display_name, data=None, metadata=None, grader_type=None, publish='make_public'):
|
||||
def __init__(self, category, display_name, data=None,
|
||||
metadata=None, grader_type=None, publish='make_public', **kwargs):
|
||||
"""
|
||||
Configure the XBlock to be created by the fixture.
|
||||
These arguments have the same meaning as in the Studio REST API:
|
||||
@@ -41,6 +42,7 @@ class XBlockFixtureDesc(object):
|
||||
self.publish = publish
|
||||
self.children = []
|
||||
self.locator = None
|
||||
self.fields = kwargs
|
||||
|
||||
def add_children(self, *args):
|
||||
"""
|
||||
@@ -59,13 +61,15 @@ class XBlockFixtureDesc(object):
|
||||
|
||||
XBlocks are always set to public visibility.
|
||||
"""
|
||||
return json.dumps({
|
||||
returned_data = {
|
||||
'display_name': self.display_name,
|
||||
'data': self.data,
|
||||
'metadata': self.metadata,
|
||||
'graderType': self.grader_type,
|
||||
'publish': self.publish
|
||||
})
|
||||
'publish': self.publish,
|
||||
'fields': self.fields,
|
||||
}
|
||||
return json.dumps(returned_data)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
@@ -354,7 +358,7 @@ class CourseFixture(XBlockContainerFixture):
|
||||
'children': None,
|
||||
'data': handouts_html,
|
||||
'id': self._handouts_loc,
|
||||
'metadata': dict()
|
||||
'metadata': dict(),
|
||||
})
|
||||
|
||||
response = self.session.post(url, data=payload, headers=self.headers)
|
||||
|
||||
42
common/test/acceptance/pages/lms/conditional.py
Normal file
42
common/test/acceptance/pages/lms/conditional.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
Conditional Pages
|
||||
"""
|
||||
from bok_choy.page_object import PageObject
|
||||
|
||||
POLL_ANSWER = 'Yes, of course'
|
||||
|
||||
|
||||
class ConditionalPage(PageObject):
|
||||
"""
|
||||
View of conditional page.
|
||||
"""
|
||||
|
||||
url = None
|
||||
|
||||
def is_browser_on_page(self):
|
||||
"""
|
||||
Returns True if the browser is currently on the right page.
|
||||
"""
|
||||
return self.q(css='.conditional-wrapper').visible
|
||||
|
||||
def is_content_visible(self):
|
||||
"""
|
||||
Returns True if the conditional's content has been revealed,
|
||||
False otherwise
|
||||
"""
|
||||
return self.q(css='.hidden-contents').visible
|
||||
|
||||
def fill_in_poll(self):
|
||||
"""
|
||||
Fills in a poll on the same page as the conditional
|
||||
with the answer that matches POLL_ANSWER
|
||||
"""
|
||||
text_selector = '.poll_answer .text'
|
||||
|
||||
text_options = self.q(css=text_selector).text
|
||||
|
||||
# Out of the possible poll answers, we want
|
||||
# to select the one that matches POLL_ANSWER and click it.
|
||||
for idx, text in enumerate(text_options):
|
||||
if text == POLL_ANSWER:
|
||||
self.q(css=text_selector).nth(idx).click()
|
||||
127
common/test/acceptance/tests/lms/test_conditional.py
Normal file
127
common/test/acceptance/tests/lms/test_conditional.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Bok choy acceptance tests for conditionals in the LMS
|
||||
"""
|
||||
from capa.tests.response_xml_factory import StringResponseXMLFactory
|
||||
from ..helpers import UniqueCourseTest
|
||||
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from ...pages.lms.courseware import CoursewarePage
|
||||
from ...pages.lms.conditional import ConditionalPage, POLL_ANSWER
|
||||
from ...pages.lms.problem import ProblemPage
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
|
||||
|
||||
class ConditionalTest(UniqueCourseTest):
|
||||
"""
|
||||
Test the conditional module in the lms.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(ConditionalTest, self).setUp()
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
AutoAuthPage(
|
||||
self.browser,
|
||||
course_id=self.course_id,
|
||||
staff=False
|
||||
).visit()
|
||||
|
||||
def install_course_fixture(self, block_type='problem'):
|
||||
"""
|
||||
Install a course fixture
|
||||
"""
|
||||
course_fixture = CourseFixture(
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
self.course_info['run'],
|
||||
self.course_info['display_name'],
|
||||
)
|
||||
vertical = XBlockFixtureDesc('vertical', 'Test Unit')
|
||||
# populate the course fixture with the right conditional modules
|
||||
course_fixture.add_children(
|
||||
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
||||
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
||||
vertical
|
||||
)
|
||||
)
|
||||
)
|
||||
course_fixture.install()
|
||||
|
||||
# Construct conditional block
|
||||
conditional_metadata = {}
|
||||
source_block = None
|
||||
if block_type == 'problem':
|
||||
problem_factory = StringResponseXMLFactory()
|
||||
problem_xml = problem_factory.build_xml(
|
||||
question_text='The answer is "correct string"',
|
||||
case_sensitive=False,
|
||||
answer='correct string',
|
||||
),
|
||||
problem = XBlockFixtureDesc('problem', 'Test Problem', data=problem_xml[0])
|
||||
conditional_metadata = {
|
||||
'xml_attributes': {
|
||||
'attempted': 'True'
|
||||
}
|
||||
}
|
||||
source_block = problem
|
||||
elif block_type == 'poll':
|
||||
poll = XBlockFixtureDesc(
|
||||
'poll_question',
|
||||
'Conditional Poll',
|
||||
question='Is this a good poll?',
|
||||
answers=[
|
||||
{'id': 'yes', 'text': POLL_ANSWER},
|
||||
{'id': 'no', 'text': 'Of course not!'}
|
||||
],
|
||||
)
|
||||
conditional_metadata = {
|
||||
'xml_attributes': {
|
||||
'poll_answer': 'yes'
|
||||
}
|
||||
}
|
||||
source_block = poll
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
course_fixture.create_xblock(vertical.locator, source_block)
|
||||
# create conditional
|
||||
conditional = XBlockFixtureDesc(
|
||||
'conditional',
|
||||
'Test Conditional',
|
||||
metadata=conditional_metadata,
|
||||
sources_list=[source_block.locator],
|
||||
)
|
||||
result_block = XBlockFixtureDesc(
|
||||
'html', 'Conditional Contents',
|
||||
data='<html><div class="hidden-contents">Hidden Contents</p></html>'
|
||||
)
|
||||
course_fixture.create_xblock(vertical.locator, conditional)
|
||||
course_fixture.create_xblock(conditional.locator, result_block)
|
||||
|
||||
def test_conditional_hides_content(self):
|
||||
self.install_course_fixture()
|
||||
self.courseware_page.visit()
|
||||
conditional_page = ConditionalPage(self.browser)
|
||||
self.assertFalse(conditional_page.is_content_visible())
|
||||
|
||||
def test_conditional_displays_content(self):
|
||||
self.install_course_fixture()
|
||||
self.courseware_page.visit()
|
||||
# Answer the problem
|
||||
problem_page = ProblemPage(self.browser)
|
||||
problem_page.fill_answer('correct string')
|
||||
problem_page.click_check()
|
||||
# The conditional does not update on its own, so we need to reload the page.
|
||||
self.courseware_page.visit()
|
||||
# Verify that we can see the content.
|
||||
conditional_page = ConditionalPage(self.browser)
|
||||
self.assertTrue(conditional_page.is_content_visible())
|
||||
|
||||
def test_conditional_handles_polls(self):
|
||||
self.install_course_fixture(block_type='poll')
|
||||
self.courseware_page.visit()
|
||||
# Fill in the conditional page poll
|
||||
conditional_page = ConditionalPage(self.browser)
|
||||
conditional_page.fill_in_poll()
|
||||
# The conditional does not update on its own, so we need to reload the page.
|
||||
self.courseware_page.visit()
|
||||
self.assertTrue(conditional_page.is_content_visible())
|
||||
@@ -1,22 +0,0 @@
|
||||
@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
|
||||
@@ -1,126 +0,0 @@
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
from lettuce import world, steps
|
||||
from nose.tools import assert_in, 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<condition_type>\w+) (?P<condition>\w+)=(?P<cond_value>\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': {
|
||||
condition: cond_value
|
||||
}
|
||||
}
|
||||
|
||||
world.scenario_dict['CONDITIONAL'] = world.ItemFactory(
|
||||
parent_location=world.scenario_dict['WRAPPER'].location,
|
||||
category='conditional',
|
||||
display_name="Test Conditional",
|
||||
metadata=metadata,
|
||||
sources_list=[world.scenario_dict['CONDITION_SOURCE'].location],
|
||||
)
|
||||
|
||||
world.ItemFactory(
|
||||
parent_location=world.scenario_dict['CONDITIONAL'].location,
|
||||
category='html',
|
||||
display_name='Conditional Contents',
|
||||
data='<html><div class="hidden-contents">Hidden Contents</p></html>'
|
||||
)
|
||||
|
||||
def setup_problem_attempts(self, step, not_attempted=None):
|
||||
r'that the conditioned problem has (?P<not_attempted>not )?been attempted$'
|
||||
visit_scenario_item('CONDITION_SOURCE')
|
||||
|
||||
if not_attempted is None:
|
||||
answer_problem(self.COURSE_NUM, 'string', True)
|
||||
world.css_click("button.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<visible>\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'))
|
||||
assert_true(
|
||||
world.css_contains_text(
|
||||
'.conditional-message',
|
||||
'must be attempted before this will become visible.'
|
||||
)
|
||||
)
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user