CORD-2665 Fix missing reverse relations for _decl models

Change-Id: Id99b56b007ba574a9e3dacd0f332a6425f7d32f4
diff --git a/xos/coreapi/apihelper.py b/xos/coreapi/apihelper.py
index a3f3552..cd9d4da 100644
--- a/xos/coreapi/apihelper.py
+++ b/xos/coreapi/apihelper.py
@@ -230,12 +230,27 @@
             elif (ftype == "GenericIPAddressField"):
                 setattr(p_obj, field.name, str(getattr(obj, field.name)))
 
-        for field in obj._meta.related_objects:
-            related_name = field.related_name
-            if not related_name:
+
+        # Introspecting the django object for related objects is problematic due to _decl-style attics. The descendant
+        # class's _meta's related_objects doesn't include related objects from the base. For example, VSGServiceInstance
+        # was missing provided_links and subscribed_links, since those were declared in ServiceInstance. (This problem
+        # does not exist with older style attics)
+        #
+        # Instead, look through the protobuf object since we know it's right because we generated it from xproto. Look
+        # for any field that ended in "_ids", and use that to extract the appropriate field from the django
+        # object. This handles both ManyToOne reverse relations and ManyToMany.
+
+        for field_name in p_obj.DESCRIPTOR.fields_by_name.keys():
+            if not field_name.endswith("_ids"):
+                # only look for reverse relations
                 continue
-            if "+" in related_name:
+
+            related_name = field_name[:-4]
+            if not hasattr(obj, related_name):
+                # if field doesn't exist in the django object, then ignore it
+                log.warning("Protobuf field %s doesn't have a corresponding django field" % field_name)
                 continue
+
             try:
                 rel_objs = getattr(obj, related_name)
             except Exception as e:
@@ -251,29 +266,9 @@
                 continue
 
             for rel_obj in rel_objs.all():
-                if not hasattr(p_obj, related_name + "_ids"):
+                if not hasattr(p_obj, field_name):
                     continue
-                getattr(p_obj, related_name + "_ids").append(rel_obj.id)
-
-        # Go through any many-to-many relations. This is almost the same as the related_objects loop above, but slightly
-        # different due to how django handles m2m.
-
-        for m2m in obj._meta.many_to_many:
-            related_name = m2m.name
-            if not related_name:
-                continue
-            if "+" in related_name:   # duplicated logic from related_objects; not sure if necessary
-                continue
-
-            rel_objs = getattr(obj, related_name)
-
-            if not hasattr(rel_objs, "all"):
-                continue
-
-            for rel_obj in rel_objs.all():
-                if not hasattr(p_obj, related_name + "_ids"):
-                    continue
-                getattr(p_obj, related_name + "_ids").append(rel_obj.id)
+                getattr(p_obj, field_name).append(rel_obj.id)
 
         # Generate a list of class names for the object. This includes its
         # ancestors. Anything that is a descendant of XOSBase or User
diff --git a/xos/xos_client/tests/orm_reverse_relations.py b/xos/xos_client/tests/orm_reverse_relations.py
new file mode 100644
index 0000000..a958e95
--- /dev/null
+++ b/xos/xos_client/tests/orm_reverse_relations.py
@@ -0,0 +1,81 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# These are functional tests of ManyToMany relations. These tests need to be conducted end-to-end with a real
+# API to verify that the client and server ends of the API are working with each other.
+
+import random
+import string
+import sys
+import unittest
+
+orm = None
+
+from xosapi import xos_grpc_client
+
+SERVICE_1_NAME = "test_service_1"
+SERVICEINSTANCE_1_NAME = "test_service_instance_1"
+
+SERVICE_2_NAME = "test_service_2"
+SERVICEINSTANCE_2_NAME = "test_service_instance_2"
+
+class TestORMReverseRelations(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def cleanup_models(self, cls, name):
+        objs = cls.objects.filter(name=name)
+        for obj in objs:
+            obj.delete()
+
+
+    def tearDown(self):
+        self.cleanup_models(orm.ServiceInstance, SERVICEINSTANCE_1_NAME)
+        self.cleanup_models(orm.ServiceInstance, SERVICEINSTANCE_2_NAME)
+        self.cleanup_models(orm.Service, SERVICE_1_NAME)
+        self.cleanup_models(orm.Service, SERVICE_2_NAME)
+
+    def test_reverse_relations(self):
+        service1 = orm.Service(name=SERVICE_1_NAME)
+        service1.save()
+
+        serviceinstance1 = orm.ServiceInstance(name=SERVICEINSTANCE_1_NAME, owner=service1)
+        serviceinstance1.save()
+
+        service2 = orm.Service(name=SERVICE_2_NAME)
+        service2.save()
+
+        serviceinstance2 = orm.ServiceInstance(name=SERVICEINSTANCE_2_NAME, owner=service2)
+        serviceinstance2.save()
+
+        link = orm.ServiceInstanceLink(provider_service_instance = serviceinstance1, subscriber_service_instance = serviceinstance2)
+        link.save()
+
+        si1_readback = orm.ServiceInstance.objects.get(id = serviceinstance1.id)
+        si2_readback = orm.ServiceInstance.objects.get(id = serviceinstance2.id)
+
+        self.assertEqual(si1_readback.provided_links.count(), 1)
+        self.assertEqual(si2_readback.subscribed_links.count(), 1)
+
+def test_callback():
+    global orm
+
+    orm = xos_grpc_client.coreclient.xos_orm
+
+    sys.argv=sys.argv[:1]  # unittest gets mad about the orm command line arguments
+    unittest.main()
+
+xos_grpc_client.start_api_parseargs(test_callback)
+