from nose.tools import eq_, ok_, raises, assert_true

from wtforms import fields, validators

from flask_admin import form
from flask_admin._compat import as_unicode
from flask_admin._compat import iteritems
from flask_admin.contrib.sqla import ModelView, filters, tools
from flask_babelex import Babel

from sqlalchemy.ext.hybrid import hybrid_property

from . import setup

from datetime import datetime, time, date


class CustomModelView(ModelView):
    def __init__(self, model, session,
                 name=None, category=None, endpoint=None, url=None,
                 **kwargs):
        for k, v in iteritems(kwargs):
            setattr(self, k, v)

        super(CustomModelView, self).__init__(model, session, name, category,
                                              endpoint, url)


def create_models(db):
    class Model1(db.Model):
        def __init__(self, test1=None, test2=None, test3=None, test4=None,
                     bool_field=False, date_field=None, time_field=None,
                     datetime_field=None, enum_field=None):
            self.test1 = test1
            self.test2 = test2
            self.test3 = test3
            self.test4 = test4
            self.bool_field = bool_field
            self.date_field = date_field
            self.time_field = time_field
            self.datetime_field = datetime_field
            self.enum_field = enum_field

        id = db.Column(db.Integer, primary_key=True)
        test1 = db.Column(db.String(20))
        test2 = db.Column(db.Unicode(20))
        test3 = db.Column(db.Text)
        test4 = db.Column(db.UnicodeText)
        bool_field = db.Column(db.Boolean)
        enum_field = db.Column(db.Enum('model1_v1', 'model1_v2'), nullable=True)

        date_field = db.Column(db.Date)
        time_field = db.Column(db.Time)
        datetime_field = db.Column(db.DateTime)

        def __unicode__(self):
            return self.test1

        def __str__(self):
            return self.test1

    class Model2(db.Model):
        def __init__(self, string_field=None, int_field=None, bool_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)
        float_field = db.Column(db.Float)

        # Relation
        model1_id = db.Column(db.Integer, db.ForeignKey(Model1.id))
        model1 = db.relationship(Model1, backref='model2')

    db.create_all()

    return Model1, Model2


def fill_db(db, Model1, Model2):
    model1_obj1 = Model1('test1_val_1', 'test2_val_1', bool_field=True)
    model1_obj2 = Model1('test1_val_2', 'test2_val_2', bool_field=False)
    model1_obj3 = Model1('test1_val_3', 'test2_val_3')
    model1_obj4 = Model1('test1_val_4', 'test2_val_4')

    model2_obj1 = Model2('test2_val_1', model1=model1_obj1, float_field=None)
    model2_obj2 = Model2('test2_val_2', model1=model1_obj2, float_field=None)
    model2_obj3 = Model2('test2_val_3', int_field=5000, float_field=25.9)
    model2_obj4 = Model2('test2_val_4', int_field=9000, float_field=75.5)
    model2_obj5 = Model2('test2_val_5', int_field=6169453081680413441)

    date_obj1 = Model1('date_obj1', date_field=date(2014,11,17))
    date_obj2 = Model1('date_obj2', date_field=date(2013,10,16))
    timeonly_obj1 = Model1('timeonly_obj1', time_field=time(11,10,9))
    timeonly_obj2 = Model1('timeonly_obj2', time_field=time(10,9,8))
    datetime_obj1 = Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0))
    datetime_obj2 = Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0))

    enum_obj1 = Model1('enum_obj1', enum_field="model1_v1")
    enum_obj2 = Model1('enum_obj2', enum_field="model1_v2")

    empty_obj = Model1(test2="empty_obj")

    db.session.add_all([
        model1_obj1, model1_obj2, model1_obj3, model1_obj4,
        model2_obj1, model2_obj2, model2_obj3, model2_obj4, model2_obj5,
        date_obj1, timeonly_obj1, datetime_obj1,
        date_obj2, timeonly_obj2, datetime_obj2,
        enum_obj1, enum_obj2, empty_obj
    ])
    db.session.commit()


def test_model():
    app, db, admin = setup()
    Model1, Model2 = create_models(db)

    view = CustomModelView(Model1, db.session)
    admin.add_view(view)

    eq_(view.model, Model1)
    eq_(view.name, 'Model1')
    eq_(view.endpoint, 'model1')

    eq_(view._primary_key, 'id')

    ok_('test1' in view._sortable_columns)
    ok_('test2' in view._sortable_columns)
    ok_('test3' in view._sortable_columns)
    ok_('test4' in view._sortable_columns)

    ok_(view._create_form_class is not None)
    ok_(view._edit_form_class is not None)
    eq_(view._search_supported, False)
    eq_(view._filters, None)

    # Verify form
    eq_(view._create_form_class.test1.field_class, fields.StringField)
    eq_(view._create_form_class.test2.field_class, fields.StringField)
    eq_(view._create_form_class.test3.field_class, fields.TextAreaField)
    eq_(view._create_form_class.test4.field_class, fields.TextAreaField)

    # Make some test clients
    client = app.test_client()

    rv = client.get('/admin/model1/')
    eq_(rv.status_code, 200)

    rv = client.get('/admin/model1/new/')
    eq_(rv.status_code, 200)

    rv = client.post('/admin/model1/new/',
                     data=dict(test1='test1large',
                               test2='test2',
                               time_field=time(0,0,0)))
    eq_(rv.status_code, 302)

    model = db.session.query(Model1).first()
    eq_(model.test1, u'test1large')
    eq_(model.test2, u'test2')
    eq_(model.test3, u'')
    eq_(model.test4, u'')

    rv = client.get('/admin/model1/')
    eq_(rv.status_code, 200)
    ok_(u'test1large' in rv.data.decode('utf-8'))

    url = '/admin/model1/edit/?id=%s' % model.id
    rv = client.get(url)
    eq_(rv.status_code, 200)

    # verify that midnight does not show as blank
    ok_(u'00:00:00' in rv.data.decode('utf-8'))

    rv = client.post(url,
                     data=dict(test1='test1small', test2='test2large'))
    eq_(rv.status_code, 302)

    model = db.session.query(Model1).first()
    eq_(model.test1, 'test1small')
    eq_(model.test2, 'test2large')
    eq_(model.test3, '')
    eq_(model.test4, '')

    url = '/admin/model1/delete/?id=%s' % model.id
    rv = client.post(url)
    eq_(rv.status_code, 302)
    eq_(db.session.query(Model1).count(), 0)


@raises(Exception)
def test_no_pk():
    app, db, admin = setup()

    class Model(db.Model):
        test = db.Column(db.Integer)

    view = CustomModelView(Model)
    admin.add_view(view)


def test_list_columns():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

    # test column_list with a list of strings
    view = CustomModelView(Model1, db.session,
                           column_list=['test1', 'test3'],
                           column_labels=dict(test1='Column1'))
    admin.add_view(view)

    eq_(len(view._list_columns), 2)
    eq_(view._list_columns, [('test1', 'Column1'), ('test3', 'Test3')])

    client = app.test_client()

    rv = client.get('/admin/model1/')
    data = rv.data.decode('utf-8')
    ok_('Column1' in data)
    ok_('Test2' not in data)

    # test column_list with a list of SQLAlchemy columns
    view2 = CustomModelView(Model1, db.session, endpoint='model1_2',
                            column_list=[Model1.test1, Model1.test3],
                            column_labels=dict(test1='Column1'))
    admin.add_view(view2)

    eq_(len(view2._list_columns), 2)
    eq_(view2._list_columns, [('test1', 'Column1'), ('test3', 'Test3')])

    rv = client.get('/admin/model1_2/')
    data = rv.data.decode('utf-8')
    ok_('Column1' in data)
    ok_('Test2' not in data)


def test_complex_list_columns():
    app, db, admin = setup()
    M1, M2 = create_models(db)

    m1 = M1('model1_val1')
    db.session.add(m1)
    db.session.add(M2('model2_val1', model1=m1))

    db.session.commit()

    # test column_list with a list of strings on a relation
    view = CustomModelView(M2, db.session,
                           column_list=['model1.test1'])
    admin.add_view(view)

    client = app.test_client()

    rv = client.get('/admin/model2/')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('model1_val1' in data)


def test_exclude_columns():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

    view = CustomModelView(
        Model1, db.session,
        column_exclude_list=['test2', 'test4', 'enum_field', 'date_field', 'time_field', 'datetime_field']
    )
    admin.add_view(view)

    eq_(
        view._list_columns,
        [('test1', 'Test1'), ('test3', 'Test3'), ('bool_field', 'Bool Field')]
    )

    client = app.test_client()

    rv = client.get('/admin/model1/')
    data = rv.data.decode('utf-8')
    ok_('Test1' in data)
    ok_('Test2' not in data)


def test_column_searchable_list():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

    view = CustomModelView(Model2, db.session,
                           column_searchable_list=['string_field', 'int_field'])
    admin.add_view(view)

    eq_(view._search_supported, True)
    eq_(len(view._search_fields), 2)

    ok_(isinstance(view._search_fields[0][0], db.Column))
    ok_(isinstance(view._search_fields[1][0], db.Column))
    eq_(view._search_fields[0][0].name, 'string_field')
    eq_(view._search_fields[1][0].name, 'int_field')

    db.session.add(Model2('model1-test', 5000))
    db.session.add(Model2('model2-test', 9000))
    db.session.commit()

    client = app.test_client()

    rv = client.get('/admin/model2/?search=model1')
    data = rv.data.decode('utf-8')
    ok_('model1-test' in data)
    ok_('model2-test' not in data)

    rv = client.get('/admin/model2/?search=9000')
    data = rv.data.decode('utf-8')
    ok_('model1-test' not in data)
    ok_('model2-test' in data)


def test_complex_searchable_list():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

    view = CustomModelView(Model2, db.session,
                           column_searchable_list=['model1.test1'])
    admin.add_view(view)

    m1 = Model1('model1-test1-val')
    m2 = Model1('model1-test2-val')
    db.session.add(m1)
    db.session.add(m2)
    db.session.add(Model2('model2-test1-val', model1=m1))
    db.session.add(Model2('model2-test2-val', model1=m2))
    db.session.commit()

    client = app.test_client()

    # test relation string - 'model1.test1'
    rv = client.get('/admin/model2/?search=model1-test1')
    data = rv.data.decode('utf-8')
    ok_('model2-test1-val' in data)
    ok_('model2-test2-val' not in data)

    view2 = CustomModelView(Model1, db.session,
                           column_searchable_list=[Model2.string_field])
    admin.add_view(view2)

    # test relation object - Model2.string_field
    rv = client.get('/admin/model1/?search=model2-test1')
    data = rv.data.decode('utf-8')
    ok_('model1-test1-val' in data)
    ok_('model1-test2-val' not in data)


def test_complex_searchable_list_missing_children():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

    view = CustomModelView(Model1, db.session,
                           column_searchable_list=[
                               'test1', 'model2.string_field'])
    admin.add_view(view)

    db.session.add(Model1('magic string'))
    db.session.commit()

    client = app.test_client()

    rv = client.get('/admin/model1/?search=magic')
    data = rv.data.decode('utf-8')
    ok_('magic string' in data)


def test_column_editable_list():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

    view = CustomModelView(Model1, db.session,
                           column_editable_list=['test1', 'enum_field'])
    admin.add_view(view)

    fill_db(db, Model1, Model2)

    client = app.test_client()

    # Test in-line edit field rendering
    rv = client.get('/admin/model1/')
    data = rv.data.decode('utf-8')
    ok_('data-role="x-editable"' in data)

    # Form - Test basic in-line edit functionality
    rv = client.post('/admin/model1/ajax/update/', data={
        'list_form_pk': '1',
        'test1': 'change-success-1',
    })
    data = rv.data.decode('utf-8')
    ok_('Record was successfully saved.' == data)

    # ensure the value has changed
    rv = client.get('/admin/model1/')
    data = rv.data.decode('utf-8')
    ok_('change-success-1' in data)

    # Test validation error
    rv = client.post('/admin/model1/ajax/update/', data={
        'list_form_pk': '1',
        'enum_field': 'problematic-input',
    })
    eq_(rv.status_code, 500)

    # Test invalid primary key
    rv = client.post('/admin/model1/ajax/update/', data={
        'list_form_pk': '1000',
        'test1': 'problematic-input',
    })
    data = rv.data.decode('utf-8')
    eq_(rv.status_code, 500)

    # Test editing column not in column_editable_list
    rv = client.post('/admin/model1/ajax/update/', data={
        'list_form_pk': '1',
        'test2': 'problematic-input',
    })
    data = rv.data.decode('utf-8')
    ok_('problematic-input' not in data)

    # Test in-line editing for relations
    view = CustomModelView(Model2, db.session, column_editable_list=['model1'])
    admin.add_view(view)

    rv = client.post('/admin/model2/ajax/update/', data={
        'list_form_pk': '1',
        'model1': '3',
    })
    data = rv.data.decode('utf-8')
    ok_('Record was successfully saved.' == data)

    # confirm the value has changed
    rv = client.get('/admin/model2/')
    data = rv.data.decode('utf-8')
    ok_('test1_val_3' in data)


def test_details_view():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

    view_no_details = CustomModelView(Model1, db.session)
    admin.add_view(view_no_details)

    # fields are scaffolded
    view_w_details = CustomModelView(Model2, db.session, can_view_details=True)
    admin.add_view(view_w_details)

    # show only specific fields in details w/ column_details_list
    string_field_view = CustomModelView(Model2, db.session,
                                        can_view_details=True,
                                        column_details_list=["string_field"],
                                        endpoint="sf_view")
    admin.add_view(string_field_view)

    fill_db(db, Model1, Model2)

    client = app.test_client()

    # ensure link to details is hidden when can_view_details is disabled
    rv = client.get('/admin/model1/')
    data = rv.data.decode('utf-8')
    ok_('/admin/model1/details/' not in data)

    # ensure link to details view appears
    rv = client.get('/admin/model2/')
    data = rv.data.decode('utf-8')
    ok_('/admin/model2/details/' in data)

    # test redirection when details are disabled
    rv = client.get('/admin/model1/details/?url=%2Fadmin%2Fmodel1%2F&id=1')
    eq_(rv.status_code, 302)

    # test if correct data appears in details view when enabled
    rv = client.get('/admin/model2/details/?url=%2Fadmin%2Fmodel2%2F&id=1')
    data = rv.data.decode('utf-8')
    ok_('String Field' in data)
    ok_('test2_val_1' in data)
    ok_('test1_val_1' in data)

    # test column_details_list
    rv = client.get('/admin/sf_view/details/?url=%2Fadmin%2Fsf_view%2F&id=1')
    data = rv.data.decode('utf-8')
    ok_('String Field' in data)
    ok_('test2_val_1' in data)
    ok_('test1_val_1' not in data)


def test_editable_list_special_pks():
    ''' Tests editable list view + a primary key with special characters
    '''
    app, db, admin = setup()

    class Model1(db.Model):
        def __init__(self, id=None, val1=None):
            self.id = id
            self.val1 = val1

        id = db.Column(db.String(20), primary_key=True)
        val1 = db.Column(db.String(20))

    db.create_all()

    view = CustomModelView(Model1, db.session, column_editable_list=['val1'])
    admin.add_view(view)

    db.session.add(Model1('1-1', 'test1'))
    db.session.add(Model1('1-5', 'test2'))
    db.session.commit()

    client = app.test_client()

    # Form - Test basic in-line edit functionality
    rv = client.post('/admin/model1/ajax/update/', data={
        'list_form_pk': '1-1',
        'val1': 'change-success-1',
    })
    data = rv.data.decode('utf-8')
    ok_('Record was successfully saved.' == data)

    # ensure the value has changed
    rv = client.get('/admin/model1/')
    data = rv.data.decode('utf-8')
    ok_('change-success-1' in data)


def test_column_filters():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

    view = CustomModelView(
        Model1, db.session,
        column_filters=['test1']
    )
    admin.add_view(view)

    client = app.test_client()

    eq_(len(view._filters), 7)

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
        [
            (0, u'contains'),
            (1, u'not contains'),
            (2, u'equals'),
            (3, u'not equal'),
            (4, u'empty'),
            (5, u'in list'),
            (6, u'not in list'),
        ])

    # Test filter that references property
    view = CustomModelView(Model2, db.session,
                           column_filters=['model1'])

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test1']],
        [
            (0, u'contains'),
            (1, u'not contains'),
            (2, u'equals'),
            (3, u'not equal'),
            (4, u'empty'),
            (5, u'in list'),
            (6, u'not in list'),
        ])

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test2']],
        [
            (7, u'contains'),
            (8, u'not contains'),
            (9, u'equals'),
            (10, u'not equal'),
            (11, u'empty'),
            (12, u'in list'),
            (13, u'not in list'),
        ])

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test3']],
        [
            (14, u'contains'),
            (15, u'not contains'),
            (16, u'equals'),
            (17, u'not equal'),
            (18, u'empty'),
            (19, u'in list'),
            (20, u'not in list'),
        ])

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test4']],
        [
            (21, u'contains'),
            (22, u'not contains'),
            (23, u'equals'),
            (24, u'not equal'),
            (25, u'empty'),
            (26, u'in list'),
            (27, u'not in list'),
        ])

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Bool Field']],
        [
            (28, u'equals'),
            (29, u'not equal'),
        ])

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Enum Field']],
        [
            (30, u'equals'),
            (31, u'not equal'),
            (32, u'empty'),
            (33, u'in list'),
            (34, u'not in list'),
        ])

    # Test filter with a dot
    view = CustomModelView(Model2, db.session,
                           column_filters=['model1.bool_field'])

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Bool Field']],
        [
            (0, 'equals'),
            (1, 'not equal'),
        ])

    # Test column_labels on filters
    view = CustomModelView(Model2, db.session,
                           column_filters=['model1.bool_field', 'string_field'],
                           column_labels={
                               'model1.bool_field': 'Test Filter #1',
                               'string_field': 'Test Filter #2',
                           })

    eq_(list(view._filter_groups.keys()), [u'Test Filter #1', u'Test Filter #2'])

    fill_db(db, Model1, Model2)

    # Test equals
    rv = client.get('/admin/model1/?flt0_0=test1_val_1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    # the filter value is always in "data"
    # need to check a different column than test1 for the expected row

    ok_('test2_val_1' in data)
    ok_('test1_val_2' not in data)

    # Test NOT IN filter
    rv = client.get('/admin/model1/?flt0_6=test1_val_1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')

    ok_('test1_val_2' in data)
    ok_('test2_val_1' not in data)

    # Test string filter
    view = CustomModelView(Model1, db.session,
                           column_filters=['test1'], endpoint='_strings')
    admin.add_view(view)

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
        [
            (0, 'contains'),
            (1, 'not contains'),
            (2, 'equals'),
            (3, 'not equal'),
            (4, 'empty'),
            (5, 'in list'),
            (6, 'not in list'),
        ])

    # string - equals
    rv = client.get('/admin/_strings/?flt0_0=test1_val_1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' in data)
    ok_('test1_val_2' not in data)

    # string - not equal
    rv = client.get('/admin/_strings/?flt0_1=test1_val_1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' not in data)
    ok_('test1_val_2' in data)

    # string - contains
    rv = client.get('/admin/_strings/?flt0_2=test1_val_1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' in data)
    ok_('test1_val_2' not in data)

    # string - not contains
    rv = client.get('/admin/_strings/?flt0_3=test1_val_1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' not in data)
    ok_('test1_val_2' in data)

    # string - empty
    rv = client.get('/admin/_strings/?flt0_4=1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('empty_obj' in data)
    ok_('test1_val_1' not in data)
    ok_('test1_val_2' not in data)

    # string - not empty
    rv = client.get('/admin/_strings/?flt0_4=0')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('empty_obj' not in data)
    ok_('test1_val_1' in data)
    ok_('test1_val_2' in data)

    # string - in list
    rv = client.get('/admin/_strings/?flt0_5=test1_val_1%2Ctest1_val_2')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' in data)
    ok_('test2_val_2' in data)
    ok_('test1_val_3' not in data)
    ok_('test1_val_4' not in data)

    # string - not in list
    rv = client.get('/admin/_strings/?flt0_6=test1_val_1%2Ctest1_val_2')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' not in data)
    ok_('test2_val_2' not in data)
    ok_('test1_val_3' in data)
    ok_('test1_val_4' in data)

    # Test integer filter
    view = CustomModelView(Model2, db.session,
                           column_filters=['int_field'])
    admin.add_view(view)

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']],
        [
            (0, 'equals'),
            (1, 'not equal'),
            (2, 'greater than'),
            (3, 'smaller than'),
            (4, 'empty'),
            (5, 'in list'),
            (6, 'not in list'),
        ])

    # integer - equals
    rv = client.get('/admin/model2/?flt0_0=5000')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_3' in data)
    ok_('test2_val_4' not in data)

    # integer - equals (huge number)
    rv = client.get('/admin/model2/?flt0_0=6169453081680413441')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_5' in data)
    ok_('test2_val_4' not in data)

    # integer - equals - test validation
    rv = client.get('/admin/model2/?flt0_0=badval')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('Invalid Filter Value' in data)

    # integer - not equal
    rv = client.get('/admin/model2/?flt0_1=5000')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_3' not in data)
    ok_('test2_val_4' in data)

    # integer - greater
    rv = client.get('/admin/model2/?flt0_2=6000')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_3' not in data)
    ok_('test2_val_4' in data)

    # integer - smaller
    rv = client.get('/admin/model2/?flt0_3=6000')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_3' in data)
    ok_('test2_val_4' not in data)

    # integer - empty
    rv = client.get('/admin/model2/?flt0_4=1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' in data)
    ok_('test2_val_2' in data)
    ok_('test2_val_3' not in data)
    ok_('test2_val_4' not in data)

    # integer - not empty
    rv = client.get('/admin/model2/?flt0_4=0')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' not in data)
    ok_('test2_val_2' not in data)
    ok_('test2_val_3' in data)
    ok_('test2_val_4' in data)

    # integer - in list
    rv = client.get('/admin/model2/?flt0_5=5000%2C9000')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' not in data)
    ok_('test2_val_2' not in data)
    ok_('test2_val_3' in data)
    ok_('test2_val_4' in data)

    # integer - in list (huge number)
    rv = client.get('/admin/model2/?flt0_5=6169453081680413441')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' not in data)
    ok_('test2_val_5' in data)

    # integer - in list - test validation
    rv = client.get('/admin/model2/?flt0_5=5000%2Cbadval')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('Invalid Filter Value' in data)

    # integer - not in list
    rv = client.get('/admin/model2/?flt0_6=5000%2C9000')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' in data)
    ok_('test2_val_2' in data)
    ok_('test2_val_3' not in data)
    ok_('test2_val_4' not in data)

    # Test boolean filter
    view = CustomModelView(Model1, db.session, column_filters=['bool_field'],
                           endpoint="_bools")
    admin.add_view(view)

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Bool Field']],
        [
            (0, 'equals'),
            (1, 'not equal'),
        ])

    # boolean - equals - Yes
    rv = client.get('/admin/_bools/?flt0_0=1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' in data)
    ok_('test2_val_2' not in data)
    ok_('test2_val_3' not in data)

    # boolean - equals - No
    rv = client.get('/admin/_bools/?flt0_0=0')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' not in data)
    ok_('test2_val_2' in data)
    ok_('test2_val_3' in data)

    # boolean - not equals - Yes
    rv = client.get('/admin/_bools/?flt0_1=1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' not in data)
    ok_('test2_val_2' in data)
    ok_('test2_val_3' in data)

    # boolean - not equals - No
    rv = client.get('/admin/_bools/?flt0_1=0')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' in data)
    ok_('test2_val_2' not in data)
    ok_('test2_val_3' not in data)

    # Test float filter
    view = CustomModelView(Model2, db.session, column_filters=['float_field'],
                           endpoint="_float")
    admin.add_view(view)

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Float Field']],
        [
            (0, 'equals'),
            (1, 'not equal'),
            (2, 'greater than'),
            (3, 'smaller than'),
            (4, 'empty'),
            (5, 'in list'),
            (6, 'not in list'),
        ])

    # float - equals
    rv = client.get('/admin/_float/?flt0_0=25.9')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_3' in data)
    ok_('test2_val_4' not in data)

    # float - equals - test validation
    rv = client.get('/admin/_float/?flt0_0=badval')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('Invalid Filter Value' in data)

    # float - not equal
    rv = client.get('/admin/_float/?flt0_1=25.9')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_3' not in data)
    ok_('test2_val_4' in data)

    # float - greater
    rv = client.get('/admin/_float/?flt0_2=60.5')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_3' not in data)
    ok_('test2_val_4' in data)

    # float - smaller
    rv = client.get('/admin/_float/?flt0_3=60.5')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_3' in data)
    ok_('test2_val_4' not in data)

    # float - empty
    rv = client.get('/admin/_float/?flt0_4=1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' in data)
    ok_('test2_val_2' in data)
    ok_('test2_val_3' not in data)
    ok_('test2_val_4' not in data)

    # float - not empty
    rv = client.get('/admin/_float/?flt0_4=0')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' not in data)
    ok_('test2_val_2' not in data)
    ok_('test2_val_3' in data)
    ok_('test2_val_4' in data)

    # float - in list
    rv = client.get('/admin/_float/?flt0_5=25.9%2C75.5')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' not in data)
    ok_('test2_val_2' not in data)
    ok_('test2_val_3' in data)
    ok_('test2_val_4' in data)

    # float - in list - test validation
    rv = client.get('/admin/_float/?flt0_5=25.9%2Cbadval')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('Invalid Filter Value' in data)

    # float - not in list
    rv = client.get('/admin/_float/?flt0_6=25.9%2C75.5')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' in data)
    ok_('test2_val_2' in data)
    ok_('test2_val_3' not in data)
    ok_('test2_val_4' not in data)

    # Test filters to joined table field
    view = CustomModelView(
        Model2, db.session,
        endpoint='_model2',
        column_filters=['model1.bool_field'],
        column_list=[
            'string_field',
            'model1.id',
            'model1.bool_field',
        ]
    )
    admin.add_view(view)

    rv = client.get('/admin/_model2/?flt1_0=1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2_val_1' in data)
    ok_('test2_val_2' not in data)
    ok_('test2_val_3' not in data)
    ok_('test2_val_4' not in data)

    # Test human readable URLs
    view = CustomModelView(
        Model1, db.session,
        column_filters=['test1'],
        endpoint='_model3',
        named_filter_urls=True
    )
    admin.add_view(view)

    rv = client.get('/admin/_model3/?flt1_test1_equals=test1_val_1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' in data)
    ok_('test1_val_2' not in data)

    # Test date, time, and datetime filters
    view = CustomModelView(Model1, db.session,
                           column_filters=['date_field', 'datetime_field', 'time_field'],
                           endpoint="_datetime")
    admin.add_view(view)

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Date Field']],
        [
            (0, 'equals'),
            (1, 'not equal'),
            (2, 'greater than'),
            (3, 'smaller than'),
            (4, 'between'),
            (5, 'not between'),
            (6, 'empty'),
        ])

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']],
        [
            (7, 'equals'),
            (8, 'not equal'),
            (9, 'greater than'),
            (10, 'smaller than'),
            (11, 'between'),
            (12, 'not between'),
            (13, 'empty'),
        ])

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Time Field']],
        [
            (14, 'equals'),
            (15, 'not equal'),
            (16, 'greater than'),
            (17, 'smaller than'),
            (18, 'between'),
            (19, 'not between'),
            (20, 'empty'),
        ])

    # date - equals
    rv = client.get('/admin/_datetime/?flt0_0=2014-11-17')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('date_obj1' in data)
    ok_('date_obj2' not in data)

    # date - not equal
    rv = client.get('/admin/_datetime/?flt0_1=2014-11-17')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('date_obj1' not in data)
    ok_('date_obj2' in data)

    # date - greater
    rv = client.get('/admin/_datetime/?flt0_2=2014-11-16')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('date_obj1' in data)
    ok_('date_obj2' not in data)

    # date - smaller
    rv = client.get('/admin/_datetime/?flt0_3=2014-11-16')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('date_obj1' not in data)
    ok_('date_obj2' in data)

    # date - between
    rv = client.get('/admin/_datetime/?flt0_4=2014-11-13+to+2014-11-20')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('date_obj1' in data)
    ok_('date_obj2' not in data)

    # date - not between
    rv = client.get('/admin/_datetime/?flt0_5=2014-11-13+to+2014-11-20')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('date_obj1' not in data)
    ok_('date_obj2' in data)

    # date - empty
    rv = client.get('/admin/_datetime/?flt0_6=1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' in data)
    ok_('date_obj1' not in data)
    ok_('date_obj2' not in data)

    # date - empty
    rv = client.get('/admin/_datetime/?flt0_6=0')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' not in data)
    ok_('date_obj1' in data)
    ok_('date_obj2' in data)

    # datetime - equals
    rv = client.get('/admin/_datetime/?flt0_7=2014-04-03+01%3A09%3A00')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('datetime_obj1' in data)
    ok_('datetime_obj2' not in data)

    # datetime - not equal
    rv = client.get('/admin/_datetime/?flt0_8=2014-04-03+01%3A09%3A00')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('datetime_obj1' not in data)
    ok_('datetime_obj2' in data)

    # datetime - greater
    rv = client.get('/admin/_datetime/?flt0_9=2014-04-03+01%3A08%3A00')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('datetime_obj1' in data)
    ok_('datetime_obj2' not in data)

    # datetime - smaller
    rv = client.get('/admin/_datetime/?flt0_10=2014-04-03+01%3A08%3A00')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('datetime_obj1' not in data)
    ok_('datetime_obj2' in data)

    # datetime - between
    rv = client.get('/admin/_datetime/?flt0_11=2014-04-02+00%3A00%3A00+to+2014-11-20+23%3A59%3A59')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('datetime_obj1' in data)
    ok_('datetime_obj2' not in data)

    # datetime - not between
    rv = client.get('/admin/_datetime/?flt0_12=2014-04-02+00%3A00%3A00+to+2014-11-20+23%3A59%3A59')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('datetime_obj1' not in data)
    ok_('datetime_obj2' in data)

    # datetime - empty
    rv = client.get('/admin/_datetime/?flt0_13=1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' in data)
    ok_('datetime_obj1' not in data)
    ok_('datetime_obj2' not in data)

    # datetime - not empty
    rv = client.get('/admin/_datetime/?flt0_13=0')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' not in data)
    ok_('datetime_obj1' in data)
    ok_('datetime_obj2' in data)

    # time - equals
    rv = client.get('/admin/_datetime/?flt0_14=11%3A10%3A09')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('timeonly_obj1' in data)
    ok_('timeonly_obj2' not in data)

    # time - not equal
    rv = client.get('/admin/_datetime/?flt0_15=11%3A10%3A09')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('timeonly_obj1' not in data)
    ok_('timeonly_obj2' in data)

    # time - greater
    rv = client.get('/admin/_datetime/?flt0_16=11%3A09%3A09')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('timeonly_obj1' in data)
    ok_('timeonly_obj2' not in data)

    # time - smaller
    rv = client.get('/admin/_datetime/?flt0_17=11%3A09%3A09')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('timeonly_obj1' not in data)
    ok_('timeonly_obj2' in data)

    # time - between
    rv = client.get('/admin/_datetime/?flt0_18=10%3A40%3A00+to+11%3A50%3A59')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('timeonly_obj1' in data)
    ok_('timeonly_obj2' not in data)

    # time - not between
    rv = client.get('/admin/_datetime/?flt0_19=10%3A40%3A00+to+11%3A50%3A59')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('timeonly_obj1' not in data)
    ok_('timeonly_obj2' in data)

    # time - empty
    rv = client.get('/admin/_datetime/?flt0_20=1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' in data)
    ok_('timeonly_obj1' not in data)
    ok_('timeonly_obj2' not in data)

    # time - not empty
    rv = client.get('/admin/_datetime/?flt0_20=0')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' not in data)
    ok_('timeonly_obj1' in data)
    ok_('timeonly_obj2' in data)

    # Test enum filter
    view = CustomModelView(Model1, db.session,
                           column_filters=['enum_field'],
                           endpoint="_enumfield")
    admin.add_view(view)

    # enum - equals
    rv = client.get('/admin/_enumfield/?flt0_0=model1_v1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('enum_obj1' in data)
    ok_('enum_obj2' not in data)

    # enum - not equal
    rv = client.get('/admin/_enumfield/?flt0_1=model1_v1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('enum_obj1' not in data)
    ok_('enum_obj2' in data)

    # enum - empty
    rv = client.get('/admin/_enumfield/?flt0_2=1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' in data)
    ok_('enum_obj1' not in data)
    ok_('enum_obj2' not in data)

    # enum - not empty
    rv = client.get('/admin/_enumfield/?flt0_2=0')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' not in data)
    ok_('enum_obj1' in data)
    ok_('enum_obj2' in data)

    # enum - in list
    rv = client.get('/admin/_enumfield/?flt0_3=model1_v1%2Cmodel1_v2')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' not in data)
    ok_('enum_obj1' in data)
    ok_('enum_obj2' in data)

    # enum - not in list
    rv = client.get('/admin/_enumfield/?flt0_4=model1_v1%2Cmodel1_v2')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1_val_1' in data)
    ok_('enum_obj1' not in data)
    ok_('enum_obj2' not in data)

    # Test single custom filter on relation
    view = CustomModelView(Model2, db.session,
                           column_filters = [
                               filters.FilterEqual(Model1.test1, "Test1")
                           ], endpoint='_relation_test')
    admin.add_view(view)

    rv = client.get('/admin/_relation_test/?flt1_0=test1_val_1')
    data = rv.data.decode('utf-8')

    ok_('test1_val_1' in data)
    ok_('test1_val_2' not in data)


def test_hybrid_property():
    app, db, admin = setup()

    class Model1(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String)
        width = db.Column(db.Integer)
        height = db.Column(db.Integer)

        @hybrid_property
        def number_of_pixels(self):
            return self.width * self.height

    db.create_all()

    db.session.add(Model1(id=1, name="test_row_1", width=25, height=25))
    db.session.add(Model1(id=2, name="test_row_2", width=10, height=10))
    db.session.commit()

    client = app.test_client()

    view = CustomModelView(
        Model1, db.session,
        column_default_sort='number_of_pixels',
        column_filters = [filters.IntGreaterFilter(Model1.number_of_pixels,
                                                   'Number of Pixels')]
    )
    admin.add_view(view)

    # filters - hybrid_property integer - greater
    rv = client.get('/admin/model1/?flt0_0=600')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test_row_1' in data)
    ok_('test_row_2' not in data)

    # sorting
    rv = client.get('/admin/model1/?sort=0')
    eq_(rv.status_code, 200)

    _, data = view.get_list(0, None, None, None, None)

    eq_(len(data), 2)
    eq_(data[0].name, 'test_row_2')
    eq_(data[1].name, 'test_row_1')


def test_url_args():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

    view = CustomModelView(Model1, db.session,
                           page_size=2,
                           column_searchable_list=['test1'],
                           column_filters=['test1'])
    admin.add_view(view)

    db.session.add(Model1('data1'))
    db.session.add(Model1('data2'))
    db.session.add(Model1('data3'))
    db.session.add(Model1('data4'))
    db.session.commit()

    client = app.test_client()

    rv = client.get('/admin/model1/')
    data = rv.data.decode('utf-8')
    ok_('data1' in data)
    ok_('data3' not in data)

    # page
    rv = client.get('/admin/model1/?page=1')
    data = rv.data.decode('utf-8')
    ok_('data1' not in data)
    ok_('data3' in data)

    # sort
    rv = client.get('/admin/model1/?sort=0&desc=1')
    data = rv.data.decode('utf-8')
    ok_('data1' not in data)
    ok_('data3' in data)
    ok_('data4' in data)

    # search
    rv = client.get('/admin/model1/?search=data1')
    data = rv.data.decode('utf-8')
    ok_('data1' in data)
    ok_('data2' not in data)

    rv = client.get('/admin/model1/?search=^data1')
    data = rv.data.decode('utf-8')
    ok_('data2' not in data)

    # like
    rv = client.get('/admin/model1/?flt0=0&flt0v=data1')
    data = rv.data.decode('utf-8')
    ok_('data1' in data)

    # not like
    rv = client.get('/admin/model1/?flt0=1&flt0v=data1')
    data = rv.data.decode('utf-8')
    ok_('data2' in data)


def test_non_int_pk():
    app, db, admin = setup()

    class Model(db.Model):
        id = db.Column(db.String, primary_key=True)
        test = db.Column(db.String)

    db.create_all()

    view = CustomModelView(Model, db.session, form_columns=['id', 'test'])
    admin.add_view(view)

    client = app.test_client()

    rv = client.get('/admin/model/')
    eq_(rv.status_code, 200)

    rv = client.post('/admin/model/new/',
                     data=dict(id='test1', test='test2'))
    eq_(rv.status_code, 302)

    rv = client.get('/admin/model/')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test1' in data)

    rv = client.get('/admin/model/edit/?id=test1')
    eq_(rv.status_code, 200)
    data = rv.data.decode('utf-8')
    ok_('test2' in data)


def test_form_columns():
    app, db, admin = setup()

    class Model(db.Model):
        id = db.Column(db.String, primary_key=True)
        int_field = db.Column(db.Integer)
        datetime_field = db.Column(db.DateTime)
        text_field = db.Column(db.UnicodeText)
        excluded_column = db.Column(db.String)

    class ChildModel(db.Model):
        id = db.Column(db.String, primary_key=True)
        model_id = db.Column(db.Integer, db.ForeignKey(Model.id))
        model = db.relationship(Model, backref='backref')

    db.create_all()

    view1 = CustomModelView(Model, db.session, endpoint='view1',
                            form_columns=('int_field', 'text_field'))
    view2 = CustomModelView(Model, db.session, endpoint='view2',
                            form_excluded_columns=('excluded_column',))
    view3 = CustomModelView(ChildModel, db.session, endpoint='view3')

    form1 = view1.create_form()
    form2 = view2.create_form()
    form3 = view3.create_form()

    ok_('int_field' in form1._fields)
    ok_('text_field' in form1._fields)
    ok_('datetime_field' not in form1._fields)

    ok_('excluded_column' not in form2._fields)

    ok_(type(form3.model).__name__ == 'QuerySelectField')

    # test form_columns with model objects
    view4 = CustomModelView(Model, db.session, endpoint='view1',
                            form_columns=[Model.int_field])
    form4 = view4.create_form()
    ok_('int_field' in form4._fields)


@raises(Exception)
def test_complex_form_columns():
    app, db, admin = setup()
    M1, M2 = create_models(db)

    # test using a form column in another table
    view = CustomModelView(M2, db.session, form_columns=['model1.test1'])
    form = view.create_form()


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():
    app, db, admin = setup()

    class Model(db.Model):
        id = db.Column(db.String, primary_key=True)
        test = db.Column(db.String)

    db.create_all()

    view1 = CustomModelView(Model, db.session, endpoint='view1')
    view2 = CustomModelView(Model, db.session, endpoint='view2', form_overrides=dict(test=fields.FileField))
    admin.add_view(view1)
    admin.add_view(view2)

    eq_(view1._create_form_class.test.field_class, fields.StringField)
    eq_(view2._create_form_class.test.field_class, fields.FileField)


def test_form_onetoone():
    app, db, admin = setup()

    class Model1(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        test = db.Column(db.String)

    class Model2(db.Model):
        id = db.Column(db.Integer, primary_key=True)

        model1_id = db.Column(db.Integer, db.ForeignKey(Model1.id))
        model1 = db.relationship(Model1, backref=db.backref('model2', uselist=False))

    db.create_all()

    view1 = CustomModelView(Model1, db.session, endpoint='view1')
    view2 = CustomModelView(Model2, db.session, endpoint='view2')
    admin.add_view(view1)
    admin.add_view(view2)

    model1 = Model1(test='test')
    model2 = Model2(model1=model1)
    db.session.add(model1)
    db.session.add(model2)
    db.session.commit()

    eq_(model1.model2, model2)
    eq_(model2.model1, model1)

    eq_(view1._create_form_class.model2.field_class.widget.multiple, False)
    eq_(view2._create_form_class.model1.field_class.widget.multiple, False)


def test_relations():
    # TODO: test relations
    pass


def test_on_model_change_delete():
    app, db, admin = setup()
    Model1, _ = create_models(db)

    class ModelView(CustomModelView):
        def on_model_change(self, form, model, is_created):
            model.test1 = model.test1.upper()

        def on_model_delete(self, model):
            self.deleted = True

    view = ModelView(Model1, db.session)
    admin.add_view(view)

    client = app.test_client()

    client.post('/admin/model1/new/',
                data=dict(test1='test1large', test2='test2'))

    model = db.session.query(Model1).first()
    eq_(model.test1, 'TEST1LARGE')

    url = '/admin/model1/edit/?id=%s' % model.id
    client.post(url, data=dict(test1='test1small', test2='test2large'))

    model = db.session.query(Model1).first()
    eq_(model.test1, 'TEST1SMALL')

    url = '/admin/model1/delete/?id=%s' % model.id
    client.post(url)
    ok_(view.deleted)


def test_multiple_delete():
    app, db, admin = setup()
    M1, _ = create_models(db)

    db.session.add_all([M1('a'), M1('b'), M1('c')])
    db.session.commit()
    eq_(M1.query.count(), 3)

    view = ModelView(M1, db.session)
    admin.add_view(view)

    client = app.test_client()

    rv = client.post('/admin/model1/action/', data=dict(action='delete', rowid=[1, 2, 3]))
    eq_(rv.status_code, 302)
    eq_(M1.query.count(), 0)


def test_default_sort():
    app, db, admin = setup()
    M1, _ = create_models(db)

    db.session.add_all([M1('c'), M1('b'), M1('a')])
    db.session.commit()
    eq_(M1.query.count(), 3)

    view = CustomModelView(M1, db.session, column_default_sort='test1')
    admin.add_view(view)

    _, data = view.get_list(0, None, None, None, None)

    eq_(len(data), 3)
    eq_(data[0].test1, 'a')
    eq_(data[1].test1, 'b')
    eq_(data[2].test1, 'c')

    # test default sort on renamed columns - with column_list scaffolding
    view2 = CustomModelView(M1, db.session, column_default_sort='test1',
                            column_labels={'test1': 'blah'}, endpoint='m1_2')
    admin.add_view(view2)

    _, data = view2.get_list(0, None, None, None, None)

    eq_(len(data), 3)
    eq_(data[0].test1, 'a')
    eq_(data[1].test1, 'b')
    eq_(data[2].test1, 'c')

    # test default sort on renamed columns - without column_list scaffolding
    view3 = CustomModelView(M1, db.session, column_default_sort='test1',
                            column_labels={'test1': 'blah'}, endpoint='m1_3',
                            column_list=['test1'])
    admin.add_view(view3)

    _, data = view3.get_list(0, None, None, None, None)

    eq_(len(data), 3)
    eq_(data[0].test1, 'a')
    eq_(data[1].test1, 'b')
    eq_(data[2].test1, 'c')


def test_complex_sort():
    app, db, admin = setup()
    M1, M2 = create_models(db)

    m1 = M1('b')
    db.session.add(m1)
    db.session.add(M2('c', model1=m1))

    m2 = M1('a')
    db.session.add(m2)
    db.session.add(M2('c', model1=m2))

    db.session.commit()

    # test sorting on relation string - 'model1.test1'
    view = CustomModelView(M2, db.session,
                           column_list=['string_field', 'model1.test1'],
                           column_sortable_list=['model1.test1'])
    admin.add_view(view)

    client = app.test_client()

    rv = client.get('/admin/model2/?sort=1')
    eq_(rv.status_code, 200)


@raises(Exception)
def test_complex_sort_exception():
    app, db, admin = setup()
    M1, M2 = create_models(db)

    # test column_sortable_list on a related table's column object
    view = CustomModelView(M2, db.session, endpoint="model2_3",
                           column_sortable_list=[M1.test1])
    admin.add_view(view)

    sort_column = view._get_column_by_idx(0)[0]
    _, data = view.get_list(0, sort_column, False, None, None)

    eq_(len(data), 2)
    eq_(data[0].model1.test1, 'a')
    eq_(data[1].model1.test1, 'b')


def test_default_complex_sort():
    app, db, admin = setup()
    M1, M2 = create_models(db)

    m1 = M1('b')
    db.session.add(m1)
    db.session.add(M2('c', model1=m1))

    m2 = M1('a')
    db.session.add(m2)
    db.session.add(M2('c', model1=m2))

    db.session.commit()

    view = CustomModelView(M2, db.session, column_default_sort='model1.test1')
    admin.add_view(view)

    _, data = view.get_list(0, None, None, None, None)

    eq_(len(data), 2)
    eq_(data[0].model1.test1, 'a')
    eq_(data[1].model1.test1, 'b')

    # test column_default_sort on a related table's column object
    view2 = CustomModelView(M2, db.session, endpoint="model2_2",
                            column_default_sort=(M1.test1, False))
    admin.add_view(view2)

    _, data = view2.get_list(0, None, None, None, None)

    eq_(len(data), 2)
    eq_(data[0].model1.test1, 'a')
    eq_(data[1].model1.test1, 'b')


def test_extra_fields():
    app, db, admin = setup()

    Model1, _ = create_models(db)

    view = CustomModelView(
        Model1, db.session,
        form_extra_fields={
            'extra_field': fields.StringField('Extra Field')
        }
    )
    admin.add_view(view)

    client = app.test_client()

    rv = client.get('/admin/model1/new/')
    eq_(rv.status_code, 200)

    # Check presence and order
    data = rv.data.decode('utf-8')
    ok_('Extra Field' in data)
    pos1 = data.find('Extra Field')
    pos2 = data.find('Test1')
    ok_(pos2 < pos1)


def test_extra_field_order():
    app, db, admin = setup()

    Model1, _ = create_models(db)

    view = CustomModelView(
        Model1, db.session,
        form_columns=('extra_field', 'test1'),
        form_extra_fields={
            'extra_field': fields.StringField('Extra Field')
        }
    )
    admin.add_view(view)

    client = app.test_client()

    rv = client.get('/admin/model1/new/')
    eq_(rv.status_code, 200)

    # Check presence and order
    data = rv.data.decode('utf-8')
    pos1 = data.find('Extra Field')
    pos2 = data.find('Test1')
    ok_(pos2 > pos1)

def test_modelview_localization():
    def test_locale(locale):
        try:
            app, db, admin = setup()

            app.config['BABEL_DEFAULT_LOCALE'] = locale
            babel = Babel(app)

            Model1, _ = create_models(db)

            view = CustomModelView(
                Model1, db.session,
                column_filters=['test1', 'bool_field', 'date_field', 'datetime_field', 'time_field']
            )

            admin.add_view(view)

            client = app.test_client()

            rv = client.get('/admin/model1/')
            eq_(rv.status_code, 200)

            rv = client.get('/admin/model1/new/')
            eq_(rv.status_code, 200)
        except:
            print("Error on the following locale:", locale)
            raise

    locales = ['en', 'cs', 'de', 'es', 'fa', 'fr', 'pt', 'ru', 'zh_CN', 'zh_TW']
    for locale in locales:
        test_locale(locale)


def test_modelview_named_filter_localization():
    app, db, admin = setup()

    app.config['BABEL_DEFAULT_LOCALE'] = 'de'
    Babel(app)

    Model1, _ = create_models(db)

    view = CustomModelView(
        Model1, db.session,
        named_filter_urls=True,
        column_filters=['test1'],
    )

    filters = view.get_filters()
    flt = filters[2]
    with app.test_request_context():
        flt_name = view.get_filter_arg(2, flt)
    eq_('test1_equals', flt_name)


def test_custom_form_base():
    app, db, admin = setup()

    class TestForm(form.BaseForm):
        pass

    Model1, _ = create_models(db)

    view = CustomModelView(
        Model1, db.session,
        form_base_class=TestForm
    )
    admin.add_view(view)

    ok_(hasattr(view._create_form_class, 'test1'))

    create_form = view.create_form()
    ok_(isinstance(create_form, TestForm))


def test_ajax_fk():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

    view = CustomModelView(
        Model2, db.session,
        url='view',
        form_ajax_refs={
            'model1': {
                'fields': ('test1', 'test2')
            }
        }
    )
    admin.add_view(view)

    ok_(u'model1' in view._form_ajax_refs)

    model = Model1(u'first')
    model2 = Model1(u'foo', u'bar')
    db.session.add_all([model, model2])
    db.session.commit()

    # Check loader
    loader = view._form_ajax_refs[u'model1']
    mdl = loader.get_one(model.id)
    eq_(mdl.test1, model.test1)

    items = loader.get_list(u'fir')
    eq_(len(items), 1)
    eq_(items[0].id, model.id)

    items = loader.get_list(u'bar')
    eq_(len(items), 1)
    eq_(items[0].test1, u'foo')

    # Check form generation
    form = view.create_form()
    eq_(form.model1.__class__.__name__, u'AjaxSelectField')

    with app.test_request_context('/admin/view/'):
        ok_(u'value=""' not in form.model1())

        form.model1.data = model
        ok_(u'data-json="[%s, &quot;first&quot;]"' % model.id in form.model1())
        ok_(u'value="1"' in form.model1())

    # Check querying
    client = app.test_client()

    req = client.get(u'/admin/view/ajax/lookup/?name=model1&query=foo')
    eq_(req.data.decode('utf-8'), u'[[%s, "foo"]]' % model2.id)

    # Check submitting
    req = client.post('/admin/view/new/', data={u'model1': as_unicode(model.id)})
    mdl = db.session.query(Model2).first()

    ok_(mdl is not None)
    ok_(mdl.model1 is not None)
    eq_(mdl.model1.id, model.id)
    eq_(mdl.model1.test1, u'first')


def test_ajax_fk_multi():
    app, db, admin = setup()

    class Model1(db.Model):
        __tablename__ = 'model1'

        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(20))

        def __str__(self):
            return self.name

    table = db.Table('m2m', db.Model.metadata,
                     db.Column('model1_id', db.Integer, db.ForeignKey('model1.id')),
                     db.Column('model2_id', db.Integer, db.ForeignKey('model2.id'))
                     )

    class Model2(db.Model):
        __tablename__ = 'model2'

        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(20))

        model1_id = db.Column(db.Integer(), db.ForeignKey(Model1.id))
        model1 = db.relationship(Model1, backref='models2', secondary=table)

    db.create_all()

    view = CustomModelView(
        Model2, db.session,
        url='view',
        form_ajax_refs={
            'model1': {
                'fields': ['name']
            }
        }
    )
    admin.add_view(view)

    ok_(u'model1' in view._form_ajax_refs)

    model = Model1(name=u'first')
    db.session.add_all([model, Model1(name=u'foo')])
    db.session.commit()

    # Check form generation
    form = view.create_form()
    eq_(form.model1.__class__.__name__, u'AjaxSelectMultipleField')

    with app.test_request_context('/admin/view/'):
        ok_(u'data-json="[]"' in form.model1())

        form.model1.data = [model]
        ok_(u'data-json="[[1, &quot;first&quot;]]"' in form.model1())

    # Check submitting
    client = app.test_client()
    client.post('/admin/view/new/', data={u'model1': as_unicode(model.id)})
    mdl = db.session.query(Model2).first()

    ok_(mdl is not None)
    ok_(mdl.model1 is not None)
    eq_(len(mdl.model1), 1)


def test_safe_redirect():
    app, db, admin = setup()
    Model1, _ = create_models(db)

    view = CustomModelView(Model1, db.session)
    admin.add_view(view)

    client = app.test_client()

    rv = client.post('/admin/model1/new/?url=http://localhost/admin/model2view/',
                     data=dict(test1='test1large', test2='test2',
                               _continue_editing='Save and Continue Editing'))

    eq_(rv.status_code, 302)
    assert_true(rv.location.startswith('http://localhost/admin/model1/edit/'))
    assert_true('url=http%3A%2F%2Flocalhost%2Fadmin%2Fmodel2view%2F' in rv.location)
    assert_true('id=1' in rv.location)

    rv = client.post('/admin/model1/new/?url=http://google.com/evil/',
                     data=dict(test1='test1large', test2='test2',
                               _continue_editing='Save and Continue Editing'))

    eq_(rv.status_code, 302)
    assert_true(rv.location.startswith('http://localhost/admin/model1/edit/'))
    assert_true('url=%2Fadmin%2Fmodel1%2F' in rv.location)
    assert_true('id=2' in rv.location)


def test_simple_list_pager():
    app, db, admin = setup()
    Model1, _ = create_models(db)

    class TestModelView(CustomModelView):
        simple_list_pager = True

        def get_count_query(self):
            assert False

    view = TestModelView(Model1, db.session)
    admin.add_view(view)

    count, data = view.get_list(0, None, None, None, None)
    assert_true(count is None)


def test_unlimited_page_size():
    app, db, admin = setup()
    M1, _ = create_models(db)

    db.session.add_all([M1('1'), M1('2'), M1('3'), M1('4'), M1('5'), M1('6'),
                        M1('7'), M1('8'), M1('9'), M1('10'), M1('11'),
                        M1('12'), M1('13'), M1('14'), M1('15'), M1('16'),
                        M1('17'), M1('18'), M1('19'), M1('20'), M1('21')])

    view = CustomModelView(M1, db.session)

    # test 0 as page_size
    _, data = view.get_list(0, None, None, None, None, execute=True,
                            page_size=0)
    eq_(len(data), 21)

    # test False as page_size
    _, data = view.get_list(0, None, None, None, None, execute=True,
                            page_size=False)
    eq_(len(data), 21)


def test_advanced_joins():
    app, db, admin = setup()

    class Model1(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        val1 = db.Column(db.String(20))
        test = db.Column(db.String(20))

    class Model2(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        val2 = db.Column(db.String(20))

        model1_id = db.Column(db.Integer, db.ForeignKey(Model1.id))
        model1 = db.relationship(Model1, backref='model2')

    class Model3(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        val2 = db.Column(db.String(20))

        model2_id = db.Column(db.Integer, db.ForeignKey(Model2.id))
        model2 = db.relationship(Model2, backref='model3')

    view1 = CustomModelView(Model1, db.session)
    admin.add_view(view1)

    view2 = CustomModelView(Model2, db.session)
    admin.add_view(view2)

    view3 = CustomModelView(Model3, db.session)
    admin.add_view(view3)

    # Test joins
    attr, path = tools.get_field_with_path(Model2, 'model1.val1')
    eq_(attr, Model1.val1)
    eq_(path, [Model2.model1])

    attr, path = tools.get_field_with_path(Model1, 'model2.val2')
    eq_(attr, Model2.val2)
    eq_(id(path[0]), id(Model1.model2))

    attr, path = tools.get_field_with_path(Model3, 'model2.model1.val1')
    eq_(attr, Model1.val1)
    eq_(path, [Model3.model2, Model2.model1])

    # Test how joins are applied
    query = view3.get_query()

    joins = {}
    q1, joins, alias = view3._apply_path_joins(query, joins, path)
    ok_((True, Model3.model2) in joins)
    ok_((True, Model2.model1) in joins)
    ok_(alias is not None)

    # Check if another join would use same path
    attr, path = tools.get_field_with_path(Model2, 'model1.test')
    q2, joins, alias = view2._apply_path_joins(query, joins, path)

    eq_(len(joins), 2)
    for p in q2._join_entities:
        ok_(p in q1._join_entities)

    ok_(alias is not None)

    # Check if normal properties are supported by tools.get_field_with_path
    attr, path = tools.get_field_with_path(Model2, Model1.test)
    eq_(attr, Model1.test)
    eq_(path, [Model1.__table__])

    q3, joins, alias = view2._apply_path_joins(view2.get_query(), joins, path)
    eq_(len(joins), 3)
    ok_(alias is None)


def test_multipath_joins():
    app, db, admin = setup()

    class Model1(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        val1 = db.Column(db.String(20))
        test = db.Column(db.String(20))

    class Model2(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        val2 = db.Column(db.String(20))

        first_id = db.Column(db.Integer, db.ForeignKey(Model1.id))
        first = db.relationship(Model1, backref='first', foreign_keys=[first_id])

        second_id = db.Column(db.Integer, db.ForeignKey(Model1.id))
        second = db.relationship(Model1, backref='second', foreign_keys=[second_id])

    db.create_all()

    view = CustomModelView(Model2, db.session, filters=['first.test'])
    admin.add_view(view)

    client = app.test_client()

    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)


def test_export_csv():
    app, db, admin = setup()
    Model1, Model2 = create_models(db)

    for x in range(5):
        fill_db(db, Model1, Model2)

    view = CustomModelView(Model1, db.session, can_export=True,
                           column_list=['test1', 'test2'], export_max_rows=2,
                           endpoint='row_limit_2')
    admin.add_view(view)

    client = app.test_client()

    # test export_max_rows
    rv = client.get('/admin/row_limit_2/export/csv/')
    data = rv.data.decode('utf-8')
    eq_(rv.status_code, 200)
    ok_("Test1,Test2\r\n"
        "test1_val_1,test2_val_1\r\n"
        "test1_val_2,test2_val_2\r\n" == data)

    view = CustomModelView(Model1, db.session, can_export=True,
                           column_list=['test1', 'test2'],
                           endpoint='no_row_limit')
    admin.add_view(view)

    # test row limit without export_max_rows
    rv = client.get('/admin/no_row_limit/export/csv/')
    data = rv.data.decode('utf-8')
    eq_(rv.status_code, 200)
    ok_(len(data.splitlines()) > 21)
