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.
Files changed (228) hide show
  1. flask_appbuilder/__init__.py +2 -3
  2. flask_appbuilder/_compat.py +0 -1
  3. flask_appbuilder/actions.py +14 -14
  4. flask_appbuilder/api/__init__.py +741 -527
  5. flask_appbuilder/api/convert.py +104 -98
  6. flask_appbuilder/api/manager.py +14 -8
  7. flask_appbuilder/api/schemas.py +12 -1
  8. flask_appbuilder/babel/manager.py +12 -16
  9. flask_appbuilder/base.py +353 -280
  10. flask_appbuilder/basemanager.py +1 -1
  11. flask_appbuilder/baseviews.py +241 -164
  12. flask_appbuilder/charts/jsontools.py +10 -10
  13. flask_appbuilder/charts/views.py +56 -60
  14. flask_appbuilder/cli.py +115 -70
  15. flask_appbuilder/const.py +52 -52
  16. flask_appbuilder/exceptions.py +67 -5
  17. flask_appbuilder/fields.py +32 -23
  18. flask_appbuilder/fieldwidgets.py +34 -27
  19. flask_appbuilder/filemanager.py +33 -45
  20. flask_appbuilder/filters.py +11 -13
  21. flask_appbuilder/forms.py +31 -35
  22. flask_appbuilder/hooks.py +90 -0
  23. flask_appbuilder/menu.py +35 -10
  24. flask_appbuilder/models/base.py +47 -57
  25. flask_appbuilder/models/decorators.py +13 -13
  26. flask_appbuilder/models/filters.py +42 -38
  27. flask_appbuilder/models/generic/__init__.py +29 -29
  28. flask_appbuilder/models/generic/filters.py +11 -3
  29. flask_appbuilder/models/generic/interface.py +1 -3
  30. flask_appbuilder/models/group.py +37 -39
  31. flask_appbuilder/models/mixins.py +22 -18
  32. flask_appbuilder/models/sqla/__init__.py +19 -72
  33. flask_appbuilder/models/sqla/base.py +24 -0
  34. flask_appbuilder/models/sqla/base_legacy.py +132 -0
  35. flask_appbuilder/models/sqla/filters.py +132 -19
  36. flask_appbuilder/models/sqla/interface.py +390 -276
  37. flask_appbuilder/security/api.py +31 -35
  38. flask_appbuilder/security/decorators.py +181 -83
  39. flask_appbuilder/security/forms.py +20 -31
  40. flask_appbuilder/security/manager.py +715 -489
  41. flask_appbuilder/security/registerviews.py +29 -112
  42. flask_appbuilder/security/schemas.py +43 -0
  43. flask_appbuilder/security/sqla/apis/__init__.py +8 -0
  44. flask_appbuilder/security/sqla/apis/group/__init__.py +1 -0
  45. flask_appbuilder/security/sqla/apis/group/api.py +227 -0
  46. flask_appbuilder/security/sqla/apis/group/schema.py +73 -0
  47. flask_appbuilder/security/sqla/apis/permission/__init__.py +1 -0
  48. flask_appbuilder/security/sqla/apis/permission/api.py +19 -0
  49. flask_appbuilder/security/sqla/apis/permission_view_menu/__init__.py +1 -0
  50. flask_appbuilder/security/sqla/apis/permission_view_menu/api.py +16 -0
  51. flask_appbuilder/security/sqla/apis/role/__init__.py +1 -0
  52. flask_appbuilder/security/sqla/apis/role/api.py +306 -0
  53. flask_appbuilder/security/sqla/apis/role/schema.py +27 -0
  54. flask_appbuilder/security/sqla/apis/user/__init__.py +1 -0
  55. flask_appbuilder/security/sqla/apis/user/api.py +292 -0
  56. flask_appbuilder/security/sqla/apis/user/schema.py +97 -0
  57. flask_appbuilder/security/sqla/apis/user/validator.py +27 -0
  58. flask_appbuilder/security/sqla/apis/view_menu/__init__.py +1 -0
  59. flask_appbuilder/security/sqla/apis/view_menu/api.py +18 -0
  60. flask_appbuilder/security/sqla/manager.py +421 -203
  61. flask_appbuilder/security/sqla/models.py +192 -57
  62. flask_appbuilder/security/utils.py +9 -0
  63. flask_appbuilder/security/views.py +232 -229
  64. flask_appbuilder/static/.DS_Store +0 -0
  65. flask_appbuilder/static/appbuilder/css/ab.css +20 -12
  66. flask_appbuilder/static/appbuilder/css/bootstrap-datepicker/bootstrap-datepicker3.min.css +7 -0
  67. flask_appbuilder/static/appbuilder/css/bootstrap.min.css.map +1 -0
  68. flask_appbuilder/static/appbuilder/css/flags/flags16.css +249 -245
  69. flask_appbuilder/static/appbuilder/css/fontawesome/all.min.css +6 -0
  70. flask_appbuilder/static/appbuilder/css/fontawesome/brands.min.css +6 -0
  71. flask_appbuilder/static/appbuilder/css/fontawesome/fontawesome.min.css +6 -0
  72. flask_appbuilder/static/appbuilder/css/fontawesome/regular.min.css +6 -0
  73. flask_appbuilder/static/appbuilder/css/fontawesome/solid.min.css +6 -0
  74. flask_appbuilder/static/appbuilder/css/fontawesome/svg-with-js.min.css +6 -0
  75. flask_appbuilder/static/appbuilder/css/fontawesome/v4-font-face.min.css +6 -0
  76. flask_appbuilder/static/appbuilder/css/fontawesome/v4-shims.min.css +6 -0
  77. flask_appbuilder/static/appbuilder/css/fontawesome/v5-font-face.min.css +6 -0
  78. flask_appbuilder/static/appbuilder/css/images/flags16.png +0 -0
  79. flask_appbuilder/static/appbuilder/css/select2/select2-bootstrap.min.css +7 -0
  80. flask_appbuilder/static/appbuilder/css/select2/select2.min.css +1 -0
  81. flask_appbuilder/static/appbuilder/css/swagger/swagger-ui.css +3 -0
  82. flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.ttf +0 -0
  83. flask_appbuilder/static/appbuilder/css/webfonts/fa-brands-400.woff2 +0 -0
  84. flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.ttf +0 -0
  85. flask_appbuilder/static/appbuilder/css/webfonts/fa-regular-400.woff2 +0 -0
  86. flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.ttf +0 -0
  87. flask_appbuilder/static/appbuilder/css/webfonts/fa-solid-900.woff2 +0 -0
  88. flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.ttf +0 -0
  89. flask_appbuilder/static/appbuilder/css/webfonts/fa-v4compatibility.woff2 +0 -0
  90. flask_appbuilder/static/appbuilder/js/ab.js +33 -23
  91. flask_appbuilder/static/appbuilder/js/ab_filters.js +91 -84
  92. flask_appbuilder/static/appbuilder/js/bootstrap-datepicker/bootstrap-datepicker.min.js +8 -0
  93. flask_appbuilder/static/appbuilder/js/jquery-latest.js +2 -2
  94. flask_appbuilder/static/appbuilder/js/select2/select2.min.js +2 -0
  95. flask_appbuilder/static/appbuilder/js/swagger-ui-bundle.js +3 -0
  96. flask_appbuilder/templates/appbuilder/baselib.html +9 -3
  97. flask_appbuilder/templates/appbuilder/general/lib.html +60 -34
  98. flask_appbuilder/templates/appbuilder/general/model/edit.html +1 -1
  99. flask_appbuilder/templates/appbuilder/general/model/edit_cascade.html +1 -1
  100. flask_appbuilder/templates/appbuilder/general/model/search.html +3 -2
  101. flask_appbuilder/templates/appbuilder/general/model/show.html +1 -1
  102. flask_appbuilder/templates/appbuilder/general/model/show_cascade.html +1 -1
  103. flask_appbuilder/templates/appbuilder/general/security/login_db.html +7 -7
  104. flask_appbuilder/templates/appbuilder/general/security/login_ldap.html +5 -5
  105. flask_appbuilder/templates/appbuilder/general/security/login_oauth.html +24 -49
  106. flask_appbuilder/templates/appbuilder/general/widgets/base_list.html +2 -1
  107. flask_appbuilder/templates/appbuilder/general/widgets/chart.html +4 -2
  108. flask_appbuilder/templates/appbuilder/general/widgets/direct_chart.html +4 -3
  109. flask_appbuilder/templates/appbuilder/general/widgets/multiple_chart.html +3 -2
  110. flask_appbuilder/templates/appbuilder/general/widgets/search.html +11 -10
  111. flask_appbuilder/templates/appbuilder/init.html +37 -43
  112. flask_appbuilder/templates/appbuilder/navbar_menu.html +1 -1
  113. flask_appbuilder/templates/appbuilder/navbar_right.html +2 -2
  114. flask_appbuilder/templates/appbuilder/swagger/swagger.html +22 -19
  115. flask_appbuilder/translations/de/LC_MESSAGES/messages.mo +0 -0
  116. flask_appbuilder/translations/de/LC_MESSAGES/messages.po +305 -161
  117. flask_appbuilder/translations/fa/LC_MESSAGES/messages.mo +0 -0
  118. flask_appbuilder/translations/fa/LC_MESSAGES/messages.po +802 -0
  119. flask_appbuilder/translations/fr/LC_MESSAGES/messages.po +461 -319
  120. flask_appbuilder/translations/pt_BR/LC_MESSAGES/messages.po +650 -650
  121. flask_appbuilder/translations/ru/LC_MESSAGES/messages.po +1 -1
  122. flask_appbuilder/translations/sl/LC_MESSAGES/messages.mo +0 -0
  123. flask_appbuilder/translations/sl/LC_MESSAGES/messages.po +690 -0
  124. flask_appbuilder/translations/tr/LC_MESSAGES/messages.mo +0 -0
  125. flask_appbuilder/translations/tr/LC_MESSAGES/messages.po +1015 -0
  126. flask_appbuilder/upload.py +20 -22
  127. flask_appbuilder/urltools.py +39 -19
  128. flask_appbuilder/utils/base.py +76 -0
  129. flask_appbuilder/utils/legacy.py +33 -0
  130. flask_appbuilder/utils/limit.py +20 -0
  131. flask_appbuilder/validators.py +73 -14
  132. flask_appbuilder/views.py +75 -424
  133. flask_appbuilder/widgets.py +50 -51
  134. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/METADATA +36 -76
  135. flask_appbuilder-5.0.2rc1.dist-info/RECORD +240 -0
  136. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/WHEEL +1 -1
  137. flask_appbuilder-5.0.2rc1.dist-info/entry_points.txt +2 -0
  138. Flask_AppBuilder-3.2.1rc1.dist-info/RECORD +0 -270
  139. Flask_AppBuilder-3.2.1rc1.dist-info/entry_points.txt +0 -6
  140. flask_appbuilder/console.py +0 -426
  141. flask_appbuilder/models/mongoengine/__init__.py +0 -0
  142. flask_appbuilder/models/mongoengine/fields.py +0 -65
  143. flask_appbuilder/models/mongoengine/filters.py +0 -145
  144. flask_appbuilder/models/mongoengine/interface.py +0 -328
  145. flask_appbuilder/security/mongoengine/__init__.py +0 -0
  146. flask_appbuilder/security/mongoengine/manager.py +0 -402
  147. flask_appbuilder/security/mongoengine/models.py +0 -120
  148. flask_appbuilder/static/appbuilder/css/font-awesome.min.css +0 -4
  149. flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.css +0 -9
  150. flask_appbuilder/static/appbuilder/datepicker/bootstrap-datepicker.js +0 -28
  151. flask_appbuilder/static/appbuilder/fonts/FontAwesome.otf +0 -0
  152. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.eot +0 -0
  153. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.svg +0 -2671
  154. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.ttf +0 -0
  155. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff +0 -0
  156. flask_appbuilder/static/appbuilder/fonts/fontawesome-webfont.woff2 +0 -0
  157. flask_appbuilder/static/appbuilder/img/aol.png +0 -0
  158. flask_appbuilder/static/appbuilder/img/flags/flags16.png +0 -0
  159. flask_appbuilder/static/appbuilder/img/flickr.png +0 -0
  160. flask_appbuilder/static/appbuilder/img/google.png +0 -0
  161. flask_appbuilder/static/appbuilder/img/myopenid.png +0 -0
  162. flask_appbuilder/static/appbuilder/img/yahoo.png +0 -0
  163. flask_appbuilder/static/appbuilder/js/_google_charts.js +0 -39
  164. flask_appbuilder/static/appbuilder/js/html5shiv.js +0 -8
  165. flask_appbuilder/static/appbuilder/js/respond.min.js +0 -6
  166. flask_appbuilder/static/appbuilder/select2/select2-spinner.gif +0 -0
  167. flask_appbuilder/static/appbuilder/select2/select2.css +0 -1205
  168. flask_appbuilder/static/appbuilder/select2/select2.js +0 -23
  169. flask_appbuilder/static/appbuilder/select2/select2.png +0 -0
  170. flask_appbuilder/static/appbuilder/select2/select2x2.png +0 -0
  171. flask_appbuilder/templates/appbuilder/general/security/login_oid.html +0 -129
  172. flask_appbuilder/templates/appbuilder/general/security/resetpassword.html +0 -29
  173. flask_appbuilder/tests/__init__.py +0 -0
  174. flask_appbuilder/tests/__pycache__/__init__.cpython-36.pyc +0 -0
  175. flask_appbuilder/tests/__pycache__/__init__.cpython-37.pyc +0 -0
  176. flask_appbuilder/tests/__pycache__/_test_auth_ldap.cpython-37.pyc +0 -0
  177. flask_appbuilder/tests/__pycache__/_test_auth_oauth.cpython-37.pyc +0 -0
  178. flask_appbuilder/tests/__pycache__/_test_ldapsearch.cpython-36.pyc +0 -0
  179. flask_appbuilder/tests/__pycache__/_test_oauth_registration_role.cpython-36.pyc +0 -0
  180. flask_appbuilder/tests/__pycache__/base.cpython-36.pyc +0 -0
  181. flask_appbuilder/tests/__pycache__/base.cpython-37.pyc +0 -0
  182. flask_appbuilder/tests/__pycache__/config_api.cpython-36.pyc +0 -0
  183. flask_appbuilder/tests/__pycache__/config_api.cpython-37.pyc +0 -0
  184. flask_appbuilder/tests/__pycache__/const.cpython-36.pyc +0 -0
  185. flask_appbuilder/tests/__pycache__/const.cpython-37.pyc +0 -0
  186. flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-36.pyc +0 -0
  187. flask_appbuilder/tests/__pycache__/test_0_fixture.cpython-37.pyc +0 -0
  188. flask_appbuilder/tests/__pycache__/test_api.cpython-36.pyc +0 -0
  189. flask_appbuilder/tests/__pycache__/test_api.cpython-37.pyc +0 -0
  190. flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-36.pyc +0 -0
  191. flask_appbuilder/tests/__pycache__/test_fab_cli.cpython-37.pyc +0 -0
  192. flask_appbuilder/tests/__pycache__/test_menu.cpython-36.pyc +0 -0
  193. flask_appbuilder/tests/__pycache__/test_menu.cpython-37.pyc +0 -0
  194. flask_appbuilder/tests/__pycache__/test_mongoengine.cpython-36.pyc +0 -0
  195. flask_appbuilder/tests/__pycache__/test_mvc.cpython-36.pyc +0 -0
  196. flask_appbuilder/tests/__pycache__/test_mvc.cpython-37.pyc +0 -0
  197. flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-36.pyc +0 -0
  198. flask_appbuilder/tests/__pycache__/test_sqlalchemy.cpython-37.pyc +0 -0
  199. flask_appbuilder/tests/_test_auth_ldap.py +0 -1045
  200. flask_appbuilder/tests/_test_auth_oauth.py +0 -419
  201. flask_appbuilder/tests/_test_ldapsearch.py +0 -135
  202. flask_appbuilder/tests/_test_oauth_registration_role.py +0 -59
  203. flask_appbuilder/tests/app.db +0 -0
  204. flask_appbuilder/tests/base.py +0 -90
  205. flask_appbuilder/tests/config_api.py +0 -21
  206. flask_appbuilder/tests/const.py +0 -9
  207. flask_appbuilder/tests/mongoengine/__init__.py +0 -0
  208. flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-36.pyc +0 -0
  209. flask_appbuilder/tests/mongoengine/__pycache__/__init__.cpython-37.pyc +0 -0
  210. flask_appbuilder/tests/mongoengine/__pycache__/models.cpython-36.pyc +0 -0
  211. flask_appbuilder/tests/mongoengine/models.py +0 -41
  212. flask_appbuilder/tests/sqla/__init__.py +0 -0
  213. flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-36.pyc +0 -0
  214. flask_appbuilder/tests/sqla/__pycache__/__init__.cpython-37.pyc +0 -0
  215. flask_appbuilder/tests/sqla/__pycache__/models.cpython-36.pyc +0 -0
  216. flask_appbuilder/tests/sqla/__pycache__/models.cpython-37.pyc +0 -0
  217. flask_appbuilder/tests/sqla/models.py +0 -340
  218. flask_appbuilder/tests/test_0_fixture.py +0 -39
  219. flask_appbuilder/tests/test_api.py +0 -2790
  220. flask_appbuilder/tests/test_fab_cli.py +0 -72
  221. flask_appbuilder/tests/test_menu.py +0 -122
  222. flask_appbuilder/tests/test_mongoengine.py +0 -572
  223. flask_appbuilder/tests/test_mvc.py +0 -1710
  224. flask_appbuilder/tests/test_sqlalchemy.py +0 -24
  225. flask_appbuilder/translations/__pycache__/__init__.cpython-36.pyc +0 -0
  226. flask_appbuilder/translations/es/LC_MESSAGES/messages.po~ +0 -582
  227. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/LICENSE +0 -0
  228. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2rc1.dist-info}/top_level.txt +0 -0
@@ -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, HTMLString
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 HTMLString(
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 HTMLString(template % args)
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.TextField):
104
+ class FileUploadField(fields.StringField):
106
105
  """
107
- Customizable file-upload field.
106
+ Customizable file-upload field.
108
107
 
109
- Saves file to configured path, handles updates and deletions.
110
- Inherits from `TextField`, resulting filename will be stored as string.
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
- Constructor.
116
+ Constructor.
118
117
 
119
- :param label:
120
- Display label
121
- :param validators:
122
- Validators
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(FileUploadField, self).__init__(label, validators, **kwargs)
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(FileUploadField, self).process(formdata, data)
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
- Image upload field.
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(ImageUploadField, self).__init__(label, validators, **kwargs)
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(ImageUploadField, self).process(formdata, data)
217
+ return super().process(formdata, data, **kwargs)
220
218
 
221
219
  def populate_obj(self, obj, name):
222
220
  field = getattr(obj, name, None)
@@ -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
- Stack data structure will not insert
9
- equal sequential data
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
- Get page arguments for group by
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
- Get page arguments, returns a dictionary
47
- { <VIEW_NAME>: PAGE_NUMBER }
49
+ Get page arguments, returns a dictionary
50
+ { <VIEW_NAME>: PAGE_NUMBER }
48
51
 
49
- Arguments are passed: page_<VIEW_NAME>=<PAGE_NUMBER>
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
- Get page size arguments, returns an int
63
- { <VIEW_NAME>: PAGE_NUMBER }
65
+ Get page size arguments, returns an int
66
+ { <VIEW_NAME>: PAGE_NUMBER }
64
67
 
65
- Arguments are passed: psize_<VIEW_NAME>=<PAGE_SIZE>
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
- Get order arguments, return a dictionary
79
- { <VIEW_NAME>: (ORDER_COL, ORDER_DIRECTION) }
81
+ Get order arguments, return a dictionary
82
+ { <VIEW_NAME>: (ORDER_COL, ORDER_DIRECTION) }
80
83
 
81
- Arguments are passed like: _oc_<VIEW_NAME>=<COL_NAME>&_od_<VIEW_NAME>='asc'|'desc'
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
- for arg in request.args:
97
- re_match = re.findall("_flt_(\d)_(.*)", arg)
98
- if re_match:
99
- filters.add_filter_index(
100
- re_match[0][1], int(re_match[0][0]), request.args.get(arg)
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))
@@ -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
@@ -1,26 +1,46 @@
1
- from wtforms import ValidationError
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
- class Unique(object):
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
- Checks field value unicity against specified table field.
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 = ("unique",)
29
+ field_flags = {"unique": True}
17
30
 
18
- def __init__(self, datamodel, col_name, message=None):
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(u"Already exists.")
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
+ )