CORD-879 add generic fk support, make contentype a messageoption instead

Change-Id: I820fdb4527adc9f55ea4c49d95662e6ac013299b
diff --git a/xos/coreapi/apihelper.py b/xos/coreapi/apihelper.py
index 9d9134c..89d1545 100644
--- a/xos/coreapi/apihelper.py
+++ b/xos/coreapi/apihelper.py
@@ -6,7 +6,6 @@
 from protos import xos_pb2
 from google.protobuf.empty_pb2 import Empty
 
-from django.contrib.contenttypes.models import ContentType
 from django.contrib.auth import authenticate as django_authenticate
 from django.db.models import F,Q
 from core.models import *
@@ -99,8 +98,6 @@
         bases = [x for x in bases if issubclass(x, PlCoreBase) or issubclass(x, User)]
         p_obj.class_names = ",".join( [x.__name__ for x in bases] )
 
-        p_obj.self_content_type_id = ContentType.objects.get_for_model(obj).id
-
         return p_obj
 
     def protoToArgs(self, djangoClass, message):
diff --git a/xos/coreapi/protos/xosoptions.proto b/xos/coreapi/protos/xosoptions.proto
index 95fc681..40e8017 100644
--- a/xos/coreapi/protos/xosoptions.proto
+++ b/xos/coreapi/protos/xosoptions.proto
@@ -24,3 +24,7 @@
   ReverseForeignKeyRule reverseForeignKey = 1003;
 }
 
+extend google.protobuf.MessageOptions {
+  int32 contentTypeId = 1001;
+}
+
diff --git a/xos/tools/apigen/modelgen b/xos/tools/apigen/modelgen
index 6e4583e..7ef76a4 100755
--- a/xos/tools/apigen/modelgen
+++ b/xos/tools/apigen/modelgen
@@ -18,6 +18,7 @@
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
 from django.db.models.fields.related import ForeignKey, ManyToManyField
 from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
 
 django.setup()
 
@@ -126,6 +127,7 @@
 		self.refs = []
                 self.reverse_refs = []
 		self.plural_name = None
+                self.content_type_id = ContentType.objects.get_for_model(m).id
 
 	def plural(self):
 		if (self.plural_name):
diff --git a/xos/tools/apigen/protobuf.template.txt b/xos/tools/apigen/protobuf.template.txt
index 986d017..43afd39 100644
--- a/xos/tools/apigen/protobuf.template.txt
+++ b/xos/tools/apigen/protobuf.template.txt
@@ -32,6 +32,7 @@
 {% for object in generator.all() %}
 
 message {{ object.camel() }} {
+    option (contentTypeId) = {{ object.content_type_id }};
   {%- for field in object.all_fields %}
     oneof {{ field.name }}_present {
     {%- if (field.get_internal_type() == "CharField") or (field.get_internal_type() == "TextField") or (field.get_internal_type() == "SlugField") %}
@@ -61,7 +62,6 @@
     repeated int32 {{ ref.related_name }}_ids  = {{ loop.index+100 }} [(reverseForeignKey).modelName = "{{ ref.camel() }}"];
   {%- endfor %}
   string class_names = 201;
-  int32 self_content_type_id = 202;
 }
 
 message {{ object.camel() }}s {
diff --git a/xos/xos_client/tests/vtr_crud.py b/xos/xos_client/tests/vtr_crud.py
new file mode 100644
index 0000000..9b29786
--- /dev/null
+++ b/xos/xos_client/tests/vtr_crud.py
@@ -0,0 +1,42 @@
+import sys
+sys.path.append("..")
+
+from xosapi import xos_grpc_client
+
+def test_callback():
+    print "TEST: vtr_crud"
+
+    c = xos_grpc_client.coreclient
+
+    sr = c.xos_orm.CordSubscriberRoot.objects.first()
+    if not sr:
+        print "No subscriber roots!"
+        return
+
+    vt = c.xos_orm.VTRTenant.objects.new()
+    vt.target = sr
+    vt.test="ping"
+    vt.scope="vm"
+    vt.argument="8.8.8.8"
+    vt.save()
+
+    assert(vt.id is not None)
+    assert(vt.id>0)
+
+    # Check and make sure we can read it back, pay particular attention to
+    # the generic foreign key.
+    vt2 = c.xos_orm.VTRTenant.objects.get(id=vt.id)
+    assert(vt2.target_id == sr.id)
+    assert(vt2.target_type_id == sr.self_content_type_id)
+    assert("TenantRoot" in vt2.target.class_names)
+
+    vt2.delete()
+
+    # now, make sure it has been deleted
+    vt3 = c.xos_orm.VTRTenant.objects.filter(id=vt.id)
+    assert(not vt3)
+
+    print "    okay"
+
+xos_grpc_client.start_api_parseargs(test_callback)
+
diff --git a/xos/xos_client/xosapi/orm.py b/xos/xos_client/xosapi/orm.py
index 1de0693..d113835 100644
--- a/xos/xos_client/xosapi/orm.py
+++ b/xos/xos_client/xosapi/orm.py
@@ -47,12 +47,20 @@
     def gen_fkmap(self):
         fkmap = {}
 
+        all_field_names = self._wrapped_class.DESCRIPTOR.fields_by_name.keys()
+
         for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
            if name.endswith("_id"):
                foreignKey = field.GetOptions().Extensions._FindExtensionByName("xos.foreignKey")
                fk = field.GetOptions().Extensions[foreignKey]
-               if fk:
-                   fkmap[name[:-3]] = {"src_fieldName": name, "modelName": fk.modelName}
+               if fk and fk.modelName:
+                   fkmap[name[:-3]] = {"src_fieldName": name, "modelName": fk.modelName, "kind": "fk"}
+               else:
+                   # If there's a corresponding _type_id field, then see if this
+                   # is a generic foreign key.
+                   type_name = name[:-3] + "_type_id"
+                   if type_name in all_field_names:
+                       fkmap[name[:-3]] = {"src_fieldName": name, "ct_fieldName": type_name, "kind": "generic_fk"}
 
         return fkmap
 
@@ -73,8 +81,21 @@
             return make_ORMWrapper(self.cache[name], self.stub)
 
         fk_entry = self._fkmap[name]
-        id=self.stub.make_ID(id=getattr(self, fk_entry["src_fieldName"]))
-        dest_model = self.stub.invoke("Get%s" % fk_entry["modelName"], id)
+        fk_kind = fk_entry["kind"]
+        fk_id = getattr(self, fk_entry["src_fieldName"])
+
+        if not fk_id:
+            return None
+
+        if fk_kind=="fk":
+            id=self.stub.make_ID(id=fk_id)
+            dest_model = self.stub.invoke("Get%s" % fk_entry["modelName"], id)
+
+        elif fk_kind=="generic_fk":
+            dest_model = self.stub.genericForeignKeyResolve(getattr(self, fk_entry["ct_fieldName"]), fk_id)
+
+        else:
+            raise Exception("unknown fk_kind")
 
         self.cache[name] = dest_model
 
@@ -89,9 +110,13 @@
 
     def fk_set(self, name, model):
         fk_entry = self._fkmap[name]
+        fk_kind = fk_entry["kind"]
         id = model.id
         setattr(self._wrapped_class, fk_entry["src_fieldName"], id)
 
+        if fk_kind=="generic_fk":
+            setattr(self._wrapped_class, fk_entry["ct_fieldName"], model.self_content_type_id)
+
         # XXX setting the cache here is a problematic, since the cached object's
         # reverse foreign key pointers will not include the reference back
         # to this object. Instead of setting the cache, let's poison the name
@@ -173,6 +198,10 @@
     def ansible_tag(self):
         return "%s_%s" % (self._wrapped_class.__class__.__name__, self.id)
 
+    @property
+    def self_content_type_id(self):
+        return getattr(self.stub, self._wrapped_class.__class__.__name__).content_type_id
+
 class ORMQuerySet(list):
     """ Makes lists look like django querysets """
     def first(self):
@@ -281,23 +310,30 @@
             return objs[0]
 
     def new(self, **kwargs):
-        full_model_name = "%s.%s" % (self._packageName, self._modelName)
-        cls = _sym_db._classes[full_model_name]
+        cls = self._stub.all_grpc_classes[self._modelName]
         return make_ORMWrapper(cls(), self._stub, is_new=True)
 
 class ORMModelClass(object):
     def __init__(self, stub, model_name, package_name):
         self.model_name = model_name
+        self._stub = stub
         self.objects = ORMObjectManager(stub, model_name, package_name)
 
     @property
     def __name__(self):
         return self.model_name
 
+    @property
+    def content_type_id(self):
+        return self._stub.reverse_content_type_map[self.model_name]
+
 class ORMStub(object):
     def __init__(self, stub, package_name, invoker=None, caller_kind="grpcapi"):
         self.grpc_stub = stub
         self.all_model_names = []
+        self.all_grpc_classes = {}
+        self.content_type_map = {}
+        self.reverse_content_type_map = {}
         self.invoker = invoker
         self.caller_kind = caller_kind
 
@@ -308,6 +344,21 @@
 
                self.all_model_names.append(model_name)
 
+               grpc_class = _sym_db._classes["%s.%s" % (package_name, model_name)]
+               self.all_grpc_classes[model_name] = grpc_class
+
+               ct = grpc_class.DESCRIPTOR.GetOptions().Extensions._FindExtensionByName("xos.contentTypeId")
+               if ct:
+                   ct = grpc_class.DESCRIPTOR.GetOptions().Extensions[ct]
+                   if ct:
+                       self.content_type_map[ct] = model_name
+                       self.reverse_content_type_map[model_name] = ct
+
+    def genericForeignKeyResolve(self, content_type_id, id):
+        model_name = self.content_type_map[content_type_id]
+        model = getattr(self, model_name)
+        return model.objects.get(id=id)
+
     def listObjects(self):
         return self.all_model_names