diff --git a/cms/envs/bok_choy.env.json b/cms/envs/bok_choy.env.json
index 0902499563..4536fb9df5 100644
--- a/cms/envs/bok_choy.env.json
+++ b/cms/envs/bok_choy.env.json
@@ -69,7 +69,8 @@
"ENABLE_S3_GRADE_DOWNLOADS": true,
"PREVIEW_LMS_BASE": "",
"SUBDOMAIN_BRANDING": false,
- "SUBDOMAIN_COURSE_LISTINGS": false
+ "SUBDOMAIN_COURSE_LISTINGS": false,
+ "ALLOW_ALL_ADVANCED_COMPONENTS": true
},
"FEEDBACK_SUBMISSION_EMAIL": "",
"GITHUB_REPO_ROOT": "** OVERRIDDEN **",
diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py
index 1decc4fc87..bc2a61a8ea 100644
--- a/cms/envs/bok_choy.py
+++ b/cms/envs/bok_choy.py
@@ -16,6 +16,7 @@ os.environ['SERVICE_VARIANT'] = 'bok_choy'
os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname() #pylint: disable=E1120
from .aws import * # pylint: disable=W0401, W0614
+from xmodule.x_module import prefer_xmodules
######################### Testing overrides ####################################
@@ -48,5 +49,8 @@ for log_name, log_level in LOG_OVERRIDES:
# Use the auto_auth workflow for creating users and logging them in
FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
+# Enable XBlocks
+XBLOCK_SELECT_FUNCTION = prefer_xmodules
+
# Unfortunately, we need to use debug mode to serve staticfiles
DEBUG = True
diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py
index 537a7592c2..fc95891063 100644
--- a/common/djangoapps/xmodule_modifiers.py
+++ b/common/djangoapps/xmodule_modifiers.py
@@ -10,6 +10,7 @@ import static_replace
from django.conf import settings
from django.utils.timezone import UTC
from edxmako.shortcuts import render_to_string
+from xblock.exceptions import InvalidScopeError
from xblock.fragment import Fragment
from xmodule.seq_module import SequenceModule
@@ -199,7 +200,15 @@ def add_staff_debug_info(user, block, view, frag, context): # pylint: disable=u
if mstart is not None:
is_released = "Yes!" if (now > mstart) else "Not yet"
- staff_context = {'fields': [(name, field.read_from(block)) for name, field in block.fields.items()],
+ field_contents = []
+ for name, field in block.fields.items():
+ try:
+ field_contents.append((name, field.read_from(block)))
+ except InvalidScopeError:
+ log.warning("Unable to read field in Staff Debug information", exc_info=True)
+ field_contents.append((name, "WARNING: Unable to read field"))
+
+ staff_context = {'fields': field_contents,
'xml_attributes': getattr(block, 'xml_attributes', {}),
'location': block.location,
'xqa_key': block.xqa_key,
diff --git a/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py
index 3f6d5602e2..a3bb4439e4 100644
--- a/common/test/acceptance/pages/studio/overview.py
+++ b/common/test/acceptance/pages/studio/overview.py
@@ -1,16 +1,155 @@
"""
Course Outline page in Studio.
"""
+from bok_choy.page_object import PageObject
+from bok_choy.query import SubQuery
+from bok_choy.promise import EmptyPromise, fulfill
from .course_page import CoursePage
+from .unit import UnitPage
-class CourseOutlinePage(CoursePage):
+class CourseOutlineContainer(object):
+ """
+ A mixin to a CourseOutline page object that adds the ability to load
+ a child page object by title.
+
+ CHILD_CLASS must be a :class:`CourseOutlineChild` subclass.
+ """
+ CHILD_CLASS = None
+
+ def child(self, title):
+ return self.CHILD_CLASS(
+ self.browser,
+ self.q(css=self.CHILD_CLASS.BODY_SELECTOR).filter(
+ SubQuery(css=self.CHILD_CLASS.NAME_SELECTOR).filter(text=title)
+ )[0]['data-locator']
+ )
+
+
+class CourseOutlineChild(PageObject):
+ """
+ A mixin to a CourseOutline page object that will be used as a child of
+ :class:`CourseOutlineContainer`.
+ """
+ NAME_SELECTOR = None
+ BODY_SELECTOR = None
+
+ def __init__(self, browser, locator):
+ super(CourseOutlineChild, self).__init__(browser)
+ self.locator = locator
+
+ def is_browser_on_page(self):
+ return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present
+
+ @property
+ def name(self):
+ """
+ Return the display name of this object.
+ """
+ titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text
+ if titles:
+ return titles[0]
+ else:
+ return None
+
+ def __repr__(self):
+ return "{}(, {!r})".format(self.__class__.__name__, self.locator)
+
+ def _bounded_selector(self, selector):
+ """
+ Return `selector`, but limited to this particular `CourseOutlineChild` context
+ """
+ return '{}[data-locator="{}"] {}'.format(
+ self.BODY_SELECTOR,
+ self.locator,
+ selector
+ )
+
+
+class CourseOutlineUnit(CourseOutlineChild):
+ """
+ PageObject that wraps a unit link on the Studio Course Overview page.
+ """
+ url = None
+ BODY_SELECTOR = '.courseware-unit'
+ NAME_SELECTOR = '.unit-name'
+
+ def go_to(self):
+ """
+ Open the unit page linked to by this unit link, and return
+ an initialized :class:`.UnitPage` for that unit.
+ """
+ return UnitPage(self.browser, self.locator).visit()
+
+
+class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer):
+ """
+ :class`.PageObject` that wraps a subsection block on the Studio Course Overview page.
+ """
+ url = None
+
+ BODY_SELECTOR = '.courseware-subsection'
+ NAME_SELECTOR = '.subsection-name-value'
+ CHILD_CLASS = CourseOutlineUnit
+
+ def unit(self, title):
+ """
+ Return the :class:`.CourseOutlineUnit with the title `title`.
+ """
+ return self.child(title)
+
+ def toggle_expand(self):
+ """
+ Toggle the expansion of this subsection.
+ """
+ self.disable_jquery_animations()
+
+ def subsection_expanded():
+ return all(
+ self.q(css=self._bounded_selector('.new-unit-item'))
+ .map(lambda el: el.visible)
+ .results
+ )
+
+ currently_expanded = subsection_expanded()
+
+ self.css_click(self._bounded_selector('.expand-collapse'))
+ fulfill(EmptyPromise(
+ lambda: subsection_expanded() != currently_expanded,
+ "Check that the subsection {} has been toggled".format(self.locator),
+ ))
+ return self
+
+
+class CourseOutlineSection(CourseOutlineChild, CourseOutlineContainer):
+ """
+ :class`.PageObject` that wraps a section block on the Studio Course Overview page.
+ """
+ url = None
+ BODY_SELECTOR = '.courseware-section'
+ NAME_SELECTOR = '.section-name-span'
+ CHILD_CLASS = CourseOutlineSubsection
+
+ def subsection(self, title):
+ """
+ Return the :class:`.CourseOutlineSubsection` with the title `title`.
+ """
+ return self.child(title)
+
+
+class CourseOutlinePage(CoursePage, CourseOutlineContainer):
"""
Course Outline page in Studio.
"""
-
url_path = "course"
+ CHILD_CLASS = CourseOutlineSection
def is_browser_on_page(self):
return self.is_css_present('body.view-outline')
+
+ def section(self, title):
+ """
+ Return the :class:`.CourseOutlineSection` with the title `title`.
+ """
+ return self.child(title)
diff --git a/common/test/acceptance/pages/studio/unit.py b/common/test/acceptance/pages/studio/unit.py
index 530ebe9ef3..9b5934abba 100644
--- a/common/test/acceptance/pages/studio/unit.py
+++ b/common/test/acceptance/pages/studio/unit.py
@@ -3,6 +3,10 @@ Unit page in Studio
"""
from bok_choy.page_object import PageObject
+from bok_choy.query import SubQuery
+from bok_choy.promise import EmptyPromise, fulfill
+
+from . import BASE_URL
class UnitPage(PageObject):
@@ -10,5 +14,77 @@ class UnitPage(PageObject):
Unit page in Studio
"""
+ def __init__(self, browser, unit_locator):
+ super(UnitPage, self).__init__(browser)
+ self.unit_locator = unit_locator
+
+ @property
+ def url(self):
+ """URL to the static pages UI in a course."""
+ return "{}/unit/{}".format(BASE_URL, self.unit_locator)
+
def is_browser_on_page(self):
return self.is_css_present('body.view-unit')
+
+ def component(self, title):
+ return Component(
+ self.browser,
+ self.q(css=Component.BODY_SELECTOR).filter(
+ SubQuery(css=Component.NAME_SELECTOR).filter(text=title)
+ )[0]['data-locator']
+ )
+
+
+class Component(PageObject):
+ """
+ A PageObject representing an XBlock child on the Studio UnitPage (including
+ the editing controls).
+ """
+ url = None
+ BODY_SELECTOR = '.component'
+ NAME_SELECTOR = '.component-header'
+
+ def __init__(self, browser, locator):
+ super(Component, self).__init__(browser)
+ self.locator = locator
+
+ def is_browser_on_page(self):
+ return self.is_css_present('{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator))
+
+ def _bounded_selector(self, selector):
+ """
+ Return `selector`, but limited to this particular `CourseOutlineChild` context
+ """
+ return '{}[data-locator="{}"] {}'.format(
+ self.BODY_SELECTOR,
+ self.locator,
+ selector
+ )
+
+ @property
+ def name(self):
+ titles = self.css_text(self._bounded_selector(self.NAME_SELECTOR))
+ if titles:
+ return titles[0]
+ else:
+ return None
+
+ @property
+ def preview_selector(self):
+ return self._bounded_selector('.xblock-student_view')
+
+ def edit(self):
+ self.css_click(self._bounded_selector('.edit-button'))
+ fulfill(EmptyPromise(
+ lambda: all(
+ self.q(css=self._bounded_selector('.component-editor'))
+ .map(lambda el: el.visible)
+ .results
+ ),
+ "Verify that the editor for component {} has been expanded".format(self.locator)
+ ))
+ return self
+
+ @property
+ def editor_selector(self):
+ return self._bounded_selector('.xblock-studio_view')
diff --git a/common/test/acceptance/pages/xblock/__init__.py b/common/test/acceptance/pages/xblock/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/common/test/acceptance/pages/xblock/acid.py b/common/test/acceptance/pages/xblock/acid.py
new file mode 100644
index 0000000000..add30c3b91
--- /dev/null
+++ b/common/test/acceptance/pages/xblock/acid.py
@@ -0,0 +1,66 @@
+"""
+PageObjects related to the AcidBlock
+"""
+
+from bok_choy.page_object import PageObject
+from bok_choy.promise import EmptyPromise, BrokenPromise, fulfill
+
+
+class AcidView(PageObject):
+ """
+ A :class:`.PageObject` representing the rendered view of the :class:`.AcidBlock`.
+ """
+ url = None
+
+ def __init__(self, browser, context_selector):
+ """
+ Args:
+ browser (splinter.browser.Browser): The browser that this page is loaded in.
+ context_selector (str): The selector that identifies where this :class:`.AcidBlock` view
+ is on the page.
+ """
+ super(AcidView, self).__init__(browser)
+ if isinstance(context_selector, unicode):
+ context_selector = context_selector.encode('utf-8')
+ self.context_selector = context_selector
+
+ def is_browser_on_page(self):
+ return (
+ self.is_css_present('{} .acid-block'.format(self.context_selector)) and
+ self.browser.evaluate_script("$({!r}).data('initialized')".format(self.context_selector))
+ )
+
+ def test_passed(self, test_selector):
+ """
+ Return whether a particular :class:`.AcidBlock` test passed.
+ """
+ selector = '{} .acid-block {} .pass'.format(self.context_selector, test_selector)
+ return bool(self.q(css=selector).execute(try_interval=0.1, timeout=3))
+
+ @property
+ def init_fn_passed(self):
+ """
+ Whether the init-fn test passed in this view of the :class:`.AcidBlock`.
+ """
+ return self.test_passed('.js-init-run')
+
+ @property
+ def doc_ready_passed(self):
+ """
+ Whether the document-ready test passed in this view of the :class:`.AcidBlock`.
+ """
+ return self.test_passed('.document-ready-run')
+
+ def scope_passed(self, scope):
+ return all(
+ self.test_passed('.scope-storage-test.scope-{} {}'.format(scope, test))
+ for test in (
+ ".server-storage-test-returned",
+ ".server-storage-test-succeeded",
+ ".client-storage-test-returned",
+ ".client-storage-test-succeeded",
+ )
+ )
+
+ def __repr__(self):
+ return "{}(, {!r})".format(self.__class__.__name__, self.context_selector)
diff --git a/common/test/acceptance/tests/test_lms.py b/common/test/acceptance/tests/test_lms.py
index b3573566f7..4f65729895 100644
--- a/common/test/acceptance/tests/test_lms.py
+++ b/common/test/acceptance/tests/test_lms.py
@@ -18,6 +18,7 @@ from ..pages.lms.tab_nav import TabNavPage
from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.progress import ProgressPage
from ..pages.lms.video import VideoPage
+from ..pages.xblock.acid import AcidView
from ..fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
@@ -103,11 +104,13 @@ class HighLevelTabTest(UniqueCourseTest):
XBlockFixtureDesc('problem', 'Test Problem 1', data=load_data_str('multiple_choice.xml')),
XBlockFixtureDesc('problem', 'Test Problem 2', data=load_data_str('formula_problem.xml')),
XBlockFixtureDesc('html', 'Test HTML'),
- )),
+ )
+ ),
XBlockFixtureDesc('chapter', 'Test Section 2').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection 2'),
XBlockFixtureDesc('sequential', 'Test Subsection 3'),
- )).install()
+ )
+ ).install()
# Auto-auth register for the course
AutoAuthPage(self.browser, course_id=self.course_id).visit()
@@ -252,3 +255,48 @@ class VideoTest(UniqueCourseTest):
# latency through the ssh tunnel
self.assertGreaterEqual(self.video.elapsed_time, 0)
self.assertGreaterEqual(self.video.duration, self.video.elapsed_time)
+
+
+class XBlockAcidTest(UniqueCourseTest):
+ """
+ Tests that verify that XBlock integration is working correctly
+ """
+
+ def setUp(self):
+ """
+ Create a unique identifier for the course used in this test.
+ """
+ # Ensure that the superclass sets up
+ super(XBlockAcidTest, self).setUp()
+
+ course_fix = CourseFixture(
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run'],
+ self.course_info['display_name']
+ )
+
+ course_fix.add_children(
+ XBlockFixtureDesc('chapter', 'Test Section').add_children(
+ XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
+ XBlockFixtureDesc('acid', 'Acid Block')
+ )
+ )
+ ).install()
+
+ AutoAuthPage(self.browser, course_id=self.course_id).visit()
+
+ self.course_info_page = CourseInfoPage(self.browser, self.course_id)
+ self.tab_nav = TabNavPage(self.browser)
+
+ self.course_info_page.visit()
+ self.tab_nav.go_to_tab('Courseware')
+
+ def test_acid_block(self):
+ """
+ Verify that all expected acid block tests pass in the lms.
+ """
+ acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
+ self.assertTrue(acid_block.init_fn_passed)
+ self.assertTrue(acid_block.doc_ready_passed)
+ self.assertTrue(acid_block.scope_passed('user_state'))
diff --git a/common/test/acceptance/tests/test_studio.py b/common/test/acceptance/tests/test_studio.py
index df5eb07a5b..989533145d 100644
--- a/common/test/acceptance/tests/test_studio.py
+++ b/common/test/acceptance/tests/test_studio.py
@@ -20,7 +20,8 @@ from ..pages.studio.settings_advanced import AdvancedSettingsPage
from ..pages.studio.settings_graders import GradingPage
from ..pages.studio.signup import SignupPage
from ..pages.studio.textbooks import TextbooksPage
-from ..fixtures.course import CourseFixture
+from ..pages.xblock.acid import AcidView
+from ..fixtures.course import CourseFixture, XBlockFixtureDesc
from .helpers import UniqueCourseTest
@@ -107,3 +108,76 @@ class CoursePagesTest(UniqueCourseTest):
# Verify that each page is available
for page in self.pages:
page.visit()
+
+
+class XBlockAcidTest(WebAppTest):
+ """
+ Tests that verify that XBlock integration is working correctly
+ """
+
+ def setUp(self):
+ """
+ Create a unique identifier for the course used in this test.
+ """
+ # Ensure that the superclass sets up
+ super(XBlockAcidTest, self).setUp()
+
+ # Define a unique course identifier
+ self.course_info = {
+ 'org': 'test_org',
+ 'number': 'course_' + self.unique_id[:5],
+ 'run': 'test_' + self.unique_id,
+ 'display_name': 'Test Course ' + self.unique_id
+ }
+
+ self.auth_page = AutoAuthPage(self.browser, staff=True)
+ self.outline = CourseOutlinePage(
+ self.browser,
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run']
+ )
+
+ self.course_id = '{org}.{number}.{run}'.format(**self.course_info)
+
+ course_fix = CourseFixture(
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run'],
+ self.course_info['display_name']
+ )
+
+ course_fix.add_children(
+ XBlockFixtureDesc('chapter', 'Test Section').add_children(
+ XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
+ XBlockFixtureDesc('vertical', 'Test Unit').add_children(
+ XBlockFixtureDesc('acid', 'Acid Block')
+ )
+ )
+ )
+ ).install()
+
+ self.auth_page.visit()
+
+ self.outline.visit()
+ unit = self.outline.section('Test Section').subsection('Test Subsection').toggle_expand().unit('Test Unit').go_to()
+ self.acid_component = unit.component('Acid Block')
+
+ def test_acid_block_preview(self):
+ """
+ Verify that all expected acid block tests pass in studio preview
+ """
+ acid_block = AcidView(self.browser, self.acid_component.preview_selector)
+ self.assertTrue(acid_block.init_fn_passed)
+ self.assertTrue(acid_block.doc_ready_passed)
+ self.assertTrue(acid_block.scope_passed('user_state'))
+
+ def test_acid_block_editor(self):
+ """
+ Verify that all expected acid block tests pass in studio preview
+ """
+ acid_block = AcidView(self.browser, self.acid_component.edit().editor_selector)
+ self.assertTrue(acid_block.init_fn_passed)
+ self.assertTrue(acid_block.doc_ready_passed)
+ self.assertTrue(acid_block.scope_passed('content'))
+ self.assertTrue(acid_block.scope_passed('settings'))
diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py
index 851ae22dd6..ae8f5bacf0 100644
--- a/lms/envs/bok_choy.py
+++ b/lms/envs/bok_choy.py
@@ -4,6 +4,7 @@ Settings for bok choy tests
import os
from path import path
+from xmodule.x_module import prefer_xmodules
CONFIG_ROOT = path(__file__).abspath().dirname() #pylint: disable=E1120
@@ -59,5 +60,8 @@ LOG_OVERRIDES = [
for log_name, log_level in LOG_OVERRIDES:
logging.getLogger(log_name).setLevel(log_level)
+# Enable XBlocks
+XBLOCK_SELECT_FUNCTION = prefer_xmodules
+
# Unfortunately, we need to use debug mode to serve staticfiles
DEBUG = True
diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt
index e4a09dff5c..d32df83bb1 100644
--- a/requirements/edx/github.txt
+++ b/requirements/edx/github.txt
@@ -15,11 +15,12 @@
-e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries:
--e git+https://github.com/edx/XBlock.git@3830ee50015b460fad63ff3b71f77bf1a2684195#egg=XBlock
+-e git+https://github.com/edx/XBlock.git@6d431d786587bd8f3a19a893364914d6e2d6c28f#egg=XBlock
-e git+https://github.com/edx/codejail.git@e3d98f9455#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.2.9#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool
-e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle
-e git+https://github.com/edx/event-tracking.git@f0211d702d#egg=event-tracking
--e git+https://github.com/edx/bok-choy.git@v0.1.0#egg=bok_choy
+-e git+https://github.com/edx/bok-choy.git@v0.2.1#egg=bok_choy
-e git+https://github.com/edx-solutions/django-splash.git@15bf143b15714e22fc451ff1b0f8a7a2a9483172#egg=django-splash
+-e git+https://github.com/edx/acid-block.git@aa95a3c#egg=acid-xblock