Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore has_many through association when undoing a destroy #121

Closed
elle opened this issue Jan 11, 2012 · 6 comments
Closed

Restore has_many through association when undoing a destroy #121

elle opened this issue Jan 11, 2012 · 6 comments
Milestone

Comments

@elle
Copy link

elle commented Jan 11, 2012

This is based on Ryan Bates' railcast and I am only trying to restore deleted join records.
I am manually defining which children I would like to restore as a meta column on versions, like so:

class Team < ActiveRecord::Base
  has_paper_trail :meta => { :children => ['team_members'] }
  has_many :team_members, :dependent => :destroy
end
class Version < ActiveRecord::Base
  attr_accessible :children
  serialize :children, Array

  after_save :restore_children

  def recreating_item?
    if item
      item.versions[-1].try(:event) == 'create' && item.versions[-2].try(:event) == 'destroy'
    end
  end

  def restore_children
    if children && recreating_item?
      children.each do |model_name|
        records = Version.where(:item_type => model_name.classify)

        records.each do |ver|
          ver.reify.save! if ver.event == 'destroy' && ver.created_at > 20.seconds.ago
        end
      end
    end
  end
end

I could be checking if the child model responds_to? :versions but since I manually define which models I would like to go through, I didn't think it was necessary.

One problem is that I am only looking 20.seconds.ago for recent records. But that option will only occur when people click on the 'undo destroy' link.

Anyhow, this works for me.

@airblade
Copy link
Collaborator

Thank you for posting your code. It's a good solution to the perennial problem of restoring associations.

Please could you explain this line?

ver.reify.save! if ver.event == 'destroy' && ver.created_at > 20.seconds.ago

Will it only restore the team's members if you try to restore the team within 20 seconds of destroying it? Wouldn't you just want the last destroy event?

@elle
Copy link
Author

elle commented Jan 11, 2012

Since I was only looking to restore items when a user clicks undo on removing an item, I used 20.seconds.ago

I wasn't sure how to get the last destroy for the children, because when the parent is recreated, it does not yet have children again.

One case:
A team has 3 team members. When a team is destroyed, 3 team members are destroyed.
Restoring just the last destroy is not enough.

Second case:
We have 2 teams, each with multiple team members. First team is destroyed. Then second team is destroyed.
Restoring one of the teams should only restore that team's members.

So I changed the restore method to be:

def restore_children
  if children && recreating_item?
    children.each do |model_name|
      records = Version.where(:item_type => model_name.classify, :event => 'destroy')

      records.each do |ver|
        ver.reify.save! if ver.created_at > item.versions[-2].created_at - 1.seconds # checking the time for the destroy version
        #temp_object = ver.reify if ver.created_at > item.versions[-2].created_at - 1.seconds
        #temp_object.save! if temp_object.send(item.class.name.tableize.singularize).id == item.id
      end
    end
  end
end

The commented out lines are something else I tried, to check that the child's parent is the item being restored.
But that still will not work for the second case.

The restore children method is still not ideal. I can think of ways it can break. For example if removing multiple teams within 1 second range, which happens in my test. So in the test, I delay the second remove by a few seconds.

@airblade
Copy link
Collaborator

Hmm, how about changing your Team class to store the team members' ids. Then the Version class can look at each team member's versions for a destroy event which occurred at the same time as the team was destroyed. I.e.:

class Team < ActiveRecord::Base
  has_paper_trail :meta => { :children => Proc.new { |team| ['team_members' => team.team_members.map(&:id)] } }
  has_many :team_members, :dependent => :destroy
end

class Version < ActiveRecord::Base
  attr_accessible :children
  serialize :children, Array

  after_save :restore_children

  def recreating_item?
    if item
      item.versions[-1].try(:event) == 'create' && item.versions[-2].try(:event) == 'destroy'
    end
  end

  def restore_children
    if children && recreating_item?
      children.each do |model_name, model_ids|
        model_ids.each do |model_id|
          child_versions = Version.where(:item_type => model_name.classify,
                                         :item_id   => model_id,
                                         :event     => 'destroy')
          child_versions.each do |ver|
            ver.reify.save! if (ver.created_at - item.versions[-2].created_at).abs < 1.seconds
          end
        end
      end
    end
  end
end

@elle
Copy link
Author

elle commented Jan 30, 2012

I tried that approach before. My problem was that if I create children models using team_member_ids which does not fire a new version for the parent model.

@mhenrixon
Copy link

@elle did you ever solve this using paper_trail or any other gem? I am currently struggling with various hacks around the problem but would like to come up with something more general that could be committed back.

Honestly though I don't care so much about destroyed children as I do about continuously updated children.

@batter
Copy link
Collaborator

batter commented Dec 22, 2014

Closed via PR #439, which was just merged!

@batter batter closed this as completed Dec 22, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants