Commit d919d2ae by Calen Pennington

Teach LMS how to render XBlockAsides

[PLAT-217]
parent e20fe2b8
...@@ -201,10 +201,11 @@ def _upload_asset(request, course_key): ...@@ -201,10 +201,11 @@ def _upload_asset(request, course_key):
'File {filename} exceeds maximum size of ' 'File {filename} exceeds maximum size of '
'{size_mb} MB. Please follow the instructions here ' '{size_mb} MB. Please follow the instructions here '
'to upload a file elsewhere and link to it instead: ' 'to upload a file elsewhere and link to it instead: '
'{faq_url}').format( '{faq_url}'
filename=filename, ).format(
size_mb=settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB, filename=filename,
faq_url=settings.MAX_ASSET_UPLOAD_FILE_SIZE_URL, size_mb=settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB,
faq_url=settings.MAX_ASSET_UPLOAD_FILE_SIZE_URL,
) )
}, status=413) }, status=413)
......
...@@ -45,7 +45,7 @@ from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primar ...@@ -45,7 +45,7 @@ from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primar
from contentstore.views.preview import get_preview_fragment from contentstore.views.preview import get_preview_fragment
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from cms.lib.xblock.runtime import handler_url, local_resource_url from cms.lib.xblock.runtime import handler_url, local_resource_url, get_asides
from opaque_keys.edx.keys import UsageKey, CourseKey from opaque_keys.edx.keys import UsageKey, CourseKey
__all__ = ['orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler'] __all__ = ['orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler']
...@@ -64,6 +64,7 @@ ALWAYS = lambda x: True ...@@ -64,6 +64,7 @@ ALWAYS = lambda x: True
# TODO: Remove this code when Runtimes are no longer created by modulestores # TODO: Remove this code when Runtimes are no longer created by modulestores
xmodule.x_module.descriptor_global_handler_url = handler_url xmodule.x_module.descriptor_global_handler_url = handler_url
xmodule.x_module.descriptor_global_local_resource_url = local_resource_url xmodule.x_module.descriptor_global_local_resource_url = local_resource_url
xmodule.x_module.descriptor_global_get_asides = get_asides
def hash_resource(resource): def hash_resource(resource):
......
...@@ -95,6 +95,10 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -95,6 +95,10 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
def local_resource_url(self, block, uri): def local_resource_url(self, block, uri):
return local_resource_url(block, uri) return local_resource_url(block, uri)
def get_asides(self, block):
# TODO: Implement this to enable XBlockAsides on previews in Studio
return []
class StudioUserService(object): class StudioUserService(object):
""" """
...@@ -110,7 +114,7 @@ class StudioUserService(object): ...@@ -110,7 +114,7 @@ class StudioUserService(object):
return self._request.user.id return self._request.user.id
def _preview_module_system(request, descriptor): def _preview_module_system(request, descriptor, field_data):
""" """
Returns a ModuleSystem for the specified descriptor that is specialized for Returns a ModuleSystem for the specified descriptor that is specialized for
rendering module previews. rendering module previews.
...@@ -163,6 +167,7 @@ def _preview_module_system(request, descriptor): ...@@ -163,6 +167,7 @@ def _preview_module_system(request, descriptor):
descriptor_runtime=descriptor.runtime, descriptor_runtime=descriptor.runtime,
services={ services={
"i18n": ModuleI18nService(), "i18n": ModuleI18nService(),
"field-data": field_data,
}, },
) )
...@@ -181,7 +186,7 @@ def _load_preview_module(request, descriptor): ...@@ -181,7 +186,7 @@ def _load_preview_module(request, descriptor):
else: else:
field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access
descriptor.bind_for_student( descriptor.bind_for_student(
_preview_module_system(request, descriptor), _preview_module_system(request, descriptor, field_data),
field_data field_data
) )
return descriptor return descriptor
......
...@@ -33,3 +33,14 @@ def local_resource_url(block, uri): ...@@ -33,3 +33,14 @@ def local_resource_url(block, uri):
'block_type': block.scope_ids.block_type, 'block_type': block.scope_ids.block_type,
'uri': uri, 'uri': uri,
}) })
def get_asides(block): # pylint: disable=unused-argument
"""
Return all of the asides which might be decorating this `block`.
Arguments:
block (:class:`.XBlock`): The block to render retrieve asides for.
"""
# TODO: Implement this method to make XBlockAsides for editing views in Studio
return []
...@@ -32,6 +32,7 @@ requirejs.config({ ...@@ -32,6 +32,7 @@ requirejs.config({
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce", "jquery.tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce",
"xmodule": "xmodule_js/src/xmodule", "xmodule": "xmodule_js/src/xmodule",
"xblock/cms.runtime.v1": "coffee/src/xblock/cms.runtime.v1", "xblock/cms.runtime.v1": "coffee/src/xblock/cms.runtime.v1",
"xblock/core": "xmodule_js/common_static/js/xblock/core",
"xblock": "xmodule_js/common_static/coffee/src/xblock", "xblock": "xmodule_js/common_static/coffee/src/xblock",
"utility": "xmodule_js/common_static/js/src/utility", "utility": "xmodule_js/common_static/js/src/utility",
"accessibility": "xmodule_js/common_static/js/src/accessibility_tools", "accessibility": "xmodule_js/common_static/js/src/accessibility_tools",
......
...@@ -30,6 +30,7 @@ requirejs.config({ ...@@ -30,6 +30,7 @@ requirejs.config({
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce", "jquery.tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce",
"xmodule": "xmodule_js/src/xmodule", "xmodule": "xmodule_js/src/xmodule",
"xblock/cms.runtime.v1": "coffee/src/xblock/cms.runtime.v1", "xblock/cms.runtime.v1": "coffee/src/xblock/cms.runtime.v1",
"xblock/core": "xmodule_js/common_static/js/xblock/core",
"xblock": "xmodule_js/common_static/coffee/src/xblock", "xblock": "xmodule_js/common_static/coffee/src/xblock",
"utility": "xmodule_js/common_static/js/src/utility", "utility": "xmodule_js/common_static/js/src/utility",
"sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1", "sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1",
......
...@@ -60,6 +60,7 @@ lib_paths: ...@@ -60,6 +60,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/URI.min.js - xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min.js - xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/js/xblock/
- xmodule_js/common_static/coffee/src/xblock/ - xmodule_js/common_static/coffee/src/xblock/
- xmodule_js/common_static/js/vendor/URI.min.js - xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js - xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js
......
...@@ -55,6 +55,7 @@ lib_paths: ...@@ -55,6 +55,7 @@ lib_paths:
- xmodule_js/src/xmodule.js - xmodule_js/src/xmodule.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/js/test/i18n.js - xmodule_js/common_static/js/test/i18n.js
- xmodule_js/common_static/js/xblock/
- xmodule_js/common_static/coffee/src/xblock/ - xmodule_js/common_static/coffee/src/xblock/
- xmodule_js/common_static/js/vendor/URI.min.js - xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js - xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js
......
...@@ -35,6 +35,7 @@ require.config({ ...@@ -35,6 +35,7 @@ require.config({
"tinymce": "js/vendor/tinymce/js/tinymce/tinymce.full.min", "tinymce": "js/vendor/tinymce/js/tinymce/tinymce.full.min",
"jquery.tinymce": "js/vendor/tinymce/js/tinymce/jquery.tinymce.min", "jquery.tinymce": "js/vendor/tinymce/js/tinymce/jquery.tinymce.min",
"xmodule": "/xmodule/xmodule", "xmodule": "/xmodule/xmodule",
"xblock/core": "js/xblock/core",
"xblock": "coffee/src/xblock", "xblock": "coffee/src/xblock",
"utility": "js/src/utility", "utility": "js/src/utility",
"accessibility": "js/src/accessibility_tools", "accessibility": "js/src/accessibility_tools",
......
"""
Useful django models for implementing XBlock infrastructure in django.
"""
import warnings import warnings
from django.db import models from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey, UsageKey, BlockTypeKey
from opaque_keys.edx.locator import Locator from opaque_keys.edx.locator import Locator
from south.modelsinspector import add_introspection_rules from south.modelsinspector import add_introspection_rules
...@@ -91,7 +95,6 @@ class OpaqueKeyField(models.CharField): ...@@ -91,7 +95,6 @@ class OpaqueKeyField(models.CharField):
super(OpaqueKeyField, self).__init__(*args, **kwargs) super(OpaqueKeyField, self).__init__(*args, **kwargs)
def to_python(self, value): def to_python(self, value):
if value is self.Empty or value is None: if value is self.Empty or value is None:
return None return None
...@@ -140,22 +143,38 @@ class OpaqueKeyField(models.CharField): ...@@ -140,22 +143,38 @@ class OpaqueKeyField(models.CharField):
class CourseKeyField(OpaqueKeyField): class CourseKeyField(OpaqueKeyField):
"""
A django Field that stores a CourseKey object as a string.
"""
description = "A CourseKey object, saved to the DB in the form of a string" description = "A CourseKey object, saved to the DB in the form of a string"
KEY_CLASS = CourseKey KEY_CLASS = CourseKey
class UsageKeyField(OpaqueKeyField): class UsageKeyField(OpaqueKeyField):
"""
A django Field that stores a UsageKey object as a string.
"""
description = "A Location object, saved to the DB in the form of a string" description = "A Location object, saved to the DB in the form of a string"
KEY_CLASS = UsageKey KEY_CLASS = UsageKey
class LocationKeyField(UsageKeyField): class LocationKeyField(UsageKeyField):
"""
A django Field that stores a UsageKey object as a string.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
warnings.warn("LocationKeyField is deprecated. Please use UsageKeyField instead.", stacklevel=2) warnings.warn("LocationKeyField is deprecated. Please use UsageKeyField instead.", stacklevel=2)
super(LocationKeyField, self).__init__(*args, **kwargs) super(LocationKeyField, self).__init__(*args, **kwargs)
class BlockTypeKeyField(OpaqueKeyField):
"""
A django Field that stores a BlockTypeKey object as a string.
"""
description = "A BlockTypeKey object, saved to the DB in the form of a string."
KEY_CLASS = BlockTypeKey
add_introspection_rules([], ["^xmodule_django\.models\.CourseKeyField"]) add_introspection_rules([], [r"^xmodule_django\.models\.CourseKeyField"])
add_introspection_rules([], ["^xmodule_django\.models\.LocationKeyField"]) add_introspection_rules([], [r"^xmodule_django\.models\.LocationKeyField"])
add_introspection_rules([], ["^xmodule_django\.models\.UsageKeyField"]) add_introspection_rules([], [r"^xmodule_django\.models\.UsageKeyField"])
...@@ -72,7 +72,11 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer, ...@@ -72,7 +72,11 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
data = {} data = {}
data.update(extra_data) data.update(extra_data)
css_classes = ['xblock', 'xblock-{}'.format(markupsafe.escape(view))]
css_classes = [
'xblock',
'xblock-{}'.format(markupsafe.escape(view))
]
if isinstance(block, (XModule, XModuleDescriptor)): if isinstance(block, (XModule, XModuleDescriptor)):
if view in PREVIEW_VIEWS: if view in PREVIEW_VIEWS:
...@@ -90,9 +94,10 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer, ...@@ -90,9 +94,10 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
data['init'] = frag.js_init_fn data['init'] = frag.js_init_fn
data['runtime-class'] = runtime_class data['runtime-class'] = runtime_class
data['runtime-version'] = frag.js_init_version data['runtime-version'] = frag.js_init_version
data['block-type'] = block.scope_ids.block_type
data['usage-id'] = usage_id_serializer(block.scope_ids.usage_id) data['block-type'] = block.scope_ids.block_type
data['request-token'] = request_token data['usage-id'] = usage_id_serializer(block.scope_ids.usage_id)
data['request-token'] = request_token
if block.name: if block.name:
data['name'] = block.name data['name'] = block.name
......
...@@ -33,7 +33,7 @@ class Date(JSONField): ...@@ -33,7 +33,7 @@ class Date(JSONField):
result = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED1) result = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED1)
result_other = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED2) result_other = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED2)
if result != result_other: if result != result_other:
log.warning("Field {0} is missing month or day".format(self._name, field)) log.warning("Field {0} is missing month or day".format(self.name))
return None return None
if result.tzinfo is None: if result.tzinfo is None:
result = result.replace(tzinfo=UTC) result = result.replace(tzinfo=UTC)
...@@ -59,7 +59,7 @@ class Date(JSONField): ...@@ -59,7 +59,7 @@ class Date(JSONField):
return field return field
else: else:
msg = "Field {0} has bad value '{1}'".format( msg = "Field {0} has bad value '{1}'".format(
self._name, field) self.name, field)
raise TypeError(msg) raise TypeError(msg)
def to_json(self, value): def to_json(self, value):
...@@ -199,7 +199,7 @@ class RelativeTime(JSONField): ...@@ -199,7 +199,7 @@ class RelativeTime(JSONField):
if isinstance(value, basestring): if isinstance(value, basestring):
return self.isotime_to_timedelta(value) return self.isotime_to_timedelta(value)
msg = "RelativeTime Field {0} has bad value '{1!r}'".format(self._name, value) msg = "RelativeTime Field {0} has bad value '{1!r}'".format(self.name, value)
raise TypeError(msg) raise TypeError(msg)
def to_json(self, value): def to_json(self, value):
......
...@@ -50,6 +50,7 @@ from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished ...@@ -50,6 +50,7 @@ from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError, ReferentialIntegrityError from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError, ReferentialIntegrityError
from xmodule.modulestore.inheritance import InheritanceMixin, inherit_metadata, InheritanceKeyValueStore from xmodule.modulestore.inheritance import InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
from xmodule.modulestore.xml import CourseLocationManager
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -173,6 +174,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -173,6 +174,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
render_template: a function for rendering templates, as per render_template: a function for rendering templates, as per
MakoDescriptorSystem MakoDescriptorSystem
""" """
id_manager = CourseLocationManager(course_key)
kwargs.setdefault('id_reader', id_manager)
kwargs.setdefault('id_generator', id_manager)
super(CachingDescriptorSystem, self).__init__( super(CachingDescriptorSystem, self).__init__(
field_data=None, field_data=None,
load_item=self.load_item, load_item=self.load_item,
......
import sys import sys
import logging import logging
from contracts import contract, new_contract from contracts import contract, new_contract
from fs.osfs import OSFS
from lazy import lazy from lazy import lazy
from xblock.runtime import KvsFieldData from xblock.runtime import KvsFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
...@@ -8,13 +9,13 @@ from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, L ...@@ -8,13 +9,13 @@ from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, L
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from ..exceptions import ItemNotFoundError
from .split_mongo_kvs import SplitMongoKVS
from fs.osfs import OSFS
from .definition_lazy_loader import DefinitionLazyLoader
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import inheriting_field_data, InheritanceMixin from xmodule.modulestore.inheritance import inheriting_field_data, InheritanceMixin
from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope
from xmodule.modulestore.split_mongo.id_manager import SplitMongoIdManager
from xmodule.modulestore.split_mongo.definition_lazy_loader import DefinitionLazyLoader
from xmodule.modulestore.split_mongo.split_mongo_kvs import SplitMongoKVS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -54,6 +55,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -54,6 +55,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
root = modulestore.fs_root / course_entry.structure['_id'] root = modulestore.fs_root / course_entry.structure['_id']
root.makedirs_p() # create directory if it doesn't exist root.makedirs_p() # create directory if it doesn't exist
id_manager = SplitMongoIdManager(self)
kwargs.setdefault('id_reader', id_manager)
kwargs.setdefault('id_generator', id_manager)
super(CachingDescriptorSystem, self).__init__( super(CachingDescriptorSystem, self).__init__(
field_data=None, field_data=None,
load_item=self._load_item, load_item=self._load_item,
......
"""
An implementation of IdReader and IdGenerator that manages ids for the SplitMongo storage
mechanism.
"""
from opaque_keys.edx.locator import LocalId, DefinitionLocator
from xmodule.x_module import OpaqueKeyReader, AsideKeyGenerator
from xmodule.modulestore.split_mongo import BlockKey
# TODO: Migrate split_mongo to use this class for all key mapping/creation.
class SplitMongoIdManager(OpaqueKeyReader, AsideKeyGenerator): # pylint: disable=abstract-method
"""
An IdManager that knows how to retrieve the DefinitionLocator, given
a usage_id and a :class:`.CachingDescriptorSystem`.
"""
def __init__(self, caching_descriptor_system):
self._cds = caching_descriptor_system
def get_definition_id(self, usage_id):
if isinstance(usage_id.block_id, LocalId):
# a LocalId indicates that this block hasn't been persisted yet, and is instead stored
# in-memory in the local_modules dictionary.
return self._cds.local_modules[usage_id].scope_ids.def_id
else:
block_key = BlockKey.from_usage_key(usage_id)
module_data = self._cds.get_module_data(block_key, usage_id.course_key)
if 'definition' in module_data:
return DefinitionLocator(usage_id.block_type, module_data['definition'])
else:
raise ValueError("All non-local blocks should have a definition specified")
...@@ -203,5 +203,6 @@ class TestLibraries(MixedSplitTestCase): ...@@ -203,5 +203,6 @@ class TestLibraries(MixedSplitTestCase):
message = u"Hello world" message = u"Hello world"
hello_render = lambda _, context: Fragment(message) hello_render = lambda _, context: Fragment(message)
with patch('xmodule.html_module.HtmlDescriptor.author_view', hello_render, create=True): with patch('xmodule.html_module.HtmlDescriptor.author_view', hello_render, create=True):
result = library.render(AUTHOR_VIEW, context) with patch('xmodule.x_module.descriptor_global_get_asides', lambda block: []):
result = library.render(AUTHOR_VIEW, context)
self.assertIn(message, result.content) self.assertIn(message, result.content)
...@@ -420,7 +420,7 @@ class TestMongoModuleStore(TestMongoModuleStoreBase): ...@@ -420,7 +420,7 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
assert_equals(len(course_locations), 1) assert_equals(len(course_locations), 1)
assert_in(SlashSeparatedCourseKey('edX', 'simple', '2012_Fall'), course_locations) assert_in(SlashSeparatedCourseKey('edX', 'simple', '2012_Fall'), course_locations)
@Plugin.register_temp_plugin(ReferenceTestXBlock, 'ref_test') @XBlock.register_temp_plugin(ReferenceTestXBlock, 'ref_test')
def test_reference_converters(self): def test_reference_converters(self):
""" """
Test that references types get deserialized correctly Test that references types get deserialized correctly
......
...@@ -18,7 +18,7 @@ from contextlib import contextmanager ...@@ -18,7 +18,7 @@ from contextlib import contextmanager
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import make_error_tracker, exc_info_to_str from xmodule.errortracker import make_error_tracker, exc_info_to_str
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XMLParsingSystem, policy_key from xmodule.x_module import XMLParsingSystem, policy_key, OpaqueKeyReader, AsideKeyGenerator
from xmodule.modulestore.xml_exporter import DEFAULT_CONTENT_FIELDS from xmodule.modulestore.xml_exporter import DEFAULT_CONTENT_FIELDS
from xmodule.modulestore import ModuleStoreEnum, ModuleStoreReadBase from xmodule.modulestore import ModuleStoreEnum, ModuleStoreReadBase
from xmodule.tabs import CourseTabList from xmodule.tabs import CourseTabList
...@@ -27,7 +27,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location ...@@ -27,7 +27,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
from xblock.runtime import DictKeyValueStore, IdGenerator from xblock.runtime import DictKeyValueStore
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
...@@ -64,7 +64,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -64,7 +64,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
""" """
self.unnamed = defaultdict(int) # category -> num of new url_names for that category self.unnamed = defaultdict(int) # category -> num of new url_names for that category
self.used_names = defaultdict(set) # category -> set of used url_names self.used_names = defaultdict(set) # category -> set of used url_names
id_generator = CourseLocationGenerator(course_id)
# cdodge: adding the course_id as passed in for later reference rather than having to recomine the org/course/url_name # cdodge: adding the course_id as passed in for later reference rather than having to recomine the org/course/url_name
self.course_id = course_id self.course_id = course_id
...@@ -175,7 +174,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -175,7 +174,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
descriptor = create_block_from_xml( descriptor = create_block_from_xml(
etree.tostring(xml_data, encoding='unicode'), etree.tostring(xml_data, encoding='unicode'),
self, self,
id_generator, id_manager,
) )
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
if not self.load_error_modules: if not self.load_error_modules:
...@@ -201,7 +200,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -201,7 +200,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
descriptor = ErrorDescriptor.from_xml( descriptor = ErrorDescriptor.from_xml(
xml, xml,
self, self,
id_generator, id_manager,
err_msg err_msg
) )
...@@ -229,12 +228,16 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -229,12 +228,16 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
resources_fs = OSFS(xmlstore.data_dir / course_dir) resources_fs = OSFS(xmlstore.data_dir / course_dir)
id_manager = CourseLocationManager(course_id)
super(ImportSystem, self).__init__( super(ImportSystem, self).__init__(
load_item=load_item, load_item=load_item,
resources_fs=resources_fs, resources_fs=resources_fs,
render_template=render_template, render_template=render_template,
error_tracker=error_tracker, error_tracker=error_tracker,
process_xml=process_xml, process_xml=process_xml,
id_generator=id_manager,
id_reader=id_manager,
**kwargs **kwargs
) )
...@@ -245,12 +248,13 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -245,12 +248,13 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
block.children.append(child_block.scope_ids.usage_id) block.children.append(child_block.scope_ids.usage_id)
class CourseLocationGenerator(IdGenerator): class CourseLocationManager(OpaqueKeyReader, AsideKeyGenerator):
""" """
IdGenerator for Location-based definition ids and usage ids IdGenerator for Location-based definition ids and usage ids
based within a course based within a course
""" """
def __init__(self, course_id): def __init__(self, course_id):
super(CourseLocationManager, self).__init__()
self.course_id = course_id self.course_id = course_id
self.autogen_ids = itertools.count(0) self.autogen_ids = itertools.count(0)
...@@ -263,6 +267,17 @@ class CourseLocationGenerator(IdGenerator): ...@@ -263,6 +267,17 @@ class CourseLocationGenerator(IdGenerator):
slug = 'autogen_{}_{}'.format(block_type, self.autogen_ids.next()) slug = 'autogen_{}_{}'.format(block_type, self.autogen_ids.next())
return self.course_id.make_usage_key(block_type, slug) return self.course_id.make_usage_key(block_type, slug)
def get_definition_id(self, usage_id):
"""Retrieve the definition that a usage is derived from.
Args:
usage_id: The id of the usage to query
Returns:
The `definition_id` the usage is derived from
"""
return usage_id
def _make_usage_key(course_key, value): def _make_usage_key(course_key, value):
""" """
......
...@@ -28,6 +28,7 @@ from xmodule.mako_module import MakoDescriptorSystem ...@@ -28,6 +28,7 @@ from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.mongo.draft import DraftModuleStore from xmodule.modulestore.mongo.draft import DraftModuleStore
from xmodule.modulestore.xml import CourseLocationManager
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES, ModuleStoreDraftAndPublished from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES, ModuleStoreDraftAndPublished
...@@ -51,9 +52,16 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -51,9 +52,16 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method
""" """
ModuleSystem for testing ModuleSystem for testing
""" """
def __init__(self, **kwargs):
id_manager = CourseLocationManager(kwargs['course_id'])
kwargs.setdefault('id_reader', id_manager)
kwargs.setdefault('id_generator', id_manager)
kwargs.setdefault('services', {}).setdefault('field-data', DictFieldData({}))
super(TestModuleSystem, self).__init__(**kwargs)
def handler_url(self, block, handler, suffix='', query='', thirdparty=False): def handler_url(self, block, handler, suffix='', query='', thirdparty=False):
return '{usage_id}/{handler}{suffix}?{query}'.format( return '{usage_id}/{handler}{suffix}?{query}'.format(
usage_id=block.scope_ids.usage_id.to_deprecated_string(), usage_id=unicode(block.scope_ids.usage_id),
handler=handler, handler=handler,
suffix=suffix, suffix=suffix,
query=query, query=query,
...@@ -61,10 +69,14 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -61,10 +69,14 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method
def local_resource_url(self, block, uri): def local_resource_url(self, block, uri):
return 'resource/{usage_id}/{uri}'.format( return 'resource/{usage_id}/{uri}'.format(
usage_id=block.scope_ids.usage_id.to_deprecated_string(), usage_id=unicode(block.scope_ids.usage_id),
uri=uri, uri=uri,
) )
# Disable XBlockAsides in most tests
def get_asides(self, block):
return []
def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')): def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')):
""" """
...@@ -113,13 +125,16 @@ def get_test_descriptor_system(): ...@@ -113,13 +125,16 @@ def get_test_descriptor_system():
""" """
Construct a test DescriptorSystem instance. Construct a test DescriptorSystem instance.
""" """
field_data = DictFieldData({})
return MakoDescriptorSystem( return MakoDescriptorSystem(
load_item=Mock(), load_item=Mock(),
resources_fs=Mock(), resources_fs=Mock(),
error_tracker=Mock(), error_tracker=Mock(),
render_template=mock_render_template, render_template=mock_render_template,
mixins=(InheritanceMixin, XModuleMixin), mixins=(InheritanceMixin, XModuleMixin),
field_data=DictFieldData({}), field_data=field_data,
services={'field-data': field_data},
) )
...@@ -149,13 +164,8 @@ class LogicTest(unittest.TestCase): ...@@ -149,13 +164,8 @@ class LogicTest(unittest.TestCase):
raw_field_data = {} raw_field_data = {}
def setUp(self): def setUp(self):
class EmptyClass:
"""Empty object."""
url_name = ''
category = 'test'
self.system = get_test_system() self.system = get_test_system()
self.descriptor = EmptyClass() self.descriptor = Mock(name="descriptor", url_name='', category='test')
self.xmodule_class = self.descriptor_class.module_class self.xmodule_class = self.descriptor_class.module_class
usage_key = self.system.course_id.make_usage_key(self.descriptor.category, 'test_loc') usage_key = self.system.course_id.make_usage_key(self.descriptor.category, 'test_loc')
......
...@@ -8,7 +8,7 @@ from xblock.field_data import DictFieldData ...@@ -8,7 +8,7 @@ from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from xmodule.error_module import NonStaffErrorDescriptor from xmodule.error_module import NonStaffErrorDescriptor
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore, CourseLocationGenerator from xmodule.modulestore.xml import ImportSystem, XMLModuleStore, CourseLocationManager
from xmodule.conditional_module import ConditionalDescriptor from xmodule.conditional_module import ConditionalDescriptor
from xmodule.tests import DATA_DIR, get_test_system, get_test_descriptor_system from xmodule.tests import DATA_DIR, get_test_system, get_test_descriptor_system
from xmodule.x_module import STUDENT_VIEW from xmodule.x_module import STUDENT_VIEW
...@@ -60,7 +60,7 @@ class ConditionalFactory(object): ...@@ -60,7 +60,7 @@ class ConditionalFactory(object):
source_descriptor = NonStaffErrorDescriptor.from_xml( source_descriptor = NonStaffErrorDescriptor.from_xml(
'some random xml data', 'some random xml data',
system, system,
id_generator=CourseLocationGenerator(SlashSeparatedCourseKey('edX', 'conditional_test', 'test_run')), id_generator=CourseLocationManager(source_location.course_key),
error_msg='random error message' error_msg='random error message'
) )
else: else:
......
...@@ -4,7 +4,7 @@ Tests for ErrorModule and NonStaffErrorModule ...@@ -4,7 +4,7 @@ Tests for ErrorModule and NonStaffErrorModule
import unittest import unittest
from xmodule.tests import get_test_system from xmodule.tests import get_test_system
from xmodule.error_module import ErrorDescriptor, ErrorModule, NonStaffErrorDescriptor from xmodule.error_module import ErrorDescriptor, ErrorModule, NonStaffErrorDescriptor
from xmodule.modulestore.xml import CourseLocationGenerator from xmodule.modulestore.xml import CourseLocationManager
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW
from mock import MagicMock, Mock, patch from mock import MagicMock, Mock, patch
...@@ -34,7 +34,7 @@ class TestErrorModule(unittest.TestCase, SetupTestErrorModules): ...@@ -34,7 +34,7 @@ class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = ErrorDescriptor.from_xml( descriptor = ErrorDescriptor.from_xml(
self.valid_xml, self.valid_xml,
self.system, self.system,
CourseLocationGenerator(self.course_id), CourseLocationManager(self.course_id),
self.error_msg self.error_msg
) )
self.assertIsInstance(descriptor, ErrorDescriptor) self.assertIsInstance(descriptor, ErrorDescriptor)
...@@ -69,7 +69,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules): ...@@ -69,7 +69,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = NonStaffErrorDescriptor.from_xml( descriptor = NonStaffErrorDescriptor.from_xml(
self.valid_xml, self.valid_xml,
self.system, self.system,
CourseLocationGenerator(self.course_id) CourseLocationManager(self.course_id)
) )
self.assertIsInstance(descriptor, NonStaffErrorDescriptor) self.assertIsInstance(descriptor, NonStaffErrorDescriptor)
...@@ -77,7 +77,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules): ...@@ -77,7 +77,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = NonStaffErrorDescriptor.from_xml( descriptor = NonStaffErrorDescriptor.from_xml(
self.valid_xml, self.valid_xml,
self.system, self.system,
CourseLocationGenerator(self.course_id) CourseLocationManager(self.course_id)
) )
descriptor.xmodule_runtime = self.system descriptor.xmodule_runtime = self.system
context_repr = self.system.render(descriptor, STUDENT_VIEW).content context_repr = self.system.render(descriptor, STUDENT_VIEW).content
......
...@@ -7,7 +7,7 @@ from unittest import TestCase ...@@ -7,7 +7,7 @@ from unittest import TestCase
from xmodule.x_module import XMLParsingSystem, policy_key from xmodule.x_module import XMLParsingSystem, policy_key
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from xmodule.modulestore.xml import create_block_from_xml, CourseLocationGenerator from xmodule.modulestore.xml import create_block_from_xml, CourseLocationManager
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xblock.runtime import KvsFieldData, DictKeyValueStore from xblock.runtime import KvsFieldData, DictKeyValueStore
...@@ -43,7 +43,7 @@ class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem): # pylint: disable ...@@ -43,7 +43,7 @@ class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem): # pylint: disable
descriptor = create_block_from_xml( descriptor = create_block_from_xml(
xml, xml,
self, self,
CourseLocationGenerator(self.course_id), CourseLocationManager(self.course_id),
) )
self._descriptors[descriptor.location.to_deprecated_string()] = descriptor self._descriptors[descriptor.location.to_deprecated_string()] = descriptor
return descriptor return descriptor
......
...@@ -18,12 +18,13 @@ from webob.multidict import MultiDict ...@@ -18,12 +18,13 @@ from webob.multidict import MultiDict
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String, Dict from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String, Dict
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.runtime import Runtime, IdReader from xblock.runtime import Runtime, IdReader, IdGenerator
from xmodule.fields import RelativeTime from xmodule.fields import RelativeTime
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.asides import AsideUsageKeyV1, AsideDefinitionKeyV1
from xmodule.exceptions import UndefinedContext from xmodule.exceptions import UndefinedContext
import dogstats_wrapper as dog_stats_api import dogstats_wrapper as dog_stats_api
...@@ -65,7 +66,7 @@ class OpaqueKeyReader(IdReader): ...@@ -65,7 +66,7 @@ class OpaqueKeyReader(IdReader):
Returns: Returns:
The `definition_id` the usage is derived from The `definition_id` the usage is derived from
""" """
return usage_id.definition_key raise NotImplementedError("Specific Modulestores must implement get_definition_id")
def get_block_type(self, def_id): def get_block_type(self, def_id):
"""Retrieve the block_type of a particular definition """Retrieve the block_type of a particular definition
...@@ -78,6 +79,91 @@ class OpaqueKeyReader(IdReader): ...@@ -78,6 +79,91 @@ class OpaqueKeyReader(IdReader):
""" """
return def_id.block_type return def_id.block_type
def get_usage_id_from_aside(self, aside_id):
"""
Retrieve the XBlock `usage_id` associated with this aside usage id.
Args:
aside_id: The usage id of the XBlockAside.
Returns:
The `usage_id` of the usage the aside is commenting on.
"""
return aside_id.usage_key
def get_definition_id_from_aside(self, aside_id):
"""
Retrieve the XBlock `definition_id` associated with this aside definition id.
Args:
aside_id: The usage id of the XBlockAside.
Returns:
The `definition_id` of the usage the aside is commenting on.
"""
return aside_id.definition_key
def get_aside_type_from_usage(self, aside_id):
"""
Retrieve the XBlockAside `aside_type` associated with this aside
usage id.
Args:
aside_id: The usage id of the XBlockAside.
Returns:
The `aside_type` of the aside.
"""
return aside_id.aside_type
def get_aside_type_from_definition(self, aside_id):
"""
Retrieve the XBlockAside `aside_type` associated with this aside
definition id.
Args:
aside_id: The definition id of the XBlockAside.
Returns:
The `aside_type` of the aside.
"""
return aside_id.aside_type
class AsideKeyGenerator(IdGenerator): # pylint: disable=abstract-method
"""
An :class:`.IdGenerator` that only provides facilities for constructing new XBlockAsides.
"""
def create_aside(self, definition_id, usage_id, aside_type):
"""
Make a new aside definition and usage ids, indicating an :class:`.XBlockAside` of type `aside_type`
commenting on an :class:`.XBlock` usage `usage_id`
Returns:
(aside_definition_id, aside_usage_id)
"""
def_key = AsideDefinitionKeyV1(definition_id, aside_type)
usage_key = AsideUsageKeyV1(usage_id, aside_type)
return (def_key, usage_key)
def create_usage(self, def_id):
"""Make a usage, storing its definition id.
Returns the newly-created usage id.
"""
raise NotImplementedError("Specific Modulestores must provide implementations of create_usage")
def create_definition(self, block_type, slug=None):
"""Make a definition, storing its block type.
If `slug` is provided, it is a suggestion that the definition id
incorporate the slug somehow.
Returns the newly-created definition id.
"""
raise NotImplementedError("Specific Modulestores must provide implementations of create_definition")
def dummy_track(_event_type, _event): def dummy_track(_event_type, _event):
pass pass
...@@ -160,6 +246,8 @@ class XModuleMixin(XBlockMixin): ...@@ -160,6 +246,8 @@ class XModuleMixin(XBlockMixin):
Adding this Mixin to an :class:`XBlock` allows it to cooperate with old-style :class:`XModules` Adding this Mixin to an :class:`XBlock` allows it to cooperate with old-style :class:`XModules`
""" """
entry_point = "xmodule.v1"
# Attributes for inspection of the descriptor # Attributes for inspection of the descriptor
# This indicates whether the xmodule is a problem-type. # This indicates whether the xmodule is a problem-type.
...@@ -526,6 +614,7 @@ class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-me ...@@ -526,6 +614,7 @@ class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-me
field_data: A dictionary-like object that maps field names to values field_data: A dictionary-like object that maps field names to values
for those fields. for those fields.
""" """
# Set the descriptor first so that we can proxy to it # Set the descriptor first so that we can proxy to it
self.descriptor = descriptor self.descriptor = descriptor
super(XModule, self).__init__(*args, **kwargs) super(XModule, self).__init__(*args, **kwargs)
...@@ -715,7 +804,6 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -715,7 +804,6 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
create a problem, and can generate XModules (which do know about student create a problem, and can generate XModules (which do know about student
state). state).
""" """
entry_point = "xmodule.v1"
module_class = XModule module_class = XModule
# VS[compat]. Backwards compatibility code that can go away after # VS[compat]. Backwards compatibility code that can go away after
...@@ -997,7 +1085,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -997,7 +1085,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
class ConfigurableFragmentWrapper(object): # pylint: disable=abstract-method class ConfigurableFragmentWrapper(object): # pylint: disable=abstract-method
""" """
Runtime mixin that allows for composition of many `wrap_child` wrappers Runtime mixin that allows for composition of many `wrap_xblock` wrappers
""" """
def __init__(self, wrappers=None, **kwargs): def __init__(self, wrappers=None, **kwargs):
""" """
...@@ -1013,7 +1101,7 @@ class ConfigurableFragmentWrapper(object): # pylint: disable=abstract-method ...@@ -1013,7 +1101,7 @@ class ConfigurableFragmentWrapper(object): # pylint: disable=abstract-method
else: else:
self.wrappers = [] self.wrappers = []
def wrap_child(self, block, view, frag, context): def wrap_xblock(self, block, view, frag, context):
""" """
See :func:`Runtime.wrap_child` See :func:`Runtime.wrap_child`
""" """
...@@ -1043,6 +1131,16 @@ def descriptor_global_local_resource_url(block, uri): # pylint: disable=invalid ...@@ -1043,6 +1131,16 @@ def descriptor_global_local_resource_url(block, uri): # pylint: disable=invalid
raise NotImplementedError("Applications must monkey-patch this function before using local_resource_url for studio_view") raise NotImplementedError("Applications must monkey-patch this function before using local_resource_url for studio_view")
# This function exists to give applications (LMS/CMS) a place to monkey-patch until
# we can refactor modulestore to split out the FieldData half of its interface from
# the Runtime part of its interface. This function matches the Runtime.get_asides interface
def descriptor_global_get_asides(block): # pylint: disable=unused-argument
"""
See :meth:`xblock.runtime.Runtime.get_asides`.
"""
raise NotImplementedError("Applications must monkey-patch this function before using get_asides from a DescriptorSystem.")
class MetricsMixin(object): class MetricsMixin(object):
""" """
Mixin for adding metric logging for render and handle methods in the DescriptorSystem and ModuleSystem. Mixin for adding metric logging for render and handle methods in the DescriptorSystem and ModuleSystem.
...@@ -1137,7 +1235,9 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p ...@@ -1137,7 +1235,9 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p
local_resource_url: an implementation of :meth:`xblock.runtime.Runtime.local_resource_url` local_resource_url: an implementation of :meth:`xblock.runtime.Runtime.local_resource_url`
""" """
super(DescriptorSystem, self).__init__(id_reader=OpaqueKeyReader(), **kwargs) kwargs.setdefault('id_reader', OpaqueKeyReader())
kwargs.setdefault('id_generator', AsideKeyGenerator())
super(DescriptorSystem, self).__init__(**kwargs)
# This is used by XModules to write out separate files during xml export # This is used by XModules to write out separate files during xml export
self.export_fs = None self.export_fs = None
...@@ -1215,6 +1315,19 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p ...@@ -1215,6 +1315,19 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p
# global function that the application can override. # global function that the application can override.
return descriptor_global_local_resource_url(block, uri) return descriptor_global_local_resource_url(block, uri)
def get_asides(self, block):
"""
See :meth:`xblock.runtime.Runtime:get_asides` for documentation.
"""
if getattr(block, 'xmodule_runtime', None) is not None:
return block.xmodule_runtime.get_asides(block)
else:
# Currently, Modulestore is responsible for instantiating DescriptorSystems
# This means that LMS/CMS don't have a way to define a subclass of DescriptorSystem
# that implements the correct get_asides. So, for now, instead, we will reference a
# global function that the application can override.
return descriptor_global_get_asides(block)
def resource_url(self, resource): def resource_url(self, resource):
""" """
See :meth:`xblock.runtime.Runtime:resource_url` for documentation. See :meth:`xblock.runtime.Runtime:resource_url` for documentation.
...@@ -1335,7 +1448,9 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin ...@@ -1335,7 +1448,9 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin
# Usage_store is unused, and field_data is often supplanted with an # Usage_store is unused, and field_data is often supplanted with an
# explicit field_data during construct_xblock. # explicit field_data during construct_xblock.
super(ModuleSystem, self).__init__(id_reader=OpaqueKeyReader(), field_data=field_data, **kwargs) kwargs.setdefault('id_reader', getattr(descriptor_runtime, 'id_reader', OpaqueKeyReader()))
kwargs.setdefault('id_generator', getattr(descriptor_runtime, 'id_generator', AsideKeyGenerator()))
super(ModuleSystem, self).__init__(field_data=field_data, **kwargs)
self.STATIC_URL = static_url self.STATIC_URL = static_url
self.xqueue = xqueue self.xqueue = xqueue
...@@ -1373,6 +1488,9 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin ...@@ -1373,6 +1488,9 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin
self.descriptor_runtime = descriptor_runtime self.descriptor_runtime = descriptor_runtime
self.rebind_noauth_module_to_user = rebind_noauth_module_to_user self.rebind_noauth_module_to_user = rebind_noauth_module_to_user
if user:
self.user_id = user.id
def get(self, attr): def get(self, attr):
""" provide uniform access to attributes (like etree).""" """ provide uniform access to attributes (like etree)."""
return self.__dict__.get(attr) return self.__dict__.get(attr)
......
...@@ -35,7 +35,7 @@ describe "XBlock", -> ...@@ -35,7 +35,7 @@ describe "XBlock", ->
window.initFnZ = jasmine.createSpy() window.initFnZ = jasmine.createSpy()
@fakeChildren = ['list', 'of', 'children'] @fakeChildren = ['list', 'of', 'children']
spyOn(XBlock, 'initializeBlocks').andReturn(@fakeChildren) spyOn(XBlock, 'initializeXBlocks').andReturn(@fakeChildren)
@vANode = $('#vA')[0] @vANode = $('#vA')[0]
@vZNode = $('#vZ')[0] @vZNode = $('#vZ')[0]
...@@ -50,8 +50,8 @@ describe "XBlock", -> ...@@ -50,8 +50,8 @@ describe "XBlock", ->
expect(TestRuntime.vZ).toHaveBeenCalledWith() expect(TestRuntime.vZ).toHaveBeenCalledWith()
it "loads the right init function", -> it "loads the right init function", ->
expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, @vANode) expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, @vANode, {})
expect(window.initFnZ).toHaveBeenCalledWith(@runtimeZ, @vZNode) expect(window.initFnZ).toHaveBeenCalledWith(@runtimeZ, @vZNode, {})
it "loads when missing versions", -> it "loads when missing versions", ->
expect(@missingVersionBlock.element).toBe($('#missing-version')) expect(@missingVersionBlock.element).toBe($('#missing-version'))
...@@ -74,8 +74,8 @@ describe "XBlock", -> ...@@ -74,8 +74,8 @@ describe "XBlock", ->
expect(@missingInitBlock.element).toBe($('#missing-init')[0]) expect(@missingInitBlock.element).toBe($('#missing-init')[0])
it "passes through the request token", -> it "passes through the request token", ->
expect(XBlock.initializeBlocks).toHaveBeenCalledWith($(@vANode), 'req-token-a') expect(XBlock.initializeXBlocks).toHaveBeenCalledWith($(@vANode), 'req-token-a')
expect(XBlock.initializeBlocks).toHaveBeenCalledWith($(@vZNode), 'req-token-z') expect(XBlock.initializeXBlocks).toHaveBeenCalledWith($(@vZNode), 'req-token-z')
describe "initializeBlocks", -> describe "initializeBlocks", ->
......
@XBlock =
Runtime: {}
###
Initialize the javascript for a single xblock element, and for all of it's
xblock children that match requestToken. If requestToken is omitted, use the
data-request-token attribute from element, or use the request-tokens specified on
the children themselves.
###
initializeBlock: (element, requestToken) ->
$element = $(element)
requestToken = requestToken or $element.data('request-token')
children = @initializeBlocks($element, requestToken)
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}"]
initFn = window[initFnName]
if initFn.length > 2
initargs = $(".xblock_json_init_args", element)
if initargs.length == 0
console.log("Warning: XBlock expects data parameters")
data = JSON.parse(initargs.text())
block = initFn(runtime, element, data) ? {}
else
block = initFn(runtime, element) ? {}
block.runtime = runtime
else
elementTag = $('<div>').append($element.clone()).html();
console.log("Block #{elementTag} is missing data-runtime, data-runtime-version or data-init, and can't be initialized")
block = {}
block.element = element
block.name = $element.data("name")
block.type = $element.data("block-type")
$element.trigger("xblock-initialized")
$element.data("initialized", true)
$element.addClass("xblock-initialized")
block
###
Initialize all XBlocks inside element that were rendered with requestToken.
If requestToken is omitted, and element has a 'data-request-token' attribute, use that.
If neither is available, then use the request tokens of the immediateDescendent xblocks.
###
initializeBlocks: (element, requestToken) ->
requestToken = requestToken or $(element).data('request-token')
if requestToken
selector = ".xblock[data-request-token='#{requestToken}']"
else
selector = ".xblock"
$(element).immediateDescendents(selector).map((idx, elem) =>
@initializeBlock(elem, requestToken)
).toArray()
(function($, JSON) {
'use strict';
function initializeBlockLikes(block_class, initializer, element, requestToken) {
var requestToken = requestToken || $(element).data('request-token');
if (requestToken) {
var selector = '.' + block_class + '[data-request-token="' + requestToken + '"]';
} else {
var selector = '.' + block_class;
}
return $(element).immediateDescendents(selector).map(function(idx, elem) {
return initializer(elem, requestToken);
}).toArray();
}
function elementRuntime(element) {
var $element = $(element);
var runtime = $element.data('runtime-class');
var version = $element.data('runtime-version');
var initFnName = $element.data('init');
if (runtime && version && initFnName) {
return new window[runtime]['v' + version];
} else {
if (!runtime || !version || !initFnName) {
var elementTag = $('<div>').append($element.clone()).html();
console.log('Block ' + elementTag + ' is missing data-runtime, data-runtime-version or data-init, and can\'t be initialized');
}
return null;
}
}
function initArgs(element) {
var initargs = $('.xblock_json_init_args', element).text();
return initargs ? JSON.parse(initargs) : {};
}
/**
* Construct an XBlock family object from an element. The constructor
* function is loaded from the 'data-init' attribute of the element.
* The constructor is called with the arguments 'runtime', 'element',
* and then all of 'block_args'.
*/
function constructBlock(element, block_args) {
var block;
var $element = $(element);
var runtime = elementRuntime(element);
block_args.unshift(element);
block_args.unshift(runtime);
if (runtime) {
block = (function() {
var initFn = window[$element.data('init')];
// This create a new constructor that can then apply() the block_args
// to the initFn.
function Block() {
return initFn.apply(this, block_args);
}
Block.prototype = initFn.prototype;
return new Block();
})();
block.runtime = runtime;
} else {
block = {};
}
block.element = element;
block.name = $element.data('name');
block.type = $element.data('block-type');
$element.trigger('xblock-initialized');
$element.data('initialized', true);
$element.addClass('xblock-initialized');
return block;
}
var XBlock = {
Runtime: {},
/**
* Initialize the javascript for a single xblock element, and for all of it's
* xblock children that match requestToken. If requestToken is omitted, use the
* data-request-token attribute from element, or use the request-tokens specified on
* the children themselves.
*/
initializeBlock: function(element, requestToken) {
var $element = $(element);
var requestToken = requestToken || $element.data('request-token');
var children = XBlock.initializeXBlocks($element, requestToken);
$element.prop('xblock_children', children);
return constructBlock(element, [initArgs(element)]);
},
/**
* Initialize the javascript for a single xblock aside element that matches requestToken.
* If requestToken is omitted, use the data-request-token attribute from element, or use
* the request-tokens specified on the children themselves.
*/
initializeAside: function(element, requestToken) {
var blockUsageId = $(element).data('block-id');
var blockElement = $(element).siblings('[data-usage-id="' + blockUsageId + '"]')[0];
return constructBlock(element, [blockElement, initArgs(element)]);
},
/**
* Initialize all XBlocks inside element that were rendered with requestToken.
* If requestToken is omitted, and element has a 'data-request-token' attribute, use that.
* If neither is available, then use the request tokens of the immediateDescendent xblocks.
*/
initializeXBlocks: function(element, requestToken) {
return initializeBlockLikes('xblock', XBlock.initializeBlock, element, requestToken);
},
/**
* Initialize all XBlockAsides inside element that were rendered with requestToken.
* If requestToken is omitted, and element has a 'data-request-token' attribute, use that.
* If neither is available, then use the request tokens of the immediateDescendent xblocks.
*/
initializeXBlockAsides: function(element, requestToken) {
return initializeBlockLikes('xblock_asides-v1', XBlock.initializeAside, element, requestToken);
},
/**
* Initialize all XBlock-family blocks inside element that were rendered with requestToken.
* If requestToken is omitted, and element has a 'data-request-token' attribute, use that.
* If neither is available, then use the request tokens of the immediateDescendent xblocks.
*/
initializeBlocks: function(element, requestToken) {
XBlock.initializeXBlockAsides(element, requestToken);
return XBlock.initializeXBlocks(element, requestToken);
}
};
this.XBlock = XBlock;
}).call(this, $, JSON);
...@@ -45,6 +45,7 @@ lib_paths: ...@@ -45,6 +45,7 @@ lib_paths:
# Paths to source JavaScript files # Paths to source JavaScript files
src_paths: src_paths:
- js/xblock
- coffee/src - coffee/src
- js/src - js/src
- js/utils - js/utils
......
...@@ -51,7 +51,7 @@ class ContainerPage(PageObject): ...@@ -51,7 +51,7 @@ class ContainerPage(PageObject):
num_wrappers = len(self.q(css='{} [data-request-token="{}"]'.format(XBlockWrapper.BODY_SELECTOR, request_token)).results) num_wrappers = len(self.q(css='{} [data-request-token="{}"]'.format(XBlockWrapper.BODY_SELECTOR, request_token)).results)
# Wait until all components have been loaded and marked as either initialized or failed. # Wait until all components have been loaded and marked as either initialized or failed.
# See: # See:
# - common/static/coffee/src/xblock/core.coffee which adds the class "xblock-initialized" # - common/static/js/xblock/core.js which adds the class "xblock-initialized"
# at the end of initializeBlock. # at the end of initializeBlock.
# - common/static/js/views/xblock.js which adds the class "xblock-initialization-failed" # - common/static/js/views/xblock.js which adds the class "xblock-initialization-failed"
# if the xblock threw an error while initializing. # if the xblock threw an error while initializing.
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
End-to-end tests for the LMS. End-to-end tests for the LMS.
""" """
from unittest import skip from unittest import expectedFailure
from ..helpers import UniqueCourseTest from ..helpers import UniqueCourseTest
from ...pages.lms.auto_auth import AutoAuthPage from ...pages.lms.auto_auth import AutoAuthPage
...@@ -44,17 +44,6 @@ class XBlockAcidBase(UniqueCourseTest): ...@@ -44,17 +44,6 @@ class XBlockAcidBase(UniqueCourseTest):
self.assertTrue(acid_block.scope_passed('preferences')) self.assertTrue(acid_block.scope_passed('preferences'))
self.assertTrue(acid_block.scope_passed('user_info')) self.assertTrue(acid_block.scope_passed('user_info'))
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.validate_acid_block_view(acid_block)
class XBlockAcidNoChildTest(XBlockAcidBase): class XBlockAcidNoChildTest(XBlockAcidBase):
""" """
...@@ -81,7 +70,15 @@ class XBlockAcidNoChildTest(XBlockAcidBase): ...@@ -81,7 +70,15 @@ class XBlockAcidNoChildTest(XBlockAcidBase):
).install() ).install()
def test_acid_block(self): def test_acid_block(self):
super(XBlockAcidNoChildTest, self).test_acid_block() """
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.validate_acid_block_view(acid_block)
class XBlockAcidChildTest(XBlockAcidBase): class XBlockAcidChildTest(XBlockAcidBase):
...@@ -129,3 +126,46 @@ class XBlockAcidChildTest(XBlockAcidBase): ...@@ -129,3 +126,46 @@ class XBlockAcidChildTest(XBlockAcidBase):
acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]') acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
self.validate_acid_block_view(acid_block) self.validate_acid_block_view(acid_block)
class XBlockAcidAsideTest(XBlockAcidBase):
"""
Tests of an AcidBlock with children
"""
__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')
)
)
)
).install()
@expectedFailure
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_aside = AcidView(self.browser, '.xblock_asides-v1-student_view[data-block-type=acid_aside]')
self.validate_acid_aside_view(acid_aside)
acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
self.validate_acid_block_view(acid_block)
def validate_acid_aside_view(self, acid_aside):
self.validate_acid_block_view(acid_aside)
...@@ -12,16 +12,17 @@ from .models import ( ...@@ -12,16 +12,17 @@ from .models import (
XModuleStudentInfoField XModuleStudentInfoField
) )
import logging import logging
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.block_types import BlockTypeKeyV1
from opaque_keys.edx.asides import AsideUsageKeyV1
from django.db import DatabaseError from django.db import DatabaseError
from django.contrib.auth.models import User
from xblock.runtime import KeyValueStore from xblock.runtime import KeyValueStore
from xblock.exceptions import KeyValueMultiSaveError, InvalidScopeError from xblock.exceptions import KeyValueMultiSaveError, InvalidScopeError
from xblock.fields import Scope, UserScope from xblock.fields import Scope, UserScope
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xblock.core import XBlockAside
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -46,7 +47,7 @@ class FieldDataCache(object): ...@@ -46,7 +47,7 @@ class FieldDataCache(object):
A cache of django model objects needed to supply the data A cache of django model objects needed to supply the data
for a module and its decendants for a module and its decendants
""" """
def __init__(self, descriptors, course_id, user, select_for_update=False): def __init__(self, descriptors, course_id, user, select_for_update=False, asides=None):
''' '''
Find any courseware.models objects that are needed by any descriptor Find any courseware.models objects that are needed by any descriptor
in descriptors. Attempts to minimize the number of queries to the database. in descriptors. Attempts to minimize the number of queries to the database.
...@@ -58,11 +59,17 @@ class FieldDataCache(object): ...@@ -58,11 +59,17 @@ class FieldDataCache(object):
course_id: The id of the current course course_id: The id of the current course
user: The user for which to cache data user: The user for which to cache data
select_for_update: True if rows should be locked until end of transaction select_for_update: True if rows should be locked until end of transaction
asides: The list of aside types to load, or None to prefetch no asides.
''' '''
self.cache = {} self.cache = {}
self.descriptors = descriptors self.descriptors = descriptors
self.select_for_update = select_for_update self.select_for_update = select_for_update
if asides is None:
self.asides = []
else:
self.asides = asides
assert isinstance(course_id, CourseKey) assert isinstance(course_id, CourseKey)
self.course_id = course_id self.course_id = course_id
self.user = user self.user = user
...@@ -75,7 +82,7 @@ class FieldDataCache(object): ...@@ -75,7 +82,7 @@ class FieldDataCache(object):
@classmethod @classmethod
def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None, def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None,
descriptor_filter=lambda descriptor: True, descriptor_filter=lambda descriptor: True,
select_for_update=False): select_for_update=False, asides=None):
""" """
course_id: the course in the context of which we want StudentModules. course_id: the course in the context of which we want StudentModules.
user: the django user for whom to load modules. user: the django user for whom to load modules.
...@@ -113,7 +120,7 @@ class FieldDataCache(object): ...@@ -113,7 +120,7 @@ class FieldDataCache(object):
with modulestore().bulk_operations(descriptor.location.course_key): with modulestore().bulk_operations(descriptor.location.course_key):
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter) descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
return FieldDataCache(descriptors, course_id, user, select_for_update) return FieldDataCache(descriptors, course_id, user, select_for_update, asides=asides)
def _query(self, model_class, **kwargs): def _query(self, model_class, **kwargs):
""" """
...@@ -140,6 +147,35 @@ class FieldDataCache(object): ...@@ -140,6 +147,35 @@ class FieldDataCache(object):
) )
return res return res
@property
def _all_usage_ids(self):
"""
Return a set of all usage_ids for the descriptors that this FieldDataCache is caching
against, and well as all asides for those descriptors.
"""
usage_ids = set()
for descriptor in self.descriptors:
usage_ids.add(descriptor.scope_ids.usage_id)
for aside_type in self.asides:
usage_ids.add(AsideUsageKeyV1(descriptor.scope_ids.usage_id, aside_type))
return usage_ids
@property
def _all_block_types(self):
"""
Return a set of all block_types that are cached by this FieldDataCache.
"""
block_types = set()
for descriptor in self.descriptors:
block_types.add(BlockTypeKeyV1(descriptor.entry_point, descriptor.scope_ids.block_type))
for aside_type in self.asides:
block_types.add(BlockTypeKeyV1(XBlockAside.entry_point, aside_type))
return block_types
def _retrieve_fields(self, scope, fields): def _retrieve_fields(self, scope, fields):
""" """
Queries the database for all of the fields in the specified scope Queries the database for all of the fields in the specified scope
...@@ -148,7 +184,7 @@ class FieldDataCache(object): ...@@ -148,7 +184,7 @@ class FieldDataCache(object):
return self._chunked_query( return self._chunked_query(
StudentModule, StudentModule,
'module_state_key__in', 'module_state_key__in',
(descriptor.scope_ids.usage_id for descriptor in self.descriptors), self._all_usage_ids,
course_id=self.course_id, course_id=self.course_id,
student=self.user.pk, student=self.user.pk,
) )
...@@ -156,14 +192,14 @@ class FieldDataCache(object): ...@@ -156,14 +192,14 @@ class FieldDataCache(object):
return self._chunked_query( return self._chunked_query(
XModuleUserStateSummaryField, XModuleUserStateSummaryField,
'usage_id__in', 'usage_id__in',
(descriptor.scope_ids.usage_id for descriptor in self.descriptors), self._all_usage_ids,
field_name__in=set(field.name for field in fields), field_name__in=set(field.name for field in fields),
) )
elif scope == Scope.preferences: elif scope == Scope.preferences:
return self._chunked_query( return self._chunked_query(
XModuleStudentPrefsField, XModuleStudentPrefsField,
'module_type__in', 'module_type__in',
set(descriptor.scope_ids.block_type for descriptor in self.descriptors), self._all_block_types,
student=self.user.pk, student=self.user.pk,
field_name__in=set(field.name for field in fields), field_name__in=set(field.name for field in fields),
) )
...@@ -195,7 +231,7 @@ class FieldDataCache(object): ...@@ -195,7 +231,7 @@ class FieldDataCache(object):
elif key.scope == Scope.user_state_summary: elif key.scope == Scope.user_state_summary:
return (key.scope, key.block_scope_id, key.field_name) return (key.scope, key.block_scope_id, key.field_name)
elif key.scope == Scope.preferences: elif key.scope == Scope.preferences:
return (key.scope, key.block_scope_id, key.field_name) return (key.scope, BlockTypeKeyV1(key.block_family, key.block_scope_id), key.field_name)
elif key.scope == Scope.user_info: elif key.scope == Scope.user_info:
return (key.scope, key.field_name) return (key.scope, key.field_name)
...@@ -239,31 +275,28 @@ class FieldDataCache(object): ...@@ -239,31 +275,28 @@ class FieldDataCache(object):
return field_object return field_object
if key.scope == Scope.user_state: if key.scope == Scope.user_state:
# When we start allowing block_scope_ids to be either Locations or Locators, field_object, __ = StudentModule.objects.get_or_create(
# this assertion will fail. Fix the code here when that happens!
assert(isinstance(key.block_scope_id, UsageKey))
field_object, _ = StudentModule.objects.get_or_create(
course_id=self.course_id, course_id=self.course_id,
student_id=key.user_id, student_id=key.user_id,
module_state_key=key.block_scope_id, module_state_key=key.block_scope_id,
defaults={ defaults={
'state': json.dumps({}), 'state': json.dumps({}),
'module_type': key.block_scope_id.category, 'module_type': key.block_scope_id.block_type,
}, },
) )
elif key.scope == Scope.user_state_summary: elif key.scope == Scope.user_state_summary:
field_object, _ = XModuleUserStateSummaryField.objects.get_or_create( field_object, __ = XModuleUserStateSummaryField.objects.get_or_create(
field_name=key.field_name, field_name=key.field_name,
usage_id=key.block_scope_id usage_id=key.block_scope_id
) )
elif key.scope == Scope.preferences: elif key.scope == Scope.preferences:
field_object, _ = XModuleStudentPrefsField.objects.get_or_create( field_object, __ = XModuleStudentPrefsField.objects.get_or_create(
field_name=key.field_name, field_name=key.field_name,
module_type=key.block_scope_id, module_type=BlockTypeKeyV1(key.block_family, key.block_scope_id),
student_id=key.user_id, student_id=key.user_id,
) )
elif key.scope == Scope.user_info: elif key.scope == Scope.user_info:
field_object, _ = XModuleStudentInfoField.objects.get_or_create( field_object, __ = XModuleStudentInfoField.objects.get_or_create(
field_name=key.field_name, field_name=key.field_name,
student_id=key.user_id, student_id=key.user_id,
) )
......
...@@ -18,7 +18,7 @@ from django.db import models ...@@ -18,7 +18,7 @@ from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from xmodule_django.models import CourseKeyField, LocationKeyField from xmodule_django.models import CourseKeyField, LocationKeyField, BlockTypeKeyField
class StudentModule(models.Model): class StudentModule(models.Model):
...@@ -36,10 +36,7 @@ class StudentModule(models.Model): ...@@ -36,10 +36,7 @@ class StudentModule(models.Model):
## These three are the key for the object ## These three are the key for the object
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True) module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
# Key used to share state. By default, this is the module_id, # Key used to share state. This is the XBlock usage_id
# but for abtests and the like, this can be set to a shared value
# for many instances of the module.
# Filename for homeworks, etc.
module_state_key = LocationKeyField(max_length=255, db_index=True, db_column='module_id') module_state_key = LocationKeyField(max_length=255, db_index=True, db_column='module_id')
student = models.ForeignKey(User, db_index=True) student = models.ForeignKey(User, db_index=True)
...@@ -150,7 +147,7 @@ class XBlockFieldBase(models.Model): ...@@ -150,7 +147,7 @@ class XBlockFieldBase(models.Model):
return u'{}<{!r}'.format( return u'{}<{!r}'.format(
self.__class__.__name__, self.__class__.__name__,
{ {
key:getattr(self, key) key: getattr(self, key)
for key in self._meta.get_all_field_names() for key in self._meta.get_all_field_names()
if key not in ('created', 'modified') if key not in ('created', 'modified')
} }
...@@ -174,11 +171,11 @@ class XModuleStudentPrefsField(XBlockFieldBase): ...@@ -174,11 +171,11 @@ class XModuleStudentPrefsField(XBlockFieldBase):
Stores data set in the Scope.preferences scope by an xmodule field Stores data set in the Scope.preferences scope by an xmodule field
""" """
class Meta: class Meta: # pylint: disable=missing-docstring
unique_together = (('student', 'module_type', 'field_name'),) unique_together = (('student', 'module_type', 'field_name'),)
# The type of the module for these preferences # The type of the module for these preferences
module_type = models.CharField(max_length=64, db_index=True) module_type = BlockTypeKeyField(max_length=64, db_index=True)
student = models.ForeignKey(User, db_index=True) student = models.ForeignKey(User, db_index=True)
......
...@@ -23,6 +23,7 @@ from courseware.masquerade import setup_masquerade ...@@ -23,6 +23,7 @@ from courseware.masquerade import setup_masquerade
from courseware.model_data import FieldDataCache, DjangoKeyValueStore from courseware.model_data import FieldDataCache, DjangoKeyValueStore
from lms.djangoapps.lms_xblock.field_data import LmsFieldData from lms.djangoapps.lms_xblock.field_data import LmsFieldData
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from eventtracking import tracker from eventtracking import tracker
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
...@@ -405,7 +406,8 @@ def get_module_system_for_user(user, field_data_cache, ...@@ -405,7 +406,8 @@ def get_module_system_for_user(user, field_data_cache,
field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents( field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents(
course_id, course_id,
real_user, real_user,
module.descriptor module.descriptor,
asides=XBlockAsidesConfig.possible_asides(),
) )
(inner_system, inner_student_data) = get_module_system_for_user( (inner_system, inner_student_data) = get_module_system_for_user(
...@@ -496,6 +498,8 @@ def get_module_system_for_user(user, field_data_cache, ...@@ -496,6 +498,8 @@ def get_module_system_for_user(user, field_data_cache,
else: else:
anonymous_student_id = anonymous_id_for_user(user, None) anonymous_student_id = anonymous_id_for_user(user, None)
field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access
system = LmsModuleSystem( system = LmsModuleSystem(
track_function=track_function, track_function=track_function,
render_template=render_to_string, render_template=render_to_string,
...@@ -541,11 +545,13 @@ def get_module_system_for_user(user, field_data_cache, ...@@ -541,11 +545,13 @@ def get_module_system_for_user(user, field_data_cache,
services={ services={
'i18n': ModuleI18nService(), 'i18n': ModuleI18nService(),
'fs': xblock.reference.plugins.FSService(), 'fs': xblock.reference.plugins.FSService(),
'field-data': field_data,
}, },
get_user_role=lambda: get_user_role(user, course_id), get_user_role=lambda: get_user_role(user, course_id),
descriptor_runtime=descriptor.runtime, descriptor_runtime=descriptor.runtime,
rebind_noauth_module_to_user=rebind_noauth_module_to_user, rebind_noauth_module_to_user=rebind_noauth_module_to_user,
user_location=user_location, user_location=user_location,
request_token=request_token,
) )
# pass position specified in URL to module through ModuleSystem # pass position specified in URL to module through ModuleSystem
...@@ -572,7 +578,7 @@ def get_module_system_for_user(user, field_data_cache, ...@@ -572,7 +578,7 @@ def get_module_system_for_user(user, field_data_cache,
else: else:
system.error_descriptor_class = NonStaffErrorDescriptor system.error_descriptor_class = NonStaffErrorDescriptor
return system, student_data return system, field_data
def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, # pylint: disable=invalid-name def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, # pylint: disable=invalid-name
...@@ -594,7 +600,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours ...@@ -594,7 +600,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
if not has_access(user, 'load', descriptor, course_id): if not has_access(user, 'load', descriptor, course_id):
return None return None
(system, student_data) = get_module_system_for_user( (system, field_data) = get_module_system_for_user(
user=user, user=user,
field_data_cache=field_data_cache, # These have implicit user bindings, the rest of args are considered not to field_data_cache=field_data_cache, # These have implicit user bindings, the rest of args are considered not to
descriptor=descriptor, descriptor=descriptor,
...@@ -609,7 +615,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours ...@@ -609,7 +615,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
request_token=request_token request_token=request_token
) )
descriptor.bind_for_student(system, LmsFieldData(descriptor._field_data, student_data)) # pylint: disable=protected-access descriptor.bind_for_student(system, field_data) # pylint: disable=protected-access
descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id) # pylint: disable=protected-access descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id) # pylint: disable=protected-access
return descriptor return descriptor
......
...@@ -16,9 +16,10 @@ from courseware.tests.factories import UserStateSummaryFactory ...@@ -16,9 +16,10 @@ from courseware.tests.factories import UserStateSummaryFactory
from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory
from xblock.fields import Scope, BlockScope, ScopeIds from xblock.fields import Scope, BlockScope, ScopeIds
from xblock.exceptions import KeyValueMultiSaveError
from xblock.core import XBlock
from django.test import TestCase from django.test import TestCase
from django.db import DatabaseError from django.db import DatabaseError
from xblock.exceptions import KeyValueMultiSaveError
def mock_field(scope, name): def mock_field(scope, name):
...@@ -29,7 +30,7 @@ def mock_field(scope, name): ...@@ -29,7 +30,7 @@ def mock_field(scope, name):
def mock_descriptor(fields=[]): def mock_descriptor(fields=[]):
descriptor = Mock() descriptor = Mock(entry_point=XBlock.entry_point)
descriptor.scope_ids = ScopeIds('user1', 'mock_problem', location('def_id'), location('usage_id')) descriptor.scope_ids = ScopeIds('user1', 'mock_problem', location('def_id'), location('usage_id'))
descriptor.module_class.fields.values.return_value = fields descriptor.module_class.fields.values.return_value = fields
descriptor.fields.values.return_value = fields descriptor.fields.values.return_value = fields
......
...@@ -39,6 +39,8 @@ from .module_render import toc_for_course, get_module_for_descriptor, get_module ...@@ -39,6 +39,8 @@ from .module_render import toc_for_course, get_module_for_descriptor, get_module
from courseware.models import StudentModule, StudentModuleHistory from courseware.models import StudentModule, StudentModuleHistory
from course_modes.models import CourseMode from course_modes.models import CourseMode
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from open_ended_grading import open_ended_notifications from open_ended_grading import open_ended_notifications
from student.models import UserTestGroup, CourseEnrollment from student.models import UserTestGroup, CourseEnrollment
from student.views import single_course_reverification_info, is_course_blocked from student.views import single_course_reverification_info, is_course_blocked
...@@ -444,7 +446,8 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -444,7 +446,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
# Load all descendants of the section, because we're going to display its # Load all descendants of the section, because we're going to display its
# html, which in general will need all of its children # html, which in general will need all of its children
section_field_data_cache = FieldDataCache.cache_for_descriptor_descendents( section_field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course_key, user, section_descriptor, depth=None) course_key, user, section_descriptor, depth=None, asides=XBlockAsidesConfig.possible_asides()
)
# Verify that position a string is in fact an int # Verify that position a string is in fact an int
if position is not None: if position is not None:
......
...@@ -18,6 +18,7 @@ from django.utils.html import escape ...@@ -18,6 +18,7 @@ from django.utils.html import escape
from django.http import Http404 from django.http import Http404
from django.conf import settings from django.conf import settings
from util.json_request import JsonResponse from util.json_request import JsonResponse
from mock import patch
from lms.djangoapps.lms_xblock.runtime import quote_slashes from lms.djangoapps.lms_xblock.runtime import quote_slashes
from xmodule_modifiers import wrap_xblock from xmodule_modifiers import wrap_xblock
...@@ -323,17 +324,28 @@ def _section_data_download(course, access): ...@@ -323,17 +324,28 @@ def _section_data_download(course, access):
return section_data return section_data
def null_get_asides(block): # pylint: disable=unused-argument
"""
get_aside method for monkey-patching into descriptor_global_get_asides
while rendering an HtmlDescriptor for email text editing. This returns
an empty list.
"""
return []
def _section_send_email(course, access): def _section_send_email(course, access):
""" Provide data for the corresponding bulk email section """ """ Provide data for the corresponding bulk email section """
course_key = course.id course_key = course.id
# This HtmlDescriptor is only being used to generate a nice text editor. # Monkey-patch descriptor_global_get_asides to return no asides for the duration of this render
html_module = HtmlDescriptor( with patch('xmodule.x_module.descriptor_global_get_asides', null_get_asides):
course.system, # This HtmlDescriptor is only being used to generate a nice text editor.
DictFieldData({'data': ''}), html_module = HtmlDescriptor(
ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake')) course.system,
) DictFieldData({'data': ''}),
fragment = course.system.render(html_module, 'studio_view') ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake'))
)
fragment = course.system.render(html_module, 'studio_view')
fragment = wrap_xblock( fragment = wrap_xblock(
'LmsRuntime', html_module, 'studio_view', fragment, None, 'LmsRuntime', html_module, 'studio_view', fragment, None,
extra_data={"course-id": unicode(course_key)}, extra_data={"course-id": unicode(course_key)},
......
"""
Django admin dashboard configuration for LMS XBlock infrastructure.
"""
from django.contrib import admin from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin from config_models.admin import ConfigurationModelAdmin
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
......
...@@ -16,7 +16,7 @@ class LmsFieldData(SplitFieldData): ...@@ -16,7 +16,7 @@ class LmsFieldData(SplitFieldData):
def __init__(self, authored_data, student_data): def __init__(self, authored_data, student_data):
# Make sure that we don't repeatedly nest LmsFieldData instances # Make sure that we don't repeatedly nest LmsFieldData instances
if isinstance(authored_data, LmsFieldData): if isinstance(authored_data, LmsFieldData):
authored_data = authored_data._authored_data # pylint: disable=protected-member authored_data = authored_data._authored_data # pylint: disable=protected-access
else: else:
authored_data = ReadOnlyFieldData(authored_data) authored_data = ReadOnlyFieldData(authored_data)
......
"""
Models used by LMS XBlock infrastructure.
Includes:
XBlockAsidesConfig: A ConfigurationModel for managing how XBlockAsides are
rendered in the LMS.
"""
from django.db.models import TextField from django.db.models import TextField
from config_models.models import ConfigurationModel from config_models.models import ConfigurationModel
from xblock.core import XBlockAside
class XBlockAsidesConfig(ConfigurationModel): class XBlockAsidesConfig(ConfigurationModel):
""" """
Configuration for XBlockAsides. Configuration for XBlockAsides.
...@@ -11,3 +22,10 @@ class XBlockAsidesConfig(ConfigurationModel): ...@@ -11,3 +22,10 @@ class XBlockAsidesConfig(ConfigurationModel):
default="about course_info static_tab", default="about course_info static_tab",
help_text="Space-separated list of XBlocks on which XBlockAsides should never render." help_text="Space-separated list of XBlocks on which XBlockAsides should never render."
) )
@classmethod
def possible_asides(cls):
"""
Return a list of all asides that are enabled across all XBlocks.
"""
return [aside_type for aside_type, __ in XBlockAside.load_classes()]
...@@ -7,7 +7,9 @@ import xblock.reference.plugins ...@@ -7,7 +7,9 @@ import xblock.reference.plugins
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from openedx.core.djangoapps.user_api.api import course_tag as user_course_tag_api from openedx.core.djangoapps.user_api.api import course_tag as user_course_tag_api
from xblock.core import XBlockAside
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xmodule.partitions.partitions_service import PartitionService from xmodule.partitions.partitions_service import PartitionService
...@@ -87,8 +89,8 @@ class LmsHandlerUrls(object): ...@@ -87,8 +89,8 @@ class LmsHandlerUrls(object):
view_name = 'xblock_handler_noauth' view_name = 'xblock_handler_noauth'
url = reverse(view_name, kwargs={ url = reverse(view_name, kwargs={
'course_id': self.course_id.to_deprecated_string(), 'course_id': unicode(self.course_id),
'usage_id': quote_slashes(block.scope_ids.usage_id.to_deprecated_string().encode('utf-8')), 'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
'handler': handler_name, 'handler': handler_name,
'suffix': suffix, 'suffix': suffix,
}) })
...@@ -198,4 +200,50 @@ class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract ...@@ -198,4 +200,50 @@ class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract
track_function=kwargs.get('track_function', None), track_function=kwargs.get('track_function', None),
) )
services['fs'] = xblock.reference.plugins.FSService() services['fs'] = xblock.reference.plugins.FSService()
self.request_token = kwargs.pop('request_token', None)
super(LmsModuleSystem, self).__init__(**kwargs) super(LmsModuleSystem, self).__init__(**kwargs)
def wrap_aside(self, block, aside, view, frag, context):
"""
Creates a div which identifies the aside, points to the original block,
and writes out the json_init_args into a script tag.
The default implementation creates a frag to wraps frag w/ a div identifying the xblock. If you have
javascript, you'll need to override this impl
"""
extra_data = {
'block-id': quote_slashes(unicode(block.scope_ids.usage_id)),
'url-selector': 'asideBaseUrl',
'runtime-class': 'LmsRuntime',
}
if self.request_token:
extra_data['request-token'] = self.request_token
return self._wrap_ele(
aside,
view,
frag,
extra_data,
)
def get_asides(self, block):
"""
Return all of the asides which might be decorating this `block`.
Arguments:
block (:class:`.XBlock`): The block to render retrieve asides for.
"""
config = XBlockAsidesConfig.current()
if not config.enabled:
return []
if block.scope_ids.block_type in config.disabled_blocks.split():
return []
return [
self.get_aside_of_type(block, aside_type)
for aside_type, __
in XBlockAside.load_classes()
]
...@@ -10,6 +10,7 @@ from unittest import TestCase ...@@ -10,6 +10,7 @@ from unittest import TestCase
from urlparse import urlparse from urlparse import urlparse
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from lms.djangoapps.lms_xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem from lms.djangoapps.lms_xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem
from xblock.fields import ScopeIds
TEST_STRINGS = [ TEST_STRINGS = [
'', '',
...@@ -42,8 +43,7 @@ class TestHandlerUrl(TestCase): ...@@ -42,8 +43,7 @@ class TestHandlerUrl(TestCase):
"""Test the LMS handler_url""" """Test the LMS handler_url"""
def setUp(self): def setUp(self):
self.block = Mock() self.block = Mock(name='block', scope_ids=ScopeIds(None, None, None, 'dummy'))
self.block.scope_ids.usage_id.to_deprecated_string.return_value.encode.return_value = 'dummy'
self.course_key = SlashSeparatedCourseKey("org", "course", "run") self.course_key = SlashSeparatedCourseKey("org", "course", "run")
self.runtime = LmsModuleSystem( self.runtime = LmsModuleSystem(
static_url='/static', static_url='/static',
......
...@@ -21,7 +21,7 @@ from xblock.fields import ScopeIds ...@@ -21,7 +21,7 @@ from xblock.fields import ScopeIds
from courseware.tests import factories from courseware.tests import factories
from courseware.tests.helpers import LoginEnrollmentTestCase from courseware.tests.helpers import LoginEnrollmentTestCase
from lms.lib.xblock.runtime import LmsModuleSystem from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem
from student.roles import CourseStaffRole from student.roles import CourseStaffRole
from student.models import unique_id_for_user from student.models import unique_id_for_user
from xmodule import peer_grading_module from xmodule import peer_grading_module
......
...@@ -1165,7 +1165,7 @@ PIPELINE_JS = { ...@@ -1165,7 +1165,7 @@ PIPELINE_JS = {
'application': { 'application': {
# Application will contain all paths not in courseware_only_js # Application will contain all paths not in courseware_only_js
'source_filenames': sorted(common_js) + sorted(project_js) + [ 'source_filenames': ['js/xblock/core.js'] + sorted(common_js) + sorted(project_js) + [
'js/form.ext.js', 'js/form.ext.js',
'js/my_courses_dropdown.js', 'js/my_courses_dropdown.js',
'js/toggle_login_modal.js', 'js/toggle_login_modal.js',
...@@ -1514,6 +1514,8 @@ INSTALLED_APPS = ( ...@@ -1514,6 +1514,8 @@ INSTALLED_APPS = (
# Surveys # Surveys
'survey', 'survey',
'lms.djangoapps.lms_xblock',
) )
######################### MARKETING SITE ############################### ######################### MARKETING SITE ###############################
......
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
'tender': '//edxedge.tenderapp.com/tender_widget', 'tender': '//edxedge.tenderapp.com/tender_widget',
'coffee/src/ajax_prefix': 'xmodule_js/common_static/coffee/src/ajax_prefix', 'coffee/src/ajax_prefix': 'xmodule_js/common_static/coffee/src/ajax_prefix',
'xmodule_js/common_static/js/test/add_ajax_prefix': 'xmodule_js/common_static/js/test/add_ajax_prefix', 'xmodule_js/common_static/js/test/add_ajax_prefix': 'xmodule_js/common_static/js/test/add_ajax_prefix',
'xblock/core': 'xmodule_js/common_static/coffee/src/xblock/core', 'xblock/core': 'xmodule_js/common_static/js/xblock/core',
'xblock/runtime.v1': 'xmodule_js/common_static/coffee/src/xblock/runtime.v1', 'xblock/runtime.v1': 'xmodule_js/common_static/coffee/src/xblock/runtime.v1',
'xblock/lms.runtime.v1': 'coffee/src/xblock/lms.runtime.v1', 'xblock/lms.runtime.v1': 'coffee/src/xblock/lms.runtime.v1',
'capa/display': 'xmodule_js/src/capa/display', 'capa/display': 'xmodule_js/src/capa/display',
......
...@@ -45,6 +45,7 @@ lib_paths: ...@@ -45,6 +45,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js - xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js
- xmodule_js/common_static/js/vendor/url.min.js - xmodule_js/common_static/js/vendor/url.min.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/js/xblock
- xmodule_js/common_static/coffee/src/xblock - xmodule_js/common_static/coffee/src/xblock
- xmodule_js/common_static/js/vendor/sinon-1.7.1.js - xmodule_js/common_static/js/vendor/sinon-1.7.1.js
- xmodule_js/src/capa/ - xmodule_js/src/capa/
......
...@@ -42,6 +42,7 @@ lib_paths: ...@@ -42,6 +42,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js - xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/common_static/js/vendor/URI.min.js - xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/js/xblock
- xmodule_js/common_static/coffee/src/xblock - xmodule_js/common_static/coffee/src/xblock
- xmodule_js/src/capa/ - xmodule_js/src/capa/
- xmodule_js/src/video/ - xmodule_js/src/video/
......
...@@ -137,6 +137,8 @@ generated-members= ...@@ -137,6 +137,8 @@ generated-members=
category, category,
name, name,
revision, revision,
# For django models
_meta,
[BASIC] [BASIC]
......
...@@ -22,16 +22,16 @@ ...@@ -22,16 +22,16 @@
git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a0c695#egg=django-cas git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a0c695#egg=django-cas
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@2029af2a4b524310847decfb34ef39da8a30dc4e#egg=XBlock -e git+https://github.com/edx/XBlock.git@9c634481dfc85a17dcb3351ca232d7098a38e10e#egg=XBlock
-e git+https://github.com/edx/codejail.git@75307b25032d8b0040b1408c01fd6cc9a1989bd5#egg=codejail -e git+https://github.com/edx/codejail.git@75307b25032d8b0040b1408c01fd6cc9a1989bd5#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.7.2#egg=diff_cover -e git+https://github.com/edx/diff-cover.git@v0.7.2#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool -e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool
-e git+https://github.com/edx/event-tracking.git@0.1.0#egg=event-tracking -e git+https://github.com/edx/event-tracking.git@0.1.0#egg=event-tracking
-e git+https://github.com/edx/bok-choy.git@4a259e3548a19e41cc39433caf68ea58d10a27ba#egg=bok_choy -e git+https://github.com/edx/bok-choy.git@4a259e3548a19e41cc39433caf68ea58d10a27ba#egg=bok_choy
-e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash -e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash
-e git+https://github.com/edx/acid-block.git@df1a7f0cae46567c251d507b8c72168aed8ec042#egg=acid-xblock -e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
-e git+https://github.com/edx/edx-ora2.git@release-2014-10-27T19.33#egg=edx-ora2 -e git+https://github.com/edx/edx-ora2.git@release-2014-10-27T19.33#egg=edx-ora2
-e git+https://github.com/edx/opaque-keys.git@b12401384921c075e5a4ed7aedc3bea57f56ec32#egg=opaque-keys -e git+https://github.com/edx/opaque-keys.git@1254ed4d615a428591850656f39f26509b86d30a#egg=opaque-keys
-e git+https://github.com/edx/ease.git@97de68448e5495385ba043d3091f570a699d5b5f#egg=ease -e git+https://github.com/edx/ease.git@97de68448e5495385ba043d3091f570a699d5b5f#egg=ease
-e git+https://github.com/edx/i18n-tools.git@56f048af9b6868613c14aeae760548834c495011#egg=i18n-tools -e git+https://github.com/edx/i18n-tools.git@56f048af9b6868613c14aeae760548834c495011#egg=i18n-tools
-e git+https://github.com/edx/edx-oauth2-provider.git@0.4.0#egg=oauth2-provider -e git+https://github.com/edx/edx-oauth2-provider.git@0.4.0#egg=oauth2-provider
......
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