Commit 2c12decc by Diana Huang

Merge pull request #12124 from edx/diana/conditional-transaction

Convert conditional module test to bok choy
parents 61ef49b9 07573c51
......@@ -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)
......
"""
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()
"""
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())
@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
# 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()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment