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
@@ -0,0 +1,306 @@
1
+ from flask import current_app, request
2
+ from flask_appbuilder import ModelRestApi
3
+ from flask_appbuilder.api import expose, safe
4
+ from flask_appbuilder.const import API_RESULT_RES_KEY
5
+ from flask_appbuilder.models.sqla.interface import SQLAInterface
6
+ from flask_appbuilder.security.decorators import permission_name, protect
7
+ from flask_appbuilder.security.sqla.apis.role.schema import (
8
+ RoleGroupPutSchema,
9
+ RolePermissionListSchema,
10
+ RolePermissionPostSchema,
11
+ RoleUserPutSchema,
12
+ )
13
+ from flask_appbuilder.security.sqla.models import Group, PermissionView, Role, User
14
+ from marshmallow import ValidationError
15
+ from sqlalchemy.exc import IntegrityError
16
+
17
+
18
+ class RoleApi(ModelRestApi):
19
+ resource_name = "security/roles"
20
+ openapi_spec_tag = "Security Roles"
21
+ class_permission_name = "Role"
22
+ datamodel = SQLAInterface(Role)
23
+ allow_browser_login = True
24
+
25
+ list_columns = ["id", "name"]
26
+ show_columns = list_columns
27
+ add_columns = ["name"]
28
+ edit_columns = ["name"]
29
+ search_columns = list_columns
30
+
31
+ update_role_group_schema = RoleGroupPutSchema()
32
+ list_role_permission_schema = RolePermissionListSchema()
33
+ add_role_permission_schema = RolePermissionPostSchema()
34
+ update_role_user_schema = RoleUserPutSchema()
35
+ openapi_spec_component_schemas = (
36
+ RolePermissionListSchema,
37
+ RolePermissionPostSchema,
38
+ RoleUserPutSchema,
39
+ RoleGroupPutSchema,
40
+ )
41
+
42
+ @expose("/<int:role_id>/permissions/", methods=["GET"])
43
+ @protect()
44
+ @safe
45
+ @permission_name("list_role_permissions")
46
+ def list_role_permissions(self, role_id):
47
+ """list role permissions
48
+ ---
49
+ get:
50
+ parameters:
51
+ - in: path
52
+ schema:
53
+ type: integer
54
+ name: role_id
55
+ responses:
56
+ 200:
57
+ description: List of permissions
58
+ content:
59
+ application/json:
60
+ schema:
61
+ type: object
62
+ properties:
63
+ result:
64
+ items:
65
+ $ref: '#/components/schemas/RolePermissionListSchema'
66
+ type: array
67
+ 400:
68
+ $ref: '#/components/responses/400'
69
+ 401:
70
+ $ref: '#/components/responses/401'
71
+ 404:
72
+ $ref: '#/components/responses/404'
73
+ 422:
74
+ $ref: '#/components/responses/422'
75
+ 500:
76
+ $ref: '#/components/responses/500'
77
+ """
78
+ role = self.datamodel.get(role_id, select_columns=["permissions"])
79
+ if not role:
80
+ return self.response_404()
81
+
82
+ permissions = [
83
+ {
84
+ "id": p.id,
85
+ "permission_name": p.permission.name,
86
+ "view_menu_name": p.view_menu.name,
87
+ }
88
+ for p in role.permissions
89
+ ]
90
+ return self.response(200, **{API_RESULT_RES_KEY: permissions})
91
+
92
+ @expose("/<int:role_id>/permissions", methods=["POST"])
93
+ @protect()
94
+ @safe
95
+ @permission_name("add_role_permissions")
96
+ def add_role_permissions(self, role_id):
97
+ """add role permissions
98
+ ---
99
+ post:
100
+ parameters:
101
+ - in: path
102
+ schema:
103
+ type: integer
104
+ name: role_id
105
+ requestBody:
106
+ description: Add role permissions schema
107
+ required: true
108
+ content:
109
+ application/json:
110
+ schema:
111
+ $ref: '#/components/schemas/RolePermissionPostSchema'
112
+ responses:
113
+ 200:
114
+ description: Permissions added
115
+ content:
116
+ application/json:
117
+ schema:
118
+ type: object
119
+ properties:
120
+ result:
121
+ $ref: '#/components/schemas/RolePermissionPostSchema'
122
+ 400:
123
+ $ref: '#/components/responses/400'
124
+ 401:
125
+ $ref: '#/components/responses/401'
126
+ 404:
127
+ $ref: '#/components/responses/404'
128
+ 422:
129
+ $ref: '#/components/responses/422'
130
+ 500:
131
+ $ref: '#/components/responses/500'
132
+ """
133
+ try:
134
+ item = self.add_role_permission_schema.load(request.json)
135
+ role = self.datamodel.get(role_id)
136
+ if not role:
137
+ return self.response_404()
138
+ permissions = []
139
+ for id in item["permission_view_menu_ids"]:
140
+ permission = (
141
+ current_app.appbuilder.session.query(PermissionView)
142
+ .filter_by(id=id)
143
+ .one_or_none()
144
+ )
145
+ if permission:
146
+ permissions.append(permission)
147
+
148
+ role.permissions = permissions
149
+ self.datamodel.edit(role)
150
+ return self.response(
151
+ 200,
152
+ **{
153
+ API_RESULT_RES_KEY: self.add_role_permission_schema.dump(
154
+ item, many=False
155
+ )
156
+ },
157
+ )
158
+
159
+ except ValidationError as error:
160
+ return self.response_400(message=error.messages)
161
+ except IntegrityError as e:
162
+ return self.response_422(message=str(e.orig))
163
+
164
+ @expose("/<int:role_id>/users", methods=["PUT"])
165
+ @protect()
166
+ @safe
167
+ @permission_name("update_role_users")
168
+ def update_role_users(self, role_id):
169
+ """update role users
170
+ ---
171
+ put:
172
+ parameters:
173
+ - in: path
174
+ schema:
175
+ type: integer
176
+ name: role_id
177
+ requestBody:
178
+ description: Update role users schema
179
+ required: true
180
+ content:
181
+ application/json:
182
+ schema:
183
+ $ref: '#/components/schemas/RoleUserPutSchema'
184
+ responses:
185
+ 200:
186
+ description: Role users updated
187
+ content:
188
+ application/json:
189
+ schema:
190
+ type: object
191
+ properties:
192
+ result:
193
+ $ref: '#/components/schemas/RoleUserPutSchema'
194
+ 400:
195
+ $ref: '#/components/responses/400'
196
+ 401:
197
+ $ref: '#/components/responses/401'
198
+ 404:
199
+ $ref: '#/components/responses/404'
200
+ 422:
201
+ $ref: '#/components/responses/422'
202
+ 500:
203
+ $ref: '#/components/responses/500'
204
+ """
205
+ try:
206
+ item = self.update_role_user_schema.load(request.json)
207
+ role = self.datamodel.get(role_id)
208
+ if not role:
209
+ return self.response_404()
210
+
211
+ users = (
212
+ current_app.appbuilder.session.query(User)
213
+ .filter(User.id.in_(item["user_ids"]))
214
+ .all()
215
+ )
216
+
217
+ if len(users) != len(item["user_ids"]):
218
+ return self.response_404() # Some users were not found
219
+
220
+ role.user = users
221
+ self.datamodel.edit(role)
222
+ return self.response(
223
+ 200,
224
+ **{
225
+ API_RESULT_RES_KEY: self.update_role_user_schema.dump(
226
+ item, many=False
227
+ )
228
+ },
229
+ )
230
+
231
+ except ValidationError as error:
232
+ return self.response_400(message=error.messages)
233
+ except IntegrityError as e:
234
+ return self.response_422(message=str(e.orig))
235
+
236
+ @expose("/<int:role_id>/groups", methods=["PUT"])
237
+ @protect()
238
+ @safe
239
+ @permission_name("update_role_groups")
240
+ def update_role_groups(self, role_id):
241
+ """Update role groups
242
+ ---
243
+ put:
244
+ parameters:
245
+ - in: path
246
+ schema:
247
+ type: integer
248
+ name: role_id
249
+ requestBody:
250
+ description: Update role groups schema
251
+ required: true
252
+ content:
253
+ application/json:
254
+ schema:
255
+ $ref: '#/components/schemas/RoleGroupPutSchema'
256
+ responses:
257
+ 200:
258
+ description: Role groups updated
259
+ content:
260
+ application/json:
261
+ schema:
262
+ type: object
263
+ properties:
264
+ result:
265
+ $ref: '#/components/schemas/RoleGroupPutSchema'
266
+ 400:
267
+ $ref: '#/components/responses/400'
268
+ 401:
269
+ $ref: '#/components/responses/401'
270
+ 404:
271
+ $ref: '#/components/responses/404'
272
+ 422:
273
+ $ref: '#/components/responses/422'
274
+ 500:
275
+ $ref: '#/components/responses/500'
276
+ """
277
+ try:
278
+ item = self.update_role_group_schema.load(request.json)
279
+ role = self.datamodel.get(role_id)
280
+ if not role:
281
+ return self.response_404()
282
+
283
+ groups = (
284
+ current_app.appbuilder.session.query(Group)
285
+ .filter(Group.id.in_(item["group_ids"]))
286
+ .all()
287
+ )
288
+
289
+ if len(groups) != len(item["group_ids"]):
290
+ return self.response_404() # Some groups were not found
291
+
292
+ role.groups = groups
293
+ self.datamodel.edit(role)
294
+ return self.response(
295
+ 200,
296
+ **{
297
+ API_RESULT_RES_KEY: self.update_role_group_schema.dump(
298
+ item, many=False
299
+ )
300
+ },
301
+ )
302
+
303
+ except ValidationError as error:
304
+ return self.response_400(message=error.messages)
305
+ except IntegrityError as e:
306
+ return self.response_422(message=str(e.orig))
@@ -0,0 +1,27 @@
1
+ from marshmallow import fields, Schema
2
+
3
+
4
+ class RolePermissionPostSchema(Schema):
5
+ permission_view_menu_ids = fields.List(
6
+ fields.Integer,
7
+ required=True,
8
+ metadata={"description": "List of permission view menu id"},
9
+ )
10
+
11
+
12
+ class RolePermissionListSchema(Schema):
13
+ id = fields.Integer()
14
+ permission_name = fields.String()
15
+ view_menu_name = fields.String()
16
+
17
+
18
+ class RoleUserPutSchema(Schema):
19
+ user_ids = fields.List(
20
+ fields.Integer, required=True, metadata={"description": "List of user ids"}
21
+ )
22
+
23
+
24
+ class RoleGroupPutSchema(Schema):
25
+ group_ids = fields.List(
26
+ fields.Integer, required=True, metadata={"description": "List of group ids"}
27
+ )
@@ -0,0 +1 @@
1
+ from .api import UserApi # noqa: F401
@@ -0,0 +1,292 @@
1
+ from datetime import datetime
2
+
3
+ from flask import current_app, request
4
+ from flask_appbuilder import ModelRestApi
5
+ from flask_appbuilder.api import expose, safe
6
+ from flask_appbuilder.const import API_RESULT_RES_KEY
7
+ from flask_appbuilder.models.sqla.interface import SQLAInterface
8
+ from flask_appbuilder.security.decorators import permission_name, protect
9
+ from flask_appbuilder.security.sqla.apis.user.schema import (
10
+ UserPostSchema,
11
+ UserPutSchema,
12
+ )
13
+ from flask_appbuilder.security.sqla.models import Group, Role, User
14
+ from marshmallow import ValidationError
15
+ from sqlalchemy.exc import IntegrityError
16
+ from werkzeug.security import generate_password_hash
17
+
18
+
19
+ class UserApi(ModelRestApi):
20
+ resource_name = "security/users"
21
+ openapi_spec_tag = "Security Users"
22
+ class_permission_name = "User"
23
+ datamodel = SQLAInterface(User)
24
+ allow_browser_login = True
25
+
26
+ list_columns = [
27
+ "id",
28
+ "roles.id",
29
+ "roles.name",
30
+ "first_name",
31
+ "last_name",
32
+ "username",
33
+ "active",
34
+ "email",
35
+ "last_login",
36
+ "login_count",
37
+ "fail_login_count",
38
+ "created_on",
39
+ "changed_on",
40
+ "created_by.id",
41
+ "changed_by.id",
42
+ "groups",
43
+ ]
44
+ show_columns = list_columns
45
+ add_columns = [
46
+ "roles",
47
+ "first_name",
48
+ "last_name",
49
+ "username",
50
+ "active",
51
+ "email",
52
+ "password",
53
+ "groups",
54
+ ]
55
+ edit_columns = add_columns
56
+ search_columns = [
57
+ "username",
58
+ "first_name",
59
+ "last_name",
60
+ "active",
61
+ "email",
62
+ "created_by",
63
+ "changed_by",
64
+ "roles",
65
+ "groups",
66
+ ]
67
+
68
+ add_model_schema = UserPostSchema()
69
+ edit_model_schema = UserPutSchema()
70
+
71
+ def pre_update(self, item, data):
72
+ item.changed_on = datetime.now()
73
+ item.changed_by_fk = self.appbuilder.sm.current_user.id
74
+ if "password" in data and data["password"]:
75
+ item.password = generate_password_hash(
76
+ password=data["password"],
77
+ method=current_app.config.get("FAB_PASSWORD_HASH_METHOD", "scrypt"),
78
+ salt_length=current_app.config.get("FAB_PASSWORD_HASH_SALT_LENGTH", 16),
79
+ )
80
+
81
+ def pre_add(self, item):
82
+ item.password = generate_password_hash(
83
+ password=item.password,
84
+ method=current_app.config.get("FAB_PASSWORD_HASH_METHOD", "scrypt"),
85
+ salt_length=current_app.config.get("FAB_PASSWORD_HASH_SALT_LENGTH", 16),
86
+ )
87
+
88
+ @expose("/", methods=["POST"])
89
+ @protect()
90
+ @safe
91
+ @permission_name("post")
92
+ def post(self):
93
+ """Create new user
94
+ ---
95
+ post:
96
+ requestBody:
97
+ description: Model schema
98
+ required: true
99
+ content:
100
+ application/json:
101
+ schema:
102
+ $ref: '#/components/schemas/{{self.__class__.__name__}}.post'
103
+ responses:
104
+ 201:
105
+ description: Item changed
106
+ content:
107
+ application/json:
108
+ schema:
109
+ type: object
110
+ properties:
111
+ result:
112
+ $ref: '#/components/schemas/{{self.__class__.__name__}}.post'
113
+ 400:
114
+ $ref: '#/components/responses/400'
115
+ 401:
116
+ $ref: '#/components/responses/401'
117
+ 404:
118
+ $ref: '#/components/responses/404'
119
+ 422:
120
+ $ref: '#/components/responses/422'
121
+ 500:
122
+ $ref: '#/components/responses/500'
123
+ """
124
+ try:
125
+ item = self.add_model_schema.load(request.json)
126
+ model = User()
127
+ roles = []
128
+ groups = []
129
+ for key, value in item.items():
130
+ if key not in ("roles", "groups"):
131
+ setattr(model, key, value)
132
+ elif key == "roles":
133
+ roles = self._fetch_entities(Role, value)
134
+ missing_ids = set(value) - {r.id for r in roles}
135
+ if missing_ids:
136
+ return self.response_400(
137
+ message={
138
+ "roles": [
139
+ (
140
+ f"Role(s) with ID(s) {sorted(missing_ids)} "
141
+ "not found."
142
+ )
143
+ ]
144
+ }
145
+ )
146
+ elif key == "groups":
147
+ groups = self._fetch_entities(Group, value)
148
+ missing_ids = set(value) - {g.id for g in groups}
149
+ if missing_ids:
150
+ return self.response_400(
151
+ message={
152
+ "groups": [
153
+ (
154
+ f"Group(s) with ID(s) {sorted(missing_ids)} "
155
+ "not found."
156
+ )
157
+ ]
158
+ }
159
+ )
160
+
161
+ if "roles" in item.keys():
162
+ model.roles = roles
163
+ if "groups" in item.keys():
164
+ model.groups = groups
165
+
166
+ self.pre_add(model)
167
+ self.datamodel.add(model)
168
+ return self.response(201, id=model.id)
169
+ except ValidationError as error:
170
+ return self.response_400(message=error.messages)
171
+ except IntegrityError as e:
172
+ return self.response_422(message=str(e.orig))
173
+
174
+ @expose("/<pk>", methods=("PUT",))
175
+ @protect()
176
+ @safe
177
+ @permission_name("put")
178
+ def put(self, pk):
179
+ """Edit user
180
+ ---
181
+ put:
182
+ parameters:
183
+ - in: path
184
+ schema:
185
+ type: integer
186
+ name: pk
187
+ requestBody:
188
+ description: Model schema
189
+ required: true
190
+ content:
191
+ application/json:
192
+ schema:
193
+ $ref: '#/components/schemas/{{self.__class__.__name__}}.put'
194
+ responses:
195
+ 200:
196
+ description: Item changed
197
+ content:
198
+ application/json:
199
+ schema:
200
+ type: object
201
+ properties:
202
+ result:
203
+ $ref: '#/components/schemas/{{self.__class__.__name__}}.put'
204
+ 400:
205
+ $ref: '#/components/responses/400'
206
+ 401:
207
+ $ref: '#/components/responses/401'
208
+ 404:
209
+ $ref: '#/components/responses/404'
210
+ 422:
211
+ $ref: '#/components/responses/422'
212
+ 500:
213
+ $ref: '#/components/responses/500'
214
+ """
215
+ try:
216
+ item = self.edit_model_schema.load(request.json)
217
+ model = self.datamodel.get(pk, self._base_filters)
218
+ roles = []
219
+ groups = []
220
+
221
+ item_roles = item.get("roles")
222
+ item_groups = item.get("groups")
223
+
224
+ if item_roles == [] and item_groups == []:
225
+ return self.response_400(
226
+ message="User must have at least one role or group!"
227
+ )
228
+
229
+ if item_roles == [] and (item_groups is None and not model.groups):
230
+ return self.response_400(
231
+ message=(
232
+ "Cannot clear all roles unless at least one group is \
233
+ assigned!"
234
+ )
235
+ )
236
+
237
+ if item_groups == [] and (item_roles is None and not model.roles):
238
+ return self.response_400(
239
+ message=(
240
+ "Cannot clear all groups unless at least one role is \
241
+ assigned!"
242
+ )
243
+ )
244
+
245
+ for key, value in item.items():
246
+ if key not in ("roles", "groups"):
247
+ setattr(model, key, value)
248
+ elif key == "roles":
249
+ roles = self._fetch_entities(Role, value)
250
+ missing_ids = set(value) - {r.id for r in roles}
251
+ if missing_ids:
252
+ return self.response_404(
253
+ message={
254
+ "roles": [
255
+ (
256
+ f"Role(s) with ID(s) {sorted(missing_ids)} "
257
+ "not found."
258
+ )
259
+ ]
260
+ }
261
+ )
262
+ elif key == "groups":
263
+ groups = self._fetch_entities(Group, value)
264
+ missing_ids = set(value) - {g.id for g in groups}
265
+ if missing_ids:
266
+ return self.response_404(
267
+ message={
268
+ "groups": [
269
+ (
270
+ f"Group(s) with ID(s) {sorted(missing_ids)} "
271
+ "not found."
272
+ )
273
+ ]
274
+ }
275
+ )
276
+
277
+ if "roles" in item.keys():
278
+ model.roles = roles
279
+ if "groups" in item.keys():
280
+ model.groups = groups
281
+
282
+ self.pre_update(model, item)
283
+ self.datamodel.edit(model)
284
+ return self.response(
285
+ 200,
286
+ **{API_RESULT_RES_KEY: self.edit_model_schema.dump(item, many=False)},
287
+ )
288
+
289
+ except ValidationError as e:
290
+ return self.response_400(message=e.messages)
291
+ except IntegrityError as e:
292
+ return self.response_422(message=str(e.orig))