CORD-762 add filter API
Change-Id: I44f3e4e58826cf680a43ae81a25cf8057b7c70c7
diff --git a/xos/grpc/apihelper.py b/xos/grpc/apihelper.py
index 6ce81b6..6857439 100644
--- a/xos/grpc/apihelper.py
+++ b/xos/grpc/apihelper.py
@@ -4,6 +4,7 @@
from google.protobuf.empty_pb2 import Empty
from django.contrib.auth import authenticate as django_authenticate
+from django.db.models import F,Q
from core.models import *
from xos.exceptions import *
@@ -164,6 +165,47 @@
obj.delete()
return Empty()
+ def query_element_to_q(self, element):
+ value = element.sValue
+ if element.HasField("iValue"):
+ value = element.iValue
+ elif element.HasField("sValue"):
+ value = element.sValue
+ else:
+ raise Exception("must specify iValue or sValue")
+
+ if element.operator == element.EQUAL:
+ q = Q(**{element.name: value})
+ elif element.operator == element.LESS_THAN:
+ q = Q(**{element.name + "__lt": value})
+ elif element.operator == element.LESS_THAN_OR_EQUAL:
+ q = Q(**{element.name + "__lte": value})
+ elif element.operator == element.GREATER_THAN:
+ q = Q(**{element.name + "__gt": value})
+ elif element.operator == element.GREATER_THAN_OR_EQUAL:
+ q = Q(**{element.name + "__gte": value})
+ else:
+ raise Exception("unknown operator")
+
+ if element.invert:
+ q = ~q
+
+ return q
+
+ def filter(self, djangoClass, request):
+ query = None
+ if request.kind == request.DEFAULT:
+ for element in request.elements:
+ if query:
+ query = query & self.query_element_to_q(element)
+ else:
+ query = self.query_element_to_q(element)
+ elif request.kind == request.SYNCHRONIZER_DIRTY_OBJECTS:
+ query = (Q(enacted__lt=F('updated')) | Q(enacted=None)) & Q(lazy_blocked=False) &Q(no_sync=False)
+ elif request.kind == request.ALL:
+ return self.querysetToProto(djangoClass, djangoClass.objects.all())
+ return self.querysetToProto(djangoClass, djangoClass.objects.filter(query))
+
def authenticate(self, context, required=False):
for (k, v) in context.invocation_metadata():
if (k.lower()=="authorization"):
diff --git a/xos/grpc/protos/common.proto b/xos/grpc/protos/common.proto
index 2b6a8e3..fbe6563 100644
--- a/xos/grpc/protos/common.proto
+++ b/xos/grpc/protos/common.proto
@@ -5,3 +5,31 @@
message ID {
int32 id = 1;
}
+
+message QueryElement {
+ enum QueryOperator {
+ EQUAL = 0;
+ GREATER_THAN = 1;
+ LESS_THAN = 2;
+ GREATER_THAN_OR_EQUAL = 3;
+ LESS_THAN_OR_EQUAL = 4;
+ }
+ QueryOperator operator = 1;
+ bool invert = 2;
+ string name = 3;
+ oneof value {
+ string sValue = 4;
+ int32 iValue = 5;
+ }
+};
+
+message Query {
+ enum QueryKind {
+ DEFAULT=0;
+ ALL=1;
+ SYNCHRONIZER_DIRTY_OBJECTS = 2;
+ }
+ QueryKind kind = 1;
+ repeated QueryElement elements = 2;
+};
+
diff --git a/xos/tools/apigen/grpc_api.template.py b/xos/tools/apigen/grpc_api.template.py
index fccec40..84693e1 100644
--- a/xos/tools/apigen/grpc_api.template.py
+++ b/xos/tools/apigen/grpc_api.template.py
@@ -21,6 +21,11 @@
model=self.get_model("{{ object.camel() }}")
return self.querysetToProto(model, model.objects.all())
+ def Filter{{ object.camel() }}(self, request, context):
+ user=self.authenticate(context)
+ model=self.get_model("{{ object.camel() }}")
+ return self.filter(model, request)
+
def Get{{ object.camel() }}(self, request, context):
user=self.authenticate(context)
model=self.get_model("{{ object.camel() }}")
diff --git a/xos/tools/apigen/protobuf.template.txt b/xos/tools/apigen/protobuf.template.txt
index 75d0b3c..91f76da 100644
--- a/xos/tools/apigen/protobuf.template.txt
+++ b/xos/tools/apigen/protobuf.template.txt
@@ -75,6 +75,8 @@
get: "/xosapi/v1/{{ object.app_name }}/{{ object.plural() }}"
};
}
+ rpc Filter{{ object.camel() }}(Query) returns ({{ object.camel() }}s) {
+ }
rpc Get{{ object.camel() }}(ID) returns ({{ object.camel() }}) {
option (google.api.http) = {
get: "/xosapi/v1/{{ object.app_name }}/{{ object.plural() }}/{id}"
diff --git a/xos/xos_client/xosapi/orm.py b/xos/xos_client/xosapi/orm.py
index 690fcf9..96de59d 100644
--- a/xos/xos_client/xosapi/orm.py
+++ b/xos/xos_client/xosapi/orm.py
@@ -178,6 +178,9 @@
class ORMObjectManager(object):
""" Manages a remote list of objects """
+ # constants better agree with common.proto
+ SYNCHRONIZER_DIRTY_OBJECTS = 2;
+
def __init__(self, stub, modelName, packageName):
self._stub = stub
self._modelName = modelName
@@ -195,6 +198,41 @@
def all(self):
return self.wrap_list(self._stub.invoke("List%s" % self._modelName, Empty()))
+ def filter(self, **kwargs):
+ q = self._stub.make_Query()
+ q.kind = q.DEFAULT
+
+ for (name, val) in kwargs.items():
+ el = q.elements.add()
+
+ if name.endswith("__gt"):
+ name = name[:-4]
+ el.operator = el.GREATER_THAN
+ elif name.endswith("__gte"):
+ name = name[:-5]
+ el.operator = el.GREATER_THAN_OR_EQUAL
+ elif name.endswith("__lt"):
+ name = name[:-4]
+ el.operator = el.LESS_THAN
+ elif name.endswith("__lte"):
+ name = name[:-5]
+ el.operator = el.LESS_THAN_OR_EQUAL
+ else:
+ el.operator = el.EQUAL
+
+ el.name = name
+ if isinstance(val, int):
+ el.iValue = val
+ else:
+ el.sValue = val
+
+ return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
+
+ def filter_special(self, kind):
+ q = self._stub.make_Query()
+ q.kind = kind
+ return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
+
def get(self, id):
return self.wrap_single(self._stub.invoke("Get%s" % self._modelName, self._stub.make_ID(id=id)))
@@ -229,6 +267,9 @@
def make_ID(self, id):
return _sym_db._classes["xos.ID"](id=id)
+ def make_Query(self):
+ return _sym_db._classes["xos.Query"]()
+
#def wrap_get(*args, **kwargs):
# stub=kwargs.pop("stub")