CORD-762 add support for update_fields

Change-Id: Ifabe918fd02be2314112543d408acb68d23cd62f
diff --git a/xos/grpc/apihelper.py b/xos/grpc/apihelper.py
index 6857439..e5ee9c6 100644
--- a/xos/grpc/apihelper.py
+++ b/xos/grpc/apihelper.py
@@ -147,7 +147,7 @@
         new_obj.save()
         return self.objToProto(new_obj)
 
-    def update(self, djangoClass, user, id, message):
+    def update(self, djangoClass, user, id, message, context):
         obj = djangoClass.objects.get(id=id)
         obj.caller = user
         if (not user) or (not obj.can_update(user)):
@@ -155,7 +155,13 @@
         args = self.protoToArgs(djangoClass, message)
         for (k,v) in args.iteritems():
             setattr(obj, k, v)
-        obj.save()
+
+        save_kwargs={}
+        for (k, v) in context.invocation_metadata():
+            if k=="update_fields":
+                save_kwargs["update_fields"] = v.split(",")
+
+        obj.save(**save_kwargs)
         return self.objToProto(obj)
 
     def delete(self, djangoClass, user, id):
diff --git a/xos/tools/apigen/grpc_api.template.py b/xos/tools/apigen/grpc_api.template.py
index 84693e1..b8dcc59 100644
--- a/xos/tools/apigen/grpc_api.template.py
+++ b/xos/tools/apigen/grpc_api.template.py
@@ -44,7 +44,7 @@
     def Update{{ object.camel() }}(self, request, context):
       user=self.authenticate(context)
       model=self.get_model("{{ object.camel() }}")
-      return self.update(model, user, request.id, request)
+      return self.update(model, user, request.id, request, context)
 
 {% endfor %}
 
diff --git a/xos/xos_client/tests/orm_user_crud.py b/xos/xos_client/tests/orm_user_crud.py
index 45bda8f..875c08e 100644
--- a/xos/xos_client/tests/orm_user_crud.py
+++ b/xos/xos_client/tests/orm_user_crud.py
@@ -49,8 +49,20 @@
     u3 = c.xos_orm.User.objects.get(id=orig_id)
     assert(u3.password=="foobar")
 
+    # try a partial update
+    u3.password = "should_not_change"
+    u3.firstname = "new_first_name"
+    u3.lastname = "new_last_name"
+    u3.save(update_fields = ["firstname", "lastname"])
+
+    # get and make sure the password was not updated, but first and last name were
+    u4 = c.xos_orm.User.objects.get(id=orig_id)
+    assert(u4.password=="foobar")
+    assert(u4.firstname == "new_first_name")
+    assert(u4.lastname == "new_last_name")
+
     # delete the user
-    u3.delete()
+    u4.delete()
 
     # make sure it is deleted
     u_all = c.xos_orm.User.objects.all()
diff --git a/xos/xos_client/xosapi/orm.py b/xos/xos_client/xosapi/orm.py
index d23c1b1..1808e08 100644
--- a/xos/xos_client/xosapi/orm.py
+++ b/xos/xos_client/xosapi/orm.py
@@ -140,13 +140,16 @@
             self.reverse_cache.clear()
             self.poisoned.clear()
 
-    def save(self):
+    def save(self, update_fields=None):
         if self.is_new:
            new_class = self.stub.invoke("Create%s" % self._wrapped_class.__class__.__name__, self._wrapped_class)
            self._wrapped_class = new_class
            self.is_new = False
         else:
-           self.stub.invoke("Update%s" % self._wrapped_class.__class__.__name__, self._wrapped_class)
+           metadata = []
+           if update_fields:
+               metadata.append( ("update_fields", ",".join(update_fields)) )
+           self.stub.invoke("Update%s" % self._wrapped_class.__class__.__name__, self._wrapped_class, metadata=metadata)
 
     def delete(self):
         id = self.stub.make_ID(id=self._wrapped_class.id)
@@ -263,7 +266,7 @@
     def listObjects(self):
         return self.all_model_names
 
-    def invoke(self, name, request):
+    def invoke(self, name, request, metadata=[]):
         if self.invoker:
             # Hook in place to call Chameleon's invoke method, as soon as we
             # have rewritten the synchronizer to use reactor.
@@ -275,7 +278,7 @@
                 backoff = [0.5, 1, 2, 4, 8]
                 try:
                     method = getattr(self.grpc_stub, name)
-                    return method(request)
+                    return method(request, metadata=metadata)
                 except grpc._channel._Rendezvous, e:
                     code = e.code()
                     if code == grpc.StatusCode.UNAVAILABLE:
@@ -294,14 +297,4 @@
         return _sym_db._classes["xos.Query"]()
 
 
-#def wrap_get(*args, **kwargs):
-#    stub=kwargs.pop("stub")
-#    getmethod=kwargs.pop("getmethod")
-#    result = getmethod(*args, **kwargs)
-#    return ORMWrapper(result)
-#
-#def wrap_stub(stub):
-#    for name in dir(stub):
-#        if name.startswith("Get"):
-#            setattr(stub, name, functools.partial(wrap_get, stub=stub, getmethod=getattr(stub,name)))