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/upload.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from flask_babel import gettext
|
|
2
|
+
from markupsafe import Markup
|
|
2
3
|
from werkzeug.datastructures import FileStorage
|
|
3
4
|
from wtforms import fields, ValidationError
|
|
4
|
-
from wtforms.widgets import html_params
|
|
5
|
+
from wtforms.widgets import html_params
|
|
5
6
|
|
|
6
7
|
from .filemanager import FileManager, ImageManager
|
|
7
8
|
|
|
@@ -18,7 +19,6 @@ except ImportError:
|
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class BS3FileUploadFieldWidget(object):
|
|
21
|
-
|
|
22
22
|
empty_template = (
|
|
23
23
|
'<div class="input-group">'
|
|
24
24
|
'<span class="input-group-addon"><i class="fa fa-upload"></i>'
|
|
@@ -45,7 +45,7 @@ class BS3FileUploadFieldWidget(object):
|
|
|
45
45
|
|
|
46
46
|
template = self.data_template if field.data else self.empty_template
|
|
47
47
|
|
|
48
|
-
return
|
|
48
|
+
return Markup(
|
|
49
49
|
template
|
|
50
50
|
% {
|
|
51
51
|
"text": html_params(type="text", value=field.data),
|
|
@@ -56,7 +56,6 @@ class BS3FileUploadFieldWidget(object):
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
class BS3ImageUploadFieldWidget(object):
|
|
59
|
-
|
|
60
59
|
empty_template = (
|
|
61
60
|
'<div class="input-group">'
|
|
62
61
|
'<span class="input-group-addon"><span class="glyphicon glyphicon-upload"></span>'
|
|
@@ -94,7 +93,7 @@ class BS3ImageUploadFieldWidget(object):
|
|
|
94
93
|
else:
|
|
95
94
|
template = self.empty_template
|
|
96
95
|
|
|
97
|
-
return
|
|
96
|
+
return Markup(template % args)
|
|
98
97
|
|
|
99
98
|
def get_url(self, field):
|
|
100
99
|
im = ImageManager()
|
|
@@ -102,30 +101,30 @@ class BS3ImageUploadFieldWidget(object):
|
|
|
102
101
|
|
|
103
102
|
|
|
104
103
|
# Fields
|
|
105
|
-
class FileUploadField(fields.
|
|
104
|
+
class FileUploadField(fields.StringField):
|
|
106
105
|
"""
|
|
107
|
-
|
|
106
|
+
Customizable file-upload field.
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
Saves file to configured path, handles updates and deletions.
|
|
109
|
+
Inherits from `StringField`, resulting filename will be stored as string.
|
|
111
110
|
"""
|
|
112
111
|
|
|
113
112
|
widget = BS3FileUploadFieldWidget()
|
|
114
113
|
|
|
115
114
|
def __init__(self, label=None, validators=None, filemanager=None, **kwargs):
|
|
116
115
|
"""
|
|
117
|
-
|
|
116
|
+
Constructor.
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
:param label:
|
|
119
|
+
Display label
|
|
120
|
+
:param validators:
|
|
121
|
+
Validators
|
|
123
122
|
"""
|
|
124
123
|
|
|
125
124
|
self.filemanager = filemanager or FileManager()
|
|
126
125
|
self._should_delete = False
|
|
127
126
|
|
|
128
|
-
super(
|
|
127
|
+
super().__init__(label, validators, **kwargs)
|
|
129
128
|
|
|
130
129
|
def process_on_delete(self, obj):
|
|
131
130
|
"""Override this method to make customised updates to the object
|
|
@@ -158,12 +157,12 @@ class FileUploadField(fields.TextField):
|
|
|
158
157
|
):
|
|
159
158
|
raise ValidationError(gettext("Invalid file extension"))
|
|
160
159
|
|
|
161
|
-
def process(self, formdata, data=unset_value):
|
|
160
|
+
def process(self, formdata, data=unset_value, **kwargs):
|
|
162
161
|
if formdata:
|
|
163
162
|
marker = "_%s-delete" % self.name
|
|
164
163
|
if marker in formdata:
|
|
165
164
|
self._should_delete = True
|
|
166
|
-
return super(
|
|
165
|
+
return super().process(formdata, data, **kwargs)
|
|
167
166
|
|
|
168
167
|
def populate_obj(self, obj, name):
|
|
169
168
|
field = getattr(obj, name, None)
|
|
@@ -192,16 +191,15 @@ class FileUploadField(fields.TextField):
|
|
|
192
191
|
|
|
193
192
|
class ImageUploadField(fields.StringField):
|
|
194
193
|
"""
|
|
195
|
-
|
|
194
|
+
Image upload field.
|
|
196
195
|
"""
|
|
197
196
|
|
|
198
197
|
widget = BS3ImageUploadFieldWidget()
|
|
199
198
|
|
|
200
199
|
def __init__(self, label=None, validators=None, imagemanager=None, **kwargs):
|
|
201
|
-
|
|
202
200
|
self.imagemanager = imagemanager or ImageManager()
|
|
203
201
|
self._should_delete = False
|
|
204
|
-
super(
|
|
202
|
+
super().__init__(label, validators, **kwargs)
|
|
205
203
|
|
|
206
204
|
def pre_validate(self, form):
|
|
207
205
|
if (
|
|
@@ -211,12 +209,12 @@ class ImageUploadField(fields.StringField):
|
|
|
211
209
|
):
|
|
212
210
|
raise ValidationError(gettext("Invalid file extension"))
|
|
213
211
|
|
|
214
|
-
def process(self, formdata, data=unset_value):
|
|
212
|
+
def process(self, formdata, data=unset_value, **kwargs):
|
|
215
213
|
if formdata:
|
|
216
214
|
marker = "_%s-delete" % self.name
|
|
217
215
|
if marker in formdata:
|
|
218
216
|
self._should_delete = True
|
|
219
|
-
return super(
|
|
217
|
+
return super().process(formdata, data, **kwargs)
|
|
220
218
|
|
|
221
219
|
def populate_obj(self, obj, name):
|
|
222
220
|
field = getattr(obj, name, None)
|
flask_appbuilder/urltools.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import re
|
|
2
3
|
|
|
3
4
|
from flask import request
|
|
4
5
|
|
|
6
|
+
log = logging.getLogger(__name__)
|
|
7
|
+
|
|
5
8
|
|
|
6
9
|
class Stack(object):
|
|
7
10
|
"""
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
Stack data structure will not insert
|
|
12
|
+
equal sequential data
|
|
10
13
|
"""
|
|
11
14
|
|
|
12
15
|
def __init__(self, list=None, size=5):
|
|
@@ -33,7 +36,7 @@ class Stack(object):
|
|
|
33
36
|
|
|
34
37
|
def get_group_by_args():
|
|
35
38
|
"""
|
|
36
|
-
|
|
39
|
+
Get page arguments for group by
|
|
37
40
|
"""
|
|
38
41
|
group_by = request.args.get("group_by")
|
|
39
42
|
if not group_by:
|
|
@@ -43,10 +46,10 @@ def get_group_by_args():
|
|
|
43
46
|
|
|
44
47
|
def get_page_args():
|
|
45
48
|
"""
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
Get page arguments, returns a dictionary
|
|
50
|
+
{ <VIEW_NAME>: PAGE_NUMBER }
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
Arguments are passed: page_<VIEW_NAME>=<PAGE_NUMBER>
|
|
50
53
|
|
|
51
54
|
"""
|
|
52
55
|
pages = {}
|
|
@@ -59,10 +62,10 @@ def get_page_args():
|
|
|
59
62
|
|
|
60
63
|
def get_page_size_args():
|
|
61
64
|
"""
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
Get page size arguments, returns an int
|
|
66
|
+
{ <VIEW_NAME>: PAGE_NUMBER }
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
Arguments are passed: psize_<VIEW_NAME>=<PAGE_SIZE>
|
|
66
69
|
|
|
67
70
|
"""
|
|
68
71
|
page_sizes = {}
|
|
@@ -75,10 +78,10 @@ def get_page_size_args():
|
|
|
75
78
|
|
|
76
79
|
def get_order_args():
|
|
77
80
|
"""
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
Get order arguments, return a dictionary
|
|
82
|
+
{ <VIEW_NAME>: (ORDER_COL, ORDER_DIRECTION) }
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
Arguments are passed like: _oc_<VIEW_NAME>=<COL_NAME>&_od_<VIEW_NAME>='asc'|'desc'
|
|
82
85
|
|
|
83
86
|
"""
|
|
84
87
|
orders = {}
|
|
@@ -91,11 +94,28 @@ def get_order_args():
|
|
|
91
94
|
return orders
|
|
92
95
|
|
|
93
96
|
|
|
94
|
-
def get_filter_args(filters):
|
|
97
|
+
def get_filter_args(filters, disallow_if_not_in_search=True):
|
|
98
|
+
"""
|
|
99
|
+
Sets filters with the given current request args
|
|
100
|
+
|
|
101
|
+
Request arg filters are of the form "_flt_<DECIMAL>_<VIEW_NAME>_<COL_NAME>"
|
|
102
|
+
|
|
103
|
+
:param filters: Filter instance to apply the request filters on
|
|
104
|
+
:param disallow_if_not_in_search: If True, disallow filters that are not in the search
|
|
105
|
+
:return:
|
|
106
|
+
"""
|
|
95
107
|
filters.clear_filters()
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
request_args = set(request.args)
|
|
109
|
+
for arg in request_args:
|
|
110
|
+
re_match = re.findall(r"_flt_(\d)_(.*)", arg)
|
|
111
|
+
if not re_match:
|
|
112
|
+
continue
|
|
113
|
+
filter_index = int(re_match[0][0])
|
|
114
|
+
filter_column = re_match[0][1]
|
|
115
|
+
if (
|
|
116
|
+
filter_column not in filters.get_search_filters().keys()
|
|
117
|
+
and disallow_if_not_in_search
|
|
118
|
+
):
|
|
119
|
+
log.warning("Filter column not allowed")
|
|
120
|
+
continue
|
|
121
|
+
filters.add_filter_index(filter_column, filter_index, request.args.getlist(arg))
|
flask_appbuilder/utils/base.py
CHANGED
|
@@ -1,3 +1,52 @@
|
|
|
1
|
+
from fnmatch import fnmatch
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
import unicodedata
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
from flask import current_app, request
|
|
8
|
+
from flask_babel import gettext
|
|
9
|
+
from flask_babel.speaklater import LazyString
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_safe_redirect_url(url: str) -> bool:
|
|
16
|
+
if url.startswith("///"):
|
|
17
|
+
return False
|
|
18
|
+
try:
|
|
19
|
+
url_info = urlparse(url)
|
|
20
|
+
except ValueError:
|
|
21
|
+
return False
|
|
22
|
+
if not url_info.netloc and url_info.scheme:
|
|
23
|
+
return False
|
|
24
|
+
if unicodedata.category(url[0])[0] == "C":
|
|
25
|
+
return False
|
|
26
|
+
scheme = url_info.scheme
|
|
27
|
+
# Consider URLs without a scheme (e.g. //example.com/p) to be http.
|
|
28
|
+
if not url_info.scheme and url_info.netloc:
|
|
29
|
+
scheme = "http"
|
|
30
|
+
valid_schemes = ["http", "https"]
|
|
31
|
+
|
|
32
|
+
safe_hosts = current_app.config.get("FAB_SAFE_REDIRECT_HOSTS", [])
|
|
33
|
+
if not safe_hosts:
|
|
34
|
+
safe_hosts = [urlparse(request.host_url).netloc]
|
|
35
|
+
|
|
36
|
+
is_host_allowed = not url_info.netloc or any(
|
|
37
|
+
fnmatch(url_info.netloc, pattern) for pattern in safe_hosts
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return is_host_allowed and (not scheme or scheme in valid_schemes)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_safe_redirect(url):
|
|
44
|
+
if url and is_safe_redirect_url(url):
|
|
45
|
+
return url
|
|
46
|
+
log.warning("Invalid redirect detected, falling back to index")
|
|
47
|
+
return current_app.appbuilder.get_url_for_index
|
|
48
|
+
|
|
49
|
+
|
|
1
50
|
def get_column_root_relation(column: str) -> str:
|
|
2
51
|
if "." in column:
|
|
3
52
|
return column.split(".")[0]
|
|
@@ -12,3 +61,30 @@ def get_column_leaf(column: str) -> str:
|
|
|
12
61
|
|
|
13
62
|
def is_column_dotted(column: str) -> bool:
|
|
14
63
|
return "." in column
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _wrap_lazy_formatter_gettext(
|
|
67
|
+
string: str, lazy_formater: Callable[[str], str], **variables: Any
|
|
68
|
+
) -> str:
|
|
69
|
+
return gettext(lazy_formater(string), **variables)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def lazy_formatter_gettext(
|
|
73
|
+
string: str, lazy_formatter: Callable[[str], str], **variables: Any
|
|
74
|
+
) -> LazyString:
|
|
75
|
+
"""Formats a lazy_gettext string with a custom function
|
|
76
|
+
|
|
77
|
+
Example::
|
|
78
|
+
|
|
79
|
+
def custom_formatter(string: str) -> str:
|
|
80
|
+
if current_app.config["CONDITIONAL_KEY"]:
|
|
81
|
+
string += " . Condition key is on"
|
|
82
|
+
return string
|
|
83
|
+
|
|
84
|
+
hello = lazy_formatter_gettext(u'Hello World', custom_formatter)
|
|
85
|
+
|
|
86
|
+
@app.route('/')
|
|
87
|
+
def index():
|
|
88
|
+
return unicode(hello)
|
|
89
|
+
"""
|
|
90
|
+
return LazyString(_wrap_lazy_formatter_gettext, string, lazy_formatter, **variables)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
from typing import Type, TYPE_CHECKING, Union
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from flask_appbuilder.models.sqla.base import SQLA as SQLA
|
|
6
|
+
from flask_appbuilder.models.sqla.base_legacy import SQLA as SQLALegacy
|
|
7
|
+
else:
|
|
8
|
+
SQLA = None # type: ignore
|
|
9
|
+
SQLALegacy = None # type: ignore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_flask_sqlalchemy_2() -> bool:
|
|
13
|
+
"""
|
|
14
|
+
Check if the installed version of flask-sqlalchemy is 2.x.x.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
fsqla_version = version("flask-sqlalchemy")
|
|
18
|
+
except PackageNotFoundError:
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
major_version = int(fsqla_version.split(".")[0])
|
|
22
|
+
return major_version == 2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_sqla_class() -> Union[Type[SQLA], Type[SQLALegacy]]:
|
|
26
|
+
"""
|
|
27
|
+
Returns the SQLA class based on the version of flask-sqlalchemy installed.
|
|
28
|
+
"""
|
|
29
|
+
if is_flask_sqlalchemy_2():
|
|
30
|
+
from flask_appbuilder.models.sqla.base_legacy import SQLA
|
|
31
|
+
else:
|
|
32
|
+
from flask_appbuilder.models.sqla.base import SQLA
|
|
33
|
+
return SQLA
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Callable, Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
from flask import Response
|
|
5
|
+
from flask_limiter import RequestLimit
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclasses.dataclass
|
|
9
|
+
class Limit:
|
|
10
|
+
limit_value: Union[Callable[[], str], str]
|
|
11
|
+
key_func: Callable[[], str]
|
|
12
|
+
scope: Optional[Union[str, Callable[[str], str]]] = None
|
|
13
|
+
methods: Optional[Tuple[str, ...]] = None
|
|
14
|
+
error_message: Optional[str] = None
|
|
15
|
+
exempt_when: Optional[Callable[[], bool]] = None
|
|
16
|
+
override_defaults: Optional[bool] = False
|
|
17
|
+
deduct_when: Optional[Callable[[Response], bool]] = None
|
|
18
|
+
on_breach: Optional[Callable[[RequestLimit], Optional[Response]]] = None
|
|
19
|
+
per_method: bool = False
|
|
20
|
+
cost: Optional[Union[Callable[[], int], int]] = None
|
flask_appbuilder/validators.py
CHANGED
|
@@ -1,26 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
2
3
|
|
|
4
|
+
from flask import current_app
|
|
5
|
+
from flask_appbuilder.exceptions import PasswordComplexityValidationError
|
|
6
|
+
from flask_appbuilder.models.base import BaseInterface
|
|
7
|
+
from flask_babel import gettext
|
|
8
|
+
from wtforms import Field, Form, ValidationError
|
|
3
9
|
|
|
4
|
-
|
|
10
|
+
password_complexity_regex = re.compile(
|
|
11
|
+
r"""(
|
|
12
|
+
^(?=.*[A-Z].*[A-Z]) # at least two capital letters
|
|
13
|
+
(?=.*[^0-9a-zA-Z]) # at least one of these special characters
|
|
14
|
+
(?=.*[0-9].*[0-9]) # at least two numeric digits
|
|
15
|
+
(?=.*[a-z].*[a-z].*[a-z]) # at least three lower case letters
|
|
16
|
+
.{10,} # at least 10 total characters
|
|
17
|
+
$
|
|
18
|
+
)""",
|
|
19
|
+
re.VERBOSE,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Unique:
|
|
5
24
|
"""
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
:param datamodel:
|
|
9
|
-
The datamodel class, abstract layer for backend
|
|
10
|
-
:param col_name:
|
|
11
|
-
The unique column name.
|
|
12
|
-
:param message:
|
|
13
|
-
The error message.
|
|
25
|
+
WTForm field validator. Checks if field value is unique against
|
|
26
|
+
a specified table field.
|
|
14
27
|
"""
|
|
15
28
|
|
|
16
|
-
field_flags =
|
|
29
|
+
field_flags = {"unique": True}
|
|
17
30
|
|
|
18
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self, datamodel: BaseInterface, col_name: str, message: Optional[str] = None
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
:param datamodel: The datamodel class, abstract layer for backend
|
|
36
|
+
:param col_name: The unique column name.
|
|
37
|
+
:param message: The error message.
|
|
38
|
+
"""
|
|
19
39
|
self.datamodel = datamodel
|
|
20
40
|
self.col_name = col_name
|
|
21
41
|
self.message = message
|
|
22
42
|
|
|
23
|
-
def __call__(self, form, field):
|
|
43
|
+
def __call__(self, form: Form, field: Field) -> None:
|
|
24
44
|
filters = self.datamodel.get_filters().add_filter(
|
|
25
45
|
self.col_name, self.datamodel.FilterEqual, field.data
|
|
26
46
|
)
|
|
@@ -29,5 +49,44 @@ class Unique(object):
|
|
|
29
49
|
# only test if Unique, if pk value is different on update.
|
|
30
50
|
if not hasattr(form, "_id") or form._id != self.datamodel.get_keys(obj)[0]:
|
|
31
51
|
if self.message is None:
|
|
32
|
-
self.message = field.gettext(
|
|
52
|
+
self.message = field.gettext("Already exists.")
|
|
33
53
|
raise ValidationError(self.message)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PasswordComplexityValidator:
|
|
57
|
+
"""
|
|
58
|
+
WTForm field validator. Calls a custom password validator, useful for imposing
|
|
59
|
+
password complexity for database Auth users.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __call__(self, form: Form, field: Field) -> None:
|
|
63
|
+
if current_app.config.get("FAB_PASSWORD_COMPLEXITY_ENABLED", False):
|
|
64
|
+
password_complexity_validator = current_app.config.get(
|
|
65
|
+
"FAB_PASSWORD_COMPLEXITY_VALIDATOR", None
|
|
66
|
+
)
|
|
67
|
+
if password_complexity_validator is not None:
|
|
68
|
+
try:
|
|
69
|
+
password_complexity_validator(field.data)
|
|
70
|
+
except PasswordComplexityValidationError as exc:
|
|
71
|
+
raise ValidationError(str(exc))
|
|
72
|
+
else:
|
|
73
|
+
try:
|
|
74
|
+
default_password_complexity(field.data)
|
|
75
|
+
except PasswordComplexityValidationError as exc:
|
|
76
|
+
raise ValidationError(str(exc))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def default_password_complexity(password: str) -> None:
|
|
80
|
+
"""
|
|
81
|
+
FAB's default password complexity validator, set FAB_PASSWORD_COMPLEXITY_ENABLED
|
|
82
|
+
to True to enable it
|
|
83
|
+
"""
|
|
84
|
+
match = re.search(password_complexity_regex, password)
|
|
85
|
+
if not match:
|
|
86
|
+
raise PasswordComplexityValidationError(
|
|
87
|
+
gettext(
|
|
88
|
+
"Must have at least two capital letters,"
|
|
89
|
+
" one special character, two digits, three lower case letters and"
|
|
90
|
+
" a minimal length of 10."
|
|
91
|
+
)
|
|
92
|
+
)
|