import wtforms from nose.tools import eq_, ok_ from flask import Flask from werkzeug.wsgi import DispatcherMiddleware from werkzeug.test import Client from wtforms import fields from flask_admin import Admin, form from flask_admin._compat import iteritems, itervalues from flask_admin.model import base, filters from flask_admin.model.template import macro def wtforms2_and_up(func): """Decorator for skipping test if wtforms <2 """ if int(wtforms.__version__[0]) < 2: func.__test__ = False return func class Model(object): def __init__(self, id=None, c1=1, c2=2, c3=3): self.id = id self.col1 = c1 self.col2 = c2 self.col3 = c3 class Form(form.BaseForm): col1 = fields.StringField() col2 = fields.StringField() col3 = fields.StringField() class SimpleFilter(filters.BaseFilter): def apply(self, query): query._applied = True return query def operation(self): return 'test' class MockModelView(base.BaseModelView): def __init__(self, model, data=None, name=None, category=None, endpoint=None, url=None, **kwargs): # Allow to set any attributes from parameters for k, v in iteritems(kwargs): setattr(self, k, v) super(MockModelView, self).__init__(model, name, category, endpoint, url) self.created_models = [] self.updated_models = [] self.deleted_models = [] self.search_arguments = [] if data is None: self.all_models = {1: Model(1), 2: Model(2)} else: self.all_models = data self.last_id = len(self.all_models) + 1 # Scaffolding def get_pk_value(self, model): return model.id def scaffold_list_columns(self): columns = ['col1', 'col2', 'col3'] if self.column_exclude_list: return filter(lambda x: x not in self.column_exclude_list, columns) return columns def init_search(self): return bool(self.column_searchable_list) def scaffold_filters(self, name): return [SimpleFilter(name)] def scaffold_sortable_columns(self): return ['col1', 'col2', 'col3'] def scaffold_form(self): return Form # Data def get_list(self, page, sort_field, sort_desc, search, filters, page_size=None): self.search_arguments.append((page, sort_field, sort_desc, search, filters)) return len(self.all_models), itervalues(self.all_models) def get_one(self, id): return self.all_models.get(int(id)) def create_model(self, form): model = Model(self.last_id) self.last_id += 1 form.populate_obj(model) self.created_models.append(model) self.all_models[model.id] = model return True def update_model(self, form, model): form.populate_obj(model) self.updated_models.append(model) return True def delete_model(self, model): self.deleted_models.append(model) return True def setup(): app = Flask(__name__) app.config['CSRF_ENABLED'] = False app.secret_key = '1' admin = Admin(app) return app, admin def test_mockview(): app, admin = setup() view = MockModelView(Model) admin.add_view(view) eq_(view.model, Model) eq_(view.name, 'Model') eq_(view.endpoint, 'model') # Verify scaffolding eq_(view._sortable_columns, ['col1', 'col2', 'col3']) eq_(view._create_form_class, Form) eq_(view._edit_form_class, Form) eq_(view._search_supported, False) eq_(view._filters, None) client = app.test_client() # Make model view requests rv = client.get('/admin/model/') eq_(rv.status_code, 200) # Test model creation view rv = client.get('/admin/model/new/') eq_(rv.status_code, 200) rv = client.post('/admin/model/new/', data=dict(col1='test1', col2='test2', col3='test3')) eq_(rv.status_code, 302) eq_(len(view.created_models), 1) model = view.created_models.pop() eq_(model.id, 3) eq_(model.col1, 'test1') eq_(model.col2, 'test2') eq_(model.col3, 'test3') # Try model edit view rv = client.get('/admin/model/edit/?id=3') eq_(rv.status_code, 200) data = rv.data.decode('utf-8') ok_('test1' in data) rv = client.post('/admin/model/edit/?id=3', data=dict(col1='test!', col2='test@', col3='test#')) eq_(rv.status_code, 302) eq_(len(view.updated_models), 1) model = view.updated_models.pop() eq_(model.col1, 'test!') eq_(model.col2, 'test@') eq_(model.col3, 'test#') rv = client.get('/admin/model/edit/?id=4') eq_(rv.status_code, 302) # Attempt to delete model rv = client.post('/admin/model/delete/?id=3') eq_(rv.status_code, 302) eq_(rv.headers['location'], 'http://localhost/admin/model/') # Create a dispatched application to test that edit view's "save and # continue" functionality works when app is not located at root dummy_app = Flask('dummy_app') dispatched_app = DispatcherMiddleware(dummy_app, {'/dispatched': app}) dispatched_client = Client(dispatched_app) app_iter, status, headers = dispatched_client.post( '/dispatched/admin/model/edit/?id=3', data=dict(col1='another test!', col2='test@', col3='test#', _continue_editing='True')) eq_(status, '302 FOUND') eq_(headers['Location'], 'http://localhost/dispatched/admin/model/edit/?id=3') model = view.updated_models.pop() eq_(model.col1, 'another test!') def test_permissions(): app, admin = setup() view = MockModelView(Model) admin.add_view(view) client = app.test_client() view.can_create = False rv = client.get('/admin/model/new/') eq_(rv.status_code, 302) view.can_edit = False rv = client.get('/admin/model/edit/?id=1') eq_(rv.status_code, 302) view.can_delete = False rv = client.post('/admin/model/delete/?id=1') eq_(rv.status_code, 302) def test_templates(): app, admin = setup() view = MockModelView(Model) admin.add_view(view) client = app.test_client() view.list_template = 'mock.html' view.create_template = 'mock.html' view.edit_template = 'mock.html' rv = client.get('/admin/model/') eq_(rv.data, b'Success!') rv = client.get('/admin/model/new/') eq_(rv.data, b'Success!') rv = client.get('/admin/model/edit/?id=1') eq_(rv.data, b'Success!') def test_list_columns(): app, admin = setup() view = MockModelView(Model, column_list=['col1', 'col3'], column_labels=dict(col1='Column1')) admin.add_view(view) eq_(len(view._list_columns), 2) eq_(view._list_columns, [('col1', 'Column1'), ('col3', 'Col3')]) client = app.test_client() rv = client.get('/admin/model/') data = rv.data.decode('utf-8') ok_('Column1' in data) ok_('Col2' not in data) def test_exclude_columns(): app, admin = setup() view = MockModelView(Model, column_exclude_list=['col2']) admin.add_view(view) eq_(view._list_columns, [('col1', 'Col1'), ('col3', 'Col3')]) client = app.test_client() rv = client.get('/admin/model/') data = rv.data.decode('utf-8') ok_('Col1' in data) ok_('Col2' not in data) def test_sortable_columns(): app, admin = setup() view = MockModelView(Model, column_sortable_list=['col1', ('col2', 'test1')]) admin.add_view(view) eq_(view._sortable_columns, dict(col1='col1', col2='test1')) def test_column_searchable_list(): app, admin = setup() view = MockModelView(Model, column_searchable_list=['col1', 'col2']) admin.add_view(view) eq_(view._search_supported, True) # TODO: Make calls with search def test_column_filters(): app, admin = setup() view = MockModelView(Model, column_filters=['col1', 'col2']) admin.add_view(view) eq_(len(view._filters), 2) eq_(view._filters[0].name, 'col1') eq_(view._filters[1].name, 'col2') eq_([(f['index'], f['operation']) for f in view._filter_groups[u'col1']], [(0, 'test')]) eq_([(f['index'], f['operation']) for f in view._filter_groups[u'col2']], [(1, 'test')]) # TODO: Make calls with filters def test_filter_list_callable(): app, admin = setup() flt = SimpleFilter('test', options=lambda: [('1', 'Test 1'), ('2', 'Test 2')]) view = MockModelView(Model, column_filters=[flt]) admin.add_view(view) opts = flt.get_options(view) eq_(len(opts), 2) eq_(opts, [('1', 'Test 1'), ('2', 'Test 2')]) def test_form(): # TODO: form_columns # TODO: form_excluded_columns # TODO: form_args # TODO: form_widget_args pass @wtforms2_and_up def test_csrf(): class SecureModelView(MockModelView): form_base_class = form.SecureForm def scaffold_form(self): return form.SecureForm def get_csrf_token(data): data = data.split('name="csrf_token" type="hidden" value="')[1] token = data.split('"')[0] return token app, admin = setup() view = SecureModelView(Model, endpoint='secure') admin.add_view(view) client = app.test_client() ################ # create_view ################ rv = client.get('/admin/secure/new/') eq_(rv.status_code, 200) ok_(u'name="csrf_token"' in rv.data.decode('utf-8')) csrf_token = get_csrf_token(rv.data.decode('utf-8')) # Create without CSRF token rv = client.post('/admin/secure/new/', data=dict(name='test1')) eq_(rv.status_code, 200) # Create with CSRF token rv = client.post('/admin/secure/new/', data=dict(name='test1', csrf_token=csrf_token)) eq_(rv.status_code, 302) ############### # edit_view ############### rv = client.get('/admin/secure/edit/?url=%2Fadmin%2Fsecure%2F&id=1') eq_(rv.status_code, 200) ok_(u'name="csrf_token"' in rv.data.decode('utf-8')) csrf_token = get_csrf_token(rv.data.decode('utf-8')) # Edit without CSRF token rv = client.post('/admin/secure/edit/?url=%2Fadmin%2Fsecure%2F&id=1', data=dict(name='test1')) eq_(rv.status_code, 200) # Edit with CSRF token rv = client.post('/admin/secure/edit/?url=%2Fadmin%2Fsecure%2F&id=1', data=dict(name='test1', csrf_token=csrf_token)) eq_(rv.status_code, 302) ################ # delete_view ################ rv = client.get('/admin/secure/') eq_(rv.status_code, 200) ok_(u'name="csrf_token"' in rv.data.decode('utf-8')) csrf_token = get_csrf_token(rv.data.decode('utf-8')) # Delete without CSRF token, test validation errors rv = client.post('/admin/secure/delete/', data=dict(id="1", url="/admin/secure/"), follow_redirects=True) eq_(rv.status_code, 200) ok_(u'Record was successfully deleted.' not in rv.data.decode('utf-8')) ok_(u'Failed to delete record.' in rv.data.decode('utf-8')) # Delete with CSRF token rv = client.post('/admin/secure/delete/', data=dict(id="1", url="/admin/secure/", csrf_token=csrf_token), follow_redirects=True) eq_(rv.status_code, 200) ok_(u'Record was successfully deleted.' in rv.data.decode('utf-8')) ################ # actions ################ rv = client.get('/admin/secure/') eq_(rv.status_code, 200) ok_(u'name="csrf_token"' in rv.data.decode('utf-8')) csrf_token = get_csrf_token(rv.data.decode('utf-8')) # Delete without CSRF token, test validation errors rv = client.post('/admin/secure/action/', data=dict(rowid='1', url='/admin/secure/', action='delete'), follow_redirects=True) eq_(rv.status_code, 200) ok_(u'Record was successfully deleted.' not in rv.data.decode('utf-8')) ok_(u'Failed to perform action.' in rv.data.decode('utf-8')) def test_custom_form(): app, admin = setup() class TestForm(form.BaseForm): pass view = MockModelView(Model, form=TestForm) admin.add_view(view) eq_(view._create_form_class, TestForm) eq_(view._edit_form_class, TestForm) ok_(not hasattr(view._create_form_class, 'col1')) def test_modal_edit(): # bootstrap 2 - test edit_modal app_bs2 = Flask(__name__) admin_bs2 = Admin(app_bs2, template_mode="bootstrap2") edit_modal_on = MockModelView(Model, edit_modal=True, endpoint="edit_modal_on") edit_modal_off = MockModelView(Model, edit_modal=False, endpoint="edit_modal_off") create_modal_on = MockModelView(Model, create_modal=True, endpoint="create_modal_on") create_modal_off = MockModelView(Model, create_modal=False, endpoint="create_modal_off") admin_bs2.add_view(edit_modal_on) admin_bs2.add_view(edit_modal_off) admin_bs2.add_view(create_modal_on) admin_bs2.add_view(create_modal_off) client_bs2 = app_bs2.test_client() # bootstrap 2 - ensure modal window is added when edit_modal is enabled rv = client_bs2.get('/admin/edit_modal_on/') eq_(rv.status_code, 200) data = rv.data.decode('utf-8') ok_('fa_modal_window' in data) # bootstrap 2 - test edit modal disabled rv = client_bs2.get('/admin/edit_modal_off/') eq_(rv.status_code, 200) data = rv.data.decode('utf-8') ok_('fa_modal_window' not in data) # bootstrap 2 - ensure modal window is added when create_modal is enabled rv = client_bs2.get('/admin/create_modal_on/') eq_(rv.status_code, 200) data = rv.data.decode('utf-8') ok_('fa_modal_window' in data) # bootstrap 2 - test create modal disabled rv = client_bs2.get('/admin/create_modal_off/') eq_(rv.status_code, 200) data = rv.data.decode('utf-8') ok_('fa_modal_window' not in data) # bootstrap 3 app_bs3 = Flask(__name__) admin_bs3 = Admin(app_bs3, template_mode="bootstrap3") admin_bs3.add_view(edit_modal_on) admin_bs3.add_view(edit_modal_off) admin_bs3.add_view(create_modal_on) admin_bs3.add_view(create_modal_off) client_bs3 = app_bs3.test_client() # bootstrap 3 - ensure modal window is added when edit_modal is enabled rv = client_bs3.get('/admin/edit_modal_on/') eq_(rv.status_code, 200) data = rv.data.decode('utf-8') ok_('fa_modal_window' in data) # bootstrap 3 - test modal disabled rv = client_bs3.get('/admin/edit_modal_off/') eq_(rv.status_code, 200) data = rv.data.decode('utf-8') ok_('fa_modal_window' not in data) # bootstrap 3 - ensure modal window is added when edit_modal is enabled rv = client_bs3.get('/admin/create_modal_on/') eq_(rv.status_code, 200) data = rv.data.decode('utf-8') ok_('fa_modal_window' in data) # bootstrap 3 - test modal disabled rv = client_bs3.get('/admin/create_modal_off/') eq_(rv.status_code, 200) data = rv.data.decode('utf-8') ok_('fa_modal_window' not in data) def check_class_name(): class DummyView(MockModelView): pass view = DummyView(Model) eq_(view.name, 'Dummy View') def test_export_csv(): app, admin = setup() client = app.test_client() # test redirect when csv export is disabled view = MockModelView(Model, column_list=['col1', 'col2'], endpoint="test") admin.add_view(view) rv = client.get('/admin/test/export/csv/') eq_(rv.status_code, 302) # basic test of csv export with a few records view_data = { 1: Model(1, "col1_1", "col2_1"), 2: Model(2, "col1_2", "col2_2"), 3: Model(3, "col1_3", "col2_3"), } view = MockModelView(Model, view_data, can_export=True, column_list=['col1', 'col2']) admin.add_view(view) rv = client.get('/admin/model/export/csv/') data = rv.data.decode('utf-8') eq_(rv.mimetype, 'text/csv') eq_(rv.status_code, 200) ok_("Col1,Col2\r\n" "col1_1,col2_1\r\n" "col1_2,col2_2\r\n" "col1_3,col2_3\r\n" == data) # test explicit use of column_export_list view = MockModelView(Model, view_data, can_export=True, column_list=['col1', 'col2'], column_export_list=['id', 'col1', 'col2'], endpoint='exportinclusion') admin.add_view(view) rv = client.get('/admin/exportinclusion/export/csv/') data = rv.data.decode('utf-8') eq_(rv.mimetype, 'text/csv') eq_(rv.status_code, 200) ok_("Id,Col1,Col2\r\n" "1,col1_1,col2_1\r\n" "2,col1_2,col2_2\r\n" "3,col1_3,col2_3\r\n" == data) # test explicit use of column_export_exclude_list view = MockModelView(Model, view_data, can_export=True, column_list=['col1', 'col2'], column_export_exclude_list=['col2'], endpoint='exportexclusion') admin.add_view(view) rv = client.get('/admin/exportexclusion/export/csv/') data = rv.data.decode('utf-8') eq_(rv.mimetype, 'text/csv') eq_(rv.status_code, 200) ok_("Col1\r\n" "col1_1\r\n" "col1_2\r\n" "col1_3\r\n" == data) # test utf8 characters in csv export view_data[4] = Model(1, u'\u2013ut8_1\u2013', u'\u2013utf8_2\u2013') view = MockModelView(Model, view_data, can_export=True, column_list=['col1', 'col2'], endpoint="utf8") admin.add_view(view) rv = client.get('/admin/utf8/export/csv/') data = rv.data.decode('utf-8') eq_(rv.status_code, 200) ok_(u'\u2013ut8_1\u2013,\u2013utf8_2\u2013\r\n' in data) # test None type, integer type, column_labels, and column_formatters view_data = { 1: Model(1, "col1_1", 1), 2: Model(2, "col1_2", 2), 3: Model(3, None, 3), } view = MockModelView( Model, view_data, can_export=True, column_list=['col1', 'col2'], column_labels={'col1': 'Str Field', 'col2': 'Int Field'}, column_formatters=dict(col2=lambda v, c, m, p: m.col2 * 2), endpoint="types_and_formatters" ) admin.add_view(view) rv = client.get('/admin/types_and_formatters/export/csv/') data = rv.data.decode('utf-8') eq_(rv.status_code, 200) ok_("Str Field,Int Field\r\n" "col1_1,2\r\n" "col1_2,4\r\n" ",6\r\n" == data) # test column_formatters_export and column_formatters_export type_formatters = {type(None): lambda view, value: "null"} view = MockModelView( Model, view_data, can_export=True, column_list=['col1', 'col2'], column_formatters_export=dict(col2=lambda v, c, m, p: m.col2 * 3), column_formatters=dict(col2=lambda v, c, m, p: m.col2 * 2), # overridden column_type_formatters_export=type_formatters, endpoint="export_types_and_formatters" ) admin.add_view(view) rv = client.get('/admin/export_types_and_formatters/export/csv/') data = rv.data.decode('utf-8') eq_(rv.status_code, 200) ok_("Col1,Col2\r\n" "col1_1,3\r\n" "col1_2,6\r\n" "null,9\r\n" == data) # Macros are not implemented for csv export yet and will throw an error view = MockModelView( Model, can_export=True, column_list=['col1', 'col2'], column_formatters=dict(col1=macro('render_macro')), endpoint="macro_exception" ) admin.add_view(view) rv = client.get('/admin/macro_exception/export/csv/') data = rv.data.decode('utf-8') eq_(rv.status_code, 500) # We should be able to specify column_formatters_export # and not get an exception if a column_formatter is using a macro def export_formatter(v, c, m, p): return m.col1 if m else '' view = MockModelView( Model, view_data, can_export=True, column_list=['col1', 'col2'], column_formatters=dict(col1=macro('render_macro')), column_formatters_export=dict(col1=export_formatter), endpoint="macro_exception_formatter_override" ) admin.add_view(view) rv = client.get('/admin/macro_exception_formatter_override/export/csv/') data = rv.data.decode('utf-8') eq_(rv.status_code, 200) ok_("Col1,Col2\r\n" "col1_1,1\r\n" "col1_2,2\r\n" ",3\r\n" == data) # We should not get an exception if a column_formatter is # using a macro but it is on the column_export_exclude_list view = MockModelView( Model, view_data, can_export=True, column_list=['col1', 'col2'], column_formatters=dict(col1=macro('render_macro')), column_export_exclude_list=['col1'], endpoint="macro_exception_exclude_override" ) admin.add_view(view) rv = client.get('/admin/macro_exception_exclude_override/export/csv/') data = rv.data.decode('utf-8') eq_(rv.status_code, 200) ok_("Col2\r\n" "1\r\n" "2\r\n" "3\r\n" == data) # When we use column_export_list to hide the macro field # we should not get an exception view = MockModelView( Model, view_data, can_export=True, column_list=['col1', 'col2'], column_formatters=dict(col1=macro('render_macro')), column_export_list=['col2'], endpoint="macro_exception_list_override" ) admin.add_view(view) rv = client.get('/admin/macro_exception_list_override/export/csv/') data = rv.data.decode('utf-8') eq_(rv.status_code, 200) ok_("Col2\r\n" "1\r\n" "2\r\n" "3\r\n" == data) # If they define a macro on the column_formatters_export list # then raise an exception view = MockModelView( Model, view_data, can_export=True, column_list=['col1', 'col2'], column_formatters=dict(col1=macro('render_macro')), endpoint="macro_exception_macro_override" ) admin.add_view(view) rv = client.get('/admin/macro_exception_macro_override/export/csv/') data = rv.data.decode('utf-8') eq_(rv.status_code, 500) def test_list_row_actions(): app, admin = setup() client = app.test_client() from flask_admin.model import template # Test default actions view = MockModelView(Model, endpoint='test') admin.add_view(view) actions = view.get_list_row_actions() ok_(isinstance(actions[0], template.EditRowAction)) ok_(isinstance(actions[1], template.DeleteRowAction)) rv = client.get('/admin/test/') eq_(rv.status_code, 200) # Test default actions view = MockModelView(Model, endpoint='test1', can_edit=False, can_delete=False, can_view_details=True) admin.add_view(view) actions = view.get_list_row_actions() eq_(len(actions), 1) ok_(isinstance(actions[0], template.ViewRowAction)) rv = client.get('/admin/test1/') eq_(rv.status_code, 200) # Test popups view = MockModelView(Model, endpoint='test2', can_view_details=True, details_modal=True, edit_modal=True) admin.add_view(view) actions = view.get_list_row_actions() ok_(isinstance(actions[0], template.ViewPopupRowAction)) ok_(isinstance(actions[1], template.EditPopupRowAction)) ok_(isinstance(actions[2], template.DeleteRowAction)) rv = client.get('/admin/test2/') eq_(rv.status_code, 200) # Test custom views view = MockModelView(Model, endpoint='test3', column_extra_row_actions=[ template.LinkRowAction('glyphicon glyphicon-off', 'http://localhost/?id={row_id}'), template.EndpointLinkRowAction('glyphicon glyphicon-test', 'test1.index_view') ]) admin.add_view(view) actions = view.get_list_row_actions() ok_(isinstance(actions[0], template.EditRowAction)) ok_(isinstance(actions[1], template.DeleteRowAction)) ok_(isinstance(actions[2], template.LinkRowAction)) ok_(isinstance(actions[3], template.EndpointLinkRowAction)) rv = client.get('/admin/test3/') eq_(rv.status_code, 200) data = rv.data.decode('utf-8') ok_('glyphicon-off' in data) ok_('http://localhost/?id=' in data) ok_('glyphicon-test' in data)