import pytest from wtforms import fields, validators from flask_admin import form from flask_admin.form.fields import Select2Field, DateTimeField 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 sqlalchemy import cast from sqlalchemy_utils import EmailType, ChoiceType, UUIDType, URLType, CurrencyType, ColorType, ArrowType, IPAddressType from . import setup from datetime import datetime, time, date import uuid import enum import arrow 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) form_choices = { 'choice_field': [ ('choice-1', 'One'), ('choice-2', 'Two') ] } 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, email_field=None, choice_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.email_field = email_field self.choice_field = choice_field self.enum_field = enum_field class EnumChoices(enum.Enum): first = 1 second = 2 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) date_field = db.Column(db.Date) time_field = db.Column(db.Time) datetime_field = db.Column(db.DateTime) email_field = db.Column(EmailType) enum_field = db.Column(db.Enum('model1_v1', 'model1_v2'), nullable=True) choice_field = db.Column(db.String, nullable=True) sqla_utils_choice = db.Column(ChoiceType([ ('choice-1', u'First choice'), ('choice-2', u'Second choice') ])) sqla_utils_enum = db.Column(ChoiceType(EnumChoices, impl=db.Integer())) sqla_utils_arrow = db.Column(ArrowType, default=arrow.utcnow()) sqla_utils_uuid = db.Column(UUIDType(binary=False), default=uuid.uuid4) sqla_utils_url = db.Column(URLType) sqla_utils_ip_address = db.Column(IPAddressType) sqla_utils_currency = db.Column(CurrencyType) sqla_utils_color = db.Column(ColorType) 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(lambda: 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', email_field="test@test.com", choice_field="choice-1") 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() with app.app_context(): Model1, Model2 = create_models(db) view = CustomModelView(Model1, db.session) admin.add_view(view) assert view.model == Model1 assert view.name == 'Model1' assert view.endpoint == 'model1' assert view._primary_key == 'id' assert 'test1' in view._sortable_columns assert 'test2' in view._sortable_columns assert 'test3' in view._sortable_columns assert 'test4' in view._sortable_columns assert view._create_form_class is not None assert view._edit_form_class is not None assert not view._search_supported assert view._filters is None # Verify form assert view._create_form_class.test1.field_class == fields.StringField assert view._create_form_class.test2.field_class == fields.StringField assert view._create_form_class.test3.field_class == fields.TextAreaField assert view._create_form_class.test4.field_class == fields.TextAreaField assert view._create_form_class.email_field.field_class == fields.StringField assert view._create_form_class.choice_field.field_class == Select2Field assert view._create_form_class.enum_field.field_class == Select2Field assert view._create_form_class.sqla_utils_choice.field_class == Select2Field assert view._create_form_class.sqla_utils_enum.field_class == Select2Field assert view._create_form_class.sqla_utils_arrow.field_class == DateTimeField assert view._create_form_class.sqla_utils_uuid.field_class == fields.StringField assert view._create_form_class.sqla_utils_url.field_class == fields.StringField assert view._create_form_class.sqla_utils_ip_address.field_class == fields.StringField assert view._create_form_class.sqla_utils_currency.field_class == fields.StringField assert view._create_form_class.sqla_utils_color.field_class == fields.StringField # Make some test clients client = app.test_client() # check that we can retrieve a list view rv = client.get('/admin/model1/') assert rv.status_code == 200 # check that we can retrieve a 'create' view rv = client.get('/admin/model1/new/') assert rv.status_code == 200 # create a new record uuid_obj = uuid.uuid4() rv = client.post( '/admin/model1/new/', data=dict( test1='test1large', test2='test2', time_field=time(0, 0, 0), email_field="Test@TEST.com", choice_field="choice-1", enum_field='model1_v1', sqla_utils_choice="choice-1", sqla_utils_enum=1, sqla_utils_arrow='2018-10-27 14:17:00', sqla_utils_uuid=str(uuid_obj), sqla_utils_url="http://www.example.com", sqla_utils_ip_address='127.0.0.1', sqla_utils_currency='USD', sqla_utils_color='#f0f0f0', ) ) assert rv.status_code == 302 # check that the new record was persisted model = db.session.query(Model1).first() assert model.test1 == u'test1large' assert model.test2 == u'test2' assert model.test3 == u'' assert model.test4 == u'' assert model.email_field == u'test@test.com' assert model.choice_field == u'choice-1' assert model.enum_field == u'model1_v1' assert model.sqla_utils_choice == u'choice-1' assert model.sqla_utils_enum.value == 1 assert model.sqla_utils_arrow == arrow.get('2018-10-27 14:17:00') assert model.sqla_utils_uuid == uuid_obj assert model.sqla_utils_url == "http://www.example.com" assert str(model.sqla_utils_ip_address) == '127.0.0.1' assert str(model.sqla_utils_currency) == 'USD' assert model.sqla_utils_color.hex == '#f0f0f0' # check that the new record shows up on the list view rv = client.get('/admin/model1/') assert rv.status_code == 200 assert u'test1large' in rv.data.decode('utf-8') # check that we can retrieve an edit view url = '/admin/model1/edit/?id=%s' % model.id rv = client.get(url) assert rv.status_code == 200 # verify that midnight does not show as blank assert u'00:00:00' in rv.data.decode('utf-8') # edit the record new_uuid_obj = uuid.uuid4() rv = client.post(url, data=dict(test1='test1small', test2='test2large', email_field='Test2@TEST.com', choice_field='__None', enum_field='__None', sqla_utils_choice='__None', sqla_utils_enum='__None', sqla_utils_arrow='', sqla_utils_uuid=str(new_uuid_obj), sqla_utils_url='', sqla_utils_ip_address='', sqla_utils_currency='', sqla_utils_color='', )) assert rv.status_code == 302 # check that the changes were persisted model = db.session.query(Model1).first() assert model.test1 == 'test1small' assert model.test2 == 'test2large' assert model.test3 == '' assert model.test4 == '' assert model.email_field == u'test2@test.com' assert model.choice_field is None assert model.enum_field is None assert model.sqla_utils_choice is None assert model.sqla_utils_enum is None assert model.sqla_utils_arrow is None assert model.sqla_utils_uuid == new_uuid_obj assert model.sqla_utils_url is None assert model.sqla_utils_ip_address is None assert model.sqla_utils_currency is None assert model.sqla_utils_color is None # check that the model can be deleted url = '/admin/model1/delete/?id=%s' % model.id rv = client.post(url) assert rv.status_code == 302 assert db.session.query(Model1).count() == 0 @pytest.mark.xfail(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() with app.app_context(): 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) # 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) assert len(view._list_columns) == 2 assert view._list_columns == [('test1', 'Column1'), ('test3', 'Test3')] client = app.test_client() rv = client.get('/admin/model1/') data = rv.data.decode('utf-8') assert 'Column1' in data assert 'Test2' not in data assert len(view2._list_columns) == 2 assert view2._list_columns == [('test1', 'Column1'), ('test3', 'Test3')] rv = client.get('/admin/model1_2/') data = rv.data.decode('utf-8') assert 'Column1' in data assert 'Test2' not in data def test_complex_list_columns(): app, db, admin = setup() with app.app_context(): 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/') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'model1_val1' in data def test_exclude_columns(): app, db, admin = setup() with app.app_context(): Model1, Model2 = create_models(db) view = CustomModelView( Model1, db.session, column_exclude_list=['test2', 'test4', 'enum_field', 'date_field', 'time_field', 'datetime_field', 'sqla_utils_choice', 'sqla_utils_enum', 'sqla_utils_arrow', 'sqla_utils_uuid', 'sqla_utils_url', 'sqla_utils_ip_address', 'sqla_utils_currency', 'sqla_utils_color'] ) admin.add_view(view) assert \ view._list_columns == \ [('test1', 'Test1'), ('test3', 'Test3'), ('bool_field', 'Bool Field'), ('email_field', 'Email Field'), ('choice_field', 'Choice Field')] client = app.test_client() rv = client.get('/admin/model1/') data = rv.data.decode('utf-8') assert 'Test1' in data assert 'Test2' not in data def test_column_searchable_list(): app, db, admin = setup() with app.app_context(): Model1, Model2 = create_models(db) view = CustomModelView(Model2, db.session, column_searchable_list=['string_field', 'int_field']) admin.add_view(view) assert view._search_supported assert len(view._search_fields) == 2 assert isinstance(view._search_fields[0][0], db.Column) assert isinstance(view._search_fields[1][0], db.Column) assert view._search_fields[0][0].name == 'string_field' assert 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') assert 'model1-test' in data assert 'model2-test' not in data rv = client.get('/admin/model2/?search=9000') data = rv.data.decode('utf-8') assert 'model1-test' not in data assert 'model2-test' in data def test_extra_args_search(): app, db, admin = setup() with app.app_context(): Model1, Model2 = create_models(db) view1 = CustomModelView(Model1, db.session, column_searchable_list=['test1', ]) admin.add_view(view1) db.session.add(Model2('model1-test', )) db.session.commit() client = app.test_client() # check that extra args in the url are propagated as hidden fields in the search form rv = client.get('/admin/model1/?search=model1&foo=bar') data = rv.data.decode('utf-8') assert '' in data def test_extra_args_filter(): app, db, admin = setup() with app.app_context(): Model1, Model2 = create_models(db) view2 = CustomModelView(Model2, db.session, column_filters=['int_field', ]) admin.add_view(view2) db.session.add(Model2('model2-test', 5000)) db.session.commit() client = app.test_client() # check that extra args in the url are propagated as hidden fields in the form rv = client.get('/admin/model2/?flt1_0=5000&foo=bar') data = rv.data.decode('utf-8') assert '' in data def test_complex_searchable_list(): app, db, admin = setup() with app.app_context(): Model1, Model2 = create_models(db) view = CustomModelView(Model2, db.session, column_searchable_list=['model1.test1']) admin.add_view(view) view2 = CustomModelView(Model1, db.session, column_searchable_list=[Model2.string_field]) admin.add_view(view2) 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') assert 'model2-test1-val' in data assert 'model2-test2-val' not in data # test relation object - Model2.string_field rv = client.get('/admin/model1/?search=model2-test1') data = rv.data.decode('utf-8') assert 'model1-test1-val' in data assert 'model1-test2-val' not in data def test_complex_searchable_list_missing_children(): app, db, admin = setup() with app.app_context(): 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') assert 'magic string' in data def test_column_editable_list(): app, db, admin = setup() with app.app_context(): Model1, Model2 = create_models(db) view = CustomModelView(Model1, db.session, column_editable_list=['test1', 'enum_field']) admin.add_view(view) # Test in-line editing for relations view = CustomModelView(Model2, db.session, column_editable_list=['model1']) 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') assert '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') assert 'Record was successfully saved.' == data # ensure the value has changed rv = client.get('/admin/model1/') data = rv.data.decode('utf-8') assert 'change-success-1' in data # Test validation error rv = client.post('/admin/model1/ajax/update/', data={ 'list_form_pk': '1', 'enum_field': 'problematic-input', }) assert 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') assert 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') assert 'problematic-input' not in data rv = client.post('/admin/model2/ajax/update/', data={ 'list_form_pk': '1', 'model1': '3', }) data = rv.data.decode('utf-8') assert 'Record was successfully saved.' == data # confirm the value has changed rv = client.get('/admin/model2/') data = rv.data.decode('utf-8') assert 'test1_val_3' in data def test_details_view(): app, db, admin = setup() with app.app_context(): 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') assert '/admin/model1/details/' not in data # ensure link to details view appears rv = client.get('/admin/model2/') data = rv.data.decode('utf-8') assert '/admin/model2/details/' in data # test redirection when details are disabled rv = client.get('/admin/model1/details/?url=%2Fadmin%2Fmodel1%2F&id=1') assert 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') assert 'String Field' in data assert 'test2_val_1' in data assert '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') assert 'String Field' in data assert 'test2_val_1' in data assert '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() with app.app_context(): 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') assert 'Record was successfully saved.' == data # ensure the value has changed rv = client.get('/admin/model1/') data = rv.data.decode('utf-8') assert 'change-success-1' in data def test_column_filters(): app, db, admin = setup() with app.app_context(): Model1, Model2 = create_models(db) view1 = CustomModelView( Model1, db.session, column_filters=['test1'] ) admin.add_view(view1) client = app.test_client() assert len(view1._filters) == 7 # Generate views view2 = CustomModelView(Model2, db.session, column_filters=['model1']) view5 = CustomModelView(Model1, db.session, column_filters=['test1'], endpoint='_strings') admin.add_view(view5) view6 = CustomModelView(Model2, db.session, column_filters=['int_field']) admin.add_view(view6) view7 = CustomModelView(Model1, db.session, column_filters=['bool_field'], endpoint="_bools") admin.add_view(view7) view8 = CustomModelView(Model2, db.session, column_filters=['float_field'], endpoint="_float") admin.add_view(view8) view9 = CustomModelView( Model2, db.session, endpoint='_model2', column_filters=['model1.bool_field'], column_list=[ 'string_field', 'model1.id', 'model1.bool_field', ] ) admin.add_view(view9) view10 = CustomModelView( Model1, db.session, column_filters=['test1'], endpoint='_model3', named_filter_urls=True ) admin.add_view(view10) view11 = CustomModelView(Model1, db.session, column_filters=['date_field', 'datetime_field', 'time_field'], endpoint="_datetime") admin.add_view(view11) view12 = CustomModelView(Model1, db.session, column_filters=['enum_field'], endpoint="_enumfield") admin.add_view(view12) view13 = CustomModelView(Model2, db.session, column_filters=[ filters.FilterEqual(Model1.test1, "Test1") ], endpoint='_relation_test') admin.add_view(view13) # Test views assert \ [(f['index'], f['operation']) for f in view1._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 property0 assert \ [(f['index'], f['operation']) for f in view2._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'), ] assert \ [(f['index'], f['operation']) for f in view2._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'), ] assert \ [(f['index'], f['operation']) for f in view2._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'), ] assert \ [(f['index'], f['operation']) for f in view2._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'), ] assert \ [(f['index'], f['operation']) for f in view2._filter_groups[u'Model1 / Bool Field']] == \ [ (28, u'equals'), (29, u'not equal'), ] assert \ [(f['index'], f['operation']) for f in view2._filter_groups[u'Model1 / Date Field']] == \ [ (30, u'equals'), (31, u'not equal'), (32, u'greater than'), (33, u'smaller than'), (34, u'between'), (35, u'not between'), (36, u'empty'), ] assert \ [(f['index'], f['operation']) for f in view2._filter_groups[u'Model1 / Time Field']] == \ [ (37, u'equals'), (38, u'not equal'), (39, u'greater than'), (40, u'smaller than'), (41, u'between'), (42, u'not between'), (43, u'empty'), ] assert \ [(f['index'], f['operation']) for f in view2._filter_groups[u'Model1 / Datetime Field']] == \ [ (44, u'equals'), (45, u'not equal'), (46, u'greater than'), (47, u'smaller than'), (48, u'between'), (49, u'not between'), (50, u'empty'), ] assert \ [(f['index'], f['operation']) for f in view2._filter_groups[u'Model1 / Email Field']] == \ [ (51, u'contains'), (52, u'not contains'), (53, u'equals'), (54, u'not equal'), (55, u'empty'), (56, u'in list'), (57, u'not in list'), ] assert \ [(f['index'], f['operation']) for f in view2._filter_groups[u'Model1 / Enum Field']] == \ [ (58, u'equals'), (59, u'not equal'), (60, u'empty'), (61, u'in list'), (62, u'not in list'), ] assert \ [(f['index'], f['operation']) for f in view2._filter_groups[u'Model1 / Choice Field']] == \ [ (63, u'contains'), (64, u'not contains'), (65, u'equals'), (66, u'not equal'), (67, u'empty'), (68, u'in list'), (69, u'not in list'), ] assert \ [(f['index'], f['operation']) for f in view2._filter_groups[u'Model1 / Sqla Utils Choice']] == \ [ (70, u'equals'), (71, u'not equal'), (72, u'contains'), (73, u'not contains'), (74, u'empty'), ] assert \ [(f['index'], f['operation']) for f in view2._filter_groups[u'Model1 / Sqla Utils Enum']] == \ [ (75, u'equals'), (76, u'not equal'), (77, u'contains'), (78, u'not contains'), (79, u'empty'), ] # Test filter with a dot view3 = CustomModelView(Model2, db.session, column_filters=['model1.bool_field']) assert \ [(f['index'], f['operation']) for f in view3._filter_groups[u'model1 / Model1 / Bool Field']] == \ [ (0, 'equals'), (1, 'not equal'), ] # Test column_labels on filters view4 = CustomModelView(Model2, db.session, column_filters=['model1.bool_field', 'string_field'], column_labels={ 'model1.bool_field': 'Test Filter #1', 'string_field': 'Test Filter #2', }) assert list(view4._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') assert 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 assert 'test2_val_1' in data assert 'test1_val_2' not in data # Test NOT IN filter rv = client.get('/admin/model1/?flt0_6=test1_val_1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_2' in data assert 'test2_val_1' not in data # Test string filter assert \ [(f['index'], f['operation']) for f in view5._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') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' in data assert 'test1_val_2' not in data # string - not equal rv = client.get('/admin/_strings/?flt0_1=test1_val_1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' not in data assert 'test1_val_2' in data # string - contains rv = client.get('/admin/_strings/?flt0_2=test1_val_1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' in data assert 'test1_val_2' not in data # string - not contains rv = client.get('/admin/_strings/?flt0_3=test1_val_1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' not in data assert 'test1_val_2' in data # string - empty rv = client.get('/admin/_strings/?flt0_4=1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'empty_obj' in data assert 'test1_val_1' not in data assert 'test1_val_2' not in data # string - not empty rv = client.get('/admin/_strings/?flt0_4=0') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'empty_obj' not in data assert 'test1_val_1' in data assert 'test1_val_2' in data # string - in list rv = client.get('/admin/_strings/?flt0_5=test1_val_1%2Ctest1_val_2') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' in data assert 'test2_val_2' in data assert 'test1_val_3' not in data assert 'test1_val_4' not in data # string - not in list rv = client.get('/admin/_strings/?flt0_6=test1_val_1%2Ctest1_val_2') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' not in data assert 'test2_val_2' not in data assert 'test1_val_3' in data assert 'test1_val_4' in data # Test integer filter assert \ [(f['index'], f['operation']) for f in view6._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') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_3' in data assert 'test2_val_4' not in data # integer - equals (huge number) rv = client.get('/admin/model2/?flt0_0=6169453081680413441') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_5' in data assert 'test2_val_4' not in data # integer - equals - test validation rv = client.get('/admin/model2/?flt0_0=badval') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'Invalid Filter Value' in data # integer - not equal rv = client.get('/admin/model2/?flt0_1=5000') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_3' not in data assert 'test2_val_4' in data # integer - greater rv = client.get('/admin/model2/?flt0_2=6000') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_3' not in data assert 'test2_val_4' in data # integer - smaller rv = client.get('/admin/model2/?flt0_3=6000') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_3' in data assert 'test2_val_4' not in data # integer - empty rv = client.get('/admin/model2/?flt0_4=1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' in data assert 'test2_val_2' in data assert 'test2_val_3' not in data assert 'test2_val_4' not in data # integer - not empty rv = client.get('/admin/model2/?flt0_4=0') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' not in data assert 'test2_val_2' not in data assert 'test2_val_3' in data assert 'test2_val_4' in data # integer - in list rv = client.get('/admin/model2/?flt0_5=5000%2C9000') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' not in data assert 'test2_val_2' not in data assert 'test2_val_3' in data assert 'test2_val_4' in data # integer - in list (huge number) rv = client.get('/admin/model2/?flt0_5=6169453081680413441') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' not in data assert 'test2_val_5' in data # integer - in list - test validation rv = client.get('/admin/model2/?flt0_5=5000%2Cbadval') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'Invalid Filter Value' in data # integer - not in list rv = client.get('/admin/model2/?flt0_6=5000%2C9000') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' in data assert 'test2_val_2' in data assert 'test2_val_3' not in data assert 'test2_val_4' not in data # Test boolean filter assert \ [(f['index'], f['operation']) for f in view7._filter_groups[u'Bool Field']] == \ [ (0, 'equals'), (1, 'not equal'), ] # boolean - equals - Yes rv = client.get('/admin/_bools/?flt0_0=1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' in data assert 'test2_val_2' not in data assert 'test2_val_3' not in data # boolean - equals - No rv = client.get('/admin/_bools/?flt0_0=0') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' not in data assert 'test2_val_2' in data assert 'test2_val_3' in data # boolean - not equals - Yes rv = client.get('/admin/_bools/?flt0_1=1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' not in data assert 'test2_val_2' in data assert 'test2_val_3' in data # boolean - not equals - No rv = client.get('/admin/_bools/?flt0_1=0') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' in data assert 'test2_val_2' not in data assert 'test2_val_3' not in data # Test float filter assert \ [(f['index'], f['operation']) for f in view8._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') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_3' in data assert 'test2_val_4' not in data # float - equals - test validation rv = client.get('/admin/_float/?flt0_0=badval') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'Invalid Filter Value' in data # float - not equal rv = client.get('/admin/_float/?flt0_1=25.9') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_3' not in data assert 'test2_val_4' in data # float - greater rv = client.get('/admin/_float/?flt0_2=60.5') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_3' not in data assert 'test2_val_4' in data # float - smaller rv = client.get('/admin/_float/?flt0_3=60.5') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_3' in data assert 'test2_val_4' not in data # float - empty rv = client.get('/admin/_float/?flt0_4=1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' in data assert 'test2_val_2' in data assert 'test2_val_3' not in data assert 'test2_val_4' not in data # float - not empty rv = client.get('/admin/_float/?flt0_4=0') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' not in data assert 'test2_val_2' not in data assert 'test2_val_3' in data assert 'test2_val_4' in data # float - in list rv = client.get('/admin/_float/?flt0_5=25.9%2C75.5') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' not in data assert 'test2_val_2' not in data assert 'test2_val_3' in data assert 'test2_val_4' in data # float - in list - test validation rv = client.get('/admin/_float/?flt0_5=25.9%2Cbadval') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'Invalid Filter Value' in data # float - not in list rv = client.get('/admin/_float/?flt0_6=25.9%2C75.5') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' in data assert 'test2_val_2' in data assert 'test2_val_3' not in data assert 'test2_val_4' not in data # Test filters to joined table field rv = client.get('/admin/_model2/?flt1_0=1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2_val_1' in data assert 'test2_val_2' not in data assert 'test2_val_3' not in data assert 'test2_val_4' not in data # Test human readable URLs rv = client.get('/admin/_model3/?flt1_test1_equals=test1_val_1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' in data assert 'test1_val_2' not in data # Test date, time, and datetime filters assert \ [(f['index'], f['operation']) for f in view11._filter_groups[u'Date Field']] == \ [ (0, 'equals'), (1, 'not equal'), (2, 'greater than'), (3, 'smaller than'), (4, 'between'), (5, 'not between'), (6, 'empty'), ] assert \ [(f['index'], f['operation']) for f in view11._filter_groups[u'Datetime Field']] == \ [ (7, 'equals'), (8, 'not equal'), (9, 'greater than'), (10, 'smaller than'), (11, 'between'), (12, 'not between'), (13, 'empty'), ] assert \ [(f['index'], f['operation']) for f in view11._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') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'date_obj1' in data assert 'date_obj2' not in data # date - not equal rv = client.get('/admin/_datetime/?flt0_1=2014-11-17') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'date_obj1' not in data assert 'date_obj2' in data # date - greater rv = client.get('/admin/_datetime/?flt0_2=2014-11-16') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'date_obj1' in data assert 'date_obj2' not in data # date - smaller rv = client.get('/admin/_datetime/?flt0_3=2014-11-16') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'date_obj1' not in data assert 'date_obj2' in data # date - between rv = client.get('/admin/_datetime/?flt0_4=2014-11-13+to+2014-11-20') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'date_obj1' in data assert 'date_obj2' not in data # date - not between rv = client.get('/admin/_datetime/?flt0_5=2014-11-13+to+2014-11-20') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'date_obj1' not in data assert 'date_obj2' in data # date - empty rv = client.get('/admin/_datetime/?flt0_6=1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' in data assert 'date_obj1' not in data assert 'date_obj2' not in data # date - empty rv = client.get('/admin/_datetime/?flt0_6=0') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' not in data assert 'date_obj1' in data assert 'date_obj2' in data # datetime - equals rv = client.get('/admin/_datetime/?flt0_7=2014-04-03+01%3A09%3A00') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'datetime_obj1' in data assert 'datetime_obj2' not in data # datetime - not equal rv = client.get('/admin/_datetime/?flt0_8=2014-04-03+01%3A09%3A00') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'datetime_obj1' not in data assert 'datetime_obj2' in data # datetime - greater rv = client.get('/admin/_datetime/?flt0_9=2014-04-03+01%3A08%3A00') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'datetime_obj1' in data assert 'datetime_obj2' not in data # datetime - smaller rv = client.get('/admin/_datetime/?flt0_10=2014-04-03+01%3A08%3A00') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'datetime_obj1' not in data assert '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') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'datetime_obj1' in data assert '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') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'datetime_obj1' not in data assert 'datetime_obj2' in data # datetime - empty rv = client.get('/admin/_datetime/?flt0_13=1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' in data assert 'datetime_obj1' not in data assert 'datetime_obj2' not in data # datetime - not empty rv = client.get('/admin/_datetime/?flt0_13=0') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' not in data assert 'datetime_obj1' in data assert 'datetime_obj2' in data # time - equals rv = client.get('/admin/_datetime/?flt0_14=11%3A10%3A09') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'timeonly_obj1' in data assert 'timeonly_obj2' not in data # time - not equal rv = client.get('/admin/_datetime/?flt0_15=11%3A10%3A09') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'timeonly_obj1' not in data assert 'timeonly_obj2' in data # time - greater rv = client.get('/admin/_datetime/?flt0_16=11%3A09%3A09') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'timeonly_obj1' in data assert 'timeonly_obj2' not in data # time - smaller rv = client.get('/admin/_datetime/?flt0_17=11%3A09%3A09') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'timeonly_obj1' not in data assert 'timeonly_obj2' in data # time - between rv = client.get('/admin/_datetime/?flt0_18=10%3A40%3A00+to+11%3A50%3A59') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'timeonly_obj1' in data assert 'timeonly_obj2' not in data # time - not between rv = client.get('/admin/_datetime/?flt0_19=10%3A40%3A00+to+11%3A50%3A59') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'timeonly_obj1' not in data assert 'timeonly_obj2' in data # time - empty rv = client.get('/admin/_datetime/?flt0_20=1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' in data assert 'timeonly_obj1' not in data assert 'timeonly_obj2' not in data # time - not empty rv = client.get('/admin/_datetime/?flt0_20=0') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' not in data assert 'timeonly_obj1' in data assert 'timeonly_obj2' in data # Test enum filter # enum - equals rv = client.get('/admin/_enumfield/?flt0_0=model1_v1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'enum_obj1' in data assert 'enum_obj2' not in data # enum - not equal rv = client.get('/admin/_enumfield/?flt0_1=model1_v1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'enum_obj1' not in data assert 'enum_obj2' in data # enum - empty rv = client.get('/admin/_enumfield/?flt0_2=1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' in data assert 'enum_obj1' not in data assert 'enum_obj2' not in data # enum - not empty rv = client.get('/admin/_enumfield/?flt0_2=0') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' not in data assert 'enum_obj1' in data assert 'enum_obj2' in data # enum - in list rv = client.get('/admin/_enumfield/?flt0_3=model1_v1%2Cmodel1_v2') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' not in data assert 'enum_obj1' in data assert 'enum_obj2' in data # enum - not in list rv = client.get('/admin/_enumfield/?flt0_4=model1_v1%2Cmodel1_v2') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1_val_1' in data assert 'enum_obj1' not in data assert 'enum_obj2' not in data # Test single custom filter on relation rv = client.get('/admin/_relation_test/?flt1_0=test1_val_1') data = rv.data.decode('utf-8') assert 'test1_val_1' in data assert 'test1_val_2' not in data def test_column_filters_sqla_obj(): app, db, admin = setup() with app.app_context(): Model1, Model2 = create_models(db) view = CustomModelView( Model1, db.session, column_filters=[Model1.test1] ) admin.add_view(view) assert len(view._filters) == 7 def test_hybrid_property(): app, db, admin = setup() with app.app_context(): 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 @hybrid_property def number_of_pixels_str(self): return str(self.number_of_pixels()) @number_of_pixels_str.expression def number_of_pixels_str(cls): return cast(cls.width * cls.height, db.String) db.create_all() assert tools.is_hybrid_property(Model1, 'number_of_pixels') assert tools.is_hybrid_property(Model1, 'number_of_pixels_str') assert not tools.is_hybrid_property(Model1, 'height') assert not tools.is_hybrid_property(Model1, 'width') 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')], column_searchable_list=['number_of_pixels_str', ] ) admin.add_view(view) # filters - hybrid_property integer - greater rv = client.get('/admin/model1/?flt0_0=600') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test_row_1' in data assert 'test_row_2' not in data # sorting rv = client.get('/admin/model1/?sort=0') assert rv.status_code == 200 _, data = view.get_list(0, None, None, None, None) assert len(data) == 2 assert data[0].name == 'test_row_2' assert data[1].name == 'test_row_1' # searching rv = client.get('/admin/model1/?search=100') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test_row_2' in data assert 'test_row_1' not in data def test_hybrid_property_nested(): app, db, admin = setup() with app.app_context(): class Model1(db.Model): id = db.Column(db.Integer, primary_key=True) firstname = db.Column(db.String) lastname = db.Column(db.String) @hybrid_property def fullname(self): return '{} {}'.format(self.firstname, self.lastname) class Model2(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String) owner_id = db.Column(db.Integer, db.ForeignKey('model1.id', ondelete='CASCADE')) owner = db.relationship('Model1', backref=db.backref("tiles"), uselist=False) db.create_all() assert tools.is_hybrid_property(Model2, 'owner.fullname') assert not tools.is_hybrid_property(Model2, 'owner.firstname') db.session.add(Model1(id=1, firstname="John", lastname="Dow")) db.session.add(Model1(id=2, firstname="Jim", lastname="Smith")) db.session.add(Model2(id=1, name="pencil", owner_id=1)) db.session.add(Model2(id=2, name="key", owner_id=1)) db.session.add(Model2(id=3, name="map", owner_id=2)) db.session.commit() client = app.test_client() view = CustomModelView( Model2, db.session, column_list=('id', 'name', 'owner.fullname'), column_default_sort='id', ) admin.add_view(view) rv = client.get('/admin/model2/') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'John Dow' in data assert 'Jim Smith' in data def test_url_args(): app, db, admin = setup() with app.app_context(): 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') assert 'data1' in data assert 'data3' not in data # page rv = client.get('/admin/model1/?page=1') data = rv.data.decode('utf-8') assert 'data1' not in data assert 'data3' in data # sort rv = client.get('/admin/model1/?sort=0&desc=1') data = rv.data.decode('utf-8') assert 'data1' not in data assert 'data3' in data assert 'data4' in data # search rv = client.get('/admin/model1/?search=data1') data = rv.data.decode('utf-8') assert 'data1' in data assert 'data2' not in data rv = client.get('/admin/model1/?search=^data1') data = rv.data.decode('utf-8') assert 'data2' not in data # like rv = client.get('/admin/model1/?flt0=0&flt0v=data1') data = rv.data.decode('utf-8') assert 'data1' in data # not like rv = client.get('/admin/model1/?flt0=1&flt0v=data1') data = rv.data.decode('utf-8') assert 'data2' in data def test_non_int_pk(): app, db, admin = setup() with app.app_context(): 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/') assert rv.status_code == 200 rv = client.post('/admin/model/new/', data=dict(id='test1', test='test2')) assert rv.status_code == 302 rv = client.get('/admin/model/') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test1' in data rv = client.get('/admin/model/edit/?id=test1') assert rv.status_code == 200 data = rv.data.decode('utf-8') assert 'test2' in data def test_form_columns(): app, db, admin = setup() with app.app_context(): 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): class EnumChoices(enum.Enum): first = 1 second = 2 id = db.Column(db.String, primary_key=True) model_id = db.Column(db.Integer, db.ForeignKey(Model.id)) model = db.relationship(Model, backref='backref') enum_field = db.Column(db.Enum('model1_v1', 'model1_v2'), nullable=True) choice_field = db.Column(db.String, nullable=True) sqla_utils_choice = db.Column(ChoiceType([ ('choice-1', u'First choice'), ('choice-2', u'Second choice') ])) sqla_utils_enum = db.Column(ChoiceType(EnumChoices, impl=db.Integer())) 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() assert 'int_field' in form1._fields assert 'text_field' in form1._fields assert 'datetime_field' not in form1._fields assert 'excluded_column' not in form2._fields # check that relation shows up as a query select assert type(form3.model).__name__ == 'QuerySelectField' # check that select field is rendered if form_choices were specified assert type(form3.choice_field).__name__ == 'Select2Field' # check that select field is rendered for enum fields assert type(form3.enum_field).__name__ == 'Select2Field' # check that sqlalchemy_utils field types are handled appropriately assert type(form3.sqla_utils_choice).__name__ == 'Select2Field' assert type(form3.sqla_utils_enum).__name__ == 'Select2Field' # test form_columns with model objects view4 = CustomModelView(Model, db.session, endpoint='view1', form_columns=[Model.int_field]) form4 = view4.create_form() assert 'int_field' in form4._fields @pytest.mark.xfail(raises=Exception) def test_complex_form_columns(): app, db, admin = setup() with app.app_context(): M1, M2 = create_models(db) # test using a form column in another table view = CustomModelView(M2, db.session, form_columns=['model1.test1']) view.create_form() def test_form_args(): app, db, admin = setup() with app.app_context(): 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() assert len(create_form.test.validators) == 2 # ensure shared field_args don't create duplicate validators edit_form = view.edit_form() assert len(edit_form.test.validators) == 2 def test_form_override(): app, db, admin = setup() with app.app_context(): 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) assert view1._create_form_class.test.field_class == fields.StringField assert view2._create_form_class.test.field_class == fields.FileField def test_form_onetoone(): app, db, admin = setup() with app.app_context(): 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() assert model1.model2 == model2 assert model2.model1 == model1 assert not view1._create_form_class.model2.field_class.widget.multiple assert not view2._create_form_class.model1.field_class.widget.multiple def test_relations(): # TODO: test relations pass def test_on_model_change_delete(): app, db, admin = setup() with app.app_context(): 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() assert 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() assert model.test1 == 'TEST1SMALL' url = '/admin/model1/delete/?id=%s' % model.id client.post(url) assert view.deleted def test_multiple_delete(): app, db, admin = setup() with app.app_context(): M1, _ = create_models(db) db.session.add_all([M1('a'), M1('b'), M1('c')]) db.session.commit() assert 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])) assert rv.status_code == 302 assert M1.query.count() == 0 def test_default_sort(): app, db, admin = setup() with app.app_context(): M1, _ = create_models(db) db.session.add_all([M1('c', 'x'), M1('b', 'x'), M1('a', 'y')]) db.session.commit() assert 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) assert len(data) == 3 assert data[0].test1 == 'a' assert data[1].test1 == 'b' assert 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) assert len(data) == 3 assert data[0].test1 == 'a' assert data[1].test1 == 'b' assert 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) assert len(data) == 3 assert data[0].test1 == 'a' assert data[1].test1 == 'b' assert data[2].test1 == 'c' # test default sort with multiple columns order = [('test2', False), ('test1', False)] view4 = CustomModelView(M1, db.session, column_default_sort=order, endpoint='m1_4') admin.add_view(view4) _, data = view4.get_list(0, None, None, None, None) assert len(data) == 3 assert data[0].test1 == 'b' assert data[1].test1 == 'c' assert data[2].test1 == 'a' def test_complex_sort(): app, db, admin = setup() with app.app_context(): M1, M2 = create_models(db) m1 = M1(test1='c', test2='x') db.session.add(m1) db.session.add(M2('c', model1=m1)) m2 = M1(test1='b', test2='x') db.session.add(m2) db.session.add(M2('b', model1=m2)) m3 = M1(test1='a', test2='y') db.session.add(m3) db.session.add(M2('a', model1=m3)) 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) view2 = CustomModelView(M2, db.session, column_list=['string_field', 'model1'], column_sortable_list=[('model1', ('model1.test2', 'model1.test1'))], endpoint="m1_2") admin.add_view(view2) client = app.test_client() rv = client.get('/admin/model2/?sort=0') assert rv.status_code == 200 _, data = view.get_list(0, 'model1.test1', False, None, None) assert data[0].model1.test1 == 'a' assert data[1].model1.test1 == 'b' assert data[2].model1.test1 == 'c' # test sorting on multiple columns in related model rv = client.get('/admin/m1_2/?sort=0') assert rv.status_code == 200 _, data = view2.get_list(0, 'model1', False, None, None) assert data[0].model1.test1 == 'b' assert data[1].model1.test1 == 'c' assert data[2].model1.test1 == 'a' @pytest.mark.xfail(raises=Exception) def test_complex_sort_exception(): app, db, admin = setup() with app.app_context(): 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) assert len(data) == 2 assert data[0].model1.test1 == 'a' assert data[1].model1.test1 == 'b' def test_default_complex_sort(): app, db, admin = setup() with app.app_context(): 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) assert len(data) == 2 assert data[0].model1.test1 == 'a' assert 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) assert len(data) == 2 assert data[0].model1.test1 == 'a' assert data[1].model1.test1 == 'b' def test_extra_fields(): app, db, admin = setup() with app.app_context(): 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/') assert rv.status_code == 200 # Check presence and order data = rv.data.decode('utf-8') assert 'Extra Field' in data pos1 = data.find('Extra Field') pos2 = data.find('Test1') assert pos2 < pos1 def test_extra_field_order(): app, db, admin = setup() with app.app_context(): 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/') assert rv.status_code == 200 # Check presence and order data = rv.data.decode('utf-8') pos1 = data.find('Extra Field') pos2 = data.find('Test1') assert pos2 > pos1 def test_modelview_localization(): def test_locale(locale): try: app, db, admin = setup() app.config['BABEL_DEFAULT_LOCALE'] = locale Babel(app) with app.app_context(): 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/') assert rv.status_code == 200 rv = client.get('/admin/model1/new/') assert 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) with app.app_context(): 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) assert 'test1_equals' == flt_name def test_custom_form_base(): app, db, admin = setup() with app.app_context(): class TestForm(form.BaseForm): pass Model1, _ = create_models(db) view = CustomModelView( Model1, db.session, form_base_class=TestForm ) admin.add_view(view) assert hasattr(view._create_form_class, 'test1') create_form = view.create_form() assert isinstance(create_form, TestForm) def test_ajax_fk(): app, db, admin = setup() with app.app_context(): Model1, Model2 = create_models(db) view = CustomModelView( Model2, db.session, url='view', form_ajax_refs={ 'model1': { 'fields': ('test1', 'test2') } } ) admin.add_view(view) assert 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) assert mdl.test1 == model.test1 items = loader.get_list(u'fir') assert len(items) == 1 assert items[0].id == model.id items = loader.get_list(u'bar') assert len(items) == 1 assert items[0].test1 == u'foo' # Check form generation form = view.create_form() assert form.model1.__class__.__name__ == u'AjaxSelectField' with app.test_request_context('/admin/view/'): assert u'value=""' not in form.model1() form.model1.data = model assert (u'data-json="[%s, "first"]"' % model.id in form.model1() or u'data-json="[%s, "first"]"' % model.id in form.model1()) assert u'value="1"' in form.model1() # Check querying client = app.test_client() req = client.get(u'/admin/view/ajax/lookup/?name=model1&query=foo') assert 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() assert mdl is not None assert mdl.model1 is not None assert mdl.model1.id == model.id assert mdl.model1.test1 == u'first' def test_ajax_fk_multi(): app, db, admin = setup() with app.app_context(): 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) assert 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() assert form.model1.__class__.__name__ == u'AjaxSelectMultipleField' with app.test_request_context('/admin/view/'): assert u'data-json="[]"' in form.model1() form.model1.data = [model] assert (u'data-json="[[1, "first"]]"' in form.model1() or u'data-json="[[1, "first"]]"' 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() assert mdl is not None assert mdl.model1 is not None assert len(mdl.model1) == 1 def test_safe_redirect(): app, db, admin = setup() with app.app_context(): 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')) assert rv.status_code == 302 # werkzeug 2.1.0+ now returns *relative* redirect/location by default. expected = '/admin/model1/edit/' # handle old werkzeug (or if relative location is disabled via `autocorrect_location_header=True`) if not hasattr(rv, 'autocorrect_location_header') or rv.autocorrect_location_header: expected = 'http://localhost' + expected assert rv.location.startswith(expected) assert 'url=http%3A%2F%2Flocalhost%2Fadmin%2Fmodel2view%2F' in rv.location assert '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')) assert rv.status_code == 302 assert rv.location.startswith(expected) assert 'url=%2Fadmin%2Fmodel1%2F' in rv.location assert 'id=2' in rv.location def test_simple_list_pager(): app, db, admin = setup() with app.app_context(): 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 count is None def test_unlimited_page_size(): app, db, admin = setup() with app.app_context(): 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) assert len(data) == 21 # test False as page_size _, data = view.get_list(0, None, None, None, None, execute=True, page_size=False) assert len(data) == 21 def test_advanced_joins(): app, db, admin = setup() with app.app_context(): 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') assert attr == Model1.val1 assert path == [Model2.model1] attr, path = tools.get_field_with_path(Model1, 'model2.val2') assert attr == Model2.val2 assert id(path[0]) == id(Model1.model2) attr, path = tools.get_field_with_path(Model3, 'model2.model1.val1') assert attr == Model1.val1 assert 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) assert (True, Model3.model2) in joins assert (True, Model2.model1) in joins assert 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) assert len(joins) == 2 if hasattr(q2, '_join_entities'): for p in q2._join_entities: assert p in q1._join_entities assert 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) assert attr == Model1.test assert path == [Model1.__table__] q3, joins, alias = view2._apply_path_joins(view2.get_query(), joins, path) assert len(joins) == 3 assert alias is None def test_multipath_joins(): app, db, admin = setup() with app.app_context(): 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/') assert rv.status_code == 200 # TODO: Why this fails? @pytest.mark.xfail(raises=Exception) def test_different_bind_joins(): app, db, admin = setup() app.config['SQLALCHEMY_BINDS'] = { 'other': 'sqlite:///' } with app.app_context(): class Model1(db.Model): id = db.Column(db.Integer, primary_key=True) val1 = db.Column(db.String(20)) class Model2(db.Model): __bind_key__ = 'other' id = db.Column(db.Integer, primary_key=True) val1 = db.Column(db.String(20)) first_id = db.Column(db.Integer, db.ForeignKey(Model1.id)) first = db.relationship(Model1) db.create_all() view = CustomModelView(Model2, db.session) admin.add_view(view) client = app.test_client() rv = client.get('/admin/model2/') assert rv.status_code == 200 def test_model_default(): app, db, admin = setup() with app.app_context(): _, 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 b'This field is required' not in rv.data def test_export_csv(): app, db, admin = setup() with app.app_context(): Model1, Model2 = create_models(db) for x in range(5): fill_db(db, Model1, Model2) view1 = CustomModelView(Model1, db.session, can_export=True, column_list=['test1', 'test2'], export_max_rows=2, endpoint='row_limit_2') admin.add_view(view1) view2 = CustomModelView(Model1, db.session, can_export=True, column_list=['test1', 'test2'], endpoint='no_row_limit') admin.add_view(view2) client = app.test_client() # test export_max_rows rv = client.get('/admin/row_limit_2/export/csv/') data = rv.data.decode('utf-8') assert rv.status_code == 200 assert "Test1,Test2\r\n" + \ "test1_val_1,test2_val_1\r\n" + \ "test1_val_2,test2_val_2\r\n" == data # test row limit without export_max_rows rv = client.get('/admin/no_row_limit/export/csv/') data = rv.data.decode('utf-8') assert rv.status_code == 200 assert len(data.splitlines()) > 21