Repository: flask-admin/flask-admin

Issue:
Access to an instance during create/update_model
There are some situations when one need do some actions after (or better during creation/saving of a new model).

For example, I need to generate a list of allowed endpoints for fresh or changed Role object.

With current behavior I have to copy the whole method code from the base class:

``` python
    def update_model(self, form, model):
        try:
            form.populate_obj(model)
            self.session.flush()

            self.update_role_allowed_endpoints(form, model)  # <------

            self.session.commit()
            return True
        except Exception, ex:
            flash(gettext('Failed to update model. %(error)s', error=str(ex)), 'error')
            return False

    def create_model(self, form):
        # Copypasted from base class to be able to modify model instance after creation
        try:
            model = self.model()
            form.populate_obj(model)
            self.session.add(model)
            self.session.flush()

            self.update_role_allowed_endpoints(form, model) # <-------

            self.session.commit()
            return True
        except Exception, ex:
            flash(gettext('Failed to create model. %(error)s', error=str(ex)), 'error')
            return False
```

Take note, changes take place in the middle of transaction, so any exceptions in my method do not break data consistency.

I propose to add 'model_changed' class attribute (a callable which takes a form and a model). And call it before transaction commit.



Discussion:

Resolving patch:
diff --git a/flask_admin/contrib/peeweemodel/view.py b/flask_admin/contrib/peeweemodel/view.py
--- a/flask_admin/contrib/peeweemodel/view.py
+++ b/flask_admin/contrib/peeweemodel/view.py
@@ -282,6 +282,8 @@ def create_model(self, form):
             # For peewee have to save inline forms after model was saved
             save_inline(form, model)
 
+            self.on_model_change(form, model)
+
             return True
         except Exception, ex:
             flash(gettext('Failed to create model. %(error)s', error=str(ex)), 'error')
@@ -295,6 +297,8 @@ def update_model(self, form, model):
             # For peewee have to save inline forms after model was saved
             save_inline(form, model)
 
+            self.on_model_change(form, model)
+
             return True
         except Exception, ex:
             flash(gettext('Failed to update model. %(error)s', error=str(ex)), 'error')
@@ -302,6 +306,7 @@ def update_model(self, form, model):
 
     def delete_model(self, model):
         try:
+            self.on_model_delete(model)
             model.delete_instance(recursive=True)
             return True
         except Exception, ex:
diff --git a/flask_admin/contrib/sqlamodel/view.py b/flask_admin/contrib/sqlamodel/view.py
--- a/flask_admin/contrib/sqlamodel/view.py
+++ b/flask_admin/contrib/sqlamodel/view.py
@@ -567,6 +567,8 @@ def create_model(self, form):
             model = self.model()
             form.populate_obj(model)
             self.session.add(model)
+            self.session.flush()
+            self.on_model_change(form, model)
             self.session.commit()
             return True
         except Exception, ex:
@@ -584,6 +586,8 @@ def update_model(self, form, model):
         """
         try:
             form.populate_obj(model)
+            self.session.flush()
+            self.on_model_change(form, model)
             self.session.commit()
             return True
         except Exception, ex:
@@ -598,6 +602,8 @@ def delete_model(self, model):
                 Model to delete
         """
         try:
+            self.on_model_delete(model)
+            self.session.flush()
             self.session.delete(model)
             self.session.commit()
             return True
diff --git a/flask_admin/model/base.py b/flask_admin/model/base.py
--- a/flask_admin/model/base.py
+++ b/flask_admin/model/base.py
@@ -531,6 +531,28 @@ def get_one(self, id):
         raise NotImplemented('Please implement get_one method')
 
     # Model handlers
+    def on_model_change(self, form, model):
+        """
+            Allow to do some actions after a model was created or updated.
+
+            Called from create_model and update_model in the same transaction
+            (if it has any meaning for a store backend).
+
+            By default do nothing.
+        """
+        pass
+
+    def on_model_delete(self, model):
+        """
+            Allow to do some actions before a model will be deleted.
+
+            Called from delete_model in the same transaction
+            (if it has any meaning for a store backend).
+
+            By default do nothing.
+        """
+        pass
+
     def create_model(self, form):
         """
             Create model from the form.


Test patch:
diff --git a/flask_admin/tests/test_sqlamodel.py b/flask_admin/tests/test_sqlamodel.py
--- a/flask_admin/tests/test_sqlamodel.py
+++ b/flask_admin/tests/test_sqlamodel.py
@@ -358,3 +358,37 @@ class Model(db.Model):
 def test_relations():
     # TODO: test relations
     pass
+
+def test_on_model_change_delete():
+    app, db, admin = setup()
+    Model1, _ = create_models(db)
+    db.create_all()
+
+    class ModelView(CustomModelView):
+        def on_model_change(self, form, model):
+            model.test1 = model.test1.upper()
+
+        def on_model_delete(self, model):
+            self.deleted = True
+
+    view = ModelView(Model1, db.session)
+    admin.add_view(view)
+
+    client = app.test_client()
+
+    client.post('/admin/model1view/new/',
+                     data=dict(test1='test1large', test2='test2'))
+
+    model = db.session.query(Model1).first()
+    eq_(model.test1, 'TEST1LARGE')
+
+    url = '/admin/model1view/edit/?id=%s' % model.id
+    client.post(url, data=dict(test1='test1small', test2='test2large'))
+
+    model = db.session.query(Model1).first()
+    eq_(model.test1, 'TEST1SMALL')
+
+    url = '/admin/model1view/delete/?id=%s' % model.id
+    client.post(url)
+    ok_(view.deleted)
+