amoeba 0.1.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -25,9 +25,11 @@ Rails 3.2 compatible.
25
25
  - `has_and_belongs_to_many`
26
26
  - A simple DSL for configuration of which fields to copy. The DSL can be applied to your rails models or used on the fly.
27
27
  - Multiple configuration styles such as inclusive, exclusive and indiscriminate (aka copy everything).
28
+ - Supports cloning of the children of Many-to-Many records as well as not cloning the child records but merely maintaining original associations
28
29
  - Supports recursive copying of child and grandchild records.
29
30
  - 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.
30
31
  - Amoeba can perform the following preprocessing operations on fields of copied records
32
+ - set
31
33
  - prepend
32
34
  - append
33
35
  - nullify
@@ -133,7 +135,7 @@ Using the inclusive style within the amoeba block actually implies that you wish
133
135
  belongs_to :post
134
136
  end
135
137
 
136
- You may also specify fields to be copied by passing an array.
138
+ You may also specify fields to be copied by passing an array. If you call the `include_field` 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.
137
139
 
138
140
  class Post < ActiveRecord::Base
139
141
  has_many :comments
@@ -171,6 +173,38 @@ If you have more fields to include than to exclude, you may wish to shorten the
171
173
 
172
174
  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.
173
175
 
176
+ ### Cloning
177
+
178
+ 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.
179
+
180
+ class Post < ActiveRecord::Base
181
+ has_and_belongs_to_many :warnings
182
+
183
+ has_many :post_widgets
184
+ has_many :widgets, :through => :post_widgets
185
+
186
+ amoeba do
187
+ enable
188
+ clone [:widgets, :tags]
189
+ end
190
+ end
191
+
192
+ class Warning < ActiveRecord::Base
193
+ has_and_belongs_to_many :posts
194
+ end
195
+
196
+ class PostWidget < ActiveRecord::Base
197
+ belongs_to :widget
198
+ belongs_to :post
199
+ end
200
+
201
+ class Widget < ActiveRecord::Base
202
+ has_many :post_widgets
203
+ has_many :posts, :through => :post_widgets
204
+ end
205
+
206
+ 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.
207
+
174
208
  ### Limiting Association Types
175
209
 
176
210
  By default, amoeba recognizes and attemps to copy any children of the following association types:
@@ -230,6 +264,18 @@ This example will copy all of a post's comments. It will also nullify the publis
230
264
 
231
265
  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.
232
266
 
267
+ #### Set
268
+
269
+ If you wish to just set a field to an aribrary 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.
270
+
271
+ class Post < ActiveRecord::Base
272
+ amoeba do
273
+ set :state_tracker => "open_for_editing"
274
+ end
275
+ end
276
+
277
+ 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.
278
+
233
279
  #### Prepend
234
280
 
235
281
  You may add a string to the beginning of a copied object's field during the copy phase:
@@ -1,3 +1,4 @@
1
+ require "active_record"
1
2
  require "amoeba/version"
2
3
 
3
4
  module Amoeba
@@ -20,6 +21,11 @@ module Amoeba
20
21
  @do_preproc
21
22
  end
22
23
 
24
+ def known_macros
25
+ @known_macros ||= [:has_one, :has_many, :has_and_belongs_to_many]
26
+ @known_macros
27
+ end
28
+
23
29
  def includes
24
30
  @includes ||= []
25
31
  @includes
@@ -30,9 +36,9 @@ module Amoeba
30
36
  @excludes
31
37
  end
32
38
 
33
- def known_macros
34
- @known_macros ||= [:has_one, :has_many, :has_and_belongs_to_many]
35
- @known_macros
39
+ def clones
40
+ @clones ||= []
41
+ @clones
36
42
  end
37
43
 
38
44
  def null_fields
@@ -40,6 +46,11 @@ module Amoeba
40
46
  @null_fields
41
47
  end
42
48
 
49
+ def coercions
50
+ @coercions ||= {}
51
+ @coercions
52
+ end
53
+
43
54
  def prefixes
44
55
  @prefixes ||= {}
45
56
  @prefixes
@@ -87,6 +98,17 @@ module Amoeba
87
98
  @excludes
88
99
  end
89
100
 
101
+ def clone(value=nil)
102
+ @enabled ||= true
103
+ @clones ||= []
104
+ if value.is_a?(Array)
105
+ @clones = value
106
+ else
107
+ @clones << value if value
108
+ end
109
+ @clones
110
+ end
111
+
90
112
  def recognize(value=nil)
91
113
  @enabled ||= true
92
114
  @known_macros ||= []
@@ -109,6 +131,25 @@ module Amoeba
109
131
  @null_fields
110
132
  end
111
133
 
134
+ def set(defs=nil)
135
+ @do_preproc ||= true
136
+ @coercions ||= {}
137
+ if defs.is_a?(Array)
138
+ @coercions = {}
139
+
140
+ defs.each do |d|
141
+ d.each do |k,v|
142
+ @coercions[k] = v if v
143
+ end
144
+ end
145
+ else
146
+ defs.each do |k,v|
147
+ @coercions[k] = v if v
148
+ end
149
+ end
150
+ @coercions
151
+ end
152
+
112
153
  def prepend(defs=nil)
113
154
  @do_preproc ||= true
114
155
  @prefixes ||= {}
@@ -194,25 +235,54 @@ module Amoeba
194
235
  @result.send(:"#{relation_name}=", copy_of_obj)
195
236
  end
196
237
  when :has_many
197
- # copying the children of the regular has many will
198
- # effectively do what is desired anyway, the through
199
- # association is really just for convenience usage
200
- # on the model
201
- if settings.is_a?(ActiveRecord::Reflection::ThroughReflection)
202
- return
238
+ clone = amoeba_conf.clones.include?(:"#{relation_name}")
239
+
240
+ # this could be DRYed up for better readability by
241
+ # duplicating the loop code, but I'm duplicating the
242
+ # loops to avoid that extra check on each iteration
243
+ if clone
244
+ # This is a M:M "has many through" where we
245
+ # actually copy and reassociate the new children
246
+ # rather than only maintaining the associations
247
+ self.send(relation_name).each do |old_obj|
248
+ copy_of_obj = old_obj.dup
249
+
250
+ # associate this new child to the new parent object
251
+ @result.send(relation_name) << copy_of_obj
252
+ end
253
+ else
254
+ # This is a regular 1:M "has many"
255
+ #
256
+ # copying the children of the regular has many will
257
+ # effectively do what is desired anyway, the through
258
+ # association is really just for convenience usage
259
+ # on the model
260
+ return if settings.is_a?(ActiveRecord::Reflection::ThroughReflection)
261
+
262
+ self.send(relation_name).each do |old_obj|
263
+ copy_of_obj = old_obj.dup
264
+ copy_of_obj[:"#{settings.foreign_key}"] = nil
265
+
266
+ # associate this new child to the new parent object
267
+ @result.send(relation_name) << copy_of_obj
268
+ end
203
269
  end
204
270
 
205
- self.send(relation_name).each do |old_obj|
206
- copy_of_obj = old_obj.dup
207
- copy_of_obj[:"#{settings.foreign_key}"] = nil
208
-
209
- # associate this new child to the new parent object
210
- @result.send(relation_name) << copy_of_obj
211
- end
212
271
  when :has_and_belongs_to_many
213
- self.send(relation_name).each do |old_obj|
214
- # associate this new child to the new parent object
215
- @result.send(relation_name) << old_obj
272
+ clone = amoeba_conf.clones.include?(relation_name)
273
+
274
+ if clone
275
+ self.send(relation_name).each do |old_obj|
276
+ copy_of_obj = old_obj.dup
277
+
278
+ # associate this new child to the new parent object
279
+ @result.send(relation_name) << copy_of_obj
280
+ end
281
+ else
282
+ self.send(relation_name).each do |old_obj|
283
+ # associate this new child to the new parent object
284
+ @result.send(relation_name) << old_obj
285
+ end
216
286
  end
217
287
  end
218
288
  end
@@ -220,6 +290,18 @@ module Amoeba
220
290
  def dup(options={})
221
291
  @result = super()
222
292
 
293
+ amoeba_conf.clones.each do |clone_field|
294
+ r = self.class.reflect_on_association clone_field
295
+
296
+ # if this is a has many through and we're gonna deep
297
+ # copy the child records, exclude the regular join
298
+ # table from copying so we don't end up with the new
299
+ # and old children on the copy
300
+ if r.macro == :has_many && r.is_a?(ActiveRecord::Reflection::ThroughReflection)
301
+ amoeba_conf.exclude_field r.options[:through]
302
+ end
303
+ end
304
+
223
305
  if amoeba_conf.enabled
224
306
  if amoeba_conf.includes.count > 0
225
307
  amoeba_conf.includes.each do |i|
@@ -252,6 +334,11 @@ module Amoeba
252
334
  @result[n] = nil
253
335
  end
254
336
 
337
+ # prepend any extra strings to indicate uniqueness of the new record(s)
338
+ amoeba_conf.coercions.each do |field,coercion|
339
+ @result[field] = "#{coercion}"
340
+ end
341
+
255
342
  # prepend any extra strings to indicate uniqueness of the new record(s)
256
343
  amoeba_conf.prefixes.each do |field,prefix|
257
344
  @result[field] = "#{prefix}#{@result[field]}"
@@ -1,3 +1,3 @@
1
1
  module Amoeba
2
- VERSION = "0.1.2"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -18,12 +18,18 @@ describe "amoeba" do
18
18
  start_cat_count = Category.all.count
19
19
  start_supercat_count = Supercat.all.count
20
20
  start_tag_count = Tag.all.count
21
+ start_note_count = Note.all.count
22
+ start_widget_count = Widget.all.count
21
23
  start_post_count = Post.all.count
22
24
  start_comment_count = Comment.all.count
23
25
  start_rating_count = Rating.all.count
24
26
  start_postconfig_count = PostConfig.all.count
27
+ start_postwidget_count = PostWidget.all.count
28
+ start_superkitten_count = Superkitten.all.count
25
29
  rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS tag_count FROM posts_tags')
26
30
  start_posttag_count = rs["tag_count"]
31
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS note_count FROM notes_posts')
32
+ start_postnote_count = rs["note_count"]
27
33
 
28
34
  new_post.save
29
35
 
@@ -32,12 +38,18 @@ describe "amoeba" do
32
38
  end_cat_count = Category.all.count
33
39
  end_supercat_count = Supercat.all.count
34
40
  end_tag_count = Tag.all.count
41
+ end_note_count = Note.all.count
42
+ end_widget_count = Widget.all.count
35
43
  end_post_count = Post.all.count
36
44
  end_comment_count = Comment.all.count
37
45
  end_rating_count = Rating.all.count
38
46
  end_postconfig_count = PostConfig.all.count
47
+ end_postwidget_count = PostWidget.all.count
48
+ end_superkitten_count = Superkitten.all.count
39
49
  rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS tag_count FROM posts_tags')
40
50
  end_posttag_count = rs["tag_count"]
51
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS note_count FROM notes_posts')
52
+ end_postnote_count = rs["note_count"]
41
53
 
42
54
  end_tag_count.should == start_tag_count
43
55
  end_cat_count.should == start_cat_count
@@ -49,10 +61,21 @@ describe "amoeba" do
49
61
  end_rating_count.should == start_rating_count * 2
50
62
  end_postconfig_count.should == start_postconfig_count * 2
51
63
  end_posttag_count.should == start_posttag_count * 2
64
+ end_widget_count.should == start_widget_count * 2
65
+ end_postwidget_count.should == start_postwidget_count * 2
66
+ end_note_count.should == start_note_count * 2
67
+ end_postnote_count.should == start_postnote_count * 2
68
+ end_superkitten_count.should == start_superkitten_count * 2
52
69
 
53
70
  new_post.supercats.map(&:ramblings).include?("Copy of zomg").should be true
71
+ new_post.supercats.map(&:other_ramblings).uniq.length.should == 1
72
+ new_post.supercats.map(&:other_ramblings).uniq.include?("La la la").should be true
54
73
  new_post.title.should == "Copy of #{old_post.title}"
55
74
  new_post.contents.should == "Here's a copy: #{old_post.contents.gsub(/dog/, 'cat')} (copied version)"
75
+
76
+ new_post.widgets.map(&:id).each do |id|
77
+ old_post.widgets.map(&:id).include?(id).should_not be true
78
+ end
56
79
  end
57
80
  end
58
81
  end
@@ -41,11 +41,41 @@ p1.tags << t2
41
41
  p1.tags << t3
42
42
  p1.save
43
43
 
44
+ w1 = Widget.create(:value => "My Sidebar")
45
+ w2 = Widget.create(:value => "Photo Gallery")
46
+ w3 = Widget.create(:value => "Share & Like")
47
+
48
+ p1.widgets << w1
49
+ p1.widgets << w2
50
+ p1.widgets << w3
51
+ p1.save
52
+
53
+ n1 = Note.create(:value => "This is important")
54
+ n2 = Note.create(:value => "You've been warned")
55
+ n3 = Note.create(:value => "Don't forget")
56
+
57
+ p1.notes << n1
58
+ p1.notes << n2
59
+ p1.notes << n3
60
+ p1.save
61
+
44
62
  c1 = Category.create(:title => "Umbrellas", :description => "Clown fart")
45
63
  c2 = Category.create(:title => "Widgets", :description => "Humpty dumpty")
46
64
  c3 = Category.create(:title => "Wombats", :description => "Slushy mushy")
47
65
 
48
- Supercat.create(:post => p1, :category => c1, :ramblings => "zomg")
49
- Supercat.create(:post => p1, :category => c2, :ramblings => "why")
50
- Supercat.create(:post => p1, :category => c3, :ramblings => "ohnoes")
66
+ s1 = Supercat.create(:post => p1, :category => c1, :ramblings => "zomg", :other_ramblings => "nerp")
67
+ s2 = Supercat.create(:post => p1, :category => c2, :ramblings => "why", :other_ramblings => "narp")
68
+ s3 = Supercat.create(:post => p1, :category => c3, :ramblings => "ohnoes", :other_ramblings => "blap")
69
+
70
+ s1.superkittens.create(:value => "Fluffy")
71
+ s1.superkittens.create(:value => "Buffy")
72
+ s1.superkittens.create(:value => "Fuzzy")
73
+
74
+ s2.superkittens.create(:value => "Hairball")
75
+ s2.superkittens.create(:value => "Crosseye")
76
+ s2.superkittens.create(:value => "Spot")
77
+
78
+ s3.superkittens.create(:value => "Dopey")
79
+ s3.superkittens.create(:value => "Sneezy")
80
+ s3.superkittens.create(:value => "Sleepy")
51
81
  # }}}
@@ -11,10 +11,15 @@ class Post < ActiveRecord::Base
11
11
  has_many :comments
12
12
  has_many :supercats
13
13
  has_many :categories, :through => :supercats
14
+ has_many :post_widgets
15
+ has_many :widgets, :through => :post_widgets
14
16
  has_and_belongs_to_many :tags
17
+ has_and_belongs_to_many :notes
18
+ #has_and_belongs_to_many :tags
15
19
 
16
20
  amoeba do
17
21
  enable
22
+ clone [:widgets, :notes]
18
23
  prepend :title => "Copy of "
19
24
  append :contents => " (copied version)"
20
25
  regex :contents => {:replace => /dog/, :with => 'cat'}
@@ -37,17 +42,30 @@ end
37
42
  class Category < ActiveRecord::Base
38
43
  has_many :supercats
39
44
  has_many :posts, :through => :supercats
45
+
46
+ amoeba do
47
+ enable
48
+ prepend :ramblings => "Copy of "
49
+ set :other_ramblings => "La la la"
50
+ end
40
51
  end
41
52
 
42
53
  class Supercat < ActiveRecord::Base
43
54
  belongs_to :post
44
55
  belongs_to :category
56
+ has_many :superkittens
45
57
 
46
58
  amoeba do
59
+ include_field :superkittens
47
60
  prepend :ramblings => "Copy of "
61
+ set :other_ramblings => "La la la"
48
62
  end
49
63
  end
50
64
 
65
+ class Superkitten < ActiveRecord::Base
66
+ belongs_to :supercat
67
+ end
68
+
51
69
  class PostConfig < ActiveRecord::Base
52
70
  belongs_to :post
53
71
  end
@@ -55,20 +73,39 @@ end
55
73
  class Comment < ActiveRecord::Base
56
74
  belongs_to :post
57
75
  has_many :ratings
76
+ has_many :reviews
58
77
 
59
78
  amoeba do
60
- enable
79
+ exclude_field :reviews
61
80
  end
62
81
  end
63
82
 
83
+ class Review < ActiveRecord::Base
84
+ belongs_to :comment
85
+ end
86
+
64
87
  class Rating < ActiveRecord::Base
65
88
  belongs_to :comment
66
89
  end
67
90
 
91
+ class Widget < ActiveRecord::Base
92
+ has_many :post_widgets
93
+ has_many :posts, :through => :post_widgets
94
+ end
95
+
96
+ class PostWidget < ActiveRecord::Base
97
+ belongs_to :post
98
+ belongs_to :widget
99
+ end
100
+
68
101
  class Tag < ActiveRecord::Base
69
102
  has_and_belongs_to_many :posts
70
103
  end
71
104
 
105
+ class Note < ActiveRecord::Base
106
+ has_and_belongs_to_many :posts
107
+ end
108
+
72
109
  class User < ActiveRecord::Base
73
110
  has_many :posts
74
111
  end
@@ -42,7 +42,6 @@ ActiveRecord::Schema.define do
42
42
  end
43
43
 
44
44
  create_table :tags, :force => true do |t|
45
- t.integer :post_id
46
45
  t.string :value
47
46
  t.timestamps
48
47
  end
@@ -59,6 +58,25 @@ ActiveRecord::Schema.define do
59
58
  t.integer :tag_id
60
59
  end
61
60
 
61
+ create_table :notes, :force => true do |t|
62
+ t.string :value
63
+ t.timestamps
64
+ end
65
+
66
+ create_table :notes_posts, :force => true do |t|
67
+ t.integer :post_id
68
+ t.integer :note_id
69
+ end
70
+
71
+ create_table :widgets, :force => true do |t|
72
+ t.string :value
73
+ end
74
+
75
+ create_table :post_widgets, :force => true do |t|
76
+ t.integer :post_id
77
+ t.integer :widget_id
78
+ end
79
+
62
80
  create_table :categories, :force => true do |t|
63
81
  t.string :title
64
82
  t.string :description
@@ -68,6 +86,13 @@ ActiveRecord::Schema.define do
68
86
  t.integer :post_id
69
87
  t.integer :category_id
70
88
  t.string :ramblings
89
+ t.string :other_ramblings
90
+ t.timestamps
91
+ end
92
+
93
+ create_table :superkittens, :force => true do |t|
94
+ t.integer :supercat_id
95
+ t.string :value
71
96
  t.timestamps
72
97
  end
73
98
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amoeba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-28 00:00:00.000000000 Z
12
+ date: 2012-03-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
16
- requirement: &22165920 !ruby/object:Gem::Requirement
16
+ requirement: &12789100 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 1.0.0
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *22165920
24
+ version_requirements: *12789100
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &22165380 !ruby/object:Gem::Requirement
27
+ requirement: &12788540 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '2.3'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *22165380
35
+ version_requirements: *12788540
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: sqlite3-ruby
38
- requirement: &22164980 !ruby/object:Gem::Requirement
38
+ requirement: &12788080 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *22164980
46
+ version_requirements: *12788080
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: activerecord
49
- requirement: &22164300 !ruby/object:Gem::Requirement
49
+ requirement: &12787380 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,7 @@ dependencies:
54
54
  version: '3.0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *22164300
57
+ version_requirements: *12787380
58
58
  description: ! 'An extension to ActiveRecord to allow the duplication method to also
59
59
  copy associated children, with recursive support for nested of grandchildren. The
60
60
  behavior is controllable with a simple DSL both on your rails models and on the