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): ...@@ -106,6 +106,9 @@ def xblock_handler(request, usage_key_string):
:children: the unicode representation of the UsageKeys of children for this xblock. :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 :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. 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 :nullout: which metadata fields to set to None
:graderType: change how this unit is graded :graderType: change how this unit is graded
:isPrereq: Set this xblock as a prerequisite which can be used to limit access to other xblocks :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): ...@@ -169,6 +172,7 @@ def xblock_handler(request, usage_key_string):
prereq_usage_key=request.json.get('prereqUsageKey'), prereq_usage_key=request.json.get('prereqUsageKey'),
prereq_min_score=request.json.get('prereqMinScore'), prereq_min_score=request.json.get('prereqMinScore'),
publish=request.json.get('publish'), publish=request.json.get('publish'),
fields=request.json.get('fields'),
) )
elif request.method in ('PUT', 'POST'): elif request.method in ('PUT', 'POST'):
if 'duplicate_source_locator' in request.json: if 'duplicate_source_locator' in request.json:
...@@ -431,11 +435,13 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None): ...@@ -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, 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. 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 nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
to default). to default).
""" """
store = modulestore() store = modulestore()
# Perform all xblock changes within a (single-versioned) transaction # 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, ...@@ -457,6 +463,10 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None,
else: else:
data = old_content['data'] if 'data' in old_content else None 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: if children_strings is not None:
children = [] children = []
for child_string in children_strings: for child_string in children_strings:
...@@ -610,7 +620,7 @@ def _create_item(request): ...@@ -610,7 +620,7 @@ def _create_item(request):
user=request.user, user=request.user,
category=category, category=category,
display_name=request.json.get('display_name'), display_name=request.json.get('display_name'),
boilerplate=request.json.get('boilerplate') boilerplate=request.json.get('boilerplate'),
) )
return JsonResponse( return JsonResponse(
......
...@@ -805,6 +805,22 @@ class TestEditItem(TestEditItemSetup): ...@@ -805,6 +805,22 @@ class TestEditItem(TestEditItemSetup):
self.assertEqual(sequential.due, datetime(2010, 11, 22, 4, 0, tzinfo=UTC)) self.assertEqual(sequential.due, datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
self.assertEqual(sequential.start, datetime(2010, 9, 12, 14, 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): def test_delete_child(self):
""" """
Test deleting a child. Test deleting a child.
......
...@@ -22,7 +22,8 @@ class XBlockFixtureDesc(object): ...@@ -22,7 +22,8 @@ class XBlockFixtureDesc(object):
Description of an XBlock, used to configure a course fixture. 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. Configure the XBlock to be created by the fixture.
These arguments have the same meaning as in the Studio REST API: These arguments have the same meaning as in the Studio REST API:
...@@ -41,6 +42,7 @@ class XBlockFixtureDesc(object): ...@@ -41,6 +42,7 @@ class XBlockFixtureDesc(object):
self.publish = publish self.publish = publish
self.children = [] self.children = []
self.locator = None self.locator = None
self.fields = kwargs
def add_children(self, *args): def add_children(self, *args):
""" """
...@@ -59,13 +61,15 @@ class XBlockFixtureDesc(object): ...@@ -59,13 +61,15 @@ class XBlockFixtureDesc(object):
XBlocks are always set to public visibility. XBlocks are always set to public visibility.
""" """
return json.dumps({ returned_data = {
'display_name': self.display_name, 'display_name': self.display_name,
'data': self.data, 'data': self.data,
'metadata': self.metadata, 'metadata': self.metadata,
'graderType': self.grader_type, 'graderType': self.grader_type,
'publish': self.publish 'publish': self.publish,
}) 'fields': self.fields,
}
return json.dumps(returned_data)
def __str__(self): def __str__(self):
""" """
...@@ -354,7 +358,7 @@ class CourseFixture(XBlockContainerFixture): ...@@ -354,7 +358,7 @@ class CourseFixture(XBlockContainerFixture):
'children': None, 'children': None,
'data': handouts_html, 'data': handouts_html,
'id': self._handouts_loc, 'id': self._handouts_loc,
'metadata': dict() 'metadata': dict(),
}) })
response = self.session.post(url, data=payload, headers=self.headers) 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