flask-appbuilder 3.2.1rc1__py3-none-any.whl → 5.0.2__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.2.dist-info}/METADATA +36 -76
  135. flask_appbuilder-5.0.2.dist-info/RECORD +240 -0
  136. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2.dist-info}/WHEEL +1 -1
  137. flask_appbuilder-5.0.2.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.2.dist-info}/LICENSE +0 -0
  228. {Flask_AppBuilder-3.2.1rc1.dist-info → flask_appbuilder-5.0.2.dist-info}/top_level.txt +0 -0
@@ -4,18 +4,11 @@ import os.path as op
4
4
  import re
5
5
  import uuid
6
6
 
7
- from flask.globals import _request_ctx_stack
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
- ctx = app_stack.top
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 ctx.app.config:
50
- self.allowed_extensions = ctx.app.config["FILE_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
- Image Manager will manage your image files referenced on SQLAlchemy Model
89
- will save files on IMG_UPLOAD_FOLDER as <uuid>_sep_<filename>
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
- ctx = app_stack.top
112
- if "IMG_SIZE" in ctx.app.config and not max_size:
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 ctx.app.config and not relative_path:
116
- relative_path = ctx.app.config["IMG_UPLOAD_URL"]
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 ctx.app.config and not base_path:
121
- base_path = ctx.app.config["IMG_UPLOAD_FOLDER"]
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
- Saves an image File
152
+ Saves an image File
165
153
 
166
- :param data: FileStorage from Flask form upload field
167
- :param filename: Filename with full path
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
- Resizes the image
195
+ Resizes the image
208
196
 
209
197
  :param image: The image object
210
- :param size: size is PIL tuple (width, heigth, force) ex: (200,100,True)
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.ANTIALIAS)
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.ANTIALIAS)
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
- Use this function to get the user's original filename.
245
- Filename is concatenated with <UUID>_sep_<FILE NAME>, to avoid collisions.
246
- Use this function on your models on an aditional function
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
- class ProjectFiles(Base):
251
- id = Column(Integer, primary_key=True)
252
- file = Column(FileColumn, nullable=False)
238
+ class ProjectFiles(Base):
239
+ id = Column(Integer, primary_key=True)
240
+ file = Column(FileColumn, nullable=False)
253
241
 
254
- def file_name(self):
255
- return get_file_original_name(str(self.file))
242
+ def file_name(self):
243
+ return get_file_original_name(str(self.file))
256
244
 
257
- :param name:
258
- The file name from model
259
- :return:
260
- Returns the user's original filename removes <UUID>_sep_
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:
@@ -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
- Arguments are passed like:
56
- _oc_<VIEW_NAME>=<COL_NAME>&_od_<VIEW_NAME>='asc'|'desc'
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
- Arguments are passed like: page_<VIEW_NAME>=<PAGE_NUMBER>
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
- Check if an item is visible on the template
148
- this changed with permission mapping feature.
149
- This is a best effort to deliver the feature
150
- and not break compatibility
151
-
152
- permission is:
153
- - 'can_' + <METHOD_NAME>: On normal routes
154
- - <METHOD_NAME>: when it's an action
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(object):
41
+ class FieldConverter:
43
42
  """
44
- Helper class that converts model fields into WTForm fields
43
+ Helper class that converts model fields into WTForm fields
45
44
 
46
- it has a conversion table with type method checks from model
47
- interfaces, these methods are invoked with a column name
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" % self.colname)
105
+ log.error("Column %s Type not supported", self.colname)
108
106
 
109
107
 
110
- class GeneralModelConverter(object):
108
+ class GeneralModelConverter:
111
109
  """
112
- Returns a form from a model only one public exposed
113
- method 'create_form'
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
- Creates a WTForm field for many to one related fields,
157
- will use a Select box based on a query. Will only
158
- work with SQLAlchemy interface.
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 {0} not supported".format(col_name))
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
- Converts a model to a form given
274
+ Converts a model to a form given
279
275
 
280
- :param label_columns:
281
- A dictionary with the column's labels.
282
- :param inc_columns:
283
- A list with the columns to include
284
- :param description_columns:
285
- A dictionary with a description for cols.
286
- :param validators_columns:
287
- A dictionary with WTForms validators ex::
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
- validators={'personal_email':EmailValidator}
285
+ validators={'personal_email':EmailValidator}
290
286
 
291
- :param extra_fields:
292
- A dictionary containing column names and a WTForm
293
- Form fields to be added to the form, these fields do not
294
- exist on the model itself ex::
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
- extra_fields={'some_col':BooleanField('Some Col', default=False)}
292
+ extra_fields={'some_col':BooleanField('Some Col', default=False)}
297
293
 
298
- :param filter_rel_fields:
299
- A filter to be applied on relationships
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
- Refresh method will force select field to refresh
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__(self, name, href="", icon="", label="", childs=None, baseview=None):
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
- Finds a menu item by name and returns it.
103
+ Finds a menu item by name and returns it.
95
104
 
96
- :param name:
97
- The menu item name.
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, href=href, icon=icon, label=label, baseview=baseview
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, href=href, icon=icon, label=label, baseview=baseview
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, href=href, icon=icon, label=label, baseview=baseview
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 self.appbuilder.app.config.get("FAB_ADD_MENU_API", True):
241
+ if current_app.config.get("FAB_ADD_MENU_API", True):
217
242
  self.appbuilder.add_api(MenuApi)