flask-appbuilder 3.2.1rc1__py3-none-any.whl → 5.0.2rc1__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.
- flask_appbuilder/__init__.py +2 -3
- flask_appbuilder/_compat.py +0 -1
- flask_appbuilder/actions.py +14 -14
- flask_appbuilder/api/__init__.py +741 -527
- flask_appbuilder/api/convert.py +104 -98
- flask_appbuilder/api/manager.py +14 -8
- flask_appbuilder/api/schemas.py +12 -1
- flask_appbuilder/babel/manager.py +12 -16
- flask_appbuilder/base.py +353 -280
- flask_appbuilder/basemanager.py +1 -1
- flask_appbuilder/baseviews.py +241 -164
- flask_appbuilder/charts/jsontools.py +10 -10
- flask_appbuilder/charts/views.py +56 -60
- flask_appbuilder/cli.py +115 -70
- flask_appbuilder/const.py +52 -52
- flask_appbuilder/exceptions.py +67 -5
- flask_appbuilder/fields.py +32 -23
- flask_appbuilder/fieldwidgets.py +34 -27
- flask_appbuilder/filemanager.py +33 -45
- flask_appbuilder/filters.py +11 -13
- flask_appbuilder/forms.py +31 -35
- flask_appbuilder/hooks.py +90 -0
- flask_appbuilder/menu.py +35 -10
- flask_appbuilder/models/base.py +47 -57
- flask_appbuilder/models/decorators.py +13 -13
- flask_appbuilder/models/filters.py +42 -38
- flask_appbuilder/models/generic/__init__.py +29 -29
- flask_appbuilder/models/generic/filters.py +11 -3
- flask_appbuilder/models/generic/interface.py +1 -3
- flask_appbuilder/models/group.py +37 -39
- flask_appbuilder/models/mixins.py +22 -18
- flask_appbuilder/models/sqla/__init__.py +19 -72
- flask_appbuilder/models/sqla/base.py +24 -0
- flask_appbuilder/models/sqla/base_legacy.py +132 -0
- flask_appbuilder/models/sqla/filters.py +132 -19
- flask_appbuilder/models/sqla/interface.py +390 -276
- flask_appbuilder/security/api.py +31 -35
- flask_appbuilder/security/decorators.py +181 -83
- flask_appbuilder/security/forms.py +20 -31
- flask_appbuilder/security/manager.py +715 -489
- flask_appbuilder/security/registerviews.py +29 -112
- flask_appbuilder/security/schemas.py +43 -0
- flask_appbuilder/security/sqla/apis/__init__.py +8 -0
- flask_appbuilder/security/sqla/apis/group/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/group/api.py +227 -0
- flask_appbuilder/security/sqla/apis/group/schema.py +73 -0
- flask_appbuilder/security/sqla/apis/permission/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/permission/api.py +19 -0
- flask_appbuilder/security/sqla/apis/permission_view_menu/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/permission_view_menu/api.py +16 -0
- flask_appbuilder/security/sqla/apis/role/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/role/api.py +306 -0
- flask_appbuilder/security/sqla/apis/role/schema.py +27 -0
- flask_appbuilder/security/sqla/apis/user/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/user/api.py +292 -0
- flask_appbuilder/security/sqla/apis/user/schema.py +97 -0
- flask_appbuilder/security/sqla/apis/user/validator.py +27 -0
- flask_appbuilder/security/sqla/apis/view_menu/__init__.py +1 -0
- flask_appbuilder/security/sqla/apis/view_menu/api.py +18 -0
- flask_appbuilder/security/sqla/manager.py +421 -203
- flask_appbuilder/security/sqla/models.py +192 -57
- flask_appbuilder/security/utils.py +9 -0
- flask_appbuilder/security/views.py +232 -229
- flask_appbuilder/static/.DS_Store +0 -0
- flask_appbuilder/static/appbuilder/css/ab.css +20 -12
- flask_appbuilder/static/appbuilder/css/bootstrap-datepicker/bootstrap-datepicker3.min.css +7 -0
- flask_appbuilder/static/appbuilder/css/bootstrap.min.css.map +1 -0
- flask_appbuilder/static/appbuilder/css/flags/flags16.css +249 -245
- flask_appbuilder/static/appbuilder/css/fontawesome/all.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/brands.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/fontawesome.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/regular.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/solid.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/svg-with-js.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v4-font-face.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v4-shims.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/fontawesome/v5-font-face.min.css +6 -0
- flask_appbuilder/static/appbuilder/css/images/flags16.png +0 -0
- flask_appbuilder/static/appbuilder/css/select2/select2-bootstrap.min.css +7 -0
- flask_appbuilder/static/appbuilder/css/select2/select2.min.css +1 -0
- flask_appbuilder/static/appbuilder/css/swagger/swagger-ui.css +3 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.woff2 +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.ttf +0 -0
- flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.woff2 +0 -0
- flask_appbuilder/static/appbuilder/js/ab.js +33 -23
- flask_appbuilder/static/appbuilder/js/ab_filters.js +91 -84
- flask_appbuilder/static/appbuilder/js/bootstrap-datepicker/bootstrap-datepicker.min.js +8 -0
- flask_appbuilder/static/appbuilder/js/jquery-latest.js +2 -2
- flask_appbuilder/static/appbuilder/js/select2/select2.min.js +2 -0
- flask_appbuilder/static/appbuilder/js/swagger-ui-bundle.js +3 -0
- flask_appbuilder/templates/appbuilder/baselib.html +9 -3
- flask_appbuilder/templates/appbuilder/general/lib.html +60 -34
- flask_appbuilder/templates/appbuilder/general/model/edit.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/edit_cascade.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/search.html +3 -2
- flask_appbuilder/templates/appbuilder/general/model/show.html +1 -1
- flask_appbuilder/templates/appbuilder/general/model/show_cascade.html +1 -1
- flask_appbuilder/templates/appbuilder/general/security/login_db.html +7 -7
- flask_appbuilder/templates/appbuilder/general/security/login_ldap.html +5 -5
- flask_appbuilder/templates/appbuilder/general/security/login_oauth.html +24 -49
- flask_appbuilder/templates/appbuilder/general/widgets/base_list.html +2 -1
- flask_appbuilder/templates/appbuilder/general/widgets/chart.html +4 -2
- flask_appbuilder/templates/appbuilder/general/widgets/direct_chart.html +4 -3
- flask_appbuilder/templates/appbuilder/general/widgets/multiple_chart.html +3 -2
- flask_appbuilder/templates/appbuilder/general/widgets/search.html +11 -10
- flask_appbuilder/templates/appbuilder/init.html +37 -43
- flask_appbuilder/templates/appbuilder/navbar_menu.html +1 -1
- flask_appbuilder/templates/appbuilder/navbar_right.html +2 -2
- flask_appbuilder/templates/appbuilder/swagger/swagger.html +22 -19
- flask_appbuilder/translations/de/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/de/LC_MESSAGES/messages.po +305 -161
- flask_appbuilder/translations/fa/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/fa/LC_MESSAGES/messages.po +802 -0
- flask_appbuilder/translations/fr/LC_MESSAGES/messages.po +461 -319
- flask_appbuilder/translations/pt_BR/LC_MESSAGES/messages.po +650 -650
- flask_appbuilder/translations/ru/LC_MESSAGES/messages.po +1 -1
- flask_appbuilder/translations/sl/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/sl/LC_MESSAGES/messages.po +690 -0
- flask_appbuilder/translations/tr/LC_MESSAGES/messages.mo +0 -0
- flask_appbuilder/translations/tr/LC_MESSAGES/messages.po +1015 -0
- flask_appbuilder/upload.py +20 -22
- flask_appbuilder/urltools.py +39 -19
- flask_appbuilder/utils/base.py +76 -0
- flask_appbuilder/utils/legacy.py +33 -0
- flask_appbuilder/utils/limit.py +20 -0
- flask_appbuilder/validators.py +73 -14
- flask_appbuilder/views.py +75 -424
- flask_appbuilder/widgets.py +50 -51
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/METADATA +36 -76
- flask_appbuilder-5.0.2rc1.dist-info/RECORD +240 -0
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/WHEEL +1 -1
- flask_appbuilder-5.0.2rc1.dist-info/entry_points.txt +2 -0
- Flask_AppBuilder-3.2.1rc1.dist-info/RECORD +0 -270
- Flask_AppBuilder-3.2.1rc1.dist-info/entry_points.txt +0 -6
- flask_appbuilder/console.py +0 -426
- flask_appbuilder/models/mongoengine/__init__.py +0 -0
- flask_appbuilder/models/mongoengine/fields.py +0 -65
- flask_appbuilder/models/mongoengine/filters.py +0 -145
- flask_appbuilder/models/mongoengine/interface.py +0 -328
- flask_appbuilder/security/mongoengine/__init__.py +0 -0
- flask_appbuilder/security/mongoengine/manager.py +0 -402
- flask_appbuilder/security/mongoengine/models.py +0 -120
- flask_appbuilder/static/appbuilder/css/font-awesome.min.css +0 -4
- flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.css +0 -9
- flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.js +0 -28
- flask_appbuilder/static/appbuilder/fonts/FontAwesome.otf +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.eot +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.svg +0 -2671
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.ttf +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff +0 -0
- flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff2 +0 -0
- flask_appbuilder/static/appbuilder/img/aol.png +0 -0
- flask_appbuilder/static/appbuilder/img/flags/flags16.png +0 -0
- flask_appbuilder/static/appbuilder/img/flickr.png +0 -0
- flask_appbuilder/static/appbuilder/img/google.png +0 -0
- flask_appbuilder/static/appbuilder/img/myopenid.png +0 -0
- flask_appbuilder/static/appbuilder/img/yahoo.png +0 -0
- flask_appbuilder/static/appbuilder/js/_google_charts.js +0 -39
- flask_appbuilder/static/appbuilder/js/html5shiv.js +0 -8
- flask_appbuilder/static/appbuilder/js/respond.min.js +0 -6
- flask_appbuilder/static/appbuilder/select2/select2-spinner.gif +0 -0
- flask_appbuilder/static/appbuilder/select2/select2.css +0 -1205
- flask_appbuilder/static/appbuilder/select2/select2.js +0 -23
- flask_appbuilder/static/appbuilder/select2/select2.png +0 -0
- flask_appbuilder/static/appbuilder/select2/select2x2.png +0 -0
- flask_appbuilder/templates/appbuilder/general/security/login_oid.html +0 -129
- flask_appbuilder/templates/appbuilder/general/security/resetpassword.html +0 -29
- flask_appbuilder/tests/__init__.py +0 -0
- flask_appbuilder/tests/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_auth_ldap.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_auth_oauth.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_ldapsearch.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/_test_oauth_registration_role.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/base.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/base.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/config_api.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/config_api.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/const.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/const.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_api.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_api.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_menu.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_menu.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mongoengine.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mvc.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_mvc.cpython-37.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-36.pyc +0 -0
- flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-37.pyc +0 -0
- flask_appbuilder/tests/_test_auth_ldap.py +0 -1045
- flask_appbuilder/tests/_test_auth_oauth.py +0 -419
- flask_appbuilder/tests/_test_ldapsearch.py +0 -135
- flask_appbuilder/tests/_test_oauth_registration_role.py +0 -59
- flask_appbuilder/tests/app.db +0 -0
- flask_appbuilder/tests/base.py +0 -90
- flask_appbuilder/tests/config_api.py +0 -21
- flask_appbuilder/tests/const.py +0 -9
- flask_appbuilder/tests/mongoengine/__init__.py +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/mongoengine/__pycache__/models.cpython-36.pyc +0 -0
- flask_appbuilder/tests/mongoengine/models.py +0 -41
- flask_appbuilder/tests/sqla/__init__.py +0 -0
- flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-37.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/models.cpython-36.pyc +0 -0
- flask_appbuilder/tests/sqla/__pycache__/models.cpython-37.pyc +0 -0
- flask_appbuilder/tests/sqla/models.py +0 -340
- flask_appbuilder/tests/test_0_fixture.py +0 -39
- flask_appbuilder/tests/test_api.py +0 -2790
- flask_appbuilder/tests/test_fab_cli.py +0 -72
- flask_appbuilder/tests/test_menu.py +0 -122
- flask_appbuilder/tests/test_mongoengine.py +0 -572
- flask_appbuilder/tests/test_mvc.py +0 -1710
- flask_appbuilder/tests/test_sqlalchemy.py +0 -24
- flask_appbuilder/translations/__pycache__/__init__.cpython-36.pyc +0 -0
- flask_appbuilder/translations/es/LC_MESSAGES/messages.po~ +0 -582
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/LICENSE +0 -0
- {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.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)
|