deferred_associations 0.5.4 → 0.5.5

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/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ 0.5.5
2
+ =====
3
+ * id-setter for has_many associations couldn't be used twice on the same object
4
+ * id-getter for has_many associations always returned the saved IDs, even if a new array was set
5
+
1
6
  0.5.0
2
7
  =====
3
8
  * Added has_many with deferred save, which works like habtm with deferred save
data/Rakefile CHANGED
@@ -1,26 +1,27 @@
1
- task :default do |t|
2
- options = "--colour"
3
- files = FileList['spec/**/*_spec.rb'].map{|f| f.sub(%r{^spec/},'') }
4
- exit system("cd spec && spec #{options} #{files}") ? 0 : 1
5
- end
6
-
7
- begin
8
- require 'jeweler'
9
- project_name = 'deferred_associations'
10
- Jeweler::Tasks.new do |gem|
11
- gem.name = project_name
12
- gem.summary = "Makes ActiveRecord defer/postpone habtm or has_many associations"
13
- gem.description = "Makes ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many
14
- association until you call model.save, allowing validation in the style of normal attributes. Additionally you
15
- can check inside before_save filters, if the association was altered."
16
- gem.homepage = "http://github.com/MartinKoerner/deferred_associations"
17
- gem.email = "martin.koerner@objectfab.de"
18
- gem.authors = ["Martin Koerner", "Tyler Rick", "Alessio Caiazza"]
19
- gem.add_dependency('activerecord')
20
- gem.add_development_dependency('rspec')
21
- end
22
-
23
- Jeweler::GemcutterTasks.new
24
- rescue LoadError
25
- puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
26
- end
1
+ task :default do |t|
2
+ options = "--colour"
3
+ files = FileList['spec/**/*_spec.rb'].map{|f| f.sub(%r{^spec/},'') }
4
+ exit system("cd spec && spec #{options} #{files}") ? 0 : 1
5
+ end
6
+
7
+ begin
8
+ require 'jeweler'
9
+ project_name = 'deferred_associations'
10
+ Jeweler::Tasks.new do |gem|
11
+ gem.name = project_name
12
+ gem.summary = "Makes ActiveRecord defer/postpone habtm or has_many associations"
13
+ gem.description = "Makes ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many
14
+ association until you call model.save, allowing validation in the style of normal attributes. Additionally you
15
+ can check inside before_save filters, if the association was altered."
16
+ gem.homepage = "http://github.com/MartinKoerner/deferred_associations"
17
+ gem.email = "martin.koerner@objectfab.de"
18
+ gem.authors = ["Martin Koerner", "Tyler Rick", "Alessio Caiazza"]
19
+ gem.add_dependency('activerecord')
20
+ gem.add_development_dependency('rspec')
21
+ gem.files.exclude 'gemfiles/*', '.travis.yml'
22
+ end
23
+
24
+ Jeweler::GemcutterTasks.new
25
+ rescue LoadError
26
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
27
+ end
data/Readme.markdown CHANGED
@@ -1,82 +1,85 @@
1
- Make ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many association
2
- until you call model.save, allowing validation in the style of normal attributes.
3
-
4
- [![Build Status](https://secure.travis-ci.org/MartinKoerner/deferred_associations.png?branch=master)](http://travis-ci.org/MartinKoerner/deferred_associations) [![Dependency Status](https://gemnasium.com/MartinKoerner/deferred_associations.png?travis)](https://gemnasium.com/MartinKoerner/deferred_associations)
5
-
6
- How to install
7
- ==============
8
-
9
- gem install deferred_associations
10
-
11
- Usage
12
- =====
13
-
14
- class Room < ActiveRecord::Base
15
- has_and_belongs_to_many_with_deferred_save :people
16
- has_many_with_deferred_save :tables
17
-
18
- validate :usage
19
- before_save :check_change
20
-
21
- def usage
22
- if people.size > 30
23
- errors.add :people, "There are too many people in this room"
24
- end
25
- if tables.size > 15
26
- errors.add :tables, "There are too many tables in this room"
27
- end
28
- # Neither people nor tables are saved to the database, if a validation error is added
29
- end
30
-
31
- def check_change
32
- # you can check, if there were changes to the association
33
- if people != people_without_deferred_save
34
- self.updated_at = Time.now.utc
35
- end
36
- end
37
- end
38
-
39
- Compatibility
40
- =============
41
-
42
- Tested with Rails 2.3.14, 3.2.3 on Ruby 1.8.7, 1.9.3 and JRuby 1.6.6
43
-
44
- Gotchas
45
- =======
46
-
47
- Be aware, that the habtm association objects sometimes asks the database instead of giving you the data directly from the array. So you can get something
48
- like
49
-
50
- room = Room.create
51
- room.people << Person.create
52
- room.people.first # => nil, since the DB doesn't have the association saved yet
53
-
54
-
55
- Also it is good to know, that the array you set to an association is stored there directly, so after setting a list, the typical association
56
- methods are not working:
57
-
58
- room = Room.create
59
- room.people.klass # => Person
60
- room.people = [Person.first]
61
- room.people.klass # => undefined method klass for #Array:0x007fa3b9efc2c0`
62
-
63
- Bugs
64
- ====
65
-
66
- http://github.com/MartinKoerner/deferred_associations/issues
67
-
68
- History
69
- ======
70
-
71
- Most of the code for the habtm association was written by TylerRick for his gem [has_and_belongs_to_many_with_deferred_save](https://github.com/TylerRick/has_and_belongs_to_many_with_deferred_save)
72
- Mainly, I changed two things:
73
-
74
- * added ActiveRecord 3 compatibility
75
- * removed singleton methods, because they interfere with caching
76
-
77
- License
78
- =======
79
-
80
- This plugin is licensed under the BSD license.
81
-
82
- 2012 (c) Martin Körner
1
+ Make ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many association
2
+ until you call model.save, allowing validation in the style of normal attributes.
3
+
4
+ [![Build Status](https://secure.travis-ci.org/MartinKoerner/deferred_associations.png?branch=master)](http://travis-ci.org/MartinKoerner/deferred_associations) [![Dependency Status](https://gemnasium.com/MartinKoerner/deferred_associations.png?travis)](https://gemnasium.com/MartinKoerner/deferred_associations)
5
+
6
+ How to install
7
+ ==============
8
+
9
+ gem install deferred_associations
10
+
11
+ Usage
12
+ =====
13
+
14
+ class Room < ActiveRecord::Base
15
+ has_and_belongs_to_many_with_deferred_save :people
16
+ has_many_with_deferred_save :tables
17
+
18
+ validate :usage
19
+ before_save :check_change
20
+
21
+ def usage
22
+ if people.size > 30
23
+ errors.add :people, "There are too many people in this room"
24
+ end
25
+ if tables.size > 15
26
+ errors.add :tables, "There are too many tables in this room"
27
+ end
28
+ # Neither people nor tables are saved to the database, if a validation error is added
29
+ end
30
+
31
+ def check_change
32
+ # you can check, if there were changes to the association
33
+ if people != people_without_deferred_save
34
+ self.updated_at = Time.now.utc
35
+ end
36
+ end
37
+ end
38
+
39
+ Compatibility
40
+ =============
41
+
42
+ Tested with Rails 2.3.14, 3.2.3, 3.2.14 on Ruby 1.8.7, 1.9.3 and JRuby 1.7.4
43
+
44
+ Note, that Rails 3.2.14 associations are partly broken under JRuby cause of https://github.com/rails/rails/issues/11595
45
+ You'll need to upgrade activerecord-jdbc-adapter to >= 1.3.0.beta1, if you want to use this combination.
46
+
47
+ Gotchas
48
+ =======
49
+
50
+ Be aware, that the habtm association objects sometimes asks the database instead of giving you the data directly from the array. So you can get something
51
+ like
52
+
53
+ room = Room.create
54
+ room.people << Person.create
55
+ room.people.first # => nil, since the DB doesn't have the association saved yet
56
+
57
+
58
+ Also it is good to know, that the array you set to an association is stored there directly, so after setting a list, the typical association
59
+ methods are not working:
60
+
61
+ room = Room.create
62
+ room.people.klass # => Person
63
+ room.people = [Person.first]
64
+ room.people.klass # => undefined method klass for #Array:0x007fa3b9efc2c0`
65
+
66
+ Bugs
67
+ ====
68
+
69
+ http://github.com/MartinKoerner/deferred_associations/issues
70
+
71
+ History
72
+ ======
73
+
74
+ Most of the code for the habtm association was written by TylerRick for his gem [has_and_belongs_to_many_with_deferred_save](https://github.com/TylerRick/has_and_belongs_to_many_with_deferred_save)
75
+ Mainly, I changed two things:
76
+
77
+ * added ActiveRecord 3 compatibility
78
+ * removed singleton methods, because they interfere with caching
79
+
80
+ License
81
+ =======
82
+
83
+ This plugin is licensed under the BSD license.
84
+
85
+ 2013 (c) Martin Körner
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.4
1
+ 0.5.5
@@ -1,75 +1,61 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
- # -*- encoding: utf-8 -*-
5
-
6
- Gem::Specification.new do |s|
7
- s.name = "deferred_associations"
8
- s.version = "0.5.4"
9
-
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Martin Koerner", "Tyler Rick", "Alessio Caiazza"]
12
- s.date = "2012-04-23"
13
- s.description = "Makes ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many\n association until you call model.save, allowing validation in the style of normal attributes. Additionally you\n can check inside before_save filters, if the association was altered."
14
- s.email = "martin.koerner@objectfab.de"
15
- s.files = [
16
- ".gitignore",
17
- ".travis.yml",
18
- "CHANGELOG",
19
- "Rakefile",
20
- "Readme.markdown",
21
- "VERSION",
22
- "deferred_associations.gemspec",
23
- "gemfiles/ar2.3.14.gemfile",
24
- "gemfiles/ar2.3.14.gemfile.lock",
25
- "gemfiles/ar3.2.3.gemfile",
26
- "gemfiles/ar3.2.3.gemfile.lock",
27
- "init.rb",
28
- "lib/array_to_association_wrapper.rb",
29
- "lib/deferred_associations.rb",
30
- "lib/has_and_belongs_to_many_with_deferred_save.rb",
31
- "lib/has_many_with_deferred_save.rb",
32
- "spec/.gitignore",
33
- "spec/db/database.yml",
34
- "spec/db/schema.rb",
35
- "spec/has_and_belongs_to_many_with_deferred_save_spec.rb",
36
- "spec/has_many_with_deferred_save_spec.rb",
37
- "spec/models/chair.rb",
38
- "spec/models/door.rb",
39
- "spec/models/person.rb",
40
- "spec/models/room.rb",
41
- "spec/models/table.rb",
42
- "spec/spec_helper.rb"
43
- ]
44
- s.homepage = "http://github.com/MartinKoerner/deferred_associations"
45
- s.rdoc_options = ["--charset=UTF-8"]
46
- s.require_paths = ["lib"]
47
- s.rubygems_version = "1.8.15"
48
- s.summary = "Makes ActiveRecord defer/postpone habtm or has_many associations"
49
- s.test_files = [
50
- "spec/has_and_belongs_to_many_with_deferred_save_spec.rb",
51
- "spec/has_many_with_deferred_save_spec.rb",
52
- "spec/spec_helper.rb",
53
- "spec/db/schema.rb",
54
- "spec/models/chair.rb",
55
- "spec/models/door.rb",
56
- "spec/models/person.rb",
57
- "spec/models/room.rb",
58
- "spec/models/table.rb"
59
- ]
60
-
61
- if s.respond_to? :specification_version then
62
- s.specification_version = 3
63
-
64
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
65
- s.add_runtime_dependency(%q<activerecord>, [">= 0"])
66
- s.add_development_dependency(%q<rspec>, [">= 0"])
67
- else
68
- s.add_dependency(%q<activerecord>, [">= 0"])
69
- s.add_dependency(%q<rspec>, [">= 0"])
70
- end
71
- else
72
- s.add_dependency(%q<activerecord>, [">= 0"])
73
- s.add_dependency(%q<rspec>, [">= 0"])
74
- end
75
- end
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "deferred_associations"
8
+ s.version = "0.5.5"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Martin Koerner", "Tyler Rick", "Alessio Caiazza"]
12
+ s.date = "2013-10-25"
13
+ s.description = "Makes ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many\n association until you call model.save, allowing validation in the style of normal attributes. Additionally you\n can check inside before_save filters, if the association was altered."
14
+ s.email = "martin.koerner@objectfab.de"
15
+ s.extra_rdoc_files = [
16
+ "CHANGELOG",
17
+ "Readme.markdown"
18
+ ]
19
+ s.files = [
20
+ "CHANGELOG",
21
+ "Rakefile",
22
+ "Readme.markdown",
23
+ "VERSION",
24
+ "deferred_associations.gemspec",
25
+ "init.rb",
26
+ "lib/array_to_association_wrapper.rb",
27
+ "lib/deferred_associations.rb",
28
+ "lib/has_and_belongs_to_many_with_deferred_save.rb",
29
+ "lib/has_many_with_deferred_save.rb",
30
+ "spec/db/database.yml",
31
+ "spec/db/schema.rb",
32
+ "spec/has_and_belongs_to_many_with_deferred_save_spec.rb",
33
+ "spec/has_many_with_deferred_save_spec.rb",
34
+ "spec/models/chair.rb",
35
+ "spec/models/door.rb",
36
+ "spec/models/person.rb",
37
+ "spec/models/room.rb",
38
+ "spec/models/table.rb",
39
+ "spec/spec_helper.rb"
40
+ ]
41
+ s.homepage = "http://github.com/MartinKoerner/deferred_associations"
42
+ s.require_paths = ["lib"]
43
+ s.rubygems_version = "1.8.24"
44
+ s.summary = "Makes ActiveRecord defer/postpone habtm or has_many associations"
45
+
46
+ if s.respond_to? :specification_version then
47
+ s.specification_version = 3
48
+
49
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
50
+ s.add_runtime_dependency(%q<activerecord>, [">= 0"])
51
+ s.add_development_dependency(%q<rspec>, [">= 0"])
52
+ else
53
+ s.add_dependency(%q<activerecord>, [">= 0"])
54
+ s.add_dependency(%q<rspec>, [">= 0"])
55
+ end
56
+ else
57
+ s.add_dependency(%q<activerecord>, [">= 0"])
58
+ s.add_dependency(%q<rspec>, [">= 0"])
59
+ end
60
+ end
61
+
data/init.rb CHANGED
@@ -1 +1 @@
1
- require 'deferred_associations'
1
+ require 'deferred_associations'
File without changes
File without changes
@@ -1,142 +1,142 @@
1
- module ActiveRecord
2
- module Associations
3
- module ClassMethods
4
-
5
- # Instructions:
6
- #
7
- # Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save.
8
- #
9
- # Then add a validation method that adds an error if there is something wrong with the (unsaved) collection. This will prevent it from being saved if there are any errors.
10
- #
11
- # Example:
12
- #
13
- # def validate
14
- # if people.size > maximum_occupancy
15
- # errors.add :people, "There are too many people in this room"
16
- # end
17
- # end
18
- def has_and_belongs_to_many_with_deferred_save(*args)
19
- has_and_belongs_to_many *args
20
- collection_name = args[0].to_s
21
- collection_singular_ids = collection_name.singularize + "_ids"
22
-
23
- add_deletion_callback
24
-
25
- attr_accessor :"unsaved_#{collection_name}"
26
- attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}"
27
-
28
- define_method "#{collection_name}_with_deferred_save=" do |collection|
29
- #puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}"
30
- self.send "unsaved_#{collection_name}=", collection
31
- end
32
-
33
- define_method "#{collection_name}_with_deferred_save" do |*args|
34
- if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
35
- self.send("#{collection_name}_without_deferred_save")
36
- else
37
- if self.send("unsaved_#{collection_name}").nil?
38
- send("initialize_unsaved_#{collection_name}", *args)
39
- end
40
- self.send("unsaved_#{collection_name}")
41
- end
42
- end
43
-
44
- alias_method_chain :"#{collection_name}=", 'deferred_save'
45
- alias_method_chain :"#{collection_name}", 'deferred_save'
46
-
47
- define_method "#{collection_singular_ids}_with_deferred_save" do |*args|
48
- if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
49
- self.send("#{collection_singular_ids}_without_deferred_save")
50
- else
51
- if self.send("unsaved_#{collection_name}").nil?
52
- send("initialize_unsaved_#{collection_name}", *args)
53
- end
54
- self.send("unsaved_#{collection_name}").map { |e| e[:id] }
55
- end
56
- end
57
-
58
- alias_method_chain :"#{collection_singular_ids}", 'deferred_save'
59
-
60
- # only needed for ActiveRecord >= 3.0
61
- if ActiveRecord::VERSION::STRING >= "3"
62
- define_method "#{collection_singular_ids}_with_deferred_save=" do |ids|
63
- ids = Array.wrap(ids).reject { |id| id.blank? }
64
- reflection_wrapper = self.send("#{collection_name}_without_deferred_save")
65
- new_values = reflection_wrapper.klass.find(ids)
66
- self.send("#{collection_name}=", new_values)
67
- end
68
- alias_method_chain :"#{collection_singular_ids}=", 'deferred_save'
69
- end
70
-
71
- define_method "do_#{collection_name}_save!" do
72
- # Question: Why do we need this @use_original_collection_reader_behavior stuff?
73
- # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only
74
- # records that have changed.
75
- # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not
76
- # knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database*
77
- # (the original behavior), so we have to provide that behavior... If we didn't provide it, it would end up trying to take the diff of
78
- # two identical collections so nothing would ever get saved.
79
- # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use
80
- # @use_original_collection_reader_behavior as a switch.
81
-
82
- self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true
83
- if self.send("unsaved_#{collection_name}").nil?
84
- send("initialize_unsaved_#{collection_name}")
85
- end
86
- self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}")
87
- # /\ This is where the actual save occurs.
88
- self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false
89
-
90
- true
91
- end
92
- after_save "do_#{collection_name}_save!"
93
-
94
-
95
- define_method "reload_with_deferred_save_for_#{collection_name}" do
96
- # Reload from the *database*, discarding any unsaved changes.
97
- self.send("reload_without_deferred_save_for_#{collection_name}").tap do
98
- self.send "unsaved_#{collection_name}=", nil
99
- # /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of
100
- # unsaved_collection that it had before the reload.
101
- end
102
- end
103
- alias_method_chain :"reload", "deferred_save_for_#{collection_name}"
104
-
105
-
106
- define_method "initialize_unsaved_#{collection_name}" do |*args|
107
- #puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}"
108
- elements = self.send("#{collection_name}_without_deferred_save", *args).clone
109
- elements = ArrayToAssociationWrapper.new(elements)
110
- elements.defer_association_methods_to self, collection_name
111
- self.send "unsaved_#{collection_name}=", elements
112
- # /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the
113
- # database, in which case we want unsaved_collection to start out with the "saved collection".
114
- # Actually, this doesn't clone the Association but the elements array instead (since the clone method is
115
- # proxied like any other methods)
116
- # Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection
117
- # will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things
118
- # immediately, which is exactly what we're trying to avoid.)
119
-
120
-
121
-
122
- end
123
- private :"initialize_unsaved_#{collection_name}"
124
-
125
- end
126
-
127
- def add_deletion_callback
128
- # this will delete all the association into the join table after obj.destroy,
129
- # but is only useful/necessary, if the record is not paranoid?
130
- unless (self.respond_to?(:paranoid?) && self.paranoid?)
131
- after_destroy { |record|
132
- begin
133
- record.save
134
- rescue Exception => e
135
- logger.warn "Association cleanup after destroy failed with #{e}"
136
- end
137
- }
138
- end
139
- end
140
- end
141
- end
142
- end
1
+ module ActiveRecord
2
+ module Associations
3
+ module ClassMethods
4
+
5
+ # Instructions:
6
+ #
7
+ # Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save.
8
+ #
9
+ # Then add a validation method that adds an error if there is something wrong with the (unsaved) collection. This will prevent it from being saved if there are any errors.
10
+ #
11
+ # Example:
12
+ #
13
+ # def validate
14
+ # if people.size > maximum_occupancy
15
+ # errors.add :people, "There are too many people in this room"
16
+ # end
17
+ # end
18
+ def has_and_belongs_to_many_with_deferred_save(*args)
19
+ has_and_belongs_to_many *args
20
+ collection_name = args[0].to_s
21
+ collection_singular_ids = collection_name.singularize + "_ids"
22
+
23
+ add_deletion_callback
24
+
25
+ attr_accessor :"unsaved_#{collection_name}"
26
+ attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}"
27
+
28
+ define_method "#{collection_name}_with_deferred_save=" do |collection|
29
+ #puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}"
30
+ self.send "unsaved_#{collection_name}=", collection
31
+ end
32
+
33
+ define_method "#{collection_name}_with_deferred_save" do |*args|
34
+ if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
35
+ self.send("#{collection_name}_without_deferred_save")
36
+ else
37
+ if self.send("unsaved_#{collection_name}").nil?
38
+ send("initialize_unsaved_#{collection_name}", *args)
39
+ end
40
+ self.send("unsaved_#{collection_name}")
41
+ end
42
+ end
43
+
44
+ alias_method_chain :"#{collection_name}=", 'deferred_save'
45
+ alias_method_chain :"#{collection_name}", 'deferred_save'
46
+
47
+ define_method "#{collection_singular_ids}_with_deferred_save" do |*args|
48
+ if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
49
+ self.send("#{collection_singular_ids}_without_deferred_save")
50
+ else
51
+ if self.send("unsaved_#{collection_name}").nil?
52
+ send("initialize_unsaved_#{collection_name}", *args)
53
+ end
54
+ self.send("unsaved_#{collection_name}").map { |e| e[:id] }
55
+ end
56
+ end
57
+
58
+ alias_method_chain :"#{collection_singular_ids}", 'deferred_save'
59
+
60
+ # only needed for ActiveRecord >= 3.0
61
+ if ActiveRecord::VERSION::STRING >= "3"
62
+ define_method "#{collection_singular_ids}_with_deferred_save=" do |ids|
63
+ ids = Array.wrap(ids).reject { |id| id.blank? }
64
+ reflection_wrapper = self.send("#{collection_name}_without_deferred_save")
65
+ new_values = reflection_wrapper.klass.find(ids)
66
+ self.send("#{collection_name}=", new_values)
67
+ end
68
+ alias_method_chain :"#{collection_singular_ids}=", 'deferred_save'
69
+ end
70
+
71
+ define_method "do_#{collection_name}_save!" do
72
+ # Question: Why do we need this @use_original_collection_reader_behavior stuff?
73
+ # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only
74
+ # records that have changed.
75
+ # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not
76
+ # knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database*
77
+ # (the original behavior), so we have to provide that behavior... If we didn't provide it, it would end up trying to take the diff of
78
+ # two identical collections so nothing would ever get saved.
79
+ # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use
80
+ # @use_original_collection_reader_behavior as a switch.
81
+
82
+ self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true
83
+ if self.send("unsaved_#{collection_name}").nil?
84
+ send("initialize_unsaved_#{collection_name}")
85
+ end
86
+ self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}")
87
+ # /\ This is where the actual save occurs.
88
+ self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false
89
+
90
+ true
91
+ end
92
+ after_save "do_#{collection_name}_save!"
93
+
94
+
95
+ define_method "reload_with_deferred_save_for_#{collection_name}" do
96
+ # Reload from the *database*, discarding any unsaved changes.
97
+ self.send("reload_without_deferred_save_for_#{collection_name}").tap do
98
+ self.send "unsaved_#{collection_name}=", nil
99
+ # /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of
100
+ # unsaved_collection that it had before the reload.
101
+ end
102
+ end
103
+ alias_method_chain :"reload", "deferred_save_for_#{collection_name}"
104
+
105
+
106
+ define_method "initialize_unsaved_#{collection_name}" do |*args|
107
+ #puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}"
108
+ elements = self.send("#{collection_name}_without_deferred_save", *args).clone
109
+ elements = ArrayToAssociationWrapper.new(elements)
110
+ elements.defer_association_methods_to self, collection_name
111
+ self.send "unsaved_#{collection_name}=", elements
112
+ # /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the
113
+ # database, in which case we want unsaved_collection to start out with the "saved collection".
114
+ # Actually, this doesn't clone the Association but the elements array instead (since the clone method is
115
+ # proxied like any other methods)
116
+ # Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection
117
+ # will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things
118
+ # immediately, which is exactly what we're trying to avoid.)
119
+
120
+
121
+
122
+ end
123
+ private :"initialize_unsaved_#{collection_name}"
124
+
125
+ end
126
+
127
+ def add_deletion_callback
128
+ # this will delete all the association into the join table after obj.destroy,
129
+ # but is only useful/necessary, if the record is not paranoid?
130
+ unless (self.respond_to?(:paranoid?) && self.paranoid?)
131
+ after_destroy { |record|
132
+ begin
133
+ record.save
134
+ rescue Exception => e
135
+ logger.warn "Association cleanup after destroy failed with #{e}"
136
+ end
137
+ }
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end