[autotest] Deserialize records and persist them

To send records from the master to shards it's necessary to serialize
them. This adds the deserialization functionality.

TEST=Ran suites.
DEPLOY=apache

Change-Id: I2806b0cfe1e4fbfba5d89e6d422800c7637ed4e9
Reviewed-on: https://chromium-review.googlesource.com/216355
Reviewed-by: Prashanth B <beeps@chromium.org>
Tested-by: Jakob Jülich <jakobjuelich@chromium.org>
Commit-Queue: Jakob Jülich <jakobjuelich@chromium.org>
diff --git a/frontend/afe/model_logic.py b/frontend/afe/model_logic.py
index 603db5e..0fe0820 100644
--- a/frontend/afe/model_logic.py
+++ b/frontend/afe/model_logic.py
@@ -1019,6 +1019,80 @@
         return attr.serialize()
 
 
+    @classmethod
+    def deserialize(cls, data):
+        """Deserializes an object's representation and saves it to the database.
+
+        This takes the result of the serialize method and creates objects
+        in the database that are just like the original. If an object already
+        exists, it will not be overwritten.
+
+        @param data: Representation of an object and its dependencies, as
+                     returned by serialize.
+
+        @returns: The object represented by data if it didn't exist before,
+                  otherwise the object that existed before and has the same type
+                  and id as the one described by data.
+        """
+        if data is None:
+            return None
+
+        try:
+            return cls.objects.get(id=data['id'])
+        except cls.DoesNotExist:
+            return cls._deserialize_new_object(data)
+
+    @classmethod
+    def _deserialize_new_object(cls, data):
+        """Deserialize an object, that does not yet exist in the database.
+
+        The caller has to ensure an object with the same type and id doesn't yet
+        exist in the database.
+
+        @param data: Representation of an object and its dependencies, as
+                     returned by serialize.
+
+        @returns: The object represented by data.
+        """
+        instance = cls()
+        links_to_related_tuples = []
+        for link, value in data.iteritems():
+            if link in cls.SERIALIZATION_LINKS_TO_FOLLOW:
+                # It's a foreign key
+                links_to_related_tuples.append((link, value))
+            else:
+                # It's a local attribute
+                setattr(instance, link, value)
+        instance.save()
+
+        for link, value in links_to_related_tuples:
+            instance._deserialize_relation(link, value)
+        instance.save()
+
+        return instance
+
+
+    def custom_deserialize_relation(self, link, data):
+        raise NotImplementedError(
+            'custom_deserialize_relation must be implemented by subclass %s '
+            'for relation %s' % (type(self), link))
+
+
+    def _deserialize_relation(self, link, data):
+        field = getattr(self, link)
+
+        if field and hasattr(field, 'all'):
+            self._deserialize_2m_relation(link, data, field.model)
+        else:
+            self.custom_deserialize_relation(link, data)
+
+
+    def _deserialize_2m_relation(self, link, data, related_class):
+        relation_set = getattr(self, link)
+        for serialized in data:
+            relation_set.add(related_class.deserialize(serialized))
+
+
 class ModelWithInvalid(ModelExtensions):
     """
     Overrides model methods save() and delete() to support invalidation in