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):
'File {filename} exceeds maximum size of '
'{size_mb} MB. Please follow the instructions here '
'to upload a file elsewhere and link to it instead: '
'{faq_url}').format(
filename=filename,
size_mb=settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB,
faq_url=settings.MAX_ASSET_UPLOAD_FILE_SIZE_URL,
'{faq_url}'
).format(
filename=filename,
size_mb=settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB,
faq_url=settings.MAX_ASSET_UPLOAD_FILE_SIZE_URL,
)
}, status=413)
......
......@@ -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 edxmako.shortcuts import render_to_string
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
__all__ = ['orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler']
......@@ -64,6 +64,7 @@ ALWAYS = lambda x: True
# 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_local_resource_url = local_resource_url
xmodule.x_module.descriptor_global_get_asides = get_asides
def hash_resource(resource):
......
......@@ -95,6 +95,10 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
def local_resource_url(self, 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):
"""
......@@ -110,7 +114,7 @@ class StudioUserService(object):
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
rendering module previews.
......@@ -163,6 +167,7 @@ def _preview_module_system(request, descriptor):
descriptor_runtime=descriptor.runtime,
services={
"i18n": ModuleI18nService(),
"field-data": field_data,
},
)
......@@ -181,7 +186,7 @@ def _load_preview_module(request, descriptor):
else:
field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access
descriptor.bind_for_student(
_preview_module_system(request, descriptor),
_preview_module_system(request, descriptor, field_data),
field_data
)
return descriptor
......
......@@ -33,3 +33,14 @@ def local_resource_url(block, uri):
'block_type': block.scope_ids.block_type,
'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({
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce",
"xmodule": "xmodule_js/src/xmodule",
"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",
"utility": "xmodule_js/common_static/js/src/utility",
"accessibility": "xmodule_js/common_static/js/src/accessibility_tools",
......
......@@ -30,6 +30,7 @@ requirejs.config({
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce",
"xmodule": "xmodule_js/src/xmodule",
"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",
"utility": "xmodule_js/common_static/js/src/utility",
"sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1",
......
......@@ -60,6 +60,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/URI.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/js/xblock/
- xmodule_js/common_static/coffee/src/xblock/
- xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js
......
......@@ -55,6 +55,7 @@ lib_paths:
- xmodule_js/src/xmodule.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.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/js/vendor/URI.min.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js
......
......@@ -35,6 +35,7 @@ require.config({
"tinymce": "js/vendor/tinymce/js/tinymce/tinymce.full.min",
"jquery.tinymce": "js/vendor/tinymce/js/tinymce/jquery.tinymce.min",
"xmodule": "/xmodule/xmodule",
"xblock/core": "js/xblock/core",
"xblock": "coffee/src/xblock",
"utility": "js/src/utility",
"accessibility": "js/src/accessibility_tools",
......
"""
Useful django models for implementing XBlock infrastructure in django.
"""
import warnings
from django.db import models
from django.core.exceptions import ValidationError
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 south.modelsinspector import add_introspection_rules
......@@ -91,7 +95,6 @@ class OpaqueKeyField(models.CharField):
super(OpaqueKeyField, self).__init__(*args, **kwargs)
def to_python(self, value):
if value is self.Empty or value is None:
return None
......@@ -140,22 +143,38 @@ class OpaqueKeyField(models.CharField):
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"
KEY_CLASS = CourseKey
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"
KEY_CLASS = UsageKey
class LocationKeyField(UsageKeyField):
"""
A django Field that stores a UsageKey object as a string.
"""
def __init__(self, *args, **kwargs):
warnings.warn("LocationKeyField is deprecated. Please use UsageKeyField instead.", stacklevel=2)
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([], ["^xmodule_django\.models\.LocationKeyField"])
add_introspection_rules([], ["^xmodule_django\.models\.UsageKeyField"])
add_introspection_rules([], [r"^xmodule_django\.models\.CourseKeyField"])
add_introspection_rules([], [r"^xmodule_django\.models\.LocationKeyField"])
add_introspection_rules([], [r"^xmodule_django\.models\.UsageKeyField"])
......@@ -72,7 +72,11 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
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 view in PREVIEW_VIEWS:
......@@ -90,9 +94,10 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
data['init'] = frag.js_init_fn
data['runtime-class'] = runtime_class
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['request-token'] = request_token
data['block-type'] = block.scope_ids.block_type
data['usage-id'] = usage_id_serializer(block.scope_ids.usage_id)
data['request-token'] = request_token
if block.name:
data['name'] = block.name
......
......@@ -33,7 +33,7 @@ class Date(JSONField):
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)
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
if result.tzinfo is None:
result = result.replace(tzinfo=UTC)
......@@ -59,7 +59,7 @@ class Date(JSONField):
return field
else:
msg = "Field {0} has bad value '{1}'".format(
self._name, field)
self.name, field)
raise TypeError(msg)
def to_json(self, value):
......@@ -199,7 +199,7 @@ class RelativeTime(JSONField):
if isinstance(value, basestring):
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)
def to_json(self, value):
......
......@@ -50,6 +50,7 @@ from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError, ReferentialIntegrityError
from xmodule.modulestore.inheritance import InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
from xmodule.modulestore.xml import CourseLocationManager
log = logging.getLogger(__name__)
......@@ -173,6 +174,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
render_template: a function for rendering templates, as per
MakoDescriptorSystem
"""
id_manager = CourseLocationManager(course_key)
kwargs.setdefault('id_reader', id_manager)
kwargs.setdefault('id_generator', id_manager)
super(CachingDescriptorSystem, self).__init__(
field_data=None,
load_item=self.load_item,
......
import sys
import logging
from contracts import contract, new_contract
from fs.osfs import OSFS
from lazy import lazy
from xblock.runtime import KvsFieldData
from xblock.fields import ScopeIds
......@@ -8,13 +9,13 @@ from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, L
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor
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.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import inheriting_field_data, InheritanceMixin
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__)
......@@ -54,6 +55,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
root = modulestore.fs_root / course_entry.structure['_id']
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__(
field_data=None,
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):
message = u"Hello world"
hello_render = lambda _, context: Fragment(message)
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)
......@@ -420,7 +420,7 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
assert_equals(len(course_locations), 1)
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):
"""
Test that references types get deserialized correctly
......
......@@ -18,7 +18,7 @@ from contextlib import contextmanager
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import make_error_tracker, exc_info_to_str
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 import ModuleStoreEnum, ModuleStoreReadBase
from xmodule.tabs import CourseTabList
......@@ -27,7 +27,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from opaque_keys.edx.locator import CourseLocator
from xblock.field_data import DictFieldData
from xblock.runtime import DictKeyValueStore, IdGenerator
from xblock.runtime import DictKeyValueStore
from .exceptions import ItemNotFoundError
......@@ -64,7 +64,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
"""
self.unnamed = defaultdict(int) # category -> num of new url_names for that category
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
self.course_id = course_id
......@@ -175,7 +174,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
descriptor = create_block_from_xml(
etree.tostring(xml_data, encoding='unicode'),
self,
id_generator,
id_manager,
)
except Exception as err: # pylint: disable=broad-except
if not self.load_error_modules:
......@@ -201,7 +200,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
descriptor = ErrorDescriptor.from_xml(
xml,
self,
id_generator,
id_manager,
err_msg
)
......@@ -229,12 +228,16 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
resources_fs = OSFS(xmlstore.data_dir / course_dir)
id_manager = CourseLocationManager(course_id)
super(ImportSystem, self).__init__(
load_item=load_item,
resources_fs=resources_fs,
render_template=render_template,
error_tracker=error_tracker,
process_xml=process_xml,
id_generator=id_manager,
id_reader=id_manager,
**kwargs
)
......@@ -245,12 +248,13 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
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
based within a course
"""
def __init__(self, course_id):
super(CourseLocationManager, self).__init__()
self.course_id = course_id
self.autogen_ids = itertools.count(0)
......@@ -263,6 +267,17 @@ class CourseLocationGenerator(IdGenerator):
slug = 'autogen_{}_{}'.format(block_type, self.autogen_ids.next())
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):
"""
......
......@@ -28,6 +28,7 @@ from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.mongo.draft import DraftModuleStore
from xmodule.modulestore.xml import CourseLocationManager
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES, ModuleStoreDraftAndPublished
......@@ -51,9 +52,16 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method
"""
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):
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,
suffix=suffix,
query=query,
......@@ -61,10 +69,14 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method
def local_resource_url(self, block, uri):
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,
)
# Disable XBlockAsides in most tests
def get_asides(self, block):
return []
def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')):
"""
......@@ -113,13 +125,16 @@ def get_test_descriptor_system():
"""
Construct a test DescriptorSystem instance.
"""
field_data = DictFieldData({})
return MakoDescriptorSystem(
load_item=Mock(),
resources_fs=Mock(),
error_tracker=Mock(),
render_template=mock_render_template,
mixins=(InheritanceMixin, XModuleMixin),
field_data=DictFieldData({}),
field_data=field_data,
services={'field-data': field_data},
)
......@@ -149,13 +164,8 @@ class LogicTest(unittest.TestCase):
raw_field_data = {}
def setUp(self):
class EmptyClass:
"""Empty object."""
url_name = ''
category = 'test'
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
usage_key = self.system.course_id.make_usage_key(self.descriptor.category, 'test_loc')
......
......@@ -8,7 +8,7 @@ from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xmodule.error_module import NonStaffErrorDescriptor
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.tests import DATA_DIR, get_test_system, get_test_descriptor_system
from xmodule.x_module import STUDENT_VIEW
......@@ -60,7 +60,7 @@ class ConditionalFactory(object):
source_descriptor = NonStaffErrorDescriptor.from_xml(
'some random xml data',
system,
id_generator=CourseLocationGenerator(SlashSeparatedCourseKey('edX', 'conditional_test', 'test_run')),
id_generator=CourseLocationManager(source_location.course_key),
error_msg='random error message'
)
else:
......
......@@ -4,7 +4,7 @@ Tests for ErrorModule and NonStaffErrorModule
import unittest
from xmodule.tests import get_test_system
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 xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW
from mock import MagicMock, Mock, patch
......@@ -34,7 +34,7 @@ class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = ErrorDescriptor.from_xml(
self.valid_xml,
self.system,
CourseLocationGenerator(self.course_id),
CourseLocationManager(self.course_id),
self.error_msg
)
self.assertIsInstance(descriptor, ErrorDescriptor)
......@@ -69,7 +69,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = NonStaffErrorDescriptor.from_xml(
self.valid_xml,
self.system,
CourseLocationGenerator(self.course_id)
CourseLocationManager(self.course_id)
)
self.assertIsInstance(descriptor, NonStaffErrorDescriptor)
......@@ -77,7 +77,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = NonStaffErrorDescriptor.from_xml(
self.valid_xml,
self.system,
CourseLocationGenerator(self.course_id)
CourseLocationManager(self.course_id)
)
descriptor.xmodule_runtime = self.system
context_repr = self.system.render(descriptor, STUDENT_VIEW).content
......
......@@ -7,7 +7,7 @@ from unittest import TestCase
from xmodule.x_module import XMLParsingSystem, policy_key
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 xblock.runtime import KvsFieldData, DictKeyValueStore
......@@ -43,7 +43,7 @@ class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem): # pylint: disable
descriptor = create_block_from_xml(
xml,
self,
CourseLocationGenerator(self.course_id),
CourseLocationManager(self.course_id),
)
self._descriptors[descriptor.location.to_deprecated_string()] = descriptor
return descriptor
......
......@@ -18,12 +18,13 @@ from webob.multidict import MultiDict
from xblock.core import XBlock
from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String, Dict
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.errortracker import exc_info_to_str
from xmodule.modulestore.exceptions import ItemNotFoundError
from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.asides import AsideUsageKeyV1, AsideDefinitionKeyV1
from xmodule.exceptions import UndefinedContext
import dogstats_wrapper as dog_stats_api
......@@ -65,7 +66,7 @@ class OpaqueKeyReader(IdReader):
Returns:
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):
"""Retrieve the block_type of a particular definition
......@@ -78,6 +79,91 @@ class OpaqueKeyReader(IdReader):
"""
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):
pass
......@@ -160,6 +246,8 @@ class XModuleMixin(XBlockMixin):
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
# This indicates whether the xmodule is a problem-type.
......@@ -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
for those fields.
"""
# Set the descriptor first so that we can proxy to it
self.descriptor = descriptor
super(XModule, self).__init__(*args, **kwargs)
......@@ -715,7 +804,6 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
create a problem, and can generate XModules (which do know about student
state).
"""
entry_point = "xmodule.v1"
module_class = XModule
# VS[compat]. Backwards compatibility code that can go away after
......@@ -997,7 +1085,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
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):
"""
......@@ -1013,7 +1101,7 @@ class ConfigurableFragmentWrapper(object): # pylint: disable=abstract-method
else:
self.wrappers = []
def wrap_child(self, block, view, frag, context):
def wrap_xblock(self, block, view, frag, context):
"""
See :func:`Runtime.wrap_child`
"""
......@@ -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")
# 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):
"""
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
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
self.export_fs = None
......@@ -1215,6 +1315,19 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p
# global function that the application can override.
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):
"""
See :meth:`xblock.runtime.Runtime:resource_url` for documentation.
......@@ -1335,7 +1448,9 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin
# Usage_store is unused, and field_data is often supplanted with an
# 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.xqueue = xqueue
......@@ -1373,6 +1488,9 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin
self.descriptor_runtime = descriptor_runtime
self.rebind_noauth_module_to_user = rebind_noauth_module_to_user
if user:
self.user_id = user.id
def get(self, attr):
""" provide uniform access to attributes (like etree)."""
return self.__dict__.get(attr)
......
......@@ -35,7 +35,7 @@ describe "XBlock", ->
window.initFnZ = jasmine.createSpy()
@fakeChildren = ['list', 'of', 'children']
spyOn(XBlock, 'initializeBlocks').andReturn(@fakeChildren)
spyOn(XBlock, 'initializeXBlocks').andReturn(@fakeChildren)
@vANode = $('#vA')[0]
@vZNode = $('#vZ')[0]
......@@ -50,8 +50,8 @@ describe "XBlock", ->
expect(TestRuntime.vZ).toHaveBeenCalledWith()
it "loads the right init function", ->
expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, @vANode)
expect(window.initFnZ).toHaveBeenCalledWith(@runtimeZ, @vZNode)
expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, @vANode, {})
expect(window.initFnZ).toHaveBeenCalledWith(@runtimeZ, @vZNode, {})
it "loads when missing versions", ->
expect(@missingVersionBlock.element).toBe($('#missing-version'))
......@@ -74,8 +74,8 @@ describe "XBlock", ->
expect(@missingInitBlock.element).toBe($('#missing-init')[0])
it "passes through the request token", ->
expect(XBlock.initializeBlocks).toHaveBeenCalledWith($(@vANode), 'req-token-a')
expect(XBlock.initializeBlocks).toHaveBeenCalledWith($(@vZNode), 'req-token-z')
expect(XBlock.initializeXBlocks).toHaveBeenCalledWith($(@vANode), 'req-token-a')
expect(XBlock.initializeXBlocks).toHaveBeenCalledWith($(@vZNode), 'req-token-z')
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:
# Paths to source JavaScript files
src_paths:
- js/xblock
- coffee/src
- js/src
- js/utils
......
......@@ -51,7 +51,7 @@ class ContainerPage(PageObject):
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.
# 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.
# - common/static/js/views/xblock.js which adds the class "xblock-initialization-failed"
# if the xblock threw an error while initializing.
......
......@@ -3,7 +3,7 @@
End-to-end tests for the LMS.
"""
from unittest import skip
from unittest import expectedFailure
from ..helpers import UniqueCourseTest
from ...pages.lms.auto_auth import AutoAuthPage
......@@ -44,17 +44,6 @@ class XBlockAcidBase(UniqueCourseTest):
self.assertTrue(acid_block.scope_passed('preferences'))
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):
"""
......@@ -81,7 +70,15 @@ class XBlockAcidNoChildTest(XBlockAcidBase):
).install()
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):
......@@ -129,3 +126,46 @@ class XBlockAcidChildTest(XBlockAcidBase):
acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
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 (
XModuleStudentInfoField
)
import logging
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.block_types import BlockTypeKeyV1
from opaque_keys.edx.asides import AsideUsageKeyV1
from django.db import DatabaseError
from django.contrib.auth.models import User
from xblock.runtime import KeyValueStore
from xblock.exceptions import KeyValueMultiSaveError, InvalidScopeError
from xblock.fields import Scope, UserScope
from xmodule.modulestore.django import modulestore
from xblock.core import XBlockAside
log = logging.getLogger(__name__)
......@@ -46,7 +47,7 @@ class FieldDataCache(object):
A cache of django model objects needed to supply the data
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
in descriptors. Attempts to minimize the number of queries to the database.
......@@ -58,11 +59,17 @@ class FieldDataCache(object):
course_id: The id of the current course
user: The user for which to cache data
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.descriptors = descriptors
self.select_for_update = select_for_update
if asides is None:
self.asides = []
else:
self.asides = asides
assert isinstance(course_id, CourseKey)
self.course_id = course_id
self.user = user
......@@ -75,7 +82,7 @@ class FieldDataCache(object):
@classmethod
def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None,
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.
user: the django user for whom to load modules.
......@@ -113,7 +120,7 @@ class FieldDataCache(object):
with modulestore().bulk_operations(descriptor.location.course_key):
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):
"""
......@@ -140,6 +147,35 @@ class FieldDataCache(object):
)
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):
"""
Queries the database for all of the fields in the specified scope
......@@ -148,7 +184,7 @@ class FieldDataCache(object):
return self._chunked_query(
StudentModule,
'module_state_key__in',
(descriptor.scope_ids.usage_id for descriptor in self.descriptors),
self._all_usage_ids,
course_id=self.course_id,
student=self.user.pk,
)
......@@ -156,14 +192,14 @@ class FieldDataCache(object):
return self._chunked_query(
XModuleUserStateSummaryField,
'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),
)
elif scope == Scope.preferences:
return self._chunked_query(
XModuleStudentPrefsField,
'module_type__in',
set(descriptor.scope_ids.block_type for descriptor in self.descriptors),
self._all_block_types,
student=self.user.pk,
field_name__in=set(field.name for field in fields),
)
......@@ -195,7 +231,7 @@ class FieldDataCache(object):
elif key.scope == Scope.user_state_summary:
return (key.scope, key.block_scope_id, key.field_name)
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:
return (key.scope, key.field_name)
......@@ -239,31 +275,28 @@ class FieldDataCache(object):
return field_object
if key.scope == Scope.user_state:
# When we start allowing block_scope_ids to be either Locations or Locators,
# 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(
field_object, __ = StudentModule.objects.get_or_create(
course_id=self.course_id,
student_id=key.user_id,
module_state_key=key.block_scope_id,
defaults={
'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:
field_object, _ = XModuleUserStateSummaryField.objects.get_or_create(
field_object, __ = XModuleUserStateSummaryField.objects.get_or_create(
field_name=key.field_name,
usage_id=key.block_scope_id
)
elif key.scope == Scope.preferences:
field_object, _ = XModuleStudentPrefsField.objects.get_or_create(
field_object, __ = XModuleStudentPrefsField.objects.get_or_create(
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,
)
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,
student_id=key.user_id,
)
......
......@@ -18,7 +18,7 @@ from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from xmodule_django.models import CourseKeyField, LocationKeyField
from xmodule_django.models import CourseKeyField, LocationKeyField, BlockTypeKeyField
class StudentModule(models.Model):
......@@ -36,10 +36,7 @@ class StudentModule(models.Model):
## These three are the key for the object
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,
# but for abtests and the like, this can be set to a shared value
# for many instances of the module.
# Filename for homeworks, etc.
# Key used to share state. This is the XBlock usage_id
module_state_key = LocationKeyField(max_length=255, db_index=True, db_column='module_id')
student = models.ForeignKey(User, db_index=True)
......@@ -150,7 +147,7 @@ class XBlockFieldBase(models.Model):
return u'{}<{!r}'.format(
self.__class__.__name__,
{
key:getattr(self, key)
key: getattr(self, key)
for key in self._meta.get_all_field_names()
if key not in ('created', 'modified')
}
......@@ -174,11 +171,11 @@ class XModuleStudentPrefsField(XBlockFieldBase):
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'),)
# 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)
......
......@@ -23,6 +23,7 @@ from courseware.masquerade import setup_masquerade
from courseware.model_data import FieldDataCache, DjangoKeyValueStore
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.models import XBlockAsidesConfig
from edxmako.shortcuts import render_to_string
from eventtracking import tracker
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
......@@ -405,7 +406,8 @@ def get_module_system_for_user(user, field_data_cache,
field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents(
course_id,
real_user,
module.descriptor
module.descriptor,
asides=XBlockAsidesConfig.possible_asides(),
)
(inner_system, inner_student_data) = get_module_system_for_user(
......@@ -496,6 +498,8 @@ def get_module_system_for_user(user, field_data_cache,
else:
anonymous_student_id = anonymous_id_for_user(user, None)
field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access
system = LmsModuleSystem(
track_function=track_function,
render_template=render_to_string,
......@@ -541,11 +545,13 @@ def get_module_system_for_user(user, field_data_cache,
services={
'i18n': ModuleI18nService(),
'fs': xblock.reference.plugins.FSService(),
'field-data': field_data,
},
get_user_role=lambda: get_user_role(user, course_id),
descriptor_runtime=descriptor.runtime,
rebind_noauth_module_to_user=rebind_noauth_module_to_user,
user_location=user_location,
request_token=request_token,
)
# pass position specified in URL to module through ModuleSystem
......@@ -572,7 +578,7 @@ def get_module_system_for_user(user, field_data_cache,
else:
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
......@@ -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):
return None
(system, student_data) = get_module_system_for_user(
(system, field_data) = get_module_system_for_user(
user=user,
field_data_cache=field_data_cache, # These have implicit user bindings, the rest of args are considered not to
descriptor=descriptor,
......@@ -609,7 +615,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
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
return descriptor
......
......@@ -16,9 +16,10 @@ from courseware.tests.factories import UserStateSummaryFactory
from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory
from xblock.fields import Scope, BlockScope, ScopeIds
from xblock.exceptions import KeyValueMultiSaveError
from xblock.core import XBlock
from django.test import TestCase
from django.db import DatabaseError
from xblock.exceptions import KeyValueMultiSaveError
def mock_field(scope, name):
......@@ -29,7 +30,7 @@ def mock_field(scope, name):
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.module_class.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
from courseware.models import StudentModule, StudentModuleHistory
from course_modes.models import CourseMode
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from open_ended_grading import open_ended_notifications
from student.models import UserTestGroup, CourseEnrollment
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):
# Load all descendants of the section, because we're going to display its
# html, which in general will need all of its children
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
if position is not None:
......
......@@ -18,6 +18,7 @@ from django.utils.html import escape
from django.http import Http404
from django.conf import settings
from util.json_request import JsonResponse
from mock import patch
from lms.djangoapps.lms_xblock.runtime import quote_slashes
from xmodule_modifiers import wrap_xblock
......@@ -323,17 +324,28 @@ def _section_data_download(course, access):
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):
""" Provide data for the corresponding bulk email section """
course_key = course.id
# This HtmlDescriptor is only being used to generate a nice text editor.
html_module = HtmlDescriptor(
course.system,
DictFieldData({'data': ''}),
ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake'))
)
fragment = course.system.render(html_module, 'studio_view')
# Monkey-patch descriptor_global_get_asides to return no asides for the duration of this render
with patch('xmodule.x_module.descriptor_global_get_asides', null_get_asides):
# This HtmlDescriptor is only being used to generate a nice text editor.
html_module = HtmlDescriptor(
course.system,
DictFieldData({'data': ''}),
ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake'))
)
fragment = course.system.render(html_module, 'studio_view')
fragment = wrap_xblock(
'LmsRuntime', html_module, 'studio_view', fragment, None,
extra_data={"course-id": unicode(course_key)},
......
"""
Django admin dashboard configuration for LMS XBlock infrastructure.
"""
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
......
......@@ -16,7 +16,7 @@ class LmsFieldData(SplitFieldData):
def __init__(self, authored_data, student_data):
# Make sure that we don't repeatedly nest LmsFieldData instances
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:
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 config_models.models import ConfigurationModel
from xblock.core import XBlockAside
class XBlockAsidesConfig(ConfigurationModel):
"""
Configuration for XBlockAsides.
......@@ -11,3 +22,10 @@ class XBlockAsidesConfig(ConfigurationModel):
default="about course_info static_tab",
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
from django.core.urlresolvers import reverse
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 xblock.core import XBlockAside
from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem
from xmodule.partitions.partitions_service import PartitionService
......@@ -87,8 +89,8 @@ class LmsHandlerUrls(object):
view_name = 'xblock_handler_noauth'
url = reverse(view_name, kwargs={
'course_id': self.course_id.to_deprecated_string(),
'usage_id': quote_slashes(block.scope_ids.usage_id.to_deprecated_string().encode('utf-8')),
'course_id': unicode(self.course_id),
'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
'handler': handler_name,
'suffix': suffix,
})
......@@ -198,4 +200,50 @@ class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract
track_function=kwargs.get('track_function', None),
)
services['fs'] = xblock.reference.plugins.FSService()
self.request_token = kwargs.pop('request_token', None)
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
from urlparse import urlparse
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from lms.djangoapps.lms_xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem
from xblock.fields import ScopeIds
TEST_STRINGS = [
'',
......@@ -42,8 +43,7 @@ class TestHandlerUrl(TestCase):
"""Test the LMS handler_url"""
def setUp(self):
self.block = Mock()
self.block.scope_ids.usage_id.to_deprecated_string.return_value.encode.return_value = 'dummy'
self.block = Mock(name='block', scope_ids=ScopeIds(None, None, None, 'dummy'))
self.course_key = SlashSeparatedCourseKey("org", "course", "run")
self.runtime = LmsModuleSystem(
static_url='/static',
......
......@@ -21,7 +21,7 @@ from xblock.fields import ScopeIds
from courseware.tests import factories
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.models import unique_id_for_user
from xmodule import peer_grading_module
......
......@@ -1165,7 +1165,7 @@ PIPELINE_JS = {
'application': {
# 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/my_courses_dropdown.js',
'js/toggle_login_modal.js',
......@@ -1514,6 +1514,8 @@ INSTALLED_APPS = (
# Surveys
'survey',
'lms.djangoapps.lms_xblock',
)
######################### MARKETING SITE ###############################
......
......@@ -49,7 +49,7 @@
'tender': '//edxedge.tenderapp.com/tender_widget',
'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',
'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/lms.runtime.v1': 'coffee/src/xblock/lms.runtime.v1',
'capa/display': 'xmodule_js/src/capa/display',
......
......@@ -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/url.min.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/js/vendor/sinon-1.7.1.js
- xmodule_js/src/capa/
......
......@@ -42,6 +42,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/common_static/js/vendor/URI.min.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/src/capa/
- xmodule_js/src/video/
......
......@@ -137,6 +137,8 @@ generated-members=
category,
name,
revision,
# For django models
_meta,
[BASIC]
......
......@@ -22,16 +22,16 @@
git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a0c695#egg=django-cas
# 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/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/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-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/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/i18n-tools.git@56f048af9b6868613c14aeae760548834c495011#egg=i18n-tools
-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