add API to list dirty models and set dirty models

Change-Id: Iea1e67e0cb24845ea329121fa0270f0b2fc21696
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index cd28553..5eb5bda 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -220,9 +220,11 @@
     kind = models.CharField(max_length=20, choices=LINK_KIND, default='internal')
 
     def save(self, *args, **kwds):
-        existing = XOSComponentLink.objects.filter(container=self.container, alias=self.alias)
-        if len(existing) > 0:
-            raise XOSValidationError('XOSComponentLink for %s:%s already defined' % (self.container, self.alias))
+        # If this is a new object, then check to make sure it doesn't already exist
+        if not self.pk:
+            existing = XOSComponentLink.objects.filter(container=self.container, alias=self.alias)
+            if len(existing) > 0:
+                raise XOSValidationError('XOSComponentLink for %s:%s already defined' % (self.container, self.alias))
         super(XOSComponentLink, self).save(*args, **kwds)
 
 
@@ -235,9 +237,11 @@
     read_only = models.BooleanField(default=False, help_text="True if mount read-only")
 
     def save(self, *args, **kwds):
-        existing = XOSComponentVolume.objects.filter(container_path=self.container_path, host_path=self.host_path)
-        if len(existing) > 0:
-            raise XOSValidationError('XOSComponentVolume for %s:%s already defined' % (self.container_path, self.host_path))
+        # If this is a new object, then check to make sure it doesn't already exist
+        if not self.pk:
+            existing = XOSComponentVolume.objects.filter(container_path=self.container_path, host_path=self.host_path)
+            if len(existing) > 0:
+                raise XOSValidationError('XOSComponentVolume for %s:%s already defined' % (self.container_path, self.host_path))
         super(XOSComponentVolume, self).save(*args, **kwds)
 
 
@@ -247,9 +251,11 @@
     container = StrippedCharField(max_length=300, help_text="Volume Name")
 
     def save(self, *args, **kwds):
-        existing = XOSComponentVolumeContainer.objects.filter(name=self.name)
-        if len(existing) > 0:
-            raise XOSValidationError('XOSComponentVolumeContainer for %s:%s already defined' % (self.container_path, self.host_path))
+        # If this is a new object, then check to make sure it doesn't already exist
+        if not self.pk:
+            existing = XOSComponentVolumeContainer.objects.filter(name=self.name)
+            if len(existing) > 0:
+                raise XOSValidationError('XOSComponentVolumeContainer for %s:%s already defined' % (self.container_path, self.host_path))
         super(XOSComponentVolumeContainer, self).save(*args, **kwds)
 
 
diff --git a/xos/coreapi/protos/utility.proto b/xos/coreapi/protos/utility.proto
index 6d5f18f..d899e41 100644
--- a/xos/coreapi/protos/utility.proto
+++ b/xos/coreapi/protos/utility.proto
@@ -28,6 +28,20 @@
     string messages = 2;
 };
 
+message ModelFilter {
+    string class_name = 1;
+};
+
+message ModelListEntry {
+    string class_name = 1;
+    int32 id = 2;
+    string info = 3;
+};
+
+message ModelList {
+    repeated ModelListEntry items = 1;
+};
+
 service utility {
 
   rpc Login(LoginRequest) returns (LoginResponse) {
@@ -64,4 +78,17 @@
             body: "*"
         };
   }
+
+  rpc SetDirtyModels(ModelFilter) returns (ModelList) {
+        option (google.api.http) = {
+            post: "/xosapi/v1/utility/dirty_models"
+            body: "*"
+        };
+  }
+
+  rpc ListDirtyModels(ModelFilter) returns (ModelList) {
+        option (google.api.http) = {
+            get: "/xosapi/v1/utility/dirty_models"
+        };
+  }
 };
diff --git a/xos/coreapi/xos_utility_api.py b/xos/coreapi/xos_utility_api.py
index 7377c91..937bdaf 100644
--- a/xos/coreapi/xos_utility_api.py
+++ b/xos/coreapi/xos_utility_api.py
@@ -1,4 +1,5 @@
 import base64
+import fnmatch
 import os
 import sys
 import time
@@ -8,10 +9,11 @@
 
 from importlib import import_module
 from django.conf import settings

-SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
+SessionStore = import_module(settings.SESSION_ENGINE).SessionStore

 
 from django.contrib.auth import authenticate as django_authenticate
 import django.apps
+from django.db.models import F,Q
 from core.models import *
 from xos.exceptions import *
 from apihelper import XOSAPIHelperMixin
@@ -22,6 +24,20 @@
 currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
 toscadir = os.path.join(currentdir, "../tosca")
 
+def is_internal_model(model):
+    """ things to be excluded from the dirty_models endpoints """
+    if 'django' in model.__module__:
+        return True
+    if 'cors' in model.__module__:
+        return True
+    if 'contenttypes' in model.__module__:
+        return True
+    if 'core.models.journal' in model.__module__:  # why?
+        return True
+    if 'core.models.project' in model.__module__:  # why?
+        return True
+    return False
+
 class UtilityService(utility_pb2.utilityServicer, XOSAPIHelperMixin):
     def __init__(self, thread_pool):
         self.thread_pool = thread_pool
@@ -107,3 +123,50 @@
     def NoOp(self, request, context):
         return Empty()
 
+    def ListDirtyModels(self, request, context):
+        dirty_models = utility_pb2.ModelList()
+
+        models = django.apps.apps.get_models()
+        for model in models:
+            if is_internal_model(model):
+                continue
+            fieldNames = [x.name for x in model._meta.fields]
+            if (not "enacted" in fieldNames) or (not "updated" in fieldNames):
+                continue
+            if (request.class_name) and (not fnmatch.fnmatch(model.__name__, request.class_name)):
+                continue
+            objs = model.objects.filter(Q(enacted__lt=F('updated')) | Q(enacted=None))
+            for obj in objs:
+                item = dirty_models.items.add()
+                item.class_name = model.__name__
+                item.id = obj.id
+
+        return dirty_models
+
+    def SetDirtyModels(self, request, context):
+        user=self.authenticate(context, required=True)
+
+        dirty_models = utility_pb2.ModelList()
+
+        models = django.apps.apps.get_models()
+        for model in models:
+            if is_internal_model(model):
+                continue
+            fieldNames = [x.name for x in model._meta.fields]
+            if (not "enacted" in fieldNames) or (not "updated" in fieldNames):
+                continue
+            if (request.class_name) and (not fnmatch.fnmatch(model.__name__, request.class_name)):
+                continue
+            objs = model.objects.all()
+            for obj in objs:
+                try:
+                     obj.caller = user
+                     obj.save()
+                except Exception, e:
+                    item = dirty_models.items.add()
+                    item.class_name = model.__name__
+                    item.id = obj.id
+                    item.info = str(e)
+
+        return dirty_models
+
diff --git a/xos/xos_client/xosapi/xos_grpc_client.py b/xos/xos_client/xosapi/xos_grpc_client.py
index aaaa97f..d0d1f19 100644
--- a/xos/xos_client/xosapi/xos_grpc_client.py
+++ b/xos/xos_client/xosapi/xos_grpc_client.py
@@ -66,6 +66,7 @@
                 stub_class = getattr(m_grpc, api+"Stub")
 
                 setattr(self, api, stub_class(self.channel))
+                setattr(self, api+"_pb2", m_protos)
             else:
                 print >> sys.stderr, "failed to locate api", api