acts_as_brand_new_copy 1.0.0
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.
- data/.gitignore +19 -0
- data/.travis.yml +5 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +149 -0
- data/Rakefile +1 -0
- data/acts_as_brand_new_copy.gemspec +34 -0
- data/db/database.yml +5 -0
- data/lib/acts_as_brand_new_copy.rb +439 -0
- data/lib/acts_as_brand_new_copy/version.rb +3 -0
- data/spec/acts_as_brand_new_copy_spec.rb +252 -0
- data/spec/spec_helper.rb +33 -0
- metadata +270 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 BenCao
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
# acts_as_brand_new_copy
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/acts_as_brand_new_copy)
|
4
|
+
[](https://travis-ci.org/bencao/acts_as_brand_new_copy)
|
5
|
+
|
6
|
+
Copy an active record with its associated records are not easy.
|
7
|
+
|
8
|
+
For example, if we have defined these classes:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class StudentTeacherAssignment < ActiveRecord::Base
|
12
|
+
belongs_to :teacher
|
13
|
+
belongs_to :student
|
14
|
+
end
|
15
|
+
|
16
|
+
class GradeTeacherAssignment < ActiveRecord::Base
|
17
|
+
belongs_to :grade
|
18
|
+
belongs_to :teacher
|
19
|
+
end
|
20
|
+
|
21
|
+
class GradeStudentAssignment < ActiveRecord::Base
|
22
|
+
belongs_to :grade
|
23
|
+
belongs_to :student
|
24
|
+
end
|
25
|
+
|
26
|
+
class Grade < ActiveRecord::Base
|
27
|
+
has_and_belongs_to_many :teachers, :join_table => ::GradeTeacherAssignment.table_name
|
28
|
+
has_and_belongs_to_many :students, :join_table => ::GradeStudentAssignment.table_name
|
29
|
+
end
|
30
|
+
|
31
|
+
class Teacher < ActiveRecord::Base
|
32
|
+
has_many :student_teacher_assignments
|
33
|
+
has_many :students,
|
34
|
+
:through => :student_teacher_assignments,
|
35
|
+
:source => :student
|
36
|
+
end
|
37
|
+
|
38
|
+
class Student < ActiveRecord::Base
|
39
|
+
has_many :student_teacher_assignments
|
40
|
+
has_many :teachers,
|
41
|
+
:through => :student_teacher_assignments,
|
42
|
+
:source => :teacher
|
43
|
+
has_many :scores
|
44
|
+
end
|
45
|
+
|
46
|
+
class Score < ActiveRecord::Base
|
47
|
+
belongs_to :student
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
Can you copy a grade with its teachers and students to another grade in a few lines of code?
|
52
|
+
To me, it's no, consequently acts_as_brand_new_copy was born.
|
53
|
+
|
54
|
+
## Usage
|
55
|
+
|
56
|
+
### copy an active record with its associations
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# copy student itself, return the id for copied student
|
60
|
+
copy_id = @student.brand_new_copy
|
61
|
+
|
62
|
+
# copy student with their scores
|
63
|
+
copy_id = @student.brand_new_copy({:associations => [:scores]})
|
64
|
+
|
65
|
+
# copy the whole grade and all the relationships between grade to students, teachers to students
|
66
|
+
# NOTE here shows the convenience bought by this gem, we've ensured that a same student won't be copied twice!
|
67
|
+
copy_id = @grade.brand_new_copy({:associations => [{:teachers => [:students]}, :students]})
|
68
|
+
```
|
69
|
+
|
70
|
+
### i'd like to do some modifications to records during copy process
|
71
|
+
|
72
|
+
Don't worry, we've already supported that!
|
73
|
+
|
74
|
+
```
|
75
|
+
# prefix student name with a 'Copy Of ' during copy
|
76
|
+
# a callback defined as a class method is needed
|
77
|
+
Student.class_eval do
|
78
|
+
def self.update_name_when_copy(hash_origin, hash_copy, full_context)
|
79
|
+
hash_copy['name'] = 'Copy of ' + hash_origin['name']
|
80
|
+
true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
copy_id = @student.brand_new_copy({:callbacks => [:update_name_when_copy]})
|
84
|
+
|
85
|
+
# prefix grade, students, teachers name with 'Copy of ', and reset students score to nil during copy
|
86
|
+
[Grade, Teacher, Student].each do |klass|
|
87
|
+
klass.class_eval do
|
88
|
+
def self.update_name_when_copy(hash_origin, hash_copy, full_context)
|
89
|
+
hash_copy['name'] = 'Copy Of ' + hash_origin['name']
|
90
|
+
true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
Score.class_eval do
|
96
|
+
def self.reset_value_when_copy(hash_origin, hash_copy, full_context)
|
97
|
+
hash_copy['value'] = nil
|
98
|
+
true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
copy_id = @grade.brand_new_copy({
|
102
|
+
:associations => [{:teachers => [:students]}, {:students => [:scores]}],
|
103
|
+
:callbacks => [
|
104
|
+
:update_name_when_copy,
|
105
|
+
{:teachers => [:update_name_when_copy]},
|
106
|
+
{:students => [:update_name_when_copy, {:scores => [:reset_value_when_copy]}]}
|
107
|
+
]
|
108
|
+
})
|
109
|
+
```
|
110
|
+
|
111
|
+
## Installation
|
112
|
+
|
113
|
+
Add this line to your application's Gemfile:
|
114
|
+
|
115
|
+
gem 'acts_as_brand_new_copy'
|
116
|
+
|
117
|
+
And then execute:
|
118
|
+
|
119
|
+
$ bundle
|
120
|
+
|
121
|
+
Or install it yourself as:
|
122
|
+
|
123
|
+
$ gem install acts_as_brand_new_copy
|
124
|
+
|
125
|
+
## Current Limitation
|
126
|
+
- do not support has_many_and_belongs_to_many associations when join table class has a strange table_name(I mean, table_name not in [Class.name.underscore, Class.name.underscore.pluralize])
|
127
|
+
|
128
|
+
## Contribute
|
129
|
+
|
130
|
+
You're highly welcome to improve this gem.
|
131
|
+
|
132
|
+
### Checkout source code to local
|
133
|
+
say you git clone the source code to /tmp/acts_as_brand_new_copy
|
134
|
+
|
135
|
+
### Install dev bundle
|
136
|
+
```bash
|
137
|
+
$ cd /tmp/acts_as_brand_new_copy
|
138
|
+
$ bundle install
|
139
|
+
```
|
140
|
+
|
141
|
+
### Do some changes
|
142
|
+
```bash
|
143
|
+
$ vi lib/acts_as_brand_new_copy.rb
|
144
|
+
```
|
145
|
+
|
146
|
+
### Run test
|
147
|
+
```bash
|
148
|
+
$ bundle exec rspec spec
|
149
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'acts_as_brand_new_copy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "acts_as_brand_new_copy"
|
8
|
+
spec.version = ActsAsBrandNewCopy::VERSION
|
9
|
+
spec.authors = ["Ben Cao"]
|
10
|
+
spec.email = ["benb88@gmail.com"]
|
11
|
+
spec.description = "A ruby gem for active record which simplify the copy of very complex tree data."
|
12
|
+
spec.summary = "Just give me the object tree specification and callbacks, I will do all the rest for you."
|
13
|
+
spec.homepage = "https://github.com/bencao/acts_as_brand_new_copy"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0.4"
|
23
|
+
spec.add_development_dependency "rspec", "~> 2.13.0"
|
24
|
+
spec.add_development_dependency "mocha", "~> 0.13.3"
|
25
|
+
spec.add_development_dependency "sqlite3", "~> 1.3.7"
|
26
|
+
spec.add_development_dependency "database_cleaner"
|
27
|
+
spec.add_development_dependency "factory_girl"
|
28
|
+
spec.add_development_dependency "factory_girl_rails"
|
29
|
+
spec.add_development_dependency "pry"
|
30
|
+
spec.add_development_dependency "pry-theme"
|
31
|
+
spec.add_development_dependency "pry-nav"
|
32
|
+
spec.add_dependency "activesupport", "~> 3.2.13"
|
33
|
+
spec.add_dependency "activerecord", "~> 3.2.13"
|
34
|
+
end
|
data/db/database.yml
ADDED
@@ -0,0 +1,439 @@
|
|
1
|
+
require "acts_as_brand_new_copy/version"
|
2
|
+
|
3
|
+
module ActsAsBrandNewCopy
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class BrandNewCopyBuilder
|
7
|
+
|
8
|
+
def initialize(hash_origin, hash_copy)
|
9
|
+
@hash_origin = hash_origin
|
10
|
+
@hash_copy = hash_copy
|
11
|
+
@save_order = calculate_save_order
|
12
|
+
@instances = extract_instances
|
13
|
+
@queue = prepare_copy_queue
|
14
|
+
@full_context = {
|
15
|
+
:root_hash_origin => @hash_origin,
|
16
|
+
:root_hash_copy => @hash_copy,
|
17
|
+
:save_order => @save_order,
|
18
|
+
:save_queue => @queue,
|
19
|
+
:instances => @instances
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def invoke_callback(callbacks)
|
24
|
+
invoke_callback_recursively([@hash_origin], callbacks)
|
25
|
+
end
|
26
|
+
|
27
|
+
def save
|
28
|
+
@queue.each_pair do |klass_name, hash_copy_array|
|
29
|
+
next if hash_copy_array.blank?
|
30
|
+
|
31
|
+
# belongs_to
|
32
|
+
hash_copy_array.each{ |hash_copy| update_key_on_self_table(klass_name, hash_copy) }
|
33
|
+
|
34
|
+
# AN OPTIMIZED APPROACH IS TO BATCH INSERT THE SAME LEVEL
|
35
|
+
# BUT NOTE CURRENT IMPLEMENTATION IS IS FOR MYSQL ONLY
|
36
|
+
batch_insert_copy(klass_name.constantize, hash_copy_array)
|
37
|
+
|
38
|
+
# has_one, has_many and has_and_belongs_to_many
|
39
|
+
hash_copy_array.each{ |hash_copy| update_key_on_association_tables(klass_name, hash_copy) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def all_saved_instances
|
44
|
+
@instances.values
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def invoke_callback_recursively(hash_origin_array, callbacks)
|
50
|
+
raise 'callbacks option must be an array' unless callbacks.is_a?(Array)
|
51
|
+
|
52
|
+
hash_origin_array.each_with_index do |hash_origin, index|
|
53
|
+
raise 'hash_origin must be an hash' unless hash_origin.is_a?(Hash)
|
54
|
+
|
55
|
+
hash_klass = hash_origin['klass'].constantize
|
56
|
+
|
57
|
+
callbacks.each do |callback|
|
58
|
+
if callback.is_a?(Symbol)
|
59
|
+
unless hash_klass.send(callback, hash_origin, find_object_by_old_id(hash_origin['klass'], hash_origin['id_before_copy']), @full_context)
|
60
|
+
raise "run #{callback} callback failed, " +
|
61
|
+
" hash_origin=#{find_object_by_old_id(hash_origin['klass'], hash_origin['id_before_copy'])}"
|
62
|
+
end
|
63
|
+
elsif callback.is_a?(Hash)
|
64
|
+
path, path_callbacks = hash_klass.reflect_on_association(callback.keys.first).class_name, callback.values.first
|
65
|
+
invoke_callback_recursively(hash_partial_by_path(hash_origin, [path]), path_callbacks)
|
66
|
+
else
|
67
|
+
raise 'the callback param value must be a symbol or hash'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def hash_partial_by_path(hash, path)
|
74
|
+
path_dup = path.dup
|
75
|
+
hash_partial = hash
|
76
|
+
hash_partial = hash_partial['associations'][path_dup.shift] while path_dup.present?
|
77
|
+
hash_partial
|
78
|
+
end
|
79
|
+
|
80
|
+
def calculate_save_order
|
81
|
+
constraints, klasses = Set.new, Set.new
|
82
|
+
traverse do |current_hash|
|
83
|
+
klasses.add(current_hash['klass']) unless klasses.include?(current_hash['klass'])
|
84
|
+
|
85
|
+
current_hash['dependencies'].each_pair do |aso_name, aso_dependencies|
|
86
|
+
aso_dependencies.each do |aso_dependency|
|
87
|
+
aso_dependency['save_order_constraints'].each do |constraint_string|
|
88
|
+
front, back = constraint_string.split('_')
|
89
|
+
klasses.add(front) unless klasses.include?(front)
|
90
|
+
klasses.add(back) unless klasses.include?(back)
|
91
|
+
# same active record class
|
92
|
+
unless front == back || constraints.include?(constraint_string)
|
93
|
+
constraints.add(constraint_string)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# construct an order which can ensure all constraints are met
|
101
|
+
valid_save_order = klasses.to_a.sort
|
102
|
+
has_modifications = true
|
103
|
+
while has_modifications
|
104
|
+
has_modifications = false
|
105
|
+
constraints.each do |constraint|
|
106
|
+
front, back = constraint.split('_')
|
107
|
+
front_index = valid_save_order.index(front)
|
108
|
+
back_index = valid_save_order.index(back)
|
109
|
+
if front_index > back_index
|
110
|
+
valid_save_order[back_index], valid_save_order[front_index] = front, back
|
111
|
+
has_modifications = true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
valid_save_order
|
116
|
+
end
|
117
|
+
|
118
|
+
# breadth first
|
119
|
+
def traverse(&block)
|
120
|
+
not_visit = [@hash_copy]
|
121
|
+
while (not_visit.size > 0)
|
122
|
+
current_hash = not_visit.shift
|
123
|
+
yield current_hash
|
124
|
+
current_hash['associations'].each_pair do |aso_name, aso_hash_array|
|
125
|
+
aso_hash_array.each{ |aso_hash| not_visit << aso_hash }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def extract_instances
|
131
|
+
unique_instances = {}
|
132
|
+
traverse do |current_hash|
|
133
|
+
key = object_key(current_hash['klass'], current_hash['id'])
|
134
|
+
if unique_instances.has_key?(key)
|
135
|
+
unique_instances[key] = merge_associations_dependencies_for_copy(unique_instances[key], current_hash)
|
136
|
+
else
|
137
|
+
unique_instances[key] = current_hash
|
138
|
+
end
|
139
|
+
end
|
140
|
+
unique_instances
|
141
|
+
end
|
142
|
+
|
143
|
+
def merge_associations_dependencies_for_copy(hash, another_hash)
|
144
|
+
dup = hash.reject{ |k, v| ['associations', 'dependencies'].include?(k) }
|
145
|
+
|
146
|
+
dup['associations'] = {}
|
147
|
+
(hash['associations'].keys + another_hash['associations'].keys).each do |aso_name|
|
148
|
+
dup['associations'][aso_name] = (hash['associations'][aso_name] || []) + (another_hash['associations'][aso_name] || [])
|
149
|
+
end
|
150
|
+
|
151
|
+
dup['dependencies'] = {}
|
152
|
+
(hash['dependencies'].keys + another_hash['dependencies'].keys).each do |aso_name|
|
153
|
+
dup['dependencies'][aso_name] = (hash['dependencies'][aso_name] || []) + (another_hash['dependencies'][aso_name] || [])
|
154
|
+
end
|
155
|
+
|
156
|
+
dup
|
157
|
+
end
|
158
|
+
|
159
|
+
def prepare_copy_queue
|
160
|
+
queue = ActiveSupport::OrderedHash.new
|
161
|
+
@save_order.each{ |item| queue[item] = [] }
|
162
|
+
@instances.each_pair{ |key, hash| queue[hash['klass']] << hash }
|
163
|
+
queue
|
164
|
+
end
|
165
|
+
|
166
|
+
def update_key_on_self_table(klass_name, hash_copy)
|
167
|
+
hash_copy['dependencies'].each_pair do |aso_name, aso_dependencies|
|
168
|
+
aso_dependencies.each do |aso_dependency|
|
169
|
+
if aso_dependency['key_position'] == 'self_table'
|
170
|
+
foreign_key = aso_dependency['association_key_on_self_table']
|
171
|
+
update_object_by_old_id(klass_name, hash_copy['id'], {
|
172
|
+
foreign_key => new_id(aso_name, hash_copy[foreign_key])
|
173
|
+
})
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def update_key_on_association_tables(klass_name, hash_copy)
|
180
|
+
hash_copy['dependencies'].each_pair do |aso_name, aso_dependencies|
|
181
|
+
aso_dependencies.each do |aso_dependency|
|
182
|
+
if aso_dependency['key_position'] == 'association_table'
|
183
|
+
aso_foreign_key = aso_dependency['self_key_on_association_table']
|
184
|
+
hash_copy['associations'][aso_name].each do |instance|
|
185
|
+
update_object_by_old_id(aso_name, instance['id'], {
|
186
|
+
aso_foreign_key => new_id(klass_name, instance[aso_foreign_key])
|
187
|
+
})
|
188
|
+
end
|
189
|
+
elsif aso_dependency['key_position'] == 'join_table'
|
190
|
+
foreign_key_to_self = aso_dependency['self_key_on_join_table']
|
191
|
+
foreign_key_to_association = aso_dependency['association_key_on_join_table']
|
192
|
+
join_table_instances = hash_copy['associations'][aso_dependency['join_table_class']]
|
193
|
+
join_table_instances.each do |instance|
|
194
|
+
update_object_by_old_id(aso_dependency['join_table_class'], instance['id'], {
|
195
|
+
foreign_key_to_self => new_id(klass_name, instance[foreign_key_to_self]),
|
196
|
+
foreign_key_to_association => new_id(aso_name, instance[foreign_key_to_association])
|
197
|
+
})
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def do_insert(klass, columns, hash_copies)
|
205
|
+
connection = klass.connection
|
206
|
+
values_string = hash_copies.map do |hash_copy|
|
207
|
+
quoted_copy_values = columns.map do |column|
|
208
|
+
case column.name
|
209
|
+
when 'updated_at', 'created_at'
|
210
|
+
'NOW()'
|
211
|
+
else
|
212
|
+
connection.quote(hash_copy[column.name], column)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
"(#{quoted_copy_values.join(',')})"
|
216
|
+
end.join(',')
|
217
|
+
result = connection.execute("INSERT INTO #{klass.table_name} (#{columns.map(&:name).join(',')}) VALUES #{values_string}")
|
218
|
+
connection.last_inserted_id(result)
|
219
|
+
end
|
220
|
+
|
221
|
+
def batch_insert_copy_auto_generate_ids(klass, hash_copy_slice)
|
222
|
+
columns = klass.columns.reject {|k, v| 'id' == k.name}
|
223
|
+
last_id = do_insert(klass, columns, hash_copy_slice)
|
224
|
+
first_id = last_id - hash_copy_slice.size + 1
|
225
|
+
hash_copy_slice.each_with_index{ |hash_copy, index| hash_copy['id'] = first_id + index }
|
226
|
+
end
|
227
|
+
|
228
|
+
def batch_insert_copy(klass, hash_copy_array)
|
229
|
+
# in case of array too large
|
230
|
+
hash_copy_array.each_slice(50) do |hash_copy_slice|
|
231
|
+
batch_insert_copy_auto_generate_ids(klass, hash_copy_slice)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.object_key(klass, id)
|
236
|
+
absolute_klass_name = klass.start_with?("::") ? "#{klass}" : "::#{klass}"
|
237
|
+
absolute_klass_name.constantize.table_name + "_#{id}"
|
238
|
+
end
|
239
|
+
|
240
|
+
def object_key(klass, id)
|
241
|
+
self.class.object_key(klass, id)
|
242
|
+
end
|
243
|
+
|
244
|
+
def find_object_by_old_id(klass_name, old_id)
|
245
|
+
@instances[object_key(klass_name, old_id)]
|
246
|
+
end
|
247
|
+
|
248
|
+
def new_id(klass_name, old_id)
|
249
|
+
copy_object = find_object_by_old_id(klass_name, old_id)
|
250
|
+
return nil if copy_object.nil?
|
251
|
+
raise 'copy object not saved to db!' if copy_object['id'] == copy_object['id_before_copy']
|
252
|
+
copy_object['id']
|
253
|
+
end
|
254
|
+
|
255
|
+
def update_object_by_old_id(klass_name, old_id, new_attributes)
|
256
|
+
copy_object = find_object_by_old_id(klass_name, old_id)
|
257
|
+
new_attributes.each_pair do |key, value|
|
258
|
+
copy_object[key] = value
|
259
|
+
end
|
260
|
+
if copy_object['id'] != copy_object['id_before_copy']
|
261
|
+
# object already saved, this may happens when the same level copies has dependencies
|
262
|
+
update_inserted_copy(copy_object['klass'].constantize, copy_object['id'], new_attributes)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def update_inserted_copy(klass, id, new_attributes)
|
267
|
+
sub_conditions = []
|
268
|
+
new_attributes.each_pair do |key, value|
|
269
|
+
sub_conditions << "#{key}=#{value}"
|
270
|
+
end
|
271
|
+
klass.connection.execute("UPDATE #{klass.table_name} SET #{sub_conditions.join(',')} WHERE id=#{id}")
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
|
276
|
+
module ClassMethods
|
277
|
+
def brand_new_copy_object_key(klass, id)
|
278
|
+
BrandNewCopyBuilder.object_key(klass, id)
|
279
|
+
end
|
280
|
+
|
281
|
+
def brand_new_copy_guess_join_table_class(join_table_name)
|
282
|
+
final_name = [join_table_name.singularize, join_table_name.pluralize].detect do |possible_name|
|
283
|
+
Object.const_defined?(possible_name.camelize)
|
284
|
+
end
|
285
|
+
if final_name
|
286
|
+
final_name.camelize
|
287
|
+
else
|
288
|
+
raise "Do not support has_and_belongs_to_many associations" +
|
289
|
+
"that can not guess join table class from join table name"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def quoted_attributes_for_copy
|
295
|
+
quoted_attributes = {}
|
296
|
+
attributes.each_pair do |key, value|
|
297
|
+
case value.class.to_s
|
298
|
+
when "Date", "Time", "ActiveSupport::TimeWithZone"
|
299
|
+
quoted_attributes[key] = connection.quoted_date(value)
|
300
|
+
when "BigDecimal"
|
301
|
+
quoted_attributes[key] = value.to_s("F")
|
302
|
+
else
|
303
|
+
quoted_attributes[key] = value
|
304
|
+
end
|
305
|
+
end
|
306
|
+
# in case the table has no id
|
307
|
+
unless quoted_attributes.has_key?('id')
|
308
|
+
quoted_attributes['id'] = quoted_attributes.values.join('_')
|
309
|
+
end
|
310
|
+
quoted_attributes['id_before_copy'] = quoted_attributes['id']
|
311
|
+
quoted_attributes['klass'] = self.class.to_s
|
312
|
+
quoted_attributes
|
313
|
+
end
|
314
|
+
|
315
|
+
# sample associations = [:billing_term_condition, {:line_items => [:targeting_criteria]}, :resource_user_assignments]
|
316
|
+
def serialize_hash_for_copy(associations=nil)
|
317
|
+
associations = [] if associations.nil?
|
318
|
+
raise "associations param (#{associations}) must be inside an array" unless associations.is_a?(Array)
|
319
|
+
|
320
|
+
result = quoted_attributes_for_copy.merge({
|
321
|
+
'associations' => {},
|
322
|
+
'dependencies' => {}
|
323
|
+
})
|
324
|
+
|
325
|
+
associations.uniq.each do |association|
|
326
|
+
if association.is_a?(Symbol)
|
327
|
+
aso_name_underscore, aso_instances, aso_option = association.to_s, self.send(association), nil
|
328
|
+
elsif association.is_a?(Hash)
|
329
|
+
aso_name_underscore, aso_instances, aso_option = association.keys.first.to_s, self.send(association.keys.first), association.values.first
|
330
|
+
else
|
331
|
+
raise 'association param value must be a symbol or hash'
|
332
|
+
end
|
333
|
+
|
334
|
+
reflection = self.class.reflect_on_association(aso_name_underscore.to_sym)
|
335
|
+
unless result['dependencies'].has_key?(reflection.class_name)
|
336
|
+
result['dependencies'][reflection.class_name] = []
|
337
|
+
end
|
338
|
+
result['dependencies'][reflection.class_name] << resolve_copy_dependencies(reflection)
|
339
|
+
|
340
|
+
unless result['associations'].has_key?(reflection.class_name)
|
341
|
+
result['associations'][reflection.class_name] = if aso_instances.is_a?(Array)
|
342
|
+
aso_instances.map{ |ass| ass.serialize_hash_for_copy(aso_option) }
|
343
|
+
else
|
344
|
+
aso_instances.present? ? [aso_instances.serialize_hash_for_copy(aso_option)] : []
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# has_and_belongs_to_many and has_one(through), has_many(through) may introduce an implicit association
|
349
|
+
implicit_association_class, implicit_association_hash = implicit_join_table_association(reflection)
|
350
|
+
if implicit_association_class.present? && (not result['associations'].has_key?(implicit_association_class))
|
351
|
+
result['associations'][implicit_association_class] = implicit_association_hash
|
352
|
+
end
|
353
|
+
end
|
354
|
+
result
|
355
|
+
end
|
356
|
+
|
357
|
+
def resolve_copy_dependencies(reflection)
|
358
|
+
self_class = reflection.active_record.to_s
|
359
|
+
association_class = reflection.class_name
|
360
|
+
if (reflection.through_reflection.present?)
|
361
|
+
join_table_class = reflection.through_reflection.class_name
|
362
|
+
# has_one through OR has_many through
|
363
|
+
{
|
364
|
+
'key_position' => 'join_table',
|
365
|
+
'save_order_constraints' => [
|
366
|
+
"#{self_class}_#{join_table_class}",
|
367
|
+
"#{association_class}_#{join_table_class}",
|
368
|
+
"#{association_class}_#{self_class}"
|
369
|
+
],
|
370
|
+
'join_table_class' => join_table_class,
|
371
|
+
'self_key_on_join_table' => reflection.through_reflection.foreign_key,
|
372
|
+
'association_key_on_join_table' => reflection.source_reflection.foreign_key
|
373
|
+
}
|
374
|
+
else
|
375
|
+
case (reflection.macro)
|
376
|
+
when :has_and_belongs_to_many
|
377
|
+
join_table_class = self.class.brand_new_copy_guess_join_table_class(reflection.options[:join_table])
|
378
|
+
{
|
379
|
+
'key_position' => 'join_table',
|
380
|
+
'save_order_constraints' => [
|
381
|
+
"#{self_class}_#{join_table_class}",
|
382
|
+
"#{association_class}_#{join_table_class}",
|
383
|
+
"#{association_class}_#{self_class}"
|
384
|
+
],
|
385
|
+
'join_table_class' => join_table_class,
|
386
|
+
'self_key_on_join_table' => reflection.foreign_key,
|
387
|
+
'association_key_on_join_table' => reflection.association_foreign_key
|
388
|
+
}
|
389
|
+
when :has_one, :has_many
|
390
|
+
{
|
391
|
+
'key_position' => 'association_table',
|
392
|
+
'save_order_constraints' => ["#{self_class}_#{association_class}"],
|
393
|
+
'self_key_on_association_table' => reflection.foreign_key
|
394
|
+
}
|
395
|
+
when :belongs_to
|
396
|
+
{
|
397
|
+
'key_position' => 'self_table',
|
398
|
+
'save_order_constraints' => ["#{association_class}_#{self_class}"],
|
399
|
+
'association_key_on_self_table' => reflection.foreign_key
|
400
|
+
}
|
401
|
+
else
|
402
|
+
raise 'should not have reflection macro other than belongs_to has_and_belongs_to_many has_one has_many'
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
def implicit_join_table_association(reflection)
|
408
|
+
# belongs_to, has_one(without through), has_many(without through) have no implicit join table
|
409
|
+
return nil, nil if (reflection.through_reflection.blank? && reflection.macro != :has_and_belongs_to_many)
|
410
|
+
|
411
|
+
if reflection.macro == :has_and_belongs_to_many
|
412
|
+
join_table_class = self.class.brand_new_copy_guess_join_table_class(reflection.options[:join_table])
|
413
|
+
self_key_on_join_table = reflection.foreign_key
|
414
|
+
else
|
415
|
+
join_table_class = reflection.through_reflection.class_name
|
416
|
+
self_key_on_join_table = reflection.through_reflection.foreign_key
|
417
|
+
end
|
418
|
+
join_table_instances = join_table_class.constantize.where("#{self_key_on_join_table} = #{self.id}").to_a
|
419
|
+
|
420
|
+
return join_table_class, join_table_instances.map{ |instance| instance.serialize_hash_for_copy(nil) }
|
421
|
+
end
|
422
|
+
|
423
|
+
def brand_new_copy(options={})
|
424
|
+
final_options = {:callbacks => [], :associations => nil}.merge(options)
|
425
|
+
|
426
|
+
eager_loaded_self = self.class.includes(final_options[:associations]).find(id)
|
427
|
+
|
428
|
+
hash_origin = eager_loaded_self.serialize_hash_for_copy(final_options[:associations])
|
429
|
+
hash_copy = JSON.parse(hash_origin.to_json) # a way to do deep clone
|
430
|
+
|
431
|
+
builder = BrandNewCopyBuilder.new(hash_origin, hash_copy)
|
432
|
+
builder.invoke_callback(final_options[:callbacks])
|
433
|
+
builder.save
|
434
|
+
|
435
|
+
return hash_copy['id']
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
ActiveRecord::Base.send(:include, ActsAsBrandNewCopy)
|
@@ -0,0 +1,252 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'acts_as_brand_new_copy'
|
3
|
+
|
4
|
+
class Schema < ActiveRecord::Migration
|
5
|
+
def change
|
6
|
+
create_table :grades do |t|
|
7
|
+
t.string :name
|
8
|
+
end
|
9
|
+
|
10
|
+
create_table :teachers do |t|
|
11
|
+
t.string :name
|
12
|
+
end
|
13
|
+
|
14
|
+
create_table :students do |t|
|
15
|
+
t.string :name
|
16
|
+
end
|
17
|
+
|
18
|
+
create_table :scores do |t|
|
19
|
+
t.string :value
|
20
|
+
t.references :student
|
21
|
+
end
|
22
|
+
|
23
|
+
create_table :grade_teacher_assignments, :id => false do |t|
|
24
|
+
t.references :grade
|
25
|
+
t.references :teacher
|
26
|
+
end
|
27
|
+
|
28
|
+
create_table :grade_student_assignments, :id => false do |t|
|
29
|
+
t.references :grade
|
30
|
+
t.references :student
|
31
|
+
end
|
32
|
+
|
33
|
+
create_table :student_teacher_assignments do |t|
|
34
|
+
t.references :teacher
|
35
|
+
t.references :student
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
Schema.new.change
|
42
|
+
|
43
|
+
class StudentTeacherAssignment < ActiveRecord::Base
|
44
|
+
belongs_to :teacher
|
45
|
+
belongs_to :student
|
46
|
+
end
|
47
|
+
|
48
|
+
class GradeTeacherAssignment < ActiveRecord::Base
|
49
|
+
belongs_to :grade
|
50
|
+
belongs_to :teacher
|
51
|
+
end
|
52
|
+
|
53
|
+
class GradeStudentAssignment < ActiveRecord::Base
|
54
|
+
belongs_to :grade
|
55
|
+
belongs_to :student
|
56
|
+
end
|
57
|
+
|
58
|
+
class Grade < ActiveRecord::Base
|
59
|
+
has_and_belongs_to_many :teachers, :join_table => ::GradeTeacherAssignment.table_name
|
60
|
+
has_and_belongs_to_many :students, :join_table => ::GradeStudentAssignment.table_name
|
61
|
+
end
|
62
|
+
|
63
|
+
class Teacher < ActiveRecord::Base
|
64
|
+
has_many :student_teacher_assignments
|
65
|
+
has_many :students,
|
66
|
+
:through => :student_teacher_assignments,
|
67
|
+
:source => :student
|
68
|
+
end
|
69
|
+
|
70
|
+
class Student < ActiveRecord::Base
|
71
|
+
has_many :student_teacher_assignments
|
72
|
+
has_many :teachers,
|
73
|
+
:through => :student_teacher_assignments,
|
74
|
+
:source => :teacher
|
75
|
+
has_many :scores
|
76
|
+
end
|
77
|
+
|
78
|
+
class Score < ActiveRecord::Base
|
79
|
+
belongs_to :student
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
FactoryGirl.define do
|
84
|
+
factory :grade do
|
85
|
+
sequence(:name) {|n| "Grade #{n}"}
|
86
|
+
end
|
87
|
+
|
88
|
+
factory :teacher do
|
89
|
+
sequence(:name) {|n| "Teacher #{n}"}
|
90
|
+
end
|
91
|
+
|
92
|
+
factory :student do
|
93
|
+
sequence(:name) {|n| "Student #{n}"}
|
94
|
+
|
95
|
+
trait :with_scores do
|
96
|
+
after(:create) do |student, evaluator|
|
97
|
+
FactoryGirl.create(:score, {:student => student, :value => "70"})
|
98
|
+
FactoryGirl.create(:score, {:student => student, :value => "80"})
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
factory :score do
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe ActsAsBrandNewCopy do
|
108
|
+
context "copy associations" do
|
109
|
+
it "should copy self" do
|
110
|
+
@student = create(:student)
|
111
|
+
copy_id = @student.brand_new_copy
|
112
|
+
copied_student = Student.find copy_id
|
113
|
+
copied_student.name.should == @student.name
|
114
|
+
copied_student.id.should_not == @student.id
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should copy belongs_to associations" do
|
118
|
+
# NOTE although brand new copy support belongs_to association copy
|
119
|
+
# it's not common in real world that we need to copy belongs_to association
|
120
|
+
# You should avoid doing that or you should check your design
|
121
|
+
@student = create(:student, :with_scores)
|
122
|
+
@score1 = @student.scores.first
|
123
|
+
|
124
|
+
copy_id = @score1.brand_new_copy({:associations => [:student]})
|
125
|
+
|
126
|
+
copied_score = Score.find(copy_id)
|
127
|
+
|
128
|
+
copied_score.student.name.should == @student.name
|
129
|
+
copied_score.student.id.should_not == @student.id
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should copy has_many associations" do
|
133
|
+
@student = create(:student, :with_scores)
|
134
|
+
@score1, @score2 = @student.scores
|
135
|
+
|
136
|
+
copy_id = @student.brand_new_copy({:associations => [:scores]})
|
137
|
+
|
138
|
+
copied_student = Student.find copy_id
|
139
|
+
copied_student.scores.size.should == @student.scores.size
|
140
|
+
copied_student.scores.map(&:value).sort.should == @student.scores.map(&:value).sort
|
141
|
+
(copied_student.scores.map(&:id) - @student.scores.map(&:id)).size.should == 2
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should copy has_many through associations" do
|
145
|
+
@teacher = create(:teacher)
|
146
|
+
@student1, @student2 = create_list(:student, 2)
|
147
|
+
@teacher.students << @student1
|
148
|
+
@teacher.students << @student2
|
149
|
+
|
150
|
+
copy_id = @teacher.brand_new_copy({:associations => [:students]})
|
151
|
+
copied_teacher = Teacher.find copy_id
|
152
|
+
copied_teacher.students.size.should == @teacher.students.size
|
153
|
+
copied_teacher.students.map(&:name).sort.should == @teacher.students.map(&:name).sort
|
154
|
+
(copied_teacher.students.map(&:id) - @teacher.students.map(&:id)).size.should == 2
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should copy has_and_belongs_to_many associations" do
|
158
|
+
@grade = create(:grade)
|
159
|
+
@student1, @student2 = create_list(:student, 2)
|
160
|
+
@grade.students << @student1
|
161
|
+
@grade.students << @student2
|
162
|
+
|
163
|
+
copy_id = @grade.brand_new_copy({:associations => [:students]})
|
164
|
+
copied_grade = Grade.find copy_id
|
165
|
+
copied_grade.students.size.should == @grade.students.size
|
166
|
+
copied_grade.students.map(&:name).sort.should == @grade.students.map(&:name).sort
|
167
|
+
(copied_grade.students.map(&:id) - @grade.students.map(&:id)).size.should == 2
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should not copy a same association instance twice" do
|
171
|
+
@grade = create(:grade)
|
172
|
+
@teacher = create(:teacher)
|
173
|
+
@grade.teachers << @teacher
|
174
|
+
@student1, @student2 = create_list(:student, 2)
|
175
|
+
@grade.students << @student1
|
176
|
+
@grade.students << @student2
|
177
|
+
@teacher.students << @student1
|
178
|
+
@teacher.students << @student2
|
179
|
+
|
180
|
+
copy_id = @grade.brand_new_copy({:associations => [{:teachers => [:students]}, :students]})
|
181
|
+
copied_grade = Grade.find copy_id
|
182
|
+
copied_grade.students.size.should == @grade.students.size
|
183
|
+
(copied_grade.students.map(&:id) - @grade.students.map(&:id)).size.should == 2
|
184
|
+
copied_grade.teachers.size.should == @grade.teachers.size
|
185
|
+
copied_grade.students.map(&:name).sort.should == @grade.students.map(&:name).sort
|
186
|
+
Student.where(:name => @student1.name).size.should == 2
|
187
|
+
Student.where(:name => @student2.name).size.should == 2
|
188
|
+
copied_grade.teachers.first.students.size.should == @teacher.students.size
|
189
|
+
copied_grade.teachers.first.students.map(&:name).sort.should == @teacher.students.map(&:name).sort
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
context "callbacks" do
|
195
|
+
[Grade, Teacher, Student].each do |klass|
|
196
|
+
klass.class_eval do
|
197
|
+
def self.update_name_when_copy(hash_origin, hash_copy, full_context)
|
198
|
+
hash_copy['name'] = copy_of_name(hash_origin['name'])
|
199
|
+
true
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.copy_of_name(name)
|
203
|
+
'Copy of ' + name
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
Score.class_eval do
|
209
|
+
def self.reset_value_when_copy(hash_origin, hash_copy, full_context)
|
210
|
+
hash_copy['value'] = nil
|
211
|
+
true
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
it "should invoke callbacks on self" do
|
216
|
+
@student = create(:student)
|
217
|
+
copy_id = @student.brand_new_copy({:callbacks => [:update_name_when_copy]})
|
218
|
+
copied_student = Student.find copy_id
|
219
|
+
copied_student.name.should == Student.copy_of_name(@student.name)
|
220
|
+
end
|
221
|
+
|
222
|
+
it "should invoke callbacks on associations" do
|
223
|
+
@grade = create(:grade)
|
224
|
+
@teacher = create(:teacher)
|
225
|
+
@grade.teachers << @teacher
|
226
|
+
@student1, @student2 = create_list(:student, 2, :with_scores)
|
227
|
+
@grade.students << @student1
|
228
|
+
@grade.students << @student2
|
229
|
+
@teacher.students << @student1
|
230
|
+
@teacher.students << @student2
|
231
|
+
|
232
|
+
copy_id = @grade.brand_new_copy({
|
233
|
+
:associations => [{:teachers => [:students]}, {:students => [:scores]}],
|
234
|
+
:callbacks => [
|
235
|
+
:update_name_when_copy,
|
236
|
+
{:teachers => [:update_name_when_copy]},
|
237
|
+
{:students => [:update_name_when_copy, {:scores => [:reset_value_when_copy]}]}
|
238
|
+
]
|
239
|
+
})
|
240
|
+
copied_grade = Grade.find copy_id
|
241
|
+
copied_grade.name.should == Grade.copy_of_name(@grade.name)
|
242
|
+
copied_grade.teachers.first.name.should == Teacher.copy_of_name(@teacher.name)
|
243
|
+
copied_grade.students.map(&:name).sort.should == [
|
244
|
+
Student.copy_of_name(@student1.name),
|
245
|
+
Student.copy_of_name(@student2.name)
|
246
|
+
].sort
|
247
|
+
copied_grade.students.map(&:scores).flatten.map(&:value).compact.should be_blank
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_record'
|
3
|
+
require 'sqlite3'
|
4
|
+
require 'pry'
|
5
|
+
require 'database_cleaner'
|
6
|
+
require 'factory_girl'
|
7
|
+
|
8
|
+
db_config = YAML::load(IO.read('db/database.yml'))
|
9
|
+
db_file = db_config['development']['database']
|
10
|
+
File.delete(db_file) if File.exists?(db_file)
|
11
|
+
ActiveRecord::Base.configurations = db_config
|
12
|
+
ActiveRecord::Base.establish_connection('development')
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
# == Mock Framework
|
16
|
+
# If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
|
17
|
+
config.mock_with :mocha
|
18
|
+
|
19
|
+
config.before(:suite) do
|
20
|
+
DatabaseCleaner.strategy = :transaction
|
21
|
+
DatabaseCleaner.clean_with(:truncation)
|
22
|
+
end
|
23
|
+
|
24
|
+
config.before(:each) do
|
25
|
+
DatabaseCleaner.start
|
26
|
+
end
|
27
|
+
|
28
|
+
config.after(:each) do
|
29
|
+
DatabaseCleaner.clean
|
30
|
+
end
|
31
|
+
|
32
|
+
config.include FactoryGirl::Syntax::Methods
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_brand_new_copy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ben Cao
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-11-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 10.0.4
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 10.0.4
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.13.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.13.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: mocha
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.13.3
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.13.3
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: sqlite3
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 1.3.7
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 1.3.7
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: database_cleaner
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: factory_girl
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: factory_girl_rails
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: pry
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: pry-theme
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ! '>='
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
- !ruby/object:Gem::Dependency
|
175
|
+
name: pry-nav
|
176
|
+
requirement: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ! '>='
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
type: :development
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
none: false
|
186
|
+
requirements:
|
187
|
+
- - ! '>='
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
- !ruby/object:Gem::Dependency
|
191
|
+
name: activesupport
|
192
|
+
requirement: !ruby/object:Gem::Requirement
|
193
|
+
none: false
|
194
|
+
requirements:
|
195
|
+
- - ~>
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: 3.2.13
|
198
|
+
type: :runtime
|
199
|
+
prerelease: false
|
200
|
+
version_requirements: !ruby/object:Gem::Requirement
|
201
|
+
none: false
|
202
|
+
requirements:
|
203
|
+
- - ~>
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: 3.2.13
|
206
|
+
- !ruby/object:Gem::Dependency
|
207
|
+
name: activerecord
|
208
|
+
requirement: !ruby/object:Gem::Requirement
|
209
|
+
none: false
|
210
|
+
requirements:
|
211
|
+
- - ~>
|
212
|
+
- !ruby/object:Gem::Version
|
213
|
+
version: 3.2.13
|
214
|
+
type: :runtime
|
215
|
+
prerelease: false
|
216
|
+
version_requirements: !ruby/object:Gem::Requirement
|
217
|
+
none: false
|
218
|
+
requirements:
|
219
|
+
- - ~>
|
220
|
+
- !ruby/object:Gem::Version
|
221
|
+
version: 3.2.13
|
222
|
+
description: A ruby gem for active record which simplify the copy of very complex
|
223
|
+
tree data.
|
224
|
+
email:
|
225
|
+
- benb88@gmail.com
|
226
|
+
executables: []
|
227
|
+
extensions: []
|
228
|
+
extra_rdoc_files: []
|
229
|
+
files:
|
230
|
+
- .gitignore
|
231
|
+
- .travis.yml
|
232
|
+
- Gemfile
|
233
|
+
- LICENSE.txt
|
234
|
+
- README.md
|
235
|
+
- Rakefile
|
236
|
+
- acts_as_brand_new_copy.gemspec
|
237
|
+
- db/database.yml
|
238
|
+
- lib/acts_as_brand_new_copy.rb
|
239
|
+
- lib/acts_as_brand_new_copy/version.rb
|
240
|
+
- spec/acts_as_brand_new_copy_spec.rb
|
241
|
+
- spec/spec_helper.rb
|
242
|
+
homepage: https://github.com/bencao/acts_as_brand_new_copy
|
243
|
+
licenses:
|
244
|
+
- MIT
|
245
|
+
post_install_message:
|
246
|
+
rdoc_options: []
|
247
|
+
require_paths:
|
248
|
+
- lib
|
249
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
250
|
+
none: false
|
251
|
+
requirements:
|
252
|
+
- - ! '>='
|
253
|
+
- !ruby/object:Gem::Version
|
254
|
+
version: '0'
|
255
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
256
|
+
none: false
|
257
|
+
requirements:
|
258
|
+
- - ! '>='
|
259
|
+
- !ruby/object:Gem::Version
|
260
|
+
version: '0'
|
261
|
+
requirements: []
|
262
|
+
rubyforge_project:
|
263
|
+
rubygems_version: 1.8.25
|
264
|
+
signing_key:
|
265
|
+
specification_version: 3
|
266
|
+
summary: Just give me the object tree specification and callbacks, I will do all the
|
267
|
+
rest for you.
|
268
|
+
test_files:
|
269
|
+
- spec/acts_as_brand_new_copy_spec.rb
|
270
|
+
- spec/spec_helper.rb
|