From d77491e46a11b913a31616b4f9adc035c53cc3c6 Mon Sep 17 00:00:00 2001
From: Calen Pennington <cale@edx.org>
Date: Fri, 6 Sep 2013 14:21:30 -0400
Subject: [PATCH] Pull XModule attributes out into a mixin that can be applied to xblocks

---
 cms/envs/common.py                                                     |   3 ++-
 common/lib/xmodule/xmodule/modulestore/split_mongo/split.py            |   2 --
 common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py |   3 ++-
 common/lib/xmodule/xmodule/tests/__init__.py                           |   8 ++------
 common/lib/xmodule/xmodule/tests/test_import.py                        |   3 ++-
 common/lib/xmodule/xmodule/tests/test_xml_module.py                    |   6 +++---
 common/lib/xmodule/xmodule/x_module.py                                 | 325 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 lms/envs/common.py                                                     |   3 ++-
 8 files changed, 148 insertions(+), 205 deletions(-)

diff --git a/cms/envs/common.py b/cms/envs/common.py
index 7f2559e..13faf55 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -31,6 +31,7 @@ from path import path
 from lms.xblock.mixin import LmsBlockMixin
 from cms.xmodule_namespace import CmsBlockMixin
 from xmodule.modulestore.inheritance import InheritanceMixin
+from xmodule.x_module import XModuleMixin
 
 ############################ FEATURE CONFIGURATION #############################
 
@@ -168,7 +169,7 @@ MIDDLEWARE_CLASSES = (
 
 # This should be moved into an XBlock Runtime/Application object
 # once the responsibility of XBlock creation is moved out of modulestore - cpennington
-XBLOCK_MIXINS = (LmsBlockMixin, CmsBlockMixin, InheritanceMixin)
+XBLOCK_MIXINS = (LmsBlockMixin, CmsBlockMixin, InheritanceMixin, XModuleMixin)
 
 
 ############################ SIGNAL HANDLERS ################################
diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
index d85092b..60bfa66 100644
--- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
+++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
@@ -43,8 +43,6 @@ log = logging.getLogger(__name__)
 #==============================================================================
 
 
-
-
 class SplitMongoModuleStore(ModuleStoreBase):
     """
     A Mongodb backed ModuleStore supporting versions, inheritance,
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
index 0f1b65d..be5c61a 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
@@ -15,6 +15,7 @@ from xmodule.modulestore.exceptions import InsufficientSpecificationError, ItemN
     DuplicateItemError
 from xmodule.modulestore.locator import CourseLocator, BlockUsageLocator, VersionTree, DescriptionLocator
 from xmodule.modulestore.inheritance import InheritanceMixin
+from xmodule.x_module import XModuleMixin
 from pytz import UTC
 from path import path
 import re
@@ -34,7 +35,7 @@ class SplitModuleTest(unittest.TestCase):
         'db': 'test_xmodule',
         'collection': 'modulestore{0}'.format(uuid.uuid4().hex),
         'fs_root': '',
-        'xblock_mixins': (InheritanceMixin,)
+        'xblock_mixins': (InheritanceMixin, XModuleMixin)
     }
 
     MODULESTORE = {
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index 088b2c3..99caec0 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -11,15 +11,11 @@ import json
 import os
 import unittest
 
-import fs
-import fs.osfs
-import numpy
 from mock import Mock
 from path import path
 
-import calc
 from xblock.field_data import DictFieldData
-from xmodule.x_module import ModuleSystem, XModuleDescriptor, DescriptorSystem
+from xmodule.x_module import ModuleSystem, XModuleDescriptor, XModuleMixin
 from xmodule.modulestore.inheritance import InheritanceMixin
 from xmodule.mako_module import MakoDescriptorSystem
 
@@ -81,7 +77,7 @@ def get_test_descriptor_system():
         resources_fs=Mock(),
         error_tracker=Mock(),
         render_template=lambda template, context: repr(context),
-        mixins=(InheritanceMixin,),
+        mixins=(InheritanceMixin, XModuleMixin),
     )
 
 
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index 19232e3..16c8275 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -13,6 +13,7 @@ from xmodule.xml_module import is_pointer_tag
 from xmodule.modulestore import Location
 from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
 from xmodule.modulestore.inheritance import compute_inherited_metadata
+from xmodule.x_module import XModuleMixin
 from xmodule.fields import Date
 from xmodule.tests import DATA_DIR
 from xmodule.modulestore.inheritance import InheritanceMixin
@@ -42,7 +43,7 @@ class DummySystem(ImportSystem):
             error_tracker=error_tracker,
             parent_tracker=parent_tracker,
             load_error_modules=load_error_modules,
-            mixins=(InheritanceMixin,)
+            mixins=(InheritanceMixin, XModuleMixin)
         )
 
     def render_template(self, _template, _context):
diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py
index 9d11b2b..da6062f 100644
--- a/common/lib/xmodule/xmodule/tests/test_xml_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py
@@ -12,10 +12,10 @@ from xblock.runtime import DbModel
 
 from xmodule.fields import Date, Timedelta
 from xmodule.modulestore.inheritance import InheritanceKeyValueStore, InheritanceMixin
-from xmodule.x_module import XModuleFields
 from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
 from xmodule.course_module import CourseDescriptor
 from xmodule.seq_module import SequenceDescriptor
+from xmodule.x_module import XModuleMixin
 
 from xmodule.tests import get_test_descriptor_system
 from xmodule.tests.xml import XModuleXmlImportTest
@@ -65,7 +65,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
         # Also tests that xml_attributes is filtered out of XmlDescriptor.
         self.assertEqual(1, len(editable_fields), editable_fields)
         self.assert_field_values(
-            editable_fields, 'display_name', XModuleFields.display_name,
+            editable_fields, 'display_name', XModuleMixin.display_name,
             explicitly_set=False, value=None, default_value=None
         )
 
@@ -73,7 +73,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
         # Tests that explicitly_set is correct when a value overrides the default (not inheritable).
         editable_fields = self.get_xml_editable_fields(DictFieldData({'display_name': 'foo'}))
         self.assert_field_values(
-            editable_fields, 'display_name', XModuleFields.display_name,
+            editable_fields, 'display_name', XModuleMixin.display_name,
             explicitly_set=True, value='foo', default_value=None
         )
 
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 63a73c4..63ff80b 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -1,5 +1,4 @@
 import logging
-import copy
 import yaml
 import os
 
@@ -11,7 +10,7 @@ from xmodule.modulestore import Location
 from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecificationError, InvalidLocationError
 
 from xblock.core import XBlock
-from xblock.fields import Scope, String, Integer, Float, List
+from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String
 from xblock.fragment import Fragment
 from xblock.runtime import Runtime
 from xmodule.modulestore.locator import BlockUsageLocator
@@ -83,7 +82,13 @@ class HTMLSnippet(object):
             .format(self.__class__))
 
 
-class XModuleFields(object):
+class XModuleMixin(XBlockMixin):
+    """
+    Fields and methods used by XModules internally.
+
+    Adding this Mixin to an :class:`XBlock` allows it to cooperate with old-style :class:`XModules`
+    """
+
     display_name = String(
         display_name="Display Name",
         help="This name appears in the horizontal navigation at the top of the page.",
@@ -93,41 +98,6 @@ class XModuleFields(object):
         default=None
     )
 
-
-class XModule(XModuleFields, HTMLSnippet, XBlock):
-    ''' Implements a generic learning module.
-
-        Subclasses must at a minimum provide a definition for get_html in order
-        to be displayed to users.
-
-        See the HTML module for a simple example.
-    '''
-
-    # The default implementation of get_icon_class returns the icon_class
-    # attribute of the class
-    #
-    # This attribute can be overridden by subclasses, and
-    # the function can also be overridden if the icon class depends on the data
-    # in the module
-    icon_class = 'other'
-
-
-    def __init__(self, descriptor, *args, **kwargs):
-        '''
-        Construct a new xmodule
-
-        runtime: An XBlock runtime allowing access to external resources
-
-        descriptor: the XModuleDescriptor that this module is an instance of.
-
-        field_data: A dictionary-like object that maps field names to values
-            for those fields.
-        '''
-        super(XModule, self).__init__(*args, **kwargs)
-        self.system = self.runtime
-        self.descriptor = descriptor
-        self._loaded_children = None
-
     @property
     def id(self):
         return self.location.url()
@@ -155,31 +125,133 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
 
     @property
     def url_name(self):
-        if self.descriptor:
-            return self.descriptor.url_name
-        elif isinstance(self.location, Location):
+        if isinstance(self.location, Location):
             return self.location.name
         elif isinstance(self.location, BlockUsageLocator):
             return self.location.usage_id
         else:
             raise InsufficientSpecificationError()
 
-
     @property
     def display_name_with_default(self):
-        '''
+        """
         Return a display name for the module: use display_name if defined in
         metadata, otherwise convert the url name.
-        '''
+        """
         name = self.display_name
         if name is None:
             name = self.url_name.replace('_', ' ')
         return name
 
+    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
+        any set to None.)
+        """
+        result = {}
+        for field in self.fields.values():
+            if (field.scope == scope and field.is_set_on(self)):
+                result[field.name] = field.read_json(self)
+        return result
+
+    @property
+    def xblock_kvs(self):
+        """
+        Use w/ caution. Really intended for use by the persistence layer.
+        """
+        # if caller wants kvs, caller's assuming it's up to date; so, decache it
+        self.save()
+        return self._field_data._kvs  # pylint: disable=protected-access
+
     def get_children(self):
-        '''
+        """Returns a list of XBlock instances for the children of
+        this module"""
+
+        if not self.has_children:
+            return []
+
+        if getattr(self, '_child_instances', None) is None:
+            self._child_instances = []  # pylint: disable=attribute-defined-outside-init
+            for child_loc in self.children:
+                try:
+                    child = self.runtime.get_block(child_loc)
+                except ItemNotFoundError:
+                    log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc))
+                    continue
+                self._child_instances.append(child)
+
+        return self._child_instances
+
+    def get_required_module_descriptors(self):
+        """Returns a list of XModuleDescriptor instances upon which this module depends, but are
+        not children of this module"""
+        return []
+
+    def get_display_items(self):
+        """
+        Returns a list of descendent module instances that will display
+        immediately inside this module.
+        """
+        items = []
+        for child in self.get_children():
+            items.extend(child.displayable_items())
+
+        return items
+
+    def displayable_items(self):
+        """
+        Returns list of displayable modules contained by this module. If this
+        module is visible, should return [self].
+        """
+        return [self]
+
+    def get_child_by(self, selector):
+        """
+        Return a child XBlock that matches the specified selector
+        """
+        for child in self.get_children():
+            if selector(child):
+                return child
+        return None
+
+
+class XModule(XModuleMixin, HTMLSnippet, XBlock):  # pylint: disable=abstract-method
+    """ Implements a generic learning module.
+
+        Subclasses must at a minimum provide a definition for get_html in order
+        to be displayed to users.
+
+        See the HTML module for a simple example.
+    """
+
+    # The default implementation of get_icon_class returns the icon_class
+    # attribute of the class
+    #
+    # This attribute can be overridden by subclasses, and
+    # the function can also be overridden if the icon class depends on the data
+    # in the module
+    icon_class = 'other'
+
+    def __init__(self, descriptor, *args, **kwargs):
+        """
+        Construct a new xmodule
+
+        runtime: An XBlock runtime allowing access to external resources
+
+        descriptor: the XModuleDescriptor that this module is an instance of.
+
+        field_data: A dictionary-like object that maps field names to values
+            for those fields.
+        """
+        super(XModule, self).__init__(*args, **kwargs)
+        self.system = self.runtime
+        self.descriptor = descriptor
+        self._loaded_children = None
+
+    def get_children(self):
+        """
         Return module instances for all the children of this module.
-        '''
+        """
         if self._loaded_children is None:
             child_descriptors = self.get_child_descriptors()
 
@@ -200,7 +272,7 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
         return '<x_module(id={0})>'.format(self.id)
 
     def get_child_descriptors(self):
-        '''
+        """
         Returns the descriptors of the child modules
 
         Overriding this changes the behavior of get_children and
@@ -211,40 +283,13 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
 
         These children will be the same children returned by the
         descriptor unless descriptor.has_dynamic_children() is true.
-        '''
-        return self.descriptor.get_children()
-
-    def get_child_by(self, selector):
         """
-        Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
-        """
-        for child in self.get_children():
-            if selector(child):
-                return child
-        return None
-
-    def get_display_items(self):
-        '''
-        Returns a list of descendent module instances that will display
-        immediately inside this module.
-        '''
-        items = []
-        for child in self.get_children():
-            items.extend(child.displayable_items())
-
-        return items
-
-    def displayable_items(self):
-        '''
-        Returns list of displayable modules contained by this module. If this
-        module is visible, should return [self].
-        '''
-        return [self]
+        return self.descriptor.get_children()
 
     def get_icon_class(self):
-        '''
+        """
         Return a css class identifying this module in the context of an icon
-        '''
+        """
         return self.icon_class
 
     # Functions used in the LMS
@@ -267,7 +312,7 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
         return None
 
     def max_score(self):
-        ''' Maximum score. Two notes:
+        """ Maximum score. Two notes:
 
             * This is generic; in abstract, a problem could be 3/5 points on one
               randomization, and 5/7 on another
@@ -275,22 +320,22 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
             * In practice, this is a Very Bad Idea, and (a) will break some code
               in place (although that code should get fixed), and (b) break some
               analytics we plan to put in place.
-        '''
+        """
         return None
 
     def get_progress(self):
-        ''' Return a progress.Progress object that represents how far the
+        """ Return a progress.Progress object that represents how far the
         student has gone in this module.  Must be implemented to get correct
         progress tracking behavior in nesting modules like sequence and
         vertical.
 
         If this module has no notion of progress, return None.
-        '''
+        """
         return None
 
     def handle_ajax(self, _dispatch, _data):
-        ''' dispatch is last part of the URL.
-            data is a dictionary-like object with the content of the request'''
+        """ dispatch is last part of the URL.
+            data is a dictionary-like object with the content of the request"""
         return ""
 
 
@@ -381,7 +426,7 @@ class ResourceTemplates(object):
             return None
 
 
-class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
+class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
     """
     An XModuleDescriptor is a specification for an element of a course. This
     could be a problem, an organizational element (a group of content), or a
@@ -446,86 +491,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
         self.edited_by = self.edited_on = self.previous_version = self.update_version = self.definition_locator = None
         self._child_instances = None
 
-    @property
-    def id(self):
-        return self.location.url()
-
-    @property
-    def category(self):
-        return self.scope_ids.block_type
-
-    @property
-    def location(self):
-        try:
-            return Location(self.scope_ids.usage_id)
-        except InvalidLocationError:
-            if isinstance(self.scope_ids.usage_id, BlockUsageLocator):
-                return self.scope_ids.usage_id
-            else:
-                return BlockUsageLocator(self.scope_ids.usage_id)
-
-    @location.setter
-    def location(self, value):
-        self.scope_ids = self.scope_ids._replace(
-            def_id=value,
-            usage_id=value,
-        )
-
-    @property
-    def url_name(self):
-        if isinstance(self.location, Location):
-            return self.location.name
-        elif isinstance(self.location, BlockUsageLocator):
-            return self.location.usage_id
-        else:
-            raise InsufficientSpecificationError()
-
-
-    @property
-    def display_name_with_default(self):
-        '''
-        Return a display name for the module: use display_name if defined in
-        metadata, otherwise convert the url name.
-        '''
-        name = self.display_name
-        if name is None:
-            name = self.url_name.replace('_', ' ')
-        return name
-
-    def get_required_module_descriptors(self):
-        """Returns a list of XModuleDescritpor instances upon which this module depends, but are
-        not children of this module"""
-        return []
-
-    def get_children(self):
-        """Returns a list of XModuleDescriptor instances for the children of
-        this module"""
-        if not self.has_children:
-            return []
-
-        if self._child_instances is None:
-            self._child_instances = []
-            for child_loc in self.children:
-                if isinstance(child_loc, XModuleDescriptor):
-                    child = child_loc
-                else:
-                    try:
-                        child = self.runtime.get_block(child_loc)
-                    except ItemNotFoundError:
-                        log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc))
-                        continue
-                self._child_instances.append(child)
-
-        return self._child_instances
-
-    def get_child_by(self, selector):
-        """
-        Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
-        """
-        for child in self.get_children():
-            if selector(child):
-                return child
-        return None
 
     def xmodule(self, system):
         """
@@ -619,15 +584,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
         raise NotImplementedError(
             'Modules must implement export_to_xml to enable xml export')
 
-    @property
-    def xblock_kvs(self):
-        """
-        Use w/ caution. Really intended for use by the persistence layer.
-        """
-        # if caller wants kvs, caller's assuming it's up to date; so, decache it
-        self.save()
-        return self._field_data._kvs
-
     # =============================== BUILTIN METHODS ==========================
     def __eq__(self, other):
         return (self.scope_ids == other.scope_ids and
@@ -655,17 +611,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
         return [XBlock.tags, XBlock.name]
 
 
-    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
-        any set to None.)
-        """
-        result = {}
-        for field in self.fields.values():
-            if (field.scope == scope and field.is_set_on(self)):
-                result[field.name] = field.read_json(self)
-        return result
-
     @property
     def editable_metadata_fields(self):
         """
@@ -825,7 +770,7 @@ class XMLParsingSystem(DescriptorSystem):
 
 
 class ModuleSystem(Runtime):
-    '''
+    """
     This is an abstraction such that x_modules can function independent
     of the courseware (e.g. import into other types of courseware, LMS,
     or if we want to have a sandbox server for user-contributed content)
@@ -835,7 +780,7 @@ class ModuleSystem(Runtime):
 
     Note that these functions can be closures over e.g. a django request
     and user, or other environment-specific info.
-    '''
+    """
     def __init__(
             self, ajax_url, track_function, get_module, render_template,
             replace_urls, xmodule_field_data, user=None, filestore=None,
@@ -844,7 +789,7 @@ class ModuleSystem(Runtime):
             open_ended_grading_interface=None, s3_interface=None,
             cache=None, can_execute_unsafe_code=None, replace_course_urls=None,
             replace_jump_to_id_urls=None, **kwargs):
-        '''
+        """
         Create a closure around the system environment.
 
         ajax_url - the url where ajax calls to the encapsulating module go.
@@ -893,7 +838,7 @@ class ModuleSystem(Runtime):
         can_execute_unsafe_code - A function returning a boolean, whether or
             not to allow the execution of unsafe, unsandboxed code.
 
-        '''
+        """
         super(ModuleSystem, self).__init__(**kwargs)
 
         self.ajax_url = ajax_url
@@ -926,11 +871,11 @@ class ModuleSystem(Runtime):
         self.replace_jump_to_id_urls = replace_jump_to_id_urls
 
     def get(self, attr):
-        '''	provide uniform access to attributes (like etree).'''
+        """	provide uniform access to attributes (like etree)."""
         return self.__dict__.get(attr)
 
     def set(self, attr, val):
-        '''provide uniform access to attributes (like etree)'''
+        """provide uniform access to attributes (like etree)"""
         self.__dict__[attr] = val
 
     def __repr__(self):
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 941809b..3858b1c 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -32,6 +32,7 @@ from .discussionsettings import *
 
 from lms.xblock.mixin import LmsBlockMixin
 from xmodule.modulestore.inheritance import InheritanceMixin
+from xmodule.x_module import XModuleMixin
 
 ################################### FEATURES ###################################
 # The display name of the platform to be used in templates/emails/etc.
@@ -363,7 +364,7 @@ CONTENTSTORE = None
 
 # This should be moved into an XBlock Runtime/Application object
 # once the responsibility of XBlock creation is moved out of modulestore - cpennington
-XBLOCK_MIXINS = (LmsBlockMixin, InheritanceMixin)
+XBLOCK_MIXINS = (LmsBlockMixin, InheritanceMixin, XModuleMixin)
 
 #################### Python sandbox ############################################
 
--
libgit2 0.26.0