Repository: level12/keg-elements

Issue:
`ValidateUnique` form validator can integrate better
Right now `ValidateUnique` requires that the Form have a `obj` member. This must be added manually to the form since it's not included by default on a WTForm form. However, WTForms do have a `_obj` member which has the same purpose. We could make `ValidateUnique` easier to use by utilizing this member.

Discussion:

Patch:
diff --git a/keg_elements/forms/validators.py b/keg_elements/forms/validators.py
index f65db1d..d473d7a 100644
--- a/keg_elements/forms/validators.py
+++ b/keg_elements/forms/validators.py
@@ -47,8 +47,18 @@ class ValidateUnique(object):
     def __init__(self, object_html_link=None):
         self.object_html_link = object_html_link
 
+    def get_obj(self, form):
+        if hasattr(form, 'obj'):
+            return form.obj
+        if hasattr(form, '_obj'):
+            return form._obj
+
+        raise AttributeError(
+            'Form must provide either `obj` or `_obj` property for uniqueness validation.'
+        )
+
     def __call__(self, form, field):
-        obj = getattr(form, 'obj')
+        obj = self.get_obj(form)
         other = form.get_object_by_field(field)
 
         both_exist = None not in (obj, other)
@@ -57,7 +67,7 @@ class ValidateUnique(object):
 
         if (both_exist and not same_record) or another_exists_with_value:
             link = (' to {}.'.format(self.object_html_link(other))
-                    if hasattr(self, 'object_html_link') else '.')
+                    if self.object_html_link is not None else '.')
             msg = jinja2.Markup('This value must be unique but is already assigned'
                                 '{}'.format(link))
             raise ValidationError(msg)


Test patch:
diff --git a/keg_elements/tests/test_forms/test_validators.py b/keg_elements/tests/test_forms/test_validators.py
index 103894d..e390e5d 100644
--- a/keg_elements/tests/test_forms/test_validators.py
+++ b/keg_elements/tests/test_forms/test_validators.py
@@ -1,24 +1,67 @@
+import jinja2
 import pytest
 import wtforms
+from werkzeug.datastructures import MultiDict
+
 import keg_elements.forms.validators as validators
+from keg_elements.forms import ModelForm
 
 
-class Form():
-    def __init__(self, obj):
-        self.obj = obj
+class UniqueForm(ModelForm):
+    uq_field = wtforms.fields.StringField('thing', validators=[
+        validators.ValidateUnique(object_html_link=lambda field: 'link')])
 
     def get_object_by_field(self, field):
-        return 1
+        return 1 if field.data == '1' else None
 
 
 class TestUniqueValidator(object):
-    def test_unique_validator(self):
-        field = wtforms.fields.StringField('thing')
-        validator = validators.ValidateUnique(object_html_link=lambda field: 'link')
+    def test_validation_passes(self):
+        form = UniqueForm(MultiDict({'uq_field': '1'}), obj=1)
+        assert form.uq_field.validate(form) is True
+
+        form = UniqueForm(MultiDict({'uq_field': '2'}), obj=1)
+        assert form.uq_field.validate(form) is True
+
+    def test_validation_fails(self):
+        form = UniqueForm(MultiDict({'uq_field': '1'}))
+        assert form.uq_field.validate(form) is False
+        assert form.uq_field.errors == [
+            jinja2.Markup('This value must be unique but is already assigned to link.')
+        ]
+
+        form = UniqueForm(MultiDict({'uq_field': '1'}), obj=2)
+        assert form.uq_field.validate(form) is False
+        assert form.uq_field.errors == [
+            jinja2.Markup('This value must be unique but is already assigned to link.')
+        ]
+
+    def test_no_object_link_provided(self):
+        class Form(ModelForm):
+            uq_field = wtforms.fields.StringField('thing', validators=[validators.ValidateUnique()])
+
+            def get_object_by_field(self, field):
+                return 1 if field.data == '1' else None
+
+        form = Form(MultiDict({'uq_field': '1'}))
+        assert form.uq_field.validate(form) is False
+        assert form.uq_field.errors == [
+            jinja2.Markup('This value must be unique but is already assigned.')
+        ]
+
+    def test_get_obj(self):
+        class Form:
+            def __init__(self, **kwargs):
+                self.__dict__.update(kwargs)
+
+        validator = validators.ValidateUnique()
+
+        assert validator.get_obj(Form(obj='foo')) == 'foo'
+        assert validator.get_obj(Form(_obj='foo')) == 'foo'
 
-        assert validator(Form(1), field) is True
+        with pytest.raises(AttributeError) as exc:
+            validator.get_obj(Form())
 
-        with pytest.raises(wtforms.ValidationError,
-                           message=('This valuue must be unique bit is already '
-                                    'assigned link.')):
-            assert validator(Form(2), field)
+        assert str(exc.value) == (
+            'Form must provide either `obj` or `_obj` property for uniqueness validation.'
+        )