flask-appbuilder 3.2.1__py3-none-any.whl → 5.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. flask_appbuilder/__init__.py +2 -3
  2. flask_appbuilder/_compat.py +0 -1
  3. flask_appbuilder/actions.py +14 -14
  4. flask_appbuilder/api/__init__.py +741 -527
  5. flask_appbuilder/api/convert.py +104 -98
  6. flask_appbuilder/api/manager.py +14 -8
  7. flask_appbuilder/api/schemas.py +12 -1
  8. flask_appbuilder/babel/manager.py +12 -16
  9. flask_appbuilder/base.py +353 -280
  10. flask_appbuilder/basemanager.py +1 -1
  11. flask_appbuilder/baseviews.py +241 -164
  12. flask_appbuilder/charts/jsontools.py +10 -10
  13. flask_appbuilder/charts/views.py +56 -60
  14. flask_appbuilder/cli.py +115 -70
  15. flask_appbuilder/const.py +52 -52
  16. flask_appbuilder/exceptions.py +67 -5
  17. flask_appbuilder/fields.py +32 -23
  18. flask_appbuilder/fieldwidgets.py +34 -27
  19. flask_appbuilder/filemanager.py +33 -45
  20. flask_appbuilder/filters.py +11 -13
  21. flask_appbuilder/forms.py +31 -35
  22. flask_appbuilder/hooks.py +90 -0
  23. flask_appbuilder/menu.py +35 -10
  24. flask_appbuilder/models/base.py +47 -57
  25. flask_appbuilder/models/decorators.py +13 -13
  26. flask_appbuilder/models/filters.py +42 -38
  27. flask_appbuilder/models/generic/__init__.py +29 -29
  28. flask_appbuilder/models/generic/filters.py +11 -3
  29. flask_appbuilder/models/generic/interface.py +1 -3
  30. flask_appbuilder/models/group.py +37 -39
  31. flask_appbuilder/models/mixins.py +22 -18
  32. flask_appbuilder/models/sqla/__init__.py +19 -72
  33. flask_appbuilder/models/sqla/base.py +24 -0
  34. flask_appbuilder/models/sqla/base_legacy.py +132 -0
  35. flask_appbuilder/models/sqla/filters.py +132 -19
  36. flask_appbuilder/models/sqla/interface.py +390 -276
  37. flask_appbuilder/security/api.py +31 -35
  38. flask_appbuilder/security/decorators.py +181 -83
  39. flask_appbuilder/security/forms.py +20 -31
  40. flask_appbuilder/security/manager.py +715 -489
  41. flask_appbuilder/security/registerviews.py +29 -112
  42. flask_appbuilder/security/schemas.py +43 -0
  43. flask_appbuilder/security/sqla/apis/__init__.py +8 -0
  44. flask_appbuilder/security/sqla/apis/group/__init__.py +1 -0
  45. flask_appbuilder/security/sqla/apis/group/api.py +227 -0
  46. flask_appbuilder/security/sqla/apis/group/schema.py +73 -0
  47. flask_appbuilder/security/sqla/apis/permission/__init__.py +1 -0
  48. flask_appbuilder/security/sqla/apis/permission/api.py +19 -0
  49. flask_appbuilder/security/sqla/apis/permission_view_menu/__init__.py +1 -0
  50. flask_appbuilder/security/sqla/apis/permission_view_menu/api.py +16 -0
  51. flask_appbuilder/security/sqla/apis/role/__init__.py +1 -0
  52. flask_appbuilder/security/sqla/apis/role/api.py +306 -0
  53. flask_appbuilder/security/sqla/apis/role/schema.py +27 -0
  54. flask_appbuilder/security/sqla/apis/user/__init__.py +1 -0
  55. flask_appbuilder/security/sqla/apis/user/api.py +292 -0
  56. flask_appbuilder/security/sqla/apis/user/schema.py +97 -0
  57. flask_appbuilder/security/sqla/apis/user/validator.py +27 -0
  58. flask_appbuilder/security/sqla/apis/view_menu/__init__.py +1 -0
  59. flask_appbuilder/security/sqla/apis/view_menu/api.py +18 -0
  60. flask_appbuilder/security/sqla/manager.py +421 -203
  61. flask_appbuilder/security/sqla/models.py +192 -57
  62. flask_appbuilder/security/utils.py +9 -0
  63. flask_appbuilder/security/views.py +232 -229
  64. flask_appbuilder/static/.DS_Store +0 -0
  65. flask_appbuilder/static/appbuilder/css/ab.css +20 -12
  66. flask_appbuilder/static/appbuilder/css/bootstrap-datepicker/bootstrap-datepicker3.min.css +7 -0
  67. flask_appbuilder/static/appbuilder/css/bootstrap.min.css.map +1 -0
  68. flask_appbuilder/static/appbuilder/css/flags/flags16.css +249 -245
  69. flask_appbuilder/static/appbuilder/css/fontawesome/all.min.css +6 -0
  70. flask_appbuilder/static/appbuilder/css/fontawesome/brands.min.css +6 -0
  71. flask_appbuilder/static/appbuilder/css/fontawesome/fontawesome.min.css +6 -0
  72. flask_appbuilder/static/appbuilder/css/fontawesome/regular.min.css +6 -0
  73. flask_appbuilder/static/appbuilder/css/fontawesome/solid.min.css +6 -0
  74. flask_appbuilder/static/appbuilder/css/fontawesome/svg-with-js.min.css +6 -0
  75. flask_appbuilder/static/appbuilder/css/fontawesome/v4-font-face.min.css +6 -0
  76. flask_appbuilder/static/appbuilder/css/fontawesome/v4-shims.min.css +6 -0
  77. flask_appbuilder/static/appbuilder/css/fontawesome/v5-font-face.min.css +6 -0
  78. flask_appbuilder/static/appbuilder/css/images/flags16.png +0 -0
  79. flask_appbuilder/static/appbuilder/css/select2/select2-bootstrap.min.css +7 -0
  80. flask_appbuilder/static/appbuilder/css/select2/select2.min.css +1 -0
  81. flask_appbuilder/static/appbuilder/css/swagger/swagger-ui.css +3 -0
  82. flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.ttf +0 -0
  83. flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.woff2 +0 -0
  84. flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.ttf +0 -0
  85. flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.woff2 +0 -0
  86. flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.ttf +0 -0
  87. flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.woff2 +0 -0
  88. flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.ttf +0 -0
  89. flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.woff2 +0 -0
  90. flask_appbuilder/static/appbuilder/js/ab.js +33 -23
  91. flask_appbuilder/static/appbuilder/js/ab_filters.js +91 -84
  92. flask_appbuilder/static/appbuilder/js/bootstrap-datepicker/bootstrap-datepicker.min.js +8 -0
  93. flask_appbuilder/static/appbuilder/js/jquery-latest.js +2 -2
  94. flask_appbuilder/static/appbuilder/js/select2/select2.min.js +2 -0
  95. flask_appbuilder/static/appbuilder/js/swagger-ui-bundle.js +3 -0
  96. flask_appbuilder/templates/appbuilder/baselib.html +9 -3
  97. flask_appbuilder/templates/appbuilder/general/lib.html +60 -34
  98. flask_appbuilder/templates/appbuilder/general/model/edit.html +1 -1
  99. flask_appbuilder/templates/appbuilder/general/model/edit_cascade.html +1 -1
  100. flask_appbuilder/templates/appbuilder/general/model/search.html +3 -2
  101. flask_appbuilder/templates/appbuilder/general/model/show.html +1 -1
  102. flask_appbuilder/templates/appbuilder/general/model/show_cascade.html +1 -1
  103. flask_appbuilder/templates/appbuilder/general/security/login_db.html +7 -7
  104. flask_appbuilder/templates/appbuilder/general/security/login_ldap.html +5 -5
  105. flask_appbuilder/templates/appbuilder/general/security/login_oauth.html +24 -49
  106. flask_appbuilder/templates/appbuilder/general/widgets/base_list.html +2 -1
  107. flask_appbuilder/templates/appbuilder/general/widgets/chart.html +4 -2
  108. flask_appbuilder/templates/appbuilder/general/widgets/direct_chart.html +4 -3
  109. flask_appbuilder/templates/appbuilder/general/widgets/multiple_chart.html +3 -2
  110. flask_appbuilder/templates/appbuilder/general/widgets/search.html +11 -10
  111. flask_appbuilder/templates/appbuilder/init.html +37 -43
  112. flask_appbuilder/templates/appbuilder/navbar_menu.html +1 -1
  113. flask_appbuilder/templates/appbuilder/navbar_right.html +2 -2
  114. flask_appbuilder/templates/appbuilder/swagger/swagger.html +22 -19
  115. flask_appbuilder/translations/de/LC_MESSAGES/messages.mo +0 -0
  116. flask_appbuilder/translations/de/LC_MESSAGES/messages.po +305 -161
  117. flask_appbuilder/translations/fa/LC_MESSAGES/messages.mo +0 -0
  118. flask_appbuilder/translations/fa/LC_MESSAGES/messages.po +802 -0
  119. flask_appbuilder/translations/fr/LC_MESSAGES/messages.po +461 -319
  120. flask_appbuilder/translations/pt_BR/LC_MESSAGES/messages.po +650 -650
  121. flask_appbuilder/translations/ru/LC_MESSAGES/messages.po +1 -1
  122. flask_appbuilder/translations/sl/LC_MESSAGES/messages.mo +0 -0
  123. flask_appbuilder/translations/sl/LC_MESSAGES/messages.po +690 -0
  124. flask_appbuilder/translations/tr/LC_MESSAGES/messages.mo +0 -0
  125. flask_appbuilder/translations/tr/LC_MESSAGES/messages.po +1015 -0
  126. flask_appbuilder/upload.py +20 -22
  127. flask_appbuilder/urltools.py +39 -19
  128. flask_appbuilder/utils/base.py +76 -0
  129. flask_appbuilder/utils/legacy.py +33 -0
  130. flask_appbuilder/utils/limit.py +20 -0
  131. flask_appbuilder/validators.py +73 -14
  132. flask_appbuilder/views.py +75 -424
  133. flask_appbuilder/widgets.py +50 -51
  134. {Flask_AppBuilder-3.2.1.dist-info → flask_appbuilder-5.0.2.dist-info}/METADATA +36 -76
  135. flask_appbuilder-5.0.2.dist-info/RECORD +240 -0
  136. {Flask_AppBuilder-3.2.1.dist-info → flask_appbuilder-5.0.2.dist-info}/WHEEL +1 -1
  137. flask_appbuilder-5.0.2.dist-info/entry_points.txt +2 -0
  138. Flask_AppBuilder-3.2.1.dist-info/RECORD +0 -270
  139. Flask_AppBuilder-3.2.1.dist-info/entry_points.txt +0 -6
  140. flask_appbuilder/console.py +0 -426
  141. flask_appbuilder/models/mongoengine/__init__.py +0 -0
  142. flask_appbuilder/models/mongoengine/fields.py +0 -65
  143. flask_appbuilder/models/mongoengine/filters.py +0 -145
  144. flask_appbuilder/models/mongoengine/interface.py +0 -328
  145. flask_appbuilder/security/mongoengine/__init__.py +0 -0
  146. flask_appbuilder/security/mongoengine/manager.py +0 -402
  147. flask_appbuilder/security/mongoengine/models.py +0 -120
  148. flask_appbuilder/static/appbuilder/css/font-awesome.min.css +0 -4
  149. flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.css +0 -9
  150. flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.js +0 -28
  151. flask_appbuilder/static/appbuilder/fonts/FontAwesome.otf +0 -0
  152. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.eot +0 -0
  153. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.svg +0 -2671
  154. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.ttf +0 -0
  155. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff +0 -0
  156. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff2 +0 -0
  157. flask_appbuilder/static/appbuilder/img/aol.png +0 -0
  158. flask_appbuilder/static/appbuilder/img/flags/flags16.png +0 -0
  159. flask_appbuilder/static/appbuilder/img/flickr.png +0 -0
  160. flask_appbuilder/static/appbuilder/img/google.png +0 -0
  161. flask_appbuilder/static/appbuilder/img/myopenid.png +0 -0
  162. flask_appbuilder/static/appbuilder/img/yahoo.png +0 -0
  163. flask_appbuilder/static/appbuilder/js/_google_charts.js +0 -39
  164. flask_appbuilder/static/appbuilder/js/html5shiv.js +0 -8
  165. flask_appbuilder/static/appbuilder/js/respond.min.js +0 -6
  166. flask_appbuilder/static/appbuilder/select2/select2-spinner.gif +0 -0
  167. flask_appbuilder/static/appbuilder/select2/select2.css +0 -1205
  168. flask_appbuilder/static/appbuilder/select2/select2.js +0 -23
  169. flask_appbuilder/static/appbuilder/select2/select2.png +0 -0
  170. flask_appbuilder/static/appbuilder/select2/select2x2.png +0 -0
  171. flask_appbuilder/templates/appbuilder/general/security/login_oid.html +0 -129
  172. flask_appbuilder/templates/appbuilder/general/security/resetpassword.html +0 -29
  173. flask_appbuilder/tests/__init__.py +0 -0
  174. flask_appbuilder/tests/__pycache__/__init__.cpython-36.pyc +0 -0
  175. flask_appbuilder/tests/__pycache__/__init__.cpython-37.pyc +0 -0
  176. flask_appbuilder/tests/__pycache__/_test_auth_ldap.cpython-37.pyc +0 -0
  177. flask_appbuilder/tests/__pycache__/_test_auth_oauth.cpython-37.pyc +0 -0
  178. flask_appbuilder/tests/__pycache__/_test_ldapsearch.cpython-36.pyc +0 -0
  179. flask_appbuilder/tests/__pycache__/_test_oauth_registration_role.cpython-36.pyc +0 -0
  180. flask_appbuilder/tests/__pycache__/base.cpython-36.pyc +0 -0
  181. flask_appbuilder/tests/__pycache__/base.cpython-37.pyc +0 -0
  182. flask_appbuilder/tests/__pycache__/config_api.cpython-36.pyc +0 -0
  183. flask_appbuilder/tests/__pycache__/config_api.cpython-37.pyc +0 -0
  184. flask_appbuilder/tests/__pycache__/const.cpython-36.pyc +0 -0
  185. flask_appbuilder/tests/__pycache__/const.cpython-37.pyc +0 -0
  186. flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-36.pyc +0 -0
  187. flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-37.pyc +0 -0
  188. flask_appbuilder/tests/__pycache__/test_api.cpython-36.pyc +0 -0
  189. flask_appbuilder/tests/__pycache__/test_api.cpython-37.pyc +0 -0
  190. flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-36.pyc +0 -0
  191. flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-37.pyc +0 -0
  192. flask_appbuilder/tests/__pycache__/test_menu.cpython-36.pyc +0 -0
  193. flask_appbuilder/tests/__pycache__/test_menu.cpython-37.pyc +0 -0
  194. flask_appbuilder/tests/__pycache__/test_mongoengine.cpython-36.pyc +0 -0
  195. flask_appbuilder/tests/__pycache__/test_mvc.cpython-36.pyc +0 -0
  196. flask_appbuilder/tests/__pycache__/test_mvc.cpython-37.pyc +0 -0
  197. flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-36.pyc +0 -0
  198. flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-37.pyc +0 -0
  199. flask_appbuilder/tests/_test_auth_ldap.py +0 -1045
  200. flask_appbuilder/tests/_test_auth_oauth.py +0 -419
  201. flask_appbuilder/tests/_test_ldapsearch.py +0 -135
  202. flask_appbuilder/tests/_test_oauth_registration_role.py +0 -59
  203. flask_appbuilder/tests/app.db +0 -0
  204. flask_appbuilder/tests/base.py +0 -90
  205. flask_appbuilder/tests/config_api.py +0 -21
  206. flask_appbuilder/tests/const.py +0 -9
  207. flask_appbuilder/tests/mongoengine/__init__.py +0 -0
  208. flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-36.pyc +0 -0
  209. flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-37.pyc +0 -0
  210. flask_appbuilder/tests/mongoengine/__pycache__/models.cpython-36.pyc +0 -0
  211. flask_appbuilder/tests/mongoengine/models.py +0 -41
  212. flask_appbuilder/tests/sqla/__init__.py +0 -0
  213. flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-36.pyc +0 -0
  214. flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-37.pyc +0 -0
  215. flask_appbuilder/tests/sqla/__pycache__/models.cpython-36.pyc +0 -0
  216. flask_appbuilder/tests/sqla/__pycache__/models.cpython-37.pyc +0 -0
  217. flask_appbuilder/tests/sqla/models.py +0 -340
  218. flask_appbuilder/tests/test_0_fixture.py +0 -39
  219. flask_appbuilder/tests/test_api.py +0 -2790
  220. flask_appbuilder/tests/test_fab_cli.py +0 -72
  221. flask_appbuilder/tests/test_menu.py +0 -122
  222. flask_appbuilder/tests/test_mongoengine.py +0 -572
  223. flask_appbuilder/tests/test_mvc.py +0 -1710
  224. flask_appbuilder/tests/test_sqlalchemy.py +0 -24
  225. flask_appbuilder/translations/__pycache__/__init__.cpython-36.pyc +0 -0
  226. flask_appbuilder/translations/es/LC_MESSAGES/messages.po~ +0 -582
  227. {Flask_AppBuilder-3.2.1.dist-info → flask_appbuilder-5.0.2.dist-info}/LICENSE +0 -0
  228. {Flask_AppBuilder-3.2.1.dist-info → flask_appbuilder-5.0.2.dist-info}/top_level.txt +0 -0
@@ -1,1710 +0,0 @@
1
- import datetime
2
- import json
3
- import logging
4
- from typing import Set
5
-
6
- from flask import Flask, redirect, request, session
7
- from flask_appbuilder import AppBuilder, SQLA
8
- from flask_appbuilder.actions import action
9
- from flask_appbuilder.charts.views import (
10
- ChartView,
11
- DirectByChartView,
12
- DirectChartView,
13
- GroupByChartView,
14
- TimeChartView,
15
- )
16
- from flask_appbuilder.models.generic import PSModel
17
- from flask_appbuilder.models.generic import PSSession
18
- from flask_appbuilder.models.generic.interface import GenericInterface
19
- from flask_appbuilder.models.group import aggregate_avg, aggregate_count, aggregate_sum
20
- from flask_appbuilder.models.sqla.filters import FilterEqual, FilterStartsWith
21
- from flask_appbuilder.models.sqla.interface import SQLAInterface
22
- from flask_appbuilder.views import CompactCRUDMixin, MasterDetailView, ModelView
23
- from flask_wtf import CSRFProtect
24
- import jinja2
25
-
26
- from .base import FABTestCase
27
- from .const import (
28
- MODEL1_DATA_SIZE,
29
- PASSWORD_ADMIN,
30
- PASSWORD_READONLY,
31
- USERNAME_ADMIN,
32
- USERNAME_READONLY,
33
- )
34
- from .sqla.models import (
35
- insert_model1,
36
- insert_model2,
37
- insert_model3,
38
- insert_model_with_enums,
39
- Model1,
40
- Model2,
41
- Model3,
42
- ModelWithEnums,
43
- TmpEnum,
44
- )
45
-
46
-
47
- logging.basicConfig(format="%(asctime)s:%(levelname)s:%(name)s:%(message)s")
48
- logging.getLogger().setLevel(logging.DEBUG)
49
-
50
-
51
- """
52
- Constant english display string from framework
53
- """
54
- DEFAULT_INDEX_STRING = "Welcome"
55
- INVALID_LOGIN_STRING = "Invalid login"
56
- ACCESS_IS_DENIED = "Access is Denied"
57
- UNIQUE_VALIDATION_STRING = "Already exists"
58
- NOTNULL_VALIDATION_STRING = "This field is required"
59
-
60
- log = logging.getLogger(__name__)
61
-
62
-
63
- class MVCBabelTestCase(FABTestCase):
64
- def test_babel_empty_languages(self):
65
- """
66
- MVC: Test babel empty languages
67
- """
68
- app = Flask(__name__)
69
- app.config.from_object("flask_appbuilder.tests.config_api")
70
- app.config["LANGUAGES"] = {}
71
- db = SQLA(app)
72
- AppBuilder(app, db.session)
73
-
74
- client = app.test_client()
75
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
76
- rv = client.get("/users/list/")
77
- self.assertEqual(rv.status_code, 200)
78
-
79
- data = rv.data.decode("utf-8")
80
- self.assertNotIn('class="f16', data)
81
-
82
- def test_babel_languages(self):
83
- """
84
- MVC: Test babel languages
85
- """
86
- app = Flask(__name__)
87
- app.config.from_object("flask_appbuilder.tests.config_api")
88
- app.config["LANGUAGES"] = {
89
- "en": {"flag": "gb", "name": "English"},
90
- "pt": {"flag": "pt", "name": "Portuguese"},
91
- }
92
- db = SQLA(app)
93
- AppBuilder(app, db.session)
94
-
95
- client = app.test_client()
96
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
97
- rv = client.get("/users/list/")
98
- self.assertEqual(rv.status_code, 200)
99
- data = rv.data.decode("utf-8")
100
- self.assertIn('href="/lang/pt"', data)
101
-
102
- # Test babel language switch endpoint
103
- rv = client.get("/lang/pt")
104
- self.assertEqual(rv.status_code, 302)
105
-
106
-
107
- class BaseMVCTestCase(FABTestCase):
108
- def setUp(self):
109
- self.app = Flask(__name__)
110
- self.app.jinja_env.undefined = jinja2.StrictUndefined
111
- self.app.config.from_object("flask_appbuilder.tests.config_api")
112
- logging.basicConfig(level=logging.ERROR)
113
-
114
- self.db = SQLA(self.app)
115
- self.appbuilder = AppBuilder(self.app, self.db.session)
116
-
117
- @property
118
- def registered_endpoints(self) -> Set:
119
- return {item.endpoint for item in self.app.url_map.iter_rules()}
120
-
121
- def get_registered_view_endpoints(self, view_name) -> Set:
122
- return {
123
- item.endpoint
124
- for item in self.app.url_map.iter_rules()
125
- if item.endpoint.split(".")[0] == view_name
126
- }
127
-
128
-
129
- class ListFilterTestCase(BaseMVCTestCase):
130
- def test_list_filter_in_valid_object(self):
131
- """
132
- MVC: Test Filter with related object not found
133
- """
134
- with self.app.test_client() as c:
135
- self.browser_login(c, USERNAME_ADMIN, PASSWORD_ADMIN)
136
-
137
- # Roles doesn't exists
138
- rv = c.get("/users/list/?_flt_0_roles=-1")
139
- self.assertEqual(rv.status_code, 200)
140
-
141
- def test_list_filter_unknow_column(self):
142
- """
143
- MVC: Test Filter with unknown field
144
- """
145
- with self.app.test_client() as c:
146
- self.browser_login(c, USERNAME_ADMIN, PASSWORD_ADMIN)
147
- # UNKNOWN_COLUMN is not a valid column
148
- rv = c.get("/users/list/?_flt_0_UNKNOWN_COLUMN=-1")
149
- self.assertEqual(rv.status_code, 200)
150
-
151
- def test_list_filter_invalid_value_format(self):
152
- """
153
- MVC: Test Filter with invalid value of date filter
154
- """
155
- with self.app.test_client() as c:
156
- self.browser_login(c, USERNAME_ADMIN, PASSWORD_ADMIN)
157
-
158
- # Greater than wrong value
159
- rv = c.get("/users/list/?_flt_1_created_on=wrongvalue")
160
- self.assertEqual(rv.status_code, 200)
161
-
162
- # Smaller than wrong value
163
- rv = c.get("/users/list/?_flt_2_created_on=wrongvalue")
164
- self.assertEqual(rv.status_code, 200)
165
-
166
-
167
- class MVCCSRFTestCase(BaseMVCTestCase):
168
- def setUp(self):
169
-
170
- self.app = Flask(__name__)
171
- self.app.config.from_object("flask_appbuilder.tests.config_api")
172
- self.app.config["WTF_CSRF_ENABLED"] = True
173
-
174
- self.csrf = CSRFProtect(self.app)
175
- self.db = SQLA(self.app)
176
- self.appbuilder = AppBuilder(self.app, self.db.session)
177
-
178
- class Model2View(ModelView):
179
- datamodel = SQLAInterface(Model1)
180
-
181
- self.appbuilder.add_view(Model2View, "Model2", category="Model2")
182
-
183
- def test_a_csrf_delete_not_allowed(self):
184
- """
185
- MVC: Test GET delete with CSRF is not allowed
186
- """
187
- client = self.app.test_client()
188
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
189
-
190
- model = (
191
- self.appbuilder.get_session.query(Model2)
192
- .filter_by(field_string="test0")
193
- .one_or_none()
194
- )
195
- pk = model.id
196
- rv = client.get(f"/model2view/delete/{pk}")
197
-
198
- self.assertEqual(rv.status_code, 302)
199
- model = (
200
- self.appbuilder.get_session.query(Model2)
201
- .filter_by(field_string="test0")
202
- .one_or_none()
203
- )
204
- self.assertIsNotNone(model)
205
-
206
- def test_a_csrf_delete_protected(self):
207
- """
208
- MVC: Test POST delete with CSRF
209
- """
210
- client = self.app.test_client()
211
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
212
-
213
- model = (
214
- self.appbuilder.get_session.query(Model1)
215
- .filter_by(field_string="test0")
216
- .one_or_none()
217
- )
218
- pk = model.id
219
- rv = client.post(f"/model2view/delete/{pk}")
220
- # Missing CSRF token
221
- self.assertEqual(rv.status_code, 400)
222
-
223
-
224
- class MVCSwitchRouteMethodsTestCase(BaseMVCTestCase):
225
- """
226
- Specific to test ModelView's:
227
- - include_route_methods
228
- - exclude_route_methods
229
- - disable_api_route_methods
230
- """
231
-
232
- def setUp(self):
233
- super().setUp()
234
-
235
- class Model2IncludeView(ModelView):
236
- datamodel = SQLAInterface(Model2)
237
- include_route_methods = {"list", "show"}
238
-
239
- self.appbuilder.add_view(Model2IncludeView, "Model2IncludeView")
240
-
241
- class Model2ExcludeView(ModelView):
242
- datamodel = SQLAInterface(Model2)
243
- exclude_route_methods: Set = {
244
- "api",
245
- "api_read",
246
- "api_get",
247
- "api_create",
248
- "api_update",
249
- "api_delete",
250
- "api_column_add",
251
- "api_column_edit",
252
- "api_readvalues",
253
- }
254
-
255
- self.appbuilder.add_view(Model2ExcludeView, "Model2ExcludeView")
256
-
257
- class Model2IncludeExcludeView(ModelView):
258
- datamodel = SQLAInterface(Model2)
259
- include_route_methods: Set = {
260
- "api",
261
- "api_read",
262
- "api_get",
263
- "api_create",
264
- "api_update",
265
- "api_delete",
266
- "api_column_add",
267
- "api_column_edit",
268
- "api_readvalues",
269
- }
270
- exclude_route_methods: Set = {
271
- "api_create",
272
- "api_update",
273
- "api_delete",
274
- "api_column_add",
275
- "api_column_edit",
276
- "api_readvalues",
277
- }
278
-
279
- self.appbuilder.add_view_no_menu(
280
- Model2IncludeExcludeView, "Model2IncludeExcludeView"
281
- )
282
-
283
- class Model2DisableMVCApiView(ModelView):
284
- datamodel = SQLAInterface(Model2)
285
- disable_api_route_methods = True
286
-
287
- self.appbuilder.add_view(Model2DisableMVCApiView, "Model2DisableMVCApiView")
288
-
289
- def test_include_route_methods(self):
290
- """
291
- MVC: Include route methods
292
- """
293
- expected_endpoints = {"Model2IncludeView.list", "Model2IncludeView.show"}
294
- self.assertEqual(
295
- expected_endpoints, self.get_registered_view_endpoints("Model2IncludeView")
296
- )
297
- # Check that permissions do not exist
298
- unexpected_permissions = [
299
- ("can_add", "Model2IncludeView"),
300
- ("can_edit", "Model2IncludeView"),
301
- ("can_delete", "Model2IncludeView"),
302
- ("can_download", "Model2IncludeView"),
303
- ]
304
- for unexpected_permission in unexpected_permissions:
305
- pvm = self.appbuilder.sm.find_permission_view_menu(*unexpected_permission)
306
- self.assertIsNone(pvm)
307
- # Login and list with admin, check that mutation links are not rendered
308
- client = self.app.test_client()
309
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
310
- rv = client.get("/model2includeview/list/")
311
- self.assertEqual(rv.status_code, 200)
312
- data = rv.data.decode("utf-8")
313
- self.assertNotIn("/model2includeview/add", data)
314
- self.assertNotIn("/model2includeview/edit", data)
315
- self.assertNotIn("/model2includeview/delete", data)
316
-
317
- def test_exclude_route_methods(self):
318
- """
319
- MVC: Exclude route methods
320
- """
321
- expected_endpoints: Set = {
322
- "Model2ExcludeView.list",
323
- "Model2ExcludeView.show",
324
- "Model2ExcludeView.edit",
325
- "Model2ExcludeView.download",
326
- "Model2ExcludeView.action",
327
- "Model2ExcludeView.delete",
328
- "Model2ExcludeView.add",
329
- "Model2ExcludeView.action_post",
330
- }
331
- self.assertEqual(
332
- expected_endpoints, self.get_registered_view_endpoints("Model2ExcludeView")
333
- )
334
-
335
- def test_include_exclude_route_methods(self):
336
- """
337
- MVC: Include and Exclude route methods
338
- """
339
-
340
- expected_endpoints: Set = {
341
- "Model2IncludeExcludeView.api",
342
- "Model2IncludeExcludeView.api_read",
343
- "Model2IncludeExcludeView.api_get",
344
- }
345
- self.assertEqual(
346
- expected_endpoints,
347
- self.get_registered_view_endpoints("Model2IncludeExcludeView"),
348
- )
349
- # Check that permissions do not exist
350
- unexpected_permissions = [
351
- ("can_add", "Model2IncludeExcludeView"),
352
- ("can_edit", "Model2IncludeExcludeView"),
353
- ("can_delete", "Model2IncludeExcludeView"),
354
- ("can_download", "Model2IncludeExcludeView"),
355
- ]
356
- for unexpected_permission in unexpected_permissions:
357
- pvm = self.appbuilder.sm.find_permission_view_menu(*unexpected_permission)
358
- self.assertIsNone(pvm)
359
-
360
- def test_disable_mvc_api_methods(self):
361
- """
362
- MVC: Disable MVC API
363
- """
364
- expected_endpoints: Set = {
365
- "Model2DisableMVCApiView.list",
366
- "Model2DisableMVCApiView.show",
367
- "Model2DisableMVCApiView.add",
368
- "Model2DisableMVCApiView.edit",
369
- "Model2DisableMVCApiView.delete",
370
- "Model2DisableMVCApiView.action",
371
- "Model2DisableMVCApiView.download",
372
- "Model2DisableMVCApiView.action_post",
373
- }
374
- self.assertEqual(
375
- expected_endpoints,
376
- self.get_registered_view_endpoints("Model2DisableMVCApiView"),
377
- )
378
-
379
-
380
- class MVCTestCase(BaseMVCTestCase):
381
- def setUp(self):
382
- super().setUp()
383
- sess = PSSession()
384
-
385
- class PSView(ModelView):
386
- datamodel = GenericInterface(PSModel, sess)
387
- base_permissions = ["can_list", "can_show"]
388
- list_columns = ["UID", "C", "CMD", "TIME"]
389
- search_columns = ["UID", "C", "CMD"]
390
-
391
- class Model2View(ModelView):
392
- datamodel = SQLAInterface(Model2)
393
- list_columns = [
394
- "field_integer",
395
- "field_float",
396
- "field_string",
397
- "field_method",
398
- "group.field_string",
399
- ]
400
- edit_form_query_rel_fields = {
401
- "group": [["field_string", FilterEqual, "test1"]]
402
- }
403
- add_form_query_rel_fields = {
404
- "group": [["field_string", FilterEqual, "test0"]]
405
- }
406
-
407
- order_columns = ["field_string", "group.field_string"]
408
-
409
- class Model22View(ModelView):
410
- datamodel = SQLAInterface(Model2)
411
- list_columns = [
412
- "field_integer",
413
- "field_float",
414
- "field_string",
415
- "field_method",
416
- "group.field_string",
417
- ]
418
- add_exclude_columns = ["excluded_string"]
419
- edit_exclude_columns = ["excluded_string"]
420
- show_exclude_columns = ["excluded_string"]
421
-
422
- class Model1View(ModelView):
423
- datamodel = SQLAInterface(Model1)
424
- related_views = [Model2View]
425
- list_columns = ["field_string", "field_integer"]
426
-
427
- class Model3View(ModelView):
428
- datamodel = SQLAInterface(Model3)
429
- list_columns = ["pk1", "pk2", "field_string"]
430
- add_columns = ["pk1", "pk2", "field_string"]
431
- edit_columns = ["pk1", "pk2", "field_string"]
432
-
433
- @action(
434
- "muldelete", "Delete", "Delete all Really?", "fa-rocket", single=False
435
- )
436
- def muldelete(self, items):
437
- self.datamodel.delete_all(items)
438
- self.update_redirect()
439
- return redirect(self.get_redirect())
440
-
441
- class Model1CompactView(CompactCRUDMixin, ModelView):
442
- datamodel = SQLAInterface(Model1)
443
-
444
- class Model3CompactView(CompactCRUDMixin, ModelView):
445
- datamodel = SQLAInterface(Model3)
446
-
447
- class Model1ViewWithRedirects(ModelView):
448
- datamodel = SQLAInterface(Model1)
449
-
450
- def post_add_redirect(self):
451
- return redirect("/")
452
-
453
- def post_edit_redirect(self):
454
- return redirect("/")
455
-
456
- def post_delete_redirect(self):
457
- return redirect("/")
458
-
459
- class Model1Filtered1View(ModelView):
460
- datamodel = SQLAInterface(Model1)
461
- base_filters = [["field_string", FilterStartsWith, "test2"]]
462
-
463
- class Model1MasterView(MasterDetailView):
464
- datamodel = SQLAInterface(Model1)
465
- related_views = [Model2View]
466
-
467
- class Model1Filtered2View(ModelView):
468
- datamodel = SQLAInterface(Model1)
469
- base_filters = [["field_integer", FilterEqual, 0]]
470
-
471
- class Model2ChartView(ChartView):
472
- datamodel = SQLAInterface(Model2)
473
- chart_title = "Test Model1 Chart"
474
- group_by_columns = ["field_string"]
475
-
476
- class Model2GroupByChartView(GroupByChartView):
477
- datamodel = SQLAInterface(Model2)
478
- chart_title = "Test Model1 Chart"
479
-
480
- definitions = [
481
- {
482
- "group": "field_string",
483
- "series": [
484
- (
485
- aggregate_sum,
486
- "field_integer",
487
- aggregate_avg,
488
- "field_integer",
489
- aggregate_count,
490
- "field_integer",
491
- )
492
- ],
493
- }
494
- ]
495
-
496
- class Model2DirectByChartView(DirectByChartView):
497
- datamodel = SQLAInterface(Model2)
498
- chart_title = "Test Model1 Chart"
499
- list_title = ""
500
-
501
- definitions = [
502
- {"group": "field_string", "series": ["field_integer", "field_float"]}
503
- ]
504
-
505
- class Model2TimeChartView(TimeChartView):
506
- datamodel = SQLAInterface(Model2)
507
- chart_title = "Test Model1 Chart"
508
- group_by_columns = ["field_date"]
509
-
510
- class Model2DirectChartView(DirectChartView):
511
- datamodel = SQLAInterface(Model2)
512
- chart_title = "Test Model1 Chart"
513
- direct_columns = {"stat1": ("group", "field_integer")}
514
-
515
- class Model1MasterChartView(MasterDetailView):
516
- datamodel = SQLAInterface(Model1)
517
- related_views = [Model2DirectByChartView]
518
-
519
- class Model1FormattedView(ModelView):
520
- datamodel = SQLAInterface(Model1)
521
- list_columns = ["field_string"]
522
- show_columns = ["field_string"]
523
- formatters_columns = {"field_string": lambda x: "FORMATTED_STRING"}
524
-
525
- class ModelWithEnumsView(ModelView):
526
- datamodel = SQLAInterface(ModelWithEnums)
527
-
528
- self.appbuilder.add_view(Model1View, "Model1", category="Model1")
529
- self.appbuilder.add_view(
530
- Model1ViewWithRedirects, "Model1ViewWithRedirects", category="Model1"
531
- )
532
- self.appbuilder.add_view(Model1CompactView, "Model1Compact", category="Model1")
533
- self.appbuilder.add_view(Model1MasterView, "Model1Master", category="Model1")
534
- self.appbuilder.add_view(
535
- Model1MasterChartView, "Model1MasterChart", category="Model1"
536
- )
537
- self.appbuilder.add_view(
538
- Model1Filtered1View, "Model1Filtered1", category="Model1"
539
- )
540
- self.appbuilder.add_view(
541
- Model1Filtered2View, "Model1Filtered2", category="Model1"
542
- )
543
- self.appbuilder.add_view(
544
- Model1FormattedView, "Model1FormattedView", category="Model1FormattedView"
545
- )
546
-
547
- self.appbuilder.add_view(Model2View, "Model2")
548
- self.appbuilder.add_view(Model22View, "Model22")
549
- self.appbuilder.add_view(Model2View, "Model2 Add", href="/model2view/add")
550
- self.appbuilder.add_view(Model2ChartView, "Model2 Chart")
551
- self.appbuilder.add_view(Model2GroupByChartView, "Model2 Group By Chart")
552
- self.appbuilder.add_view(Model2DirectByChartView, "Model2 Direct By Chart")
553
- self.appbuilder.add_view(Model2TimeChartView, "Model2 Time Chart")
554
- self.appbuilder.add_view(Model2DirectChartView, "Model2 Direct Chart")
555
-
556
- self.appbuilder.add_view(Model3View, "Model3")
557
- self.appbuilder.add_view(Model3CompactView, "Model3Compact")
558
-
559
- self.appbuilder.add_view(ModelWithEnumsView, "ModelWithEnums")
560
-
561
- self.appbuilder.add_view(PSView, "Generic DS PS View", category="PSView")
562
- role_admin = self.appbuilder.sm.find_role("Admin")
563
- self.appbuilder.sm.add_user(
564
- "admin", "admin", "user", "admin@fab.org", role_admin, "general"
565
- )
566
- role_read_only = self.appbuilder.sm.find_role("ReadOnly")
567
- self.appbuilder.sm.add_user(
568
- USERNAME_READONLY,
569
- "readonly",
570
- "readonly",
571
- "readonly@fab.org",
572
- role_read_only,
573
- PASSWORD_READONLY,
574
- )
575
-
576
- def tearDown(self):
577
- self.appbuilder = None
578
- self.app = None
579
- self.db = None
580
- log.debug("TEAR DOWN")
581
-
582
- def test_fab_views(self):
583
- """
584
- Test views creation and registration
585
- """
586
- self.assertEqual(len(self.appbuilder.baseviews), 36)
587
-
588
- def test_back(self):
589
- """
590
- Test Back functionality
591
- """
592
- with self.app.test_client() as c:
593
- self.browser_login(c, USERNAME_ADMIN, PASSWORD_ADMIN)
594
- c.get("/model1view/list/?_flt_0_field_string=f")
595
- c.get("/model2view/list/")
596
- c.get("/back", follow_redirects=True)
597
- assert request.args["_flt_0_field_string"] == "f"
598
- assert "/model1view/list/" == request.path
599
-
600
- def test_model_creation(self):
601
- """
602
- Test Model creation
603
- """
604
- from sqlalchemy.engine.reflection import Inspector
605
-
606
- engine = self.db.session.get_bind(mapper=None, clause=None)
607
- inspector = Inspector.from_engine(engine)
608
- # Check if tables exist
609
- self.assertIn("model1", inspector.get_table_names())
610
- self.assertIn("model2", inspector.get_table_names())
611
- self.assertIn("model3", inspector.get_table_names())
612
- self.assertIn("model_with_enums", inspector.get_table_names())
613
-
614
- def test_index(self):
615
- """
616
- Test initial access and index message
617
- """
618
- client = self.app.test_client()
619
-
620
- # Check for Welcome Message
621
- rv = client.get("/")
622
- data = rv.data.decode("utf-8")
623
- self.assertIn(DEFAULT_INDEX_STRING, data)
624
-
625
- def test_sec_login(self):
626
- """
627
- Test Security Login, Logout, invalid login, invalid access
628
- """
629
- client = self.app.test_client()
630
-
631
- # Try to List and Redirect to Login
632
- rv = client.get("/model1view/list/")
633
- self.assertEqual(rv.status_code, 302)
634
- rv = client.get("/model2view/list/")
635
- self.assertEqual(rv.status_code, 302)
636
-
637
- # Login and list with admin
638
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
639
- rv = client.get("/model1view/list/")
640
- self.assertEqual(rv.status_code, 200)
641
- rv = client.get("/model2view/list/")
642
- self.assertEqual(rv.status_code, 200)
643
-
644
- # Logout and and try to list
645
- self.browser_logout(client)
646
- rv = client.get("/model1view/list/")
647
- self.assertEqual(rv.status_code, 302)
648
- rv = client.get("/model2view/list/")
649
- self.assertEqual(rv.status_code, 302)
650
-
651
- # Invalid Login
652
- rv = self.browser_login(client, USERNAME_ADMIN, "wrong_password")
653
- data = rv.data.decode("utf-8")
654
- self.assertIn(INVALID_LOGIN_STRING, data)
655
-
656
- def test_auth_builtin_roles(self):
657
- """
658
- Test Security builtin roles readonly
659
- """
660
- client = self.app.test_client()
661
- self.browser_login(client, USERNAME_READONLY, PASSWORD_READONLY)
662
- # Test authorized GET
663
- rv = client.get("/model1view/list/")
664
- self.assertEqual(rv.status_code, 200)
665
- # Test authorized SHOW
666
- rv = client.get("/model1view/show/1")
667
- self.assertEqual(rv.status_code, 200)
668
- # Test unauthorized EDIT
669
- rv = client.get("/model1view/edit/1")
670
- self.assertEqual(rv.status_code, 302)
671
- # Test unauthorized DELETE
672
- rv = client.get("/model1view/delete/1")
673
- self.assertEqual(rv.status_code, 302)
674
-
675
- def test_sec_reset_password(self):
676
- """
677
- Test Security reset password
678
- """
679
- client = self.app.test_client()
680
-
681
- # Try Reset My password
682
- rv = client.get("/users/action/resetmypassword/1", follow_redirects=True)
683
- # Werkzeug update to 0.15.X sends this action to wrong redirect
684
- # Old test was:
685
- # data = rv.data.decode("utf-8")
686
- # ok_(ACCESS_IS_DENIED in data)
687
- self.assertEqual(rv.status_code, 404)
688
-
689
- # Reset My password
690
- rv = self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
691
- rv = client.get("/users/action/resetmypassword/1", follow_redirects=True)
692
- data = rv.data.decode("utf-8")
693
- self.assertIn("Reset Password Form", data)
694
- rv = client.post(
695
- "/resetmypassword/form",
696
- data=dict(password="password", conf_password="password"),
697
- follow_redirects=True,
698
- )
699
- self.assertEqual(rv.status_code, 200)
700
- self.browser_logout(client)
701
- self.browser_login(client, USERNAME_ADMIN, "password")
702
- rv = client.post(
703
- "/resetmypassword/form",
704
- data=dict(password=PASSWORD_ADMIN, conf_password=PASSWORD_ADMIN),
705
- follow_redirects=True,
706
- )
707
- self.assertEqual(rv.status_code, 200)
708
-
709
- # Reset Password Admin
710
- rv = client.get("/users/action/resetpasswords/1", follow_redirects=True)
711
- data = rv.data.decode("utf-8")
712
- self.assertIn("Reset Password Form", data)
713
- rv = client.post(
714
- "/resetmypassword/form",
715
- data=dict(password=PASSWORD_ADMIN, conf_password=PASSWORD_ADMIN),
716
- follow_redirects=True,
717
- )
718
- self.assertEqual(rv.status_code, 200)
719
-
720
- def test_generic_interface(self):
721
- """
722
- Test Generic Interface for generic-alter datasource
723
- """
724
- client = self.app.test_client()
725
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
726
- rv = client.get("/psview/list", follow_redirects=True)
727
- self.assertEqual(rv.status_code, 200)
728
-
729
- def test_model_crud_add(self):
730
- """
731
- Test ModelView CRUD Add
732
- """
733
- client = self.app.test_client()
734
- rv = self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
735
-
736
- field_string = f"test{MODEL1_DATA_SIZE+1}"
737
- rv = client.post(
738
- "/model1view/add",
739
- data=dict(
740
- field_string=field_string,
741
- field_integer=f"{MODEL1_DATA_SIZE}",
742
- field_float=f"{float(MODEL1_DATA_SIZE)}",
743
- field_date="2014-01-01",
744
- ),
745
- follow_redirects=True,
746
- )
747
- self.assertEqual(rv.status_code, 200)
748
-
749
- model = (
750
- self.db.session.query(Model1)
751
- .filter_by(field_string=field_string)
752
- .one_or_none()
753
- )
754
- self.assertEqual(model.field_string, field_string)
755
- self.assertEqual(model.field_integer, MODEL1_DATA_SIZE)
756
-
757
- # Revert data changes
758
- self.appbuilder.get_session.delete(model)
759
- self.appbuilder.get_session.commit()
760
-
761
- def test_model_crud_edit(self):
762
- """
763
- Test ModelView CRUD Edit
764
- """
765
- client = self.app.test_client()
766
- rv = self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
767
-
768
- model = (
769
- self.appbuilder.get_session.query(Model1)
770
- .filter_by(field_string="test0")
771
- .one_or_none()
772
- )
773
- pk = model.id
774
- rv = client.post(
775
- f"/model1view/edit/{pk}",
776
- data=dict(field_string="test_edit", field_integer="200"),
777
- follow_redirects=True,
778
- )
779
- self.assertEqual(rv.status_code, 200)
780
-
781
- model = self.db.session.query(Model1).filter_by(id=pk).one_or_none()
782
- self.assertEqual(model.field_string, "test_edit")
783
- self.assertEqual(model.field_integer, 200)
784
-
785
- # Revert data changes
786
- insert_model1(self.appbuilder.get_session, i=pk - 1)
787
-
788
- def test_model_crud_delete(self):
789
- """
790
- Test Model CRUD delete
791
- """
792
- client = self.app.test_client()
793
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
794
-
795
- model = (
796
- self.appbuilder.get_session.query(Model2)
797
- .filter_by(field_string="test0")
798
- .one_or_none()
799
- )
800
- pk = model.id
801
- rv = client.get(f"/model2view/delete/{pk}", follow_redirects=True)
802
-
803
- self.assertEqual(rv.status_code, 200)
804
- model = self.db.session.query(Model2).get(pk)
805
- self.assertEqual(model, None)
806
-
807
- # Revert data changes
808
- insert_model2(self.appbuilder.get_session, i=0)
809
-
810
- def test_model_delete_integrity(self):
811
- """
812
- Test Model CRUD delete integrity validation
813
- """
814
- client = self.app.test_client()
815
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
816
- model1 = (
817
- self.appbuilder.get_session.query(Model1)
818
- .filter_by(field_string="test1")
819
- .one_or_none()
820
- )
821
- pk = model1.id
822
- rv = client.get(f"/model1view/delete/{pk}", follow_redirects=True)
823
-
824
- self.assertEqual(rv.status_code, 200)
825
- model = self.db.session.query(Model1).filter_by(id=pk).one_or_none()
826
- self.assertNotEqual(model, None)
827
-
828
- def test_model_crud_composite_pk(self):
829
- """
830
- MVC CRUD generic-alter datasource where model has composite
831
- primary keys
832
- """
833
- try:
834
- from urllib import quote
835
- except Exception:
836
- from urllib.parse import quote
837
-
838
- client = self.app.test_client()
839
- rv = self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
840
-
841
- rv = client.post(
842
- "/model3view/add",
843
- data=dict(pk1="1", pk2=datetime.datetime(2017, 1, 1), field_string="foo2"),
844
- follow_redirects=True,
845
- )
846
-
847
- self.assertEqual(rv.status_code, 200)
848
- model = (
849
- self.appbuilder.get_session.query(Model3).filter_by(pk1="1").one_or_none()
850
- )
851
- self.assertEqual(model.pk1, 1)
852
- self.assertEqual(model.pk2, datetime.datetime(2017, 1, 1))
853
- self.assertEqual(model.field_string, "foo2")
854
-
855
- pk = '[1, {"_type": "datetime", "value": "2017-01-01T00:00:00.000000"}]'
856
- rv = client.get(f"/model3view/show/{quote(pk)}", follow_redirects=True)
857
- self.assertEqual(rv.status_code, 200)
858
-
859
- rv = client.post(
860
- "/model3view/edit/" + quote(pk),
861
- data=dict(pk1="2", pk2="2017-02-02 00:00:00", field_string="bar"),
862
- follow_redirects=True,
863
- )
864
- self.assertEqual(rv.status_code, 200)
865
-
866
- model = (
867
- self.appbuilder.get_session.query(Model3)
868
- .filter_by(pk1="2", pk2="2017-02-02 00:00:00")
869
- .one_or_none()
870
- )
871
- self.assertEqual(model.pk1, 2)
872
- self.assertEqual(model.pk2, datetime.datetime(2017, 2, 2))
873
- self.assertEqual(model.field_string, "bar")
874
-
875
- pk = '[2, {"_type": "datetime", "value": "2017-02-02T00:00:00.000000"}]'
876
- rv = client.get("/model3view/delete/" + quote(pk), follow_redirects=True)
877
- self.assertEqual(rv.status_code, 200)
878
- model = self.db.session.query(Model3).filter_by(pk1=2).one_or_none()
879
- self.assertEqual(model, None)
880
-
881
- # Add it back, then delete via muldelete
882
- self.appbuilder.get_session.add(
883
- Model3(pk1=1, pk2=datetime.datetime(2017, 1, 1), field_string="baz")
884
- )
885
- self.appbuilder.get_session.commit()
886
- rv = client.post(
887
- "/model3view/action_post",
888
- data=dict(
889
- action="muldelete",
890
- rowid=[
891
- json.dumps(
892
- [
893
- "1",
894
- {
895
- "_type": "datetime",
896
- "value": "2017-01-01T00:00:00.000000",
897
- },
898
- ]
899
- )
900
- ],
901
- ),
902
- follow_redirects=True,
903
- )
904
- self.assertEqual(rv.status_code, 200)
905
- model = self.db.session.query(Model3).filter_by(pk1=1).one_or_none()
906
- self.assertEqual(model, None)
907
-
908
- def test_model_crud_add_with_enum(self):
909
- """
910
- Test Model add for Model with Enum Columns
911
- """
912
- client = self.app.test_client()
913
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
914
-
915
- data = {"enum1": "e3", "enum2": "e3"}
916
- rv = client.post("/modelwithenumsview/add", data=data, follow_redirects=True)
917
- self.assertEqual(rv.status_code, 200)
918
-
919
- model = (
920
- self.appbuilder.get_session.query(ModelWithEnums)
921
- .filter_by(enum1="e3")
922
- .one_or_none()
923
- )
924
- self.assertIsNotNone(model)
925
- self.assertEqual(model.enum2, TmpEnum.e3)
926
-
927
- # Revert data changes
928
- model = (
929
- self.appbuilder.get_session.query(ModelWithEnums)
930
- .filter_by(enum1="e3")
931
- .one_or_none()
932
- )
933
- self.appbuilder.get_session.delete(model)
934
- self.appbuilder.get_session.commit()
935
-
936
- def test_model_crud_edit_with_enum(self):
937
- """
938
- Test Model edit for Model with Enum Columns
939
- """
940
- client = self.app.test_client()
941
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
942
-
943
- data = {"enum1": "e3", "enum2": "e3"}
944
- pk = 1
945
- rv = client.post(
946
- f"/modelwithenumsview/edit/{pk}", data=data, follow_redirects=True
947
- )
948
- self.assertEqual(rv.status_code, 200)
949
-
950
- model = (
951
- self.appbuilder.get_session.query(ModelWithEnums)
952
- .filter_by(enum1="e3")
953
- .one_or_none()
954
- )
955
- self.assertIsNotNone(model)
956
- self.assertEqual(model.enum2, TmpEnum.e3)
957
-
958
- # Revert data changes
959
- insert_model_with_enums(self.appbuilder.get_session, i=pk - 1)
960
-
961
- def test_formatted_cols(self):
962
- """
963
- Test ModelView's formatters_columns
964
- """
965
- client = self.app.test_client()
966
- rv = self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
967
- rv = client.get("/model1formattedview/list/")
968
- self.assertEqual(rv.status_code, 200)
969
- data = rv.data.decode("utf-8")
970
- self.assertIn("FORMATTED_STRING", data)
971
- rv = client.get("/model1formattedview/show/1")
972
- self.assertEqual(rv.status_code, 200)
973
- data = rv.data.decode("utf-8")
974
- self.assertIn("FORMATTED_STRING", data)
975
-
976
- def test_modelview_add_redirects(self):
977
- """
978
- Test ModelView redirects after add
979
- """
980
- client = self.app.test_client()
981
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
982
-
983
- rv = client.post(
984
- "/model1viewwithredirects/add", data=dict(field_string="test_redirect")
985
- )
986
-
987
- self.assertEqual(rv.status_code, 302)
988
- self.assertEqual("http://localhost/", rv.headers["Location"])
989
-
990
- # Revert data changes
991
- model1 = (
992
- self.appbuilder.get_session.query(Model1)
993
- .filter_by(field_string="test_redirect")
994
- .one_or_none()
995
- )
996
- self.appbuilder.get_session.delete(model1)
997
- self.appbuilder.get_session.commit()
998
-
999
- def test_modelview_edit_redirects(self):
1000
- """
1001
- Test ModelView redirects after edit
1002
- """
1003
- client = self.app.test_client()
1004
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1005
- model_id = (
1006
- self.db.session.query(Model1)
1007
- .filter_by(field_string="test0")
1008
- .one_or_none()
1009
- .id
1010
- )
1011
- rv = client.post(
1012
- f"/model1viewwithredirects/edit/{model_id}",
1013
- data=dict(field_string="test_redirect", field_integer="200"),
1014
- )
1015
- self.assertEqual(rv.status_code, 302)
1016
- self.assertEqual("http://localhost/", rv.headers["Location"])
1017
-
1018
- # Revert data changes
1019
- insert_model1(self.appbuilder.get_session, i=model_id - 1)
1020
-
1021
- def test_modelview_delete_redirects(self):
1022
- """
1023
- Test ModelView redirects after delete
1024
- """
1025
- client = self.app.test_client()
1026
- rv = self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1027
- model_id = (
1028
- self.db.session.query(Model1).filter_by(field_string="test0").first().id
1029
- )
1030
- rv = client.get(f"/model1viewwithredirects/delete/{model_id}")
1031
- self.assertEqual(rv.status_code, 302)
1032
- self.assertEqual("http://localhost/", rv.headers["Location"])
1033
- # Revert data changes
1034
- insert_model1(self.appbuilder.get_session, i=model_id - 1)
1035
-
1036
- def test_add_excluded_cols(self):
1037
- """
1038
- Test add_exclude_columns
1039
- """
1040
- client = self.app.test_client()
1041
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1042
- rv = client.get("/model22view/add")
1043
- self.assertEqual(rv.status_code, 200)
1044
- data = rv.data.decode("utf-8")
1045
- self.assertIn("field_string", data)
1046
- self.assertIn("field_integer", data)
1047
- self.assertIn("field_float", data)
1048
- self.assertIn("field_date", data)
1049
- self.assertNotIn("excluded_string", data)
1050
-
1051
- def test_edit_excluded_cols(self):
1052
- """
1053
- Test edit_exclude_columns
1054
- """
1055
- client = self.app.test_client()
1056
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1057
-
1058
- model = (
1059
- self.appbuilder.get_session.query(Model2)
1060
- .filter_by(field_string="test0")
1061
- .one_or_none()
1062
- )
1063
- rv = client.get(f"/model22view/edit/{model.id}")
1064
- self.assertEqual(rv.status_code, 200)
1065
- data = rv.data.decode("utf-8")
1066
- self.assertIn("field_string", data)
1067
- self.assertIn("field_integer", data)
1068
- self.assertIn("field_float", data)
1069
- self.assertIn("field_date", data)
1070
- self.assertNotIn("excluded_string", data)
1071
-
1072
- def test_show_excluded_cols(self):
1073
- """
1074
- Test show_exclude_columns
1075
- """
1076
- client = self.app.test_client()
1077
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1078
- model = (
1079
- self.appbuilder.get_session.query(Model2)
1080
- .filter_by(field_string="test0")
1081
- .one_or_none()
1082
- )
1083
- rv = client.get(f"/model22view/show/{model.id}")
1084
- self.assertEqual(rv.status_code, 200)
1085
- data = rv.data.decode("utf-8")
1086
- self.assertIn("Field String", data)
1087
- self.assertIn("Field Integer", data)
1088
- self.assertIn("Field Float", data)
1089
- self.assertIn("Field Date", data)
1090
- self.assertNotIn("Excluded String", data)
1091
-
1092
- def test_query_rel_fields(self):
1093
- """
1094
- Test add and edit form related fields filter
1095
- """
1096
- client = self.app.test_client()
1097
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1098
-
1099
- # Base filter string starts with
1100
- rv = client.get("/model2view/add")
1101
- data = rv.data.decode("utf-8")
1102
- self.assertIn("test0", data)
1103
- self.assertNotIn("test1", data)
1104
-
1105
- model2 = (
1106
- self.appbuilder.get_session.query(Model2)
1107
- .filter_by(field_string="test0")
1108
- .one_or_none()
1109
- )
1110
- # Base filter string starts with
1111
- rv = client.get(f"/model2view/edit/{model2.id}")
1112
- data = rv.data.decode("utf-8")
1113
- self.assertIn("test1", data)
1114
-
1115
- def test_model_list_order(self):
1116
- """
1117
- Test Model order on lists
1118
- """
1119
- client = self.app.test_client()
1120
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1121
-
1122
- rv = client.get(
1123
- "/model1view/list?_oc_Model1View=field_string&_od_Model1View=asc",
1124
- follow_redirects=True,
1125
- )
1126
- self.assertEqual(rv.status_code, 200)
1127
- data = rv.data.decode("utf-8")
1128
- self.assertIn("test0", data)
1129
- rv = client.get(
1130
- "/model1view/list?_oc_Model1View=field_string&_od_Model1View=desc",
1131
- follow_redirects=True,
1132
- )
1133
- self.assertEqual(rv.status_code, 200)
1134
- data = rv.data.decode("utf-8")
1135
- self.assertIn(f"test{MODEL1_DATA_SIZE-1}", data)
1136
-
1137
- def test_model_list_order_related(self):
1138
- """
1139
- Test Model order related field on lists
1140
- """
1141
- client = self.app.test_client()
1142
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1143
-
1144
- rv = client.get(
1145
- "/model2view/list?_oc_Model2View=group.field_string&_od_Model2View=asc",
1146
- follow_redirects=True,
1147
- )
1148
- self.assertEqual(rv.status_code, 200)
1149
-
1150
- def test_model_add_unique_validation(self):
1151
- """
1152
- Test Model add unique field validation
1153
- """
1154
- client = self.app.test_client()
1155
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1156
-
1157
- # Test unique constraint
1158
- rv = client.post(
1159
- "/model1view/add",
1160
- data=dict(field_string="test1", field_integer="2"),
1161
- follow_redirects=True,
1162
- )
1163
- self.assertEqual(rv.status_code, 200)
1164
- data = rv.data.decode("utf-8")
1165
- self.assertIn(UNIQUE_VALIDATION_STRING, data)
1166
-
1167
- model = self.db.session.query(Model1).all()
1168
- self.assertEqual(len(model), MODEL1_DATA_SIZE)
1169
-
1170
- def test_model_add_required_validation(self):
1171
- """
1172
- Test Model add required fields validation
1173
- """
1174
- client = self.app.test_client()
1175
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1176
-
1177
- # Test field required
1178
- rv = client.post(
1179
- "/model1view/add",
1180
- data=dict(field_string="", field_integer="1"),
1181
- follow_redirects=True,
1182
- )
1183
- self.assertEqual(rv.status_code, 200)
1184
- data = rv.data.decode("utf-8")
1185
- self.assertIn(NOTNULL_VALIDATION_STRING, data)
1186
-
1187
- model = self.db.session.query(Model1).all()
1188
- self.assertEqual(len(model), MODEL1_DATA_SIZE)
1189
-
1190
- def test_model_edit_unique_validation(self):
1191
- """
1192
- Test Model edit unique validation
1193
- """
1194
- client = self.app.test_client()
1195
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1196
-
1197
- rv = client.post(
1198
- "/model1view/edit/1",
1199
- data=dict(field_string="test2", field_integer="2"),
1200
- follow_redirects=True,
1201
- )
1202
- self.assertEqual(rv.status_code, 200)
1203
- data = rv.data.decode("utf-8")
1204
- self.assertIn(UNIQUE_VALIDATION_STRING, data)
1205
-
1206
- def test_model_edit_required_validation(self):
1207
- """
1208
- Test Model edit required validation
1209
- """
1210
- client = self.app.test_client()
1211
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1212
-
1213
- rv = client.post(
1214
- "/model1view/edit/1",
1215
- data=dict(field_string="", field_integer="2"),
1216
- follow_redirects=True,
1217
- )
1218
- self.assertEqual(rv.status_code, 200)
1219
- data = rv.data.decode("utf-8")
1220
- self.assertIn(NOTNULL_VALIDATION_STRING, data)
1221
-
1222
- def test_model_base_filter(self):
1223
- """
1224
- Test Model base filtered views
1225
- """
1226
- client = self.app.test_client()
1227
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1228
- models = self.db.session.query(Model1).all()
1229
- self.assertEqual(len(models), MODEL1_DATA_SIZE)
1230
-
1231
- # Base filter string starts with
1232
- rv = client.get("/model1filtered1view/list/")
1233
- data = rv.data.decode("utf-8")
1234
- self.assertIn("test2", data)
1235
- self.assertNotIn("test0", data)
1236
-
1237
- # Base filter integer equals
1238
- rv = client.get("/model1filtered2view/list/")
1239
- data = rv.data.decode("utf-8")
1240
- self.assertIn("test0", data)
1241
- self.assertNotIn("test1", data)
1242
-
1243
- def test_model_list_method_field(self):
1244
- """
1245
- Tests a model's field has a method
1246
- """
1247
- client = self.app.test_client()
1248
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1249
- rv = client.get("/model2view/list/")
1250
- self.assertEqual(rv.status_code, 200)
1251
- data = rv.data.decode("utf-8")
1252
- self.assertIn("_field_method", data)
1253
-
1254
- def test_compactCRUDMixin(self):
1255
- """
1256
- Test CompactCRUD Mixin view with composite keys
1257
- """
1258
- client = self.app.test_client()
1259
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1260
- rv = client.get("/model1compactview/list/")
1261
- self.assertEqual(rv.status_code, 200)
1262
-
1263
- # test with composite pk
1264
- try:
1265
- from urllib import quote
1266
- except Exception:
1267
- from urllib.parse import quote
1268
-
1269
- pk = '[3, {"_type": "datetime", "value": "2017-03-03T00:00:00"}]'
1270
- rv = client.post(
1271
- "/model3compactview/edit/" + quote(pk),
1272
- data=dict(field_string="bar"),
1273
- follow_redirects=True,
1274
- )
1275
- self.assertEqual(rv.status_code, 200)
1276
- model = self.db.session.query(Model3).first()
1277
- self.assertEqual(model.field_string, "bar")
1278
-
1279
- rv = client.get("/model3compactview/delete/" + quote(pk), follow_redirects=True)
1280
- self.assertEqual(rv.status_code, 200)
1281
- model = self.db.session.query(Model3).first()
1282
- self.assertEqual(model, None)
1283
-
1284
- # Revert data changes
1285
- insert_model3(self.appbuilder.get_session)
1286
-
1287
- def test_edit_add_form_action_prefix_for_compactCRUDMixin(self):
1288
- """
1289
- Test form_action in add, form_action in edit (CompactCRUDMixin)
1290
- """
1291
- client = self.app.test_client()
1292
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1293
-
1294
- # Make sure we have something to edit.
1295
- prefix = "/some-prefix"
1296
- base_url = "http://localhost" + prefix
1297
- session_form_action_key = "Model1CompactView__session_form_action"
1298
-
1299
- with client as c:
1300
- expected_form_action = prefix + "/model1compactview/add/?"
1301
-
1302
- c.get("/model1compactview/add/", base_url=base_url)
1303
- self.assertEqual(session[session_form_action_key], expected_form_action)
1304
-
1305
- expected_form_action = prefix + "/model1compactview/edit/1?"
1306
- c.get("/model1compactview/edit/1", base_url=base_url)
1307
-
1308
- self.assertEqual(session[session_form_action_key], expected_form_action)
1309
-
1310
- def test_charts_view(self):
1311
- """
1312
- Test Various Chart views
1313
- """
1314
- client = self.app.test_client()
1315
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1316
- # self.insert_data2()
1317
- rv = client.get("/model2chartview/chart/")
1318
- self.assertEqual(rv.status_code, 200)
1319
- rv = client.get("/model2groupbychartview/chart/")
1320
- self.assertEqual(rv.status_code, 200)
1321
- rv = client.get("/model2directbychartview/chart/")
1322
- self.assertEqual(rv.status_code, 200)
1323
- # TODO: fix this
1324
- rv = client.get("/model2timechartview/chart/")
1325
- self.assertEqual(rv.status_code, 200)
1326
-
1327
- def test_master_detail_view(self):
1328
- """
1329
- Test Master detail view
1330
- """
1331
- client = self.app.test_client()
1332
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1333
- # self.insert_data2()
1334
- rv = client.get("/model1masterview/list/")
1335
- self.assertEqual(rv.status_code, 200)
1336
- rv = client.get("/model1masterview/list/1")
1337
- self.assertEqual(rv.status_code, 200)
1338
-
1339
- rv = client.get("/model1masterchartview/list/")
1340
- self.assertEqual(rv.status_code, 200)
1341
- rv = client.get("/model1masterchartview/list/1")
1342
- self.assertEqual(rv.status_code, 200)
1343
-
1344
- def test_api_read(self):
1345
- """
1346
- Testing the api/read endpoint
1347
- """
1348
- client = self.app.test_client()
1349
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1350
- rv = client.get("/model1formattedview/api/read")
1351
- self.assertEqual(rv.status_code, 200)
1352
- data = json.loads(rv.data.decode("utf-8"))
1353
- self.assertIn("result", data)
1354
- self.assertIn("pks", data)
1355
- assert len(data.get("result")) > 10
1356
-
1357
- def test_api_create(self):
1358
- """
1359
- Testing the api/create endpoint
1360
- """
1361
- client = self.app.test_client()
1362
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1363
- rv = client.post(
1364
- "/model1view/api/create",
1365
- data=dict(field_string="zzz"),
1366
- follow_redirects=True,
1367
- )
1368
- self.assertEqual(rv.status_code, 200)
1369
- model1 = (
1370
- self.db.session.query(Model1).filter_by(field_string="zzz").one_or_none()
1371
- )
1372
- self.assertIsNotNone(model1)
1373
-
1374
- # Revert data changes
1375
- self.appbuilder.get_session.delete(model1)
1376
- self.appbuilder.get_session.commit()
1377
-
1378
- def test_api_update(self):
1379
- """
1380
- Validate that the api update endpoint updates [only] the fields in
1381
- POST data
1382
- """
1383
- client = self.app.test_client()
1384
- self.browser_login(client, USERNAME_ADMIN, PASSWORD_ADMIN)
1385
- item = self.db.session.query(Model1).filter_by(id=1).one()
1386
- field_integer_before = item.field_integer
1387
- rv = client.put(
1388
- "/model1view/api/update/1",
1389
- data=dict(field_string="zzz"),
1390
- follow_redirects=True,
1391
- )
1392
- self.assertEqual(rv.status_code, 200)
1393
- item = self.db.session.query(Model1).filter_by(id=1).one()
1394
- self.assertEqual(item.field_string, "zzz")
1395
- self.assertEqual(item.field_integer, field_integer_before)
1396
-
1397
- # Revert data changes
1398
- insert_model1(self.appbuilder.get_session, i=0)
1399
-
1400
- def test_class_method_permission_override(self):
1401
- """
1402
- MVC: Test class method permission name override
1403
- """
1404
- from flask_appbuilder import ModelView
1405
- from flask_appbuilder.models.sqla.interface import SQLAInterface
1406
-
1407
- class Model1PermOverride(ModelView):
1408
- datamodel = SQLAInterface(Model1)
1409
- class_permission_name = "view"
1410
- method_permission_name = {
1411
- "list": "access",
1412
- "show": "access",
1413
- "edit": "access",
1414
- "add": "access",
1415
- "delete": "access",
1416
- "download": "access",
1417
- "api_readvalues": "access",
1418
- "api_column_edit": "access",
1419
- "api_column_add": "access",
1420
- "api_delete": "access",
1421
- "api_update": "access",
1422
- "api_create": "access",
1423
- "api_get": "access",
1424
- "api_read": "access",
1425
- "api": "access",
1426
- }
1427
-
1428
- self.model1permoverride = Model1PermOverride
1429
- self.appbuilder.add_view_no_menu(Model1PermOverride)
1430
-
1431
- role = self.appbuilder.sm.add_role("Test")
1432
- pvm = self.appbuilder.sm.find_permission_view_menu("can_access", "view")
1433
- self.appbuilder.sm.add_permission_role(role, pvm)
1434
- self.appbuilder.sm.add_user(
1435
- "test", "test", "user", "test@fab.org", role, "test"
1436
- )
1437
-
1438
- client = self.app.test_client()
1439
-
1440
- self.browser_login(client, "test", "test")
1441
- rv = client.get("/model1permoverride/list/")
1442
- self.assertEqual(rv.status_code, 200)
1443
- rv = client.post(
1444
- "/model1permoverride/add",
1445
- data=dict(
1446
- field_string="test1",
1447
- field_integer="1",
1448
- field_float="0.12",
1449
- field_date="2014-01-01",
1450
- ),
1451
- follow_redirects=True,
1452
- )
1453
- self.assertEqual(rv.status_code, 200)
1454
-
1455
- model = (
1456
- self.db.session.query(Model1).filter_by(field_string="test1").one_or_none()
1457
- )
1458
- self.assertEqual(model.field_string, "test1")
1459
- self.assertEqual(model.field_integer, 1)
1460
-
1461
- def test_method_permission_override(self):
1462
- """
1463
- MVC: Test method permission name override
1464
- """
1465
- from flask_appbuilder import ModelView
1466
- from flask_appbuilder.models.sqla.interface import SQLAInterface
1467
-
1468
- class Model1PermOverride(ModelView):
1469
- datamodel = SQLAInterface(Model1)
1470
- method_permission_name = {
1471
- "list": "read",
1472
- "show": "read",
1473
- "edit": "write",
1474
- "add": "write",
1475
- "delete": "write",
1476
- "download": "read",
1477
- "api_readvalues": "read",
1478
- "api_column_edit": "write",
1479
- "api_column_add": "write",
1480
- "api_delete": "write",
1481
- "api_update": "write",
1482
- "api_create": "write",
1483
- "api_get": "read",
1484
- "api_read": "read",
1485
- "api": "read",
1486
- }
1487
-
1488
- self.model1permoverride = Model1PermOverride
1489
- self.appbuilder.add_view_no_menu(Model1PermOverride)
1490
-
1491
- role = self.appbuilder.sm.add_role("Test")
1492
- pvm_read = self.appbuilder.sm.find_permission_view_menu(
1493
- "can_read", "Model1PermOverride"
1494
- )
1495
- pvm_write = self.appbuilder.sm.find_permission_view_menu(
1496
- "can_write", "Model1PermOverride"
1497
- )
1498
- self.appbuilder.sm.add_permission_role(role, pvm_read)
1499
- self.appbuilder.sm.add_permission_role(role, pvm_write)
1500
-
1501
- self.appbuilder.sm.add_user(
1502
- "test", "test", "user", "test@fab.org", role, "test"
1503
- )
1504
-
1505
- client = self.app.test_client()
1506
- self.browser_login(client, "test", "test")
1507
-
1508
- rv = client.post(
1509
- "/model1permoverride/add",
1510
- data=dict(
1511
- field_string=f"test{MODEL1_DATA_SIZE+1}",
1512
- field_integer="1",
1513
- field_float="0.12",
1514
- field_date="2014-01-01",
1515
- ),
1516
- follow_redirects=True,
1517
- )
1518
- self.assertEqual(rv.status_code, 200)
1519
- model1 = (
1520
- self.appbuilder.get_session.query(Model1)
1521
- .filter_by(field_string=f"test{MODEL1_DATA_SIZE+1}")
1522
- .one_or_none()
1523
- )
1524
- self.assertIsNotNone(model1)
1525
-
1526
- # Revert data changes
1527
- self.appbuilder.get_session.delete(model1)
1528
- self.appbuilder.get_session.commit()
1529
-
1530
- # Verify write links are on the UI
1531
- rv = client.get("/model1permoverride/list/")
1532
- self.assertEqual(rv.status_code, 200)
1533
- data = rv.data.decode("utf-8")
1534
- self.assertIn("/model1permoverride/delete/1", data)
1535
- self.assertIn("/model1permoverride/add", data)
1536
- self.assertIn("/model1permoverride/edit/1", data)
1537
- self.assertIn("/model1permoverride/show/1", data)
1538
-
1539
- # Delete write permission from Test Role
1540
- role = self.appbuilder.sm.find_role("Test")
1541
- pvm_write = self.appbuilder.sm.find_permission_view_menu(
1542
- "can_write", "Model1PermOverride"
1543
- )
1544
- self.appbuilder.sm.del_permission_role(role, pvm_write)
1545
-
1546
- # Unauthorized delete
1547
- model1 = (
1548
- self.appbuilder.get_session.query(Model1)
1549
- .filter_by(field_string="test1")
1550
- .one_or_none()
1551
- )
1552
- pk = model1.id
1553
- rv = client.get(f"/model1permoverride/delete/{pk}")
1554
- self.assertEqual(rv.status_code, 302)
1555
- model = self.db.session.query(Model1).filter_by(id=pk).one_or_none()
1556
- self.assertEqual(model.field_string, "test1")
1557
-
1558
- # Verify write links are gone from UI
1559
- rv = client.get("/model1permoverride/list/")
1560
- self.assertEqual(rv.status_code, 200)
1561
- data = rv.data.decode("utf-8")
1562
- self.assertNotIn("/model1permoverride/delete/1", data)
1563
- self.assertNotIn("/model1permoverride/add/", data)
1564
- self.assertNotIn("/model1permoverride/edit/1", data)
1565
- self.assertIn("/model1permoverride/show/1", data)
1566
-
1567
- # Revert data changes
1568
- self.appbuilder.get_session.delete(self.appbuilder.sm.find_role("Test"))
1569
- self.appbuilder.get_session.commit()
1570
-
1571
- def test_action_permission_override(self):
1572
- """
1573
- MVC: Test action permission name override
1574
- """
1575
- from flask_appbuilder import action, ModelView
1576
- from flask_appbuilder.models.sqla.interface import SQLAInterface
1577
-
1578
- class Model1PermOverride(ModelView):
1579
- datamodel = SQLAInterface(Model1)
1580
- method_permission_name = {
1581
- "list": "read",
1582
- "show": "read",
1583
- "edit": "write",
1584
- "add": "write",
1585
- "delete": "write",
1586
- "download": "read",
1587
- "api_readvalues": "read",
1588
- "api_column_edit": "write",
1589
- "api_column_add": "write",
1590
- "api_delete": "write",
1591
- "api_update": "write",
1592
- "api_create": "write",
1593
- "api_get": "read",
1594
- "api_read": "read",
1595
- "api": "read",
1596
- "action_one": "write",
1597
- }
1598
-
1599
- @action("action1", "Action1", "", "fa-lock", multiple=True)
1600
- def action_one(self, item):
1601
- return "ACTION ONE"
1602
-
1603
- self.model1permoverride = Model1PermOverride
1604
- self.appbuilder.add_view_no_menu(Model1PermOverride)
1605
-
1606
- # Add a user and login before enabling CSRF
1607
- role = self.appbuilder.sm.add_role("Test")
1608
- self.appbuilder.sm.add_user(
1609
- "test", "test", "user", "test@fab.org", role, "test"
1610
- )
1611
- pvm_read = self.appbuilder.sm.find_permission_view_menu(
1612
- "can_read", "Model1PermOverride"
1613
- )
1614
- pvm_write = self.appbuilder.sm.find_permission_view_menu(
1615
- "can_write", "Model1PermOverride"
1616
- )
1617
- self.appbuilder.sm.add_permission_role(role, pvm_read)
1618
- self.appbuilder.sm.add_permission_role(role, pvm_write)
1619
-
1620
- client = self.app.test_client()
1621
- self.browser_login(client, "test", "test")
1622
-
1623
- model1 = (
1624
- self.appbuilder.get_session.query(Model1)
1625
- .filter_by(field_string="test0")
1626
- .one_or_none()
1627
- )
1628
- pk = model1.id
1629
- rv = client.get(f"/model1permoverride/action/action1/{pk}")
1630
- self.assertEqual(rv.status_code, 200)
1631
-
1632
- # Delete write permission from Test Role
1633
- role = self.appbuilder.sm.find_role("Test")
1634
- pvm_write = self.appbuilder.sm.find_permission_view_menu(
1635
- "can_write", "Model1PermOverride"
1636
- )
1637
- self.appbuilder.sm.del_permission_role(role, pvm_write)
1638
-
1639
- rv = client.get("/model1permoverride/action/action1/1")
1640
- self.assertEqual(rv.status_code, 302)
1641
-
1642
- def test_permission_converge_compress(self):
1643
- """
1644
- MVC: Test permission name converge compress
1645
- """
1646
- from flask_appbuilder import ModelView
1647
- from flask_appbuilder.models.sqla.interface import SQLAInterface
1648
-
1649
- class Model1PermConverge(ModelView):
1650
- datamodel = SQLAInterface(Model1)
1651
- class_permission_name = "view2"
1652
- previous_class_permission_name = "Model1View"
1653
- method_permission_name = {
1654
- "list": "access",
1655
- "show": "access",
1656
- "edit": "access",
1657
- "add": "access",
1658
- "delete": "access",
1659
- "download": "access",
1660
- "api_readvalues": "access",
1661
- "api_column_edit": "access",
1662
- "api_column_add": "access",
1663
- "api_delete": "access",
1664
- "api_update": "access",
1665
- "api_create": "access",
1666
- "api_get": "access",
1667
- "api_read": "access",
1668
- "api": "access",
1669
- }
1670
-
1671
- self.appbuilder.add_view_no_menu(Model1PermConverge)
1672
- role = self.appbuilder.sm.add_role("Test")
1673
- pvm = self.appbuilder.sm.find_permission_view_menu("can_list", "Model1View")
1674
- self.appbuilder.sm.add_permission_role(role, pvm)
1675
- pvm = self.appbuilder.sm.find_permission_view_menu("can_add", "Model1View")
1676
- self.appbuilder.sm.add_permission_role(role, pvm)
1677
- role = self.appbuilder.sm.find_role("Test")
1678
- self.appbuilder.sm.add_user(
1679
- "test", "test", "user", "test@fab.org", role, "test"
1680
- )
1681
- # Remove previous class, Hack to test code change
1682
- for i, baseview in enumerate(self.appbuilder.baseviews):
1683
- if baseview.__class__.__name__ == "Model1View":
1684
- break
1685
- self.appbuilder.baseviews.pop(i)
1686
-
1687
- target_state_transitions = {
1688
- "add": {
1689
- ("Model1View", "can_edit"): {("view2", "can_access")},
1690
- ("Model1View", "can_add"): {("view2", "can_access")},
1691
- ("Model1View", "can_list"): {("view2", "can_access")},
1692
- ("Model1View", "can_download"): {("view2", "can_access")},
1693
- ("Model1View", "can_show"): {("view2", "can_access")},
1694
- ("Model1View", "can_delete"): {("view2", "can_access")},
1695
- },
1696
- "del_role_pvm": {
1697
- ("Model1View", "can_show"),
1698
- ("Model1View", "can_add"),
1699
- ("Model1View", "can_download"),
1700
- ("Model1View", "can_list"),
1701
- ("Model1View", "can_edit"),
1702
- ("Model1View", "can_delete"),
1703
- },
1704
- "del_views": {"Model1View"},
1705
- "del_perms": set(),
1706
- }
1707
- state_transitions = self.appbuilder.security_converge()
1708
- self.assertEqual(state_transitions, target_state_transitions)
1709
- role = self.appbuilder.sm.find_role("Test")
1710
- self.assertEqual(len(role.permissions), 1)