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