Repository: flask-admin/flask-admin

Issue:
Issue with Not-Nullable text fields: forms don't let you leave them empty
This will sound similar to [#1003](https://github.com/flask-admin/flask-admin/issues/1003), but the latter had to do with nullable fields, while this issue has to do with not-nullable fields.

For databases, `NULL` is not the same as an empty string. There are benefits to having `NOT NULL` fields that default to an empty string, and it's quite a common practice. 

In [this gist](https://gist.github.com/boolbag/61ecf048b7f08964b956), some rows are successfully created that have `NOT NULL` fields populated with empty strings. For variety, for the `nickname` field the empty string is explicitly provided, while for the `notes` name it is populated by the database because the default value is set.

Now if you click `edit` on one of the rows, you won't be able to leave `nickname` or `notes` as an empty string, even though they are already empty. Likewise, if you try to create a new record, you can not provide an empty string. 

This should be feasible for both fields, whether or not the default value is set to an empty string. 
If you feel the user should understand that an empty string will be created for this required field (marked with a red asterisk), maybe a greyed out `<empty string>` could appear in the form.



Discussion:

Resolving patch:
diff --git a/flask_admin/contrib/sqla/form.py b/flask_admin/contrib/sqla/form.py
--- a/flask_admin/contrib/sqla/form.py
+++ b/flask_admin/contrib/sqla/form.py
@@ -205,7 +205,12 @@ def convert(self, model, mapper, prop, field_args, hidden_pk):
 
                 optional_types = getattr(self.view, 'form_optional_types', (Boolean,))
 
-                if not column.nullable and not isinstance(column.type, optional_types):
+                if (
+                    not column.nullable
+                    and not isinstance(column.type, optional_types)
+                    and not column.default
+                    and not column.server_default
+                ):
                     kwargs['validators'].append(validators.InputRequired())
 
                 # Apply label and description if it isn't inline form field


Test patch:
diff --git a/flask_admin/tests/sqla/test_basic.py b/flask_admin/tests/sqla/test_basic.py
--- a/flask_admin/tests/sqla/test_basic.py
+++ b/flask_admin/tests/sqla/test_basic.py
@@ -61,15 +61,22 @@ def __str__(self):
 
     class Model2(db.Model):
         def __init__(self, string_field=None, int_field=None, bool_field=None,
-                     model1=None, float_field=None):
+                     model1=None, float_field=None, string_field_default=None,
+                     string_field_empty_default=None):
             self.string_field = string_field
             self.int_field = int_field
             self.bool_field = bool_field
             self.model1 = model1
             self.float_field = float_field
+            self.string_field_default = string_field_default
+            self.string_field_empty_default = string_field_empty_default
 
         id = db.Column(db.Integer, primary_key=True)
         string_field = db.Column(db.String)
+        string_field_default = db.Column(db.Text, nullable=False,
+                                         default='')
+        string_field_empty_default = db.Column(db.Text, nullable=False,
+                                               default='')
         int_field = db.Column(db.Integer)
         bool_field = db.Column(db.Boolean)
         enum_field = db.Column(db.Enum('model2_v1', 'model2_v2'), nullable=True)
@@ -1959,3 +1966,18 @@ class Model2(db.Model):
 
     rv = client.get('/admin/model2/')
     eq_(rv.status_code, 200)
+
+
+def test_model_default():
+    app, db, admin = setup()
+    _, Model2 = create_models(db)
+
+    class ModelView(CustomModelView):
+        pass
+
+    view = ModelView(Model2, db.session)
+    admin.add_view(view)
+
+    client = app.test_client()
+    rv = client.post('/admin/model2/new/', data=dict())
+    assert_true(b'This field is required' not in rv.data)