Paper Trail: create a version on parent whenever associated model changes?
Asked Answered
U

1

6

I'm working on a Rails app where I need to show the audit trail on a Record, which has_many Data. I have paper_trail on my Record, and associated Datum models, and it is saving versions of them just fine.

However, what I need is for one version for Record to be created whenever one or more associated Data are changed. Currently, it creates versions on each Datum that changes, but it only creates a version of the Record if the Record's attributes change; it's not doing it when the associated Data change.

I tried putting touch_with_version in Record's after_touch callback, like so:

class Record < ActiveRecord::Base
  has_many :data

  has_paper_trail

  after_touch do |record|
    puts 'touched record'
    record.touch_with_version
  end

end

and

class Datum < ActiveRecord::Base
  belongs_to :record, :touch => true

  has_paper_trail

end

The after_touch callback fires, but unfortunately it creates a new version for each Datum, so when a Record is created it already has like 10 versions, one for each Datum.

Is there a way to tell in the callbacks if a version has been created, so I don't create multiples? Like check in one of the Record callbacks and if Datum has already triggered a version, don't do any more?

Thanks!

Upwind answered 9/10, 2015 at 16:48 Comment(8)
I think after_touch is a good idea, but might be tricky to prevent duplicate versions of Record. Instead, what about starting a transaction, updating your Data, updating your Record, then committing the transaction. If all the updates happen in the same transaction, what result do you get?Housebound
A transaction sounds like what I want, but I didn't have to create one, I think I just got it for free because all the Data belong_to the Record, so all those saves just happen in the course of create/update for Record. One other wrinkle--if multiple Data would create multiple versions on Record, I think I need to keep the last version, not the first, if I want Record to be synced with its Data. Thanks for your advice--I'm still new to Rails and I'm not sure if I'm quite thinking in Rails yet. Working on it!Upwind
@JaredBeck, how might I set up that transaction? Record's data attributes are already in a transaction(I think?) because of the Rails association. Should I turn off the paper_trail callbacks, like: has_paper_trail on: [], and then create my own after_create and after_update callbacks with ``` Record.transaction do record.update record.touch_with_version end ```Upwind
For documentation on transactions in ActiveRecord, you might start here: guides.rubyonrails.org/…Housebound
If you're new to rails, I wouldn't recommend using PaperTrail's assocations feature yet, as it is experimental and has a few known issues.Housebound
Thanks for the pointer to the Guides page, I had searched there for transactions but didn't see that. And I saw the caveats on associations, but I don't think any of them apply to me; my Data are only ever accessed through Record. I ended up coming at the problem from another direction; paper_trail is saving Record and Data (I'm not losing data anywhere), so I'm coming at the problem from the view end. Thanks for your help!Upwind
@DavidHam So you gave up to create one Record.version when associated model changes?Nevanevada
I did, but there is another approach I found afterward that I haven't tried yet. (In my case, there was always a change to the parent's status property whenever a child Datum changed, so it was sort of moot.) But I think if I did belongs_to :record, touch: true in my Datum class, it would create the timestamp I want on my Record.Upwind
N
4

This works for me.

class Place < ActiveRecord::Base
  has_paper_trail
  before_update :check_update

  def check_update
    return if changed_notably?

    tracking_has_many_associations = [ ... ]
    tracking_has_has_one_associations = [ ... ]

    tracking_has_many_associations.each do |a|
      send(a).each do |r|
        if r.send(:changed_notably?) || r.marked_for_destruction?
          self.touch
          return
        end
      end
    end
    tracking_has_one_associations.each do |a|
      r = send(a)
      if r.send(:changed_notably?) || r.marked_for_destruction?
        self.touch
        return
      end
    end
  end
end
Nevanevada answered 22/12, 2015 at 8:15 Comment(1)
You may have to prefix the call to :changed_notably? with with a call to paper_trail first. e.g. instead of changed_notably? => paper_trail.changed_notably?`Samaniego

© 2022 - 2024 — McMap. All rights reserved.