test_model.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
  1. import wtforms
  2. from nose.tools import eq_, ok_
  3. from flask import Flask
  4. from werkzeug.wsgi import DispatcherMiddleware
  5. from werkzeug.test import Client
  6. from wtforms import fields
  7. from flask_admin import Admin, form
  8. from flask_admin._compat import iteritems, itervalues
  9. from flask_admin.model import base, filters
  10. from flask_admin.model.template import macro
  11. def wtforms2_and_up(func):
  12. """Decorator for skipping test if wtforms <2
  13. """
  14. if int(wtforms.__version__[0]) < 2:
  15. func.__test__ = False
  16. return func
  17. class Model(object):
  18. def __init__(self, id=None, c1=1, c2=2, c3=3):
  19. self.id = id
  20. self.col1 = c1
  21. self.col2 = c2
  22. self.col3 = c3
  23. class Form(form.BaseForm):
  24. col1 = fields.StringField()
  25. col2 = fields.StringField()
  26. col3 = fields.StringField()
  27. class SimpleFilter(filters.BaseFilter):
  28. def apply(self, query):
  29. query._applied = True
  30. return query
  31. def operation(self):
  32. return 'test'
  33. class MockModelView(base.BaseModelView):
  34. def __init__(self, model, data=None, name=None, category=None,
  35. endpoint=None, url=None, **kwargs):
  36. # Allow to set any attributes from parameters
  37. for k, v in iteritems(kwargs):
  38. setattr(self, k, v)
  39. super(MockModelView, self).__init__(model, name, category, endpoint, url)
  40. self.created_models = []
  41. self.updated_models = []
  42. self.deleted_models = []
  43. self.search_arguments = []
  44. if data is None:
  45. self.all_models = {1: Model(1), 2: Model(2)}
  46. else:
  47. self.all_models = data
  48. self.last_id = len(self.all_models) + 1
  49. # Scaffolding
  50. def get_pk_value(self, model):
  51. return model.id
  52. def scaffold_list_columns(self):
  53. columns = ['col1', 'col2', 'col3']
  54. if self.column_exclude_list:
  55. return filter(lambda x: x not in self.column_exclude_list, columns)
  56. return columns
  57. def init_search(self):
  58. return bool(self.column_searchable_list)
  59. def scaffold_filters(self, name):
  60. return [SimpleFilter(name)]
  61. def scaffold_sortable_columns(self):
  62. return ['col1', 'col2', 'col3']
  63. def scaffold_form(self):
  64. return Form
  65. # Data
  66. def get_list(self, page, sort_field, sort_desc, search, filters,
  67. page_size=None):
  68. self.search_arguments.append((page, sort_field, sort_desc, search, filters))
  69. return len(self.all_models), itervalues(self.all_models)
  70. def get_one(self, id):
  71. return self.all_models.get(int(id))
  72. def create_model(self, form):
  73. model = Model(self.last_id)
  74. self.last_id += 1
  75. form.populate_obj(model)
  76. self.created_models.append(model)
  77. self.all_models[model.id] = model
  78. return True
  79. def update_model(self, form, model):
  80. form.populate_obj(model)
  81. self.updated_models.append(model)
  82. return True
  83. def delete_model(self, model):
  84. self.deleted_models.append(model)
  85. return True
  86. def setup():
  87. app = Flask(__name__)
  88. app.config['CSRF_ENABLED'] = False
  89. app.secret_key = '1'
  90. admin = Admin(app)
  91. return app, admin
  92. def test_mockview():
  93. app, admin = setup()
  94. view = MockModelView(Model)
  95. admin.add_view(view)
  96. eq_(view.model, Model)
  97. eq_(view.name, 'Model')
  98. eq_(view.endpoint, 'model')
  99. # Verify scaffolding
  100. eq_(view._sortable_columns, ['col1', 'col2', 'col3'])
  101. eq_(view._create_form_class, Form)
  102. eq_(view._edit_form_class, Form)
  103. eq_(view._search_supported, False)
  104. eq_(view._filters, None)
  105. client = app.test_client()
  106. # Make model view requests
  107. rv = client.get('/admin/model/')
  108. eq_(rv.status_code, 200)
  109. # Test model creation view
  110. rv = client.get('/admin/model/new/')
  111. eq_(rv.status_code, 200)
  112. rv = client.post('/admin/model/new/',
  113. data=dict(col1='test1', col2='test2', col3='test3'))
  114. eq_(rv.status_code, 302)
  115. eq_(len(view.created_models), 1)
  116. model = view.created_models.pop()
  117. eq_(model.id, 3)
  118. eq_(model.col1, 'test1')
  119. eq_(model.col2, 'test2')
  120. eq_(model.col3, 'test3')
  121. # Try model edit view
  122. rv = client.get('/admin/model/edit/?id=3')
  123. eq_(rv.status_code, 200)
  124. data = rv.data.decode('utf-8')
  125. ok_('test1' in data)
  126. rv = client.post('/admin/model/edit/?id=3',
  127. data=dict(col1='test!', col2='test@', col3='test#'))
  128. eq_(rv.status_code, 302)
  129. eq_(len(view.updated_models), 1)
  130. model = view.updated_models.pop()
  131. eq_(model.col1, 'test!')
  132. eq_(model.col2, 'test@')
  133. eq_(model.col3, 'test#')
  134. rv = client.get('/admin/model/edit/?id=4')
  135. eq_(rv.status_code, 302)
  136. # Attempt to delete model
  137. rv = client.post('/admin/model/delete/?id=3')
  138. eq_(rv.status_code, 302)
  139. eq_(rv.headers['location'], 'http://localhost/admin/model/')
  140. # Create a dispatched application to test that edit view's "save and
  141. # continue" functionality works when app is not located at root
  142. dummy_app = Flask('dummy_app')
  143. dispatched_app = DispatcherMiddleware(dummy_app, {'/dispatched': app})
  144. dispatched_client = Client(dispatched_app)
  145. app_iter, status, headers = dispatched_client.post(
  146. '/dispatched/admin/model/edit/?id=3',
  147. data=dict(col1='another test!', col2='test@', col3='test#', _continue_editing='True'))
  148. eq_(status, '302 FOUND')
  149. eq_(headers['Location'], 'http://localhost/dispatched/admin/model/edit/?id=3')
  150. model = view.updated_models.pop()
  151. eq_(model.col1, 'another test!')
  152. def test_permissions():
  153. app, admin = setup()
  154. view = MockModelView(Model)
  155. admin.add_view(view)
  156. client = app.test_client()
  157. view.can_create = False
  158. rv = client.get('/admin/model/new/')
  159. eq_(rv.status_code, 302)
  160. view.can_edit = False
  161. rv = client.get('/admin/model/edit/?id=1')
  162. eq_(rv.status_code, 302)
  163. view.can_delete = False
  164. rv = client.post('/admin/model/delete/?id=1')
  165. eq_(rv.status_code, 302)
  166. def test_templates():
  167. app, admin = setup()
  168. view = MockModelView(Model)
  169. admin.add_view(view)
  170. client = app.test_client()
  171. view.list_template = 'mock.html'
  172. view.create_template = 'mock.html'
  173. view.edit_template = 'mock.html'
  174. rv = client.get('/admin/model/')
  175. eq_(rv.data, b'Success!')
  176. rv = client.get('/admin/model/new/')
  177. eq_(rv.data, b'Success!')
  178. rv = client.get('/admin/model/edit/?id=1')
  179. eq_(rv.data, b'Success!')
  180. def test_list_columns():
  181. app, admin = setup()
  182. view = MockModelView(Model,
  183. column_list=['col1', 'col3'],
  184. column_labels=dict(col1='Column1'))
  185. admin.add_view(view)
  186. eq_(len(view._list_columns), 2)
  187. eq_(view._list_columns, [('col1', 'Column1'), ('col3', 'Col3')])
  188. client = app.test_client()
  189. rv = client.get('/admin/model/')
  190. data = rv.data.decode('utf-8')
  191. ok_('Column1' in data)
  192. ok_('Col2' not in data)
  193. def test_exclude_columns():
  194. app, admin = setup()
  195. view = MockModelView(Model, column_exclude_list=['col2'])
  196. admin.add_view(view)
  197. eq_(view._list_columns, [('col1', 'Col1'), ('col3', 'Col3')])
  198. client = app.test_client()
  199. rv = client.get('/admin/model/')
  200. data = rv.data.decode('utf-8')
  201. ok_('Col1' in data)
  202. ok_('Col2' not in data)
  203. def test_sortable_columns():
  204. app, admin = setup()
  205. view = MockModelView(Model, column_sortable_list=['col1', ('col2', 'test1')])
  206. admin.add_view(view)
  207. eq_(view._sortable_columns, dict(col1='col1', col2='test1'))
  208. def test_column_searchable_list():
  209. app, admin = setup()
  210. view = MockModelView(Model, column_searchable_list=['col1', 'col2'])
  211. admin.add_view(view)
  212. eq_(view._search_supported, True)
  213. # TODO: Make calls with search
  214. def test_column_filters():
  215. app, admin = setup()
  216. view = MockModelView(Model, column_filters=['col1', 'col2'])
  217. admin.add_view(view)
  218. eq_(len(view._filters), 2)
  219. eq_(view._filters[0].name, 'col1')
  220. eq_(view._filters[1].name, 'col2')
  221. eq_([(f['index'], f['operation']) for f in view._filter_groups[u'col1']], [(0, 'test')])
  222. eq_([(f['index'], f['operation']) for f in view._filter_groups[u'col2']], [(1, 'test')])
  223. # TODO: Make calls with filters
  224. def test_filter_list_callable():
  225. app, admin = setup()
  226. flt = SimpleFilter('test', options=lambda: [('1', 'Test 1'), ('2', 'Test 2')])
  227. view = MockModelView(Model, column_filters=[flt])
  228. admin.add_view(view)
  229. opts = flt.get_options(view)
  230. eq_(len(opts), 2)
  231. eq_(opts, [('1', 'Test 1'), ('2', 'Test 2')])
  232. def test_form():
  233. # TODO: form_columns
  234. # TODO: form_excluded_columns
  235. # TODO: form_args
  236. # TODO: form_widget_args
  237. pass
  238. @wtforms2_and_up
  239. def test_csrf():
  240. class SecureModelView(MockModelView):
  241. form_base_class = form.SecureForm
  242. def scaffold_form(self):
  243. return form.SecureForm
  244. def get_csrf_token(data):
  245. data = data.split('name="csrf_token" type="hidden" value="')[1]
  246. token = data.split('"')[0]
  247. return token
  248. app, admin = setup()
  249. view = SecureModelView(Model, endpoint='secure')
  250. admin.add_view(view)
  251. client = app.test_client()
  252. ################
  253. # create_view
  254. ################
  255. rv = client.get('/admin/secure/new/')
  256. eq_(rv.status_code, 200)
  257. ok_(u'name="csrf_token"' in rv.data.decode('utf-8'))
  258. csrf_token = get_csrf_token(rv.data.decode('utf-8'))
  259. # Create without CSRF token
  260. rv = client.post('/admin/secure/new/', data=dict(name='test1'))
  261. eq_(rv.status_code, 200)
  262. # Create with CSRF token
  263. rv = client.post('/admin/secure/new/', data=dict(name='test1',
  264. csrf_token=csrf_token))
  265. eq_(rv.status_code, 302)
  266. ###############
  267. # edit_view
  268. ###############
  269. rv = client.get('/admin/secure/edit/?url=%2Fadmin%2Fsecure%2F&id=1')
  270. eq_(rv.status_code, 200)
  271. ok_(u'name="csrf_token"' in rv.data.decode('utf-8'))
  272. csrf_token = get_csrf_token(rv.data.decode('utf-8'))
  273. # Edit without CSRF token
  274. rv = client.post('/admin/secure/edit/?url=%2Fadmin%2Fsecure%2F&id=1',
  275. data=dict(name='test1'))
  276. eq_(rv.status_code, 200)
  277. # Edit with CSRF token
  278. rv = client.post('/admin/secure/edit/?url=%2Fadmin%2Fsecure%2F&id=1',
  279. data=dict(name='test1', csrf_token=csrf_token))
  280. eq_(rv.status_code, 302)
  281. ################
  282. # delete_view
  283. ################
  284. rv = client.get('/admin/secure/')
  285. eq_(rv.status_code, 200)
  286. ok_(u'name="csrf_token"' in rv.data.decode('utf-8'))
  287. csrf_token = get_csrf_token(rv.data.decode('utf-8'))
  288. # Delete without CSRF token, test validation errors
  289. rv = client.post('/admin/secure/delete/',
  290. data=dict(id="1", url="/admin/secure/"), follow_redirects=True)
  291. eq_(rv.status_code, 200)
  292. ok_(u'Record was successfully deleted.' not in rv.data.decode('utf-8'))
  293. ok_(u'Failed to delete record.' in rv.data.decode('utf-8'))
  294. # Delete with CSRF token
  295. rv = client.post('/admin/secure/delete/',
  296. data=dict(id="1", url="/admin/secure/", csrf_token=csrf_token),
  297. follow_redirects=True)
  298. eq_(rv.status_code, 200)
  299. ok_(u'Record was successfully deleted.' in rv.data.decode('utf-8'))
  300. ################
  301. # actions
  302. ################
  303. rv = client.get('/admin/secure/')
  304. eq_(rv.status_code, 200)
  305. ok_(u'name="csrf_token"' in rv.data.decode('utf-8'))
  306. csrf_token = get_csrf_token(rv.data.decode('utf-8'))
  307. # Delete without CSRF token, test validation errors
  308. rv = client.post('/admin/secure/action/',
  309. data=dict(rowid='1', url='/admin/secure/', action='delete'),
  310. follow_redirects=True)
  311. eq_(rv.status_code, 200)
  312. ok_(u'Record was successfully deleted.' not in rv.data.decode('utf-8'))
  313. ok_(u'Failed to perform action.' in rv.data.decode('utf-8'))
  314. def test_custom_form():
  315. app, admin = setup()
  316. class TestForm(form.BaseForm):
  317. pass
  318. view = MockModelView(Model, form=TestForm)
  319. admin.add_view(view)
  320. eq_(view._create_form_class, TestForm)
  321. eq_(view._edit_form_class, TestForm)
  322. ok_(not hasattr(view._create_form_class, 'col1'))
  323. def test_modal_edit():
  324. # bootstrap 2 - test edit_modal
  325. app_bs2 = Flask(__name__)
  326. admin_bs2 = Admin(app_bs2, template_mode="bootstrap2")
  327. edit_modal_on = MockModelView(Model, edit_modal=True,
  328. endpoint="edit_modal_on")
  329. edit_modal_off = MockModelView(Model, edit_modal=False,
  330. endpoint="edit_modal_off")
  331. create_modal_on = MockModelView(Model, create_modal=True,
  332. endpoint="create_modal_on")
  333. create_modal_off = MockModelView(Model, create_modal=False,
  334. endpoint="create_modal_off")
  335. admin_bs2.add_view(edit_modal_on)
  336. admin_bs2.add_view(edit_modal_off)
  337. admin_bs2.add_view(create_modal_on)
  338. admin_bs2.add_view(create_modal_off)
  339. client_bs2 = app_bs2.test_client()
  340. # bootstrap 2 - ensure modal window is added when edit_modal is enabled
  341. rv = client_bs2.get('/admin/edit_modal_on/')
  342. eq_(rv.status_code, 200)
  343. data = rv.data.decode('utf-8')
  344. ok_('fa_modal_window' in data)
  345. # bootstrap 2 - test edit modal disabled
  346. rv = client_bs2.get('/admin/edit_modal_off/')
  347. eq_(rv.status_code, 200)
  348. data = rv.data.decode('utf-8')
  349. ok_('fa_modal_window' not in data)
  350. # bootstrap 2 - ensure modal window is added when create_modal is enabled
  351. rv = client_bs2.get('/admin/create_modal_on/')
  352. eq_(rv.status_code, 200)
  353. data = rv.data.decode('utf-8')
  354. ok_('fa_modal_window' in data)
  355. # bootstrap 2 - test create modal disabled
  356. rv = client_bs2.get('/admin/create_modal_off/')
  357. eq_(rv.status_code, 200)
  358. data = rv.data.decode('utf-8')
  359. ok_('fa_modal_window' not in data)
  360. # bootstrap 3
  361. app_bs3 = Flask(__name__)
  362. admin_bs3 = Admin(app_bs3, template_mode="bootstrap3")
  363. admin_bs3.add_view(edit_modal_on)
  364. admin_bs3.add_view(edit_modal_off)
  365. admin_bs3.add_view(create_modal_on)
  366. admin_bs3.add_view(create_modal_off)
  367. client_bs3 = app_bs3.test_client()
  368. # bootstrap 3 - ensure modal window is added when edit_modal is enabled
  369. rv = client_bs3.get('/admin/edit_modal_on/')
  370. eq_(rv.status_code, 200)
  371. data = rv.data.decode('utf-8')
  372. ok_('fa_modal_window' in data)
  373. # bootstrap 3 - test modal disabled
  374. rv = client_bs3.get('/admin/edit_modal_off/')
  375. eq_(rv.status_code, 200)
  376. data = rv.data.decode('utf-8')
  377. ok_('fa_modal_window' not in data)
  378. # bootstrap 3 - ensure modal window is added when edit_modal is enabled
  379. rv = client_bs3.get('/admin/create_modal_on/')
  380. eq_(rv.status_code, 200)
  381. data = rv.data.decode('utf-8')
  382. ok_('fa_modal_window' in data)
  383. # bootstrap 3 - test modal disabled
  384. rv = client_bs3.get('/admin/create_modal_off/')
  385. eq_(rv.status_code, 200)
  386. data = rv.data.decode('utf-8')
  387. ok_('fa_modal_window' not in data)
  388. def check_class_name():
  389. class DummyView(MockModelView):
  390. pass
  391. view = DummyView(Model)
  392. eq_(view.name, 'Dummy View')
  393. def test_export_csv():
  394. app, admin = setup()
  395. client = app.test_client()
  396. # test redirect when csv export is disabled
  397. view = MockModelView(Model, column_list=['col1', 'col2'], endpoint="test")
  398. admin.add_view(view)
  399. rv = client.get('/admin/test/export/csv/')
  400. eq_(rv.status_code, 302)
  401. # basic test of csv export with a few records
  402. view_data = {
  403. 1: Model(1, "col1_1", "col2_1"),
  404. 2: Model(2, "col1_2", "col2_2"),
  405. 3: Model(3, "col1_3", "col2_3"),
  406. }
  407. view = MockModelView(Model, view_data, can_export=True,
  408. column_list=['col1', 'col2'])
  409. admin.add_view(view)
  410. rv = client.get('/admin/model/export/csv/')
  411. data = rv.data.decode('utf-8')
  412. eq_(rv.mimetype, 'text/csv')
  413. eq_(rv.status_code, 200)
  414. ok_("Col1,Col2\r\n"
  415. "col1_1,col2_1\r\n"
  416. "col1_2,col2_2\r\n"
  417. "col1_3,col2_3\r\n" == data)
  418. # test explicit use of column_export_list
  419. view = MockModelView(Model, view_data, can_export=True,
  420. column_list=['col1', 'col2'],
  421. column_export_list=['id', 'col1', 'col2'],
  422. endpoint='exportinclusion')
  423. admin.add_view(view)
  424. rv = client.get('/admin/exportinclusion/export/csv/')
  425. data = rv.data.decode('utf-8')
  426. eq_(rv.mimetype, 'text/csv')
  427. eq_(rv.status_code, 200)
  428. ok_("Id,Col1,Col2\r\n"
  429. "1,col1_1,col2_1\r\n"
  430. "2,col1_2,col2_2\r\n"
  431. "3,col1_3,col2_3\r\n" == data)
  432. # test explicit use of column_export_exclude_list
  433. view = MockModelView(Model, view_data, can_export=True,
  434. column_list=['col1', 'col2'],
  435. column_export_exclude_list=['col2'],
  436. endpoint='exportexclusion')
  437. admin.add_view(view)
  438. rv = client.get('/admin/exportexclusion/export/csv/')
  439. data = rv.data.decode('utf-8')
  440. eq_(rv.mimetype, 'text/csv')
  441. eq_(rv.status_code, 200)
  442. ok_("Col1\r\n"
  443. "col1_1\r\n"
  444. "col1_2\r\n"
  445. "col1_3\r\n" == data)
  446. # test utf8 characters in csv export
  447. view_data[4] = Model(1, u'\u2013ut8_1\u2013', u'\u2013utf8_2\u2013')
  448. view = MockModelView(Model, view_data, can_export=True,
  449. column_list=['col1', 'col2'], endpoint="utf8")
  450. admin.add_view(view)
  451. rv = client.get('/admin/utf8/export/csv/')
  452. data = rv.data.decode('utf-8')
  453. eq_(rv.status_code, 200)
  454. ok_(u'\u2013ut8_1\u2013,\u2013utf8_2\u2013\r\n' in data)
  455. # test None type, integer type, column_labels, and column_formatters
  456. view_data = {
  457. 1: Model(1, "col1_1", 1),
  458. 2: Model(2, "col1_2", 2),
  459. 3: Model(3, None, 3),
  460. }
  461. view = MockModelView(
  462. Model, view_data, can_export=True, column_list=['col1', 'col2'],
  463. column_labels={'col1': 'Str Field', 'col2': 'Int Field'},
  464. column_formatters=dict(col2=lambda v, c, m, p: m.col2 * 2),
  465. endpoint="types_and_formatters"
  466. )
  467. admin.add_view(view)
  468. rv = client.get('/admin/types_and_formatters/export/csv/')
  469. data = rv.data.decode('utf-8')
  470. eq_(rv.status_code, 200)
  471. ok_("Str Field,Int Field\r\n"
  472. "col1_1,2\r\n"
  473. "col1_2,4\r\n"
  474. ",6\r\n" == data)
  475. # test column_formatters_export and column_formatters_export
  476. type_formatters = {type(None): lambda view, value: "null"}
  477. view = MockModelView(
  478. Model, view_data, can_export=True, column_list=['col1', 'col2'],
  479. column_formatters_export=dict(col2=lambda v, c, m, p: m.col2 * 3),
  480. column_formatters=dict(col2=lambda v, c, m, p: m.col2 * 2), # overridden
  481. column_type_formatters_export=type_formatters,
  482. endpoint="export_types_and_formatters"
  483. )
  484. admin.add_view(view)
  485. rv = client.get('/admin/export_types_and_formatters/export/csv/')
  486. data = rv.data.decode('utf-8')
  487. eq_(rv.status_code, 200)
  488. ok_("Col1,Col2\r\n"
  489. "col1_1,3\r\n"
  490. "col1_2,6\r\n"
  491. "null,9\r\n" == data)
  492. # Macros are not implemented for csv export yet and will throw an error
  493. view = MockModelView(
  494. Model, can_export=True, column_list=['col1', 'col2'],
  495. column_formatters=dict(col1=macro('render_macro')),
  496. endpoint="macro_exception"
  497. )
  498. admin.add_view(view)
  499. rv = client.get('/admin/macro_exception/export/csv/')
  500. data = rv.data.decode('utf-8')
  501. eq_(rv.status_code, 500)
  502. # We should be able to specify column_formatters_export
  503. # and not get an exception if a column_formatter is using a macro
  504. def export_formatter(v, c, m, p):
  505. return m.col1 if m else ''
  506. view = MockModelView(
  507. Model, view_data, can_export=True, column_list=['col1', 'col2'],
  508. column_formatters=dict(col1=macro('render_macro')),
  509. column_formatters_export=dict(col1=export_formatter),
  510. endpoint="macro_exception_formatter_override"
  511. )
  512. admin.add_view(view)
  513. rv = client.get('/admin/macro_exception_formatter_override/export/csv/')
  514. data = rv.data.decode('utf-8')
  515. eq_(rv.status_code, 200)
  516. ok_("Col1,Col2\r\n"
  517. "col1_1,1\r\n"
  518. "col1_2,2\r\n"
  519. ",3\r\n" == data)
  520. # We should not get an exception if a column_formatter is
  521. # using a macro but it is on the column_export_exclude_list
  522. view = MockModelView(
  523. Model, view_data, can_export=True, column_list=['col1', 'col2'],
  524. column_formatters=dict(col1=macro('render_macro')),
  525. column_export_exclude_list=['col1'],
  526. endpoint="macro_exception_exclude_override"
  527. )
  528. admin.add_view(view)
  529. rv = client.get('/admin/macro_exception_exclude_override/export/csv/')
  530. data = rv.data.decode('utf-8')
  531. eq_(rv.status_code, 200)
  532. ok_("Col2\r\n"
  533. "1\r\n"
  534. "2\r\n"
  535. "3\r\n" == data)
  536. # When we use column_export_list to hide the macro field
  537. # we should not get an exception
  538. view = MockModelView(
  539. Model, view_data, can_export=True, column_list=['col1', 'col2'],
  540. column_formatters=dict(col1=macro('render_macro')),
  541. column_export_list=['col2'],
  542. endpoint="macro_exception_list_override"
  543. )
  544. admin.add_view(view)
  545. rv = client.get('/admin/macro_exception_list_override/export/csv/')
  546. data = rv.data.decode('utf-8')
  547. eq_(rv.status_code, 200)
  548. ok_("Col2\r\n"
  549. "1\r\n"
  550. "2\r\n"
  551. "3\r\n" == data)
  552. # If they define a macro on the column_formatters_export list
  553. # then raise an exception
  554. view = MockModelView(
  555. Model, view_data, can_export=True, column_list=['col1', 'col2'],
  556. column_formatters=dict(col1=macro('render_macro')),
  557. endpoint="macro_exception_macro_override"
  558. )
  559. admin.add_view(view)
  560. rv = client.get('/admin/macro_exception_macro_override/export/csv/')
  561. data = rv.data.decode('utf-8')
  562. eq_(rv.status_code, 500)
  563. def test_list_row_actions():
  564. app, admin = setup()
  565. client = app.test_client()
  566. from flask_admin.model import template
  567. # Test default actions
  568. view = MockModelView(Model, endpoint='test')
  569. admin.add_view(view)
  570. actions = view.get_list_row_actions()
  571. ok_(isinstance(actions[0], template.EditRowAction))
  572. ok_(isinstance(actions[1], template.DeleteRowAction))
  573. rv = client.get('/admin/test/')
  574. eq_(rv.status_code, 200)
  575. # Test default actions
  576. view = MockModelView(Model, endpoint='test1', can_edit=False, can_delete=False, can_view_details=True)
  577. admin.add_view(view)
  578. actions = view.get_list_row_actions()
  579. eq_(len(actions), 1)
  580. ok_(isinstance(actions[0], template.ViewRowAction))
  581. rv = client.get('/admin/test1/')
  582. eq_(rv.status_code, 200)
  583. # Test popups
  584. view = MockModelView(Model, endpoint='test2',
  585. can_view_details=True,
  586. details_modal=True,
  587. edit_modal=True)
  588. admin.add_view(view)
  589. actions = view.get_list_row_actions()
  590. ok_(isinstance(actions[0], template.ViewPopupRowAction))
  591. ok_(isinstance(actions[1], template.EditPopupRowAction))
  592. ok_(isinstance(actions[2], template.DeleteRowAction))
  593. rv = client.get('/admin/test2/')
  594. eq_(rv.status_code, 200)
  595. # Test custom views
  596. view = MockModelView(Model, endpoint='test3',
  597. column_extra_row_actions=[
  598. template.LinkRowAction('glyphicon glyphicon-off', 'http://localhost/?id={row_id}'),
  599. template.EndpointLinkRowAction('glyphicon glyphicon-test', 'test1.index_view')
  600. ])
  601. admin.add_view(view)
  602. actions = view.get_list_row_actions()
  603. ok_(isinstance(actions[0], template.EditRowAction))
  604. ok_(isinstance(actions[1], template.DeleteRowAction))
  605. ok_(isinstance(actions[2], template.LinkRowAction))
  606. ok_(isinstance(actions[3], template.EndpointLinkRowAction))
  607. rv = client.get('/admin/test3/')
  608. eq_(rv.status_code, 200)
  609. data = rv.data.decode('utf-8')
  610. ok_('glyphicon-off' in data)
  611. ok_('http://localhost/?id=' in data)
  612. ok_('glyphicon-test' in data)