CORD-762 add modeldefs API

Change-Id: Ieed10ec8d40533906c8956d2b8c2ea62709bd8b4
diff --git a/xos/grpc/grpc_client.py b/xos/grpc/grpc_client.py
index 52f7f90..8defd42 100644
--- a/xos/grpc/grpc_client.py
+++ b/xos/grpc/grpc_client.py
@@ -2,7 +2,7 @@
 import grpc
 from protos.common_pb2 import *
 from protos.xos_pb2 import *
-from protos import xos_pb2_grpc
+from protos import xos_pb2_grpc, modeldefs_pb2_grpc
 from google.protobuf.empty_pb2 import Empty
 from grpc import metadata_call_credentials, ChannelCredentials, composite_channel_credentials, ssl_channel_credentials
 
@@ -28,6 +28,7 @@
         super(InsecureClient,self).__init__(hostname, port)
         self.channel = grpc.insecure_channel("%s:%d" % (self.hostname, self.port))
         self.stub = xos_pb2_grpc.xosStub(self.channel)
+        self.modeldefs = modeldefs_pb2_grpc.modeldefsStub(self.channel)
 
 class SecureClient(XOSClient):
     def __init__(self, hostname, port=50051, cacert=SERVER_CA, username=None, password=None):
@@ -40,6 +41,7 @@
 
         self.channel = grpc.secure_channel("%s:%d" % (self.hostname, self.port), chan_creds)
         self.stub = xos_pb2_grpc.xosStub(self.channel)
+        self.modeldefs = modeldefs_pb2_grpc.modeldefsStub(self.channel)
 
 def main():  # self-test
     client = InsecureClient("xos-core.cord.lab")
diff --git a/xos/grpc/grpc_server.py b/xos/grpc/grpc_server.py
index ec7cc9e..d231bfc 100644
--- a/xos/grpc/grpc_server.py
+++ b/xos/grpc/grpc_server.py
@@ -29,8 +29,9 @@
 sys.path.append('/opt/xos')
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
 
-from protos import xos_pb2, schema_pb2
+from protos import xos_pb2, schema_pb2, modeldefs_pb2
 from xos_grpc_api import XosService
+from xos_modeldefs_api import ModelDefsService
 from google.protobuf.empty_pb2 import Empty
 
 from xos.logger import Logger, logging
@@ -116,6 +117,7 @@
         for activator_func, service_class in (
             (schema_pb2.add_SchemaServiceServicer_to_server, SchemaService),
             (xos_pb2.add_xosServicer_to_server, XosService),
+            (modeldefs_pb2.add_modeldefsServicer_to_server, ModelDefsService),
         ):
             service = service_class(self.thread_pool)
             self.register(activator_func, service)
diff --git a/xos/grpc/protos/modeldefs.proto b/xos/grpc/protos/modeldefs.proto
new file mode 100644
index 0000000..d738c49
--- /dev/null
+++ b/xos/grpc/protos/modeldefs.proto
@@ -0,0 +1,50 @@
+syntax = "proto3";
+
+package xos;
+
+import "google/protobuf/empty.proto";
+import "google/api/annotations.proto";
+import "common.proto";
+
+// This API is used by the UI to validate fields.
+
+message FieldValidator {
+    string name = 1;
+    oneof val_value {
+        bool bool_value = 2;
+        int32 int_value = 3;
+        string str_value = 4;
+    };
+};
+
+message FieldRelation {
+    string model = 1;
+    string type = 2;
+};
+
+message ModelField {
+    string name = 1;
+    string hint = 2;
+    string type = 3;
+    FieldRelation relation = 4;
+    repeated FieldValidator validators = 5;
+};
+
+message ModelDef {
+    string name = 1;
+    repeated ModelField fields = 2;
+    repeated FieldRelation relations = 3;
+};
+
+message ModelDefs {
+    repeated ModelDef items = 1;
+};
+
+service modeldefs {
+
+  rpc ListModelDefs(google.protobuf.Empty) returns (ModelDefs) {
+        option (google.api.http) = {
+            get: "/api/v1/modeldefs"
+        };
+  }
+};
diff --git a/xos/grpc/xos_grpc_api.py b/xos/grpc/xos_grpc_api.py
index cf98031..a9ed79d 100644
--- a/xos/grpc/xos_grpc_api.py
+++ b/xos/grpc/xos_grpc_api.py
@@ -6,160 +6,15 @@
 from django.contrib.auth import authenticate as django_authenticate
 from core.models import *
 from xos.exceptions import *
+from apihelper import XOSAPIHelperMixin
 
-class XosService(xos_pb2.xosServicer):
+class XosService(xos_pb2.xosServicer, XOSAPIHelperMixin):
     def __init__(self, thread_pool):
         self.thread_pool = thread_pool
 
     def stop(self):
         pass
 
-    def getProtoClass(self, djangoClass):
-        pClass = getattr(xos_pb2, djangoClass.__name__)
-        return pClass
-
-    def getPluralProtoClass(self, djangoClass):
-        pClass = getattr(xos_pb2, djangoClass.__name__ + "s")
-        return pClass
-
-    def convertFloat(self, x):
-        if not x:
-            return 0
-        else:
-            return float(x)
-
-    def convertDateTime(self, x):
-        if not x:
-            return 0
-        else:
-            return time.mktime(x.timetuple())
-
-    def convertForeignKey(self, x):
-        if not x:
-            return 0
-        else:
-            return int(x.id)
-
-    def objToProto(self, obj):
-        p_obj = self.getProtoClass(obj.__class__)()
-        for field in obj._meta.fields:
-            if getattr(obj, field.name) == None:
-                continue
-
-            ftype = field.get_internal_type()
-            if (ftype == "CharField") or (ftype == "TextField") or (ftype == "SlugField"):
-                setattr(p_obj, field.name, str(getattr(obj, field.name)))
-            elif (ftype == "BooleanField"):
-                setattr(p_obj, field.name, getattr(obj, field.name))
-            elif (ftype == "AutoField"):
-                setattr(p_obj, field.name, int(getattr(obj, field.name)))
-            elif (ftype == "IntegerField") or (ftype == "PositiveIntegerField"):
-                setattr(p_obj, field.name, int(getattr(obj, field.name)))
-            elif (ftype == "ForeignKey"):
-                setattr(p_obj, field.name+"_id", self.convertForeignKey(getattr(obj, field.name)))
-            elif (ftype == "DateTimeField"):
-                setattr(p_obj, field.name, self.convertDateTime(getattr(obj, field.name)))
-            elif (ftype == "FloatField"):
-                setattr(p_obj, field.name, float(getattr(obj, field.name)))
-            elif (ftype == "GenericIPAddressField"):
-                setattr(p_obj, field.name, str(getattr(obj, field.name)))
-        return p_obj
-
-    def protoToArgs(self, djangoClass, message):
-        args={}
-        fmap={}
-        fset={}
-        for field in djangoClass._meta.fields:
-            fmap[field.name] = field
-            if field.get_internal_type() == "ForeignKey":
-               # foreign key can be represented as an id
-               fmap[field.name + "_id"] = field
-
-        for (fieldDesc, val) in message.ListFields():
-            name = fieldDesc.name
-            if name in fmap:
-                if (name=="id"):
-                    # don't let anyone set the id
-                    continue
-                ftype = fmap[name].get_internal_type()
-                if (ftype == "CharField") or (ftype == "TextField") or (ftype == "SlugField"):
-                    args[name] = val
-                elif (ftype == "BooleanField"):
-                    args[name] = val
-                elif (ftype == "AutoField"):
-                    args[name] = val
-                elif (ftype == "IntegerField") or (ftype == "PositiveIntegerField"):
-                    args[name] = val
-                elif (ftype == "ForeignKey"):
-                    args[name] = val # field name already has "_id" at the end
-                elif (ftype == "DateTimeField"):
-                    pass # do something special here
-                elif (ftype == "FloatField"):
-                    args[name] = val
-                elif (ftype == "GenericIPAddressField"):
-                    args[name] = val
-                fset[name] = True
-
-        return args
-
-    def querysetToProto(self, djangoClass, queryset):
-        objs = queryset
-        p_objs = self.getPluralProtoClass(djangoClass)()
-
-        for obj in objs:
-           new_obj = p_objs.items.add()
-           new_obj.CopyFrom(self.objToProto(obj))
-
-        return p_objs
-
-    def get(self, djangoClass, id):
-        obj = djangoClass.objects.get(id=id)
-        return self.objToProto(obj)
-
-    def create(self, djangoClass, user, request):
-        args = self.protoToArgs(djangoClass, request)
-        new_obj = djangoClass(**args)
-        new_obj.caller = user
-        if (not user) or (not new_obj.can_update(user)):
-            raise XOSPermissionDenied()
-        new_obj.save()
-        return self.objToProto(new_obj)
-
-    def update(self, djangoClass, user, id, message):
-        obj = djangoClass.objects.get(id=id)
-        obj.caller = user
-        if (not user) or (not obj.can_update(user)):
-            raise XOSPermissionDenied()
-        args = self.protoToArgs(djangoClass, message)
-        for (k,v) in args.iteritems():
-            setattr(obj, k, v)
-        obj.save()
-        return self.objToProto(obj)
-
-    def delete(self, djangoClass, user, id):
-      obj = djangoClass.objects.get(id=id)
-      if (not user) or (not obj.can_update(user)):
-          raise XOSPermissionDenied()
-      obj.delete()
-      return Empty()
-
-    def authenticate(self, context):
-        for (k, v) in context.invocation_metadata():
-            if (k.lower()=="authorization"):
-                (method, auth) = v.split(" ",1)
-                if (method.lower() == "basic"):
-                    auth = base64.b64decode(auth)
-                    (username, password) = auth.split(":")
-                    user = django_authenticate(username=username, password=password)
-                    if not user:
-                        raise Exception("failed to authenticate %s:%s" % (username, password))
-                    print "authenticated %s:%s as %s" % (username, password, user)
-                    return user
-
-        return None
-
-
-
     def ListServiceControllerResource(self, request, context):
       user=self.authenticate(context)
       return self.querysetToProto(ServiceControllerResource, ServiceControllerResource.objects.all())
diff --git a/xos/grpc/xos_modeldefs_api.py b/xos/grpc/xos_modeldefs_api.py
new file mode 100644
index 0000000..b918683
--- /dev/null
+++ b/xos/grpc/xos_modeldefs_api.py
@@ -0,0 +1,119 @@
+import base64
+import time
+from protos import modeldefs_pb2
+from google.protobuf.empty_pb2 import Empty
+
+from django.contrib.auth import authenticate as django_authenticate
+import django.apps
+from core.models import *
+from xos.exceptions import *
+from apihelper import XOSAPIHelperMixin
+
+class ModelDefsService(modeldefs_pb2.modeldefsServicer, XOSAPIHelperMixin):
+    def __init__(self, thread_pool):
+        self.thread_pool = thread_pool
+
+    def stop(self):
+        pass
+
+    typeMap = {
+        'BooleanField': 'boolean',
+        'TextField': 'text',
+        'CharField': 'string',
+        'ForeignKey': 'number',
+        'IntegerField': 'number',
+        'AutoField': 'number',
+        'DateTimeField': 'date'
+    }
+
+    validatorMap = {
+        'EmailValidator': 'email',
+        'MaxLengthValidator': 'maxlength',
+        'URLValidator': 'url',
+        'MinValueValidator': 'min',
+        'MaxValueValidator': 'max',
+        'validate_ipv46_address': 'ip'
+    }
+
+    def convertType(self, type):
+        try:
+            jsType = self.typeMap[type]
+            return jsType
+        except Exception:
+            return None
+
+    def convertValidator(self, validator):
+        try:
+            jsValidator = self.validatorMap[validator]
+            return jsValidator
+        except Exception:
+            return None
+
+    def getRelationType(self, field):
+        if (field.many_to_many):
+            return 'many_to_many'
+        if (field.many_to_one):
+            return 'many_to_one'
+        if (field.one_to_many):
+            return 'one_to_many'
+        if (field.one_to_one):
+            return 'one_to_one'
+
+    def ListModelDefs(self, request, context):
+        models = django.apps.apps.get_models()
+
+        modeldefs = modeldefs_pb2.ModelDefs();
+
+        response = []
+
+        for model in models:
+            if 'core' in model.__module__:
+                modeldef = modeldefs.items.add()
+
+                modeldef.name = model.__name__
+
+                for f in model._meta.fields:
+                    field = modeldef.fields.add()
+
+                    field.name = f.name
+                    field.hint = f.help_text
+
+                    fieldtype = self.convertType(f.get_internal_type())
+                    if fieldtype is not None:
+                        field.type = fieldtype
+                    else:
+                        field.type = 'string'
+
+                    if not f.blank and not f.null:
+                        val = field.validators.add()
+                        val.name = "required"
+                        val.bool_value = True
+
+                    for v in f.validators:
+                        val = field.validators.add()
+                        validator_name = v.__class__.__name__
+                        if 'function' in validator_name:
+                            validator_name = v.__name__
+                        validator_name = self.convertValidator(validator_name)
+
+                        if not validator_name:
+                            continue
+
+                        val.name = validator_name
+                        if hasattr(v, 'limit_value'):
+                            try:
+                                val.int_value = v.limit_value
+                            except TypeError:
+                                val.str_value = str(v.limit_value)
+                        else:
+                            val.bool_value = True
+
+                    if f.is_relation and f.related_model:
+                        field.relation.model = f.related_model.__name__
+                        field.relation.type = self.getRelationType(f)
+
+                        rel = modeldef.relations.add()
+                        rel.model = f.related_model.__name__
+                        rel.type = self.getRelationType(f)
+        return modeldefs
+