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
@@ -5,17 +5,17 @@ from flask_appbuilder._compat import as_unicode
5
5
 
6
6
  def dict_to_json(xcol, ycols, labels, value_columns): # pragma: no cover
7
7
  """
8
- Converts a list of dicts from datamodel query results
9
- to google chart json data.
8
+ Converts a list of dicts from datamodel query results
9
+ to google chart json data.
10
10
 
11
- :param xcol:
12
- The name of a string column to be used has X axis on chart
13
- :param ycols:
14
- A list with the names of series cols, that can be used as numeric
15
- :param labels:
16
- A dict with the columns labels.
17
- :param value_columns:
18
- A list of dicts with the values to convert
11
+ :param xcol:
12
+ The name of a string column to be used has X axis on chart
13
+ :param ycols:
14
+ A list with the names of series cols, that can be used as numeric
15
+ :param labels:
16
+ A dict with the columns labels.
17
+ :param value_columns:
18
+ A list of dicts with the values to convert
19
19
  """
20
20
  json_data = dict()
21
21
 
@@ -15,10 +15,10 @@ log = logging.getLogger(__name__)
15
15
 
16
16
  class BaseChartView(BaseModelView):
17
17
  """
18
- This is the base class for all chart views.
19
- Use DirectByChartView or GroupByChartView, override their properties
20
- and their base classes
21
- (BaseView, BaseModelView, BaseChartView) to customise your charts
18
+ This is the base class for all chart views.
19
+ Use DirectByChartView or GroupByChartView, override their properties
20
+ and their base classes
21
+ (BaseView, BaseModelView, BaseChartView) to customise your charts
22
22
  """
23
23
 
24
24
  chart_template = "appbuilder/general/charts/chart.html"
@@ -60,8 +60,8 @@ class BaseChartView(BaseModelView):
60
60
 
61
61
  def _get_view_widget(self, **kwargs):
62
62
  """
63
- :return:
64
- Returns a widget
63
+ :return:
64
+ Returns a widget
65
65
  """
66
66
  return self._get_chart_widget(**kwargs).get("chart")
67
67
 
@@ -139,7 +139,7 @@ class GroupByChartView(BaseChartView):
139
139
 
140
140
  def get_group_by_class(self, definition):
141
141
  """
142
- intantiates the processing class (Direct or Grouped) and returns it.
142
+ intantiates the processing class (Direct or Grouped) and returns it.
143
143
  """
144
144
  group_by = definition["group"]
145
145
  series = definition["series"]
@@ -160,7 +160,6 @@ class GroupByChartView(BaseChartView):
160
160
  definition="",
161
161
  **args
162
162
  ):
163
-
164
163
  height = height or self.height
165
164
  widgets = widgets or dict()
166
165
  joined_filters = filters.get_joined_filters(self._base_filters)
@@ -221,45 +220,45 @@ class GroupByChartView(BaseChartView):
221
220
 
222
221
  class DirectByChartView(GroupByChartView):
223
222
  """
224
- Use this class to display charts with multiple series,
225
- based on columns or methods defined on models.
226
- You can display multiple charts on the same view.
223
+ Use this class to display charts with multiple series,
224
+ based on columns or methods defined on models.
225
+ You can display multiple charts on the same view.
227
226
 
228
- Default routing point is '/chart'
227
+ Default routing point is '/chart'
229
228
 
230
- Setup definitions property to configure the chart
229
+ Setup definitions property to configure the chart
231
230
 
232
- :label: (optional) String label to display on chart selection.
233
- :group: String with the column name or method from model.
234
- :formatter: (optional) function that formats the output of 'group' key
235
- :series: A list of tuples with the aggregation function and the column name
236
- to apply the aggregation
231
+ :label: (optional) String label to display on chart selection.
232
+ :group: String with the column name or method from model.
233
+ :formatter: (optional) function that formats the output of 'group' key
234
+ :series: A list of tuples with the aggregation function and the column name
235
+ to apply the aggregation
237
236
 
238
- The **definitions** property respects the following grammar::
237
+ The **definitions** property respects the following grammar::
239
238
 
240
- definitions = [
241
- {
242
- 'label': 'label for chart definition',
243
- 'group': '<COLNAME>'|'<MODEL FUNCNAME>',
244
- 'formatter': <FUNC FORMATTER FOR GROUP COL>,
245
- 'series': ['<COLNAME>'|'<MODEL FUNCNAME>',...]
246
- }, ...
247
- ]
239
+ definitions = [
240
+ {
241
+ 'label': 'label for chart definition',
242
+ 'group': '<COLNAME>'|'<MODEL FUNCNAME>',
243
+ 'formatter': <FUNC FORMATTER FOR GROUP COL>,
244
+ 'series': ['<COLNAME>'|'<MODEL FUNCNAME>',...]
245
+ }, ...
246
+ ]
248
247
 
249
- example::
248
+ example::
250
249
 
251
- class CountryDirectChartView(DirectByChartView):
252
- datamodel = SQLAInterface(CountryStats)
253
- chart_title = 'Direct Data Example'
250
+ class CountryDirectChartView(DirectByChartView):
251
+ datamodel = SQLAInterface(CountryStats)
252
+ chart_title = 'Direct Data Example'
254
253
 
255
- definitions = [
256
- {
257
- 'label': 'Unemployment',
258
- 'group': 'stat_date',
259
- 'series': ['unemployed_perc',
260
- 'college_perc']
261
- }
262
- ]
254
+ definitions = [
255
+ {
256
+ 'label': 'Unemployment',
257
+ 'group': 'stat_date',
258
+ 'series': ['unemployed_perc',
259
+ 'college_perc']
260
+ }
261
+ ]
263
262
 
264
263
  """
265
264
 
@@ -293,7 +292,6 @@ class BaseSimpleGroupByChartView(BaseChartView): # pragma: no cover
293
292
  height=None,
294
293
  **args
295
294
  ):
296
-
297
295
  height = height or self.height
298
296
  widgets = widgets or dict()
299
297
  group_by = group_by or self.group_by_columns[0]
@@ -333,8 +331,8 @@ class BaseSimpleDirectChartView(BaseChartView): # pragma: no cover
333
331
 
334
332
  def get_group_by_columns(self):
335
333
  """
336
- returns the keys from direct_columns
337
- Used in template, so that user can choose from options
334
+ returns the keys from direct_columns
335
+ Used in template, so that user can choose from options
338
336
  """
339
337
  return list(self.direct_columns.keys())
340
338
 
@@ -348,7 +346,6 @@ class BaseSimpleDirectChartView(BaseChartView): # pragma: no cover
348
346
  height=None,
349
347
  **args
350
348
  ):
351
-
352
349
  height = height or self.height
353
350
  widgets = widgets or dict()
354
351
  joined_filters = filters.get_joined_filters(self._base_filters)
@@ -377,11 +374,11 @@ class BaseSimpleDirectChartView(BaseChartView): # pragma: no cover
377
374
 
378
375
  class ChartView(BaseSimpleGroupByChartView): # pragma: no cover
379
376
  """
380
- **DEPRECATED**
377
+ **DEPRECATED**
381
378
 
382
- Provides a simple (and hopefully nice) way to draw charts on your application.
379
+ Provides a simple (and hopefully nice) way to draw charts on your application.
383
380
 
384
- This will show Google Charts based on group by of your tables.
381
+ This will show Google Charts based on group by of your tables.
385
382
  """
386
383
 
387
384
  @expose("/chart/<group_by>")
@@ -410,12 +407,12 @@ class ChartView(BaseSimpleGroupByChartView): # pragma: no cover
410
407
 
411
408
  class TimeChartView(BaseSimpleGroupByChartView): # pragma: no cover
412
409
  """
413
- **DEPRECATED**
410
+ **DEPRECATED**
414
411
 
415
- Provides a simple way to draw some time charts on your application.
412
+ Provides a simple way to draw some time charts on your application.
416
413
 
417
- This will show Google Charts based on count and group
418
- by month and year for your tables.
414
+ This will show Google Charts based on count and group
415
+ by month and year for your tables.
419
416
  """
420
417
 
421
418
  chart_template = "appbuilder/general/charts/chart_time.html"
@@ -432,7 +429,6 @@ class TimeChartView(BaseSimpleGroupByChartView): # pragma: no cover
432
429
  height=None,
433
430
  **args
434
431
  ):
435
-
436
432
  height = height or self.height
437
433
  widgets = widgets or dict()
438
434
  group_by = group_by or self.group_by_columns[0]
@@ -487,17 +483,17 @@ class TimeChartView(BaseSimpleGroupByChartView): # pragma: no cover
487
483
 
488
484
  class DirectChartView(BaseSimpleDirectChartView): # pragma: no cover
489
485
  """
490
- **DEPRECATED**
486
+ **DEPRECATED**
491
487
 
492
- This class is responsible for displaying a Google chart with
493
- direct model values. Chart widget uses json.
494
- No group by is processed, example::
488
+ This class is responsible for displaying a Google chart with
489
+ direct model values. Chart widget uses json.
490
+ No group by is processed, example::
495
491
 
496
- class StatsChartView(DirectChartView):
497
- datamodel = SQLAInterface(Stats)
498
- chart_title = lazy_gettext('Statistics')
499
- direct_columns = {'Some Stats': ('X_col_1', 'stat_col_1', 'stat_col_2'),
500
- 'Other Stats': ('X_col2', 'stat_col_3')}
492
+ class StatsChartView(DirectChartView):
493
+ datamodel = SQLAInterface(Stats)
494
+ chart_title = lazy_gettext('Statistics')
495
+ direct_columns = {'Some Stats': ('X_col_1', 'stat_col_1', 'stat_col_2'),
496
+ 'Other Stats': ('X_col2', 'stat_col_3')}
501
497
 
502
498
  """
503
499
 
flask_appbuilder/cli.py CHANGED
@@ -1,35 +1,59 @@
1
+ from __future__ import annotations
2
+
1
3
  from io import BytesIO
2
4
  import os
3
5
  import shutil
6
+ from typing import Any, Optional, Union
4
7
  from urllib.request import urlopen
5
8
  from zipfile import ZipFile
6
9
 
7
10
  import click
8
11
  from flask import current_app
9
12
  from flask.cli import with_appcontext
13
+ import jinja2
10
14
 
11
- from .const import AUTH_DB, AUTH_LDAP, AUTH_OAUTH, AUTH_OID, AUTH_REMOTE_USER
15
+ from .const import AUTH_DB, AUTH_LDAP, AUTH_OAUTH, AUTH_REMOTE_USER
12
16
 
13
17
 
14
18
  SQLA_REPO_URL = (
15
- "https://github.com/dpgaspar/Flask-AppBuilder-Skeleton/archive/master.zip"
16
- )
17
- MONGOENGIE_REPO_URL = (
18
- "https://github.com/dpgaspar/Flask-AppBuilder-Skeleton-me/archive/master.zip"
19
+ "https://github.com/dpgaspar/Flask-AppBuilder-Skeleton/archive/refs/heads/v5.zip"
19
20
  )
20
21
  ADDON_REPO_URL = (
21
22
  "https://github.com/dpgaspar/Flask-AppBuilder-Skeleton-AddOn/archive/master.zip"
22
23
  )
23
24
 
25
+ MIN_SECRET_KEY_SIZE = 20
26
+
24
27
 
25
- def echo_header(title):
28
+ def validate_secret_key(ctx: click.Context, param: click.Option, value: str) -> str:
29
+ if len(value) < MIN_SECRET_KEY_SIZE:
30
+ raise click.BadParameter(f"SECRET_KEY size is less then {MIN_SECRET_KEY_SIZE}")
31
+ return value
32
+
33
+
34
+ def echo_header(title: str) -> None:
26
35
  click.echo(click.style(title, fg="green"))
27
36
  click.echo(click.style("-" * len(title), fg="green"))
28
37
 
29
38
 
39
+ def cast_int_like_to_int(cli_arg: Any) -> Union[None, str, int]:
40
+ """Cast int-like objects to int if possible
41
+
42
+ If the arg cannot be cast to an integer, return the unmodified object instead."""
43
+ try:
44
+ cli_arg_int = int(cli_arg)
45
+ return cli_arg_int
46
+ except TypeError:
47
+ # Don't cast if None
48
+ return cli_arg
49
+ except ValueError:
50
+ # Don't cast non-int-like strings
51
+ return cli_arg
52
+
53
+
30
54
  @click.group()
31
- def fab():
32
- """ FAB flask group commands"""
55
+ def fab() -> None:
56
+ """FAB flask group commands"""
33
57
  pass
34
58
 
35
59
 
@@ -40,13 +64,14 @@ def fab():
40
64
  @click.option("--email", default="admin@fab.org", prompt="Email")
41
65
  @click.password_option()
42
66
  @with_appcontext
43
- def create_admin(username, firstname, lastname, email, password):
67
+ def create_admin(
68
+ username: str, firstname: str, lastname: str, email: str, password: str
69
+ ) -> None:
44
70
  """
45
- Creates an admin user
71
+ Creates an admin user
46
72
  """
47
73
  auth_type = {
48
74
  AUTH_DB: "Database Authentications",
49
- AUTH_OID: "OpenID Authentication",
50
75
  AUTH_LDAP: "LDAP Authentication",
51
76
  AUTH_REMOTE_USER: "WebServer REMOTE_USER Authentication",
52
77
  AUTH_OAUTH: "OAuth Authentication",
@@ -76,7 +101,7 @@ def create_admin(username, firstname, lastname, email, password):
76
101
  if user:
77
102
  click.echo(click.style("Admin User {0} created.".format(username), fg="green"))
78
103
  else:
79
- click.echo(click.style("No user created an error occured", fg="red"))
104
+ click.echo(click.style("No user created an error occurred", fg="red"))
80
105
 
81
106
 
82
107
  @fab.command("create-user")
@@ -87,9 +112,11 @@ def create_admin(username, firstname, lastname, email, password):
87
112
  @click.option("--email", prompt="Email")
88
113
  @click.password_option()
89
114
  @with_appcontext
90
- def create_user(role, username, firstname, lastname, email, password):
115
+ def create_user(
116
+ role: str, username: str, firstname: str, lastname: str, email: str, password: str
117
+ ) -> None:
91
118
  """
92
- Create a user
119
+ Create a user
93
120
  """
94
121
  user = current_app.appbuilder.sm.find_user(username=username)
95
122
  if user:
@@ -121,9 +148,9 @@ def create_user(role, username, firstname, lastname, email, password):
121
148
  )
122
149
  @click.password_option()
123
150
  @with_appcontext
124
- def reset_password(username, password):
151
+ def reset_password(username: str, password: str) -> None:
125
152
  """
126
- Resets a user's password
153
+ Resets a user's password
127
154
  """
128
155
  user = current_app.appbuilder.sm.find_user(username=username)
129
156
  if not user:
@@ -135,22 +162,45 @@ def reset_password(username, password):
135
162
 
136
163
  @fab.command("create-db")
137
164
  @with_appcontext
138
- def create_db():
165
+ def create_db() -> None:
139
166
  """
140
- Create all your database objects (SQLAlchemy specific).
167
+ Create all your database objects (SQLAlchemy specific).
141
168
  """
142
169
  from flask_appbuilder.models.sqla import Model
143
170
 
144
- engine = current_app.appbuilder.get_session.get_bind(mapper=None, clause=None)
171
+ engine = current_app.appbuilder.session.get_bind(mapper=None, clause=None)
145
172
  Model.metadata.create_all(engine)
146
173
  click.echo(click.style("DB objects created", fg="green"))
147
174
 
148
175
 
176
+ @fab.command("export-roles")
177
+ @with_appcontext
178
+ @click.option("--path", "-path", help="Specify filepath to export roles to")
179
+ @click.option("--indent", help="Specify indent of generated JSON file")
180
+ def export_roles(
181
+ path: Optional[str] = None, indent: Optional[Union[int, str]] = None
182
+ ) -> None:
183
+ """Exports roles with permissions and view menus to JSON file"""
184
+ # Cast negative numbers to int (as they are passed as str from CLI)
185
+ cast_indent = cast_int_like_to_int(indent)
186
+ current_app.appbuilder.sm.export_roles(path=path, indent=cast_indent)
187
+
188
+
189
+ @fab.command("import-roles")
190
+ @with_appcontext
191
+ @click.option(
192
+ "--path", "-p", help="Path to a JSON file containing roles", required=True
193
+ )
194
+ def import_roles(path: str) -> None:
195
+ """Imports roles with permissions and view menus from JSON file"""
196
+ current_app.appbuilder.sm.import_roles(path)
197
+
198
+
149
199
  @fab.command("version")
150
200
  @with_appcontext
151
- def version():
201
+ def version() -> None:
152
202
  """
153
- Flask-AppBuilder package version
203
+ Flask-AppBuilder package version
154
204
  """
155
205
  click.echo(
156
206
  click.style(
@@ -163,9 +213,9 @@ def version():
163
213
 
164
214
  @fab.command("security-cleanup")
165
215
  @with_appcontext
166
- def security_cleanup():
216
+ def security_cleanup() -> None:
167
217
  """
168
- Cleanup unused permissions from views and roles.
218
+ Cleanup unused permissions from views and roles.
169
219
  """
170
220
  current_app.appbuilder.security_cleanup()
171
221
  click.echo(click.style("Finished security cleanup", fg="green"))
@@ -176,9 +226,9 @@ def security_cleanup():
176
226
  "--dry-run", "-d", is_flag=True, help="Dry run & print state transitions."
177
227
  )
178
228
  @with_appcontext
179
- def security_converge(dry_run=False):
229
+ def security_converge(dry_run: bool = False) -> None:
180
230
  """
181
- Converges security deletes previous_class_permission_name
231
+ Converges security deletes previous_class_permission_name
182
232
  """
183
233
  state_transitions = current_app.appbuilder.security_converge(dry=dry_run)
184
234
  if dry_run:
@@ -201,9 +251,9 @@ def security_converge(dry_run=False):
201
251
 
202
252
  @fab.command("create-permissions")
203
253
  @with_appcontext
204
- def create_permissions():
254
+ def create_permissions() -> None:
205
255
  """
206
- Creates all permissions and add them to the ADMIN Role.
256
+ Creates all permissions and add them to the ADMIN Role.
207
257
  """
208
258
  current_app.appbuilder.add_permissions(update_perms=True)
209
259
  click.echo(click.style("Created all permissions", fg="green"))
@@ -211,9 +261,9 @@ def create_permissions():
211
261
 
212
262
  @fab.command("list-views")
213
263
  @with_appcontext
214
- def list_views():
264
+ def list_views() -> None:
215
265
  """
216
- List all registered views
266
+ List all registered views
217
267
  """
218
268
  echo_header("List of registered views")
219
269
  for view in current_app.appbuilder.baseviews:
@@ -226,9 +276,9 @@ def list_views():
226
276
 
227
277
  @fab.command("list-users")
228
278
  @with_appcontext
229
- def list_users():
279
+ def list_users() -> None:
230
280
  """
231
- List all users on the database
281
+ List all users on the database
232
282
  """
233
283
  echo_header("List of users")
234
284
  for user in current_app.appbuilder.sm.get_all_users():
@@ -246,43 +296,38 @@ def list_users():
246
296
  help="Your application name, directory will have this name",
247
297
  )
248
298
  @click.option(
249
- "--engine",
250
- prompt="Your engine type, SQLAlchemy or MongoEngine",
251
- type=click.Choice(["SQLAlchemy", "MongoEngine"]),
252
- default="SQLAlchemy",
253
- help="Write your engine type",
299
+ "--secret-key",
300
+ prompt="Your app SECRET_KEY. It should be a long random string. Minimal size is 20",
301
+ callback=validate_secret_key,
302
+ help="This secret key is used by Flask for"
303
+ "securely signing the session cookie and can be used for any other security"
304
+ "related needs by extensions or your application."
305
+ "It should be a long random bytes or str",
254
306
  )
255
- def create_app(name, engine):
307
+ def create_app(name: str, secret_key: str) -> None:
256
308
  """
257
- Create a Skeleton application (needs internet connection to github)
309
+ Create a Skeleton application (needs internet connection to github)
258
310
  """
259
311
  try:
260
- if engine.lower() == "sqlalchemy":
261
- url = urlopen(SQLA_REPO_URL)
262
- dirname = "Flask-AppBuilder-Skeleton-master"
263
- elif engine.lower() == "mongoengine":
264
- url = urlopen(MONGOENGIE_REPO_URL)
265
- dirname = "Flask-AppBuilder-Skeleton-me-master"
312
+ url = urlopen(SQLA_REPO_URL)
313
+ dirname = "Flask-AppBuilder-Skeleton-5"
266
314
  zipfile = ZipFile(BytesIO(url.read()))
267
315
  zipfile.extractall()
268
316
  os.rename(dirname, name)
317
+
318
+ template_filename = os.path.join(os.path.abspath(name), "config.py.tpl")
319
+ config_filename = os.path.join(os.path.abspath(name), "config.py")
320
+ template = jinja2.Template(open(template_filename).read())
321
+ rendered_template = template.render({"secret_key": secret_key})
322
+ with open(config_filename, "w") as fd:
323
+ fd.write(rendered_template)
324
+
269
325
  click.echo(click.style("Downloaded the skeleton app, good coding!", fg="green"))
270
- return True
271
326
  except Exception as e:
272
327
  click.echo(click.style("Something went wrong {0}".format(e), fg="red"))
273
- if engine.lower() == "sqlalchemy":
274
- click.echo(
275
- click.style(
276
- "Try downloading from {0}".format(SQLA_REPO_URL), fg="green"
277
- )
278
- )
279
- elif engine.lower() == "mongoengine":
280
- click.echo(
281
- click.style(
282
- "Try downloading from {0}".format(MONGOENGIE_REPO_URL), fg="green"
283
- )
284
- )
285
- return False
328
+ click.echo(
329
+ click.style("Try downloading from {0}".format(SQLA_REPO_URL), fg="green")
330
+ )
286
331
 
287
332
 
288
333
  @fab.command("create-addon")
@@ -291,9 +336,9 @@ def create_app(name, engine):
291
336
  prompt="Your new addon name",
292
337
  help="Your addon name will be prefixed by fab_addon_, directory will have this name",
293
338
  )
294
- def create_addon(name):
339
+ def create_addon(name: str) -> None:
295
340
  """
296
- Create a Skeleton AddOn (needs internet connection to github)
341
+ Create a Skeleton AddOn (needs internet connection to github)
297
342
  """
298
343
  try:
299
344
  full_name = "fab_addon_" + name
@@ -311,19 +356,17 @@ def create_addon(name):
311
356
  click.echo(
312
357
  click.style("Downloaded the skeleton addon, good coding!", fg="green")
313
358
  )
314
- return True
315
359
  except Exception as e:
316
360
  click.echo(click.style("Something went wrong {0}".format(e), fg="red"))
317
- return False
318
361
 
319
362
 
320
363
  @fab.command("collect-static")
321
364
  @click.option(
322
365
  "--static_folder", default="app/static", help="Your projects static folder"
323
366
  )
324
- def collect_static(static_folder):
367
+ def collect_static(static_folder: str) -> None:
325
368
  """
326
- Copies flask-appbuilder static files to your projects static folder
369
+ Copies flask-appbuilder static files to your projects static folder
327
370
  """
328
371
  appbuilder_static_path = os.path.join(
329
372
  os.path.dirname(os.path.abspath(__file__)), "static/appbuilder"
@@ -357,9 +400,11 @@ def collect_static(static_folder):
357
400
  @click.option(
358
401
  "--keywords", "-k", multiple=True, default=["lazy_gettext", "gettext", "_", "__"]
359
402
  )
360
- def babel_extract(config, input, output, target, keywords):
403
+ def babel_extract(
404
+ config: str, input: str, output: str, target: str, keywords: list[str]
405
+ ) -> None:
361
406
  """
362
- Babel, Extracts and updates all messages marked for translation
407
+ Babel, Extracts and updates all messages marked for translation
363
408
  """
364
409
  click.echo(
365
410
  click.style(
@@ -369,10 +414,10 @@ def babel_extract(config, input, output, target, keywords):
369
414
  fg="green",
370
415
  )
371
416
  )
372
- keywords = " -k ".join(keywords)
417
+ keywords_args = " -k ".join(keywords)
373
418
  os.popen(
374
419
  "pybabel extract -F {0} -k {1} -o {2} {3}".format(
375
- config, keywords, output, input
420
+ config, keywords_args, output, input
376
421
  )
377
422
  )
378
423
  click.echo(click.style("Starting Update target:{0}".format(target), fg="green"))
@@ -386,9 +431,9 @@ def babel_extract(config, input, output, target, keywords):
386
431
  default="app/translations",
387
432
  help="The target directory where translations reside",
388
433
  )
389
- def babel_compile(target):
434
+ def babel_compile(target: str) -> None:
390
435
  """
391
- Babel, Compiles all translations
436
+ Babel, Compiles all translations
392
437
  """
393
438
  click.echo(click.style("Starting Compile target:{0}".format(target), fg="green"))
394
439
  os.popen("pybabel compile -f -d {0}".format(target))