activerecord_cloneable 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README +70 -0
- data/Rakefile +23 -0
- data/activerecord_cloneable.gemspec +12 -0
- data/lib/active_record/cloneable.rb +147 -0
- data/test/activerecord_cloneable_test.rb +54 -0
- data/test/test_helper.rb +35 -0
- metadata +50 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c7356c885224c919a9ff09103fa118b69857ddfc
|
4
|
+
data.tar.gz: 9be80f2671b2464ea0202688d62979d755452df9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fdbf35075828f2cc9be75cddd0c5925109e2bb50a4063e15450728afb956677168cb3e77dafd2cc5e0f2078c44f4165176ecc48eddf16b3fbe0407300f226547
|
7
|
+
data.tar.gz: 8d24200b3d7cdc87dba52bac2371f34ad1c7821683947ac1db68dfec8909e3761f3b7488e6e2fd7a6ba25fb22cfc52e7101e8c7438e6fc5d91ef0c626365c6ee
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 [name of plugin creator]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
Activerecord-cloneable
|
2
|
+
======================
|
3
|
+
|
4
|
+
This is a quick tool to help clone active records.
|
5
|
+
|
6
|
+
Let's say you have a great rails app, and you want to quickly add clone
|
7
|
+
functionality to an object. Use this.
|
8
|
+
|
9
|
+
INSTALL
|
10
|
+
=======
|
11
|
+
Bundler:
|
12
|
+
|
13
|
+
Into your Gemfile put:
|
14
|
+
gem 'activerecord_cloneable', :git => "git://github.com/apalmblad/activerecord_cloneable.git",
|
15
|
+
:require => 'active_record/cloneable'
|
16
|
+
|
17
|
+
In initialize code, do:
|
18
|
+
|
19
|
+
ActiveRecord::Base.send( :include, ActiveRecord::Cloneable )
|
20
|
+
|
21
|
+
In each of the models that you want to clone, add in:
|
22
|
+
class Record < ActiveRecord::Base
|
23
|
+
cloneable
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
Example
|
28
|
+
=======
|
29
|
+
|
30
|
+
Basic:
|
31
|
+
|
32
|
+
cloned_record = record.clone_record
|
33
|
+
|
34
|
+
By default, we'll look at belongs to relations, and clone those to. Often,
|
35
|
+
that's not desirable. We might want to share some of the parents with our
|
36
|
+
clone.
|
37
|
+
|
38
|
+
Then,
|
39
|
+
cloned_record = record.clone_record( :shared_parents => [:belongs_to_association_name]
|
40
|
+
|
41
|
+
We might want to set up some attributes on our cloned record.
|
42
|
+
|
43
|
+
cloned_record = record.clone_record( :attributes => { :name => "Ima Clone" } )
|
44
|
+
|
45
|
+
Sometimes there is child data that you might want to ignore. Lets say logs of
|
46
|
+
activity, or other things that are not desired to be attached to the clone.
|
47
|
+
|
48
|
+
cloned_record = record.clone_record( :skipped_child_relations => [:logs] )
|
49
|
+
|
50
|
+
Gotta go deep? Want to clone something from a parent but not it's children?
|
51
|
+
|
52
|
+
cloned_record = record.clone_record( :skipped_child_relations => [{ :child_1 => :logs} ]] )
|
53
|
+
|
54
|
+
Finally, everyone knows that clones you cannot tell apart are a plot device for
|
55
|
+
a formulaic scifi movie. Therefore, by default, if a name or title attribute is present,
|
56
|
+
it's marked to indicate that the cloned object is a copy. If you have another
|
57
|
+
string field that you want to indicate as such, pass them in:
|
58
|
+
|
59
|
+
cloned_record = record.clone_record( :name_fields => [ :name_title] )
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
Disclaimer
|
64
|
+
==========
|
65
|
+
|
66
|
+
This scratches an itch for me. I don't know if it will scratch an itch for you.
|
67
|
+
It might ruin your day -- or worse. I haven't tested this nearly as well as I
|
68
|
+
should. If you send in problem reports, I'll try and fix them. No warranties.
|
69
|
+
|
70
|
+
Copyright (c) 2011 Adam Palmblad, apalmblad@gmail.com, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the activerecord_cloneable plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.libs << 'test'
|
12
|
+
t.pattern = 'test/**/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Generate documentation for the activerecord_cloneable plugin.'
|
17
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
18
|
+
rdoc.rdoc_dir = 'rdoc'
|
19
|
+
rdoc.title = 'Activerecord-cloneable'
|
20
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
21
|
+
rdoc.rdoc_files.include('README')
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'activerecord_cloneable'
|
3
|
+
s.version = '0.1.3'
|
4
|
+
s.summary = "A library to help clone active records - that is, generate active new record objects with the data copied from an existing record."
|
5
|
+
s.authors = ["Adam Palmblad"]
|
6
|
+
s.email = 'apalmblad@gmail.com'
|
7
|
+
s.date = %q{2013-08-16}
|
8
|
+
s.description = %q{A tool to help with cloning of active record objects.}
|
9
|
+
s.files= ['MIT-LICENSE', 'README', 'Rakefile', 'activerecord_cloneable.gemspec',
|
10
|
+
'test/activerecord_cloneable_test.rb', 'test/test_helper.rb',
|
11
|
+
'lib/active_record/cloneable.rb']
|
12
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module ActiveRecord::Cloneable
|
2
|
+
# ------------------------------------------------------------------- included
|
3
|
+
def self.included( base )
|
4
|
+
base.extend( ClassMethods )
|
5
|
+
end
|
6
|
+
module ClassMethods
|
7
|
+
# ---------------------------------------------------------------- cloneable
|
8
|
+
def cloneable
|
9
|
+
# HACK: check that this best way
|
10
|
+
class_eval <<-EOV
|
11
|
+
include ActiveRecord::Cloneable::InstanceMethods
|
12
|
+
EOV
|
13
|
+
end
|
14
|
+
end
|
15
|
+
module InstanceMethods
|
16
|
+
# ------------------------------------------------------ clone_basic_details
|
17
|
+
def clone_basic_details(cloned_record, belongs_to_keys, args )
|
18
|
+
# assign the attributes, minus any assigned or any from a belongs_to relation
|
19
|
+
self.class.content_columns.each do |column|
|
20
|
+
if !args[:attributes].has_key?( column.name.to_sym ) && !belongs_to_keys.include?( column.name.to_sym )
|
21
|
+
m = "#{column.name}=".to_sym
|
22
|
+
if cloned_record.respond_to?( m )
|
23
|
+
cloned_record.send( m, read_attribute( column.name ) )
|
24
|
+
else
|
25
|
+
cloned_record.write_attribute( column.name, read_attribute( column.name ) )
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
args[:attributes].each_pair do |k,v|
|
30
|
+
cloned_record.send( "#{k}=", v )
|
31
|
+
end
|
32
|
+
# Set the name or title field to be the existing value + (copy)
|
33
|
+
( args[:name_fields] || [:name,:title] ).each do |name_attr|
|
34
|
+
if has_attribute?( name_attr ) && read_attribute( :name_attr )
|
35
|
+
cloned_form.write_attribute( name_attr, read_attribute( name_attr ) + ' (copy)' )
|
36
|
+
break if args[:name_fields].nil?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
#cloned_record.save
|
40
|
+
end
|
41
|
+
# ----------------------------------------------- clone_bleongs_to_relations
|
42
|
+
def clone_belongs_to_relations( relations, cloned_record, args )
|
43
|
+
return if relations.nil?
|
44
|
+
relations.each do |parent_relation|
|
45
|
+
obj = send( parent_relation.name )
|
46
|
+
next if obj.nil?
|
47
|
+
if args[:shared_parent_relations].include?( parent_relation.name.to_sym )
|
48
|
+
cloned_record.send( "#{parent_relation.name}=", obj )
|
49
|
+
elsif !args[:cloned_parents].include?( obj )
|
50
|
+
# We don't know what the parent calls this child.
|
51
|
+
begin
|
52
|
+
rec = obj.clone_record( :skipped_children => args[:skipped_children] + [self],
|
53
|
+
:skipped_child_relations => find_applicable_clone_args( parent_relation.name, args[:skipped_child_relations] ),
|
54
|
+
:skipped_parent_relations => find_applicable_clone_args( parent_relation.name, args[:skipped_parent_relations] ),
|
55
|
+
:shared_parent_relations => find_applicable_clone_args( parent_relation.name, args[:shared_parent_relations] )
|
56
|
+
)
|
57
|
+
rescue NoMethodError => e
|
58
|
+
raise "#{obj.class.name} objects do not know how to clone themselves; they should be marked as cloneable or skipped."
|
59
|
+
end
|
60
|
+
cloned_record.send( "#{parent_relation.name}=", rec )
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
# ----------------------------------------------- find_applicable_clone_args
|
65
|
+
def find_applicable_clone_args( relation_name, args )
|
66
|
+
relation_name = relation_name.to_sym
|
67
|
+
return nil if args.nil?
|
68
|
+
r_val = args.map do |x|
|
69
|
+
if x.is_a?( Hash )
|
70
|
+
x[relation_name]
|
71
|
+
else
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
r_val.flatten
|
76
|
+
end
|
77
|
+
# ---------------------------------------------------- clone_child_relation?
|
78
|
+
def clone_child_relation?( relation_name, skipped_child_relations )
|
79
|
+
relation_name = relation_name.to_sym
|
80
|
+
skipped_child_relations.each do |relation|
|
81
|
+
unless relation.is_a?( Hash )
|
82
|
+
return false if relation == relation_name
|
83
|
+
end
|
84
|
+
end
|
85
|
+
return true
|
86
|
+
end
|
87
|
+
# ---------------------------------------------------------------------- clone
|
88
|
+
def clone_record( args = {} )
|
89
|
+
args[:shared_parent_relations] ||= []
|
90
|
+
args[:skipped_child_relations] ||= []
|
91
|
+
args[:cloned_parents] ||= []
|
92
|
+
args[:skipped_children] ||= []
|
93
|
+
args[:attributes] ||={}
|
94
|
+
cloned_record = args[:object] || self.class.new
|
95
|
+
data = {}
|
96
|
+
self.class.reflections.each do |k,v|
|
97
|
+
data[v.macro] ||= []
|
98
|
+
data[v.macro] << v
|
99
|
+
end
|
100
|
+
belongs_to_keys = ( data[:belongs_to] || [] ).map{ |x| x.primary_key_name.to_sym }
|
101
|
+
|
102
|
+
clone_belongs_to_relations( data[:belongs_to], cloned_record, args )
|
103
|
+
clone_basic_details( cloned_record, belongs_to_keys, args )
|
104
|
+
|
105
|
+
((data[:has_many] || []) + (data[:has_and_belongs_to_many]||[]) ).each do |child_relation|
|
106
|
+
next if child_relation.through_reflection
|
107
|
+
next if !clone_child_relation?( child_relation.name, args[:skipped_child_relations] )
|
108
|
+
kids = send( child_relation.name )
|
109
|
+
next if kids.nil?
|
110
|
+
records = kids.find( :all )
|
111
|
+
records.each do |child_record|
|
112
|
+
next if args[:skipped_children].include?( child_record )
|
113
|
+
cloned_child_record = kids.build
|
114
|
+
child_args = { :cloned_parents => args[:cloned_parents] + [self], :attributes => {}, :object => cloned_child_record,
|
115
|
+
:skipped_child_relations => find_applicable_clone_args( child_relation.name, args[:skipped_child_relations] ) }
|
116
|
+
#if child_relation.macro == :has_many ||child_relation.macro == :has_one
|
117
|
+
# child_args[:attributes][child_relation.primary_key_name.to_sym] = nil
|
118
|
+
#end
|
119
|
+
begin
|
120
|
+
cloned_child_record = child_record.clone_record( child_args )
|
121
|
+
cloned_record.send( child_relation.name ) << cloned_child_record
|
122
|
+
rescue NoMethodError => e
|
123
|
+
raise "#{child_record.class.name} objects do not know how to clone themselves; they should be marked as cloneable or skipped. (#{self.class.name} / #{child_relation.name}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
( data[:has_one] || []).each do |child_relation|
|
128
|
+
next if child_relation.through_reflection
|
129
|
+
next if !clone_child_relation?( child_relation.name, args[:skipped_child_relations] )
|
130
|
+
kid = send( child_relation.name )
|
131
|
+
next if kid.nil?
|
132
|
+
next if args[:skipped_children].include?( kid )
|
133
|
+
cloned_child_record = kid.build
|
134
|
+
child_args = { :cloned_parents => args[:cloned_parents] + [self],
|
135
|
+
:attributes => {}, :object => cloned_child_record,
|
136
|
+
:skipped_child_relations => args[:skipped_child_relations].find_all{ |x| x.is_a?( Hash ) && x[child_relation.name.to_sym] }.map{ |x| x.values }.flatten }
|
137
|
+
begin
|
138
|
+
cloned_child_record = kid.clone_record( child_args )
|
139
|
+
cloned_record.send( "#{child_relation.name}=", cloned_child_record )
|
140
|
+
rescue NoMethodError => e
|
141
|
+
raise "#{kid.class.name} objects do not know how to clone themselves; they should be marked as cloneable or skipped. (#{self.class.name} / #{child_relation.name}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
return cloned_record
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ActiveRecordCloneableTest < ActiveSupport::TestCase
|
4
|
+
# Replace this with your real tests.
|
5
|
+
class Apple < ActiveRecord::Base
|
6
|
+
cloneable
|
7
|
+
belongs_to :banana
|
8
|
+
validates_presence_of :banana
|
9
|
+
validates_associated :banana
|
10
|
+
end
|
11
|
+
class Banana < ActiveRecord::Base
|
12
|
+
cloneable
|
13
|
+
has_many :apples
|
14
|
+
has_one :bread
|
15
|
+
end
|
16
|
+
class NonCloneableThing < ActiveRecord::Base
|
17
|
+
belongs_to :apple
|
18
|
+
end
|
19
|
+
class Bread < ActiveRecord::Base
|
20
|
+
cloneable
|
21
|
+
belongs_to :banana
|
22
|
+
end
|
23
|
+
test "Basic test" do
|
24
|
+
original = Banana.new
|
25
|
+
original.save
|
26
|
+
a = original.apples.build
|
27
|
+
apple =original.apples.new
|
28
|
+
original.save!
|
29
|
+
original.reload
|
30
|
+
assert( original.apples.any? )
|
31
|
+
clone = original.clone_record
|
32
|
+
clone.save!
|
33
|
+
assert( clone.apples.any? )
|
34
|
+
end
|
35
|
+
def test_with_non_cloneable_things
|
36
|
+
Apple.has_many( :non_cloneable_things )
|
37
|
+
original = Banana.new
|
38
|
+
original.save
|
39
|
+
a = original.apples.create
|
40
|
+
a.non_cloneable_things.create
|
41
|
+
original.save
|
42
|
+
clone = original.clone_record( :skipped_child_relations => [{ :apples => :non_cloneable_things}] )
|
43
|
+
assert( clone.apples.any? )
|
44
|
+
end
|
45
|
+
def test_with_have_one
|
46
|
+
b = Bread.new
|
47
|
+
b.banana = Banana.new
|
48
|
+
b.save!
|
49
|
+
new_bread = b.clone_record( :skipped_parent_relations => [ { :bread => :banana }] )
|
50
|
+
new_bread.save!
|
51
|
+
assert( new_bread.banana )
|
52
|
+
assert_equal( new_bread, new_bread.banana.bread )
|
53
|
+
end
|
54
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/test_case'
|
5
|
+
require 'active_record'
|
6
|
+
require 'active_record/cloneable'
|
7
|
+
ActiveRecord::Base.send( :include, ActiveRecord::Cloneable )
|
8
|
+
ActiveRecord::Base.establish_connection(:adapter => 'mysql',
|
9
|
+
:database => 'cloneable_test',
|
10
|
+
:username => 'root' )
|
11
|
+
class DoSetup < ActiveRecord::Migration
|
12
|
+
def self.up
|
13
|
+
create_table :breads do |t|
|
14
|
+
t.column :banana_id, :integer
|
15
|
+
end
|
16
|
+
drop_table :non_cloneable_things
|
17
|
+
create_table :non_cloneable_things do |t|
|
18
|
+
t.column :apple_id,:integer
|
19
|
+
end
|
20
|
+
create_table :apples do |t|
|
21
|
+
t.column :banana_id, :integer
|
22
|
+
end
|
23
|
+
create_table :bananas do |t|
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.down
|
28
|
+
drop_table :bananas
|
29
|
+
drop_table :apples
|
30
|
+
end
|
31
|
+
end
|
32
|
+
begin
|
33
|
+
DoSetup.up
|
34
|
+
rescue ActiveRecord::StatementInvalid => e
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord_cloneable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Palmblad
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-16 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A tool to help with cloning of active record objects.
|
14
|
+
email: apalmblad@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- MIT-LICENSE
|
20
|
+
- README
|
21
|
+
- Rakefile
|
22
|
+
- activerecord_cloneable.gemspec
|
23
|
+
- lib/active_record/cloneable.rb
|
24
|
+
- test/activerecord_cloneable_test.rb
|
25
|
+
- test/test_helper.rb
|
26
|
+
homepage:
|
27
|
+
licenses: []
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubyforge_project:
|
45
|
+
rubygems_version: 2.2.1
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: A library to help clone active records - that is, generate active new record
|
49
|
+
objects with the data copied from an existing record.
|
50
|
+
test_files: []
|