deferred_associations 0.5.4 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
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