flask-appbuilder 3.2.1__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.1.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.1.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.1.dist-info/RECORD +0 -270
  139. Flask_AppBuilder-3.2.1.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.1.dist-info → flask_appbuilder-5.0.2.dist-info}/LICENSE +0 -0
  228. {Flask_AppBuilder-3.2.1.dist-info → flask_appbuilder-5.0.2.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ from inspect import isclass
3
3
  import json
4
4
  import logging
5
5
  import re
6
+ from typing import List, Optional, TYPE_CHECKING
6
7
 
7
8
  from flask import (
8
9
  abort,
@@ -14,31 +15,39 @@ from flask import (
14
15
  session,
15
16
  url_for,
16
17
  )
17
-
18
- from ._compat import as_unicode
19
- from .actions import ActionItem
20
- from .const import PERMISSION_PREFIX
21
- from .forms import GeneralModelConverter
22
- from .urltools import (
18
+ from flask_appbuilder._compat import as_unicode
19
+ from flask_appbuilder.actions import ActionItem
20
+ from flask_appbuilder.const import PERMISSION_PREFIX
21
+ from flask_appbuilder.forms import GeneralModelConverter
22
+ from flask_appbuilder.hooks import (
23
+ get_before_request_hooks,
24
+ wrap_route_handler_with_hooks,
25
+ )
26
+ from flask_appbuilder.urltools import (
23
27
  get_filter_args,
24
28
  get_order_args,
25
29
  get_page_args,
26
30
  get_page_size_args,
27
31
  Stack,
28
32
  )
29
- from .widgets import FormWidget, ListWidget, SearchWidget, ShowWidget
33
+ from flask_appbuilder.widgets import FormWidget, ListWidget, SearchWidget, ShowWidget
34
+ from flask_babel import lazy_gettext
35
+
36
+ if TYPE_CHECKING:
37
+ from flask_appbuilder.base import AppBuilder
38
+
30
39
 
31
40
  log = logging.getLogger(__name__)
32
41
 
33
42
 
34
43
  def expose(url="/", methods=("GET",)):
35
44
  """
36
- Use this decorator to expose views on your view classes.
45
+ Use this decorator to expose views on your view classes.
37
46
 
38
- :param url:
39
- Relative URL for the view
40
- :param methods:
41
- Allowed HTTP methods. By default only GET is allowed.
47
+ :param url:
48
+ Relative URL for the view
49
+ :param methods:
50
+ Allowed HTTP methods. By default only GET is allowed.
42
51
  """
43
52
 
44
53
  def wrap(f):
@@ -64,14 +73,43 @@ def expose_api(name="", url="", methods=("GET",), description=""):
64
73
  return wrap
65
74
 
66
75
 
67
- class BaseView(object):
76
+ class AbstractViewApi:
77
+ appbuilder: "AppBuilder"
78
+ base_permissions: Optional[List[str]]
79
+ class_permission_name: Optional[str]
80
+ endpoint: Optional[str]
81
+ default_view: str
82
+
83
+ def create_blueprint(
84
+ self,
85
+ appbuilder: "AppBuilder",
86
+ endpoint: Optional[str] = None,
87
+ static_folder: Optional[str] = None,
88
+ ):
89
+ ...
90
+
91
+ def get_uninit_inner_views(self):
92
+ """
93
+ Will return a list with views that need to be initialized.
94
+ Normally related_views from ModelView
95
+ """
96
+ ...
97
+
98
+ def get_init_inner_views(self):
99
+ """
100
+ Sets initialized inner views
101
+ """
102
+ ...
103
+
104
+
105
+ class BaseView(AbstractViewApi):
68
106
  """
69
- All views inherit from this class.
70
- it's constructor will register your exposed urls on flask as a Blueprint.
107
+ All views inherit from this class.
108
+ it's constructor will register your exposed urls on flask as a Blueprint.
71
109
 
72
- This class does not expose any urls, but provides a common base for all views.
110
+ This class does not expose any urls, but provides a common base for all views.
73
111
 
74
- Extend this class if you want to expose methods for your own templates
112
+ Extend this class if you want to expose methods for your own templates
75
113
  """
76
114
 
77
115
  appbuilder = None
@@ -145,16 +183,28 @@ class BaseView(object):
145
183
  default_view = "list"
146
184
  """ the default view for this BaseView, to be used with url_for (method name) """
147
185
  extra_args = None
148
-
149
186
  """ dictionary for injecting extra arguments into template """
187
+
188
+ limits = None
189
+ """
190
+ List of limits for this view.
191
+
192
+ Use it like this if you want to restrict the rate of requests to a view:
193
+
194
+ class MyView(ModelView):
195
+ limits = [Limit("2 per 5 second")]
196
+
197
+ or use the decorator @limit.
198
+ """
199
+
150
200
  _apis = None
151
201
 
152
202
  def __init__(self):
153
203
  """
154
- Initialization of base permissions
155
- based on exposed methods and actions
204
+ Initialization of base permissions
205
+ based on exposed methods and actions
156
206
 
157
- Initialization of extra args
207
+ Initialization of extra args
158
208
  """
159
209
  # Init class permission override attrs
160
210
  if not self.previous_class_permission_name and self.class_permission_name:
@@ -176,6 +226,9 @@ class BaseView(object):
176
226
  self.base_permissions = set()
177
227
  is_add_base_permissions = True
178
228
 
229
+ if self.limits is None:
230
+ self.limits = []
231
+
179
232
  for attr_name in dir(self):
180
233
  # If include_route_methods is not None white list
181
234
  if (
@@ -203,19 +256,21 @@ class BaseView(object):
203
256
  _extra = getattr(getattr(self, attr_name), "_extra")
204
257
  for key in _extra:
205
258
  self._apis[key] = _extra[key]
259
+ if hasattr(getattr(self, attr_name), "_limit"):
260
+ self.limits.append(getattr(getattr(self, attr_name), "_limit"))
206
261
 
207
262
  def create_blueprint(self, appbuilder, endpoint=None, static_folder=None):
208
263
  """
209
- Create Flask blueprint. You will generally not use it
264
+ Create Flask blueprint. You will generally not use it
210
265
 
211
- :param appbuilder:
212
- the AppBuilder object
213
- :param endpoint:
214
- endpoint override for this blueprint,
215
- will assume class name if not provided
216
- :param static_folder:
217
- the relative override for static folder,
218
- if omitted application will use the appbuilder static
266
+ :param appbuilder:
267
+ the AppBuilder object
268
+ :param endpoint:
269
+ endpoint override for this blueprint,
270
+ will assume class name if not provided
271
+ :param static_folder:
272
+ the relative override for static folder,
273
+ if omitted application will use the appbuilder static
219
274
  """
220
275
  # Store appbuilder instance
221
276
  self.appbuilder = appbuilder
@@ -247,6 +302,7 @@ class BaseView(object):
247
302
  return self.blueprint
248
303
 
249
304
  def _register_urls(self):
305
+ before_request_hooks = get_before_request_hooks(self)
250
306
  for attr_name in dir(self):
251
307
  if (
252
308
  self.include_route_methods is not None
@@ -254,59 +310,69 @@ class BaseView(object):
254
310
  ):
255
311
  continue
256
312
  if attr_name in self.exclude_route_methods:
257
- log.info(
258
- f"Not registering route for method "
259
- f"{self.__class__.__name__}.{attr_name}"
313
+ log.debug(
314
+ "Not registering route for method %s.%s",
315
+ self.__class__.__name__,
316
+ attr_name,
260
317
  )
261
318
  continue
262
319
  attr = getattr(self, attr_name)
263
320
  if hasattr(attr, "_urls"):
264
321
  for url, methods in attr._urls:
265
- log.info(
266
- f"Registering route {self.blueprint.url_prefix}{url} {methods}"
322
+ log.debug(
323
+ "Registering route %s%s %s",
324
+ self.blueprint.url_prefix,
325
+ url,
326
+ methods,
327
+ )
328
+ route_handler = wrap_route_handler_with_hooks(
329
+ attr_name, attr, before_request_hooks
330
+ )
331
+ self.blueprint.add_url_rule(
332
+ url, attr_name, route_handler, methods=methods
267
333
  )
268
- self.blueprint.add_url_rule(url, attr_name, attr, methods=methods)
269
334
 
270
335
  def render_template(self, template, **kwargs):
271
336
  """
272
- Use this method on your own endpoints, will pass the extra_args
273
- to the templates.
337
+ Use this method on your own endpoints, will pass the extra_args
338
+ to the templates.
274
339
 
275
- :param template: The template relative path
276
- :param kwargs: arguments to be passed to the template
340
+ :param template: The template relative path
341
+ :param kwargs: arguments to be passed to the template
277
342
  """
278
343
  kwargs["base_template"] = self.appbuilder.base_template
279
344
  kwargs["appbuilder"] = self.appbuilder
345
+ kwargs["current_app"] = current_app
280
346
  return render_template(
281
347
  template, **dict(list(kwargs.items()) + list(self.extra_args.items()))
282
348
  )
283
349
 
284
350
  def _prettify_name(self, name):
285
351
  """
286
- Prettify pythonic variable name.
352
+ Prettify pythonic variable name.
287
353
 
288
- For example, 'HelloWorld' will be converted to 'Hello World'
354
+ For example, 'HelloWorld' will be converted to 'Hello World'
289
355
 
290
- :param name:
291
- Name to prettify.
356
+ :param name:
357
+ Name to prettify.
292
358
  """
293
359
  return re.sub(r"(?<=.)([A-Z])", r" \1", name)
294
360
 
295
361
  def _prettify_column(self, name):
296
362
  """
297
- Prettify pythonic variable name.
363
+ Prettify pythonic variable name.
298
364
 
299
- For example, 'hello_world' will be converted to 'Hello World'
365
+ For example, 'hello_world' will be converted to 'Hello World'
300
366
 
301
- :param name:
302
- Name to prettify.
367
+ :param name:
368
+ Name to prettify.
303
369
  """
304
370
  return re.sub("[._]", " ", name).title()
305
371
 
306
372
  def update_redirect(self):
307
373
  """
308
- Call it on your own endpoint's to update the back history navigation.
309
- If you bypass it, the next submit or back will go over it.
374
+ Call it on your own endpoint's to update the back history navigation.
375
+ If you bypass it, the next submit or back will go over it.
310
376
  """
311
377
  page_history = Stack(session.get("page_history", []))
312
378
  page_history.push(request.url)
@@ -314,7 +380,7 @@ class BaseView(object):
314
380
 
315
381
  def get_redirect(self):
316
382
  """
317
- Returns the previous url.
383
+ Returns the previous url.
318
384
  """
319
385
  index_url = self.appbuilder.get_url_for_index
320
386
  page_history = Stack(session.get("page_history", []))
@@ -328,26 +394,25 @@ class BaseView(object):
328
394
  @classmethod
329
395
  def get_default_url(cls, **kwargs):
330
396
  """
331
- Returns the url for this class default endpoint
397
+ Returns the url for this class default endpoint
332
398
  """
333
399
  return url_for(cls.__name__ + "." + cls.default_view, **kwargs)
334
400
 
335
401
  def get_uninit_inner_views(self):
336
402
  """
337
- Will return a list with views that need to be initialized.
338
- Normally related_views from ModelView
403
+ Will return a list with views that need to be initialized.
404
+ Normally related_views from ModelView
339
405
  """
340
406
  return []
341
407
 
342
- def get_init_inner_views(self, views):
408
+ def get_init_inner_views(self):
343
409
  """
344
- Sets initialized inner views
410
+ Sets initialized inner views
345
411
  """
346
- pass
347
412
 
348
413
  def get_method_permission(self, method_name: str) -> str:
349
414
  """
350
- Returns the permission name for a method
415
+ Returns the permission name for a method
351
416
  """
352
417
  permission = self.method_permission_name.get(method_name)
353
418
  if permission:
@@ -358,7 +423,7 @@ class BaseView(object):
358
423
 
359
424
  class BaseFormView(BaseView):
360
425
  """
361
- Base class FormView's
426
+ Base class FormView's
362
427
  """
363
428
 
364
429
  form_template = "appbuilder/general/model/edit.html"
@@ -392,20 +457,18 @@ class BaseFormView(BaseView):
392
457
 
393
458
  def form_get(self, form):
394
459
  """
395
- Override this method to implement your form processing
460
+ Override this method to implement your form processing
396
461
  """
397
- pass
398
462
 
399
463
  def form_post(self, form):
400
464
  """
401
- Override this method to implement your form processing
465
+ Override this method to implement your form processing
402
466
 
403
- :param form: WTForm form
467
+ :param form: WTForm form
404
468
 
405
- Return None or a flask response to render
406
- a custom template or redirect the user
469
+ Return None or a flask response to render
470
+ a custom template or redirect the user
407
471
  """
408
- pass
409
472
 
410
473
  def _get_edit_widget(self, form=None, exclude_cols=None, widgets=None):
411
474
  exclude_cols = exclude_cols or []
@@ -422,10 +485,10 @@ class BaseFormView(BaseView):
422
485
 
423
486
  class BaseModelView(BaseView):
424
487
  """
425
- The base class of ModelView and ChartView, all properties are inherited
426
- Customize ModelView and ChartView overriding this properties
488
+ The base class of ModelView and ChartView, all properties are inherited
489
+ Customize ModelView and ChartView overriding this properties
427
490
 
428
- This class supports all the basics for query
491
+ This class supports all the basics for query
429
492
  """
430
493
 
431
494
  datamodel = None
@@ -458,7 +521,7 @@ class BaseModelView(BaseView):
458
521
  search_form_extra_fields = None
459
522
  """
460
523
  A dictionary containing column names and a WTForm
461
- Form fields to be added to the Add form, these fields do not
524
+ Form fields to be added to the search form, these fields do not
462
525
  exist on the model itself ex::
463
526
 
464
527
  search_form_extra_fields = {'some_col':BooleanField('Some Col', default=False)}
@@ -475,7 +538,7 @@ class BaseModelView(BaseView):
475
538
 
476
539
  class ContactModelView(ModelView):
477
540
  datamodel = SQLAModel(Contact, db.session)
478
- search_form_query_rel_fields = [('group':[['name',FilterStartsWith,'W']]}
541
+ search_form_query_rel_fields = {'group':[['name',FilterStartsWith,'W']]}
479
542
 
480
543
  """
481
544
 
@@ -530,7 +593,7 @@ class BaseModelView(BaseView):
530
593
 
531
594
  def __init__(self, **kwargs):
532
595
  """
533
- Constructor
596
+ Constructor
534
597
  """
535
598
  datamodel = kwargs.get("datamodel", None)
536
599
  if datamodel:
@@ -542,7 +605,7 @@ class BaseModelView(BaseView):
542
605
 
543
606
  def _gen_labels_columns(self, list_columns):
544
607
  """
545
- Auto generates pretty label_columns from list of columns
608
+ Auto generates pretty label_columns from list of columns
546
609
  """
547
610
  for col in list_columns:
548
611
  if not self.label_columns.get(col):
@@ -594,7 +657,7 @@ class BaseModelView(BaseView):
594
657
 
595
658
  def _label_columns_json(self):
596
659
  """
597
- Prepares dict with labels to be JSON serializable
660
+ Prepares dict with labels to be JSON serializable
598
661
  """
599
662
  ret = {}
600
663
  for key, value in list(self.label_columns.items()):
@@ -604,9 +667,24 @@ class BaseModelView(BaseView):
604
667
 
605
668
  class BaseCRUDView(BaseModelView):
606
669
  """
607
- The base class for ModelView, all properties are inherited
608
- Customize ModelView overriding this properties
609
- """
670
+ The base class for ModelView, all properties are inherited
671
+ Customize ModelView overriding this properties
672
+ """
673
+
674
+ """ Messages to display on CRUD Events """
675
+ add_row_message = lazy_gettext("Added Row")
676
+ edit_row_message = lazy_gettext("Changed Row")
677
+ delete_row_message = lazy_gettext("Deleted Row")
678
+ delete_integrity_error_message = lazy_gettext(
679
+ "Associated data exists, please delete them first"
680
+ )
681
+ add_integrity_error_message = lazy_gettext(
682
+ "Integrity error, probably unique constraint"
683
+ )
684
+ edit_integrity_error_message = lazy_gettext(
685
+ "Integrity error, probably unique constraint"
686
+ )
687
+ database_error_message = lazy_gettext("Database Error")
610
688
 
611
689
  related_views = None
612
690
  """
@@ -821,7 +899,7 @@ class BaseCRUDView(BaseModelView):
821
899
 
822
900
  def _init_forms(self):
823
901
  """
824
- Init forms for Add and Edit
902
+ Init forms for Add and Edit
825
903
  """
826
904
  super(BaseCRUDView, self)._init_forms()
827
905
  conv = GeneralModelConverter(self.datamodel)
@@ -846,7 +924,7 @@ class BaseCRUDView(BaseModelView):
846
924
 
847
925
  def _init_titles(self):
848
926
  """
849
- Init Titles if not defined
927
+ Init Titles if not defined
850
928
  """
851
929
  super(BaseCRUDView, self)._init_titles()
852
930
  class_name = self.datamodel.model_name
@@ -862,7 +940,7 @@ class BaseCRUDView(BaseModelView):
862
940
 
863
941
  def _init_properties(self):
864
942
  """
865
- Init Properties
943
+ Init Properties
866
944
  """
867
945
  super(BaseCRUDView, self)._init_properties()
868
946
  # Reset init props
@@ -933,7 +1011,6 @@ class BaseCRUDView(BaseModelView):
933
1011
  page=None,
934
1012
  page_size=None,
935
1013
  ):
936
-
937
1014
  fk = related_view.datamodel.get_related_fk(self.datamodel.obj)
938
1015
  filters = related_view.datamodel.get_filters()
939
1016
  # Check if it's a many to one model relation
@@ -955,7 +1032,7 @@ class BaseCRUDView(BaseModelView):
955
1032
  name = related_view.__name__
956
1033
  else:
957
1034
  name = related_view.__class__.__name__
958
- log.error("Can't find relation on related view {0}".format(name))
1035
+ log.error("Can't find relation on related view %s", name)
959
1036
  return None
960
1037
  return related_view._get_view_widget(
961
1038
  filters=filters,
@@ -969,9 +1046,9 @@ class BaseCRUDView(BaseModelView):
969
1046
  self, item, orders=None, pages=None, page_sizes=None, widgets=None, **args
970
1047
  ):
971
1048
  """
972
- :return:
973
- Returns a dict with 'related_views' key with a list of
974
- Model View widgets
1049
+ :return:
1050
+ Returns a dict with 'related_views' key with a list of
1051
+ Model View widgets
975
1052
  """
976
1053
  widgets = widgets or {}
977
1054
  widgets["related_views"] = []
@@ -994,8 +1071,8 @@ class BaseCRUDView(BaseModelView):
994
1071
 
995
1072
  def _get_view_widget(self, **kwargs):
996
1073
  """
997
- :return:
998
- Returns a Model View widget
1074
+ :return:
1075
+ Returns a Model View widget
999
1076
  """
1000
1077
  return self._get_list_widget(**kwargs).get("list")
1001
1078
 
@@ -1008,10 +1085,9 @@ class BaseCRUDView(BaseModelView):
1008
1085
  page=None,
1009
1086
  page_size=None,
1010
1087
  widgets=None,
1011
- **args,
1088
+ **kwargs,
1012
1089
  ):
1013
-
1014
- """ get joined base filter and current active filter for query """
1090
+ """get joined base filter and current active filter for query"""
1015
1091
  widgets = widgets or {}
1016
1092
  actions = actions or self.actions
1017
1093
  page_size = page_size or self.page_size
@@ -1043,6 +1119,7 @@ class BaseCRUDView(BaseModelView):
1043
1119
  actions=actions,
1044
1120
  filters=filters,
1045
1121
  modelview_name=self.__class__.__name__,
1122
+ **kwargs,
1046
1123
  )
1047
1124
  return widgets
1048
1125
 
@@ -1088,14 +1165,14 @@ class BaseCRUDView(BaseModelView):
1088
1165
 
1089
1166
  def get_uninit_inner_views(self):
1090
1167
  """
1091
- Will return a list with views that need to be initialized.
1092
- Normally related_views from ModelView
1168
+ Will return a list with views that need to be initialized.
1169
+ Normally related_views from ModelView
1093
1170
  """
1094
1171
  return self.related_views
1095
1172
 
1096
1173
  def get_init_inner_views(self):
1097
1174
  """
1098
- Get the list of related ModelViews after they have been initialized
1175
+ Get the list of related ModelViews after they have been initialized
1099
1176
  """
1100
1177
  return self._related_views
1101
1178
 
@@ -1105,10 +1182,10 @@ class BaseCRUDView(BaseModelView):
1105
1182
  -----------------------------------------------------
1106
1183
  """
1107
1184
 
1108
- def _list(self):
1185
+ def _list(self, **kwargs):
1109
1186
  """
1110
- list function logic, override to implement different logic
1111
- returns list and search widget
1187
+ list function logic, override to implement different logic
1188
+ returns list and search widget
1112
1189
  """
1113
1190
  if get_order_args().get(self.__class__.__name__):
1114
1191
  order_column, order_direction = get_order_args().get(
@@ -1125,6 +1202,7 @@ class BaseCRUDView(BaseModelView):
1125
1202
  order_direction=order_direction,
1126
1203
  page=page,
1127
1204
  page_size=page_size,
1205
+ **kwargs,
1128
1206
  )
1129
1207
  form = self.search_form.refresh()
1130
1208
  self.update_redirect()
@@ -1132,8 +1210,8 @@ class BaseCRUDView(BaseModelView):
1132
1210
 
1133
1211
  def _show(self, pk):
1134
1212
  """
1135
- show function logic, override to implement different logic
1136
- returns show and related list widget
1213
+ show function logic, override to implement different logic
1214
+ returns show and related list widget
1137
1215
  """
1138
1216
  pages = get_page_args()
1139
1217
  page_sizes = get_page_size_args()
@@ -1150,11 +1228,11 @@ class BaseCRUDView(BaseModelView):
1150
1228
 
1151
1229
  def _add(self):
1152
1230
  """
1153
- Add function logic, override to implement different logic
1154
- returns add widget or None
1231
+ Add function logic, override to implement different logic
1232
+ returns add widget or None
1155
1233
  """
1156
1234
  is_valid_form = True
1157
- get_filter_args(self._filters)
1235
+ get_filter_args(self._filters, disallow_if_not_in_search=False)
1158
1236
  exclude_cols = self._filters.get_relation_cols()
1159
1237
  form = self.add_form.refresh()
1160
1238
 
@@ -1170,9 +1248,12 @@ class BaseCRUDView(BaseModelView):
1170
1248
  except Exception as e:
1171
1249
  flash(str(e), "danger")
1172
1250
  else:
1173
- if self.datamodel.add(item):
1251
+ try:
1252
+ self.datamodel.add(item)
1174
1253
  self.post_add(item)
1175
- flash(*self.datamodel.message)
1254
+ flash(self.add_row_message, "success")
1255
+ except Exception as e:
1256
+ flash(str(e))
1176
1257
  finally:
1177
1258
  return None
1178
1259
  else:
@@ -1183,14 +1264,14 @@ class BaseCRUDView(BaseModelView):
1183
1264
 
1184
1265
  def _edit(self, pk):
1185
1266
  """
1186
- Edit function logic, override to implement different logic
1187
- returns Edit widget and related list or None
1267
+ Edit function logic, override to implement different logic
1268
+ returns Edit widget and related list or None
1188
1269
  """
1189
1270
  is_valid_form = True
1190
1271
  pages = get_page_args()
1191
1272
  page_sizes = get_page_size_args()
1192
1273
  orders = get_order_args()
1193
- get_filter_args(self._filters)
1274
+ get_filter_args(self._filters, disallow_if_not_in_search=False)
1194
1275
  exclude_cols = self._filters.get_relation_cols()
1195
1276
 
1196
1277
  item = self.datamodel.get(pk, self._base_filters)
@@ -1214,9 +1295,12 @@ class BaseCRUDView(BaseModelView):
1214
1295
  except Exception as e:
1215
1296
  flash(str(e), "danger")
1216
1297
  else:
1217
- if self.datamodel.edit(item):
1298
+ try:
1299
+ self.datamodel.edit(item)
1218
1300
  self.post_update(item)
1219
- flash(*self.datamodel.message)
1301
+ flash(self.edit_row_message, "success")
1302
+ except Exception:
1303
+ flash(self.database_error_message, "danger")
1220
1304
  finally:
1221
1305
  return None
1222
1306
  else:
@@ -1242,11 +1326,11 @@ class BaseCRUDView(BaseModelView):
1242
1326
 
1243
1327
  def _delete(self, pk):
1244
1328
  """
1245
- Delete function logic, override to implement different logic
1246
- deletes the record with primary_key = pk
1329
+ Delete function logic, override to implement different logic
1330
+ deletes the record with primary_key = pk
1247
1331
 
1248
- :param pk:
1249
- record primary key to delete
1332
+ :param pk:
1333
+ record primary key to delete
1250
1334
  """
1251
1335
  item = self.datamodel.get(pk, self._base_filters)
1252
1336
  if not item:
@@ -1256,9 +1340,12 @@ class BaseCRUDView(BaseModelView):
1256
1340
  except Exception as e:
1257
1341
  flash(str(e), "danger")
1258
1342
  else:
1259
- if self.datamodel.delete(item):
1343
+ try:
1344
+ self.datamodel.delete(item)
1260
1345
  self.post_delete(item)
1261
- flash(*self.datamodel.message)
1346
+ flash(self.delete_row_message, "success")
1347
+ except Exception as e:
1348
+ flash(str(e))
1262
1349
  self.update_redirect()
1263
1350
 
1264
1351
  """
@@ -1303,7 +1390,7 @@ class BaseCRUDView(BaseModelView):
1303
1390
 
1304
1391
  def _fill_form_exclude_cols(self, exclude_cols, form):
1305
1392
  """
1306
- fill the form with the suppressed cols, generated from exclude_cols
1393
+ fill the form with the suppressed cols, generated from exclude_cols
1307
1394
  """
1308
1395
  for filter_key in exclude_cols:
1309
1396
  filter_value = self._filters.get_filter_value(filter_key)
@@ -1314,93 +1401,83 @@ class BaseCRUDView(BaseModelView):
1314
1401
 
1315
1402
  def is_get_mutation_allowed(self) -> bool:
1316
1403
  """
1317
- Check is mutations on HTTP GET methods are allowed.
1318
- Always called on a request
1404
+ Check is mutations on HTTP GET methods are allowed.
1405
+ Always called on a request
1319
1406
  """
1320
1407
  if current_app.config.get("FAB_ALLOW_GET_UNSAFE_MUTATIONS", False):
1321
1408
  return True
1322
- return not (
1323
- request.method == "GET" and self.appbuilder.app.extensions.get("csrf")
1324
- )
1409
+ return not (request.method == "GET" and current_app.extensions.get("csrf"))
1325
1410
 
1326
1411
  def prefill_form(self, form, pk):
1327
1412
  """
1328
- Override this, will be called only if the current action is rendering
1329
- an edit form (a GET request), and is used to perform additional action to
1330
- prefill the form.
1413
+ Override this, will be called only if the current action is rendering
1414
+ an edit form (a GET request), and is used to perform additional action to
1415
+ prefill the form.
1331
1416
 
1332
- This is useful when you have added custom fields that depend on the
1333
- database contents. Fields that were added by name of a normal column
1334
- or relationship should work out of the box.
1417
+ This is useful when you have added custom fields that depend on the
1418
+ database contents. Fields that were added by name of a normal column
1419
+ or relationship should work out of the box.
1335
1420
 
1336
- example::
1421
+ example::
1337
1422
 
1338
- def prefill_form(self, form, pk):
1339
- if form.email.data:
1340
- form.email_confirmation.data = form.email.data
1423
+ def prefill_form(self, form, pk):
1424
+ if form.email.data:
1425
+ form.email_confirmation.data = form.email.data
1341
1426
  """
1342
- pass
1343
1427
 
1344
1428
  def process_form(self, form, is_created):
1345
1429
  """
1346
- Override this, will be called only if the current action is submitting
1347
- a create/edit form (a POST request), and is used to perform additional
1348
- action before the form is used to populate the item.
1430
+ Override this, will be called only if the current action is submitting
1431
+ a create/edit form (a POST request), and is used to perform additional
1432
+ action before the form is used to populate the item.
1349
1433
 
1350
- By default does nothing.
1434
+ By default does nothing.
1351
1435
 
1352
- example::
1436
+ example::
1353
1437
 
1354
- def process_form(self, form, is_created):
1355
- if not form.owner:
1356
- form.owner.data = 'n/a'
1438
+ def process_form(self, form, is_created):
1439
+ if not form.owner:
1440
+ form.owner.data = 'n/a'
1357
1441
  """
1358
- pass
1359
1442
 
1360
1443
  def pre_update(self, item):
1361
1444
  """
1362
- Override this, this method is called before the update takes place.
1363
- If an exception is raised by this method,
1364
- the message is shown to the user and the update operation is
1365
- aborted. Because of this behavior, it can be used as a way to
1366
- implement more complex logic around updates. For instance
1367
- allowing only the original creator of the object to update it.
1445
+ Override this, this method is called before the update takes place.
1446
+ If an exception is raised by this method,
1447
+ the message is shown to the user and the update operation is
1448
+ aborted. Because of this behavior, it can be used as a way to
1449
+ implement more complex logic around updates. For instance
1450
+ allowing only the original creator of the object to update it.
1368
1451
  """
1369
- pass
1370
1452
 
1371
1453
  def post_update(self, item):
1372
1454
  """
1373
- Override this, will be called after update
1455
+ Override this, will be called after update
1374
1456
  """
1375
- pass
1376
1457
 
1377
1458
  def pre_add(self, item):
1378
1459
  """
1379
- Override this, will be called before add.
1380
- If an exception is raised by this method,
1381
- the message is shown to the user and the add operation is aborted.
1460
+ Override this, will be called before add.
1461
+ If an exception is raised by this method,
1462
+ the message is shown to the user and the add operation is aborted.
1382
1463
  """
1383
- pass
1384
1464
 
1385
1465
  def post_add(self, item):
1386
1466
  """
1387
- Override this, will be called after update
1467
+ Override this, will be called after update
1388
1468
  """
1389
- pass
1390
1469
 
1391
1470
  def pre_delete(self, item):
1392
1471
  """
1393
- Override this, will be called before delete
1394
- If an exception is raised by this method,
1395
- the message is shown to the user and the delete operation is
1396
- aborted. Because of this behavior, it can be used as a way to
1397
- implement more complex logic around deletes. For instance
1398
- allowing only the original creator of the object to delete it.
1472
+ Override this, will be called before delete
1473
+ If an exception is raised by this method,
1474
+ the message is shown to the user and the delete operation is
1475
+ aborted. Because of this behavior, it can be used as a way to
1476
+ implement more complex logic around deletes. For instance
1477
+ allowing only the original creator of the object to delete it.
1399
1478
  """
1400
- pass
1401
1479
 
1402
1480
  def post_delete(self, item):
1403
1481
  """
1404
- Override this, will be called after delete
1482
+ Override this, will be called after delete
1405
1483
  """
1406
- pass