diff --git a/common/lib/xmodule/xmodule/model.py b/common/lib/xmodule/xmodule/model.py index 93ed12f69d..380c81dbe9 100644 --- a/common/lib/xmodule/xmodule/model.py +++ b/common/lib/xmodule/xmodule/model.py @@ -133,8 +133,6 @@ class NamespaceDescriptor(object): self._namespace = namespace def __get__(self, instance, owner): - if owner is None: - return self return self._namespace(instance) diff --git a/common/lib/xmodule/xmodule/runtime.py b/common/lib/xmodule/xmodule/runtime.py index 7030f04f9f..6f20997894 100644 --- a/common/lib/xmodule/xmodule/runtime.py +++ b/common/lib/xmodule/xmodule/runtime.py @@ -11,13 +11,13 @@ class KeyValueStore(object): # data. Key = namedtuple("Key", "scope, student_id, module_scope_id, field_name") - def get(key): + def get(self, key): pass - def set(key, value): + def set(self, key, value): pass - def delete(key): + def delete(self, key): pass @@ -50,7 +50,7 @@ class DbModel(MutableMapping): for namespace_name in self._module_cls.namespaces: namespace = getattr(self._module_cls, namespace_name) namespace_field = getattr(type(namespace), name, None) - if namespace_field is not None and isinstance(module_field, ModelType): + if namespace_field is not None and isinstance(namespace_field, ModelType): return namespace_field # Not in the class or in any of the namespaces, so name @@ -59,7 +59,6 @@ class DbModel(MutableMapping): def _key(self, name): field = self._getfield(name) - print name, field module = field.scope.module if module == ModuleScope.ALL: @@ -69,7 +68,7 @@ class DbModel(MutableMapping): elif module == ModuleScope.DEFINITION: module_id = self._usage.def_id elif module == ModuleScope.TYPE: - module_id = self.module_type.__name__ + module_id = self._module_cls.__name__ if field.scope.student: student_id = self._student_id diff --git a/common/lib/xmodule/xmodule/tests/test_model.py b/common/lib/xmodule/xmodule/tests/test_model.py index 0e42df80a1..b0ae9e5844 100644 --- a/common/lib/xmodule/xmodule/tests/test_model.py +++ b/common/lib/xmodule/xmodule/tests/test_model.py @@ -1,8 +1,154 @@ +from mock import patch +from unittest import TestCase +from nose.tools import assert_in, assert_equals, assert_raises -class ModelMetaclassTester(object): - __metaclass__ = ModelMetaclass +from xmodule.model import * - field_a = Int(scope=Scope.settings) - field_b = Int(scope=Scope.content) def test_model_metaclass(): + class ModelMetaclassTester(object): + __metaclass__ = ModelMetaclass + + field_a = Int(scope=Scope.settings) + field_b = Int(scope=Scope.content) + + def __init__(self, model_data): + self._model_data = model_data + + assert hasattr(ModelMetaclassTester, 'field_a') + assert hasattr(ModelMetaclassTester, 'field_b') + + assert_in(ModelMetaclassTester.field_a, ModelMetaclassTester.fields) + assert_in(ModelMetaclassTester.field_b, ModelMetaclassTester.fields) + + +def test_parent_metaclass(): + + class HasChildren(object): + __metaclass__ = ParentModelMetaclass + + has_children = True + + class WithoutChildren(object): + __metaclass = ParentModelMetaclass + + assert hasattr(HasChildren, 'children') + assert not hasattr(WithoutChildren, 'children') + + assert isinstance(HasChildren.children, List) + assert_equals(Scope.settings, HasChildren.children.scope) + + +def test_field_access(): + class FieldTester(object): + __metaclass__ = ModelMetaclass + + field_a = Int(scope=Scope.settings) + field_b = Int(scope=Scope.content, default=10) + field_c = Int(scope=Scope.student_state, computed_default=lambda s: s.field_a + s.field_b) + + def __init__(self, model_data): + self._model_data = model_data + + field_tester = FieldTester({'field_a': 5, 'field_x': 15}) + + assert_equals(5, field_tester.field_a) + assert_equals(10, field_tester.field_b) + assert_equals(15, field_tester.field_c) + assert not hasattr(field_tester, 'field_x') + + field_tester.field_a = 20 + assert_equals(20, field_tester._model_data['field_a']) + assert_equals(10, field_tester.field_b) + assert_equals(30, field_tester.field_c) + + del field_tester.field_a + assert_equals(None, field_tester.field_a) + assert hasattr(FieldTester, 'field_a') + + +class TestNamespace(Namespace): + field_x = List(scope=Scope.content) + field_y = String(scope=Scope.student_state, default="default_value") + + +@patch('xmodule.model.Namespace.load_classes', return_value=[('test', TestNamespace)]) +def test_namespace_metaclass(mock_load_classes): + class TestClass(object): + __metaclass__ = NamespacesMetaclass + + assert hasattr(TestClass, 'test') + assert hasattr(TestClass.test, 'field_x') + assert hasattr(TestClass.test, 'field_y') + + assert_in(TestNamespace.field_x, TestClass.test.fields) + assert_in(TestNamespace.field_y, TestClass.test.fields) + assert isinstance(TestClass.test, Namespace) + + +@patch('xmodule.model.Namespace.load_classes', return_value=[('test', TestNamespace)]) +def test_namespace_field_access(mock_load_classes): + class Metaclass(ModelMetaclass, NamespacesMetaclass): + pass + + class FieldTester(object): + __metaclass__ = Metaclass + + field_a = Int(scope=Scope.settings) + field_b = Int(scope=Scope.content, default=10) + field_c = Int(scope=Scope.student_state, computed_default=lambda s: s.field_a + s.field_b) + + def __init__(self, model_data): + self._model_data = model_data + + field_tester = FieldTester({ + 'field_a': 5, + 'field_x': [1, 2, 3], + }) + + assert_equals(5, field_tester.field_a) + assert_equals(10, field_tester.field_b) + assert_equals(15, field_tester.field_c) + assert_equals([1, 2, 3], field_tester.test.field_x) + assert_equals('default_value', field_tester.test.field_y) + + field_tester.test.field_x = ['a', 'b'] + assert_equals(['a', 'b'], field_tester._model_data['field_x']) + + del field_tester.test.field_x + assert_equals(None, field_tester.test.field_x) + + assert_raises(AttributeError, getattr, field_tester.test, 'field_z') + assert_raises(AttributeError, delattr, field_tester.test, 'field_z') + + # Namespaces are created on the fly, so setting a new attribute on one + # has no long-term effect + field_tester.test.field_z = 'foo' + assert_raises(AttributeError, getattr, field_tester.test, 'field_z') + assert 'field_z' not in field_tester._model_data + + +def test_field_serialization(): + + class CustomField(ModelType): + def from_json(self, value): + return value['value'] + + def to_json(self, value): + return {'value': value} + + class FieldTester(object): + __metaclass__ = ModelMetaclass + + field = CustomField() + + def __init__(self, model_data): + self._model_data = model_data + + field_tester = FieldTester({ + 'field': {'value': 4} + }) + + assert_equals(4, field_tester.field) + field_tester.field = 5 + assert_equals({'value': 5}, field_tester._model_data['field']) diff --git a/common/lib/xmodule/xmodule/tests/test_runtime.py b/common/lib/xmodule/xmodule/tests/test_runtime.py index e69de29bb2..1bbe3235b8 100644 --- a/common/lib/xmodule/xmodule/tests/test_runtime.py +++ b/common/lib/xmodule/xmodule/tests/test_runtime.py @@ -0,0 +1,67 @@ +from nose.tools import assert_equals +from mock import patch + +from xmodule.model import * +from xmodule.runtime import * + +class Metaclass(NamespacesMetaclass, ParentModelMetaclass, ModelMetaclass): + pass + + +class TestNamespace(Namespace): + n_content = String(scope=Scope.content, default='nc') + n_settings = String(scope=Scope.settings, default='ns') + n_student_state = String(scope=Scope.student_state, default='nss') + n_student_preferences = String(scope=Scope.student_preferences, default='nsp') + n_student_info = String(scope=Scope.student_info, default='nsi') + n_by_type = String(scope=Scope(False, ModuleScope.TYPE), default='nbt') + n_for_all = String(scope=Scope(False, ModuleScope.ALL), default='nfa') + n_student_def = String(scope=Scope(True, ModuleScope.DEFINITION), default='nsd') + + +with patch('xmodule.model.Namespace.load_classes', return_value=[('test', TestNamespace)]): + class TestModel(object): + __metaclass__ = Metaclass + + content = String(scope=Scope.content, default='c') + settings = String(scope=Scope.settings, default='s') + student_state = String(scope=Scope.student_state, default='ss') + student_preferences = String(scope=Scope.student_preferences, default='sp') + student_info = String(scope=Scope.student_info, default='si') + by_type = String(scope=Scope(False, ModuleScope.TYPE), default='bt') + for_all = String(scope=Scope(False, ModuleScope.ALL), default='fa') + student_def = String(scope=Scope(True, ModuleScope.DEFINITION), default='sd') + + def __init__(self, model_data): + self._model_data = model_data + + +class DictKeyValueStore(KeyValueStore): + def __init__(self): + self.db = {} + + def get(self, key): + return self.db[key] + + def set(self, key, value): + self.db[key] = value + + def delete(self, key): + del self.db[key] + + +Usage = namedtuple('Usage', 'id, def_id') + + +def test_empty(): + tester = TestModel(DbModel(DictKeyValueStore(), TestModel, 's0', Usage('u0', 'd0'))) + + for collection in (tester, tester.test): + for field in collection.fields: + print "Getting %s from %r" % (field.name, collection) + assert_equals(field.default, getattr(collection, field.name)) + new_value = 'new ' + field.name + print "Setting %s to %s on %r" % (field.name, new_value, collection) + setattr(collection, field.name, new_value) + print "Checking %s on %r" % (field.name, collection) + assert_equals(new_value, getattr(collection, field.name))