diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 81325cc9fa..a47837fe2c 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -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( diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 0e790b227a..1cb8a6f6b8 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -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. diff --git a/common/test/acceptance/fixtures/course.py b/common/test/acceptance/fixtures/course.py index f48fe74ea4..53c1974b2f 100644 --- a/common/test/acceptance/fixtures/course.py +++ b/common/test/acceptance/fixtures/course.py @@ -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) diff --git a/common/test/acceptance/pages/lms/conditional.py b/common/test/acceptance/pages/lms/conditional.py new file mode 100644 index 0000000000..139c9b7486 --- /dev/null +++ b/common/test/acceptance/pages/lms/conditional.py @@ -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() diff --git a/common/test/acceptance/tests/lms/test_conditional.py b/common/test/acceptance/tests/lms/test_conditional.py new file mode 100644 index 0000000000..7d02665248 --- /dev/null +++ b/common/test/acceptance/tests/lms/test_conditional.py @@ -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='