amoeba 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -4,11 +4,21 @@ Easy copying of rails associations such as `has_many`.
4
4
 
5
5
  ![amoebalogo](http://rocksolidwebdesign.com/wp_cms/wp-content/uploads/2012/02/amoeba_logo.jpg)
6
6
 
7
- ## Background
7
+ ## Installation
8
+
9
+ is hopefully as you would expect:
10
+
11
+ gem install amoeba
12
+
13
+ or just add it to your Gemfile:
14
+
15
+ gem 'amoeba'
16
+
17
+ ## What?
8
18
 
9
19
  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.
10
20
 
11
- I named this gem "Amoeba" because amoebas are (small life forms that are) good at reproducing. Their children and grandchildren also reproduce themselves quickly and easily.
21
+ 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.
12
22
 
13
23
  ## Details
14
24
 
@@ -24,11 +34,12 @@ Rails 3.2 compatible.
24
34
  - `has_many :through`
25
35
  - `has_and_belongs_to_many`
26
36
  - A simple DSL for configuration of which fields to copy. The DSL can be applied to your rails models or used on the fly.
37
+ - Supports STI (Single Table Inheritance) children inheriting their parent amoeba settings.
27
38
  - Multiple configuration styles such as inclusive, exclusive and indiscriminate (aka copy everything).
28
39
  - Supports cloning of the children of Many-to-Many records or merely maintaining original associations
29
- - Supports recursive copying of child and grandchild records.
40
+ - Supports automatic drill-down i.e. recursive copying of child and grandchild records.
30
41
  - 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.
31
- - Supports preprocessing of fields with custom lambda blocks so you can basically whatever you want, for example if you need some custom logic while making copies.
42
+ - 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.
32
43
  - Amoeba can perform the following preprocessing operations on fields of copied records
33
44
  - set
34
45
  - prepend
@@ -37,16 +48,6 @@ Rails 3.2 compatible.
37
48
  - customize
38
49
  - regex
39
50
 
40
- ## Installation
41
-
42
- is hopefully as you would expect:
43
-
44
- gem install amoeba
45
-
46
- or just add it to your Gemfile:
47
-
48
- gem 'amoeba'
49
-
50
51
  ## Usage
51
52
 
52
53
  Configure your models with one of the styles below and then just run the `dup` method on your model as you normally would:
@@ -155,6 +156,8 @@ You may also specify fields to be copied by passing an array. If you call the `i
155
156
 
156
157
  These examples will copy the post's tags and authors but not its comments.
157
158
 
159
+ The inclusive style, when used, will automatically disable any ther style that was previously selected.
160
+
158
161
  ### Exclusive Style
159
162
 
160
163
  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:
@@ -175,6 +178,8 @@ If you have more fields to include than to exclude, you may wish to shorten the
175
178
 
176
179
  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.
177
180
 
181
+ 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_field` method will disable the previously slected inclusive style and wipe out any corresponding include fields.
182
+
178
183
  ### Cloning
179
184
 
180
185
  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.
@@ -207,6 +212,188 @@ If you are using a Many-to-Many relationship, you may tell amoeba to actually ma
207
212
 
208
213
  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.
209
214
 
215
+ ## Inheritance
216
+
217
+ 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.
218
+
219
+ create_table :products, :force => true do |t|
220
+ t.string :type # this is the STI column
221
+
222
+ # these belong to all products
223
+ t.string :title
224
+ t.decimal :price
225
+
226
+ # these are for shirts only
227
+ t.decimal :sleeve_length
228
+ t.decimal :collar_size
229
+
230
+ # these are for computers only
231
+ t.integer :ram_size
232
+ t.integer :hard_drive_size
233
+ end
234
+
235
+ class Product < ActiveRecord::Base
236
+ has_many :images
237
+ has_and_belongs_to_many :categories
238
+
239
+ amoeba do
240
+ enable
241
+ propagate
242
+ end
243
+ end
244
+
245
+ class Shirt < Product
246
+ end
247
+
248
+ class Computer < Product
249
+ end
250
+
251
+ class ProductsController
252
+ def some_method
253
+ my_shirt = Shirt.find(1)
254
+ my_shirt.dup
255
+ my_shirt.save
256
+
257
+ # this shirt should now:
258
+ # - have its own copy of all parent images
259
+ # - be in the same categories as the parent
260
+ end
261
+ end
262
+
263
+ This example should duplicate all the images and sections associated with this Shirt, which is a child of Product
264
+
265
+ ### Parenting Style
266
+
267
+ 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.
268
+
269
+ 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.
270
+
271
+ #### Relaxed Parenting
272
+
273
+ The `:relaxed` parenting style will prefer parent settings.
274
+
275
+ class Product < ActiveRecord::Base
276
+ has_many :images
277
+ has_and_belongs_to_many :sections
278
+
279
+ amoeba do
280
+ exclude_field :images
281
+ propagate :relaxed
282
+ end
283
+ end
284
+
285
+ class Shirt < Product
286
+ include_field :images
287
+ include_field :sections
288
+ prepend :title => "Copy of "
289
+ end
290
+
291
+ In this example, the conflicting `include_field` settings on the child will be ignored and the parent `exclude_field` setting will be used, while the `prepend` setting on the child will be honored because it doesn't conflict with the parent.
292
+
293
+ #### Strict Parenting
294
+
295
+ The `:strict` style will ignore child settings altogether and inherit any parent settings.
296
+
297
+ class Product < ActiveRecord::Base
298
+ has_many :images
299
+ has_and_belongs_to_many :sections
300
+
301
+ amoeba do
302
+ exclude_field :images
303
+ propagate :strict
304
+ end
305
+ end
306
+
307
+ class Shirt < Product
308
+ include_field :images
309
+ include_field :sections
310
+ prepend :title => "Copy of "
311
+ end
312
+
313
+ 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_field` directive takes precedence over the child's `include_field` 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.
314
+
315
+ #### Parenting and Precedence
316
+
317
+ 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.
318
+
319
+ Just remember that:
320
+
321
+ * If you pass an array you will wipe all previous settings
322
+ * If you pass single values, you will add to currently existing settings
323
+
324
+ This means that, for example:
325
+
326
+ * 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.
327
+ * 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.
328
+
329
+ #### A Submissive Override Example
330
+
331
+ This version will use both the parent and child settings, so both the images and sections will be copied.
332
+
333
+ class Product < ActiveRecord::Base
334
+ has_many :images
335
+ has_and_belongs_to_many :sections
336
+
337
+ amoeba do
338
+ include_field :images
339
+ propagate
340
+ end
341
+ end
342
+
343
+ class Shirt < Product
344
+ include_field :sections
345
+ end
346
+
347
+ 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.
348
+
349
+ class Product < ActiveRecord::Base
350
+ has_many :images
351
+ has_and_belongs_to_many :sections
352
+
353
+ amoeba do
354
+ include_field :images
355
+ propagate
356
+ end
357
+ end
358
+
359
+ class Shirt < Product
360
+ include_field [:sections]
361
+ end
362
+
363
+ #### A Relaxed Override Example
364
+
365
+ This version will use both the parent and child settings, so both the images and sections will be copied.
366
+
367
+ class Product < ActiveRecord::Base
368
+ has_many :images
369
+ has_and_belongs_to_many :sections
370
+
371
+ amoeba do
372
+ include_field :images
373
+ propagate :relaxed
374
+ end
375
+ end
376
+
377
+ class Shirt < Product
378
+ include_field :sections
379
+ end
380
+
381
+ 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.
382
+
383
+ class Product < ActiveRecord::Base
384
+ has_many :images
385
+ has_and_belongs_to_many :sections
386
+
387
+ amoeba do
388
+ include_field [:images]
389
+ propagate
390
+ end
391
+ end
392
+
393
+ class Shirt < Product
394
+ include_field :sections
395
+ end
396
+
210
397
  ### Limiting Association Types
211
398
 
212
399
  By default, amoeba recognizes and attemps to copy any children of the following association types:
@@ -552,55 +739,83 @@ You may control how amoeba copies your object, on the fly, by passing a configur
552
739
  end
553
740
  end
554
741
 
555
- ### Config-Block Reference
742
+ ### Configuration Reference
556
743
 
557
744
  Here is a static reference to the available configuration methods, usable within the amoeba block on your rails models.
558
745
 
559
746
  #### Controlling Associations
560
747
 
561
- `enable`
748
+ * `enable`
749
+
750
+ 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_field` or `exclude_field` 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.
751
+
752
+ * `include_field`
753
+
754
+ 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.
562
755
 
563
- 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_field` or `exclude_field` 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.
756
+ * `exclude_field`
564
757
 
565
- `include_field`
758
+ 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.
566
759
 
567
- 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.
760
+ * `clone`
568
761
 
569
- `exclude_field`
762
+ 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.
570
763
 
571
- 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.
764
+ * `propagate`
572
765
 
573
- `clone`
766
+ 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`.
767
+
768
+ The default "parenting style" is `submissive`
769
+
770
+ for example
771
+
772
+ amoeba do
773
+ propagate :strict
774
+ end
775
+
776
+ will choose the strict parenting style of inherited settings.
777
+
778
+ * `raised`
779
+
780
+ 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`.
781
+
782
+ The default "parenting style" is `submissive`
783
+
784
+ for example:
785
+
786
+ amoeba do
787
+ raised :relaxed
788
+ end
574
789
 
575
- 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.
790
+ 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.
576
791
 
577
792
  #### Pre-Processing Fields
578
793
 
579
- `nullify`
794
+ * `nullify`
580
795
 
581
- 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.
796
+ 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.
582
797
 
583
- `prepend`
798
+ * `prepend`
584
799
 
585
- 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 "}]`.
800
+ 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 "}]`.
586
801
 
587
- `append`
802
+ * `append`
588
803
 
589
- Append some text to a field. 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 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)"}]`.
804
+ Append some text to a field. 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 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)"}]`.
590
805
 
591
- `set`
806
+ * `set`
592
807
 
593
- Set a field to a given value. This sould 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"}]`.
808
+ Set a field to a given value. This sould 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"}]`.
594
809
 
595
- `regex`
810
+ * `regex`
596
811
 
597
- 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"}]`.
812
+ 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"}]`.
598
813
 
599
- `customize`
814
+ * `customize`
600
815
 
601
- 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
816
+ 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
602
817
 
603
- 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.
818
+ 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.
604
819
 
605
820
  ## Known Limitations and Issues
606
821
 
@@ -2,25 +2,38 @@ require "active_record"
2
2
  require "amoeba/version"
3
3
 
4
4
  module Amoeba
5
- module ClassMethods
6
- def amoeba(&block)
7
- @config ||= Amoeba::ClassMethods::Config.new
8
- @config.instance_eval(&block) if block_given?
9
- @config
10
- end
11
-
5
+ module Dsl # {{{
12
6
  class Config
7
+ def initialize(parent)
8
+ @parent = parent
9
+ end
10
+
13
11
  # Getters {{{
12
+ def upbringing
13
+ @raised ||= false
14
+ @raised
15
+ end
16
+
14
17
  def enabled
15
18
  @enabled ||= false
16
19
  @enabled
17
20
  end
18
21
 
22
+ def inherit
23
+ @inherit ||= false
24
+ @inherit
25
+ end
26
+
19
27
  def do_preproc
20
28
  @do_preproc ||= false
21
29
  @do_preproc
22
30
  end
23
31
 
32
+ def parenting
33
+ @parenting ||= false
34
+ @parenting
35
+ end
36
+
24
37
  def known_macros
25
38
  @known_macros ||= [:has_one, :has_many, :has_and_belongs_to_many]
26
39
  @known_macros
@@ -81,8 +94,18 @@ module Amoeba
81
94
  @enabled = false
82
95
  end
83
96
 
97
+ def raised(style=:submissive)
98
+ @raised = style
99
+ end
100
+
101
+ def propagate(style=:submissive)
102
+ @parenting ||= style
103
+ @inherit = true
104
+ end
105
+
84
106
  def include_field(value=nil)
85
107
  @enabled ||= true
108
+ @excludes = []
86
109
  @includes ||= []
87
110
  if value.is_a?(Array)
88
111
  @includes = value
@@ -94,6 +117,7 @@ module Amoeba
94
117
 
95
118
  def exclude_field(value=nil)
96
119
  @enabled ||= true
120
+ @includes = []
97
121
  @excludes ||= []
98
122
  if value.is_a?(Array)
99
123
  @excludes = value
@@ -224,13 +248,141 @@ module Amoeba
224
248
  end
225
249
  # }}}
226
250
  end
251
+ end # }}}
252
+
253
+ module ClassMethods
254
+ def amoeba(&block)
255
+ @config_block ||= block if block_given?
256
+
257
+ @config ||= Amoeba::Dsl::Config.new(self)
258
+ @config.instance_eval(&block) if block_given?
259
+ @config
260
+ end
261
+
262
+ def fresh_amoeba(&block)
263
+ @config_block = block if block_given?
264
+
265
+ @config = Amoeba::Dsl::Config.new(self)
266
+ @config.instance_eval(&block) if block_given?
267
+ @config
268
+ end
269
+
270
+ def amoeba_block
271
+ @config_block
272
+ end
227
273
  end
228
274
 
229
275
  module InstanceMethods
276
+ # Config Getters {{{
230
277
  def amoeba_conf
231
278
  self.class.amoeba
232
279
  end
233
280
 
281
+ def has_parent_amoeba_conf?
282
+ self.class.superclass.respond_to?(:amoeba)
283
+ end
284
+
285
+ def parent_amoeba_conf
286
+ if has_parent_amoeba_conf?
287
+ self.class.superclass.amoeba
288
+ else
289
+ false
290
+ end
291
+ end
292
+
293
+ def amoeba_settings
294
+ self.class.amoeba_block
295
+ end
296
+
297
+ def has_parent_amoeba_settings?
298
+ self.class.superclass.respond_to?(:amoeba_block)
299
+ end
300
+
301
+ def parent_amoeba_settings
302
+ if has_parent_amoeba_conf?
303
+ self.class.superclass.amoeba_block
304
+ else
305
+ false
306
+ end
307
+ end
308
+ # }}}
309
+
310
+ def dup(options={})
311
+ @result = super()
312
+
313
+ if !amoeba_conf.enabled && parent_amoeba_conf.inherit
314
+ if amoeba_conf.upbringing
315
+ parenting_style = amoeba_conf.upbringing
316
+ else
317
+ parenting_style = parent_amoeba_conf.parenting
318
+ end
319
+
320
+ case parenting_style
321
+ when :strict
322
+ # parent settings only
323
+ self.class.fresh_amoeba(&parent_amoeba_settings)
324
+ when :relaxed
325
+ # parent takes precedence
326
+ self.class.amoeba(&parent_amoeba_settings)
327
+ when :submissive
328
+ # parent suggests things
329
+ # child does what it wants to anyway
330
+ child_settings = amoeba_settings
331
+ self.class.fresh_amoeba(&parent_amoeba_settings)
332
+ self.class.amoeba(&child_settings)
333
+ end
334
+ end
335
+
336
+ # Run Amoeba {{{
337
+ if amoeba_conf.enabled
338
+ # Deep Clone Settings {{{
339
+ amoeba_conf.clones.each do |clone_field|
340
+ r = self.class.reflect_on_association clone_field
341
+
342
+ # if this is a has many through and we're gonna deep
343
+ # copy the child records, exclude the regular join
344
+ # table from copying so we don't end up with the new
345
+ # and old children on the copy
346
+ if r.macro == :has_many && r.is_a?(ActiveRecord::Reflection::ThroughReflection)
347
+ amoeba_conf.exclude_field r.options[:through]
348
+ end
349
+ end
350
+ # }}}
351
+
352
+ # Inclusive Style {{{
353
+ if amoeba_conf.includes.count > 0
354
+ amoeba_conf.includes.each do |i|
355
+ r = self.class.reflect_on_association i
356
+ amo_process_association(i, r)
357
+ end
358
+ # }}}
359
+ # Exclusive Style {{{
360
+ elsif amoeba_conf.excludes.count > 0
361
+ reflections.each do |r|
362
+ if not amoeba_conf.excludes.include?(r[0])
363
+ amo_process_association(r[0], r[1])
364
+ end
365
+ end
366
+ # }}}
367
+ # Indiscriminate Style {{{
368
+ else
369
+ reflections.each do |r|
370
+ amo_process_association(r[0], r[1])
371
+ end
372
+ end
373
+ # }}}
374
+ end
375
+
376
+ if amoeba_conf.do_preproc
377
+ amo_preprocess_parent_copy
378
+ end
379
+ # }}}
380
+
381
+ @result
382
+ end
383
+
384
+ private
385
+ # Copy Children {{{
234
386
  def amo_process_association(relation_name, settings)
235
387
  if not amoeba_conf.known_macros.include?(settings.macro)
236
388
  return
@@ -303,49 +455,10 @@ module Amoeba
303
455
  end
304
456
  end
305
457
  end
458
+ # }}}
306
459
 
307
- def dup(options={})
308
- @result = super()
309
-
310
- amoeba_conf.clones.each do |clone_field|
311
- r = self.class.reflect_on_association clone_field
312
-
313
- # if this is a has many through and we're gonna deep
314
- # copy the child records, exclude the regular join
315
- # table from copying so we don't end up with the new
316
- # and old children on the copy
317
- if r.macro == :has_many && r.is_a?(ActiveRecord::Reflection::ThroughReflection)
318
- amoeba_conf.exclude_field r.options[:through]
319
- end
320
- end
321
-
322
- if amoeba_conf.enabled
323
- if amoeba_conf.includes.count > 0
324
- amoeba_conf.includes.each do |i|
325
- r = self.class.reflect_on_association i
326
- amo_process_association(i, r)
327
- end
328
- elsif amoeba_conf.excludes.count > 0
329
- reflections.each do |r|
330
- if not amoeba_conf.excludes.include?(r[0])
331
- amo_process_association(r[0], r[1])
332
- end
333
- end
334
- else
335
- reflections.each do |r|
336
- amo_process_association(r[0], r[1])
337
- end
338
- end
339
- end
340
-
341
- if amoeba_conf.do_preproc
342
- preprocess_parent_copy
343
- end
344
-
345
- @result
346
- end
347
-
348
- def preprocess_parent_copy
460
+ # Field Preprocessor {{{
461
+ def amo_preprocess_parent_copy
349
462
  # nullify any fields the user has configured
350
463
  amoeba_conf.null_fields.each do |n|
351
464
  @result[n] = nil
@@ -376,6 +489,7 @@ module Amoeba
376
489
  @result[field].gsub!(action[:replace], action[:with])
377
490
  end
378
491
  end
492
+ # }}}
379
493
  end
380
494
  end
381
495
 
@@ -1,3 +1,3 @@
1
1
  module Amoeba
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -4,6 +4,7 @@ require 'spec_helper'
4
4
  describe "amoeba" do
5
5
  context "dup" do
6
6
  it "duplicates associated child records" do
7
+ # Posts {{{
7
8
  old_post = Post.find(1)
8
9
  old_post.comments.map(&:contents).include?("I love it!").should be true
9
10
 
@@ -81,6 +82,88 @@ describe "amoeba" do
81
82
  new_post.widgets.map(&:id).each do |id|
82
83
  old_post.widgets.map(&:id).include?(id).should_not be true
83
84
  end
85
+ # }}}
86
+ # Products {{{
87
+ # Base Class {{{
88
+ old_product = Product.find(1)
89
+
90
+ start_image_count = Image.where(:product_id => old_product.id).count
91
+ start_section_count = Section.all.length
92
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', old_product.id)
93
+ start_prodsection_count = rs["section_count"]
94
+
95
+ new_product = old_product.dup
96
+ new_product.save
97
+
98
+ end_image_count = Image.where(:product_id => old_product.id).count
99
+ end_newimage_count = Image.where(:product_id => new_product.id).count
100
+ end_section_count = Section.all.length
101
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', 1)
102
+ end_prodsection_count = rs["section_count"]
103
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', new_product.id)
104
+ end_newprodsection_count = rs["section_count"]
105
+
106
+ end_image_count.should == start_image_count
107
+ end_newimage_count.should == start_image_count
108
+ end_section_count.should == start_section_count
109
+ end_prodsection_count.should == start_prodsection_count
110
+ end_newprodsection_count.should == start_prodsection_count
111
+ # }}}
112
+
113
+ # Inherited Class {{{
114
+ # Shirt {{{
115
+ old_product = Shirt.find(2)
116
+
117
+ start_image_count = Image.where(:product_id => old_product.id).count
118
+ start_section_count = Section.all.length
119
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', old_product.id)
120
+ start_prodsection_count = rs["section_count"]
121
+
122
+ new_product = old_product.dup
123
+ new_product.save
124
+
125
+ end_image_count = Image.where(:product_id => old_product.id).count
126
+ end_newimage_count = Image.where(:product_id => new_product.id).count
127
+ end_section_count = Section.all.length
128
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', 1)
129
+ end_prodsection_count = rs["section_count"]
130
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', new_product.id)
131
+ end_newprodsection_count = rs["section_count"]
132
+
133
+ end_image_count.should == start_image_count
134
+ end_newimage_count.should == start_image_count
135
+ end_section_count.should == start_section_count
136
+ end_prodsection_count.should == start_prodsection_count
137
+ end_newprodsection_count.should == start_prodsection_count
138
+ # }}}
139
+
140
+ # Necklace {{{
141
+ old_product = Necklace.find(3)
142
+
143
+ start_image_count = Image.where(:product_id => old_product.id).count
144
+ start_section_count = Section.all.length
145
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', old_product.id)
146
+ start_prodsection_count = rs["section_count"]
147
+
148
+ new_product = old_product.dup
149
+ new_product.save
150
+
151
+ end_image_count = Image.where(:product_id => old_product.id).count
152
+ end_newimage_count = Image.where(:product_id => new_product.id).count
153
+ end_section_count = Section.all.length
154
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', 1)
155
+ end_prodsection_count = rs["section_count"]
156
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', new_product.id)
157
+ end_newprodsection_count = rs["section_count"]
158
+
159
+ end_image_count.should == start_image_count
160
+ end_newimage_count.should == start_image_count
161
+ end_section_count.should == start_section_count
162
+ end_prodsection_count.should == start_prodsection_count
163
+ end_newprodsection_count.should == start_prodsection_count
164
+ # }}}
165
+ # }}}
166
+ # }}}
84
167
  end
85
168
  end
86
169
  end
@@ -79,3 +79,37 @@ s3.superkittens.create(:value => "Dopey")
79
79
  s3.superkittens.create(:value => "Sneezy")
80
80
  s3.superkittens.create(:value => "Sleepy")
81
81
  # }}}
82
+
83
+ # Product {{{
84
+ product1 = Product.create(:title => "Sticky Notes 5-Pak", :price => 5.99, :weight => 0.56)
85
+ shirt1 = Shirt.create(:title => "Fancy Shirt", :price => 48.95, :sleeve => 32, :collar => 15.5)
86
+ necklace1 = Necklace.create(:title => "Pearl Necklace", :price => 2995.99, :length => 18, :metal => "14k")
87
+
88
+ img1 = product1.images.create(:filename => "sticky.jpg")
89
+ img2 = product1.images.create(:filename => "notes.jpg")
90
+
91
+ img1 = shirt1.images.create(:filename => "02948u31.jpg")
92
+ img2 = shirt1.images.create(:filename => "zsif8327.jpg")
93
+
94
+ img1 = necklace1.images.create(:filename => "ae02x9f1.jpg")
95
+ img2 = necklace1.images.create(:filename => "cba9f912.jpg")
96
+
97
+ office = Section.create(:name => "Office", :num_employees => 2, :total_sales => "1234.56")
98
+ supplies = Section.create(:name => "Supplies", :num_employees => 1, :total_sales => "543.21")
99
+ mens = Section.create(:name => "Mens", :num_employees => 3, :total_sales => "11982.63")
100
+ apparel = Section.create(:name => "Apparel", :num_employees => 5, :total_sales => "1315.20")
101
+ accessories = Section.create(:name => "Accessories", :num_employees => 1, :total_sales => "8992.34")
102
+ jewelry = Section.create(:name => "Jewelry", :num_employees => 3, :total_sales => "25481.77")
103
+
104
+ product1.sections << office
105
+ product1.sections << supplies
106
+ product1.save
107
+
108
+ shirt1.sections << mens
109
+ shirt1.sections << apparel
110
+ shirt1.save
111
+
112
+ necklace1.sections << jewelry
113
+ necklace1.sections << accessories
114
+ necklace1.save
115
+ # }}}
@@ -144,3 +144,46 @@ end
144
144
  class User < ActiveRecord::Base
145
145
  has_many :posts
146
146
  end
147
+
148
+ # Inheritance
149
+ class Product < ActiveRecord::Base
150
+ has_many :images
151
+ has_and_belongs_to_many :sections
152
+
153
+ amoeba do
154
+ enable
155
+ propagate
156
+ end
157
+ end
158
+
159
+ class Section < ActiveRecord::Base
160
+ end
161
+
162
+ class Image < ActiveRecord::Base
163
+ end
164
+
165
+ class Shirt < Product
166
+ amoeba do
167
+ raised :submissive
168
+ end
169
+ end
170
+
171
+ class Necklace < Product
172
+ amoeba do
173
+ raised :relaxed
174
+ end
175
+ end
176
+
177
+ # Polymorphism
178
+ class Address < ActiveRecord::Base
179
+ belongs_to :addressable, :polymorphic => true
180
+ end
181
+
182
+ class Employee < ActiveRecord::Base
183
+ has_many :addresses, :as => :addressable
184
+ end
185
+
186
+ class Customer < ActiveRecord::Base
187
+ has_many :addresses, :as => :addressable
188
+ end
189
+
@@ -15,6 +15,57 @@ ActiveRecord::Schema.define do
15
15
  t.timestamps
16
16
  end
17
17
 
18
+ create_table :products, :force => true do |t|
19
+ t.string :type
20
+ t.string :title
21
+ t.decimal :price
22
+ t.decimal :weight
23
+ t.decimal :cost
24
+ t.decimal :sleeve
25
+ t.decimal :collar
26
+ t.decimal :length
27
+ t.string :metal
28
+ end
29
+
30
+ create_table :products_sections, :force => true do |t|
31
+ t.integer :section_id
32
+ t.integer :product_id
33
+ end
34
+
35
+ create_table :sections, :force => true do |t|
36
+ t.string :name
37
+ t.integer :num_employees
38
+ t.decimal :total_sales
39
+ end
40
+
41
+ create_table :images, :force => true do |t|
42
+ t.string :filename
43
+ t.integer :product_id
44
+ end
45
+
46
+ create_table :employees, :force => true do |t|
47
+ t.string :name
48
+ t.string :ssn
49
+ t.decimal :salary
50
+ end
51
+
52
+ create_table :customers, :force => true do |t|
53
+ t.string :email
54
+ t.string :password
55
+ t.decimal :balance
56
+ end
57
+
58
+ create_table :addresses, :force => true do |t|
59
+ t.integer :addressable_id
60
+ t.string :addressable_type
61
+
62
+ t.string :street
63
+ t.string :unit
64
+ t.string :city
65
+ t.string :state
66
+ t.string :zip
67
+ end
68
+
18
69
  create_table :post_configs, :force => true do |t|
19
70
  t.integer :post_id
20
71
  t.integer :is_visible
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: 1.1.0
4
+ version: 1.2.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-03-22 00:00:00.000000000 Z
12
+ date: 2012-03-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
16
- requirement: &8806100 !ruby/object:Gem::Requirement
16
+ requirement: &11153980 !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: *8806100
24
+ version_requirements: *11153980
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &8805580 !ruby/object:Gem::Requirement
27
+ requirement: &11153480 !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: *8805580
35
+ version_requirements: *11153480
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: sqlite3-ruby
38
- requirement: &8805180 !ruby/object:Gem::Requirement
38
+ requirement: &11153080 !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: *8805180
46
+ version_requirements: *11153080
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: activerecord
49
- requirement: &8804640 !ruby/object:Gem::Requirement
49
+ requirement: &11152540 !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: *8804640
57
+ version_requirements: *11152540
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