From f500b72290e3edf1e3c60b87bced55316d1466ee Mon Sep 17 00:00:00 2001
From: Calen Pennington <cale@edx.org>
Date: Tue, 17 Sep 2013 11:55:34 -0400
Subject: [PATCH] Make sure that we have the right set of fields available
 during xml parsing

We had a bug where mixins weren't being applied before `load_from_xml`
was called. This meant that not all of the fields were being loaded
correctly. To fix it, we used the mixoligist from the runtime to apply
the mixins earlier in the process. However, that caused the mixins to be
applied twice.

The included fixes to xblock resolved the multiply-applied mixins, and
the fixes to the parsing code make it simpler to understand, and add
some unit tests of the parsing to boot.
---
 .../lib/xmodule/xmodule/tests/test_import.py  |   1 -
 .../xmodule/xmodule/tests/test_xml_module.py  |  97 ++++++++++++-
 .../lib/xmodule/xmodule/tests/xml/__init__.py |  46 +++++++
 .../xmodule/xmodule/tests/xml/factories.py    | 128 ++++++++++++++++++
 .../xmodule/tests/xml/test_inheritance.py     |  29 ++++
 .../xmodule/xmodule/tests/xml/test_policy.py  |  30 ++++
 common/lib/xmodule/xmodule/video_module.py    |  35 ++---
 common/lib/xmodule/xmodule/xml_module.py      |  81 +++--------
 lms/djangoapps/courseware/module_render.py    |   2 +-
 pylintrc                                      |   8 +-
 requirements/edx/base.txt                     |   2 +-
 requirements/edx/github.txt                   |   4 +-
 12 files changed, 370 insertions(+), 93 deletions(-)
 create mode 100644 common/lib/xmodule/xmodule/tests/xml/__init__.py
 create mode 100644 common/lib/xmodule/xmodule/tests/xml/factories.py
 create mode 100644 common/lib/xmodule/xmodule/tests/xml/test_inheritance.py
 create mode 100644 common/lib/xmodule/xmodule/tests/xml/test_policy.py

diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index cd7749dc09d..19232e383fc 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -91,7 +91,6 @@ class ImportTestCase(BaseCourseTestCase):
 
         self.assertNotEqual(descriptor1.location, descriptor2.location)
 
-    @unittest.skip('Temporarily disabled')
     def test_reimport(self):
         '''Make sure an already-exported error xml tag loads properly'''
 
diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py
index 1515c76237d..9d11b2b168f 100644
--- a/common/lib/xmodule/xmodule/tests/test_xml_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py
@@ -1,17 +1,25 @@
 # disable missing docstring
 #pylint: disable=C0111
 
-from xmodule.x_module import XModuleFields
-from xblock.fields import Scope, String, Dict, Boolean, Integer, Float, Any, List
-from xblock.field_data import DictFieldData
-from xmodule.fields import Date, Timedelta
-from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
 import unittest
-from nose.tools import assert_equals  # pylint: disable=E0611
+
 from mock import Mock
-from xmodule.modulestore.inheritance import InheritanceKeyValueStore, InheritanceMixin
+from nose.tools import assert_equals, assert_not_equals, assert_true, assert_false, assert_in, assert_not_in  # pylint: disable=E0611
+
+from xblock.field_data import DictFieldData
+from xblock.fields import Scope, String, Dict, Boolean, Integer, Float, Any, List
 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.tests import get_test_descriptor_system
+from xmodule.tests.xml import XModuleXmlImportTest
+from xmodule.tests.xml.factories import CourseFactory, SequenceFactory, ProblemFactory
 
 
 class CrazyJsonString(String):
@@ -379,3 +387,78 @@ class TestDeserializeTimedelta(TestDeserialize):
         self.assertDeserializeEqual('1 day 12 hours 59 minutes 59 seconds',
             '"1 day 12 hours 59 minutes 59 seconds"')
         self.assertDeserializeNonString()
+
+
+class TestXmlAttributes(XModuleXmlImportTest):
+
+    def test_unknown_attribute(self):
+        assert_false(hasattr(CourseDescriptor, 'unknown_attr'))
+        course = self.process_xml(CourseFactory.build(unknown_attr='value'))
+        assert_false(hasattr(course, 'unknown_attr'))
+        assert_equals('value', course.xml_attributes['unknown_attr'])
+
+    def test_known_attribute(self):
+        assert_true(hasattr(CourseDescriptor, 'show_chat'))
+        course = self.process_xml(CourseFactory.build(show_chat='true'))
+        assert_true(course.show_chat)
+        assert_not_in('show_chat', course.xml_attributes)
+
+    def test_rerandomize_in_policy(self):
+        # Rerandomize isn't a basic attribute of Sequence
+        assert_false(hasattr(SequenceDescriptor, 'rerandomize'))
+
+        root = SequenceFactory.build(policy={'rerandomize': 'never'})
+        ProblemFactory.build(parent=root)
+
+        seq = self.process_xml(root)
+
+        # Rerandomize is added to the constructed sequence via the InheritanceMixin
+        assert_equals('never', seq.rerandomize)
+
+        # Rerandomize is a known value coming from policy, and shouldn't appear
+        # in xml_attributes
+        assert_not_in('rerandomize', seq.xml_attributes)
+
+    def test_attempts_in_policy(self):
+        # attempts isn't a basic attribute of Sequence
+        assert_false(hasattr(SequenceDescriptor, 'attempts'))
+
+        root = SequenceFactory.build(policy={'attempts': '1'})
+        ProblemFactory.build(parent=root)
+
+        seq = self.process_xml(root)
+
+        # attempts isn't added to the constructed sequence, because
+        # it's not in the InheritanceMixin
+        assert_false(hasattr(seq, 'attempts'))
+
+        # attempts is an unknown attribute, so we should include it
+        # in xml_attributes so that it gets written out (despite the misleading
+        # name)
+        assert_in('attempts', seq.xml_attributes)
+
+    def test_inheritable_attribute(self):
+        # days_early_for_beta isn't a basic attribute of Sequence
+        assert_false(hasattr(SequenceDescriptor, 'days_early_for_beta'))
+
+        # days_early_for_beta is added by InheritanceMixin
+        assert_true(hasattr(InheritanceMixin, 'days_early_for_beta'))
+
+        root = SequenceFactory.build(policy={'days_early_for_beta': '2'})
+        ProblemFactory.build(parent=root)
+
+        # InheritanceMixin will be used when processing the XML
+        assert_in(InheritanceMixin, root.xblock_mixins)
+
+        seq = self.process_xml(root)
+
+        assert_equals(seq.unmixed_class, SequenceDescriptor)
+        assert_not_equals(type(seq), SequenceDescriptor)
+
+        # days_early_for_beta is added to the constructed sequence, because
+        # it's in the InheritanceMixin
+        assert_equals(2, seq.days_early_for_beta)
+
+        # days_early_for_beta is a known attribute, so we shouldn't include it
+        # in xml_attributes
+        assert_not_in('days_early_for_beta', seq.xml_attributes)
diff --git a/common/lib/xmodule/xmodule/tests/xml/__init__.py b/common/lib/xmodule/xmodule/tests/xml/__init__.py
new file mode 100644
index 00000000000..32cefadca72
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/xml/__init__.py
@@ -0,0 +1,46 @@
+"""
+Xml parsing tests for XModules
+"""
+import pprint
+from mock import Mock
+
+from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
+from xmodule.mako_module import MakoDescriptorSystem
+
+
+class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem):  # pylint: disable=abstract-method
+    """
+    The simplest possible XMLParsingSystem
+    """
+    def __init__(self, xml_import_data):
+        self.org = xml_import_data.org
+        self.course = xml_import_data.course
+        self.default_class = xml_import_data.default_class
+        self._descriptors = {}
+        super(InMemorySystem, self).__init__(
+            policy=xml_import_data.policy,
+            process_xml=self.process_xml,
+            load_item=self.load_item,
+            error_tracker=Mock(),
+            resources_fs=xml_import_data.filesystem,
+            mixins=xml_import_data.xblock_mixins,
+            render_template=lambda template, context: pprint.pformat((template, context))
+        )
+
+    def process_xml(self, xml):  # pylint: disable=method-hidden
+        """Parse `xml` as an XModuleDescriptor, and add it to `self._descriptors`"""
+        descriptor = XModuleDescriptor.load_from_xml(xml, self, self.org, self.course, self.default_class)
+        self._descriptors[descriptor.location.url()] = descriptor
+        return descriptor
+
+    def load_item(self, location):  # pylint: disable=method-hidden
+        """Return the descriptor loaded for `location`"""
+        return self._descriptors[location]
+
+
+class XModuleXmlImportTest(object):
+    """Base class for tests that use basic `XModuleDescriptor.load_from_xml` xml parsing"""
+    def process_xml(self, xml_import_data):
+        """Use the `xml_import_data` to import an :class:`XModuleDescriptor` from xml"""
+        system = InMemorySystem(xml_import_data)
+        return system.process_xml(xml_import_data.xml_string)
diff --git a/common/lib/xmodule/xmodule/tests/xml/factories.py b/common/lib/xmodule/xmodule/tests/xml/factories.py
new file mode 100644
index 00000000000..168dcd579ba
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/xml/factories.py
@@ -0,0 +1,128 @@
+"""
+Factories for generating edXML for testing XModule import
+"""
+
+import inspect
+
+from fs.memoryfs import MemoryFS
+from factory import Factory, lazy_attribute, post_generation, Sequence
+from lxml import etree
+
+from xmodule.modulestore.inheritance import InheritanceMixin
+
+
+class XmlImportData(object):
+    """
+    Class to capture all of the data needed to actually run an XML import,
+    so that the Factories have something to generate
+    """
+    def __init__(self, xml_node, xml=None, org=None, course=None,
+                 default_class=None, policy=None,
+                 filesystem=None, parent=None,
+                 xblock_mixins=()):
+
+        self._xml_node = xml_node
+        self._xml_string = xml
+        self.org = org
+        self.course = course
+        self.default_class = default_class
+        self.filesystem = filesystem
+        self.xblock_mixins = xblock_mixins
+        self.parent = parent
+
+        if policy is None:
+            self.policy = {}
+        else:
+            self.policy = policy
+
+    @property
+    def xml_string(self):
+        """Return the stringified version of the generated xml"""
+        if self._xml_string is not None:
+            return self._xml_string
+
+        return etree.tostring(self._xml_node)
+
+    def __repr__(self):
+        return u"XmlImportData{!r}".format((
+            self._xml_node, self._xml_string, self.org,
+            self.course, self.default_class, self.policy,
+            self.filesystem, self.parent, self.xblock_mixins
+        ))
+
+
+# Extract all argument names used to construct XmlImportData objects,
+# so that the factory doesn't treat them as XML attributes
+XML_IMPORT_ARGS = inspect.getargspec(XmlImportData.__init__).args
+
+
+class XmlImportFactory(Factory):
+    """
+    Factory for generating XmlImportData's, which can hold all the data needed
+    to run an XModule XML import
+    """
+    FACTORY_FOR = XmlImportData
+
+    filesystem = MemoryFS()
+    xblock_mixins = (InheritanceMixin,)
+    url_name = Sequence(str)
+    attribs = {}
+    policy = {}
+    tag = 'unknown'
+
+    @classmethod
+    def _adjust_kwargs(cls, **kwargs):
+        """
+        Adjust the kwargs to be passed to the generated class.
+
+        Any kwargs that match :fun:`XmlImportData.__init__` will be passed
+        through. Any other unknown `kwargs` will be treated as XML attributes
+
+        :param tag: xml tag for the generated :class:`Element` node
+        :param text: (Optional) specifies the text of the generated :class:`Element`.
+        :param policy: (Optional) specifies data for the policy json file for this node
+        :type policy: dict
+        :param attribs: (Optional) specify attributes for the XML node
+        :type attribs: dict
+        """
+        tag = kwargs.pop('tag', 'unknown')
+        kwargs['policy'] = {'{tag}/{url_name}'.format(tag=tag, url_name=kwargs['url_name']): kwargs['policy']}
+
+        kwargs['xml_node'].text = kwargs.pop('text', None)
+
+        kwargs['xml_node'].attrib.update(kwargs.pop('attribs', {}))
+        for key in kwargs.keys():
+            if key not in XML_IMPORT_ARGS:
+                kwargs['xml_node'].set(key, kwargs.pop(key))
+
+        return kwargs
+
+    @lazy_attribute
+    def xml_node(self):
+        """An :class:`xml.etree.Element`"""
+        return etree.Element(self.tag)
+
+    @post_generation
+    def parent(self, _create, extracted, **_):
+        """Hook to merge this xml into a parent xml node"""
+        if extracted is None:
+            return
+
+        extracted._xml_node.append(self._xml_node)  # pylint: disable=no-member, protected-access
+        extracted.policy.update(self.policy)
+
+
+class CourseFactory(XmlImportFactory):
+    """Factory for <course> nodes"""
+    tag = 'course'
+
+
+class SequenceFactory(XmlImportFactory):
+    """Factory for <sequential> nodes"""
+    tag = 'sequential'
+
+
+class ProblemFactory(XmlImportFactory):
+    """Factory for <problem> nodes"""
+    tag = 'problem'
+    text = '<h1>Empty Problem!</h1>'
diff --git a/common/lib/xmodule/xmodule/tests/xml/test_inheritance.py b/common/lib/xmodule/xmodule/tests/xml/test_inheritance.py
new file mode 100644
index 00000000000..dc27f0c7920
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/xml/test_inheritance.py
@@ -0,0 +1,29 @@
+"""
+Test that inherited fields work correctly when parsing XML
+"""
+from nose.tools import assert_equals  # pylint: disable=no-name-in-module
+
+from xmodule.tests.xml import XModuleXmlImportTest
+from xmodule.tests.xml.factories import CourseFactory, SequenceFactory, ProblemFactory
+
+
+class TestInheritedFieldParsing(XModuleXmlImportTest):
+    """
+    Test that inherited fields work correctly when parsing XML
+
+    """
+    def test_null_string(self):
+        # Test that the string inherited fields are passed through 'deserialize_field',
+        # which converts the string "null" to the python value None
+        root = CourseFactory.build(days_early_for_beta="null")
+        sequence = SequenceFactory.build(parent=root)
+        ProblemFactory.build(parent=sequence)
+
+        course = self.process_xml(root)
+        assert_equals(None, course.days_early_for_beta)
+
+        sequence = course.get_children()[0]
+        assert_equals(None, sequence.days_early_for_beta)
+
+        problem = sequence.get_children()[0]
+        assert_equals(None, problem.days_early_for_beta)
diff --git a/common/lib/xmodule/xmodule/tests/xml/test_policy.py b/common/lib/xmodule/xmodule/tests/xml/test_policy.py
new file mode 100644
index 00000000000..2e702cc82d1
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/xml/test_policy.py
@@ -0,0 +1,30 @@
+"""
+Tests that policy json files import correctly when loading XML
+"""
+
+from nose.tools import assert_equals, assert_raises  # pylint: disable=no-name-in-module
+
+from xmodule.tests.xml.factories import CourseFactory
+from xmodule.tests.xml import XModuleXmlImportTest
+
+
+class TestPolicy(XModuleXmlImportTest):
+    """
+    Tests that policy json files import correctly when loading xml
+    """
+    def test_no_attribute_mapping(self):
+        # Policy files are json, and thus the values aren't passed through 'deserialize_field'
+        # Therefor, the string 'null' is passed unchanged to the Float field, which will trigger
+        # a ValueError
+        with assert_raises(ValueError):
+            course = self.process_xml(CourseFactory.build(policy={'days_early_for_beta': 'null'}))
+
+            # Trigger the exception by looking at the imported data
+            course.days_early_for_beta  # pylint: disable=pointless-statement
+
+    def test_course_policy(self):
+        course = self.process_xml(CourseFactory.build(policy={'days_early_for_beta': None}))
+        assert_equals(None, course.days_early_for_beta)
+
+        course = self.process_xml(CourseFactory.build(policy={'days_early_for_beta': 9}))
+        assert_equals(9, course.days_early_for_beta)
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py
index d97d5aa144a..febfe5961f8 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module.py
@@ -24,7 +24,7 @@ from django.conf import settings
 from xmodule.x_module import XModule
 from xmodule.editing_module import TabsEditingDescriptor
 from xmodule.raw_module import EmptyDataRawDescriptor
-from xmodule.xml_module import is_pointer_tag, name_to_pathname
+from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field
 from xmodule.modulestore import Location
 from xblock.fields import Scope, String, Boolean, Float, List, Integer, ScopeIds
 
@@ -217,7 +217,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
         # For backwards compatibility -- if we've got XML data, parse
         # it out and set the metadata fields
         if self.data:
-            field_data = VideoDescriptor._parse_video_xml(self.data)
+            field_data = self._parse_video_xml(self.data)
             self._field_data.set_many(self, field_data)
             del self.data
 
@@ -241,7 +241,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
         if is_pointer_tag(xml_object):
             filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name))
             xml_data = etree.tostring(cls.load_file(filepath, system.resources_fs, location))
-        field_data = VideoDescriptor._parse_video_xml(xml_data)
+        field_data = cls._parse_video_xml(xml_data)
         field_data['location'] = location
         kvs = InheritanceKeyValueStore(initial_values=field_data)
         field_data = DbModel(kvs)
@@ -292,8 +292,8 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
             xml.append(ele)
         return xml
 
-    @staticmethod
-    def _parse_youtube(data):
+    @classmethod
+    def _parse_youtube(cls, data):
         """
         Parses a string of Youtube IDs such as "1.0:AXdE34_U,1.5:VO3SxfeD"
         into a dictionary. Necessary for backwards compatibility with
@@ -310,14 +310,14 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
                 # Handle the fact that youtube IDs got double-quoted for a period of time.
                 # Note: we pass in "VideoFields.youtube_id_1_0" so we deserialize as a String--
                 # it doesn't matter what the actual speed is for the purposes of deserializing.
-                youtube_id = VideoDescriptor._deserialize(VideoFields.youtube_id_1_0.name, pieces[1])
+                youtube_id = deserialize_field(cls.youtube_id_1_0, pieces[1])
                 ret[speed] = youtube_id
             except (ValueError, IndexError):
                 log.warning('Invalid YouTube ID: %s' % video)
         return ret
 
-    @staticmethod
-    def _parse_video_xml(xml_data):
+    @classmethod
+    def _parse_video_xml(cls, xml_data):
         """
         Parse video fields out of xml_data. The fields are set if they are
         present in the XML.
@@ -326,8 +326,8 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
         field_data = {}
 
         conversions = {
-            'start_time': VideoDescriptor._parse_time,
-            'end_time': VideoDescriptor._parse_time
+            'start_time': cls._parse_time,
+            'end_time': cls._parse_time
         }
 
         # Convert between key names for certain attributes --
@@ -349,10 +349,10 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
         for attr, value in xml.items():
             if attr in compat_keys:
                 attr = compat_keys[attr]
-            if attr in VideoDescriptor.metadata_to_strip + ('url_name', 'name'):
+            if attr in cls.metadata_to_strip + ('url_name', 'name'):
                 continue
             if attr == 'youtube':
-                speeds = VideoDescriptor._parse_youtube(value)
+                speeds = cls._parse_youtube(value)
                 for speed, youtube_id in speeds.items():
                     # should have made these youtube_id_1_00 for
                     # cleanliness, but hindsight doesn't need glasses
@@ -367,20 +367,13 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
                 else:
                 # We export values with json.dumps (well, except for Strings, but
                 # for about a month we did it for Strings also).
-                    value = VideoDescriptor._deserialize(attr, value)
+                    value = deserialize_field(cls.fields[attr], value)
                 field_data[attr] = value
 
         return field_data
 
     @classmethod
-    def _deserialize(cls, attr, value):
-        """
-        Handles deserializing values that may have been encoded with json.dumps.
-        """
-        return cls.get_map_for_field(attr).from_xml(value)
-
-    @staticmethod
-    def _parse_time(str_time):
+    def _parse_time(cls, str_time):
         """Converts s in '12:34:45' format to seconds. If s is
         None, returns empty string"""
         if not str_time:
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index 5fa9ff3260f..83bb6faffdc 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -62,23 +62,6 @@ def get_metadata_from_xml(xml_object, remove=True):
         xml_object.remove(meta)
     return dmdata
 
-_AttrMapBase = namedtuple('_AttrMap', 'from_xml to_xml')
-
-
-class AttrMap(_AttrMapBase):
-    """
-    A class that specifies two functions:
-
-        from_xml: convert value from the xml representation into
-            an internal python representation
-
-        to_xml: convert the internal python representation into
-            the value to store in the xml.
-    """
-    def __new__(_cls, from_xml=lambda x: x,
-                to_xml=lambda x: x):
-        return _AttrMapBase.__new__(_cls, from_xml, to_xml)
-
 
 def serialize_field(value):
     """
@@ -166,20 +149,6 @@ class XmlDescriptor(XModuleDescriptor):
 
     metadata_to_export_to_policy = ('discussion_topics', 'checklists')
 
-    @classmethod
-    def get_map_for_field(cls, attr):
-        """
-        Returns a serialize/deserialize AttrMap for the given field of a class.
-
-        Searches through fields defined by cls to find one named attr.
-        """
-        if attr in cls.fields:
-            from_xml = lambda val: deserialize_field(cls.fields[attr], val)
-            to_xml = lambda val: serialize_field(val)
-            return AttrMap(from_xml, to_xml)
-        else:
-            return AttrMap()
-
     @classmethod
     def definition_from_xml(cls, xml_object, system):
         """
@@ -274,19 +243,19 @@ class XmlDescriptor(XModuleDescriptor):
 
         Returns a dictionary {key: value}.
         """
-        metadata = {}
-        for attr in xml_object.attrib:
-            val = xml_object.get(attr)
-            if val is not None:
-                # VS[compat].  Remove after all key translations done
-                attr = cls._translate(attr)
-
-                if attr in cls.metadata_to_strip:
-                    # don't load these
-                    continue
-
-                attr_map = cls.get_map_for_field(attr)
-                metadata[attr] = attr_map.from_xml(val)
+        metadata = {'xml_attributes': {}}
+        for attr, val in xml_object.attrib.iteritems():
+            # VS[compat].  Remove after all key translations done
+            attr = cls._translate(attr)
+
+            if attr in cls.metadata_to_strip:
+                # don't load these
+                continue
+
+            if attr not in cls.fields:
+                metadata['xml_attributes'][attr] = val
+            else:
+                metadata[attr] = deserialize_field(cls.fields[attr], val)
         return metadata
 
     @classmethod
@@ -295,9 +264,14 @@ class XmlDescriptor(XModuleDescriptor):
         Add the keys in policy to metadata, after processing them
         through the attrmap.  Updates the metadata dict in place.
         """
-        for attr in policy:
-            attr_map = cls.get_map_for_field(attr)
-            metadata[cls._translate(attr)] = attr_map.from_xml(policy[attr])
+        for attr, value in policy.iteritems():
+            attr = cls._translate(attr)
+            if attr not in cls.fields:
+                # Store unknown attributes coming from policy.json
+                # in such a way that they will export to xml unchanged
+                metadata['xml_attributes'][attr] = value
+            else:
+                metadata[attr] = value
 
     @classmethod
     def from_xml(cls, xml_data, system, org=None, course=None):
@@ -357,11 +331,7 @@ class XmlDescriptor(XModuleDescriptor):
         field_data.update(definition)
         field_data['children'] = children
 
-        field_data['xml_attributes'] = {}
         field_data['xml_attributes']['filename'] = definition.get('filename', ['', None])  # for git link
-        for key, value in metadata.items():
-            if key not in cls.fields:
-                field_data['xml_attributes'][key] = value
         field_data['location'] = location
         field_data['category'] = xml_object.tag
         kvs = InheritanceKeyValueStore(initial_values=field_data)
@@ -415,18 +385,11 @@ class XmlDescriptor(XModuleDescriptor):
         # Set the tag so we get the file path right
         xml_object.tag = self.category
 
-        def val_for_xml(attr):
-            """Get the value for this attribute that we want to store.
-            (Possible format conversion through an AttrMap).
-             """
-            attr_map = self.get_map_for_field(attr)
-            return attr_map.to_xml(self._field_data.get(self, attr))
-
         # Add the non-inherited metadata
         for attr in sorted(own_metadata(self)):
             # don't want e.g. data_dir
             if attr not in self.metadata_to_strip and attr not in self.metadata_to_export_to_policy:
-                val = val_for_xml(attr)
+                val = serialize_field(self._field_data.get(self, attr))
                 try:
                     xml_object.set(attr, val)
                 except Exception, e:
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index acdd4f0aa0c..f86bef29ade 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -306,7 +306,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
         # Construct the key for the module
         key = KeyValueStore.Key(
             scope=Scope.user_state,
-            student_id=user.id,
+            user_id=user.id,
             block_scope_id=descriptor.location,
             field_name='grade'
         )
diff --git a/pylintrc b/pylintrc
index 9525f04362e..a3c84c15558 100644
--- a/pylintrc
+++ b/pylintrc
@@ -89,6 +89,9 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme
 # evaluation report (RP0004).
 comment=no
 
+# Display symbolic names of messages in reports
+symbols=yes
+
 
 [TYPECHECK]
 
@@ -120,7 +123,10 @@ generated-members=
     content,
     status_code,
 # For factory_boy factories
-    create
+    create,
+    build,
+# For xblocks
+    fields,
 
 
 [BASIC]
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index addef7b629c..52fe3ce44f0 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -47,7 +47,7 @@ Pillow==1.7.8
 pip>=1.4
 polib==1.0.3
 pycrypto>=2.6
-pygments==1.5
+pygments==1.6
 pygraphviz==1.1
 pymongo==2.4.1
 pyparsing==1.5.6
diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt
index add9bda1d1c..e0c88b217e8 100644
--- a/requirements/edx/github.txt
+++ b/requirements/edx/github.txt
@@ -14,8 +14,8 @@
 -e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
 
 # Our libraries:
--e git+https://github.com/edx/XBlock.git@aa0d60627#egg=XBlock
+-e git+https://github.com/edx/XBlock.git@a8de02c0#egg=XBlock
 -e git+https://github.com/edx/codejail.git@0a1b468#egg=codejail
--e git+https://github.com/edx/diff-cover.git@v0.2.3#egg=diff_cover
+-e git+https://github.com/edx/diff-cover.git@v0.2.4#egg=diff_cover
 -e git+https://github.com/edx/js-test-tool.git@v0.0.7#egg=js_test_tool
 -e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle
-- 
GitLab