Repository: flask-admin/flask-admin

Issue:
sqla.Unique validator is inserted twice
when a table column is set to be unique when defining the model:

`
class Example(Base):
     email = Column(String(50), unique=True)
`

and when adding any validators in form_args for the Example model view,
e.g.
`
form_args = dict(email=dict(validators=[required()]))
`

the form's validators list for the email field contains the sqla.Unique **twice** and the  "Already exists" message appears twice when having a unique violation



Discussion:

Resolving patch:
diff --git a/flask_admin/contrib/mongoengine/form.py b/flask_admin/contrib/mongoengine/form.py
--- a/flask_admin/contrib/mongoengine/form.py
+++ b/flask_admin/contrib/mongoengine/form.py
@@ -1,5 +1,3 @@
-from copy import deepcopy
-
 from mongoengine import ReferenceField, ListField
 from mongoengine.base import BaseDocument, DocumentMetaclass, get_document
 
@@ -70,8 +68,11 @@ def convert(self, model, field, field_args):
         }
 
         if field_args:
-            # prevent modification of self.form_args
-            kwargs.update(deepcopy(field_args))
+            kwargs.update(field_args)
+
+        if kwargs['validators']:
+            # Create a copy of the list since we will be modifying it.
+            kwargs['validators'] = list(kwargs['validators'])
 
         if field.required:
             kwargs['validators'].append(validators.InputRequired())
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
@@ -1,7 +1,5 @@
 import warnings
 
-from copy import deepcopy
-
 from wtforms import fields, validators
 from sqlalchemy import Boolean, Column
 
@@ -143,8 +141,11 @@ def convert(self, model, mapper, prop, field_args, hidden_pk):
         }
 
         if field_args:
-            # prevent modification of self.form_args
-            kwargs.update(deepcopy(field_args))
+            kwargs.update(field_args)
+
+        if kwargs['validators']:
+            # Create a copy of the list since we will be modifying it.
+            kwargs['validators'] = list(kwargs['validators'])
 
         # Check if it is relation or property
         if hasattr(prop, 'direction'):


Test patch:
diff --git a/flask_admin/tests/mongoengine/test_basic.py b/flask_admin/tests/mongoengine/test_basic.py
--- a/flask_admin/tests/mongoengine/test_basic.py
+++ b/flask_admin/tests/mongoengine/test_basic.py
@@ -6,7 +6,7 @@
 if not PY2:
     raise SkipTest('MongoEngine is not Python 3 compatible')
 
-from wtforms import fields
+from wtforms import fields, validators
 
 from flask_admin import form
 from flask_admin.contrib.mongoengine import ModelView
@@ -1040,6 +1040,25 @@ class Model(db.Document):
     eq_(form.name.choices, [('a', 'a'), ('b', 'b'), ('c', 'c')])
 
 
+def test_form_args():
+    app, db, admin = setup()
+
+    class Model(db.Document):
+        test = db.StringField(required=True)
+
+    shared_form_args = {'test': {'validators': [validators.Regexp('test')]}}
+
+    view = CustomModelView(Model, form_args=shared_form_args)
+    admin.add_view(view)
+
+    # ensure shared field_args don't create duplicate validators
+    create_form = view.create_form()
+    eq_(len(create_form.test.validators), 2)
+
+    edit_form = view.edit_form()
+    eq_(len(edit_form.test.validators), 2)
+
+
 def test_form_args_embeddeddoc():
     app, db, admin = setup()
 
diff --git a/flask_admin/tests/peeweemodel/test_basic.py b/flask_admin/tests/peeweemodel/test_basic.py
--- a/flask_admin/tests/peeweemodel/test_basic.py
+++ b/flask_admin/tests/peeweemodel/test_basic.py
@@ -8,7 +8,7 @@
 
 import peewee
 
-from wtforms import fields
+from wtforms import fields, validators
 
 from flask_admin import form
 from flask_admin._compat import iteritems
@@ -910,6 +910,31 @@ class TestForm(form.BaseForm):
     ok_(isinstance(create_form, TestForm))
 
 
+def test_form_args():
+    app, db, admin = setup()
+
+    class BaseModel(peewee.Model):
+        class Meta:
+            database = db
+
+    class Model(BaseModel):
+        test = peewee.CharField(null=False)
+
+    Model.create_table()
+
+    shared_form_args = {'test': {'validators': [validators.Regexp('test')]}}
+
+    view = CustomModelView(Model, form_args=shared_form_args)
+    admin.add_view(view)
+
+    # ensure shared field_args don't create duplicate validators
+    create_form = view.create_form()
+    eq_(len(create_form.test.validators), 2)
+
+    edit_form = view.edit_form()
+    eq_(len(edit_form.test.validators), 2)
+
+
 def test_ajax_fk():
     app, db, admin = setup()
 
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
@@ -1,6 +1,6 @@
 from nose.tools import eq_, ok_, raises, assert_true
 
-from wtforms import fields
+from wtforms import fields, validators
 
 from flask_admin import form
 from flask_admin._compat import as_unicode
@@ -1441,7 +1441,27 @@ class ChildModel(db.Model):
 
     ok_(type(form3.model).__name__ == 'QuerySelectField')
 
-    # TODO: form_args
+
+def test_form_args():
+    app, db, admin = setup()
+
+    class Model(db.Model):
+        id = db.Column(db.String, primary_key=True)
+        test = db.Column(db.String, nullable=False)
+
+    db.create_all()
+
+    shared_form_args = {'test': {'validators': [validators.Regexp('test')]}}
+
+    view = CustomModelView(Model, db.session, form_args=shared_form_args)
+    admin.add_view(view)
+
+    create_form = view.create_form()
+    eq_(len(create_form.test.validators), 2)
+
+    # ensure shared field_args don't create duplicate validators
+    edit_form = view.edit_form()
+    eq_(len(edit_form.test.validators), 2)
 
 
 def test_form_override():