CORD-3001 enforce blank=False when saving models

Change-Id: Ic88268cf8e8a7caa91b8e9898cbe2f19ea3eee7d
diff --git a/xos/core/models/core.xproto b/xos/core/models/core.xproto
index 2ba61eb..caec9c0 100644
--- a/xos/core/models/core.xproto
+++ b/xos/core/models/core.xproto
@@ -92,10 +92,10 @@
 
 message Privilege::grant_policy (XOSBase) {
      required int32 accessor_id = 1 [null = False, blank=False];
-     required string accessor_type = 2 [null = False, max_length=1024];
-     required int32 controller_id = 3 [null = True];
+     required string accessor_type = 2 [null = False, max_length=1024, blank = False];
+     optional int32 controller_id = 3 [null = True, blank = True];
      required int32 object_id = 4 [null = False, blank=False];
-     required string object_type = 5 [null = False, max_length=1024];
+     required string object_type = 5 [null = False, max_length=1024, blank = False];
      required string permission = 6 [null = False, default = "all", max_length=1024, tosca_key=True];
      required string granted = 7 [content_type = "date", auto_now_add = True, max_length=1024];
      required string expires = 8 [content_type = "date", null = True, max_length=1024];
@@ -262,15 +262,15 @@
 message Flavor (XOSBase) {
      required string name = 1 [max_length = 32, content_type = "stripped", blank = False, help_text = "name of this flavor, as displayed to users", null = False, db_index = False, unique = True];
      optional string description = 2 [db_index = False, max_length = 1024, null = True, content_type = "stripped", blank = True];
-     required string flavor = 3 [max_length = 32, content_type = "stripped", blank = False, help_text = "flavor string used to configure deployments", null = False, db_index = False];
+     required string flavor = 3 [max_length = 32, content_type = "stripped", blank = True, help_text = "flavor string used to configure deployments", null = False, db_index = False];
 }
 
 
 message Image (XOSBase) {
      required string name = 1 [db_index = False, max_length = 256, null = False, content_type = "stripped", blank = False, unique = True];
      required string kind = 2 [default = "vm", choices = "(('vm', 'Virtual Machine'), ('container', 'Container'))", max_length = 30, blank = False, null = False, db_index = False];
-     required string disk_format = 3 [db_index = False, max_length = 256, null = False, content_type = "stripped", blank = False];
-     required string container_format = 4 [db_index = False, max_length = 256, null = False, content_type = "stripped", blank = False];
+     optional string disk_format = 3 [db_index = False, max_length = 256, null = True, content_type = "stripped", blank = True];
+     optional string container_format = 4 [db_index = False, max_length = 256, null = True, content_type = "stripped", blank = True];
      optional string path = 5 [max_length = 256, content_type = "stripped", blank = True, help_text = "Path to image on local disk", null = True, db_index = False];
      optional string tag = 6 [max_length = 256, content_type = "stripped", blank = True, help_text = "For Docker Images, tag of image", null = True, db_index = False];
 }
@@ -341,7 +341,7 @@
 
 message NetworkParameterType (XOSBase) {
      required string name = 1 [help_text = "The name of this parameter", max_length = 128, null = False, db_index = True, blank = False, unique = True];
-     required string description = 2 [db_index = False, max_length = 1024, null = False, blank = False];
+     required string description = 2 [db_index = False, max_length = 1024, null = False, blank = True];
 }
 
 policy network_slice_validator < (obj.slice in obj.network.permitted_slices.all()) | (obj.slice = obj.network.owner) | obj.network.permit_all_slices >
@@ -371,10 +371,10 @@
 message Node::node_policy (XOSBase) {
      required string name = 1 [max_length = 200, content_type = "stripped", blank = False, help_text = "Name of the Node", null = False, db_index = False, unique = True];
      required manytoone site_deployment->SiteDeployment:nodes = 2 [db_index = True, null = False, blank = False];
-     required string bridgeId = 3 [max_length = 200, content_type = "stripped", blank = False, help_text = "Bridge Id", null = False, db_index = False];
-     required string dataPlaneIntf = 4 [max_length = 200, content_type = "stripped", blank = False, help_text = "Dataplane Interface", null = False, db_index = False];
-     required string dataPlaneIp = 5 [max_length = 200, content_type = "stripped", blank = True, help_text = "Dataplane Ip", null = True, db_index = False];
-     required string hostManagementIface = 6 [max_length = 200, content_type = "stripped", blank = True, help_text = "Host Management Interface", null = True, db_index = False];
+     optional string bridgeId = 3 [max_length = 200, content_type = "stripped", blank = True, help_text = "Bridge Id", null = True, db_index = False];
+     optional string dataPlaneIntf = 4 [max_length = 200, content_type = "stripped", blank = True, help_text = "Dataplane Interface", null = True, db_index = False];
+     optional string dataPlaneIp = 5 [max_length = 200, content_type = "stripped", blank = True, help_text = "Dataplane Ip", null = True, db_index = False];
+     optional string hostManagementIface = 6 [max_length = 200, content_type = "stripped", blank = True, help_text = "Host Management Interface", null = True, db_index = False];
 }
 message NodeLabel (XOSBase) {
      required string name = 1 [max_length = 200, content_type = "stripped", blank = False, help_text = "label name", null = False, db_index = False, unique = True];
diff --git a/xos/core/models/xosbase.py b/xos/core/models/xosbase.py
index 9ef96a4..8ccb2b8 100644
--- a/xos/core/models/xosbase.py
+++ b/xos/core/models/xosbase.py
@@ -156,6 +156,14 @@
                 if not (field in ["backend_register", "backend_status", "deleted", "enacted", "updated"]):
                     ignore_composite_key_check=False
 
+        # Django only enforces field.blank=False during form validation. We'd like it to be enforced when saving the
+        # model.
+        for field in self._meta.fields:
+            if field.get_internal_type() == "CharField":
+                if getattr(field, "blank", None)==False:
+                    if getattr(self, field.name) == "":
+                        raise XOSValidationError("Blank is not allowed on field %s" % field.name)
+
         if (caller_kind!="synchronizer") or always_update_timestamp:
             self.updated = timezone.now()
         else: