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")