Git Product home page Git Product logo

amoeba's Introduction

Amoeba

Easy cloning of active_record objects including associations and several operations under associations and attributes.

Maintainability Gem Version Build Status

What?

The goal was to be able to easily and quickly reproduce ActiveRecord objects including their children, for example copying a blog post maintaining its associated tags or categories.

This gem is named "Amoeba" because amoebas are (small life forms that are) good at reproducing. Their children and grandchildren also reproduce themselves quickly and easily.

Technical Details

An ActiveRecord extension gem to allow the duplication of associated child record objects when duplicating an active record model.

Rails 5.2, 6.0, 6.1 compatible. For Rails 4.2 to 5.1 use version 3.x.

Features

  • Supports the following association types
    • has_many
    • has_one :through
    • has_many :through
    • has_and_belongs_to_many
  • A simple DSL for configuration of which fields to copy. The DSL can be applied to your rails models or used on the fly.
  • Supports STI (Single Table Inheritance) children inheriting their parent amoeba settings.
  • Multiple configuration styles such as inclusive, exclusive and indiscriminate (aka copy everything).
  • Supports cloning of the children of Many-to-Many records or merely maintaining original associations
  • Supports automatic drill-down i.e. recursive copying of child and grandchild records.
  • Supports preprocessing of fields to help indicate uniqueness and ensure the integrity of your data depending on your business logic needs, e.g. prepending "Copy of " or similar text.
  • Supports preprocessing of fields with custom lambda blocks so you can do basically whatever you want if, for example, you need some custom logic while making copies.
  • Amoeba can perform the following preprocessing operations on fields of copied records
    • set
    • prepend
    • append
    • nullify
    • customize
    • regex

Usage

Installation

is hopefully as you would expect:

gem install amoeba

or just add it to your Gemfile:

gem 'amoeba'

Configure your models with one of the styles below and then just run the amoeba_dup method on your model where you would run the dup method normally:

p = Post.create(:title => "Hello World!", :content => "Lorum ipsum dolor")
p.comments.create(:content => "I love it!")
p.comments.create(:content => "This sucks!")

puts Comment.all.count # should be 2

my_copy = p.amoeba_dup
my_copy.save

puts Comment.all.count # should be 4

By default, when enabled, amoeba will copy any and all associated child records automatically and associate them with the new parent record.

You can configure the behavior to only include fields that you list or to only include fields that you don't exclude. Of the three, the most performant will be the indiscriminate style, followed by the inclusive style, and the exclusive style will be the slowest because of the need for an extra explicit check on each field. This performance difference is likely negligible enough that you can choose the style to use based on which is easiest to read and write, however, if your data tree is large enough and you need control over what fields get copied, inclusive style is probably a better choice than exclusive style.

Configuration

Please note that these examples are only loose approximations of real world scenarios and may not be particularly realistic, they are only for the purpose of demonstrating feature usage.

Indiscriminate Style

This is the most basic usage case and will simply enable the copying of any known associations.

If you have some models for a blog about like this:

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

simply add the amoeba configuration block to your model and call the enable method to enable the copying of child records, like this:

class Post < ActiveRecord::Base
  has_many :comments

  amoeba do
    enable
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

Child records will be automatically copied when you run the amoeba_dup method.

Inclusive Style

If you only want some of the associations copied but not others, you may use the inclusive style:

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

  amoeba do
    enable
    include_association :tags
    include_association :authors
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

Using the inclusive style within the amoeba block actually implies that you wish to enable amoeba, so there is no need to run the enable method, though it won't hurt either:

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

  amoeba do
    include_association :tags
    include_association :authors
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

You may also specify fields to be copied by passing an array. If you call the include_association with a single value, it will be appended to the list of already included fields. If you pass an array, your array will overwrite the original values.

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

  amoeba do
    include_association [:tags, :authors]
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

These examples will copy the post's tags and authors but not its comments.

The inclusive style, when used, will automatically disable any other style that was previously selected.

Exclusive Style

If you have more fields to include than to exclude, you may wish to shorten the amount of typing and reading you need to do by using the exclusive style. All fields that are not explicitly excluded will be copied:

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

  amoeba do
    exclude_association :comments
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

This example does the same thing as the inclusive style example, it will copy the post's tags and authors but not its comments. As with inclusive style, there is no need to explicitly enable amoeba when specifying fields to exclude.

The exclusive style, when used, will automatically disable any other style that was previously selected, so if you selected include fields, and then you choose some exclude fields, the exclude_association method will disable the previously selected inclusive style and wipe out any corresponding include fields.

Conditions

Also if you need to path extra condition for include or exclude relationship you can path method name to :if option.

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

  amoeba do
    include_association :comments, if: :popular?
  end
  
  def popular?
    likes > 15
  end
end

After call Post.first.amoeba_dup if likes is larger 15 than all comments will be duplicated too, but in another situation - no relations will be cloned. Same behavior will be for exclude_association.

Be aware! If you wrote:

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

  amoeba do
    exclude_association :tags
    include_association :comments, if: :popular?
  end
  
  def popular?
    likes > 15
  end
end

inclusion strategy will be chosen regardless of the result of popular? method call (the same for reverse situation).

Cloning

If you are using a Many-to-Many relationship, you may tell amoeba to actually make duplicates of the original related records rather than merely maintaining association with the original records. Cloning is easy, merely tell amoeba which fields to clone in the same way you tell it which fields to include or exclude.

class Post < ActiveRecord::Base
  has_and_belongs_to_many :warnings

  has_many :post_widgets
  has_many :widgets, :through => :post_widgets

  amoeba do
    enable
    clone [:widgets, :warnings]
  end
end

class Warning < ActiveRecord::Base
  has_and_belongs_to_many :posts
end

class PostWidget < ActiveRecord::Base
  belongs_to :widget
  belongs_to :post
end

class Widget < ActiveRecord::Base
  has_many :post_widgets
  has_many :posts, :through => :post_widgets
end

This example will actually duplicate the warnings and widgets in the database. If there were originally 3 warnings in the database then, upon duplicating a post, you will end up with 6 warnings in the database. This is in contrast to the default behavior where your new post would merely be re-associated with any previously existing warnings and those warnings themselves would not be duplicated.

Limiting Association Types

By default, amoeba recognizes and attempts to copy any children of the following association types:

  • has one
  • has many
  • has and belongs to many

You may control which association types amoeba applies itself to by using the recognize method within the amoeba configuration block.

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

  amoeba do
    recognize [:has_one, :has_and_belongs_to_many]
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

class Tag < ActiveRecord::Base
  has_and_belongs_to_many :posts
end

This example will copy the post's configuration data and keep tags associated with the new post, but will not copy the post's comments because amoeba will only recognize and copy children of has_one and has_and_belongs_to_many associations and in this example, comments are not an has_and_belongs_to_many association.

Field Preprocessors

Nullify

If you wish to prevent a regular (non has_* association based) field from retaining it's value when copied, you may "zero out" or "nullify" the field, like this:

class Topic < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :topic
  has_many :comments

  amoeba do
    enable
    nullify :date_published
    nullify :topic_id
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

This example will copy all of a post's comments. It will also nullify the publishing date and dissociate the post from its original topic.

Unlike inclusive and exclusive styles, specifying null fields will not automatically enable amoeba to copy all child records. As with any active record object, the default field value will be used instead of nil if a default value exists on the migration.

Set

If you wish to just set a field to an arbitrary value on all duplicated objects you may use the set directive. For example, if you wanted to copy an object that has some kind of approval process associated with it, you likely may wish to set the new object's state to be open or "in progress" again.

class Post < ActiveRecord::Base
  amoeba do
    set :state_tracker => "open_for_editing"
  end
end

In this example, when a post is duplicated, it's state_tracker field will always be given a value of open_for_editing to start.

Prepend

You may add a string to the beginning of a copied object's field during the copy phase:

class Post < ActiveRecord::Base
  amoeba do
    enable
    prepend :title => "Copy of "
  end
end

Append

You may add a string to the end of a copied object's field during the copy phase:

class Post < ActiveRecord::Base
  amoeba do
    enable
    append :title => "Copy of "
  end
end

Regex

You may run a search and replace query on a copied object's field during the copy phase:

class Post < ActiveRecord::Base
  amoeba do
    enable
    regex :contents => {:replace => /dog/, :with => 'cat'}
  end
end

Custom Methods

Customize

You may run a custom method or methods to do basically anything you like, simply pass a lambda block, or an array of lambda blocks to the customize directive. Each block must have the same form, meaning that each block must accept two parameters, the original object and the newly copied object. You may then do whatever you wish, like this:

class Post < ActiveRecord::Base
  amoeba do
    prepend :title => "Hello world! "

    customize(lambda { |original_post,new_post|
      if original_post.foo == "bar"
        new_post.baz = "qux"
      end
    })

    append :comments => "... know what I'm sayin?"
  end
end

or this, using an array:

class Post < ActiveRecord::Base
  has_and_belongs_to_many :tags

  amoeba do
    include_association :tags

    customize([
      lambda do |orig_obj,copy_of_obj|
        # good stuff goes here
      end,

      lambda do |orig_obj,copy_of_obj|
        # more good stuff goes here
      end
    ])
  end
end
Override

Lambda blocks passed to customize run, by default, after all copying and field pre-processing. If you wish to run a method before any customization or field pre-processing, you may use override the cousin of customize. Usage is the same as above.

class Post < ActiveRecord::Base
  amoeba do
    prepend :title => "Hello world! "

    override(lambda { |original_post,new_post|
      if original_post.foo == "bar"
        new_post.baz = "qux"
      end
    })

    append :comments => "... know what I'm sayin?"
  end
end

Chaining

You may apply a single preprocessor to multiple fields at once.

class Post < ActiveRecord::Base
  amoeba do
    enable
    prepend :title => "Copy of ", :contents => "Copied contents: "
  end
end

Stacking

You may apply multiple preprocessing directives to a single model at once.

class Post < ActiveRecord::Base
  amoeba do
    prepend :title => "Copy of ", :contents => "Original contents: "
    append :contents => " (copied version)"
    regex :contents => {:replace => /dog/, :with => 'cat'}
  end
end

This example should result in something like this:

post = Post.create(
  :title => "Hello world",
  :contents =>  "I like dogs, dogs are awesome."
)

new_post = post.amoeba_dup

new_post.title # "Copy of Hello world"
new_post.contents # "Original contents: I like cats, cats are awesome. (copied version)"

Like nullify, the preprocessing directives do not automatically enable the copying of associated child records. If only preprocessing directives are used and you do want to copy child records and no include_association or exclude_association list is provided, you must still explicitly enable the copying of child records by calling the enable method from within the amoeba block on your model.

Precedence

You may use a combination of configuration methods within each model's amoeba block. Recognized association types take precedence over inclusion or exclusion lists. Inclusive style takes precedence over exclusive style, and these two explicit styles take precedence over the indiscriminate style. In other words, if you list fields to copy, amoeba will only copy the fields you list, or only copy the fields you don't exclude as the case may be. Additionally, if a field type is not recognized it will not be copied, regardless of whether it appears in an inclusion list. If you want amoeba to automatically copy all of your child records, do not list any fields using either include_association or exclude_association.

The following example syntax is perfectly valid, and will result in the usage of inclusive style. The order in which you call the configuration methods within the amoeba block does not matter:

class Topic < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :topic
  has_many :comments
  has_many :tags
  has_many :authors

  amoeba do
    exclude_association :authors
    include_association :tags
    nullify :date_published
    prepend :title => "Copy of "
    append :contents => " (copied version)"
    regex :contents => {:replace => /dog/, :with => 'cat'}
    include_association :authors
    enable
    nullify :topic_id
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

This example will copy all of a post's tags and authors, but not its comments. It will also nullify the publishing date and dissociate the post from its original topic. It will also preprocess the post's fields as in the previous preprocessing example.

Note that, because of precedence, inclusive style is used and the list of exclude fields is never consulted. Additionally, the enable method is redundant because amoeba is automatically enabled when using include_association.

The preprocessing directives are run after child records are copied and are run in this order.

  1. Null fields
  2. Prepends
  3. Appends
  4. Search and Replace

Preprocessing directives do not affect inclusion and exclusion lists.

Recursing

You may cause amoeba to keep copying down the chain as far as you like, simply add amoeba blocks to each model you wish to have copy its children. Amoeba will automatically recurse into any enabled grandchildren and copy them 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

In this example, when a post is copied, amoeba will copy each all of a post's comments and will also copy each comment's ratings.

Has One Through

Using the has_one :through association is simple, just be sure to enable amoeba on the each model with a has_one association and amoeba will automatically and recursively drill down, like so:

class Supplier < ActiveRecord::Base
  has_one :account
  has_one :history, :through => :account

  amoeba do
    enable
  end
end

class Account < ActiveRecord::Base
  belongs_to :supplier
  has_one :history

  amoeba do
    enable
  end
end

class History < ActiveRecord::Base
  belongs_to :account
end

Has Many Through

Copying of has_many :through associations works automatically. They perform the copy in the same way as the has_and_belongs_to_many association, meaning the actual child records are not copied, but rather the associations are simply maintained. You can add some field preprocessors to the middle model if you like but this is not strictly necessary:

class Assembly < ActiveRecord::Base
  has_many :manifests
  has_many :parts, :through => :manifests

  amoeba do
    enable
  end
end

class Manifest < ActiveRecord::Base
  belongs_to :assembly
  belongs_to :part

  amoeba do
    prepend :notes => "Copy of "
  end
end

class Part < ActiveRecord::Base
  has_many :manifests
  has_many :assemblies, :through => :manifests

  amoeba do
    enable
  end
end

On The Fly Configuration

You may control how amoeba copies your object, on the fly, by passing a configuration block to the model's amoeba method. The configuration method is static but the configuration is applied on a per instance basis.

class Post < ActiveRecord::Base
  has_many :comments

  amoeba do
    enable
    prepend :title => "Copy of "
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

class PostsController < ActionController
  def duplicate_a_post
    old_post = Post.create(
      :title => "Hello world",
      :contents => "Lorum ipsum"
    )

    old_post.class.amoeba do
      prepend :contents => "Here's a copy: "
    end

    new_post = old_post.amoeba_dup

    new_post.title # should be "Copy of Hello world"
    new_post.contents # should be "Here's a copy: Lorum ipsum"
    new_post.save
  end
end

Inheritance

If you are using the Single Table Inheritance provided by ActiveRecord, you may cause amoeba to automatically process child classes in the same way as their parents. All you need to do is call the propagate method within the amoeba block of the parent class and all child classes should copy in a similar manner.

create_table :products, :force => true do |t|
  t.string :type # this is the STI column

  # these belong to all products
  t.string :title
  t.decimal :price

  # these are for shirts only
  t.decimal :sleeve_length
  t.decimal :collar_size

  # these are for computers only
  t.integer :ram_size
  t.integer :hard_drive_size
end

class Product < ActiveRecord::Base
  has_many :images
  has_and_belongs_to_many :categories

  amoeba do
    enable
    propagate
  end
end

class Shirt < Product
end

class Computer < Product
end

class ProductsController
  def some_method
    my_shirt = Shirt.find(1)
    my_shirt.amoeba_dup
    my_shirt.save

    # this shirt should now:
    # - have its own copy of all parent images
    # - be in the same categories as the parent
  end
end

This example should duplicate all the images and sections associated with this Shirt, which is a child of Product

Parenting Style

By default, propagation uses submissive parenting, meaning the config settings on the parent will be applied, but any child settings, if present, will either add to or overwrite the parent settings depending on how you call the DSL methods.

You may change this behavior, the so called "parenting style", to give preference to the parent settings or to ignore any and all child settings.

Relaxed Parenting

The :relaxed parenting style will prefer parent settings.

class Product < ActiveRecord::Base
  has_many :images
  has_and_belongs_to_many :sections

  amoeba do
    exclude_association :images
    propagate :relaxed
  end
end

class Shirt < Product
  include_association :images
  include_association :sections
  prepend :title => "Copy of "
end

In this example, the conflicting include_association settings on the child will be ignored and the parent exclude_association setting will be used, while the prepend setting on the child will be honored because it doesn't conflict with the parent.

Strict Parenting

The :strict style will ignore child settings altogether and inherit any parent settings.

class Product < ActiveRecord::Base
  has_many :images
  has_and_belongs_to_many :sections

  amoeba do
    exclude_association :images
    propagate :strict
  end
end

class Shirt < Product
  include_association :images
  include_association :sections
  prepend :title => "Copy of "
end

In this example, the only processing that will happen when a Shirt is duplicated is whatever processing is allowed by the parent. So in this case the parent's exclude_association directive takes precedence over the child's include_association settings, and not only that, but none of the other settings for the child are used either. The prepend setting of the child is completely ignored.

Parenting and Precedence

Because of the two general forms of DSL config parameter usage, you may wish to make yourself mindful of how your coding style will affect the outcome of duplicating an object.

Just remember that:

  • If you pass an array you will wipe all previous settings
  • If you pass single values, you will add to currently existing settings

This means that, for example:

  • When using the submissive parenting style, you can child take full precedence on a per field basis by passing an array of config values. This will cause the setting from the parent to be overridden instead of added to.
  • When using the relaxed parenting style, you can still let the parent take precedence on a per field basis by passing an array of config values. This will cause the setting for that child to be overridden instead of added to.
A Submissive Override Example

This version will use both the parent and child settings, so both the images and sections will be copied.

class Product < ActiveRecord::Base
  has_many :images
  has_and_belongs_to_many :sections

  amoeba do
    include_association :images
    propagate
  end
end

class Shirt < Product
  include_association :sections
end

The next version will use only the child settings because passing an array will override any previous settings rather than adding to them and the child config takes precedence in the submissive parenting style. So in this case only the sections will be copied.

class Product < ActiveRecord::Base
  has_many :images
  has_and_belongs_to_many :sections

  amoeba do
    include_association :images
    propagate
  end
end

class Shirt < Product
  include_association [:sections]
end
A Relaxed Override Example

This version will use both the parent and child settings, so both the images and sections will be copied.

class Product < ActiveRecord::Base
  has_many :images
  has_and_belongs_to_many :sections

  amoeba do
    include_association :images
    propagate :relaxed
  end
end

class Shirt < Product
  include_association :sections
end

The next version will use only the parent settings because passing an array will override any previous settings rather than adding to them and the parent config takes precedence in the relaxed parenting style. So in this case only the images will be copied.

class Product < ActiveRecord::Base
  has_many :images
  has_and_belongs_to_many :sections

  amoeba do
    include_association [:images]
    propagate
  end
end

class Shirt < Product
  include_association :sections
end

Validating Nested Attributes

If you end up with some validation issues when trying to validate the presence of a child's belongs_to association, just be sure to include the :inverse_of declaration on your relationships and all should be well.

For example this will throw a validation error saying that your posts are invalid:

class Author < ActiveRecord::Base
  has_many :posts

  amoeba do
    enable
  end
end

class Post < ActiveRecord::Base
  belongs_to :author
  validates_presence_of :author

  amoeba do
    enable
  end
end

author = Author.find(1)
author.amoeba_dup

author.save # this will fail validation

Where this will work fine:

class Author < ActiveRecord::Base
  has_many :posts, :inverse_of => :author

  amoeba do
    enable
  end
end

class Post < ActiveRecord::Base
  belongs_to :author, :inverse_of => :posts
  validates_presence_of :author

  amoeba do
    enable
  end
end

author = Author.find(1)
author.amoeba_dup

author.save # this will pass validation

This issue is not amoeba specific and also occurs when creating new objects using accepts_nested_attributes_for, like this:

class Author < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts
end

class Post < ActiveRecord::Base
  belongs_to :author
  validates_presence_of :author
end

# this will fail validation
author = Author.create({:name => "Jim Smith", :posts => [{:title => "Hello World", :contents => "Lorum ipsum dolor}]})

This issue with accepts_nested_attributes_for can also be solved by using :inverse_of, like this:

class Author < ActiveRecord::Base
  has_many :posts, :inverse_of => :author
  accepts_nested_attributes_for :posts
end

class Post < ActiveRecord::Base
  belongs_to :author, :inverse_of => :posts
  validates_presence_of :author
end

# this will pass validation
author = Author.create({:name => "Jim Smith", :posts => [{:title => "Hello World", :contents => "Lorum ipsum dolor}]})

The crux of the issue is that upon duplication, the new Author instance does not yet have an ID because it has not yet been persisted, so the :posts do not yet have an :author_id either, and thus no :author and thus they will fail validation. This issue may likely affect amoeba usage so if you get some validation failures, be sure to add :inverse_of to your models.

Cloning using custom method

If you need to clone model with custom method you can use through:

class ChildPrototype < ActiveRecord::Base
  amoeba do
    through :become_child
  end

  def become_child
    self.dup.becomes(Child)
  end
end

class Child < ChildPrototype
end

After cloning we will get instance of Child instead of ChildPrototype

Remapping associations

If you will need to do complex cloning with remapping associations name you can use remapper:

class ObjectPrototype < ActiveRecord::Base
  has_many :child_prototypes

  amoeba do
    method :become_real
    remapper :remap_associations
  end

  def become_real
    self.dup().becomes( RealObject )
  end

  def remap_associations( name )
    :childs if name == :child_prototypes
  end
end

class RealObject < ObjectPrototype
  has_many :childs
end

class ChildPrototype < ActiveRecord::Base
  amoeba do
    method :become_child
  end

  def become_child
    self.dup().becomes( Child )
  end
end

class Child < ChildPrototype
end

In result we will get next:

prototype = ObjectPrototype.new
prototype.child_prototypes << ChildPrototype.new
object = prototype.amoeba_dup
object.class # => RealObject
object.childs.first.class #=> Child

Configuration Reference

Here is a static reference to the available configuration methods, usable within the amoeba block on your rails models.

through

Set method what we will use for cloning model instead of dup.

for example:

amoeba do
  through :supper_pupper_dup
end

def supper_pupper_dup
  puts "multiplied by budding"
  self.dup
end

Controlling Associations

enable

Enables amoeba in the default style of copying all known associated child records. Using the enable method is only required if you wish to enable amoeba but you are not using either the include_association or exclude_association directives. If you use either inclusive or exclusive style, amoeba is automatically enabled for you, so calling enable would be redundant, though it won't hurt.

include_association

Adds a field to the list of fields which should be copied. All associations not in this list will not be copied. This method may be called multiple times, once per desired field, or you may pass an array of field names. Passing a single symbol will add to the list of included fields. Passing an array will empty the list and replace it with the array you pass.

exclude_association

Adds a field to the list of fields which should not be copied. Only the associations that are not in this list will be copied. This method may be called multiple times, once per desired field, or you may pass an array of field names. Passing a single symbol will add to the list of excluded fields. Passing an array will empty the list and replace it with the array you pass.

clone

Adds a field to the list of associations which should have their associated children actually cloned. This means for example, that instead of just maintaining original associations with previously existing tags, a copy will be made of each tag, and the new record will be associated with these new tag copies rather than the old tag copies. This method may be called multiple times, once per desired field, or you may pass an array of field names. Passing a single symbol will add to the list of excluded fields. Passing an array will empty the list and replace it with the array you pass.

propagate

This causes any inherited child models to take the same config settings when copied. This method may take up to one argument to control the so called "parenting style". The argument should be one of strict, relaxed or submissive.

The default "parenting style" is submissive

for example

amoeba do
  propagate :strict
end

will choose the strict parenting style of inherited settings.

raised

This causes any child to behave with a (potentially) different "parenting style" than its actual parent. This method takes up to a single parameter for which there are three options, strict, relaxed and submissive.

The default "parenting style" is submissive

for example:

amoeba do
  raised :relaxed
end

will choose the relaxed parenting style of inherited settings for this child. A parenting style set via the raised method takes precedence over the parenting style set using the propagate method.

remapper

Set the method what will be used for remapping of association name. Method will have one argument - association name as Symbol. If method will return nil then association will not be remapped.

for example:

amoeba do
  remapper :childs_to_parents
end

def childs_to_parents(association_name)
  :parents if association_name == :childs
end

Pre-Processing Fields

nullify

Adds a field to the list of non-association based fields which should be set to nil during copy. All fields in this list will be set to nil - note that any nullified field will be given its default value if a default value exists on this model's migration. This method may be called multiple times, once per desired field, or you may pass an array of field names. Passing a single symbol will add to the list of null fields. Passing an array will empty the list and replace it with the array you pass.

prepend

Prefix a field with some text. This only works for string fields. Accepts a hash of fields to prepend. The keys are the field names and the values are the prefix strings. An example scenario would be to add a string such as "Copy of " to your title field. Don't forget to include extra space to the right if you want it. Passing a hash will add each key value pair to the list of prepend directives. If you wish to empty the list of directives, you may pass the hash inside of an array like this [{:title => "Copy of "}].

append

Append some text to a field. This only works for string fields. Accepts a hash of fields to append. The keys are the field names and the values are the prefix strings. An example would be to add " (copied version)" to your description field. Don't forget to add a leading space if you want it. Passing a hash will add each key value pair to the list of append directives. If you wish to empty the list of directives, you may pass the hash inside of an array like this [{:contents => " (copied version)"}].

set

Set a field to a given value. This should work for almost any type of field. Accepts a hash of fields and the values you want them set to.. The keys are the field names and the values are the prefix strings. An example would be to add " (copied version)" to your description field. Don't forget to add a leading space if you want it. Passing a hash will add each key value pair to the list of append directives. If you wish to empty the list of directives, you may pass the hash inside of an array like this [{:approval_state => "open_for_editing"}].

regex

Globally search and replace the field for a given pattern. Accepts a hash of fields to run search and replace upon. The keys are the field names and the values are each a hash with information about what to find and what to replace it with. in the form of . An example would be to replace all occurrences of the word "dog" with the word "cat", the parameter hash would look like this :contents => {:replace => /dog/, :with => "cat"}. Passing a hash will add each key value pair to the list of regex directives. If you wish to empty the list of directives, you may pass the hash inside of an array like this [{:contents => {:replace => /dog/, :with => "cat"}].

override

Runs a custom method so you can do basically whatever you want. All you need to do is pass a lambda block or an array of lambda blocks that take two parameters, the original object and the new object copy. These blocks will run before any other duplication or field processing.

This method may be called multiple times, once per desired customizer block, or you may pass an array of lambdas. Passing a single lambda will add to the list of processing directives. Passing an array will empty the list and replace it with the array you pass.

customize

Runs a custom method so you can do basically whatever you want. All you need to do is pass a lambda block or an array of lambda blocks that take two parameters, the original object and the new object copy. These blocks will run after all copying and field processing.

This method may be called multiple times, once per desired customizer block, or you may pass an array of lambdas. Passing a single lambda will add to the list of processing directives. Passing an array will empty the list and replace it with the array you pass.

Known Limitations and Issues

The regular expression preprocessor uses case-sensitive String#gsub. Given the performance decreases inherrent in using regular expressions already, the fact that character classes can essentially account for case-insensitive searches, the desire to keep the DSL simple and the general use cases for this gem, I don't see a good reason to add yet more decision based conditional syntax to accommodate using case-insensitive searches or singular replacements with String#sub. If you find yourself wanting either of these features, by all means fork the code base and if you like your changes, submit a pull request.

The behavior when copying nested hierarchical models is undefined. Copying a category model which has a parent_id field pointing to the parent category, for example, is currently undefined.

The behavior when copying polymorphic has_many associations is also undefined. Support for these types of associations is planned for a future release.

For Developers

You may run the rspec tests like this:

bundle exec rspec spec

TODO

  • add ability to cancel further processing from within an override block
  • write some spec for the override method

amoeba's People

Contributors

brentdodell avatar budu avatar ciddennis avatar gingermusketeer avatar hirurg103 avatar imdrasil avatar jrmhaig avatar mstate avatar palkan avatar robinroestenburg avatar rocksolidwebdesign avatar simonoff avatar srachner avatar swistaczek avatar tagliala avatar vieditcom avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

amoeba's Issues

Support for counter_cache

I recently added a counter_cache to a model only to be surprised when duplication via amoeba brought it out of sync. I should have anticipated this, but it would be nice for amoeba to "just work" in this situation.

Here's the scenario. I dup a deep model hierarchy. One of the collections (A) on the root object has another collection (B). Collection B is not to be duplicated. Easy enough; I simply did not include amoeba do in the class for A. But then I added a counter_cache for collection B.

On the clone collection B empty, but the counter_cache says that it has items. The fix is simple:

  amoeba do
    set signups_count: 0
    propagate #this is in a base class
  end

But it would be nice if the counter cache stayed in sync without doing anything special. I'm not sure how it would be implemented but probably involves this: http://apidock.com/rails/ActiveRecord/CounterCache/reset_counters

include/exclude_association with conditions

I'd like to use include/exclude_association with conditions, similar to the :if option of the validation methods in rails. For example

amoeba do
  include_association :tags, if: my_condition_method?
end

def my_condition_method?
   # ... do some stuff that returns bool
end

Any chance that you build that in?

Bug with clone method?

Hi!
I doing the same thing like in docs:

class Post < ActiveRecord::Base
  has_many :post_widgets
  has_many :widgets, :through => :post_widgets

  amoeba do
    enable
    clone [:widgets]
  end
end

Then when I cloned post instance, amoeba adds extra records into widgets table, but not into post_widgets where they should really be.

Is this a bug?

(Rails 4.2.1, amoeba 3.0.0)

Applying customize to children

Love the gem.

So in my application, I'm saving duplicated objects in the same table as the original object. I associate the two (with self-referential associations), so I'm able to find the "draft" version of an object. It works great for the most part.

Assume all objects have an :approved_version_id, with this amoeba configuration:

    amoeba do
      include_association :ratings
      exclude_association [:approved, :draft]
      customize(lambda {|live_obj, draft_obj|
        draft_obj.approved_version_id = live_obj.id
      })
    end

(see the entire sample of 3 models at https://gist.github.com/stevehodges/e85a1e367d546d527984)

By default, propagation uses submissive parenting, meaning the config settings on the parent will be applied, but any child settings, if present, will either add to or overwrite the parent settings depending on how you call the DSL methods.

I've found that the customize directive doesn't get passed to the model's children. The easy way around this is to define another amoeba block in the child, which is convenient when the child model is also duplicating its children (the grandchildren).

However, I can't figure out a way to set that customize directive on the grandchild.

The result: the original object has an approved_version_id, as does its child. The grandchild does not.

Gist of Rails models
Gist of Rails Console output

  1. Am I doing something wrong, or is there any way for customize to be applied to children (including children which don't have an amoeba block)?
  2. Are there any workarounds if this isn't supported?

self has_one creates another record in 2.0.0

Hi,
We experiment some troubles lately, as took us time to find the root of the problem, here it is with the solution, to keep a record for next ones :

In 2.0.0 version of amoeba, an object which has a self references through a belongs_to and a has_one make a second record on cloning.

class Post < ActiveRecord::Base

belongs_to :next_post, class_name: 'Post', inverse_of: :father_post
has_one :father_post, class_name: 'Post', dependent: :nullify, inverse_of: :next_post, foreign_key: :next_post_id

amoeba do
  enable
  clone :comments
end

To resolve that we had to add a '''exclude_field :father_post''' inside the amoeaba block.

NB: it is exclude_fieldand NOT exclude_associationin 2.0.0

Dunno how and why it would create another record in addition to the clone but that how we stopped it from doing it !

Cheers.

Nullify action not matching documentation regarding nullified field value

Howdy,

Reading over the docs for nullify, while it does say that it puts nil into specified fields, it also says those fields should take on their default values. At least for us using PostgreSQL, that is not the case. The nil values installed via 'nullify' as nil/null and then are stored that way (or blow up in the save for fields that have default values, but do not allow null values).

I suspect this is a DB thing and may be different between DBs.

What we are really looking for is a way to exclude copying a simple field/attribute so that the 'new' record (the copy) has the initial/default value as if you had just executed a .new on the model. Perhaps a "default" command that, like "nullify", would not copy the value but would leave it in an unmolested state (i.e. DB default, like .new). Or, alternately, from my other post, extend the concept of "exclude/include_field" to record attributes and/or add a exclude/include_attr that would leave un-specified record attribute fields alone.

Self-referential has_many :through

How to copy the following association? Seems doesn't work right now. Thx.

class Field < ActiveRecord::Base
  has_many :field_trees
  has_many :subfields, through: :field_trees, dependent: :destroy
end

class FieldTree < ActiveRecord::Base
  belongs_to :field
  belongs_to :subfield, :class_name => "Field"
end

doesn't deal with validations

Not sure of the details here - I only know that the dup subcollection was unable to save b/c of validation errors.

I was started to debug it, but then I simply flipped a bit on this library instead, rewrite my copy functions as

  alias :orig_dup :dup
  def dup
    copy = orig_dup
    model_runs.each {|model_run| copy.model_runs << model_run.dup}
    copy
  end

and moved on. Filed this ticket here in hopes it would trigger some better test case on your part.

Looks like it'll be a great library when it's finished. 

Association between copied objects is missing

Hi, first amoeba is a great gem and i really enjoy using it :)

But now my following situation doesn't seem to work:

3 Models:

project.rb
has_many :risks
has_many :targets
amoeba do
enable
prepend :project_name => "Copy of: "
end

risk.rb
belongs_to :project
has_many :risk_target_allocations
has_many :targets, :through => :risk_target_allocations
amoeba do
enable
end

target.rb
belongs_to :project
has_many :risk_target_allocations
has_many :risks, :through => :risk_target_allocations

With the setup described above amoeba generates a copy of a project, the risks of that project and the targets of that project. But the association between the copied risks and the copied targets isn't showing up anymore.

I'm not sure if this is a real issue or that I've done something wrong.

Thanks for some feedback.

include/exclude_association is undefined, yet other amoeba configurations work.

Amoeba config block:

  amoeba do
    enable

    include_association :redacted_associatons_1
    include_association :redacted_associatons_2
    include_association :redacted_associatons_3

    nullify :created_at
    nullify :updated_at
    nullify :published_at

    set published: false
    prepend title: 'Copy of '
  end

Here's the error:

/Users/austinsaylor/.rvm/gems/ruby-1.9.3-p484/gems/hobo_support-2.0.1/lib/hobo_support/string.rb:23:in `class_eval': undefined method `include_association' for #<Amoeba::Dsl::Config:0x007fbea38eca60> (NoMethodError)

Could it be a conflict with Hobo? (http://www.hobocentral.net/manual/about)

The amoeba_dup works with the nullify, set, and prepend config when include_association isn't present.

carrierwave files cloning

Vaughn, thanks for awesome gem.

I found it doesn't maintain carrierwave cloning. Do you plan to maintain it?

Relations and related items does not copied/cloned

So i have Tour model wich defines amoeba like this:

  amoeba do
    enable
    append :name => " COPY"
    include_field [:tags, :managers]
    clone [:days, :departures, :assets, :prices, :locations]
  end

All related and relation models has this:

  amoeba do
    enable
  end

From 'rails c' i do

Tour.first.amoeba_dup.save!

And model itself is duplicated.. But relations to tags and managers does not respected aswell as other relations does not get cloned...
All i see is INSERT SQL query to Tours table..

Not updating the value after amoeba_dup

Assume i am having a Model like as below :

Recipe is the model which is having a "name" attribute. So Recipe is having name as 'test1'

Now i am able to made a copy of recipe by amoeba_dup method and able to save that into database also like following :
r = Recipe.find(1).amoeba_dup
r.save;

But before storing i want to update the name of the recipe after made a copy.
r = Recipe.find(1).amoeba_dup
r.name = 'test2';
r.save;

While doing this iam getting :
NoMethodError: undefined method `name=' for #Recipe:0x0000000ab971e8

First of all, is it possible to update the value after copy by amoeba_dup ?

Could you please clarify me...Thank you

Child association might be duplicated before the parent?

First off, in what order are the model and associations duplicated? I couldn't tell from the documentation.

Secondly, I'm trying to duplicate a Site record, which in turn is duplicating the association. This is fine. The issue is that I'm hitting a validation on the child Placement model before the duplicated Site is created.

Simplified code...

class Placement < ActiveRecord::Base
  belongs_to :site, inverse_of: :placements
  validate :site_must_have_stuff, if: proc { |record| record.some_condition? }

  def site_must_have_stuff
    if site.url.blank?
      errors.add(:url, "must be present")
      false
    else
      true
    end
  end
end

class Site < ActiveRecord::Base
  has_many :placements, inverse_of: :site
end

site = Site.new(attributes)
site.amoeba_dup  #=> (Placement invalid error)

Am I doing something wrong? Is the expected behavior to create the duplicated associations before the object you call amoeba_dup on? And if all of this is true, is there a way to coerce the duplication order?

Nullify and set don't work on an STI model

Is this intended? We have the following

class Activity < ActiveRecord::Base
  amoeba do
    nullify :uuid
  end
end

module Activities
  class Video < ::Activity
  end
end

activity = Activities::Video.last
cloned_activity = activity.amoeba_dup
cloned_activity.uuid == activity.uuid # true when I would have expected false

It does work when I move the amoeba block into the the child class.

Switch databases before saving

Is there a way to use this gem and switch to a database that has the same structure and then save the duped data into that new database?

Enable for all models by default

We have lots of models that should be copied recursively. This means that each involved model needs to have amoeba enabled. This is error-prone since a developer can forget to include new models. Because of this, we simply enable by convention all models by hand. That means adding an enable-amoeba block which is cumbersome.

Is it possible to just enable amoeba for all models?

amoeba method doesn't deal with class hierarchies correctly

If you have have:

class Post
  has_many :comments
  amoeba do enabled end
end

class EnhancedPost < Post
end

and then following code fails:

a=EnhancedPost.find(... a enhanced post with comments)
b = a.dup
b.comments.size.should == b.comments.size ## FAILS

The reason is that the Amoeba::Config in the EnhancedPost class has enabled=false (the default) b/c it is not the same instance as the Amoeba::Config in the Post base class.

I'm thinking that the amoeba block in EnhancedPost should perhaps be written this way:

class EnhancedPost < Post
  amoeba do 
      super.amoeba ## get the defaults from the base class
      ## then do anything special for EnhancedPost class
  end
end

and I'm wishing that you could include a note about the behaviour of Amoeba and inherited classes in your wonderful documentation, it took two of us quite a few hours to figure out what was happening.

Thanks!
Rob.

ownership transfer

Hi @simonoff I just tried transfering ownership to the amoeba-rb organization but github will not let me do that unless I have admin rights.

custom serialization

Sorry to open the ticket so fast again, but the serialization only had one problem and then it works with the 'old_post'

    def load(str)
      str.split(',').collect(&:to_i)
    end

I was missing the to_i so it was trying to match ['1'] to [1]. This all seems rather silly but I use it in my app to store a path so I can search for [6,7] in the database as '6,7'. It is working fine, i just thought i would simplify the cloning.

I wish there was more documentation for the custom serialization to actually prove it's a Rails feature ;-) The only thing I can find in the code is here:
activerecord/lib/active_record/attribute_methods/serialization.rb:

          coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
                    class_name
                  else
                    Coders::YAMLColumn.new(class_name)
                  end

After fixing the custom serializer the old_post works fine, but when cloning it throws an exception

NoMethodError: undefined method `to_i' for [1, 2]:Array
    from /Users/a/RubyMineProjects/amoeba/spec/support/models.rb:79:in `collect'
    from /Users/a/RubyMineProjects/amoeba/spec/support/models.rb:79:in `load'
    from /Users/a/.rvm/gems/ruby-1.9.3-p125-falcon@amoeba/gems/activerecord-3.2.3/lib/active_record/attribute_methods/serialization.rb:24:in `unserialize'
    from /Users/a/.rvm/gems/ruby-1.9.3-p125-falcon@amoeba/gems/activerecord-3.2.3/lib/active_record/attribute_methods/serialization.rb:15:in `unserialized_value'

So what is interesting is that if you take the first custom_thing and you dup & save it, it works fine. But when you dup and save the post object, it throws the exception. So there is something happening through the recursion. I looked at the code but don't see anything that would explain it to me.

Applying different amoeba configurations for different use cases

Hi guys,

I have a small challenge. I'd like to apply different configs for amoeba, when I have different cases of cloning my objects.

Let assume that I have class Job with fields: name, description. The thing is that in one case I'd like to clone name & description and in the other case, I'd like to skip cloning description.

The other example can be, that I have a structure: MilestoneTemplate, TaskTemplate. Milestone template has many task templates. For one case I cast objects into the other classes: Milestone & Task and in the other case, I'd like to keep the original classes.

I'm not sure how to handle these cases - conditionally applying logic for cloning across multiple classes. Most likely I'll write custom thing for that. I'd really like to use / extend amoeba for this case. Maybe it could be solved by e.g. using refinements and overriding method for cloning?

Rails 3.1 custom serialization and it does not clone correctly

This gem is amazing. This is some serious meta-fu. Is very impressive.

I am using a custom serialization class in one of my models and I am not getting a correct clone.

This is probably some bad programming but I have a class to store an array of numbers as a sorted comma separated string. I don't think it is cloning correctly.

  class ArrayPack
    def load(array_or_text)
      array_or_text = array_or_text.split(',').collect(&:to_i) if array_or_text.class == String
      return [] if array_or_text.blank?
      raise "Need to search a string or array, you passed #{array_or_text}" unless array_or_text.class == Array
      array_or_text.collect(&:to_i).sort
    end

    def dump(int_array)
      self.load(int_array).join(',')
    end
  end

  serialize :path, ArrayPack.new

Issue with cloning has_many :through

I'm on Rails 4.1.9 and when trying to clone a through association I'm getting the following error.

Cannot modify association 'Client#cost_timespans' because the source reflection class 'CostTimespan' is associated to 'Cost' via :has_many.

Heres my simplified code

class CostTimespan < ActiveRecord::Base

  belongs_to :cost

end

class Cost < ActiveRecord::Base

  has_many :cost_timespans, -> { order(end_date: :desc) }

end

class Client < ActiveRecord::Base
  has_many :costs
  has_many :cost_timespans, through: :costs

  amoeba do
    include_association :available_services
    include_association :sign_cancellation_charges
    include_association :costs
    include_association :cost_timespans

    clone [:cost_timespans]

    customize ->(original_client, new_client) {
      new_client.service_ids = original_client.service_ids
      new_client.assign_attributes new_client.attributes.select { |k, v| k.ends_with?('_count') }.inject({}) { |h, (k, v)| h[k] = 0; h }
    }
  end
end

Here's the stack trace

activerecord (4.1.9) lib/active_record/associations/through_association.rb:86:in `ensure_mutable'
activerecord (4.1.9) lib/active_record/associations/has_many_through_association.rb:84:in `build_through_record'
activerecord (4.1.9) lib/active_record/associations/has_many_through_association.rb:46:in `block in concat_records'
activerecord (4.1.9) lib/active_record/associations/has_many_through_association.rb:45:in `concat_records'
activerecord (4.1.9) lib/active_record/associations/collection_association.rb:157:in `concat'
activerecord (4.1.9) lib/active_record/associations/has_many_through_association.rb:36:in `concat'
activerecord (4.1.9) lib/active_record/associations/collection_proxy.rb:972:in `<<'
amoeba (3.0.0) lib/amoeba/macros/has_many.rb:19:in `block in follow_with_clone'
activerecord (4.1.9) lib/active_record/relation/delegation.rb:46:in `each'
amoeba (3.0.0) lib/amoeba/macros/has_many.rb:16:in `follow_with_clone'
amoeba (3.0.0) lib/amoeba/macros/has_many.rb:6:in `follow'
amoeba (3.0.0) lib/amoeba/cloner.rb:108:in `follow_association'
amoeba (3.0.0) lib/amoeba/cloner.rb:80:in `block in follow_all_except_excludes'
amoeba (3.0.0) lib/amoeba/cloner.rb:78:in `follow_all_except_excludes'
amoeba (3.0.0) lib/amoeba/cloner.rb:94:in `apply_associations'
amoeba (3.0.0) lib/amoeba/cloner.rb:102:in `apply'
amoeba (3.0.0) lib/amoeba/cloner.rb:23:in `run'
amoeba (3.0.0) lib/amoeba/instance_methods.rb:34:in `amoeba_dup'
app/controllers/account/clients_controller.rb:39:in `duplicate'
actionpack (4.1.9) lib/action_controller/metal/implicit_render.rb:4:in `send_action'
actionpack (4.1.9) lib/abstract_controller/base.rb:189:in `process_action'
actionpack (4.1.9) lib/action_controller/metal/rendering.rb:10:in `process_action'
actionpack (4.1.9) lib/abstract_controller/callbacks.rb:20:in `block in process_action'
activesupport (4.1.9) lib/active_support/callbacks.rb:113:in `call'
activesupport (4.1.9) lib/active_support/callbacks.rb:149:in `block in halting_and_conditional'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:229:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:229:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:166:in `block in halting'
activesupport (4.1.9) lib/active_support/callbacks.rb:86:in `run_callbacks'
actionpack (4.1.9) lib/abstract_controller/callbacks.rb:19:in `process_action'
actionpack (4.1.9) lib/action_controller/metal/rescue.rb:29:in `process_action'
actionpack (4.1.9) lib/action_controller/metal/instrumentation.rb:31:in `block in process_action'
activesupport (4.1.9) lib/active_support/notifications.rb:159:in `block in instrument'
activesupport (4.1.9) lib/active_support/notifications/instrumenter.rb:20:in `instrument'
activesupport (4.1.9) lib/active_support/notifications.rb:159:in `instrument'
actionpack (4.1.9) lib/action_controller/metal/instrumentation.rb:30:in `process_action'
actionpack (4.1.9) lib/action_controller/metal/params_wrapper.rb:250:in `process_action'
activerecord (4.1.9) lib/active_record/railties/controller_runtime.rb:18:in `process_action'
actionpack (4.1.9) lib/abstract_controller/base.rb:136:in `process'
actionview (4.1.9) lib/action_view/rendering.rb:30:in `process'
actionpack (4.1.9) lib/action_controller/metal.rb:196:in `dispatch'
actionpack (4.1.9) lib/action_controller/metal/rack_delegation.rb:13:in `dispatch'
actionpack (4.1.9) lib/action_controller/metal.rb:232:in `block in action'
actionpack (4.1.9) lib/action_dispatch/routing/route_set.rb:82:in `dispatch'
actionpack (4.1.9) lib/action_dispatch/routing/route_set.rb:50:in `call'
actionpack (4.1.9) lib/action_dispatch/journey/router.rb:73:in `block in call'
actionpack (4.1.9) lib/action_dispatch/journey/router.rb:59:in `call'
actionpack (4.1.9) lib/action_dispatch/routing/route_set.rb:685:in `call'
meta_request (0.3.4) lib/meta_request/middlewares/app_request_handler.rb:13:in `call'
meta_request (0.3.4) lib/meta_request/middlewares/meta_request_handler.rb:13:in `call'
rack-ssl-enforcer (0.2.6) lib/rack/ssl-enforcer.rb:56:in `call'
warden (1.2.3) lib/warden/manager.rb:35:in `block in call'
warden (1.2.3) lib/warden/manager.rb:34:in `call'
rack (1.5.2) lib/rack/etag.rb:23:in `call'
rack (1.5.2) lib/rack/conditionalget.rb:25:in `call'
rack (1.5.2) lib/rack/head.rb:11:in `call'
actionpack (4.1.9) lib/action_dispatch/middleware/params_parser.rb:27:in `call'
actionpack (4.1.9) lib/action_dispatch/middleware/flash.rb:254:in `call'
rack (1.5.2) lib/rack/session/abstract/id.rb:225:in `context'
rack (1.5.2) lib/rack/session/abstract/id.rb:220:in `call'
actionpack (4.1.9) lib/action_dispatch/middleware/cookies.rb:562:in `call'
activerecord (4.1.9) lib/active_record/query_cache.rb:36:in `call'
activerecord (4.1.9) lib/active_record/connection_adapters/abstract/connection_pool.rb:621:in `call'
activerecord (4.1.9) lib/active_record/migration.rb:380:in `call'
actionpack (4.1.9) lib/action_dispatch/middleware/callbacks.rb:29:in `block in call'
activesupport (4.1.9) lib/active_support/callbacks.rb:82:in `run_callbacks'
actionpack (4.1.9) lib/action_dispatch/middleware/callbacks.rb:27:in `call'
actionpack (4.1.9) lib/action_dispatch/middleware/reloader.rb:73:in `call'
actionpack (4.1.9) lib/action_dispatch/middleware/remote_ip.rb:76:in `call'
rollbar (1.4.2) lib/rollbar/middleware/rails/rollbar.rb:24:in `block in call'
rollbar (1.4.2) lib/rollbar.rb:754:in `scoped'
rollbar (1.4.2) lib/rollbar/middleware/rails/rollbar.rb:22:in `call'
better_errors (2.0.0) lib/better_errors/middleware.rb:84:in `protected_app_call'
better_errors (2.0.0) lib/better_errors/middleware.rb:79:in `better_errors_call'
better_errors (2.0.0) lib/better_errors/middleware.rb:57:in `call'
rack-contrib (1.1.0) lib/rack/contrib/response_headers.rb:17:in `call'
meta_request (0.3.4) lib/meta_request/middlewares/headers.rb:16:in `call'
actionpack (4.1.9) lib/action_dispatch/middleware/debug_exceptions.rb:17:in `call'
rollbar (1.4.2) lib/rollbar/middleware/rails/show_exceptions.rb:22:in `call_with_rollbar'
actionpack (4.1.9) lib/action_dispatch/middleware/show_exceptions.rb:30:in `call'
railties (4.1.9) lib/rails/rack/logger.rb:38:in `call_app'
railties (4.1.9) lib/rails/rack/logger.rb:20:in `block in call'
activesupport (4.1.9) lib/active_support/tagged_logging.rb:68:in `block in tagged'
activesupport (4.1.9) lib/active_support/tagged_logging.rb:26:in `tagged'
activesupport (4.1.9) lib/active_support/tagged_logging.rb:68:in `tagged'
railties (4.1.9) lib/rails/rack/logger.rb:20:in `call'
quiet_assets (1.0.2) lib/quiet_assets.rb:18:in `call_with_quiet_assets'
actionpack (4.1.9) lib/action_dispatch/middleware/request_id.rb:21:in `call'
rack (1.5.2) lib/rack/methodoverride.rb:21:in `call'
rack (1.5.2) lib/rack/runtime.rb:17:in `call'
activesupport (4.1.9) lib/active_support/cache/strategy/local_cache_middleware.rb:26:in `call'
rack (1.5.2) lib/rack/lock.rb:17:in `call'
actionpack (4.1.9) lib/action_dispatch/middleware/static.rb:84:in `call'
rack (1.5.2) lib/rack/sendfile.rb:112:in `call'
railties (4.1.9) lib/rails/engine.rb:514:in `call'
railties (4.1.9) lib/rails/application.rb:144:in `call'
rack (1.5.2) lib/rack/content_length.rb:14:in `call'
thin (1.6.2) lib/thin/connection.rb:86:in `block in pre_process'
thin (1.6.2) lib/thin/connection.rb:84:in `pre_process'
thin (1.6.2) lib/thin/connection.rb:53:in `process'
thin (1.6.2) lib/thin/connection.rb:39:in `receive_data'
eventmachine (1.0.3) lib/eventmachine.rb:187:in `run'
thin (1.6.2) lib/thin/backends/base.rb:73:in `start'
thin (1.6.2) lib/thin/server.rb:162:in `start'
rack (1.5.2) lib/rack/handler/thin.rb:16:in `run'
rack (1.5.2) lib/rack/server.rb:264:in `start'
railties (4.1.9) lib/rails/commands/server.rb:69:in `start'
railties (4.1.9) lib/rails/commands/commands_tasks.rb:81:in `block in server'
railties (4.1.9) lib/rails/commands/commands_tasks.rb:76:in `server'
railties (4.1.9) lib/rails/commands/commands_tasks.rb:40:in `run_command!'
railties (4.1.9) lib/rails/commands.rb:17:in `<top (required)>'
spring (1.0.0) lib/spring/client/rails.rb:29:in `call'
spring (1.0.0) lib/spring/client/command.rb:7:in `call'
spring (1.0.0) lib/spring/client.rb:23:in `run'
spring (1.0.0) bin/spring:31:in `<top (required)>'
bin/rails:9:in `<main>'

initialize_dup and ruby 2.0.0

I'm getting this error (amoeba 2.0.0) after upgrading to ruby 2.0.0

NoMethodError: private method `initialize_dup' called for #<Class:something>

How to prevent callbacks?

I tried out amoeba and running in a problem. My model has an after_create callback where some associated datas will be created. The user can change these predefined datas. On dup I will duplicate the changed associated datas. The original data I will not clone.

New release coming soon?

Hello! What does the roadmap look like for the next gem release? We're using a few features from master and would love to be able to use a true released version before releasing.

Is the usage of On The Fly configurations thread-safe?

Let's say one thread calls the amoeba method (as in, some_object.class.amoeba do ...) to set custom configurations. But then before it has a chance to complete its some_object.amoeba_dup call, another thread calls amoeba on the same type of object (but a different instance) and sets configurations differently. Is the process happening in amoeba_dup on that first thread/object affected by that second call to amoeba?

If so, is there a supported way to ensure thread safety and concurrency while using On The Fly configurations?

Deep inheritance cloning

Hi,

We use amoeba to preview modifications on our data models, but we have an issue with deep inheritance as described below:

amoeba_inheritance

Basically a box has_many sub_products. In our example, box.sub_products returns SubSubProduct objects and we are unable to access to a parent class association, like in the graph above.
box.amoeba_dup.sub_products.first.another_product returns nil instead of AnotherProduct instance.

We have created a dummy app that reproduces the problem, you can find specs in the following repository: https://github.com/kdisneur/amoeba_inheritance

We've been really happy with amoeba so far and we're sorry we don't come with a fix (for now) but we wanted to share the issue with you.

Kevin

before_validation called twice

before_validation seems to be called twice.

Callback:

before_validation :convert_tax_to_decimal, if: :tax_changed?

private
def convert_tax_to_decimal
  puts 'here'
  self.tax ||= 0
  self.tax = tax / 100
end

Controller:

job = current_business.jobs.find(params[:job_id])
copy_job = job.amoeba_dup
copy_job.save!
copy_job.build_client if copy_job.client.nil?
redirect_to edit_job_path(copy_job)

Output:

here
here

undefined method `include_association'

I'm trying to load index action
My model

class Responder < ActiveRecord::Base
    has_many :responder_actions
    validates :name, presence: true, uniqueness: true
    accepts_nested_attributes_for :responder_actions

    def self.search(search)
      if search
        where('name LIKE ?', "%#{search}%").order(id: :desc)
      else
        all.order(id: :desc)
      end
    end

    amoeba do
        enable
        prepend :title => "Copy of "
        include_association :responder_actions
    end
end

I'm getting an error

NoMethodError at /dashboard/messages/responders
undefined method `include_association' for #<Amoeba::Dsl::Config:0x007ff83c37cd78>

Where is my mistake?
amoeba 2.1.0
rails 4.1.7

Models Saved and Committed on amoeba_dup (Rails 4)

When I call amoeba_dup, everything works correctly, except that all cloned models and associations are committed to the database without calling save. Here is my config block:

35 amoeba do
36 include_field [:campaign_providers, :data_fields, :delivery_methods]
37 append :name => ' (copy)'
38 end

in the console i type: ModelName.last.amoeba_dup and all models and associations are saved/commited. This should only happen when i save the model after calling amoeba_dup.

Fails when presence validation uses _id

Given I have a post and comments, if Comment has:

belongs_to :post
validates :post, presence: true

Then when I do post.amoeba_dup it clones the post and the comments.

However if my Comment model has:

belongs_to :post
validates :post_id, presence: true

amoeba won't set the post_id, so the comments are not saved, the error for comments contains:

@messages={:post_id=>["can't be blank"]}

Overwriting ActiveRecord::Base#dup

Hi,
I have trouble with using Amoeba together with PaperTrail ( ). The reason for that is overwrite of ActiveRecord::Base#dup by Amoeba. PaperTrail uses .dup method to clone object and serialize it. Is there any way to use .dup method by skiping Amoeba part for 'amobeoized' ( ๐Ÿ˜„ ) models?

Loading Amoeba causes nil and false to be frozen

The following code will cause nil and false to be frozen after loading amoeba. This is unexpected imho and causes problems in code that depends on object.frozen?.

class Config
  DEFAULTS = {
    enabled:        false,
    inherit:        false,
    do_preproc:     false,
    parenting:      false,
    raised:         false,
    dup_method:     :dup,
    remap_method:   nil,
    ...
  }

  ...
  DEFAULTS.freeze

  DEFAULTS.each do |key, value|
    value.freeze
    ...
  end

I don't think freezing every value inside the hash is needed here?

Can exclude_field and include_field be extended to attributes?

Howdy,

I understand that as things are, include_field and exclude_field are really only for associations (it might be more clear to new users to call them include_assoc and exclude_assoc, just a thought as the 'field' part tripped me up a bit at first). However, I can see some serious benefit to allowing them and the associated behavior to be used for normal, non-associative record attributes as well.

For example, I have a model with 30 fields in it and when a dup() is called, I want to exclude 25 of them. For those 25, I want them to get the default value for a new record. Of the 5 remaining, 3 are associations and 2 are regular fields. If I could use the inclusive model for everything, then my amoeba block would be nice and short (5 include_field or one with an array of the fields/attrs to copy and leave all the rest alone). Right now, I have a lengthy series of nullify lines (which don't exactly do the same thing as exclude_ would -- see the other issue I created about nullify).

Alternately, if refitting the include/exclude_field methods is a problem, would be to add include/exclude_attr that would work the same as include/exclude_field, but for non-assoc fields.

passing in parameter to dup method?

When creating a deep-copy of a model, I'd like to pass in a parameter to be used in the preprocessing of fields. Is it possible to send parameters in to model.dup? Or is it necessary to instead do this logic separately after running the dup (but before actually saving the deep-copy to the database).

Thanks for your help--not to mention writing a helpful gem!

docs question.

Reading through Known Limitations... section in the docs, there is a part that talks about case sensitive substitutions using gsub.
I may have not understood correctly what you meant gsub is case sensitive. gsub doesn't care about case sensitivity unless you pass a string, otherwise when passing a regexp it depends on the /i flag determining case insensitivity...

Clone a has_many collection

In my use-case, I have an incoming email message, which has_many :attachments. I want to copy these attachments to another model. To use amoeba with this, my code now is as follows:

    def attachments_deep_copy
        self.attachments.map {|attachment| attachment.amoeba_dup }
    end

What seems nicer to me, is to add an amoeba_dup method to ActiveRecord::Associations::CollectionProxy, so it would simply be possible to use self.attachments.amoeba_dup.

Set user_id in association

Hi there,

I have a Tasklist & a Task Model.

I'm using amoeba to duplicate one task list with its associated tasks.
Both Tasklist & Task have a user_id field.

The user_id is set to NULL by default.

When I duplicate, I want the user_id to be set to the current_user.id (from Devise).

I'm able to copy the task list with the proper user_id using:

   @tasklist = Tasklist.find(topic_params[:tasklist])
   @tasklist.user_id = current_user.id
   @tasklist.tasks.user_id = current_user.id
   @tasklist.amoeba_dup.save

Tasks are also properly copied but the user_id is not updated in the copied tasks (only task list).
I can't use the current_user.id in the Model with :set.

So I'm wondering how I can achieve this?

Thanks!
Vincent

How do i duplicate a `belongs_to` relationship?

I have a model that looks like this:

class Foo < ActiveRecord::Base
  belongs_to :bar

  amoeba do
    enable
    include_field :bar
  end
end

But it doesn't work --- how should i go about duplicating the belongs_to association with amoeba?

Dump to file (feature request)

I was wondering if it is possible to dump the copied model to a file so it can be loaded from another application with same model configuration. I search a way to replicate content between same apps but with complex models.

Polymorphic workaround?

I can't seem to find a 'great' way to hook up polymorphic has many. Any preferred workarounds?

Dealing with translations not working

Hi,
we use Globalize3 (https://github.com/svenfuchs/globalize3) which is used to have translations for your objects. That is one can use things like below, which generates the needed translations in a table:

class Answer < ActiveRecord::Base
    attr_accessible :text
    belongs_to :question
    translate :text

    amoeba do
        enable
    end
end

Globalize3 creates a has_many :translations relation. If one creates a new answer, sets the text and saves everything works fine, and the text is also saved to the translations' table. If one dups an answer and saves it, it won't create a translation row. If one saves again it creates a nil raw. And if one then changes the text of the duplicated element, the translation of the first element is changed:

a = Answer.new
a.text = "Test"
a.save

-> Everything ok, entry in answers table and translations table with correct text

b = a.dup
b.save

-> Nothing happens in translations table

b.save (again)

-> Empty entry in translations table

b.text = "Test 2"
b.save

-> Changes in the row of object a

Summary: The has_many :translations has to be recognized and used correctly, which is not the case at the moment

When using amoeba_dub : undefined method for NilClass

I tried amoeba in a rails 2 console (with Ruby 2.1):

class Dict < ActiveRecord::Base
  belongs_to  :user
  has_many    :cards,  dependent: :destroy
  has_many :idioms, through: :cards
end
class Card < ActiveRecord::Base
  belongs_to :dict
  has_many  :idioms, dependent: :destroy
  amoeba do
    exclude_field :dict_id
  end
end
class Idiom < ActiveRecord::Base
  belongs_to :card
  amoeba do
    include_field :repres
    include_field :card_id
    include_field :kind
    include_field :note
  end
end

NOW IN THE CONSOLE:

c=Card.find_by_id(19) # yields a Card object
c.amoeba_dup

THIS RAISES THE EXCEPTION:

NoMethodError: undefined method `macro' for nil:NilClass
    from .../amoeba-2.0.0/lib/amoeba.rb:412:in `amo_process_association'
    from .../amoeba-2.0.0/lib/amoeba.rb:381:in `block in amoeba_dup'
    from .../amoeba-2.0.0/lib/amoeba.rb:379:in `each'
    from ..../amoeba-2.0.0/lib/amoeba.rb:379:in `amoeba_dup'
    from .../amoeba-2.0.0/lib/amoeba.rb:457:in `block in amo_process_association'

Where did I make a mistake?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.