pythonflask-admin

Flask Admin - Access old values using on_model_change()


My goal is to perform some additional action when a user changes a value of a existing record.

I found on_model_change() in the docs and wrote the following code:

def on_model_change(self, form, model, is_created):
    # get old and new value
    old_name = model.name
    new_name = form.name

    if new_name != old_name:
        # if the value is changed perform some other action 
        rename_files(new_name)

My expectation was that the model parameter would represent the record before the new values from the form was applied. It did not. Instead i found that model always had the same values as form, meaning that the if statement never was fulfilled.

Later i tried this:

class MyView(ModelView):
    # excluding the name field from the form
    form_excluded_columns = ('name')

    form_extra_fields = {
        # and adding a set_name field instead
        'set_name':StringField('Name')
    }
    ...
def on_model_change(self, form, model, is_created):
    # getting the new value from set_name instead of name 
    new_name = form.set_name
    ...

Although this solved my goal, it also caused a problem:

The set_name field would not be prefilled with the existing name, forcing the user to type the name even when not intending to change it

I also tried doing db.rollback() at the start of on_model_change() which would undo all changes done by flask-admin, and make model represent the old data. This was rather hacky and lead my to reimplement alot of flask admin code myself, which got messy.

What is the best way to solve this problem?

HOW I SOLVED IT
I used on_form_prefill to prefill the new name field instead of @pjcunningham 's answer.

# fill values from model 
def on_form_prefill(self, form, id):
    # get track model
    track = Tracks.query.filter_by(id=id).first()

    # fill new values
    form.set_name.data = track.name

Solution

  • Override method update_model in your view. Here is the default behaviour if you are using SqlAlchemy views, I have added some notes to explain the model's state.

    def update_model(self, form, model):
        """
            Update model from form.
            :param form:
                Form instance
            :param model:
                Model instance
        """
        try:
            # at this point model variable has the unmodified values
    
            form.populate_obj(model)
    
            # at this point model variable has the form values
    
            # your on_model_change is called
            self._on_model_change(form, model, False)
    
            # model is now being committed
            self.session.commit()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
                log.exception('Failed to update record.')
    
            self.session.rollback()
    
            return False
        else:
            # model is now committed to the database
            self.after_model_change(form, model, False)
    
        return True
    

    You'll want something like the following, it's up to you where place the check, I've put it after the model has been committed:

    def update_model(self, form, model):
        """
            Update model from form.
            :param form:
                Form instance
            :param model:
                Model instance
        """
        try:
    
            old_name = model.name
            new_name = form.name.data
    
            # continue processing the form
    
            form.populate_obj(model)
            self._on_model_change(form, model, False)
            self.session.commit()
        except Exception as ex:
            if not self.handle_view_exception(ex):
                flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
                log.exception('Failed to update record.')
    
            self.session.rollback()
    
            return False
        else:
    
            # the model got committed now run our check:
            if new_name != old_name:
                # if the value is changed perform some other action
                rename_files(new_name)
    
            self.after_model_change(form, model, False)
    
        return True
    

    There are similar methods you can override for create_model and delete_model.