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
flask_appbuilder/api/convert.py
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
from typing import List, Optional, Type
|
|
1
|
+
from typing import Any, Callable, Dict, List, Optional, Type
|
|
2
2
|
|
|
3
|
+
from flask import current_app
|
|
3
4
|
from flask_appbuilder.models.sqla import Model
|
|
4
5
|
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
|
5
|
-
from marshmallow import fields
|
|
6
|
+
from marshmallow import fields, Schema
|
|
6
7
|
from marshmallow.fields import Field
|
|
7
|
-
from marshmallow_enum import EnumField
|
|
8
8
|
from marshmallow_sqlalchemy import field_for
|
|
9
9
|
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TreeNode:
|
|
13
|
-
def __init__(self,
|
|
14
|
-
self.
|
|
15
|
-
self.
|
|
13
|
+
def __init__(self, name: str) -> None:
|
|
14
|
+
self.name = name
|
|
15
|
+
self.children: List["TreeNode"] = []
|
|
16
16
|
|
|
17
|
-
def __repr__(self):
|
|
18
|
-
return f"{self.
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
return f"{self.name}.{str(self.children)}"
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class Tree:
|
|
@@ -23,26 +23,26 @@ class Tree:
|
|
|
23
23
|
Simplistic one level Tree
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
def __init__(self):
|
|
26
|
+
def __init__(self) -> None:
|
|
27
27
|
self.root = TreeNode("+")
|
|
28
28
|
|
|
29
|
-
def add(self,
|
|
30
|
-
node = TreeNode(
|
|
31
|
-
self.root.
|
|
29
|
+
def add(self, name: str) -> None:
|
|
30
|
+
node = TreeNode(name)
|
|
31
|
+
self.root.children.append(node)
|
|
32
32
|
|
|
33
|
-
def add_child(self, parent,
|
|
34
|
-
node = TreeNode(
|
|
35
|
-
for
|
|
36
|
-
if
|
|
37
|
-
|
|
33
|
+
def add_child(self, parent: str, name: str) -> None:
|
|
34
|
+
node = TreeNode(name)
|
|
35
|
+
for child in self.root.children:
|
|
36
|
+
if child.name == parent:
|
|
37
|
+
child.children.append(node)
|
|
38
38
|
return
|
|
39
39
|
root = TreeNode(parent)
|
|
40
|
-
self.root.
|
|
41
|
-
root.
|
|
40
|
+
self.root.children.append(root)
|
|
41
|
+
root.children.append(node)
|
|
42
42
|
|
|
43
|
-
def __repr__(self):
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
44
|
ret = ""
|
|
45
|
-
for node in self.root.
|
|
45
|
+
for node in self.root.children:
|
|
46
46
|
ret += str(node)
|
|
47
47
|
return ret
|
|
48
48
|
|
|
@@ -51,43 +51,62 @@ def columns2Tree(columns: List[str]) -> Tree:
|
|
|
51
51
|
tree = Tree()
|
|
52
52
|
for column in columns:
|
|
53
53
|
if "." in column:
|
|
54
|
-
|
|
54
|
+
parent, child = column.split(".")
|
|
55
|
+
tree.add_child(parent, child)
|
|
55
56
|
else:
|
|
56
57
|
tree.add(column)
|
|
57
58
|
return tree
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
class BaseModel2SchemaConverter(object):
|
|
61
|
-
def __init__(
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
datamodel: SQLAInterface,
|
|
65
|
+
validators_columns: Dict[str, Callable[[Any], Any]],
|
|
66
|
+
):
|
|
62
67
|
"""
|
|
63
68
|
:param datamodel: SQLAInterface
|
|
64
69
|
"""
|
|
65
70
|
self.datamodel = datamodel
|
|
66
71
|
self.validators_columns = validators_columns
|
|
67
72
|
|
|
68
|
-
def convert(
|
|
73
|
+
def convert(
|
|
74
|
+
self,
|
|
75
|
+
columns: List[str],
|
|
76
|
+
model: Optional[Type[Model]] = None,
|
|
77
|
+
nested: bool = True,
|
|
78
|
+
parent_schema_name: Optional[str] = None,
|
|
79
|
+
) -> SQLAlchemyAutoSchema:
|
|
69
80
|
pass
|
|
70
81
|
|
|
71
82
|
|
|
72
83
|
class Model2SchemaConverter(BaseModel2SchemaConverter):
|
|
73
84
|
"""
|
|
74
|
-
|
|
85
|
+
Class that converts Models to marshmallow Schemas
|
|
75
86
|
"""
|
|
76
87
|
|
|
77
|
-
def __init__(
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
datamodel: SQLAInterface,
|
|
91
|
+
validators_columns: Dict[str, Callable[[Any], Any]],
|
|
92
|
+
):
|
|
78
93
|
"""
|
|
79
94
|
:param datamodel: SQLAInterface
|
|
80
95
|
"""
|
|
81
96
|
super(Model2SchemaConverter, self).__init__(datamodel, validators_columns)
|
|
82
97
|
|
|
83
98
|
@staticmethod
|
|
84
|
-
def _debug_schema(schema):
|
|
99
|
+
def _debug_schema(schema: SQLAlchemyAutoSchema) -> None:
|
|
85
100
|
for k, v in schema._declared_fields.items():
|
|
86
101
|
print(k, v)
|
|
87
102
|
|
|
88
103
|
def _meta_schema_factory(
|
|
89
|
-
self,
|
|
90
|
-
|
|
104
|
+
self,
|
|
105
|
+
columns: List[str],
|
|
106
|
+
model: Optional[Type[Model]],
|
|
107
|
+
class_mixin: Type[Schema],
|
|
108
|
+
parent_schema_name: Optional[str] = None,
|
|
109
|
+
) -> Type[SQLAlchemyAutoSchema]:
|
|
91
110
|
"""
|
|
92
111
|
Creates ModelSchema marshmallow-sqlalchemy
|
|
93
112
|
|
|
@@ -100,45 +119,44 @@ class Model2SchemaConverter(BaseModel2SchemaConverter):
|
|
|
100
119
|
_parent_schema_name = parent_schema_name
|
|
101
120
|
if columns:
|
|
102
121
|
|
|
103
|
-
class MetaSchema(SQLAlchemyAutoSchema, class_mixin):
|
|
122
|
+
class MetaSchema(SQLAlchemyAutoSchema, class_mixin): # type: ignore
|
|
104
123
|
class Meta:
|
|
105
124
|
model = _model
|
|
106
125
|
fields = columns
|
|
107
126
|
load_instance = True
|
|
108
|
-
sqla_session =
|
|
127
|
+
sqla_session = current_app.appbuilder.session
|
|
109
128
|
# The parent_schema_name is useful to humanize nested schema names
|
|
110
129
|
# This name comes from ModelRestApi
|
|
111
130
|
parent_schema_name = _parent_schema_name
|
|
112
131
|
|
|
113
|
-
|
|
132
|
+
return MetaSchema
|
|
114
133
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
class MetaSchema(SQLAlchemyAutoSchema, class_mixin): # type: ignore
|
|
135
|
+
class Meta:
|
|
136
|
+
model = _model
|
|
137
|
+
load_instance = True
|
|
138
|
+
sqla_session = current_app.appbuilder.session
|
|
139
|
+
# The parent_schema_name is useful to humanize nested schema names
|
|
140
|
+
# This name comes from ModelRestApi
|
|
141
|
+
parent_schema_name = _parent_schema_name
|
|
123
142
|
|
|
124
143
|
return MetaSchema
|
|
125
144
|
|
|
126
|
-
def _column2enum(
|
|
127
|
-
|
|
128
|
-
datamodel
|
|
129
|
-
column
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if
|
|
137
|
-
|
|
145
|
+
def _column2enum(self, datamodel: SQLAInterface, column: TreeNode) -> Field:
|
|
146
|
+
required = not datamodel.is_nullable(column.name)
|
|
147
|
+
sqla_column = datamodel.list_columns[column.name]
|
|
148
|
+
# get SQLAlchemy column user info, we use it to get the marshmallow enum options
|
|
149
|
+
column_info = sqla_column.info
|
|
150
|
+
# TODO: Default should be False, but keeping this to True to keep compatibility
|
|
151
|
+
# Turn this to False in the next major release
|
|
152
|
+
by_value = column_info.get("marshmallow_by_value", True)
|
|
153
|
+
# Get the original enum class from SQLAlchemy Enum field
|
|
154
|
+
enum_class = sqla_column.type.enum_class
|
|
155
|
+
if not enum_class:
|
|
156
|
+
field = field_for(datamodel.obj, column.name)
|
|
138
157
|
else:
|
|
139
|
-
|
|
140
|
-
field =
|
|
141
|
-
field.unique = datamodel.is_unique(column.data)
|
|
158
|
+
field = fields.Enum(enum_class, required=required, by_value=by_value)
|
|
159
|
+
field.unique = datamodel.is_unique(column.name)
|
|
142
160
|
return field
|
|
143
161
|
|
|
144
162
|
def _column2relation(
|
|
@@ -147,40 +165,37 @@ class Model2SchemaConverter(BaseModel2SchemaConverter):
|
|
|
147
165
|
column: TreeNode,
|
|
148
166
|
nested: bool = False,
|
|
149
167
|
parent_schema_name: Optional[str] = None,
|
|
150
|
-
):
|
|
168
|
+
) -> Field:
|
|
151
169
|
if nested:
|
|
152
|
-
required = not datamodel.is_nullable(column.
|
|
153
|
-
nested_model = datamodel.get_related_model(column.
|
|
154
|
-
lst = [item.
|
|
170
|
+
required = not datamodel.is_nullable(column.name)
|
|
171
|
+
nested_model = datamodel.get_related_model(column.name)
|
|
172
|
+
lst = [item.name for item in column.children]
|
|
155
173
|
nested_schema = self.convert(
|
|
156
174
|
lst, nested_model, nested=False, parent_schema_name=parent_schema_name
|
|
157
175
|
)
|
|
158
|
-
if datamodel.is_relation_many_to_one(column.
|
|
176
|
+
if datamodel.is_relation_many_to_one(column.name):
|
|
159
177
|
many = False
|
|
160
|
-
elif datamodel.is_relation_many_to_many(column.
|
|
178
|
+
elif datamodel.is_relation_many_to_many(column.name):
|
|
161
179
|
many = True
|
|
162
180
|
required = False
|
|
163
|
-
elif datamodel.is_relation_one_to_many(column.
|
|
181
|
+
elif datamodel.is_relation_one_to_many(column.name):
|
|
164
182
|
many = True
|
|
165
183
|
else:
|
|
166
184
|
many = False
|
|
167
185
|
field = fields.Nested(nested_schema, many=many, required=required)
|
|
168
|
-
field.unique = datamodel.is_unique(column.
|
|
186
|
+
field.unique = datamodel.is_unique(column.name)
|
|
169
187
|
return field
|
|
170
188
|
# Handle bug on marshmallow-sqlalchemy
|
|
171
189
|
# https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/163
|
|
172
190
|
if datamodel.is_relation_many_to_many(
|
|
173
|
-
column.
|
|
174
|
-
) or datamodel.is_relation_one_to_many(column.
|
|
175
|
-
|
|
176
|
-
required = True
|
|
177
|
-
else:
|
|
178
|
-
required = False
|
|
191
|
+
column.name
|
|
192
|
+
) or datamodel.is_relation_one_to_many(column.name):
|
|
193
|
+
required = datamodel.get_info(column.name).get("required", False)
|
|
179
194
|
else:
|
|
180
|
-
required = not datamodel.is_nullable(column.
|
|
181
|
-
field = field_for(datamodel.obj, column.
|
|
195
|
+
required = not datamodel.is_nullable(column.name)
|
|
196
|
+
field = field_for(datamodel.obj, column.name)
|
|
182
197
|
field.required = required
|
|
183
|
-
field.unique = datamodel.is_unique(column.
|
|
198
|
+
field.unique = datamodel.is_unique(column.name)
|
|
184
199
|
return field
|
|
185
200
|
|
|
186
201
|
def _column2field(
|
|
@@ -188,7 +203,6 @@ class Model2SchemaConverter(BaseModel2SchemaConverter):
|
|
|
188
203
|
datamodel: SQLAInterface,
|
|
189
204
|
column: TreeNode,
|
|
190
205
|
nested: bool = True,
|
|
191
|
-
enum_dump_by_name: bool = False,
|
|
192
206
|
parent_schema_name: Optional[str] = None,
|
|
193
207
|
) -> Field:
|
|
194
208
|
"""
|
|
@@ -196,34 +210,31 @@ class Model2SchemaConverter(BaseModel2SchemaConverter):
|
|
|
196
210
|
:param datamodel: SQLAInterface
|
|
197
211
|
:param column: TreeNode column (childs are dotted columns)
|
|
198
212
|
:param nested: Boolean if will create nested fields
|
|
199
|
-
:param enum_dump_by_name:
|
|
200
213
|
:return: Schema.field
|
|
201
214
|
"""
|
|
202
215
|
# Handle relations
|
|
203
|
-
if datamodel.is_relation(column.
|
|
216
|
+
if datamodel.is_relation(column.name):
|
|
204
217
|
return self._column2relation(
|
|
205
218
|
datamodel, column, nested=nested, parent_schema_name=parent_schema_name
|
|
206
219
|
)
|
|
207
220
|
# Handle Enums
|
|
208
|
-
|
|
209
|
-
return self._column2enum(
|
|
210
|
-
datamodel, column, enum_dump_by_name=enum_dump_by_name
|
|
211
|
-
)
|
|
221
|
+
if datamodel.is_enum(column.name):
|
|
222
|
+
return self._column2enum(datamodel, column)
|
|
212
223
|
# is custom property method field?
|
|
213
|
-
if hasattr(getattr(datamodel.obj, column.
|
|
224
|
+
if hasattr(getattr(datamodel.obj, column.name), "fget"):
|
|
214
225
|
return fields.Raw(dump_only=True)
|
|
215
226
|
# its a model function
|
|
216
|
-
if hasattr(getattr(datamodel.obj, column.
|
|
217
|
-
return fields.Function(getattr(datamodel.obj, column.
|
|
227
|
+
if hasattr(getattr(datamodel.obj, column.name), "__call__"):
|
|
228
|
+
return fields.Function(getattr(datamodel.obj, column.name), dump_only=True)
|
|
218
229
|
# is a normal model field not a function?
|
|
219
|
-
if not hasattr(getattr(datamodel.obj, column.
|
|
220
|
-
field = field_for(datamodel.obj, column.
|
|
221
|
-
field.unique = datamodel.is_unique(column.
|
|
222
|
-
if column.
|
|
230
|
+
if not hasattr(getattr(datamodel.obj, column.name), "__call__"):
|
|
231
|
+
field = field_for(datamodel.obj, column.name)
|
|
232
|
+
field.unique = datamodel.is_unique(column.name)
|
|
233
|
+
if column.name in self.validators_columns:
|
|
223
234
|
if field.validate is None:
|
|
224
235
|
field.validate = []
|
|
225
|
-
field.validate.append(self.validators_columns[column.
|
|
226
|
-
field.validators.append(self.validators_columns[column.
|
|
236
|
+
field.validate.append(self.validators_columns[column.name])
|
|
237
|
+
field.validators.append(self.validators_columns[column.name])
|
|
227
238
|
return field
|
|
228
239
|
|
|
229
240
|
def convert(
|
|
@@ -231,9 +242,8 @@ class Model2SchemaConverter(BaseModel2SchemaConverter):
|
|
|
231
242
|
columns: List[str],
|
|
232
243
|
model: Optional[Type[Model]] = None,
|
|
233
244
|
nested: bool = True,
|
|
234
|
-
enum_dump_by_name: bool = False,
|
|
235
245
|
parent_schema_name: Optional[str] = None,
|
|
236
|
-
):
|
|
246
|
+
) -> SQLAlchemyAutoSchema:
|
|
237
247
|
"""
|
|
238
248
|
Creates a Marshmallow ModelSchema class
|
|
239
249
|
|
|
@@ -257,16 +267,12 @@ class Model2SchemaConverter(BaseModel2SchemaConverter):
|
|
|
257
267
|
|
|
258
268
|
_columns = list()
|
|
259
269
|
tree_columns = columns2Tree(columns)
|
|
260
|
-
for column in tree_columns.root.
|
|
270
|
+
for column in tree_columns.root.children:
|
|
261
271
|
# Get child model is column is dotted notation
|
|
262
|
-
ma_sqla_fields_override[column.
|
|
263
|
-
_datamodel,
|
|
264
|
-
column,
|
|
265
|
-
nested,
|
|
266
|
-
enum_dump_by_name,
|
|
267
|
-
parent_schema_name=parent_schema_name,
|
|
272
|
+
ma_sqla_fields_override[column.name] = self._column2field(
|
|
273
|
+
_datamodel, column, nested, parent_schema_name=parent_schema_name
|
|
268
274
|
)
|
|
269
|
-
_columns.append(column.
|
|
275
|
+
_columns.append(column.name)
|
|
270
276
|
for k, v in ma_sqla_fields_override.items():
|
|
271
277
|
setattr(SchemaMixin, k, v)
|
|
272
278
|
return self._meta_schema_factory(
|
flask_appbuilder/api/manager.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from apispec import APISpec
|
|
2
2
|
from apispec.ext.marshmallow import MarshmallowPlugin
|
|
3
3
|
from apispec.ext.marshmallow.common import resolve_schema_cls
|
|
4
|
-
from flask import current_app
|
|
4
|
+
from flask import current_app, request
|
|
5
5
|
from flask_appbuilder.api import BaseApi
|
|
6
6
|
from flask_appbuilder.api import expose, protect, safe
|
|
7
7
|
from flask_appbuilder.basemanager import BaseManager
|
|
@@ -30,7 +30,7 @@ class OpenApi(BaseApi):
|
|
|
30
30
|
@protect()
|
|
31
31
|
@safe
|
|
32
32
|
def get(self, version):
|
|
33
|
-
"""
|
|
33
|
+
"""Endpoint that renders an OpenApi spec for all views that belong
|
|
34
34
|
to a certain version
|
|
35
35
|
---
|
|
36
36
|
get:
|
|
@@ -66,18 +66,20 @@ class OpenApi(BaseApi):
|
|
|
66
66
|
|
|
67
67
|
@staticmethod
|
|
68
68
|
def _create_api_spec(version):
|
|
69
|
+
servers = current_app.config.get(
|
|
70
|
+
"FAB_OPENAPI_SERVERS", [{"url": request.host_url}]
|
|
71
|
+
)
|
|
69
72
|
return APISpec(
|
|
70
73
|
title=current_app.appbuilder.app_name,
|
|
71
74
|
version=version,
|
|
72
75
|
openapi_version="3.0.2",
|
|
73
76
|
info=dict(description=current_app.appbuilder.app_name),
|
|
74
77
|
plugins=[MarshmallowPlugin(schema_name_resolver=resolver)],
|
|
75
|
-
servers=
|
|
78
|
+
servers=servers,
|
|
76
79
|
)
|
|
77
80
|
|
|
78
81
|
|
|
79
82
|
class SwaggerView(BaseView):
|
|
80
|
-
|
|
81
83
|
route_base = "/swagger"
|
|
82
84
|
default_view = "ui"
|
|
83
85
|
openapi_uri = "/api/{}/_openapi"
|
|
@@ -85,16 +87,20 @@ class SwaggerView(BaseView):
|
|
|
85
87
|
@expose("/<version>")
|
|
86
88
|
@has_access
|
|
87
89
|
def show(self, version):
|
|
90
|
+
# Apply APPLICATION_ROOT config to the swagger URL
|
|
91
|
+
openapi_uri = request.script_root + self.openapi_uri.format(version)
|
|
88
92
|
return self.render_template(
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
current_app.config.get(
|
|
94
|
+
"FAB_API_SWAGGER_TEMPLATE", "appbuilder/swagger/swagger.html"
|
|
95
|
+
),
|
|
96
|
+
openapi_uri=openapi_uri,
|
|
91
97
|
)
|
|
92
98
|
|
|
93
99
|
|
|
94
100
|
class OpenApiManager(BaseManager):
|
|
95
101
|
def register_views(self):
|
|
96
|
-
if not
|
|
102
|
+
if not current_app.config.get("FAB_ADD_OPENAPI_VIEWS", True):
|
|
97
103
|
return
|
|
98
|
-
if
|
|
104
|
+
if current_app.config.get("FAB_API_SWAGGER_UI", False):
|
|
99
105
|
self.appbuilder.add_api(OpenApi)
|
|
100
106
|
self.appbuilder.add_view_no_menu(SwaggerView)
|
flask_appbuilder/api/schemas.py
CHANGED
|
@@ -18,6 +18,7 @@ from ..const import (
|
|
|
18
18
|
API_PERMISSIONS_RIS_KEY,
|
|
19
19
|
API_SELECT_COLUMNS_RIS_KEY,
|
|
20
20
|
API_SELECT_KEYS_RIS_KEY,
|
|
21
|
+
API_SELECT_SEL_COLUMNS_RIS_KEY,
|
|
21
22
|
API_SHOW_COLUMNS_RIS_KEY,
|
|
22
23
|
API_SHOW_TITLE_RIS_KEY,
|
|
23
24
|
)
|
|
@@ -70,6 +71,7 @@ get_list_schema = {
|
|
|
70
71
|
},
|
|
71
72
|
},
|
|
72
73
|
API_SELECT_COLUMNS_RIS_KEY: {"type": "array", "items": {"type": "string"}},
|
|
74
|
+
API_SELECT_SEL_COLUMNS_RIS_KEY: {"type": "array", "items": {"type": "string"}},
|
|
73
75
|
API_ORDER_COLUMN_RIS_KEY: {"type": "string"},
|
|
74
76
|
API_ORDER_DIRECTION_RIS_KEY: {"type": "string", "enum": ["asc", "desc"]},
|
|
75
77
|
API_PAGE_INDEX_RIS_KEY: {"type": "integer"},
|
|
@@ -86,7 +88,16 @@ get_list_schema = {
|
|
|
86
88
|
{"type": "number"},
|
|
87
89
|
{"type": "string"},
|
|
88
90
|
{"type": "boolean"},
|
|
89
|
-
{
|
|
91
|
+
{
|
|
92
|
+
"type": "array",
|
|
93
|
+
"items": {
|
|
94
|
+
"anyOf": [
|
|
95
|
+
{"type": "number"},
|
|
96
|
+
{"type": "string"},
|
|
97
|
+
{"type": "boolean"},
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
},
|
|
90
101
|
]
|
|
91
102
|
},
|
|
92
103
|
},
|
|
@@ -1,31 +1,28 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
from flask import has_request_context, request, session
|
|
3
|
+
from flask import current_app, has_request_context, request, session
|
|
4
|
+
from flask_appbuilder.babel.views import LocaleView
|
|
5
|
+
from flask_appbuilder.basemanager import BaseManager
|
|
4
6
|
from flask_babel import Babel
|
|
5
7
|
|
|
6
|
-
from .views import LocaleView
|
|
7
|
-
from ..basemanager import BaseManager
|
|
8
|
-
|
|
9
8
|
|
|
10
9
|
class BabelManager(BaseManager):
|
|
11
|
-
|
|
12
10
|
babel = None
|
|
13
11
|
locale_view = None
|
|
14
12
|
|
|
15
13
|
def __init__(self, appbuilder):
|
|
16
14
|
super(BabelManager, self).__init__(appbuilder)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
app.config["LANGUAGES"] = {"en": {"flag": "us", "name": "English"}}
|
|
15
|
+
current_app.config.setdefault("BABEL_DEFAULT_LOCALE", "en")
|
|
16
|
+
if not current_app.config.get("LANGUAGES"):
|
|
17
|
+
current_app.config["LANGUAGES"] = {"en": {"flag": "us", "name": "English"}}
|
|
21
18
|
appbuilder_parent_dir = os.path.join(
|
|
22
19
|
os.path.dirname(os.path.abspath(__file__)), os.pardir
|
|
23
20
|
)
|
|
24
21
|
appbuilder_translations_path = os.path.join(
|
|
25
22
|
appbuilder_parent_dir, "translations"
|
|
26
23
|
)
|
|
27
|
-
if "BABEL_TRANSLATION_DIRECTORIES" in
|
|
28
|
-
current_translation_directories =
|
|
24
|
+
if "BABEL_TRANSLATION_DIRECTORIES" in current_app.config:
|
|
25
|
+
current_translation_directories = current_app.config.get(
|
|
29
26
|
"BABEL_TRANSLATION_DIRECTORIES"
|
|
30
27
|
)
|
|
31
28
|
translations_path = (
|
|
@@ -33,9 +30,8 @@ class BabelManager(BaseManager):
|
|
|
33
30
|
)
|
|
34
31
|
else:
|
|
35
32
|
translations_path = appbuilder_translations_path + ";translations"
|
|
36
|
-
|
|
37
|
-
self.babel = Babel(
|
|
38
|
-
self.babel.locale_selector_func = self.get_locale
|
|
33
|
+
current_app.config["BABEL_TRANSLATION_DIRECTORIES"] = translations_path
|
|
34
|
+
self.babel = Babel(current_app, locale_selector=self.get_locale)
|
|
39
35
|
|
|
40
36
|
def register_views(self):
|
|
41
37
|
self.locale_view = LocaleView()
|
|
@@ -43,11 +39,11 @@ class BabelManager(BaseManager):
|
|
|
43
39
|
|
|
44
40
|
@property
|
|
45
41
|
def babel_default_locale(self):
|
|
46
|
-
return
|
|
42
|
+
return current_app.config["BABEL_DEFAULT_LOCALE"]
|
|
47
43
|
|
|
48
44
|
@property
|
|
49
45
|
def languages(self):
|
|
50
|
-
return
|
|
46
|
+
return current_app.config["LANGUAGES"]
|
|
51
47
|
|
|
52
48
|
def get_locale(self):
|
|
53
49
|
if has_request_context():
|