from nose.tools import eq_, ok_, raises

from wtforms import fields

from flask.ext.admin import form
from flask.ext.admin._compat import as_unicode
from flask.ext.admin._compat import iteritems
from flask.ext.admin.contrib.sqla import ModelView, filters
from flask.ext.babelex import Babel

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):
            self.string_field = string_field
            self.int_field = int_field
            self.bool_field = bool_field
            self.model1 = model1
            self.float_field = float_field

        id = db.Column(db.Integer, primary_key=True)
        string_field = db.Column(db.String)
        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 test_model():
    app, db, admin = setup()
    Model1, Model2 = create_models(db)
    db.create_all()

    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)

    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)


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(Model1, db.session,
                           column_searchable_list=['test1', 'test2'])
    admin.add_view(view)

    eq_(view._search_supported, True)
    eq_(len(view._search_fields), 2)
    ok_(isinstance(view._search_fields[0], db.Column))
    ok_(isinstance(view._search_fields[1], db.Column))
    eq_(view._search_fields[0].name, 'test1')
    eq_(view._search_fields[1].name, 'test2')

    db.session.add(Model1('model1'))
    db.session.add(Model1('model2'))
    db.session.commit()

    client = app.test_client()

    rv = client.get('/admin/model1/?search=model1')
    data = rv.data.decode('utf-8')
    ok_('model1' in data)
    ok_('model2' not 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')
    db.session.add(m1)
    db.session.add(Model2('model2', model1=m1))
    db.session.add(Model2('model3'))
    db.session.commit()

    client = app.test_client()

    rv = client.get('/admin/model2/?search=model1')
    data = rv.data.decode('utf-8')
    ok_('model1' in data)
    ok_('model3' 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_filters():
    app, db, admin = setup()

    Model1, Model2 = create_models(db)

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

    eq_(len(view._filters), 7)

    eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
        [
            (0, u'equals'),
            (1, u'not equal'),
            (2, u'contains'),
            (3, u'not contains'),
            (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'equals'),
            (1, u'not equal'),
            (2, u'contains'),
            (3, u'not contains'),
            (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'equals'),
            (8, u'not equal'),
            (9, u'contains'),
            (10, u'not contains'),
            (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'equals'),
            (15, u'not equal'),
            (16, u'contains'),
            (17, u'not contains'),
            (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'equals'),
            (22, u'not equal'),
            (23, u'contains'),
            (24, u'not contains'),
            (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
    model1_obj1 = Model1('test1_val_1', 'test2_val_1', bool_field=True)
    model1_obj2 = Model1('test1_val_2', 'test2_val_2')
    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)
    
    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,
        date_obj1, timeonly_obj1, datetime_obj1,
        date_obj2, timeonly_obj2, datetime_obj2,
        enum_obj1, enum_obj2, empty_obj
    ])
    db.session.commit()

    client = app.test_client()

    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)

    rv = client.get('/admin/model1/?flt0_6=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)

    # 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, 'equals'),
            (1, 'not equal'),
            (2, 'contains'),
            (3, 'not contains'),
            (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 - 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 - 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 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_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')

    # TODO: form_args


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.kwargs['widget'].multiple, False)
    eq_(view2._create_form_class.model1.kwargs['widget'].multiple, False)


def test_relations():
    # TODO: test relations
    pass


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

    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')


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')

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_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)
    db.create_all()

    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'))

    eq_(rv.status_code, 302)
    eq_(rv.location, 'http://localhost/admin/model2view/')

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

    eq_(rv.status_code, 302)
    eq_(rv.location, 'http://localhost/admin/model1/')
