deferred_associations 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG ADDED
@@ -0,0 +1,21 @@
1
+ 0.5.0
2
+ =====
3
+ * Added has_many with deferred save, which works like habtm with deferred save
4
+ * Added id setters for AR >= 3.0 compatibility
5
+ * HABTMs are changed in an after_save instead of before_save, dropping the need of
6
+ special before_save call sequences
7
+
8
+ 0.4.0
9
+ =====
10
+ * Added Rails 3.2.2 compatibility
11
+ * used "before_save :callback" instead of redefining "before_save"
12
+
13
+ 0.3.0
14
+ =====
15
+ * method "last" proxies to collections "last" instead of "first"
16
+ * removed singleton methods into a wrapper array
17
+ * renamed to "deferred_associations"
18
+
19
+ 0.2.0
20
+ =====
21
+ * forked from TylerRick
data/Rakefile ADDED
@@ -0,0 +1,26 @@
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 Körner", "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
data/Readme.markdown ADDED
@@ -0,0 +1,70 @@
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
+ How to install
5
+ ==============
6
+
7
+ gem install deferred_associations
8
+
9
+ Usage
10
+ =====
11
+
12
+ class Room < ActiveRecord::Base
13
+ has_and_belongs_to_many_with_deferred_save :people
14
+ has_many_with_deferred_save :tables
15
+
16
+ validate :usage
17
+ before_save :check_change
18
+
19
+ def usage
20
+ if people.size > 30
21
+ errors.add :people, "There are too many people in this room"
22
+ end
23
+ if tables.size > 15
24
+ errors.add :tables, "There are too many tables in this room"
25
+ end
26
+ # Neither people nor tables are saved to the database, if a validation error is added
27
+ end
28
+
29
+ def check_usage
30
+ # you can check, if there were changes to the association
31
+ if people != people_without_deferred_save
32
+ self.updated_at = Time.now.utc
33
+ end
34
+ end
35
+ end
36
+
37
+ Compatibility
38
+ =============
39
+
40
+ Tested with Rails 2.3.14, 3.2.2
41
+
42
+ Gotchas
43
+ =======
44
+
45
+ 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
46
+ like
47
+
48
+ room = Room.new
49
+ room.people << Person.create
50
+ room.people.first # => nil, since the DB doesn't have the association saved yet
51
+
52
+ Bugs
53
+ ====
54
+
55
+ http://github.com/MartinKoerner/deferred_associations/issues
56
+
57
+ History
58
+ ======
59
+
60
+ Most of the code for the habtm association was written by [TylerRick] for his gem [has_and_belongs_to_many_with_deferred](https://github.com/TylerRick/has_and_belongs_to_many_with_deferred)
61
+ Mainly, I changed two things:
62
+ * added ActiveRecord 3 compatibility
63
+ * removed singleton methods, because they interfere with caching
64
+
65
+ License
66
+ =======
67
+
68
+ This plugin is licensed under the BSD license.
69
+
70
+ 2012 (c) Martin Körner
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.0
@@ -0,0 +1,63 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{deferred_associations}
8
+ s.version = "0.4.0"
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 = %q{2012-03-18}
13
+ s.files = [
14
+ ".gitignore",
15
+ "Rakefile",
16
+ "Readme.markdown",
17
+ "VERSION",
18
+ "has_and_belongs_to_many_with_deferred_save.gemspec",
19
+ "init.rb",
20
+ "install.rb",
21
+ "lib/has_and_belongs_to_many_with_deferred_save.rb",
22
+ "lib/array_to_association_wrapper.rb",
23
+ "spec/.gitignore",
24
+ "spec/db/database.yml",
25
+ "spec/db/schema.rb",
26
+ "spec/has_and_belongs_to_many_with_deferred_save_spec.rb",
27
+ "spec/models/door.rb",
28
+ "spec/models/person.rb",
29
+ "spec/models/room.rb",
30
+ "spec/spec_helper.rb",
31
+ "uninstall.rb"
32
+ ]
33
+ s.homepage = %q{http://github.com/neogrande/deferred_associations}
34
+ s.rdoc_options = ["--charset=UTF-8"]
35
+ s.require_paths = ["lib"]
36
+ s.rubygems_version = %q{1.3.5}
37
+ s.summary = %q{Make ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many association until you call model.save, allowing validation in the style of normal attributes.}
38
+ s.test_files = [
39
+ "spec/models/door.rb",
40
+ "spec/models/room.rb",
41
+ "spec/models/person.rb",
42
+ "spec/has_and_belongs_to_many_with_deferred_save_spec.rb",
43
+ "spec/spec_helper.rb",
44
+ "spec/db/schema.rb"
45
+ ]
46
+
47
+ if s.respond_to? :specification_version then
48
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
49
+ s.specification_version = 3
50
+
51
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
52
+ s.add_runtime_dependency(%q<activerecord>, [">= 0"])
53
+ s.add_development_dependency(%q<rspec>, [">= 0"])
54
+ else
55
+ s.add_dependency(%q<activerecord>, [">= 0"])
56
+ s.add_dependency(%q<rspec>, [">= 0"])
57
+ end
58
+ else
59
+ s.add_dependency(%q<activerecord>, [">= 0"])
60
+ s.add_dependency(%q<rspec>, [">= 0"])
61
+ end
62
+ end
63
+
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), 'lib/array_to_association_wrapper')
2
+ require File.join(File.dirname(__FILE__), 'lib/has_and_belongs_to_many_with_deferred_save')
3
+ require File.join(File.dirname(__FILE__), 'lib/has_many_with_deferred_save')
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,67 @@
1
+ class ArrayToAssociationWrapper < Array
2
+
3
+ def defer_association_methods_to owner, association_name
4
+ @association_owner = owner
5
+ @association_name = association_name
6
+ end
7
+
8
+ # trick collection_name.include?(obj)
9
+ # If you use a collection of SingleTableInheritance and didn't :select 'type' the
10
+ # include? method will not find any subclassed object.
11
+ def include_with_deferred_save?(obj)
12
+ if @association_owner.present?
13
+ if self.detect { |itm| itm == obj || (itm[:id] == obj[:id] && obj.is_a?(itm.class)) }
14
+ return true
15
+ else
16
+ return false
17
+ end
18
+ else
19
+ include_without_deferred_save?(obj)
20
+ end
21
+ end
22
+
23
+ alias_method_chain :include?, 'deferred_save'
24
+
25
+ def find_with_deferred_save *args
26
+ if @association_owner.present?
27
+ collection_without_deferred_save.send(:find, *args)
28
+ else
29
+ find_without_deferred_save
30
+ end
31
+ end
32
+
33
+ alias_method_chain :find, :deferred_save
34
+
35
+ def first_with_deferred_save *args
36
+ if @association_owner.present?
37
+ collection_without_deferred_save.send(:first, *args)
38
+ else
39
+ first_without_deferred_save
40
+ end
41
+ end
42
+
43
+ alias_method_chain :first, :deferred_save
44
+
45
+ def last_with_deferred_save *args
46
+ if @association_owner.present?
47
+ collection_without_deferred_save.send(:last, *args)
48
+ else
49
+ last_without_deferred_save
50
+ end
51
+ end
52
+
53
+ alias_method_chain :last, :deferred_save
54
+
55
+ define_method :method_missing do |method, *args|
56
+ #puts "#{self.class}.method_missing(#{method}) (#{collection_without_deferred_save.inspect})"
57
+ if @association_owner.present?
58
+ collection_without_deferred_save.send(method, *args) unless method == :set_inverse_instance
59
+ else
60
+ super
61
+ end
62
+ end
63
+
64
+ def collection_without_deferred_save
65
+ @association_owner.send("#{@association_name}_without_deferred_save")
66
+ end
67
+ end
@@ -0,0 +1,144 @@
1
+ # To do: make it work to call this twice in a class. Currently that probably wouldn't work, because it would try to alias methods to existing names...
2
+ # Note: before_save must be defined *before* including this module, not after.
3
+
4
+ module ActiveRecord
5
+ module Associations
6
+ module ClassMethods
7
+
8
+ # Instructions:
9
+ #
10
+ # Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save.
11
+ #
12
+ # 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.
13
+ #
14
+ # Example:
15
+ #
16
+ # def validate
17
+ # if people.size > maximum_occupancy
18
+ # errors.add :people, "There are too many people in this room"
19
+ # end
20
+ # end
21
+ def has_and_belongs_to_many_with_deferred_save(*args)
22
+ has_and_belongs_to_many *args
23
+ collection_name = args[0].to_s
24
+ collection_singular_ids = collection_name.singularize + "_ids"
25
+
26
+ add_deletion_callback
27
+
28
+ attr_accessor :"unsaved_#{collection_name}"
29
+ attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}"
30
+
31
+ define_method "#{collection_name}_with_deferred_save=" do |collection|
32
+ #puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}"
33
+ self.send "unsaved_#{collection_name}=", collection
34
+ end
35
+
36
+ define_method "#{collection_name}_with_deferred_save" do |*args|
37
+ if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
38
+ self.send("#{collection_name}_without_deferred_save")
39
+ else
40
+ if self.send("unsaved_#{collection_name}").nil?
41
+ send("initialize_unsaved_#{collection_name}", *args)
42
+ end
43
+ self.send("unsaved_#{collection_name}")
44
+ end
45
+ end
46
+
47
+ alias_method_chain :"#{collection_name}=", 'deferred_save'
48
+ alias_method_chain :"#{collection_name}", 'deferred_save'
49
+
50
+ define_method "#{collection_singular_ids}_with_deferred_save" do |*args|
51
+ if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
52
+ self.send("#{collection_singular_ids}_without_deferred_save")
53
+ else
54
+ if self.send("unsaved_#{collection_name}").nil?
55
+ send("initialize_unsaved_#{collection_name}", *args)
56
+ end
57
+ self.send("unsaved_#{collection_name}").map { |e| e[:id] }
58
+ end
59
+ end
60
+
61
+ alias_method_chain :"#{collection_singular_ids}", 'deferred_save'
62
+
63
+ # only needed for ActiveRecord >= 3.0
64
+ if ActiveRecord::VERSION::STRING >= "3"
65
+ define_method "#{collection_singular_ids}_with_deferred_save=" do |ids|
66
+ ids = Array.wrap(ids).reject { |id| id.blank? }
67
+ new_values = self.send("#{collection_name}").klass.find(ids)
68
+ self.send("#{collection_name}=", new_values)
69
+ end
70
+ alias_method_chain :"#{collection_singular_ids}=", 'deferred_save'
71
+ end
72
+
73
+ define_method "do_#{collection_name}_save!" do
74
+ # Question: Why do we need this @use_original_collection_reader_behavior stuff?
75
+ # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only
76
+ # records that have changed.
77
+ # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not
78
+ # knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database*
79
+ # (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
80
+ # two identical collections so nothing would ever get saved.
81
+ # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use
82
+ # @use_original_collection_reader_behavior as a switch.
83
+
84
+ self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true
85
+ if self.send("unsaved_#{collection_name}").nil?
86
+ send("initialize_unsaved_#{collection_name}")
87
+ end
88
+ self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}")
89
+ # /\ This is where the actual save occurs.
90
+ self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false
91
+
92
+ true
93
+ end
94
+ after_save "do_#{collection_name}_save!"
95
+
96
+
97
+ define_method "reload_with_deferred_save_for_#{collection_name}" do
98
+ # Reload from the *database*, discarding any unsaved changes.
99
+ self.send("reload_without_deferred_save_for_#{collection_name}").tap do
100
+ self.send "unsaved_#{collection_name}=", nil
101
+ # /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of
102
+ # unsaved_collection that it had before the reload.
103
+ end
104
+ end
105
+ alias_method_chain :"reload", "deferred_save_for_#{collection_name}"
106
+
107
+
108
+ define_method "initialize_unsaved_#{collection_name}" do |*args|
109
+ #puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}"
110
+ elements = self.send("#{collection_name}_without_deferred_save", *args).clone
111
+ elements = ArrayToAssociationWrapper.new(elements)
112
+ elements.defer_association_methods_to self, collection_name
113
+ self.send "unsaved_#{collection_name}=", elements
114
+ # /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the
115
+ # database, in which case we want unsaved_collection to start out with the "saved collection".
116
+ # Actually, this doesn't clone the Association but the elements array instead (since the clone method is
117
+ # proxied like any other methods)
118
+ # Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection
119
+ # will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things
120
+ # immediately, which is exactly what we're trying to avoid.)
121
+
122
+
123
+
124
+ end
125
+ private :"initialize_unsaved_#{collection_name}"
126
+
127
+ end
128
+
129
+ def add_deletion_callback
130
+ # this will delete all the association into the join table after obj.destroy,
131
+ # but is only useful/necessary, if the record is not paranoid?
132
+ unless (self.respond_to?(:paranoid?) && self.paranoid?)
133
+ after_destroy { |record|
134
+ begin
135
+ record.save
136
+ rescue Exception => e
137
+ logger.warn "Association cleanup after destroy failed with #{e}"
138
+ end
139
+ }
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,102 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ module ClassMethods
4
+
5
+ def has_many_with_deferred_save *args
6
+ has_many *args
7
+
8
+ collection_name = args[0].to_s
9
+
10
+ if args[1].is_a?(Hash) && args[1].keys.include?(:through)
11
+ logger.warn "You are using the option :through on #{self.name}##{collection_name}. This was not tested very much with has_many_with_deferred_save. Please write many tests for your functionality!"
12
+ end
13
+
14
+ after_save "hmwds_update_#{collection_name}"
15
+
16
+ define_obj_setter collection_name
17
+ define_obj_getter collection_name
18
+ define_id_setter collection_name
19
+
20
+ define_update_method collection_name
21
+ define_reload_method collection_name
22
+ end
23
+
24
+
25
+ def define_obj_setter collection_name
26
+
27
+ define_method("#{collection_name}_with_deferred_save=") do |objs|
28
+ instance_variable_set "@hmwds_temp_#{collection_name}", objs || []
29
+ end
30
+
31
+ method_name = "#{collection_name}="
32
+ alias_method_chain method_name, :deferred_save
33
+ end
34
+
35
+ def define_obj_getter collection_name
36
+
37
+ define_method("#{collection_name}_with_deferred_save") do
38
+ save_in_progress = instance_variable_get "@hmwds_#{collection_name}_save_in_progress"
39
+
40
+ # while updating the association, rails loads the association object - this needs to be the original one
41
+ unless save_in_progress
42
+ elements = instance_variable_get "@hmwds_temp_#{collection_name}"
43
+ if elements.nil?
44
+ elements = ArrayToAssociationWrapper.new(self.send("#{collection_name}_without_deferred_save"))
45
+ elements.defer_association_methods_to self, collection_name
46
+ instance_variable_set "@hmwds_temp_#{collection_name}", elements
47
+ end
48
+
49
+ result = elements
50
+ else
51
+ result = self.send("#{collection_name}_without_deferred_save")
52
+ end
53
+
54
+ result
55
+ end
56
+
57
+ alias_method_chain collection_name, :deferred_save
58
+ end
59
+
60
+ def define_id_setter collection_name
61
+ # only needed for ActiveRecord >= 3.0
62
+ if ActiveRecord::VERSION::STRING >= "3"
63
+ collection_singular_ids = "#{collection_name.singularize}_ids"
64
+ define_method "#{collection_singular_ids}_with_deferred_save=" do |ids|
65
+ ids = Array.wrap(ids).reject { |id| id.blank? }
66
+ new_values = self.send("#{collection_name}").klass.find(ids)
67
+ self.send("#{collection_name}=", new_values)
68
+ end
69
+ alias_method_chain :"#{collection_singular_ids}=", 'deferred_save'
70
+ end
71
+ end
72
+
73
+ def define_update_method collection_name
74
+
75
+ define_method "hmwds_update_#{collection_name}" do
76
+
77
+ unless frozen?
78
+ elements = instance_variable_get "@hmwds_temp_#{collection_name}"
79
+ unless elements.nil? # nothing has been done with the association
80
+ # save is done automatically, if original behaviour is restored
81
+ instance_variable_set "@hmwds_#{collection_name}_save_in_progress", true
82
+ self.send("#{collection_name}_without_deferred_save=", elements)
83
+ instance_variable_set "@hmwds_#{collection_name}_save_in_progress", false
84
+
85
+ instance_variable_set "@hmwds_temp_#{collection_name}", nil
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def define_reload_method collection_name
92
+ define_method "reload_with_deferred_save_for_#{collection_name}" do
93
+ # Reload from the *database*, discarding any unsaved changes.
94
+ self.send("reload_without_deferred_save_for_#{collection_name}").tap do
95
+ instance_variable_set "@hmwds_temp_#{collection_name}", nil
96
+ end
97
+ end
98
+ alias_method_chain :"reload", "deferred_save_for_#{collection_name}"
99
+ end
100
+ end
101
+ end
102
+ end
data/spec/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.log
2
+ *.db
@@ -0,0 +1,21 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: test.sqlite3.db
4
+
5
+ sqlite3mem:
6
+ adapter: sqlite3
7
+ database: ":memory:"
8
+
9
+ postgresql:
10
+ adapter: postgresql
11
+ username: postgres
12
+ password: postgres
13
+ database: has_and_belongs_to_many_with_deferred_save_test
14
+ min_messages: ERROR
15
+
16
+ mysql:
17
+ adapter: mysql
18
+ host: localhost
19
+ username: root
20
+ password:
21
+ database: has_and_belongs_to_many_with_deferred_save_test
data/spec/db/schema.rb ADDED
@@ -0,0 +1,40 @@
1
+ # This file is autogenerated. Instead of editing this file, please use the
2
+ # migrations feature of ActiveRecord to incrementally modify your database, and
3
+ # then regenerate this schema definition.
4
+
5
+ ActiveRecord::Schema.define(:version => 1) do
6
+
7
+ create_table "people", :force => true do |t|
8
+ t.column "name", :string
9
+ end
10
+
11
+ create_table "people_rooms", :id => false, :force => true do |t|
12
+ t.column "person_id", :integer
13
+ t.column "room_id", :integer
14
+ end
15
+
16
+ create_table "rooms", :force => true do |t|
17
+ t.column "name", :string
18
+ t.column "maximum_occupancy", :integer
19
+ end
20
+
21
+ create_table "doors_rooms", :id => false, :force => true do |t|
22
+ t.column "door_id", :integer
23
+ t.column "room_id", :integer
24
+ end
25
+
26
+ create_table "doors", :force => true do |t|
27
+ t.column "name", :string
28
+ end
29
+
30
+ create_table "tables", :force => true do |t|
31
+ t.column "name", :string
32
+ t.column "room_id", :integer
33
+ end
34
+
35
+ create_table "chairs", :force => true do |t|
36
+ t.column "name", :string
37
+ t.column "table_id", :integer
38
+ end
39
+
40
+ end
@@ -0,0 +1,214 @@
1
+ require "spec_helper"
2
+ require 'has_and_belongs_to_many_with_deferred_save'
3
+
4
+ describe "has_and_belongs_to_many_with_deferred_save" do
5
+ describe "room maximum_occupancy" do
6
+ before :all do
7
+ @people = []
8
+ @people << Person.create(:name => 'Filbert')
9
+ @people << Person.create(:name => 'Miguel')
10
+ @people << Person.create(:name => 'Rainer')
11
+ @room = Room.new(:maximum_occupancy => 2)
12
+ end
13
+ after :all do
14
+ Person.delete_all
15
+ Room.delete_all
16
+ end
17
+
18
+ it "passes initial checks" do
19
+ Room .count.should == 0
20
+ Person.count.should == 3
21
+
22
+ @room.people.should == []
23
+ @room.people_without_deferred_save.should == []
24
+ @room.people_without_deferred_save.object_id.should_not ==
25
+ @room.unsaved_people.object_id
26
+ end
27
+
28
+ it "after adding people to room, it should not have saved anything to the database" do
29
+ @room.people << @people[0]
30
+ @room.people << @people[1]
31
+
32
+ # Still not saved to the association table!
33
+ Room.count_by_sql("select count(*) from people_rooms").should == 0
34
+ @room.people_without_deferred_save.size. should == 0
35
+ end
36
+
37
+ it "but room.people.size should still report the current size of 2" do
38
+ @room.people.size.should == 2 # 2 because this looks at unsaved_people and not at the database
39
+ end
40
+
41
+ it "after saving the model, the association should be saved in the join table" do
42
+ @room.save # Only here is it actually saved to the association table!
43
+ @room.errors.full_messages.should == []
44
+ Room.count_by_sql("select count(*) from people_rooms").should == 2
45
+ @room.people.size. should == 2
46
+ @room.people_without_deferred_save.size. should == 2
47
+ end
48
+
49
+ it "when we try to add a 3rd person, it should add a validation error to the errors object like any other validation error" do
50
+ lambda { @room.people << @people[2] }.should_not raise_error
51
+ @room.people.size. should == 3
52
+
53
+ Room.count_by_sql("select count(*) from people_rooms").should == 2
54
+ @room.valid?
55
+ @room.get_error(:people).should == "This room has reached its maximum occupancy"
56
+ @room.people.size. should == 3 # Just like with normal attributes that fail validation... the attribute still contains the invalid data but we refuse to save until it is changed to something that is *valid*.
57
+ end
58
+
59
+ it "when we try to save, it should fail, because room.people is still invalid" do
60
+ @room.save.should == false
61
+ Room.count_by_sql("select count(*) from people_rooms").should == 2 # It's still not there, because it didn't pass the validation.
62
+ @room.get_error(:people).should == "This room has reached its maximum occupancy"
63
+ @room.people.size. should == 3
64
+ @people.map {|p| p.reload; p.rooms.size}.should == [1, 1, 0]
65
+ end
66
+
67
+ it "when we reload, it should go back to only having 2 people in the room" do
68
+ @room.reload
69
+ @room.people.size. should == 2
70
+ @room.people_without_deferred_save.size. should == 2
71
+ @people.map {|p| p.reload; p.rooms.size}. should == [1, 1, 0]
72
+ end
73
+
74
+ it "if they try to go around our accessors and use the original accessors, then (and only then) will the exception be raised in before_adding_person..." do
75
+ lambda do
76
+ @room.people_without_deferred_save << @people[2]
77
+ end.should raise_error(RuntimeError)
78
+ end
79
+
80
+ it "lets you bypass the validation on Room if we add the association from the other side (person.rooms <<)?" do
81
+ @people[2].rooms << @room
82
+ @people[2].rooms.size.should == 1
83
+
84
+ # Adding it from one direction does not add it to the other object's association (@room.people), so the validation passes.
85
+ @room.reload.people.size.should == 2
86
+ @people[2].valid?
87
+ @people[2].errors.full_messages.should == []
88
+ @people[2].save.should == true
89
+
90
+ # It is only after reloading that @room.people has this 3rd object, causing it to be invalid, and by then it's too late to do anything about it.
91
+ @room.reload.people.size.should == 3
92
+ @room.valid?.should == false
93
+ end
94
+
95
+ it "only if you add the validation to both sides, can you ensure that the size of the association does not exceed some limit" do
96
+ @room.reload.people.size.should == 3
97
+ @room.people.delete(@people[2])
98
+ @room.save.should == true
99
+ @room.reload.people.size.should == 2
100
+ @people[2].reload.rooms.size.should == 0
101
+
102
+ obj = @people[2]
103
+ def obj.extra_validation
104
+ rooms.each do |room|
105
+ this_room_unsaved = rooms_without_deferred_save.include?(room) ? 0 : 1
106
+ if room.people.size + this_room_unsaved > room.maximum_occupancy
107
+ errors.add :rooms, "This room has reached its maximum occupancy"
108
+ end
109
+ end
110
+ end
111
+ obj.class.send(:validate, :extra_validation)
112
+
113
+ @people[2].rooms << @room
114
+ @people[2].rooms.size.should == 1
115
+
116
+ @room.reload.people.size.should == 2
117
+ @people[2].valid?.should be_false
118
+ @people[2].get_error(:rooms).should == "This room has reached its maximum occupancy"
119
+ @room.reload.people.size.should == 2
120
+ end
121
+
122
+ it "still lets you do find" do
123
+ @room.people2. find(:first, :conditions => {:name => 'Filbert'}).should == @people[0]
124
+ @room.people_without_deferred_save.find(:first, :conditions => {:name => 'Filbert'}).should == @people[0]
125
+ @room.people2.first(:conditions => {:name => 'Filbert'}).should == @people[0]
126
+ @room.people_without_deferred_save.first(:conditions => {:name => 'Filbert'}).should == @people[0]
127
+ @room.people_without_deferred_save.find_by_name('Filbert').should == @people[0]
128
+
129
+ @room.people.find(:first, :conditions => {:name => 'Filbert'}).should == @people[0]
130
+ @room.people.first(:conditions => {:name => 'Filbert'}). should == @people[0]
131
+ @room.people.last(:conditions => {:name => 'Filbert'}). should == @people[0]
132
+ @room.people.first. should == @people[0]
133
+ @room.people.last. should == @people[1] # @people[2] was removed before
134
+ @room.people.find_by_name('Filbert'). should == @people[0]
135
+ end
136
+
137
+ it "should be dumpable with Marshal" do
138
+ lambda { Marshal.dump(@room.people) }.should_not raise_exception
139
+ lambda { Marshal.dump(Room.new.people) }.should_not raise_exception
140
+ end
141
+
142
+ it "should detect difference in association" do
143
+ @room = Room.find(@room.id)
144
+ @room.bs_diff_before_module.should be_nil
145
+ @room.bs_diff_after_module.should be_nil
146
+ @room.bs_diff_method.should be_nil
147
+
148
+ @room.people.size.should == 2
149
+ @room.people = [@room.people[0]]
150
+ @room.save.should be_true
151
+
152
+ @room.bs_diff_before_module.should be_true
153
+ @room.bs_diff_after_module.should be_true
154
+ if ActiveRecord::VERSION::STRING >= "3"
155
+ @room.bs_diff_method.should be_nil # Rails 3.2: nil (before_save filter is not supported)
156
+ else
157
+ @room.bs_diff_method.should be_true
158
+ end
159
+ end
160
+
161
+ it "should act like original habtm when using ID array with array manipulation" do
162
+ @room = Room.find(@room.id)
163
+ @room.people = [@people[0]]
164
+ @room.save
165
+ @room = Room.find(@room.id) # we don't want to let id and object setters interfere with each other
166
+ @room.people2_ids << @people[1].id
167
+ @room.people2_ids.should == [@people[0].id] # ID array manipulation is ignored
168
+
169
+ @room.person_ids.size.should == 1
170
+ @room.person_ids << @people[1].id
171
+ @room.person_ids.should == [@people[0].id]
172
+ Room.find(@room.id).person_ids.should == [@people[0].id]
173
+ @room.save.should be_true
174
+ Room.find(@room.id).person_ids.should == [@people[0].id] # ID array manipulation is ignored, too
175
+ end
176
+
177
+ it "should work with id setters" do
178
+ @room = Room.find(@room.id)
179
+ @room.people = [@people[0], @people[1]]
180
+ @room.save
181
+ @room = Room.find(@room.id)
182
+ @room.person_ids.should == [@people[0].id, @people[1].id]
183
+ @room.person_ids = [@people[1].id]
184
+ @room.person_ids.should == [@people[1].id]
185
+ Room.find(@room.id).person_ids.should == [@people[0].id,@people[1].id]
186
+ @room.save.should be_true
187
+ Room.find(@room.id).person_ids.should == [@people[1].id]
188
+ end
189
+ end
190
+
191
+ describe "doors" do
192
+ before :all do
193
+ @rooms = []
194
+ @rooms << Room.create(:name => 'Kitchen', :maximum_occupancy => 1)
195
+ @rooms << Room.create(:name => 'Dining room', :maximum_occupancy => 10)
196
+ @door = Door.new(:name => 'Kitchen-Dining-room door')
197
+ end
198
+
199
+ it "passes initial checks" do
200
+ Room.count.should == 2
201
+ Door.count.should == 0
202
+
203
+ @door.rooms.should == []
204
+ @door.rooms_without_deferred_save.should == []
205
+ end
206
+
207
+ it "the association has an include? method" do
208
+ @door.rooms << @rooms[0]
209
+ @door.rooms.include?(@rooms[0]).should be_true
210
+ @door.rooms.include?(@rooms[1]).should be_false
211
+ end
212
+ end
213
+
214
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'has_many_with_deferred_save' do
4
+
5
+ before :each do
6
+ @room = Room.create(:maximum_occupancy => 2)
7
+ @table1 = Table.create(:room_id => @room.id)
8
+ @table2 = Table.create
9
+ @chair1 = Chair.create(:table_id => @table1.id, :name => "First")
10
+ @chair2 = Chair.create(:table_id => @table2.id, :name => "Second")
11
+ end
12
+
13
+ it 'should work with tables obj setter/getter' do
14
+ @room.tables.should == [@table1]
15
+ @room.tables = [@table1, @table2]
16
+ Room.find(@room.id).tables.should == [@table1] # not saved yet
17
+ @room.save.should be_true
18
+ Room.find(@room.id).tables.should == [@table1, @table2]
19
+ end
20
+
21
+ it 'should work with tables id setter/getter' do
22
+ @room.table_ids.should == [@table1.id]
23
+ @room.table_ids = [@table1.id, @table2.id]
24
+ Room.find(@room.id).table_ids.should == [@table1.id] # not saved yet
25
+ @room.save.should be_true
26
+ Room.find(@room.id).table_ids.should == [@table1.id, @table2.id]
27
+ end
28
+
29
+ it 'should work with array methods' do
30
+ @room.tables.should == [@table1]
31
+ @room.tables << @table2
32
+ Room.find(@room.id).tables.should == [@table1] # not saved yet
33
+ @room.save.should be_true
34
+ Room.find(@room.id).tables.should == [@table1, @table2]
35
+ @room.tables -= [@table1]
36
+ Room.find(@room.id).tables.should == [@table1, @table2]
37
+ @room.save.should be_true
38
+ Room.find(@room.id).tables.should == [@table2]
39
+ end
40
+
41
+ it 'should reload temporary objects' do
42
+ @room.tables << @table2
43
+ @room.tables.should == [@table1, @table2]
44
+ @room.reload
45
+ @room.tables.should == [@table1]
46
+ end
47
+
48
+ it "should be dumpable with Marshal" do
49
+ lambda { Marshal.dump(@room.tables) }.should_not raise_exception
50
+ lambda { Marshal.dump(Room.new.tables) }.should_not raise_exception
51
+ end
52
+
53
+ describe 'with through option' do
54
+ it 'should have a correct list' do
55
+ # TODO these testcases need to be improved
56
+ @room.chairs.should == [@chair1] # through table1
57
+ @room.tables << @table2
58
+ @room.save.should be_true
59
+ @room.chairs.should == [@chair1] # association doesn't reload itself
60
+ @room.reload
61
+ @room.chairs.should == [@chair1, @chair2]
62
+ end
63
+
64
+ it 'should defer association methods' do
65
+ @room.chairs.first.should == @chair1
66
+ @room.chairs.find(:all, :conditions => {:name => "First"}).should == [@chair1]
67
+ lambda {
68
+ @room.chairs.create(:name => "New one")
69
+ }.should raise_error(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection)
70
+ end
71
+
72
+ it "should be dumpable with Marshal" do
73
+ lambda { Marshal.dump(@room.chairs) }.should_not raise_exception
74
+ lambda { Marshal.dump(Room.new.chairs) }.should_not raise_exception
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ class Chair < ActiveRecord::Base
2
+ belongs_to :table
3
+ end
@@ -0,0 +1,3 @@
1
+ class Door < ActiveRecord::Base
2
+ has_and_belongs_to_many_with_deferred_save :rooms
3
+ end
@@ -0,0 +1,3 @@
1
+ class Person < ActiveRecord::Base
2
+ has_and_belongs_to_many_with_deferred_save :rooms, :validate => true
3
+ end
@@ -0,0 +1,51 @@
1
+ class Room < ActiveRecord::Base
2
+
3
+ attr :bs_diff_before_module, true
4
+ attr :bs_diff_after_module, true
5
+ attr :bs_diff_method, true
6
+
7
+ before_save :diff_before_module
8
+
9
+ has_and_belongs_to_many_with_deferred_save :people, :before_add => :before_adding_person
10
+ has_and_belongs_to_many :people2, :class_name => 'Person'
11
+ has_and_belongs_to_many_with_deferred_save :doors
12
+
13
+ has_many_with_deferred_save :tables
14
+ has_many_with_deferred_save :chairs, :through => :tables #TODO test compatibility with through associations
15
+
16
+ before_save :diff_after_module
17
+
18
+ validate :people_count
19
+
20
+ def people_count
21
+ if people.size > maximum_occupancy
22
+ errors.add :people, "This room has reached its maximum occupancy"
23
+ end
24
+ end
25
+
26
+ # Just in case they try to bypass our new accessor and call people_without_deferred_save directly...
27
+ # (This should never be necessary; it is for demonstration purposes only...)
28
+ def before_adding_person(person)
29
+ if self.people_without_deferred_save.size + [person].size > maximum_occupancy
30
+ raise "There are too many people in this room"
31
+ end
32
+ end
33
+
34
+ def diff_before_module
35
+ #should detect the changes
36
+ self.bs_diff_before_module = (people.size - people_without_deferred_save.size) != 0
37
+ true
38
+ end
39
+
40
+ def diff_after_module
41
+ # should not detect the changes
42
+ self.bs_diff_after_module = (people.size - people_without_deferred_save.size) != 0
43
+ true
44
+ end
45
+
46
+ def before_save
47
+ # old_style, should not detect the changes
48
+ self.bs_diff_method = (people.size - people_without_deferred_save.size) != 0
49
+ true
50
+ end
51
+ end
@@ -0,0 +1,4 @@
1
+ class Table < ActiveRecord::Base
2
+ belongs_to :room
3
+ has_many_with_deferred_save :chairs
4
+ end
@@ -0,0 +1,44 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
2
+ plugin_test_dir = File.dirname(__FILE__)
3
+
4
+ require 'rubygems'
5
+ USE_AR_3 = true
6
+
7
+ if defined?(USE_AR_3) && USE_AR_3
8
+ gem 'activerecord', '=3.2.2'
9
+ require 'logger'
10
+ require 'active_record'
11
+ else
12
+ gem 'activerecord', '=2.3.14'
13
+ require 'active_record'
14
+ # Workaround for https://rails.lighthouseapp.com/projects/8994/tickets/2577-when-using-activerecordassociations-outside-of-rails-a-nameerror-is-thrown
15
+ ActiveRecord::ActiveRecordError
16
+ end
17
+
18
+ require plugin_test_dir + '/../init.rb'
19
+
20
+ ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/test.log")
21
+
22
+ ActiveRecord::Base.configurations = YAML::load(IO.read(plugin_test_dir + "/db/database.yml"))
23
+ ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite3mem")
24
+ ActiveRecord::Migration.verbose = false
25
+ load(File.join(plugin_test_dir, "db", "schema.rb"))
26
+
27
+ Dir["#{plugin_test_dir}/models/*.rb"].each {|file| require file }
28
+
29
+ RSpec.configure do |config|
30
+ config.before do
31
+ end
32
+ end
33
+
34
+ class ActiveRecord::Base
35
+
36
+ # Compatibility method for AR 2.3.x and AR 3.2.x
37
+ def get_error attr
38
+ if errors.respond_to?(:on)
39
+ errors.on(attr)
40
+ else
41
+ errors[attr].try(:first)
42
+ end
43
+ end
44
+ end
data/uninstall.rb ADDED
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: deferred_associations
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.5.0
6
+ platform: ruby
7
+ authors:
8
+ - "Martin K\xC3\xB6rner"
9
+ - Tyler Rick
10
+ - Alessio Caiazza
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+
15
+ date: 2012-03-18 00:00:00 Z
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: activerecord
19
+ prerelease: false
20
+ requirement: &id001 !ruby/object:Gem::Requirement
21
+ none: false
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: "0"
26
+ type: :runtime
27
+ version_requirements: *id001
28
+ - !ruby/object:Gem::Dependency
29
+ name: rspec
30
+ prerelease: false
31
+ requirement: &id002 !ruby/object:Gem::Requirement
32
+ none: false
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: "0"
37
+ type: :development
38
+ version_requirements: *id002
39
+ description: |-
40
+ Makes ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) or has_many
41
+ association until you call model.save, allowing validation in the style of normal attributes. Additionally you
42
+ can check inside before_save filters, if the association was altered.
43
+ email: martin.koerner@objectfab.de
44
+ executables: []
45
+
46
+ extensions: []
47
+
48
+ extra_rdoc_files: []
49
+
50
+ files:
51
+ - CHANGELOG
52
+ - Rakefile
53
+ - Readme.markdown
54
+ - VERSION
55
+ - has_and_belongs_to_many_with_deferred_save.gemspec
56
+ - init.rb
57
+ - install.rb
58
+ - lib/array_to_association_wrapper.rb
59
+ - lib/has_and_belongs_to_many_with_deferred_save.rb
60
+ - lib/has_many_with_deferred_save.rb
61
+ - spec/.gitignore
62
+ - spec/db/database.yml
63
+ - spec/db/schema.rb
64
+ - spec/has_and_belongs_to_many_with_deferred_save_spec.rb
65
+ - spec/has_many_with_deferred_save_spec.rb
66
+ - spec/models/chair.rb
67
+ - spec/models/door.rb
68
+ - spec/models/person.rb
69
+ - spec/models/room.rb
70
+ - spec/models/table.rb
71
+ - spec/spec_helper.rb
72
+ - uninstall.rb
73
+ homepage: http://github.com/MartinKoerner/deferred_associations
74
+ licenses: []
75
+
76
+ post_install_message:
77
+ rdoc_options: []
78
+
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.15
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: Makes ActiveRecord defer/postpone habtm or has_many associations
100
+ test_files: []
101
+