Commit 9b54e44b by Calen Pennington

Merge pull request #2602 from cpennington/xblock-acid-child-tests

Add tests of the frontend children api using the AcidXBlock
parents 29857036 dbc99ef8
......@@ -316,9 +316,18 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
# Make public after updating the xblock, in case the caller asked
# for both an update and a publish.
if publish and publish == 'make_public':
def _publish(block):
# This is super gross, but prevents us from publishing something that
# we shouldn't. Ideally, all modulestores would have a consistant
# interface for publishing. However, as of now, only the DraftMongoModulestore
# does, so we have to check for the attribute explicitly.
store = get_modulestore(block.location)
if hasattr(store, 'publish'):
store.publish(block.location, request.user.id)
_xmodule_recurse(
existing_item,
lambda i: modulestore().publish(i.location, request.user.id)
_publish
)
# Note that children aren't being returned until we have a use case.
......
......@@ -131,6 +131,7 @@ def _preview_module_system(request, descriptor):
# get_user_role accepts a location or a CourseLocator.
# If descriptor.location is a CourseLocator, course_id is unused.
get_user_role=lambda: get_user_role(request.user, descriptor.location, course_id),
descriptor_runtime=descriptor.runtime,
)
......@@ -158,6 +159,6 @@ def get_preview_fragment(request, descriptor):
try:
fragment = module.render("student_view")
except Exception as exc: # pylint: disable=W0703
log.debug("Unable to render student_view for %r", module, exc_info=True)
log.warning("Unable to render student_view for %r", module, exc_info=True)
fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
return fragment
......@@ -3,7 +3,7 @@ define ["jquery", "xblock/runtime.v1", "URI"], ($, XBlock, URI) ->
class PreviewRuntime.v1 extends XBlock.Runtime.v1
handlerUrl: (element, handlerName, suffix, query, thirdparty) ->
uri = URI("/preview/xblock").segment($(@element).data('usage-id'))
uri = URI("/preview/xblock").segment($(element).data('usage-id'))
.segment('handler')
.segment(handlerName)
if suffix? then uri.segment(suffix)
......@@ -14,7 +14,7 @@ define ["jquery", "xblock/runtime.v1", "URI"], ($, XBlock, URI) ->
class StudioRuntime.v1 extends XBlock.Runtime.v1
handlerUrl: (element, handlerName, suffix, query, thirdparty) ->
uri = URI("/xblock").segment($(@element).data('usage-id'))
uri = URI("/xblock").segment($(element).data('usage-id'))
.segment('handler')
.segment(handlerName)
if suffix? then uri.segment(suffix)
......
......@@ -78,6 +78,7 @@ def get_test_system(course_id=''):
course_id=course_id,
error_descriptor_class=ErrorDescriptor,
get_user_role=Mock(is_staff=False),
descriptor_runtime=get_test_descriptor_system(),
)
......
......@@ -1048,7 +1048,7 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
"""
def __init__(
self, static_url, track_function, get_module, render_template,
replace_urls, user=None, filestore=None,
replace_urls, descriptor_runtime, user=None, filestore=None,
debug=False, hostname="", xqueue=None, publish=None, node_path="",
anonymous_student_id='', course_id=None,
open_ended_grading_interface=None, s3_interface=None,
......@@ -1089,6 +1089,8 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
that capa_module can use to fix up the static urls in
ajax results.
descriptor_runtime - A `DescriptorSystem` to use for loading xblocks by id
anonymous_student_id - Used for tracking modules with student id
course_id - the course_id containing this module
......@@ -1148,6 +1150,7 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
self.get_real_user = get_real_user
self.get_user_role = get_user_role
self.descriptor_runtime = descriptor_runtime
def get(self, attr):
""" provide uniform access to attributes (like etree)."""
......@@ -1172,7 +1175,7 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
return self.handler_url(self.xmodule_instance, 'xmodule_handler', '', '').rstrip('/?')
def get_block(self, block_id):
raise NotImplementedError("XModules must use get_module to load other modules")
return self.get_module(self.descriptor_runtime.get_block(block_id))
def resource_url(self, resource):
raise NotImplementedError("edX Platform doesn't currently implement XBlock resource urls")
......
......@@ -31,8 +31,8 @@ describe "XBlock", ->
@missingInitBlock = XBlock.initializeBlock($('#missing-init')[0])
it "loads the right runtime version", ->
expect(TestRuntime.vA).toHaveBeenCalledWith($('#vA')[0], @fakeChildren)
expect(TestRuntime.vZ).toHaveBeenCalledWith($('#vZ')[0], @fakeChildren)
expect(TestRuntime.vA).toHaveBeenCalledWith()
expect(TestRuntime.vZ).toHaveBeenCalledWith()
it "loads the right init function", ->
expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, $('#vA')[0])
......
......@@ -9,12 +9,13 @@ describe "XBlock.Runtime.v1", ->
]
@element = $('.xblock')[0]
$(@element).prop('xblock_children', @children)
@runtime = new XBlock.Runtime.v1(@element, @children)
@runtime = new XBlock.Runtime.v1(@element)
it "provides a list of children", ->
expect(@runtime.children).toBe(@children)
expect(@runtime.children(@element)).toBe(@children)
it "maps children by name", ->
expect(@runtime.childMap.childA).toBe(@children[0])
expect(@runtime.childMap.childB).toBe(@children[1])
expect(@runtime.childMap(@element, 'childA')).toBe(@children[0])
expect(@runtime.childMap(@element, 'childB')).toBe(@children[1])
......@@ -7,8 +7,9 @@
runtime = $element.data("runtime-class")
version = $element.data("runtime-version")
initFnName = $element.data("init")
$element.prop('xblock_children', children)
if runtime? and version? and initFnName?
runtime = new window[runtime]["v#{version}"](element, children)
runtime = new window[runtime]["v#{version}"]
initFn = window[initFnName]
block = initFn(runtime, element) ? {}
else
......
class XBlock.Runtime.v1
constructor: (@element, @children) ->
@childMap = {}
$.each @children, (idx, child) =>
@childMap[child.name] = child
children: (block) => $(block).prop('xblock_children')
childMap: (block, childName) =>
for child in @children(block)
return child if child.name == childName
\ No newline at end of file
......@@ -95,31 +95,20 @@ class XBlockFixtureDesc(object):
self.children.extend(args)
return self
def serialize(self, parent_loc=None):
def serialize(self):
"""
Return a JSON representation of the XBlock, suitable
for sending as POST data to /xblock
XBlocks are always set to public visibility.
"""
payload = {
'category': self.category,
return json.dumps({
'display_name': self.display_name,
'data': self.data,
'metadata': self.metadata,
'grader_type': self.grader_type,
'graderType': self.grader_type,
'publish': self.publish
}
# Need to handle detached categories differently, since they are not published
# This may change in the future.
if self.category in ['static_tab']:
del payload['publish']
if parent_loc is not None:
payload['parent_locator'] = parent_loc
return json.dumps(payload)
})
def __str__(self):
"""
......@@ -395,15 +384,25 @@ class CourseFixture(StudioApiFixture):
loc = self._create_xblock(parent_loc, desc)
self._create_xblock_children(loc, desc.children)
self._publish_xblock(parent_loc)
def _create_xblock(self, parent_loc, xblock_desc):
"""
Create an XBlock with `parent_loc` (the location of the parent block)
and `xblock_desc` (an `XBlockFixtureDesc` instance).
"""
create_payload = {
'category': xblock_desc.category,
'display_name': xblock_desc.display_name,
}
if parent_loc is not None:
create_payload['parent_locator'] = parent_loc
# Create the new XBlock
response = self.session.post(
STUDIO_BASE_URL + '/xblock',
data=xblock_desc.serialize(parent_loc=parent_loc),
data=json.dumps(create_payload),
headers=self.headers,
)
......@@ -417,8 +416,6 @@ class CourseFixture(StudioApiFixture):
except ValueError:
raise CourseFixtureError("Could not decode JSON from '{0}'".format(response.content))
if loc is not None:
# Configure the XBlock
response = self.session.post(
STUDIO_BASE_URL + '/xblock/' + loc,
......@@ -433,8 +430,20 @@ class CourseFixture(StudioApiFixture):
"Could not update {0}. Status code: {1}".format(
xblock_desc, response.status_code))
else:
raise CourseFixtureError("Could not retrieve location of {0}".format(xblock_desc))
def _publish_xblock(self, locator):
"""
Publish the xblock at `locator`.
"""
# Create the new XBlock
response = self.session.put(
"{}/xblock/{}".format(STUDIO_BASE_URL, locator),
data=json.dumps({'publish': 'make_public'}),
headers=self.headers,
)
if not response.ok:
msg = "Could not publish {}. Status was {}".format(locator, response.status_code)
raise CourseFixtureError(msg)
def _encode_post_dict(self, post_dict):
"""
......
......@@ -4,7 +4,7 @@ Unit page in Studio
from bok_choy.page_object import PageObject
from bok_choy.query import SubQuery
from bok_choy.promise import Promise, EmptyPromise, fulfill
from bok_choy.promise import EmptyPromise, fulfill
from . import BASE_URL
......@@ -24,20 +24,32 @@ class UnitPage(PageObject):
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._locator(title))
def _locator(self, title):
def _check_func():
locators = self.q(css=Component.BODY_SELECTOR).filter(
SubQuery(css=Component.NAME_SELECTOR).filter(text=title)
).map(lambda el: el['data-locator']).results
# Wait until all components have been loaded
return (
self.is_css_present('body.view-unit') and
len(self.q(css=Component.BODY_SELECTOR)) == len(self.q(css='{} .xblock-student_view'.format(Component.BODY_SELECTOR)))
)
return (len(locators) > 0, locators[0])
@property
def components(self):
"""
Return a list of components loaded on the unit page.
"""
return self.q(css=Component.BODY_SELECTOR).map(lambda el: Component(self.browser, el['data-locator'])).results
return fulfill(Promise(_check_func, "Found data locator for component"))
def edit_draft(self):
"""
Started editing a draft of this unit.
"""
fulfill(EmptyPromise(
lambda: self.q(css='.create-draft').present,
'Wait for edit draft link to be present'
))
self.q(css='.create-draft').click()
fulfill(EmptyPromise(
lambda: self.q(css='.editing-draft-alert').present,
'Wait for draft mode to be activated'
))
class Component(PageObject):
......
......@@ -51,6 +51,16 @@ class AcidView(PageObject):
"""
return self.test_passed('.document-ready-run')
@property
def child_tests_passed(self):
"""
Whether the tests of children passed
"""
return all([
self.test_passed('.child-counts-match'),
self.test_passed('.child-values-match')
])
def scope_passed(self, scope):
return all(
self.test_passed('.scope-storage-test.scope-{} {}'.format(scope, test))
......
......@@ -3,7 +3,7 @@
E2E tests for the LMS.
"""
from unittest import skip
from unittest import skip, expectedFailure
from bok_choy.web_app_test import WebAppTest
from bok_choy.promise import EmptyPromise, fulfill_before, fulfill, Promise
......@@ -292,8 +292,9 @@ class VideoTest(UniqueCourseTest):
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc('video', 'Video')
))).install()
)))).install()
# Auto-auth register for the course
......@@ -338,18 +339,48 @@ class VideoTest(UniqueCourseTest):
self.assertGreaterEqual(self.video.duration, self.video.elapsed_time)
class XBlockAcidTest(UniqueCourseTest):
class XBlockAcidBase(UniqueCourseTest):
"""
Tests that verify that XBlock integration is working correctly
Base class for tests that verify that XBlock integration is working correctly
"""
__test__ = False
def setUp(self):
"""
Create a unique identifier for the course used in this test.
"""
# Ensure that the superclass sets up
super(XBlockAcidTest, self).setUp()
super(XBlockAcidBase, self).setUp()
self.setup_fixtures()
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)
def test_acid_block(self):
"""
Verify that all expected acid block tests pass in the lms.
"""
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
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.child_tests_passed)
self.assertTrue(acid_block.scope_passed('user_state'))
class XBlockAcidNoChildTest(XBlockAcidBase):
"""
Tests of an AcidBlock with no children
"""
__test__ = True
def setup_fixtures(self):
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
......@@ -360,24 +391,43 @@ class XBlockAcidTest(UniqueCourseTest):
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()
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)
class XBlockAcidChildTest(XBlockAcidBase):
"""
Tests of an AcidBlock with children
"""
__test__ = True
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
def setup_fixtures(self):
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').add_children(
XBlockFixtureDesc('acid', 'First Acid Child', metadata={'name': 'first'}),
XBlockFixtureDesc('acid', 'Second Acid Child', metadata={'name': 'second'}),
XBlockFixtureDesc('html', 'Html Child', data="<html>Contents</html>"),
)
)
)
)
).install()
# This will fail until we fix support of children in pure XBlocks
@expectedFailure
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'))
super(XBlockAcidChildTest, self).test_acid_block()
"""
Acceptance tests for Studio.
"""
from unittest import expectedFailure
from bok_choy.web_app_test import WebAppTest
from ..pages.studio.asset_index import AssetIndexPage
......@@ -110,17 +112,18 @@ class CoursePagesTest(UniqueCourseTest):
page.visit()
class XBlockAcidTest(WebAppTest):
class XBlockAcidBase(WebAppTest):
"""
Tests that verify that XBlock integration is working correctly
Base class for tests that verify that XBlock integration is working correctly
"""
__test__ = False
def setUp(self):
"""
Create a unique identifier for the course used in this test.
"""
# Ensure that the superclass sets up
super(XBlockAcidTest, self).setUp()
super(XBlockAcidBase, self).setUp()
# Define a unique course identifier
self.course_info = {
......@@ -140,6 +143,50 @@ class XBlockAcidTest(WebAppTest):
self.course_id = '{org}.{number}.{run}'.format(**self.course_info)
self.setup_fixtures()
self.auth_page.visit()
def test_acid_block_preview(self):
"""
Verify that all expected acid block tests pass in studio preview
"""
self.outline.visit()
unit = self.outline.section('Test Section').subsection('Test Subsection').toggle_expand().unit('Test Unit').go_to()
acid_block = AcidView(self.browser, unit.components[0].preview_selector)
self.assertTrue(acid_block.init_fn_passed)
self.assertTrue(acid_block.doc_ready_passed)
self.assertTrue(acid_block.child_tests_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
"""
self.outline.visit()
unit = self.outline.section('Test Section').subsection('Test Subsection').toggle_expand().unit('Test Unit').go_to()
unit.edit_draft()
acid_block = AcidView(self.browser, unit.components[0].edit().editor_selector)
self.assertTrue(acid_block.init_fn_passed)
self.assertTrue(acid_block.doc_ready_passed)
self.assertTrue(acid_block.child_tests_passed)
self.assertTrue(acid_block.scope_passed('content'))
self.assertTrue(acid_block.scope_passed('settings'))
class XBlockAcidNoChildTest(XBlockAcidBase):
"""
Tests of an AcidBlock with no children
"""
__test__ = True
def setup_fixtures(self):
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
......@@ -157,27 +204,42 @@ class XBlockAcidTest(WebAppTest):
)
).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):
class XBlockAcidChildTest(XBlockAcidBase):
"""
Verify that all expected acid block tests pass in studio preview
Tests of an AcidBlock with children
"""
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'))
__test__ = True
def setup_fixtures(self):
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').add_children(
XBlockFixtureDesc('acid', 'First Acid Child', metadata={'name': 'first'}),
XBlockFixtureDesc('acid', 'Second Acid Child', metadata={'name': 'second'}),
XBlockFixtureDesc('html', 'Html Child', data="<html>Contents</html>"),
)
)
)
)
).install()
# This will fail until we fix support of children in pure XBlocks
@expectedFailure
def test_acid_block_preview(self):
super(XBlockAcidChildTest, self).test_acid_block_preview()
# This will fail until we fix support of children in pure XBlocks
@expectedFailure
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'))
super(XBlockAcidChildTest, self).test_acid_block_editor()
......@@ -227,7 +227,6 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
return None
student_data = KvsFieldData(DjangoKeyValueStore(field_data_cache))
descriptor._field_data = LmsFieldData(descriptor._field_data, student_data)
def make_xqueue_callback(dispatch='score_update'):
......@@ -433,6 +432,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
'i18n': ModuleI18nService(),
},
get_user_role=lambda: get_user_role(user, course_id),
descriptor_runtime=descriptor.runtime,
)
# pass position specified in URL to module through ModuleSystem
......@@ -451,8 +451,8 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
else:
system.error_descriptor_class = NonStaffErrorDescriptor
descriptor.xmodule_runtime = system
descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id)
descriptor.bind_for_student(system, LmsFieldData(descriptor._field_data, student_data)) # pylint: disable=protected-access
descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id) # pylint: disable=protected-access
return descriptor
......
......@@ -2,6 +2,7 @@
Test for lms courseware app, module render unit
"""
from ddt import ddt, data
from functools import partial
from mock import MagicMock, patch, Mock
import json
......@@ -657,6 +658,9 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase):
),
scope_ids=Mock(spec=ScopeIds),
)
# Use the xblock_class's bind_for_student method
descriptor.bind_for_student = partial(xblock_class.bind_for_student, descriptor)
if hasattr(xblock_class, 'module_class'):
descriptor.module_class = xblock_class.module_class
......
......@@ -69,6 +69,7 @@ def peer_grading_notifications(course, user):
get_module=None,
render_template=render_to_string,
replace_urls=None,
descriptor_runtime=None,
)
peer_gs = peer_grading_service.PeerGradingService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
pending_grading = False
......@@ -129,6 +130,7 @@ def combined_notifications(course, user):
get_module=None,
render_template=render_to_string,
replace_urls=None,
descriptor_runtime=None,
)
#Initialize controller query service using our mock system
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
......
......@@ -83,6 +83,7 @@ class StaffGradingService(GradingService):
get_module=None,
render_template=render_to_string,
replace_urls=None,
descriptor_runtime=None,
)
super(StaffGradingService, self).__init__(config)
self.url = config['url'] + config['staff_grading']
......
......@@ -285,6 +285,7 @@ class TestPeerGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
open_ended_grading_interface=test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
mixins=settings.XBLOCK_MIXINS,
error_descriptor_class=ErrorDescriptor,
descriptor_runtime=None,
)
self.descriptor = peer_grading_module.PeerGradingDescriptor(self.system, field_data, ScopeIds(None, None, None, None))
self.descriptor.xmodule_runtime = self.system
......
......@@ -33,6 +33,7 @@ SYSTEM = LmsModuleSystem(
get_module=None,
render_template=render_to_string,
replace_urls=None,
descriptor_runtime=None,
)
......
......@@ -48,6 +48,7 @@ class TestHandlerUrl(TestCase):
render_template=Mock(),
replace_urls=str,
course_id=self.course_id,
descriptor_runtime=Mock(),
)
def test_trailing_characters(self):
......
......@@ -2,8 +2,8 @@
class LmsRuntime.v1 extends XBlock.Runtime.v1
handlerUrl: (element, handlerName, suffix, query, thirdparty) ->
courseId = $(@element).data("course-id")
usageId = $(@element).data("usage-id")
courseId = $(element).data("course-id")
usageId = $(element).data("usage-id")
handlerAuth = if thirdparty then "handler_noauth" else "handler"
uri = URI('/courses').segment(courseId)
......
......@@ -21,6 +21,6 @@
-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.2.1#egg=bok_choy
-e git+https://github.com/edx/bok-choy.git@62de7b576a08f36cde5b030c52bccb1a2f3f8df1#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
-e git+https://github.com/edx/acid-block.git@9c832513f0c01f79227bea894fba11c66fe4c08c#egg=acid-xblock
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