kasper.jha
kasper.jha

Reputation: 113

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

Upvotes: 5

Views: 2001

Answers (2)

Sean McCarthy
Sean McCarthy

Reputation: 5558

Per @pjcunningham's excellent recommendation above, here's how I did it:

class MyModel(ModelView):

    def update_model(self, form, model):
        """Override this method to record something from the model before it's updated"""

        # Make a copy of the old surface location before it's updated, so we can compare it later
        self.old_surface = model.surface

        return super().update_model(form, model)

    def after_model_change(self, form, model, is_created):
        """
        Check to see if the "surface" field has changed, and if so, update the GPS coordinates.
        Do this after the model has changed, so we can start a new database transaction.
        """
        if (
            "surface" in form.data.keys()
            and form.data["surface"] != self.old_model.surface
        ):
            get_or_update_gps(structure_obj=model)

Upvotes: 0

pjcunningham
pjcunningham

Reputation: 8046

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.

Upvotes: 4

Related Questions