[SEBA-126] Adding xproto support for min and max validators

Change-Id: I6141c678d88a894db2a86132bdbad4e9c6b31b2f
diff --git a/.gitignore b/.gitignore
index 1235703..7da3feb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
 profile
 *.moved-aside
 .idea
+.vscode
 setup/*
 setup/id_rsa/*
 xos/configurations/frontend/Dockerfile
diff --git a/docs/dev/xproto.md b/docs/dev/xproto.md
index a8787c2..8b10863 100644
--- a/docs/dev/xproto.md
+++ b/docs/dev/xproto.md
@@ -266,6 +266,13 @@
 option verbose_name = “Verbose name goes here”;
 ```
 
+A min/max value for the field
+
+```protobuf
+option min_value = 10;
+option max_value = 100;
+```
+
 Do not display this field in the GUI (also available at the model level):
 
 ```protobuf
diff --git a/lib/xos-genx/xos-genx-tests/test_django_generator.py b/lib/xos-genx/xos-genx-tests/test_django_generator.py
index c3ffef5..d2b3c8a 100644
--- a/lib/xos-genx/xos-genx-tests/test_django_generator.py
+++ b/lib/xos-genx/xos-genx-tests/test_django_generator.py
@@ -95,10 +95,30 @@
         args.target = 'django.xtarget'
         output = XOSProcessor.process(args)
 
-        print output
-
         self.assertIn("feedback_state_fields = ['parent_name', 'name']", output)
 
+    def test_min_max_validators(self):
+        """
+        [XOS-GenX] Use django validors for min and max values
+        """
+        xproto = \
+            """
+            option app_label = "test";
+
+            message Foo (ParentFoo) {
+                required int32 val = 1 [min_value = 1, max_value = 10];
+            }
+            """
+
+        args = FakeArgs()
+        args.inputs = xproto
+        args.target = 'django.xtarget'
+        output = XOSProcessor.process(args)
+
+        self.assertIn("validators=[", output)
+        self.assertIn("MinValueValidator(1)", output)
+        self.assertIn("MaxValueValidator(10)", output)
+
 if __name__ == '__main__':
     unittest.main()
 
diff --git a/lib/xos-genx/xos-genx-tests/test_jinja2_django.py b/lib/xos-genx/xos-genx-tests/test_jinja2_django.py
index ea8738e..ab47443 100644
--- a/lib/xos-genx/xos-genx-tests/test_jinja2_django.py
+++ b/lib/xos-genx/xos-genx-tests/test_jinja2_django.py
@@ -44,6 +44,75 @@
         res = xproto_optioned_fields_to_list(fields, 'feedback_state', 'True')
         self.assertEqual(res, ["has_feedback_1", "has_feedback_2"])
 
+    def test_xproto_required_to_django(self):
+        field = {
+            'name': 'foo',
+            'options': {
+                'modifier': 'required'
+            }
+        }
+
+        res = map_xproto_to_django(field)
+        self.assertEqual(res, {'blank': False, 'null': False})
+
+    def test_xproto_optional_to_django(self):
+        field = {
+            'name': 'foo',
+            'options': {
+                'modifier': 'optional'
+            }
+        }
+
+        res = map_xproto_to_django(field)
+        self.assertEqual(res, {'blank': True, 'null': True})
+
+
+    def test_map_xproto_to_django(self):
+
+        options = {
+            'help_text': 'bar',
+            'default': 'default_value',
+            'null':  True,
+            'db_index': False,
+            'blank': False,
+            'min_value': 16,
+            'max_value': 16
+        }
+
+        field = {
+            'name': 'foo',
+            'options': options
+        }
+
+        res = map_xproto_to_django(field)
+        self.assertEqual(res, options)
+
+    def test_format_options_string(self):
+
+        options = {
+            'null':  True,
+            'min_value': 16,
+            'max_value': 16
+        }
+
+        res = format_options_string(options)
+        self.assertEqual(res, "null = True, validators=[MaxValueValidator(16), MinValueValidator(16)]")
+
+        options = {
+            'min_value': 16,
+            'max_value': 16
+        }
+
+        res = format_options_string(options)
+        self.assertEqual(res, "validators=[MaxValueValidator(16), MinValueValidator(16)]")
+
+        options = {
+            'null': True,
+        }
+
+        res = format_options_string(options)
+        self.assertEqual(res, "null = True")
+
 if __name__ == '__main__':
     unittest.main()
 
diff --git a/lib/xos-genx/xosgenx/jinja2_extensions/django.py b/lib/xos-genx/xosgenx/jinja2_extensions/django.py
index 464172a..64ab51a 100644
--- a/lib/xos-genx/xosgenx/jinja2_extensions/django.py
+++ b/lib/xos-genx/xosgenx/jinja2_extensions/django.py
@@ -79,18 +79,22 @@
             return 'GenericRelation'
 
 def map_xproto_to_django(f):
-    allowed_keys=['help_text','default','max_length','modifier','blank','choices','db_index','null','editable','on_delete','verbose_name', 'auto_now_add', 'unique']
+    allowed_keys=['help_text','default','max_length','modifier','blank','choices','db_index','null','editable','on_delete','verbose_name', 'auto_now_add', 'unique', 'min_value', 'max_value']
 
-    m = {'modifier':{'optional':True, 'required':False, '_target':'null'}}
+    # TODO evaluate if setting Null = False for all strings
+    m = {'modifier':{'optional':True, 'required':False, '_targets': ['null', 'blank']}}
     out = {}
 
     for k,v in f['options'].items():
-        if (k in allowed_keys):
+        if k in allowed_keys:
             try:
+                # NOTE this will be used to parse xproto optional/required field prefix and apply it to the null and blank fields
                 kv2 = m[k]
-                out[kv2['_target']] = kv2[v]
+                for t in kv2['_targets']:
+                    out[t] = kv2[v]
             except:
                 out[k] = v
+
     return out
 
 def xproto_django_link_options_str(field, dport=None):
@@ -121,17 +125,33 @@
 
     return format_options_string(output_dict)
 
+def use_native_django_validators(k, v):
+
+    validators_map = {
+        'min_value': 'MinValueValidator',
+        'max_value': 'MaxValueValidator'
+    }
+
+    return "%s(%s)" % (validators_map[k], v)
+
 def format_options_string(d):
+
+    known_validators = ['min_value', 'max_value']
+    validator_lst = []
+
     if (not d):
         return ''
     else:
 
         lst = []
         for k,v in d.items():
-            if (type(v)==str and k=='default' and v.endswith('()"')):
+            if k in known_validators:
+                validator_lst.append(use_native_django_validators(k, v))
+            elif (type(v)==str and k=='default' and v.endswith('()"')):
                 lst.append('%s = %s'%(k,v[1:-3]))
             elif (type(v)==str and v.startswith('"')): 
                 try:
+                    # unquote the value if necessary
                     tup = eval(v[1:-1])
                     if (type(tup)==tuple):
                         lst.append('%s = %r'%(k,tup))
@@ -146,8 +166,14 @@
                     lst.append('%s = %r'%(k,int(v)))
                 except ValueError:
                     lst.append('%s = %s'%(k,v))
-
-        return ', '.join(lst)
+        validator_string = "validators=[%s]" % ', '.join(validator_lst)
+        option_string = ', '.join(lst)
+        if len(validator_lst) == 0:
+            return option_string
+        elif len(lst) == 0:
+            return validator_string
+        else:
+            return  option_string + ", " + validator_string
 
 def xproto_django_options_str(field, dport=None):
     output_dict = map_xproto_to_django(field)
diff --git a/lib/xos-genx/xosgenx/targets/django.xtarget b/lib/xos-genx/xosgenx/targets/django.xtarget
index 631e8cd..ab67936 100644
--- a/lib/xos-genx/xosgenx/targets/django.xtarget
+++ b/lib/xos-genx/xosgenx/targets/django.xtarget
@@ -99,6 +99,8 @@
       except AttributeError:
           pass
 
+      self.full_clean()
+
       {% for policy,error in xproto_validations(m.options) %}
       policy_{{policy}}_validator(self, None)
       {% endfor %}
diff --git a/lib/xos-genx/xosgenx/targets/service.xtarget b/lib/xos-genx/xosgenx/targets/service.xtarget
index e5816ab..1e83747 100644
--- a/lib/xos-genx/xosgenx/targets/service.xtarget
+++ b/lib/xos-genx/xosgenx/targets/service.xtarget
@@ -91,6 +91,8 @@
       except AttributeError:
           base_save_in_attic = False
 
+      self.full_clean()
+
       if not base_save_in_attic:
           super({{ m.name }}{{ legacy_tag }}, self).save(*args, **kwds)
   
diff --git a/xos/core/models/core.xproto b/xos/core/models/core.xproto
index f7fcbdd..13680ef 100644
--- a/xos/core/models/core.xproto
+++ b/xos/core/models/core.xproto
@@ -416,7 +416,7 @@
 message Service (XOSBase,AttributeMixin) {
      optional string description = 1 [help_text = "Description of Service", max_length = 254, null = True, db_index = False, blank = True, varchar = True];
      required bool enabled = 2 [default = True, null = False, db_index = False, blank = True, gui_hidden = True];
-     required string kind = 3 [default = "generic", max_length = 30, content_type = "stripped", blank = False, help_text = "Kind of service", null = False, db_index = False, choices="(('generic', 'Generic'), ('data', 'Data Plane'), ('control', 'Control Plane'))"];
+     required string kind = 3 [default = "generic", max_length = 30, content_type = "stripped", blank = False, help_text = "Kind of service", null = False, db_index = False, choices="(('generic', 'Generic'), ('data', 'Data Plane'), ('control', 'Control Plane'), ('oss', 'OSS'))"];
      required string name = 4 [max_length = 30, content_type = "stripped", blank = False, help_text = "Service Name", null = False, db_index = False, unique = True];
      optional string versionNumber = 5 [max_length = 30, content_type = "stripped", blank = True, help_text = "Version of Service Definition", null = True, db_index = False];
      required bool published = 6 [default = True, null = False, db_index = False, blank = True, gui_hidden = True];
@@ -445,7 +445,7 @@
 message ServiceDependency (XOSBase) {
      required manytoone provider_service->Service:provided_dependencies = 1 [help_text = "The service that provides this dependency", null=False, db_index = True, blank=False, tosca_key=True];
      required manytoone subscriber_service->Service:subscribed_dependencies = 2 [help_text = "The services that subscribes to this dependency", null=False, db_index=True, blank=False, tosca_key=True];
-     required string connect_method = 3 [max_length = 30, help_text = "method to connect the two services", null=False, blank=False, default="none", choices = "(('none', 'None'), ('private', 'Private'), ('public', 'Public'))"];
+     required string connect_method = 3 [max_length = 30, help_text = "method to connect the two services", default="none", choices = "(('none', 'None'), ('private', 'Private'), ('public', 'Public'))"];
 }
 
 
@@ -524,7 +524,7 @@
 
 
 message SliceRole (XOSBase) {
-     required string role = 1 [choices = "(('admin', 'Admin'), ('default', 'Default'))", max_length = 30, content_type = "stripped", blank = False, null = False, db_index = False, tosca_key=True];
+     required string role = 1 [choices = "(('admin', 'Admin'), ('default', 'Default'), ('access', 'Access'))", max_length = 30, content_type = "stripped", blank = False, null = False, db_index = False, tosca_key=True];
 }
 
 policy tag_policy < ctx.user.is_admin >
@@ -553,7 +553,7 @@
      optional string service_specific_id = 3 [db_index = False, max_length = 30, null = True, content_type = "stripped", blank = True, gui_hidden = True];
      optional string service_specific_attribute = 10 [db_index = False, null = True, blank = True, varchar = True, gui_hidden = True];
      optional uint32 link_deleted_count = 11 [default = 0, help_text = "Incremented each time a provided_link is deleted from this ServiceInstance", gui_hidden = True];
-     optional manytoone master_serviceinstance->ServiceInstance:child_serviceinstances = 12 [help_text = "The master service instance that set this service instance up", gui_hidden = True];
+     optional manytoone master_serviceinstance->ServiceInstance:child_serviceinstances = 12 [help_text = "The master service instance that set this service instance up", gui_hidden = True, blank = True];
 }
 
 message ServiceInstanceLink (XOSBase) {
diff --git a/xos/core/models/user.py b/xos/core/models/user.py
index 75c87a4..4bd0e0f 100644
--- a/xos/core/models/user.py
+++ b/xos/core/models/user.py
@@ -149,8 +149,8 @@
 
     created = models.DateTimeField(auto_now_add=True)
     updated = models.DateTimeField(auto_now=True)
-    enacted = models.DateTimeField(null=True, default=None)
-    policed = models.DateTimeField(null=True, default=None)
+    enacted = models.DateTimeField(null=True, blank = True, default=None)
+    policed = models.DateTimeField(null=True, blank = True, default=None)
     backend_status = StrippedCharField(max_length=1024,
                                        default="Provisioning in progress")
     backend_code = models.IntegerField( default = 0, null = False )
@@ -305,6 +305,8 @@
         if not self.username:
             self.username = self.email
 
+        self.full_clean()
+
         super(User, self).save(*args, **kwargs)
 
         self.push_redis_event()
diff --git a/xos/core/models/xosbase_header.py b/xos/core/models/xosbase_header.py
index 4b33116..0e3a5bb 100644
--- a/xos/core/models/xosbase_header.py
+++ b/xos/core/models/xosbase_header.py
@@ -34,6 +34,7 @@
 from django.db.models.deletion import Collector
 from django.db import router
 from django.contrib.contenttypes.models import ContentType
+from django.core.validators import MaxValueValidator, MinValueValidator
 
 import redis
 from redis import ConnectionError