Commit fc131fa8 by Don Mitchell

Add InheritanceKVS and standardize inherited attr patterns

parent 8201b141
...@@ -218,7 +218,7 @@ class TemplateTests(unittest.TestCase): ...@@ -218,7 +218,7 @@ class TemplateTests(unittest.TestCase):
) )
usage_id = json_data.get('_id', None) usage_id = json_data.get('_id', None)
if not '_inherited_settings' in json_data and parent_xblock is not None: if not '_inherited_settings' in json_data and parent_xblock is not None:
json_data['_inherited_settings'] = parent_xblock.xblock_kvs.get_inherited_settings().copy() json_data['_inherited_settings'] = parent_xblock.xblock_kvs.inherited_settings.copy()
json_fields = json_data.get('fields', {}) json_fields = json_data.get('fields', {})
for field_name in inheritance.InheritanceMixin.fields: for field_name in inheritance.InheritanceMixin.fields:
if field_name in json_fields: if field_name in json_fields:
......
...@@ -26,7 +26,6 @@ describe "Test Metadata Editor", -> ...@@ -26,7 +26,6 @@ describe "Test Metadata Editor", ->
explicitly_set: true, explicitly_set: true,
field_name: "display_name", field_name: "display_name",
help: "Specifies the name for this component.", help: "Specifies the name for this component.",
inheritable: false,
options: [], options: [],
type: CMS.Models.Metadata.GENERIC_TYPE, type: CMS.Models.Metadata.GENERIC_TYPE,
value: "Word cloud" value: "Word cloud"
...@@ -38,7 +37,6 @@ describe "Test Metadata Editor", -> ...@@ -38,7 +37,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "show_answer", field_name: "show_answer",
help: "When should you show the answer", help: "When should you show the answer",
inheritable: true,
options: [ options: [
{"display_name": "Always", "value": "always"}, {"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"}, {"display_name": "Answered", "value": "answered"},
...@@ -54,7 +52,6 @@ describe "Test Metadata Editor", -> ...@@ -54,7 +52,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "num_inputs", field_name: "num_inputs",
help: "Number of text boxes for student to input words/sentences.", help: "Number of text boxes for student to input words/sentences.",
inheritable: false,
options: {min: 1}, options: {min: 1},
type: CMS.Models.Metadata.INTEGER_TYPE, type: CMS.Models.Metadata.INTEGER_TYPE,
value: 5 value: 5
...@@ -66,7 +63,6 @@ describe "Test Metadata Editor", -> ...@@ -66,7 +63,6 @@ describe "Test Metadata Editor", ->
explicitly_set: true, explicitly_set: true,
field_name: "weight", field_name: "weight",
help: "Weight for this problem", help: "Weight for this problem",
inheritable: true,
options: {min: 1.3, max:100.2, step:0.1}, options: {min: 1.3, max:100.2, step:0.1},
type: CMS.Models.Metadata.FLOAT_TYPE, type: CMS.Models.Metadata.FLOAT_TYPE,
value: 10.2 value: 10.2
...@@ -78,7 +74,6 @@ describe "Test Metadata Editor", -> ...@@ -78,7 +74,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "list", field_name: "list",
help: "A list of things.", help: "A list of things.",
inheritable: false,
options: [], options: [],
type: CMS.Models.Metadata.LIST_TYPE, type: CMS.Models.Metadata.LIST_TYPE,
value: ["the first display value", "the second"] value: ["the first display value", "the second"]
...@@ -99,7 +94,6 @@ describe "Test Metadata Editor", -> ...@@ -99,7 +94,6 @@ describe "Test Metadata Editor", ->
explicitly_set: true, explicitly_set: true,
field_name: "unknown_type", field_name: "unknown_type",
help: "Mystery property.", help: "Mystery property.",
inheritable: false,
options: [ options: [
{"display_name": "Always", "value": "always"}, {"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"}, {"display_name": "Answered", "value": "answered"},
...@@ -145,7 +139,6 @@ describe "Test Metadata Editor", -> ...@@ -145,7 +139,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "display_name", field_name: "display_name",
help: "", help: "",
inheritable: false,
options: [], options: [],
type: CMS.Models.Metadata.GENERIC_TYPE, type: CMS.Models.Metadata.GENERIC_TYPE,
value: null value: null
......
...@@ -3,6 +3,7 @@ from pytz import UTC ...@@ -3,6 +3,7 @@ from pytz import UTC
from xblock.fields import Scope, Boolean, String, Float, XBlockMixin from xblock.fields import Scope, Boolean, String, Float, XBlockMixin
from xmodule.fields import Date, Timedelta from xmodule.fields import Date, Timedelta
from xblock.runtime import KeyValueStore
class InheritanceMixin(XBlockMixin): class InheritanceMixin(XBlockMixin):
...@@ -51,16 +52,18 @@ def compute_inherited_metadata(descriptor): ...@@ -51,16 +52,18 @@ def compute_inherited_metadata(descriptor):
NOTE: This means that there is no such thing as lazy loading at the NOTE: This means that there is no such thing as lazy loading at the
moment--this accesses all the children.""" moment--this accesses all the children."""
for child in descriptor.get_children(): if descriptor.has_children:
inherit_metadata( parent_metadata = descriptor.xblock_kvs.inherited_settings.copy()
child, # add any of descriptor's explicitly set fields to the inheriting list
{ for field in InheritanceMixin.fields.values():
name: field.read_from(descriptor) # pylint: disable = W0212
for name, field in InheritanceMixin.fields.items() if descriptor._field_data.has(descriptor, field.name):
if field.is_set_on(descriptor) # inherited_settings values are json repr
} parent_metadata[field.name] = field.read_json(descriptor)
)
compute_inherited_metadata(child) for child in descriptor.get_children():
inherit_metadata(child, parent_metadata)
compute_inherited_metadata(child)
def inherit_metadata(descriptor, inherited_data): def inherit_metadata(descriptor, inherited_data):
...@@ -72,53 +75,46 @@ def inherit_metadata(descriptor, inherited_data): ...@@ -72,53 +75,46 @@ def inherit_metadata(descriptor, inherited_data):
`inherited_data`: A dictionary mapping field names to the values that `inherited_data`: A dictionary mapping field names to the values that
they should inherit they should inherit
""" """
# The inherited values that are actually being used. try:
if not hasattr(descriptor, '_inherited_metadata'): descriptor.xblock_kvs.inherited_settings = inherited_data
setattr(descriptor, '_inherited_metadata', {}) except AttributeError: # the kvs doesn't have inherited_settings probably b/c it's an error module
pass
# All inheritable metadata values (for which a value exists in field_data).
if not hasattr(descriptor, '_inheritable_metadata'):
setattr(descriptor, '_inheritable_metadata', {})
# Set all inheritable metadata from kwargs that are
# in self.inheritable_metadata and aren't already set in metadata
for name, field in InheritanceMixin.fields.items():
if name not in inherited_data:
continue
inherited_value = inherited_data[name]
descriptor._inheritable_metadata[name] = inherited_value
if not field.is_set_on(descriptor):
descriptor._inherited_metadata[name] = inherited_value
field.write_to(descriptor, inherited_value)
# We've updated the fields on the descriptor, so we need to save it
descriptor.save()
def own_metadata(module): def own_metadata(module):
# IN SPLIT MONGO this is just ['metadata'] as it keeps ['_inherited_metadata'] separate!
# FIXME move into kvs? will that work for xml mongo?
""" """
Return a dictionary that contains only non-inherited field keys, Return a dictionary that contains only non-inherited field keys,
mapped to their serialized values mapped to their serialized values
""" """
inherited_metadata = getattr(module, '_inherited_metadata', {}) return module.get_explicitly_set_fields_by_scope(Scope.settings)
metadata = {}
for name, field in module.fields.items(): class InheritanceKeyValueStore(KeyValueStore):
# Only save metadata that wasn't inherited """
if field.scope != Scope.settings: Common superclass for kvs's which know about inheritance of settings. Offers simple
continue dict-based storage of fields and lookup of inherited values.
if not field.is_set_on(module): Note: inherited_settings is a dict of key to json values (internal xblock field repr)
continue """
def __init__(self, initial_values=None, inherited_settings=None):
if name in inherited_metadata and field.read_from(module) == inherited_metadata.get(name): super(InheritanceKeyValueStore, self).__init__()
continue self.inherited_settings = inherited_settings or {}
self._fields = initial_values or {}
try:
metadata[name] = field.read_json(module) def get(self, key):
except KeyError: return self._fields[key.field_name]
# Ignore any missing keys in _field_data
pass def set(self, key, value):
# xml backed courses are read-only, but they do have some computed fields
return metadata self._fields[key.field_name] = value
def delete(self, key):
del self._fields[key.field_name]
def has(self, key):
return key.field_name in self._fields
def default(self, key):
"""
Check to see if the default should be from inheritance rather than from the field's global default
"""
return self.inherited_settings[key.field_name]
...@@ -17,25 +17,23 @@ import sys ...@@ -17,25 +17,23 @@ import sys
import logging import logging
import copy import copy
from collections import namedtuple
from fs.osfs import OSFS from fs.osfs import OSFS
from itertools import repeat from itertools import repeat
from path import path from path import path
from operator import attrgetter from operator import attrgetter
from uuid import uuid4
from importlib import import_module from importlib import import_module
from xmodule.errortracker import null_error_tracker, exc_info_to_str from xmodule.errortracker import null_error_tracker, exc_info_to_str
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xblock.runtime import DbModel, KeyValueStore from xblock.runtime import DbModel
from xblock.exceptions import InvalidScopeError from xblock.exceptions import InvalidScopeError
from xblock.fields import Scope, ScopeIds from xblock.fields import Scope, ScopeIds
from xmodule.modulestore import ModuleStoreBase, Location, namedtuple_to_son, MONGO_MODULESTORE_TYPE from xmodule.modulestore import ModuleStoreBase, Location, namedtuple_to_son, MONGO_MODULESTORE_TYPE
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -58,12 +56,13 @@ class InvalidWriteError(Exception): ...@@ -58,12 +56,13 @@ class InvalidWriteError(Exception):
""" """
class MongoKeyValueStore(KeyValueStore): class MongoKeyValueStore(InheritanceKeyValueStore):
""" """
A KeyValueStore that maps keyed data access to one of the 3 data areas A KeyValueStore that maps keyed data access to one of the 3 data areas
known to the MongoModuleStore (data, children, and metadata) known to the MongoModuleStore (data, children, and metadata)
""" """
def __init__(self, data, children, metadata): def __init__(self, data, children, metadata):
super(MongoKeyValueStore, self).__init__()
self._data = data self._data = data
self._children = children self._children = children
self._metadata = metadata self._metadata = metadata
...@@ -201,10 +200,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -201,10 +200,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
# Convert the serialized fields values in self.cached_metadata # Convert the serialized fields values in self.cached_metadata
# to python values # to python values
metadata_to_inherit = { metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {})
key: module.fields[key].from_json(value)
for key, value in self.cached_metadata.get(non_draft_loc.url(), {}).items()
}
inherit_metadata(module, metadata_to_inherit) inherit_metadata(module, metadata_to_inherit)
# decache any computed pending field settings # decache any computed pending field settings
module.save() module.save()
......
...@@ -4,37 +4,31 @@ from collections import namedtuple ...@@ -4,37 +4,31 @@ from collections import namedtuple
from xblock.runtime import KeyValueStore from xblock.runtime import KeyValueStore
from xblock.exceptions import InvalidScopeError from xblock.exceptions import InvalidScopeError
from .definition_lazy_loader import DefinitionLazyLoader from .definition_lazy_loader import DefinitionLazyLoader
from xmodule.modulestore.inheritance import InheritanceKeyValueStore
# id is a BlockUsageLocator, def_id is the definition's guid # id is a BlockUsageLocator, def_id is the definition's guid
SplitMongoKVSid = namedtuple('SplitMongoKVSid', 'id, def_id') SplitMongoKVSid = namedtuple('SplitMongoKVSid', 'id, def_id')
PROVENANCE_LOCAL = 'local' class SplitMongoKVS(InheritanceKeyValueStore):
PROVENANCE_DEFAULT = 'default'
PROVENANCE_INHERITED = 'inherited'
class SplitMongoKVS(KeyValueStore):
""" """
A KeyValueStore that maps keyed data access to one of the 3 data areas A KeyValueStore that maps keyed data access to one of the 3 data areas
known to the MongoModuleStore (data, children, and metadata) known to the MongoModuleStore (data, children, and metadata)
""" """
def __init__(self, definition, fields, _inherited_settings): def __init__(self, definition, fields, inherited_settings):
""" """
:param definition: either a lazyloader or definition id for the definition :param definition: either a lazyloader or definition id for the definition
:param fields: a dictionary of the locally set fields :param fields: a dictionary of the locally set fields
:param _inherited_settings: the value of each inheritable field from above this. :param inherited_settings: the json value of each inheritable field from above this.
Note, local fields may override and disagree w/ this b/c this says what the value Note, local fields may override and disagree w/ this b/c this says what the value
should be if the field is undefined. should be if the field is undefined.
""" """
# ensure kvs's don't share objects w/ others so that changes can't appear in separate ones super(SplitMongoKVS, self).__init__(copy.copy(fields), inherited_settings)
# the particular use case was that changes to kvs's were polluting caches. My thinking was
# that kvs's should be independent thus responsible for the isolation.
self._definition = definition # either a DefinitionLazyLoader or the db id of the definition. self._definition = definition # either a DefinitionLazyLoader or the db id of the definition.
# if the db id, then the definition is presumed to be loaded into _fields # if the db id, then the definition is presumed to be loaded into _fields
self._fields = copy.copy(fields)
self._inherited_settings = _inherited_settings
def get(self, key): def get(self, key):
# simplest case, field is directly set # simplest case, field is directly set
...@@ -49,12 +43,8 @@ class SplitMongoKVS(KeyValueStore): ...@@ -49,12 +43,8 @@ class SplitMongoKVS(KeyValueStore):
# didn't find children in _fields; so, see if there's a default # didn't find children in _fields; so, see if there's a default
raise KeyError() raise KeyError()
elif key.scope == Scope.settings: elif key.scope == Scope.settings:
# didn't find in _fields; so, get from inheritance since not locally set # get default which may be the inherited value
if key.field_name in self._inherited_settings: raise KeyError()
return self._inherited_settings[key.field_name]
else:
# or get default
raise KeyError()
elif key.scope == Scope.content: elif key.scope == Scope.content:
if isinstance(self._definition, DefinitionLazyLoader): if isinstance(self._definition, DefinitionLazyLoader):
self._load_definition() self._load_definition()
...@@ -113,40 +103,6 @@ class SplitMongoKVS(KeyValueStore): ...@@ -113,40 +103,6 @@ class SplitMongoKVS(KeyValueStore):
# if someone changes it so that they do, then change any tests of field.name in xx._field_data # if someone changes it so that they do, then change any tests of field.name in xx._field_data
return key.field_name in self._fields return key.field_name in self._fields
# would like to just take a key, but there's a bunch of magic in DbModel for constructing the key via
# a private method
def field_value_provenance(self, key_scope, key_name):
"""
Where the field value comes from: one of [PROVENANCE_LOCAL, PROVENANCE_DEFAULT, PROVENANCE_INHERITED].
"""
# handle any special cases
if key_scope == Scope.content:
if key_name == 'location':
return PROVENANCE_LOCAL
elif key_name == 'category':
return PROVENANCE_LOCAL
else:
self._load_definition()
if key_name in self._fields:
return PROVENANCE_LOCAL
else:
return PROVENANCE_DEFAULT
elif key_scope == Scope.parent:
return PROVENANCE_DEFAULT
# catch the locally set state
elif key_name in self._fields:
return PROVENANCE_LOCAL
elif key_scope == Scope.settings and key_name in self._inherited_settings:
return PROVENANCE_INHERITED
else:
return PROVENANCE_DEFAULT
def get_inherited_settings(self):
"""
Get the settings set by the ancestors (which locally set fields may override or not)
"""
return self._inherited_settings
def _load_definition(self): def _load_definition(self):
""" """
Update fields w/ the lazily loaded definitions Update fields w/ the lazily loaded definitions
......
...@@ -19,7 +19,9 @@ from path import path ...@@ -19,7 +19,9 @@ from path import path
import calc import calc
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
from xmodule.x_module import ModuleSystem, XModuleDescriptor from xmodule.x_module import ModuleSystem, XModuleDescriptor, DescriptorSystem
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.mako_module import MakoDescriptorSystem
# Location of common test DATA directory # Location of common test DATA directory
...@@ -64,7 +66,20 @@ def get_test_system(): ...@@ -64,7 +66,20 @@ def get_test_system():
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
xblock_field_data=lambda descriptor: descriptor._field_data, xblock_field_data=lambda descriptor: descriptor._field_data,
anonymous_student_id='student', anonymous_student_id='student',
open_ended_grading_interface= open_ended_grading_interface open_ended_grading_interface=open_ended_grading_interface
)
def get_test_descriptor_system():
"""
Construct a test DescriptorSystem instance.
"""
return MakoDescriptorSystem(
load_item=Mock(),
resources_fs=Mock(),
error_tracker=Mock(),
render_template=lambda template, context: repr(context),
mixins=(InheritanceMixin,),
) )
......
...@@ -9,7 +9,7 @@ from xmodule.editing_module import TabsEditingDescriptor ...@@ -9,7 +9,7 @@ from xmodule.editing_module import TabsEditingDescriptor
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from .import get_test_system from xmodule.tests import get_test_descriptor_system
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -19,7 +19,7 @@ class TabsEditingDescriptorTestCase(unittest.TestCase): ...@@ -19,7 +19,7 @@ class TabsEditingDescriptorTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
super(TabsEditingDescriptorTestCase, self).setUp() super(TabsEditingDescriptorTestCase, self).setUp()
system = get_test_system() system = get_test_descriptor_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
self.tabs = [ self.tabs = [
{ {
...@@ -44,8 +44,8 @@ class TabsEditingDescriptorTestCase(unittest.TestCase): ...@@ -44,8 +44,8 @@ class TabsEditingDescriptorTestCase(unittest.TestCase):
] ]
TabsEditingDescriptor.tabs = self.tabs TabsEditingDescriptor.tabs = self.tabs
self.descriptor = TabsEditingDescriptor( self.descriptor = system.construct_xblock_from_class(
runtime=system, TabsEditingDescriptor,
field_data=DictFieldData({}), field_data=DictFieldData({}),
scope_ids=ScopeIds(None, None, None, None), scope_ids=ScopeIds(None, None, None, None),
) )
......
...@@ -158,11 +158,10 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -158,11 +158,10 @@ class ImportTestCase(BaseCourseTestCase):
# Check that the child inherits due correctly # Check that the child inherits due correctly
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.due, ImportTestCase.date.from_json(v)) self.assertEqual(child.due, ImportTestCase.date.from_json(v))
self.assertEqual(child._inheritable_metadata, child._inherited_metadata) # need to convert v to canonical json b4 comparing
self.assertEqual(1, len(child._inherited_metadata))
self.assertEqual( self.assertEqual(
datetime.datetime(2013, 3, 20, 17, 0, tzinfo=UTC()), ImportTestCase.date.to_json(ImportTestCase.date.from_json(v)),
child._inherited_metadata['due'] child.xblock_kvs.inherited_settings['due']
) )
# Now export and check things # Now export and check things
...@@ -218,8 +217,6 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -218,8 +217,6 @@ class ImportTestCase(BaseCourseTestCase):
# Check that the child does not inherit a value for due # Check that the child does not inherit a value for due
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.due, None) self.assertEqual(child.due, None)
# pylint: disable=W0212
self.assertEqual(child._inheritable_metadata, child._inherited_metadata)
self.assertLessEqual( self.assertLessEqual(
child.start, child.start,
datetime.datetime.now(UTC()) datetime.datetime.now(UTC())
...@@ -249,10 +246,9 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -249,10 +246,9 @@ class ImportTestCase(BaseCourseTestCase):
self.assertEqual(descriptor.due, ImportTestCase.date.from_json(course_due)) self.assertEqual(descriptor.due, ImportTestCase.date.from_json(course_due))
self.assertEqual(child.due, ImportTestCase.date.from_json(child_due)) self.assertEqual(child.due, ImportTestCase.date.from_json(child_due))
# Test inherited metadata. Due does not appear here (because explicitly set on child). # Test inherited metadata. Due does not appear here (because explicitly set on child).
self.assertEqual(1, len(child._inheritable_metadata))
self.assertEqual( self.assertEqual(
datetime.datetime(2013, 3, 20, 17, 0, tzinfo=UTC()), ImportTestCase.date.to_json(ImportTestCase.date.from_json(course_due)),
child._inheritable_metadata['due'] child.xblock_kvs.inherited_settings['due']
) )
def test_is_pointer_tag(self): def test_is_pointer_tag(self):
...@@ -288,14 +284,14 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -288,14 +284,14 @@ class ImportTestCase(BaseCourseTestCase):
print("Starting import") print("Starting import")
course = self.get_course('toy') course = self.get_course('toy')
def check_for_key(key, node): def check_for_key(key, node, value):
"recursive check for presence of key" "recursive check for presence of key"
print("Checking {0}".format(node.location.url())) print("Checking {0}".format(node.location.url()))
self.assertTrue(node._field_data.has(node, key)) self.assertEqual(getattr(node, key), value)
for c in node.get_children(): for c in node.get_children():
check_for_key(key, c) check_for_key(key, c, value)
check_for_key('graceperiod', course) check_for_key('graceperiod', course, course.graceperiod)
def test_policy_loading(self): def test_policy_loading(self):
"""Make sure that when two courses share content with the same """Make sure that when two courses share content with the same
......
...@@ -18,7 +18,6 @@ from mock import Mock ...@@ -18,7 +18,6 @@ from mock import Mock
from . import LogicTest from . import LogicTest
from lxml import etree from lxml import etree
from .import get_test_system
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.video_module import VideoDescriptor, _create_youtube_string from xmodule.video_module import VideoDescriptor, _create_youtube_string
from .test_import import DummySystem from .test_import import DummySystem
...@@ -26,6 +25,7 @@ from xblock.field_data import DictFieldData ...@@ -26,6 +25,7 @@ from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from textwrap import dedent from textwrap import dedent
from xmodule.tests import get_test_descriptor_system
class VideoModuleTest(LogicTest): class VideoModuleTest(LogicTest):
...@@ -124,9 +124,9 @@ class VideoDescriptorTest(unittest.TestCase): ...@@ -124,9 +124,9 @@ class VideoDescriptorTest(unittest.TestCase):
"""Test for VideoDescriptor""" """Test for VideoDescriptor"""
def setUp(self): def setUp(self):
system = get_test_system() system = get_test_descriptor_system()
self.descriptor = VideoDescriptor( self.descriptor = system.construct_xblock_from_class(
runtime=system, VideoDescriptor,
field_data=DictFieldData({}), field_data=DictFieldData({}),
scope_ids=ScopeIds(None, None, None, None), scope_ids=ScopeIds(None, None, None, None),
) )
...@@ -304,7 +304,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): ...@@ -304,7 +304,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
a few weeks). a few weeks).
""" """
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
xml_data =''' xml_data = '''
<video display_name="&quot;display_name&quot;" <video display_name="&quot;display_name&quot;"
html5_sources="[&quot;source_1&quot;, &quot;source_2&quot;]" html5_sources="[&quot;source_1&quot;, &quot;source_2&quot;]"
show_captions="false" show_captions="false"
...@@ -418,6 +418,11 @@ class VideoExportTestCase(unittest.TestCase): ...@@ -418,6 +418,11 @@ class VideoExportTestCase(unittest.TestCase):
Make sure that VideoDescriptor can export itself to XML Make sure that VideoDescriptor can export itself to XML
correctly. correctly.
""" """
def assertXmlEqual(self, expected, xml):
for attr in ['tag', 'attrib', 'text', 'tail']:
self.assertEqual(getattr(expected, attr), getattr(xml, attr))
for left, right in zip(expected, xml):
self.assertXmlEqual(left, right)
def test_export_to_xml(self): def test_export_to_xml(self):
"""Test that we write the correct XML on export.""" """Test that we write the correct XML on export."""
...@@ -436,7 +441,7 @@ class VideoExportTestCase(unittest.TestCase): ...@@ -436,7 +441,7 @@ class VideoExportTestCase(unittest.TestCase):
desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'] desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter
expected = dedent('''\ expected = etree.fromstring('''\
<video url_name="SampleProblem1" start_time="0:00:01" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" end_time="0:01:00"> <video url_name="SampleProblem1" start_time="0:00:01" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" end_time="0:01:00">
<source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/> <source src="http://www.example.com/source.ogg"/>
...@@ -444,7 +449,7 @@ class VideoExportTestCase(unittest.TestCase): ...@@ -444,7 +449,7 @@ class VideoExportTestCase(unittest.TestCase):
</video> </video>
''') ''')
self.assertEquals(expected, etree.tostring(xml, pretty_print=True)) self.assertXmlEqual(expected, xml)
def test_export_to_xml_empty_parameters(self): def test_export_to_xml_empty_parameters(self):
"""Test XML export with defaults.""" """Test XML export with defaults."""
......
...@@ -29,6 +29,7 @@ from xmodule.conditional_module import ConditionalDescriptor ...@@ -29,6 +29,7 @@ from xmodule.conditional_module import ConditionalDescriptor
from xmodule.randomize_module import RandomizeDescriptor from xmodule.randomize_module import RandomizeDescriptor
from xmodule.vertical_module import VerticalDescriptor from xmodule.vertical_module import VerticalDescriptor
from xmodule.wrapper_module import WrapperDescriptor from xmodule.wrapper_module import WrapperDescriptor
from xmodule.tests import get_test_descriptor_system
LEAF_XMODULES = ( LEAF_XMODULES = (
AnnotatableDescriptor, AnnotatableDescriptor,
...@@ -80,20 +81,12 @@ class TestXBlockWrapper(object): ...@@ -80,20 +81,12 @@ class TestXBlockWrapper(object):
) )
return runtime return runtime
@property
def leaf_descriptor_runtime(self):
runtime = MakoDescriptorSystem(
load_item=Mock(),
resources_fs=Mock(),
error_tracker=Mock(),
render_template=(lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)),
)
return runtime
def leaf_descriptor(self, descriptor_cls): def leaf_descriptor(self, descriptor_cls):
location = 'i4x://org/course/category/name' location = 'i4x://org/course/category/name'
return descriptor_cls( runtime = get_test_descriptor_system()
self.leaf_descriptor_runtime, runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
return runtime.construct_xblock_from_class(
descriptor_cls,
DictFieldData({}), DictFieldData({}),
ScopeIds(None, descriptor_cls.__name__, location, location) ScopeIds(None, descriptor_cls.__name__, location, location)
) )
...@@ -110,16 +103,12 @@ class TestXBlockWrapper(object): ...@@ -110,16 +103,12 @@ class TestXBlockWrapper(object):
runtime.position = 2 runtime.position = 2
return runtime return runtime
@property
def container_descriptor_runtime(self):
runtime = Mock()
runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
return runtime
def container_descriptor(self, descriptor_cls): def container_descriptor(self, descriptor_cls):
location = 'i4x://org/course/category/name' location = 'i4x://org/course/category/name'
return descriptor_cls( runtime = get_test_descriptor_system()
self.container_descriptor_runtime, runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
return runtime.construct_xblock_from_class(
descriptor_cls,
DictFieldData({ DictFieldData({
'children': range(3) 'children': range(3)
}), }),
......
...@@ -7,9 +7,11 @@ from xblock.field_data import DictFieldData ...@@ -7,9 +7,11 @@ from xblock.field_data import DictFieldData
from xmodule.fields import Date, Timedelta from xmodule.fields import Date, Timedelta
from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
import unittest import unittest
from .import get_test_system
from nose.tools import assert_equals # pylint: disable=E0611 from nose.tools import assert_equals # pylint: disable=E0611
from mock import Mock from mock import Mock
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, InheritanceMixin
from xblock.runtime import DbModel
from xmodule.tests import get_test_descriptor_system
class CrazyJsonString(String): class CrazyJsonString(String):
...@@ -34,6 +36,11 @@ class TestFields(object): ...@@ -34,6 +36,11 @@ class TestFields(object):
values=[{'display_name': 'first', 'value': 'value a'}, values=[{'display_name': 'first', 'value': 'value a'},
{'display_name': 'second', 'value': 'value b'}] {'display_name': 'second', 'value': 'value b'}]
) )
showanswer = String(
help="When to show the problem answer to the student",
scope=Scope.settings,
default="finished"
)
# Used for testing select type # Used for testing select type
float_select = Float(scope=Scope.settings, default=.999, values=[1.23, 0.98]) float_select = Float(scope=Scope.settings, default=.999, values=[1.23, 0.98])
# Used for testing float type # Used for testing float type
...@@ -48,10 +55,10 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -48,10 +55,10 @@ class EditableMetadataFieldsTest(unittest.TestCase):
editable_fields = self.get_xml_editable_fields(DictFieldData({})) editable_fields = self.get_xml_editable_fields(DictFieldData({}))
# Tests that the xblock fields (currently tags and name) get filtered out. # Tests that the xblock fields (currently tags and name) get filtered out.
# Also tests that xml_attributes is filtered out of XmlDescriptor. # Also tests that xml_attributes is filtered out of XmlDescriptor.
self.assertEqual(1, len(editable_fields), "Expected only 1 editable field for xml descriptor.") self.assertEqual(1, len(editable_fields), editable_fields)
self.assert_field_values( self.assert_field_values(
editable_fields, 'display_name', XModuleFields.display_name, editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=False, inheritable=False, value=None, default_value=None explicitly_set=False, value=None, default_value=None
) )
def test_override_default(self): def test_override_default(self):
...@@ -59,50 +66,51 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -59,50 +66,51 @@ class EditableMetadataFieldsTest(unittest.TestCase):
editable_fields = self.get_xml_editable_fields(DictFieldData({'display_name': 'foo'})) editable_fields = self.get_xml_editable_fields(DictFieldData({'display_name': 'foo'}))
self.assert_field_values( self.assert_field_values(
editable_fields, 'display_name', XModuleFields.display_name, editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=True, inheritable=False, value='foo', default_value=None explicitly_set=True, value='foo', default_value=None
) )
def test_integer_field(self): def test_integer_field(self):
descriptor = self.get_descriptor(DictFieldData({'max_attempts': '7'})) descriptor = self.get_descriptor(DictFieldData({'max_attempts': '7'}))
editable_fields = descriptor.editable_metadata_fields editable_fields = descriptor.editable_metadata_fields
self.assertEqual(7, len(editable_fields)) self.assertEqual(8, len(editable_fields))
self.assert_field_values( self.assert_field_values(
editable_fields, 'max_attempts', TestFields.max_attempts, editable_fields, 'max_attempts', TestFields.max_attempts,
explicitly_set=True, inheritable=False, value=7, default_value=1000, type='Integer', explicitly_set=True, value=7, default_value=1000, type='Integer',
options=TestFields.max_attempts.values options=TestFields.max_attempts.values
) )
self.assert_field_values( self.assert_field_values(
editable_fields, 'display_name', TestFields.display_name, editable_fields, 'display_name', TestFields.display_name,
explicitly_set=False, inheritable=False, value='local default', default_value='local default' explicitly_set=False, value='local default', default_value='local default'
) )
editable_fields = self.get_descriptor(DictFieldData({})).editable_metadata_fields editable_fields = self.get_descriptor(DictFieldData({})).editable_metadata_fields
self.assert_field_values( self.assert_field_values(
editable_fields, 'max_attempts', TestFields.max_attempts, editable_fields, 'max_attempts', TestFields.max_attempts,
explicitly_set=False, inheritable=False, value=1000, default_value=1000, type='Integer', explicitly_set=False, value=1000, default_value=1000, type='Integer',
options=TestFields.max_attempts.values options=TestFields.max_attempts.values
) )
def test_inherited_field(self): def test_inherited_field(self):
model_val = {'display_name': 'inherited'} kvs = InheritanceKeyValueStore(initial_values={}, inherited_settings={'showanswer': 'inherited'})
descriptor = self.get_descriptor(DictFieldData(model_val)) model_data = DbModel(kvs)
# Mimic an inherited value for display_name (inherited and inheritable are the same in this case). descriptor = self.get_descriptor(model_data)
descriptor._inherited_metadata = model_val
descriptor._inheritable_metadata = model_val
editable_fields = descriptor.editable_metadata_fields editable_fields = descriptor.editable_metadata_fields
self.assert_field_values( self.assert_field_values(
editable_fields, 'display_name', TestFields.display_name, editable_fields, 'showanswer', InheritanceMixin.showanswer,
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited' explicitly_set=False, value='inherited', default_value='inherited'
) )
descriptor = self.get_descriptor(DictFieldData({'display_name': 'explicit'}))
# Mimic the case where display_name WOULD have been inherited, except we explicitly set it. # Mimic the case where display_name WOULD have been inherited, except we explicitly set it.
descriptor._inheritable_metadata = {'display_name': 'inheritable value'} kvs = InheritanceKeyValueStore(
descriptor._inherited_metadata = {} initial_values={'showanswer': 'explicit'},
inherited_settings={'showanswer': 'inheritable value'}
)
model_data = DbModel(kvs)
descriptor = self.get_descriptor(model_data)
editable_fields = descriptor.editable_metadata_fields editable_fields = descriptor.editable_metadata_fields
self.assert_field_values( self.assert_field_values(
editable_fields, 'display_name', TestFields.display_name, editable_fields, 'showanswer', InheritanceMixin.showanswer,
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value' explicitly_set=True, value='explicit', default_value='inheritable value'
) )
def test_type_and_options(self): def test_type_and_options(self):
...@@ -115,41 +123,44 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -115,41 +123,44 @@ class EditableMetadataFieldsTest(unittest.TestCase):
# Tests for select # Tests for select
self.assert_field_values( self.assert_field_values(
editable_fields, 'string_select', TestFields.string_select, editable_fields, 'string_select', TestFields.string_select,
explicitly_set=False, inheritable=False, value='default value', default_value='default value', explicitly_set=False, value='default value', default_value='default value',
type='Select', options=[{'display_name': 'first', 'value': 'value a JSON'}, type='Select', options=[{'display_name': 'first', 'value': 'value a JSON'},
{'display_name': 'second', 'value': 'value b JSON'}] {'display_name': 'second', 'value': 'value b JSON'}]
) )
self.assert_field_values( self.assert_field_values(
editable_fields, 'float_select', TestFields.float_select, editable_fields, 'float_select', TestFields.float_select,
explicitly_set=False, inheritable=False, value=.999, default_value=.999, explicitly_set=False, value=.999, default_value=.999,
type='Select', options=[1.23, 0.98] type='Select', options=[1.23, 0.98]
) )
self.assert_field_values( self.assert_field_values(
editable_fields, 'boolean_select', TestFields.boolean_select, editable_fields, 'boolean_select', TestFields.boolean_select,
explicitly_set=False, inheritable=False, value=None, default_value=None, explicitly_set=False, value=None, default_value=None,
type='Select', options=[{'display_name': "True", "value": True}, {'display_name': "False", "value": False}] type='Select', options=[{'display_name': "True", "value": True}, {'display_name': "False", "value": False}]
) )
# Test for float # Test for float
self.assert_field_values( self.assert_field_values(
editable_fields, 'float_non_select', TestFields.float_non_select, editable_fields, 'float_non_select', TestFields.float_non_select,
explicitly_set=False, inheritable=False, value=.999, default_value=.999, explicitly_set=False, value=.999, default_value=.999,
type='Float', options={'min': 0, 'step': .3} type='Float', options={'min': 0, 'step': .3}
) )
self.assert_field_values( self.assert_field_values(
editable_fields, 'list_field', TestFields.list_field, editable_fields, 'list_field', TestFields.list_field,
explicitly_set=False, inheritable=False, value=[], default_value=[], explicitly_set=False, value=[], default_value=[],
type='List' type='List'
) )
# Start of helper methods # Start of helper methods
def get_xml_editable_fields(self, field_data): def get_xml_editable_fields(self, field_data):
system = get_test_system() runtime = get_test_descriptor_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") return runtime.construct_xblock_from_class(
return XmlDescriptor(runtime=system, field_data=field_data, scope_ids=Mock()).editable_metadata_fields XmlDescriptor,
field_data=field_data,
scope_ids=Mock()
).editable_metadata_fields
def get_descriptor(self, field_data): def get_descriptor(self, field_data):
class TestModuleDescriptor(TestFields, XmlDescriptor): class TestModuleDescriptor(TestFields, XmlDescriptor):
...@@ -159,11 +170,11 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -159,11 +170,11 @@ class EditableMetadataFieldsTest(unittest.TestCase):
non_editable_fields.append(TestModuleDescriptor.due) non_editable_fields.append(TestModuleDescriptor.due)
return non_editable_fields return non_editable_fields
system = get_test_system() system = get_test_descriptor_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
return TestModuleDescriptor(runtime=system, field_data=field_data, scope_ids=Mock()) return system.construct_xblock_from_class(TestModuleDescriptor, field_data=field_data, scope_ids=Mock())
def assert_field_values(self, editable_fields, name, field, explicitly_set, inheritable, value, default_value, def assert_field_values(self, editable_fields, name, field, explicitly_set, value, default_value,
type='Generic', options=[]): type='Generic', options=[]):
test_field = editable_fields[name] test_field = editable_fields[name]
...@@ -178,7 +189,6 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -178,7 +189,6 @@ class EditableMetadataFieldsTest(unittest.TestCase):
self.assertEqual(type, test_field['type']) self.assertEqual(type, test_field['type'])
self.assertEqual(explicitly_set, test_field['explicitly_set']) self.assertEqual(explicitly_set, test_field['explicitly_set'])
self.assertEqual(inheritable, test_field['inheritable'])
class TestSerialize(unittest.TestCase): class TestSerialize(unittest.TestCase):
......
...@@ -15,6 +15,8 @@ import logging ...@@ -15,6 +15,8 @@ import logging
from lxml import etree from lxml import etree
from pkg_resources import resource_string from pkg_resources import resource_string
import datetime
import time
from django.http import Http404 from django.http import Http404
from django.conf import settings from django.conf import settings
...@@ -28,8 +30,8 @@ from xblock.fields import Scope, String, Boolean, Float, List, Integer, ScopeIds ...@@ -28,8 +30,8 @@ from xblock.fields import Scope, String, Boolean, Float, List, Integer, ScopeIds
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
import datetime from xmodule.modulestore.inheritance import InheritanceKeyValueStore
import time from xblock.runtime import DbModel
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -240,9 +242,11 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ...@@ -240,9 +242,11 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
xml_data = etree.tostring(cls.load_file(filepath, system.resources_fs, location)) xml_data = etree.tostring(cls.load_file(filepath, system.resources_fs, location))
field_data = VideoDescriptor._parse_video_xml(xml_data) field_data = VideoDescriptor._parse_video_xml(xml_data)
field_data['location'] = location field_data['location'] = location
kvs = InheritanceKeyValueStore(initial_values=field_data)
field_data = DbModel(kvs)
video = system.construct_xblock_from_class( video = system.construct_xblock_from_class(
cls, cls,
DictFieldData(field_data), field_data,
# We're loading a descriptor, so student_id is meaningless # We're loading a descriptor, so student_id is meaningless
# We also don't have separate notions of definition and usage ids yet, # We also don't have separate notions of definition and usage ids yet,
...@@ -259,25 +263,22 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ...@@ -259,25 +263,22 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
youtube_string = _create_youtube_string(self) youtube_string = _create_youtube_string(self)
# Mild workaround to ensure that tests pass -- if a field # Mild workaround to ensure that tests pass -- if a field
# is set to its default value, we don't need to write it out. # is set to its default value, we don't need to write it out.
if youtube_string == '1.00:OEoXaMPEzfM': if youtube_string and youtube_string != '1.00:OEoXaMPEzfM':
youtube_string = '' xml.set('youtube', unicode(youtube_string))
xml.set('url_name', self.url_name)
attrs = { attrs = {
'display_name': self.display_name, 'display_name': self.display_name,
'show_captions': json.dumps(self.show_captions), 'show_captions': json.dumps(self.show_captions),
'youtube': youtube_string,
'start_time': datetime.timedelta(seconds=self.start_time), 'start_time': datetime.timedelta(seconds=self.start_time),
'end_time': datetime.timedelta(seconds=self.end_time), 'end_time': datetime.timedelta(seconds=self.end_time),
'sub': self.sub, 'sub': self.sub,
'url_name': self.url_name
} }
fields = {field.name: field for field in self.fields.values()}
for key, value in attrs.items(): for key, value in attrs.items():
# Mild workaround to ensure that tests pass -- if a field # Mild workaround to ensure that tests pass -- if a field
# is set to its default value, we don't need to write it out. # is set to its default value, we don't write it out.
if key in fields and fields[key].default == getattr(self, key):
continue
if value: if value:
xml.set(key, unicode(value)) if key in self.fields and self.fields[key].is_set_on(self):
xml.set(key, unicode(value))
for source in self.html5_sources: for source in self.html5_sources:
ele = etree.Element('source') ele = etree.Element('source')
......
...@@ -648,26 +648,17 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -648,26 +648,17 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# We are not allowing editing of xblock tag and name fields at this time (for any component). # We are not allowing editing of xblock tag and name fields at this time (for any component).
return [XBlock.tags, XBlock.name] return [XBlock.tags, XBlock.name]
def get_explicitly_set_fields_by_scope(self, scope=Scope.content): def get_explicitly_set_fields_by_scope(self, scope=Scope.content):
""" """
Get a dictionary of the fields for the given scope which are set explicitly on this xblock. (Including Get a dictionary of the fields for the given scope which are set explicitly on this xblock. (Including
any set to None.) any set to None.)
""" """
if scope == Scope.settings and hasattr(self, '_inherited_metadata'): result = {}
inherited_metadata = getattr(self, '_inherited_metadata') for field in self.fields.values():
result = {} if (field.scope == scope and self._field_data.has(self, field.name)):
for field in self.fields.values(): result[field.name] = self._field_data.get(self, field.name)
if (field.scope == scope and return result
self._field_data.has(self, field.name) and
field.name not in inherited_metadata):
result[field.name] = self._field_data.get(self, field.name)
return result
else:
result = {}
for field in self.fields.values():
if (field.scope == scope and self._field_data.has(self, field.name)):
result[field.name] = self._field_data.get(self, field.name)
return result
@property @property
def editable_metadata_fields(self): def editable_metadata_fields(self):
...@@ -676,8 +667,14 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -676,8 +667,14 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
Can be limited by extending `non_editable_metadata_fields`. Can be limited by extending `non_editable_metadata_fields`.
""" """
inherited_metadata = getattr(self, '_inherited_metadata', {}) def jsonify_value(field, json_choice):
inheritable_metadata = getattr(self, '_inheritable_metadata', {}) if isinstance(json_choice, dict) and 'value' in json_choice:
json_choice = dict(json_choice) # make a copy so below doesn't change the original
json_choice['value'] = field.to_json(json_choice['value'])
else:
json_choice = field.to_json(json_choice)
return json_choice
metadata_fields = {} metadata_fields = {}
# Only use the fields from this class, not mixins # Only use the fields from this class, not mixins
...@@ -688,56 +685,35 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -688,56 +685,35 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
if field.scope != Scope.settings or field in self.non_editable_metadata_fields: if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
continue continue
inheritable = False # gets the 'default_value' and 'explicitly_set' attrs
value = getattr(self, field.name) metadata_fields[field.name] = self.runtime.get_field_provenance(self, field)
default_value = field.default metadata_fields[field.name]['field_name'] = field.name
explicitly_set = self._field_data.has(self, field.name) metadata_fields[field.name]['display_name'] = field.display_name
if field.name in inheritable_metadata: metadata_fields[field.name]['help'] = field.help
inheritable = True metadata_fields[field.name]['value'] = field.read_json(self)
default_value = field.from_json(inheritable_metadata.get(field.name))
if field.name in inherited_metadata:
explicitly_set = False
# We support the following editors: # We support the following editors:
# 1. A select editor for fields with a list of possible values (includes Booleans). # 1. A select editor for fields with a list of possible values (includes Booleans).
# 2. Number editors for integers and floats. # 2. Number editors for integers and floats.
# 3. A generic string editor for anything else (editing JSON representation of the value). # 3. A generic string editor for anything else (editing JSON representation of the value).
editor_type = "Generic" editor_type = "Generic"
values = copy.deepcopy(field.values) values = field.values
if isinstance(values, tuple): if isinstance(values, (tuple, list)) and len(values) > 0:
values = list(values) editor_type = "Select"
if isinstance(values, list): values = [jsonify_value(field, json_choice) for json_choice in values]
if len(values) > 0:
editor_type = "Select"
for index, choice in enumerate(values):
json_choice = copy.deepcopy(choice)
if isinstance(json_choice, dict) and 'value' in json_choice:
json_choice['value'] = field.to_json(json_choice['value'])
else:
json_choice = field.to_json(json_choice)
values[index] = json_choice
elif isinstance(field, Integer): elif isinstance(field, Integer):
editor_type = "Integer" editor_type = "Integer"
elif isinstance(field, Float): elif isinstance(field, Float):
editor_type = "Float" editor_type = "Float"
elif isinstance(field, List): elif isinstance(field, List):
editor_type = "List" editor_type = "List"
metadata_fields[field.name] = { metadata_fields[field.name]['type'] = editor_type
'field_name': field.name, metadata_fields[field.name]['options'] = [] if values is None else values
'type': editor_type,
'display_name': field.display_name,
'value': field.to_json(value),
'options': [] if values is None else values,
'default_value': field.to_json(default_value),
'inheritable': inheritable,
'explicitly_set': explicitly_set,
'help': field.help,
}
return metadata_fields return metadata_fields
# ~~~~~~~~~~~~~~~ XBlock API Wrappers ~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~ XBlock API Wrappers ~~~~~~~~~~~~~~~~
def studio_view(self, context): def studio_view(self, _context):
""" """
Return a fragment with the html from this XModuleDescriptor's editing view Return a fragment with the html from this XModuleDescriptor's editing view
...@@ -750,6 +726,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -750,6 +726,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
class DescriptorSystem(Runtime): class DescriptorSystem(Runtime):
def __init__(self, load_item, resources_fs, error_tracker, **kwargs): def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
""" """
load_item: Takes a Location and returns an XModuleDescriptor load_item: Takes a Location and returns an XModuleDescriptor
...@@ -797,6 +774,33 @@ class DescriptorSystem(Runtime): ...@@ -797,6 +774,33 @@ class DescriptorSystem(Runtime):
"""See documentation for `xblock.runtime:Runtime.get_block`""" """See documentation for `xblock.runtime:Runtime.get_block`"""
return self.load_item(block_id) return self.load_item(block_id)
def get_field_provenance(self, xblock, field):
"""
For the given xblock, return a dict for the field's current state:
{
'default_value': what json'd value will take effect if field is unset: either the field default or
inherited value,
'explicitly_set': boolean for whether the current value is set v default/inherited,
}
:param xblock:
:param field:
"""
# in runtime b/c runtime contains app-specific xblock behavior. Studio's the only app
# which needs this level of introspection right now. runtime also is 'allowed' to know
# about the kvs, dbmodel, etc.
result = {}
result['explicitly_set'] = xblock._field_data.has(xblock, field.name)
try:
block_inherited = xblock.xblock_kvs.inherited_settings
except AttributeError: # if inherited_settings doesn't exist on kvs
block_inherited = {}
if field.name in block_inherited:
result['default_value'] = block_inherited[field.name]
else:
result['default_value'] = field.to_json(field.default)
return result
class XMLParsingSystem(DescriptorSystem): class XMLParsingSystem(DescriptorSystem):
def __init__(self, process_xml, policy, **kwargs): def __init__(self, process_xml, policy, **kwargs):
......
...@@ -9,10 +9,9 @@ from lxml import etree ...@@ -9,10 +9,9 @@ from lxml import etree
from xblock.fields import Dict, Scope, ScopeIds from xblock.fields import Dict, Scope, ScopeIds
from xmodule.x_module import (XModuleDescriptor, policy_key) from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata, InheritanceKeyValueStore
from xmodule.modulestore.xml_exporter import EdxJSONEncoder from xmodule.modulestore.xml_exporter import EdxJSONEncoder
from xblock.runtime import DbModel
from xblock.field_data import DictFieldData
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -365,10 +364,12 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -365,10 +364,12 @@ class XmlDescriptor(XModuleDescriptor):
field_data['xml_attributes'][key] = value field_data['xml_attributes'][key] = value
field_data['location'] = location field_data['location'] = location
field_data['category'] = xml_object.tag field_data['category'] = xml_object.tag
kvs = InheritanceKeyValueStore(initial_values=field_data)
field_data = DbModel(kvs)
return system.construct_xblock_from_class( return system.construct_xblock_from_class(
cls, cls,
DictFieldData(field_data), field_data,
# We're loading a descriptor, so student_id is meaningless # We're loading a descriptor, so student_id is meaningless
# We also don't have separate notions of definition and usage ids yet, # We also don't have separate notions of definition and usage ids yet,
......
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