28

I'm currently cloning a single-level association like this:

class Survey < ActiveRecord::Base
  def duplicate
    new_template = self.clone
    new_template.questions << self.questions.collect { |question| question.clone } 
    new_template.save   
  end
end

So that clones the Survey then clones the Questions associated with that survey. Fine. That works quite well.

But what I'm having trouble with is that each question has_many Answers. So Survey has_many Questions which has_many Answers.

I can't figure out how to clone the answers properly. I've tried this:

def duplicate
  new_template = self.clone

  self.questions.each do |question|
    new_question = question.clone
    new_question.save

    question.answers.each do |answer|
      new_answer = answer.clone
      new_answer.save
      new_question.answers << answer
    end

    new_template.questions << question
  end

  new_template.save   
end

But that does some weird stuff with actually replacing the original answers then creating new ones, so ID's stop matching correctly.

Shpigford
  • 24,748
  • 58
  • 163
  • 252

5 Answers5

46

Use deep_clonable gem

new_survey = original_survey.clone :include => [:questions => :answers]
fl00r
  • 82,987
  • 33
  • 217
  • 237
  • 2
    the clone method has changed to dup. see [the repo readme](https://github.com/moiristo/deep_cloneable) – Sean M Jun 24 '14 at 18:36
  • 7
    And now it changes to: new_survey = original_survey.deep_clone :include => [:questions => :answers] – halbano Nov 02 '14 at 01:04
3

Without using gems, you can do the following:

class Survey < ApplicationRecord
  has_and_belongs_to_many :questions

  def copy_from(last_survey)
    last_survery.questions.each do |question|
      new_question = question.dup
      new_question.save

      questions << new_question
    end

    save
  end
  …
end

Then you can call:

new_survey = Survey.create
new_survey.copy_from(past_survey)

That will duplicate all questions from last Survey to new Survey and tie them.

lucasarruda
  • 1,462
  • 1
  • 25
  • 45
  • +1 for your answer as I was stuck somewhere and it helped me alot. Thanks! but my question is that why we need to do `questions << new_question` when we already doing `new_question.save`. Can you please explain for my understanding? Like we saving object but why again to `questions` array like `<<`? – LearningROR Apr 25 '21 at 17:21
  • Also `save` refers to what in your above answer? I am not using it and it still works. my code: `def duplicate_records new_course = self.dup new_course.image = File.open(self.image.file.file) if self.image.present? new_course.save new_course.chapters = self.chapters new_course.chapters.each do |chapter| new_chapter = chapter.dup new_chapter.image = File.open(chapter.image.file.file) if chapter.image.present? new_chapter.save chapters << new_chapter end new_course end` – LearningROR Apr 25 '21 at 17:29
  • @LearningROR So, `questions << new_question` is just so we save that question as question on the new survey object. So, copy_from is a method you access from an object, like you would do `survey.copy_from(other_survey)` and that would duplicate each question from `other_survey` and then assign them to `survey.questions`. We are inside the object scope, so `questions` is just that object `.` questions. – lucasarruda Apr 25 '21 at 17:33
  • @LearningROR as for the `save`, this is just some like vice of programming. Because I didn't test my answer, I added it to make sure that save gets called in the object you call `.copy_from(other_survey)`. It's probably not necessary since `<<` should already do the job of persising `new_question` as in the `questions` relationship of the object you call `.copy_from(…)` – lucasarruda Apr 25 '21 at 17:35
  • @LearningROR if you want to understand better why that `save` or `questions` is `obj.save` and `obj.questions`, please read more about "class methods vs instance methods". Here is an article I found googling it https://medium.com/@lauren.kroner/ruby-class-vs-instance-methods-a5182ce7de49 – lucasarruda Apr 25 '21 at 17:38
  • Thanks so much for in-depth response. Now, that really makes sense to me. Stay blessed brother. :) – LearningROR Apr 26 '21 at 18:21
3

You may also like the Amoeba gem for ActiveRecord 3.2.

In your case, you probably want to make use of the nullify, regex or prefix options available in the configuration DSL.

It supports easy and automatic recursive duplication of has_one, has_many and has_and_belongs_to_many associations, field preprocessing and a highly flexible and powerful configuration DSL that can be applied both to the model and on the fly.

be sure to check out the Amoeba Documentation but usage is pretty easy...

just

gem install amoeba

or add

gem 'amoeba'

to your Gemfile

then add the amoeba block to your model and run the dup method as usual

class Post < ActiveRecord::Base
  has_many :comments
  has_and_belongs_to_many :tags

  amoeba do
    enable
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

class Tag < ActiveRecord::Base
  has_and_belongs_to_many :posts
end

class PostsController < ActionController
  def some_method
    my_post = Post.find(params[:id])
    new_post = my_post.dup
    new_post.save
  end
end

You can also control which fields get copied in numerous ways, but for example, if you wanted to prevent comments from being duplicated but you wanted to maintain the same tags, you could do something like this:

class Post < ActiveRecord::Base
  has_many :comments
  has_and_belongs_to_many :tags

  amoeba do
    exclude_field :comments
  end
end

You can also preprocess fields to help indicate uniqueness with both prefixes and suffixes as well as regexes. In addition, there are also numerous options so you can write in the most readable style for your purpose:

class Post < ActiveRecord::Base
  has_many :comments
  has_and_belongs_to_many :tags

  amoeba do
    include_field :tags
    prepend :title => "Copy of "
    append :contents => " (copied version)"
    regex :contents => {:replace => /dog/, :with => "cat"}
  end
end

Recursive copying of associations is easy, just enable amoeba on child models as well

class Post < ActiveRecord::Base
  has_many :comments

  amoeba do
    enable
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
  has_many :ratings

  amoeba do
    enable
  end
end

class Rating < ActiveRecord::Base
  belongs_to :comment
end

The configuration DSL has yet more options, so be sure to check out the documentation.

Enjoy! :)

Vaughn Draughon
  • 1,380
  • 14
  • 8
  • 1
    I actually tried this gem. It is very promising, but not ready yet. I had two issues with it, both of which I posted back to the github project, but ultimately gave up and wrote my copy function manually, using dup. I hope to try it again in a few months and have better luck! – Rob Mar 22 '12 at 23:42
0

Shouldn't it be..

  new_question.answers << new_answer
end

new_template.questions << new_question
-2

You can also alias the rails dup method, as follows:

class Survey
   has_many :questions, :inverse_of=>:survey, :autosave=>true
   alias orig_dup dup
   def dup
       copy=orig_dup
       copy.questions=questions
       copy
   end
end

class Questions
   belongs_to :survey, :inverse_of=>:questions
   has_many :answers, :inverse_of=>:question, :autosave=>true
   alias orig_dup dup
   def dup
       copy=orig_dup
       copy.answers=answers
       copy
   end
end

class Answer
    belongs_to :question
end

and then you can do this

aaa = Survey.find(123).dup
aaa.save
Rob
  • 4,404
  • 2
  • 32
  • 33