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/filemanager.py
CHANGED
|
@@ -4,18 +4,11 @@ import os.path as op
|
|
|
4
4
|
import re
|
|
5
5
|
import uuid
|
|
6
6
|
|
|
7
|
-
from flask
|
|
7
|
+
from flask import current_app
|
|
8
8
|
from werkzeug.datastructures import FileStorage
|
|
9
9
|
from werkzeug.utils import secure_filename
|
|
10
10
|
from wtforms import ValidationError
|
|
11
11
|
|
|
12
|
-
try:
|
|
13
|
-
from flask import _app_ctx_stack
|
|
14
|
-
except ImportError:
|
|
15
|
-
_app_ctx_stack = None
|
|
16
|
-
|
|
17
|
-
app_stack = _app_ctx_stack or _request_ctx_stack
|
|
18
|
-
|
|
19
12
|
log = logging.getLogger(__name__)
|
|
20
13
|
|
|
21
14
|
try:
|
|
@@ -35,19 +28,16 @@ class FileManager(object):
|
|
|
35
28
|
permission=0o755,
|
|
36
29
|
**kwargs
|
|
37
30
|
):
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if "UPLOAD_FOLDER" in ctx.app.config and not base_path:
|
|
42
|
-
base_path = ctx.app.config["UPLOAD_FOLDER"]
|
|
31
|
+
if "UPLOAD_FOLDER" in current_app.config and not base_path:
|
|
32
|
+
base_path = current_app.config["UPLOAD_FOLDER"]
|
|
43
33
|
if not base_path:
|
|
44
34
|
raise Exception("Config key UPLOAD_FOLDER is mandatory")
|
|
45
35
|
|
|
46
36
|
self.base_path = base_path
|
|
47
37
|
self.relative_path = relative_path
|
|
48
38
|
self.namegen = namegen or uuid_namegen
|
|
49
|
-
if not allowed_extensions and "FILE_ALLOWED_EXTENSIONS" in
|
|
50
|
-
self.allowed_extensions =
|
|
39
|
+
if not allowed_extensions and "FILE_ALLOWED_EXTENSIONS" in current_app.config:
|
|
40
|
+
self.allowed_extensions = current_app.config["FILE_ALLOWED_EXTENSIONS"]
|
|
51
41
|
else:
|
|
52
42
|
self.allowed_extensions = allowed_extensions
|
|
53
43
|
self.permission = permission
|
|
@@ -85,8 +75,8 @@ class FileManager(object):
|
|
|
85
75
|
|
|
86
76
|
class ImageManager(FileManager):
|
|
87
77
|
"""
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
Image Manager will manage your image files referenced on SQLAlchemy Model
|
|
79
|
+
will save files on IMG_UPLOAD_FOLDER as <uuid>_sep_<filename>
|
|
90
80
|
"""
|
|
91
81
|
|
|
92
82
|
keep_image_formats = ("PNG",)
|
|
@@ -103,22 +93,20 @@ class ImageManager(FileManager):
|
|
|
103
93
|
permission=0o755,
|
|
104
94
|
**kwargs
|
|
105
95
|
):
|
|
106
|
-
|
|
107
96
|
# Check if PIL is installed
|
|
108
97
|
if Image is None:
|
|
109
98
|
raise Exception("PIL library was not found")
|
|
110
99
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
self.max_size = ctx.app.config["IMG_SIZE"]
|
|
100
|
+
if "IMG_SIZE" in current_app.config and not max_size:
|
|
101
|
+
self.max_size = current_app.config["IMG_SIZE"]
|
|
114
102
|
|
|
115
|
-
if "IMG_UPLOAD_URL" in
|
|
116
|
-
relative_path =
|
|
103
|
+
if "IMG_UPLOAD_URL" in current_app.config and not relative_path:
|
|
104
|
+
relative_path = current_app.config["IMG_UPLOAD_URL"]
|
|
117
105
|
if not relative_path:
|
|
118
106
|
raise Exception("Config key IMG_UPLOAD_URL is mandatory")
|
|
119
107
|
|
|
120
|
-
if "IMG_UPLOAD_FOLDER" in
|
|
121
|
-
base_path =
|
|
108
|
+
if "IMG_UPLOAD_FOLDER" in current_app.config and not base_path:
|
|
109
|
+
base_path = current_app.config["IMG_UPLOAD_FOLDER"]
|
|
122
110
|
if not base_path:
|
|
123
111
|
raise Exception("Config key IMG_UPLOAD_FOLDER is mandatory")
|
|
124
112
|
|
|
@@ -161,10 +149,10 @@ class ImageManager(FileManager):
|
|
|
161
149
|
# Saving
|
|
162
150
|
def save_file(self, data, filename, size=None, thumbnail_size=None):
|
|
163
151
|
"""
|
|
164
|
-
|
|
152
|
+
Saves an image File
|
|
165
153
|
|
|
166
|
-
|
|
167
|
-
|
|
154
|
+
:param data: FileStorage from Flask form upload field
|
|
155
|
+
:param filename: Filename with full path
|
|
168
156
|
|
|
169
157
|
"""
|
|
170
158
|
max_size = size or self.max_size
|
|
@@ -204,19 +192,19 @@ class ImageManager(FileManager):
|
|
|
204
192
|
|
|
205
193
|
def resize(self, image, size):
|
|
206
194
|
"""
|
|
207
|
-
|
|
195
|
+
Resizes the image
|
|
208
196
|
|
|
209
197
|
:param image: The image object
|
|
210
|
-
:param size: size is PIL tuple (width,
|
|
198
|
+
:param size: size is PIL tuple (width, height, force) ex: (200,100,True)
|
|
211
199
|
"""
|
|
212
200
|
(width, height, force) = size
|
|
213
201
|
|
|
214
202
|
if image.size[0] > width or image.size[1] > height:
|
|
215
203
|
if force:
|
|
216
|
-
return ImageOps.fit(self.image, (width, height), Image.
|
|
204
|
+
return ImageOps.fit(self.image, (width, height), Image.LANCZOS)
|
|
217
205
|
else:
|
|
218
206
|
thumb = self.image.copy()
|
|
219
|
-
thumb.thumbnail((width, height), Image.
|
|
207
|
+
thumb.thumbnail((width, height), Image.LANCZOS)
|
|
220
208
|
return thumb
|
|
221
209
|
|
|
222
210
|
return image
|
|
@@ -241,23 +229,23 @@ def uuid_namegen(file_data):
|
|
|
241
229
|
|
|
242
230
|
def get_file_original_name(name):
|
|
243
231
|
"""
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
232
|
+
Use this function to get the user's original filename.
|
|
233
|
+
Filename is concatenated with <UUID>_sep_<FILE NAME>, to avoid collisions.
|
|
234
|
+
Use this function on your models on an additional function
|
|
247
235
|
|
|
248
|
-
|
|
236
|
+
::
|
|
249
237
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
238
|
+
class ProjectFiles(Base):
|
|
239
|
+
id = Column(Integer, primary_key=True)
|
|
240
|
+
file = Column(FileColumn, nullable=False)
|
|
253
241
|
|
|
254
|
-
|
|
255
|
-
|
|
242
|
+
def file_name(self):
|
|
243
|
+
return get_file_original_name(str(self.file))
|
|
256
244
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
245
|
+
:param name:
|
|
246
|
+
The file name from model
|
|
247
|
+
:return:
|
|
248
|
+
Returns the user's original filename removes <UUID>_sep_
|
|
261
249
|
"""
|
|
262
250
|
re_match = re.findall(".*_sep_(.*)", name)
|
|
263
251
|
if re_match:
|
flask_appbuilder/filters.py
CHANGED
|
@@ -13,11 +13,9 @@ def app_template_filter(filter_name=""):
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class TemplateFilters(object):
|
|
16
|
-
|
|
17
16
|
security_manager = None
|
|
18
17
|
|
|
19
18
|
def __init__(self, app, security_manager):
|
|
20
|
-
|
|
21
19
|
self.security_manager = security_manager
|
|
22
20
|
for attr_name in dir(self):
|
|
23
21
|
if hasattr(getattr(self, attr_name), "_filter"):
|
|
@@ -52,8 +50,8 @@ class TemplateFilters(object):
|
|
|
52
50
|
@app_template_filter("link_order")
|
|
53
51
|
def link_order_filter(self, column, modelview_name):
|
|
54
52
|
"""
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
Arguments are passed like:
|
|
54
|
+
_oc_<VIEW_NAME>=<COL_NAME>&_od_<VIEW_NAME>='asc'|'desc'
|
|
57
55
|
"""
|
|
58
56
|
new_args = request.view_args.copy()
|
|
59
57
|
args = request.args.copy()
|
|
@@ -74,7 +72,7 @@ class TemplateFilters(object):
|
|
|
74
72
|
@app_template_filter("link_page")
|
|
75
73
|
def link_page_filter(self, page, modelview_name):
|
|
76
74
|
"""
|
|
77
|
-
|
|
75
|
+
Arguments are passed like: page_<VIEW_NAME>=<PAGE_NUMBER>
|
|
78
76
|
"""
|
|
79
77
|
new_args = request.view_args.copy()
|
|
80
78
|
args = request.args.copy()
|
|
@@ -144,14 +142,14 @@ class TemplateFilters(object):
|
|
|
144
142
|
@app_template_filter("is_item_visible")
|
|
145
143
|
def is_item_visible(self, permission: str, item: str) -> bool:
|
|
146
144
|
"""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
145
|
+
Check if an item is visible on the template
|
|
146
|
+
this changed with permission mapping feature.
|
|
147
|
+
This is a best effort to deliver the feature
|
|
148
|
+
and not break compatibility
|
|
149
|
+
|
|
150
|
+
permission is:
|
|
151
|
+
- 'can_' + <METHOD_NAME>: On normal routes
|
|
152
|
+
- <METHOD_NAME>: when it's an action
|
|
155
153
|
|
|
156
154
|
"""
|
|
157
155
|
_view = self.find_views_by_name(item)
|
flask_appbuilder/forms.py
CHANGED
|
@@ -22,7 +22,6 @@ from .fieldwidgets import (
|
|
|
22
22
|
Select2ManyWidget,
|
|
23
23
|
Select2Widget,
|
|
24
24
|
)
|
|
25
|
-
from .models.mongoengine.fields import MongoFileField, MongoImageField
|
|
26
25
|
from .upload import (
|
|
27
26
|
BS3FileUploadFieldWidget,
|
|
28
27
|
BS3ImageUploadFieldWidget,
|
|
@@ -39,21 +38,20 @@ except Exception:
|
|
|
39
38
|
log = logging.getLogger(__name__)
|
|
40
39
|
|
|
41
40
|
|
|
42
|
-
class FieldConverter
|
|
41
|
+
class FieldConverter:
|
|
43
42
|
"""
|
|
44
|
-
|
|
43
|
+
Helper class that converts model fields into WTForm fields
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
it has a conversion table with type method checks from model
|
|
46
|
+
interfaces, these methods are invoked with a column name
|
|
48
47
|
"""
|
|
49
48
|
|
|
50
49
|
conversion_table = (
|
|
51
50
|
("is_image", ImageUploadField, BS3ImageUploadFieldWidget),
|
|
52
51
|
("is_file", FileUploadField, BS3FileUploadFieldWidget),
|
|
53
|
-
("is_gridfs_file", MongoFileField, BS3FileUploadFieldWidget),
|
|
54
|
-
("is_gridfs_image", MongoImageField, BS3ImageUploadFieldWidget),
|
|
55
52
|
("is_text", TextAreaField, BS3TextAreaFieldWidget),
|
|
56
53
|
("is_binary", TextAreaField, BS3TextAreaFieldWidget),
|
|
54
|
+
("is_json", TextAreaField, BS3TextAreaFieldWidget),
|
|
57
55
|
("is_string", StringField, BS3TextFieldWidget),
|
|
58
56
|
("is_integer", IntegerField, BS3TextFieldWidget),
|
|
59
57
|
("is_numeric", DecimalField, BS3TextFieldWidget),
|
|
@@ -104,13 +102,13 @@ class FieldConverter(object):
|
|
|
104
102
|
validators=self.validators,
|
|
105
103
|
default=self.default,
|
|
106
104
|
)
|
|
107
|
-
log.error("Column %s Type not supported"
|
|
105
|
+
log.error("Column %s Type not supported", self.colname)
|
|
108
106
|
|
|
109
107
|
|
|
110
|
-
class GeneralModelConverter
|
|
108
|
+
class GeneralModelConverter:
|
|
111
109
|
"""
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
Returns a form from a model only one public exposed
|
|
111
|
+
method 'create_form'
|
|
114
112
|
"""
|
|
115
113
|
|
|
116
114
|
def __init__(self, datamodel):
|
|
@@ -153,9 +151,9 @@ class GeneralModelConverter(object):
|
|
|
153
151
|
form_props,
|
|
154
152
|
):
|
|
155
153
|
"""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
Creates a WTForm field for many to one related fields,
|
|
155
|
+
will use a Select box based on a query. Will only
|
|
156
|
+
work with SQLAlchemy interface.
|
|
159
157
|
"""
|
|
160
158
|
query_func = self._get_related_query_func(col_name, filter_rel_fields)
|
|
161
159
|
get_pk_func = self._get_related_pk_func(col_name)
|
|
@@ -188,13 +186,11 @@ class GeneralModelConverter(object):
|
|
|
188
186
|
):
|
|
189
187
|
query_func = self._get_related_query_func(col_name, filter_rel_fields)
|
|
190
188
|
get_pk_func = self._get_related_pk_func(col_name)
|
|
191
|
-
allow_blank = True
|
|
192
189
|
form_props[col_name] = QuerySelectMultipleField(
|
|
193
190
|
label,
|
|
194
191
|
description=description,
|
|
195
192
|
query_func=query_func,
|
|
196
193
|
get_pk_func=get_pk_func,
|
|
197
|
-
allow_blank=allow_blank,
|
|
198
194
|
validators=lst_validators,
|
|
199
195
|
widget=Select2ManyWidget(),
|
|
200
196
|
)
|
|
@@ -259,7 +255,7 @@ class GeneralModelConverter(object):
|
|
|
259
255
|
form_props,
|
|
260
256
|
)
|
|
261
257
|
else:
|
|
262
|
-
log.warning("Relation
|
|
258
|
+
log.warning("Relation %s not supported", col_name)
|
|
263
259
|
else:
|
|
264
260
|
return self._convert_simple(
|
|
265
261
|
col_name, label, description, lst_validators, form_props
|
|
@@ -275,28 +271,28 @@ class GeneralModelConverter(object):
|
|
|
275
271
|
filter_rel_fields=None,
|
|
276
272
|
):
|
|
277
273
|
"""
|
|
278
|
-
|
|
274
|
+
Converts a model to a form given
|
|
279
275
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
276
|
+
:param label_columns:
|
|
277
|
+
A dictionary with the column's labels.
|
|
278
|
+
:param inc_columns:
|
|
279
|
+
A list with the columns to include
|
|
280
|
+
:param description_columns:
|
|
281
|
+
A dictionary with a description for cols.
|
|
282
|
+
:param validators_columns:
|
|
283
|
+
A dictionary with WTForms validators ex::
|
|
288
284
|
|
|
289
|
-
|
|
285
|
+
validators={'personal_email':EmailValidator}
|
|
290
286
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
287
|
+
:param extra_fields:
|
|
288
|
+
A dictionary containing column names and a WTForm
|
|
289
|
+
Form fields to be added to the form, these fields do not
|
|
290
|
+
exist on the model itself ex::
|
|
295
291
|
|
|
296
|
-
|
|
292
|
+
extra_fields={'some_col':BooleanField('Some Col', default=False)}
|
|
297
293
|
|
|
298
|
-
|
|
299
|
-
|
|
294
|
+
:param filter_rel_fields:
|
|
295
|
+
A filter to be applied on relationships
|
|
300
296
|
"""
|
|
301
297
|
label_columns = label_columns or {}
|
|
302
298
|
inc_columns = inc_columns or []
|
|
@@ -321,7 +317,7 @@ class GeneralModelConverter(object):
|
|
|
321
317
|
|
|
322
318
|
class DynamicForm(FlaskForm):
|
|
323
319
|
"""
|
|
324
|
-
|
|
320
|
+
Refresh method will force select field to refresh
|
|
325
321
|
"""
|
|
326
322
|
|
|
327
323
|
@classmethod
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def before_request(
|
|
5
|
+
hook: Callable[[], Any] = None, only: List[str] = None
|
|
6
|
+
) -> Callable[..., Any]:
|
|
7
|
+
"""
|
|
8
|
+
This decorator provides a way to hook into the request
|
|
9
|
+
lifecycle by enqueueing methods to be invoked before
|
|
10
|
+
each handler in the view. If the method returns a value
|
|
11
|
+
other than :code:`None`, then that value will be returned
|
|
12
|
+
to the client. If invoked with the :code:`only` kwarg,
|
|
13
|
+
the hook will only be invoked for the given list of
|
|
14
|
+
handler methods.
|
|
15
|
+
|
|
16
|
+
Examples::
|
|
17
|
+
|
|
18
|
+
class MyFeature(ModelView)
|
|
19
|
+
|
|
20
|
+
@before_request
|
|
21
|
+
def ensure_feature_is_enabled(self):
|
|
22
|
+
if self.feature_is_disabled:
|
|
23
|
+
return self.response_404()
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
# etc...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MyView(ModelRestAPI):
|
|
30
|
+
|
|
31
|
+
@before_request(only=["create", "update", "delete"])
|
|
32
|
+
def ensure_write_mode_enabled(self):
|
|
33
|
+
if self.read_only:
|
|
34
|
+
return self.response_400()
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
# etc...
|
|
38
|
+
|
|
39
|
+
:param hook:
|
|
40
|
+
A callable to be invoked before handlers in the class. If the
|
|
41
|
+
hook returns :code:`None`, then the request proceeds and the
|
|
42
|
+
handler is invoked. If it returns something other than :code:`None`,
|
|
43
|
+
then execution halts and that value is returned to the client.
|
|
44
|
+
:param only:
|
|
45
|
+
An optional list of the names of handler methods. If present,
|
|
46
|
+
:code:`hook` will only be invoked before the handlers specified
|
|
47
|
+
in the list. If absent, :code:`hook` will be invoked for before
|
|
48
|
+
all handlers in the class.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def wrap(hook: Callable[[], Any]) -> Callable[[], Any]:
|
|
52
|
+
hook._before_request_hook = True
|
|
53
|
+
hook._before_request_only = only
|
|
54
|
+
return hook
|
|
55
|
+
|
|
56
|
+
return wrap if hook is None else wrap(hook)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def wrap_route_handler_with_hooks(
|
|
60
|
+
handler_name: str,
|
|
61
|
+
handler: Callable[..., Any],
|
|
62
|
+
before_request_hooks: List[Callable[[], Any]],
|
|
63
|
+
) -> Callable[..., Any]:
|
|
64
|
+
applicable_hooks = []
|
|
65
|
+
for hook in before_request_hooks:
|
|
66
|
+
only = hook._before_request_only
|
|
67
|
+
applicable_hook = only is None or handler_name in only
|
|
68
|
+
if applicable_hook:
|
|
69
|
+
applicable_hooks.append(hook)
|
|
70
|
+
|
|
71
|
+
if not applicable_hooks:
|
|
72
|
+
return handler
|
|
73
|
+
|
|
74
|
+
def wrapped_handler(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
|
|
75
|
+
for hook in applicable_hooks:
|
|
76
|
+
result = hook()
|
|
77
|
+
if result is not None:
|
|
78
|
+
return result
|
|
79
|
+
return handler(*args, **kwargs)
|
|
80
|
+
|
|
81
|
+
return wrapped_handler
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_before_request_hooks(view_or_api_instance: Any) -> List[Callable[[], Any]]:
|
|
85
|
+
before_request_hooks = []
|
|
86
|
+
for attr_name in dir(view_or_api_instance):
|
|
87
|
+
attr = getattr(view_or_api_instance, attr_name)
|
|
88
|
+
if hasattr(attr, "_before_request_hook") and attr._before_request_hook is True:
|
|
89
|
+
before_request_hooks.append(attr)
|
|
90
|
+
return before_request_hooks
|
flask_appbuilder/menu.py
CHANGED
|
@@ -9,13 +9,19 @@ from .security.decorators import permission_name, protect
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class MenuItem(object):
|
|
12
|
-
def __init__(
|
|
12
|
+
def __init__(
|
|
13
|
+
self, name, href="", icon="", label="", childs=None, baseview=None, cond=None
|
|
14
|
+
):
|
|
13
15
|
self.name = name
|
|
14
16
|
self.href = href
|
|
15
17
|
self.icon = icon
|
|
16
18
|
self.label = label
|
|
17
19
|
self.childs = childs or []
|
|
18
20
|
self.baseview = baseview
|
|
21
|
+
self.cond = cond
|
|
22
|
+
|
|
23
|
+
def should_render(self) -> bool:
|
|
24
|
+
return bool(self.cond()) if self.cond is not None else True
|
|
19
25
|
|
|
20
26
|
def get_url(self):
|
|
21
27
|
if not self.href:
|
|
@@ -65,6 +71,9 @@ class Menu(object):
|
|
|
65
71
|
)
|
|
66
72
|
|
|
67
73
|
for i, item in enumerate(menu):
|
|
74
|
+
if not item.should_render():
|
|
75
|
+
continue
|
|
76
|
+
|
|
68
77
|
if item.name == "-" and not i == len(menu) - 1:
|
|
69
78
|
ret_list.append("-")
|
|
70
79
|
elif item.name not in allowed_menus:
|
|
@@ -91,10 +100,10 @@ class Menu(object):
|
|
|
91
100
|
|
|
92
101
|
def find(self, name, menu=None):
|
|
93
102
|
"""
|
|
94
|
-
|
|
103
|
+
Finds a menu item by name and returns it.
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
:param name:
|
|
106
|
+
The menu item name.
|
|
98
107
|
"""
|
|
99
108
|
menu = menu or self.menu
|
|
100
109
|
for i in menu:
|
|
@@ -125,20 +134,31 @@ class Menu(object):
|
|
|
125
134
|
category_icon="",
|
|
126
135
|
category_label="",
|
|
127
136
|
baseview=None,
|
|
137
|
+
cond=None,
|
|
128
138
|
):
|
|
129
139
|
label = label or name
|
|
130
140
|
category_label = category_label or category
|
|
131
141
|
if category == "":
|
|
132
142
|
self.menu.append(
|
|
133
143
|
MenuItem(
|
|
134
|
-
name=name,
|
|
144
|
+
name=name,
|
|
145
|
+
href=href,
|
|
146
|
+
icon=icon,
|
|
147
|
+
label=label,
|
|
148
|
+
baseview=baseview,
|
|
149
|
+
cond=cond,
|
|
135
150
|
)
|
|
136
151
|
)
|
|
137
152
|
else:
|
|
138
153
|
menu_item = self.find(category)
|
|
139
154
|
if menu_item:
|
|
140
155
|
new_menu_item = MenuItem(
|
|
141
|
-
name=name,
|
|
156
|
+
name=name,
|
|
157
|
+
href=href,
|
|
158
|
+
icon=icon,
|
|
159
|
+
label=label,
|
|
160
|
+
baseview=baseview,
|
|
161
|
+
cond=cond,
|
|
142
162
|
)
|
|
143
163
|
menu_item.childs.append(new_menu_item)
|
|
144
164
|
else:
|
|
@@ -146,14 +166,19 @@ class Menu(object):
|
|
|
146
166
|
category=category, icon=category_icon, label=category_label
|
|
147
167
|
)
|
|
148
168
|
new_menu_item = MenuItem(
|
|
149
|
-
name=name,
|
|
169
|
+
name=name,
|
|
170
|
+
href=href,
|
|
171
|
+
icon=icon,
|
|
172
|
+
label=label,
|
|
173
|
+
baseview=baseview,
|
|
174
|
+
cond=cond,
|
|
150
175
|
)
|
|
151
176
|
self.find(category).childs.append(new_menu_item)
|
|
152
177
|
|
|
153
|
-
def add_separator(self, category=""):
|
|
178
|
+
def add_separator(self, category="", cond=None):
|
|
154
179
|
menu_item = self.find(category)
|
|
155
180
|
if menu_item:
|
|
156
|
-
menu_item.childs.append(MenuItem("-"))
|
|
181
|
+
menu_item.childs.append(MenuItem("-", cond=cond))
|
|
157
182
|
else:
|
|
158
183
|
raise Exception(
|
|
159
184
|
"Menu separator does not have correct category {}".format(category)
|
|
@@ -213,5 +238,5 @@ class MenuApi(BaseApi):
|
|
|
213
238
|
|
|
214
239
|
class MenuApiManager(BaseManager):
|
|
215
240
|
def register_views(self):
|
|
216
|
-
if
|
|
241
|
+
if current_app.config.get("FAB_ADD_MENU_API", True):
|
|
217
242
|
self.appbuilder.add_api(MenuApi)
|