has_and_belongs_to_many_with_deferred_save 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  pkg
2
+ *.db
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{has_and_belongs_to_many_with_deferred_save}
8
- s.version = "0.1.0"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Tyler Rick", "Alessio Caiazza"]
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
24
24
  "spec/db/database.yml",
25
25
  "spec/db/schema.rb",
26
26
  "spec/has_and_belongs_to_many_with_deferred_save_spec.rb",
27
+ "spec/models/door.rb",
27
28
  "spec/models/person.rb",
28
29
  "spec/models/room.rb",
29
30
  "spec/spec_helper.rb",
@@ -35,7 +36,8 @@ Gem::Specification.new do |s|
35
36
  s.rubygems_version = %q{1.3.5}
36
37
  s.summary = %q{Make ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) association until you call model.save, allowing validation in the style of normal attributes.}
37
38
  s.test_files = [
38
- "spec/models/room.rb",
39
+ "spec/models/door.rb",
40
+ "spec/models/room.rb",
39
41
  "spec/models/person.rb",
40
42
  "spec/has_and_belongs_to_many_with_deferred_save_spec.rb",
41
43
  "spec/spec_helper.rb",
@@ -21,7 +21,7 @@ module ActiveRecord
21
21
  has_and_belongs_to_many *args
22
22
  collection_name = args[0].to_s
23
23
  collection_singular_ids = collection_name.singularize + "_ids"
24
-
24
+
25
25
  # this will delete all the assocation into the join table after obj.destroy
26
26
  after_destroy { |record| record.save }
27
27
 
@@ -63,18 +63,18 @@ module ActiveRecord
63
63
 
64
64
  define_method "before_save_with_deferred_save_for_#{collection_name}" do
65
65
  # Question: Why do we need this @use_original_collection_reader_behavior stuff?
66
- # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only
66
+ # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only
67
67
  # records that have changed.
68
- # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not
68
+ # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not
69
69
  # knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database*
70
70
  # (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
71
71
  # two identical collections so nothing would ever get saved.
72
- # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use
72
+ # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use
73
73
  # @use_original_collection_reader_behavior as a switch.
74
-
74
+
75
75
  if self.respond_to? :"before_save_without_deferred_save_for_#{collection_name}"
76
- self.send("before_save_without_deferred_save_for_#{collection_name}")
77
- end
76
+ self.send("before_save_without_deferred_save_for_#{collection_name}")
77
+ end
78
78
 
79
79
  self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true
80
80
  if self.send("unsaved_#{collection_name}").nil?
@@ -103,20 +103,20 @@ module ActiveRecord
103
103
  define_method "initialize_unsaved_#{collection_name}" do |*args|
104
104
  #puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}"
105
105
  self.send "unsaved_#{collection_name}=", self.send("#{collection_name}_without_deferred_save", *args).clone
106
- # /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the
106
+ # /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the
107
107
  # database, in which case we want unsaved_collection to start out with the "saved collection".
108
- # If they just constructed a *new* object, this will still work, because self.collection_without_deferred_save.clone
108
+ # If they just constructed a *new* object, this will still work, because self.collection_without_deferred_save.clone
109
109
  # will return a new HasAndBelongsToManyAssociation (which acts like an empty array, []).
110
110
  # Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection
111
111
  # will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things
112
112
  # immediately, which is exactly what we're trying to avoid.)
113
-
113
+
114
114
  # trick collection_name.include?(obj)
115
115
  # If you use a collection of SignelTableInheritance and didn't :select 'type' the
116
116
  # include? method will not find any subclassed object.
117
117
  class << eval("@unsaved_#{collection_name}")
118
118
  def include_with_deferred_save?(obj)
119
- if self.find { |itm| itm == obj || (itm[:id] == obj[:id] && obj.is_a?(itm.class) ) }
119
+ if self.detect { |itm| itm == obj || (itm[:id] == obj[:id] && obj.is_a?(itm.class) ) }
120
120
  return true
121
121
  else
122
122
  return false
@@ -124,9 +124,32 @@ module ActiveRecord
124
124
  end
125
125
  alias_method_chain :include?, 'deferred_save'
126
126
  end
127
+
128
+
129
+ collection_without_deferred_save = self.send("#{collection_name}_without_deferred_save")
130
+ # (We don't have access to locals inside a normal class << object block, so we have to do it this way instead.)
131
+ (class << eval("@unsaved_#{collection_name}"); self end).class_eval do
132
+ define_method :find do |*args|
133
+ collection_without_deferred_save.send(:find, *args)
134
+ end
135
+ # We have to override these so that it doesn't call Array's version of these methods.
136
+ # Otherwise user will get a "can't convert Hash into Integer" error
137
+ define_method :first do |*args|
138
+ collection_without_deferred_save.send(:first, *args)
139
+ end
140
+ define_method :last do |*args|
141
+ collection_without_deferred_save.send(:first, *args)
142
+ end
143
+
144
+ define_method :method_missing do |method, *args|
145
+ #puts "#{self.class}.method_missing(#{method}) (#{collection_without_deferred_save.inspect})"
146
+ collection_without_deferred_save.send(method, *args)
147
+ end
148
+ end
149
+
127
150
  end
128
151
  private :"initialize_unsaved_#{collection_name}"
129
-
152
+
130
153
  end
131
154
  end
132
155
  end
@@ -18,4 +18,13 @@ ActiveRecord::Schema.define(:version => 1) do
18
18
  t.column "maximum_occupancy", :integer
19
19
  end
20
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
+
21
30
  end
@@ -5,13 +5,17 @@ describe "has_and_belongs_to_many_with_deferred_save" do
5
5
  describe "room maximum_occupancy" do
6
6
  before :all do
7
7
  @people = []
8
- @people << Person.create
9
- @people << Person.create
10
- @people << Person.create
8
+ @people << Person.create(:name => 'Filbert')
9
+ @people << Person.create(:name => 'Miguel')
10
+ @people << Person.create(:name => 'Rainer')
11
11
  @room = Room.new(:maximum_occupancy => 2)
12
12
  end
13
+ after :all do
14
+ Person.delete_all
15
+ Room.delete_all
16
+ end
13
17
 
14
- it "initial checks" do
18
+ it "passes initial checks" do
15
19
  Room .count.should == 0
16
20
  Person.count.should == 3
17
21
 
@@ -48,21 +52,23 @@ describe "has_and_belongs_to_many_with_deferred_save" do
48
52
 
49
53
  Room.count_by_sql("select count(*) from people_rooms").should == 2
50
54
  @room.valid?
51
- @room.errors.on(:people).should == "There are too many people in this room"
55
+ @room.errors.on(:people).should == "This room has reached its maximum occupancy"
52
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*.
53
57
  end
54
58
 
55
59
  it "when we try to save, it should fail, because room.people is still invaild" do
56
60
  @room.save.should == false
57
61
  Room.count_by_sql("select count(*) from people_rooms").should == 2 # It's still not there, because it didn't pass the validation.
58
- @room.errors.on(:people).should == "There are too many people in this room"
62
+ @room.errors.on(:people).should == "This room has reached its maximum occupancy"
59
63
  @room.people.size. should == 3
64
+ @people.map {|p| p.reload; p.rooms.size}.should == [1, 1, 0]
60
65
  end
61
66
 
62
67
  it "when we reload, it should go back to only having 2 people in the room" do
63
68
  @room.reload
64
69
  @room.people.size. should == 2
65
70
  @room.people_without_deferred_save.size. should == 2
71
+ @people.map {|p| p.reload; p.rooms.size}. should == [1, 1, 0]
66
72
  end
67
73
 
68
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
@@ -70,5 +76,84 @@ describe "has_and_belongs_to_many_with_deferred_save" do
70
76
  @room.people_without_deferred_save << @people[2]
71
77
  end.should raise_error(RuntimeError)
72
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
+ class << @people[2]
103
+ def validate
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
+ end
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?
118
+ @people[2].errors.on(: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.find_by_name('Filbert'). should == @people[0]
133
+ end
73
134
  end
135
+
136
+ describe "doors" do
137
+ before :all do
138
+ @rooms = []
139
+ @rooms << Room.create(:name => 'Kitchen', :maximum_occupancy => 1)
140
+ @rooms << Room.create(:name => 'Dining room', :maximum_occupancy => 10)
141
+ @door = Door.new(:name => 'Kitchen-Dining-room door')
142
+ end
143
+
144
+ it "passes initial checks" do
145
+ Room.count.should == 2
146
+ Door.count.should == 0
147
+
148
+ @door.rooms.should == []
149
+ @door.rooms_without_deferred_save.should == []
150
+ end
151
+
152
+ it "the association has an include? method" do
153
+ @door.rooms << @rooms[0]
154
+ @door.rooms.include?(@rooms[0]).should be_true
155
+ @door.rooms.include?(@rooms[1]).should be_false
156
+ end
157
+ end
158
+
74
159
  end
@@ -0,0 +1,3 @@
1
+ class Door < ActiveRecord::Base
2
+ has_and_belongs_to_many_with_deferred_save :rooms
3
+ end
@@ -1,3 +1,3 @@
1
1
  class Person < ActiveRecord::Base
2
- has_and_belongs_to_many :room
2
+ has_and_belongs_to_many_with_deferred_save :rooms, :validate => true
3
3
  end
@@ -1,9 +1,11 @@
1
1
  class Room < ActiveRecord::Base
2
2
  has_and_belongs_to_many_with_deferred_save :people, :before_add => :before_adding_person
3
+ has_and_belongs_to_many :people2, :class_name => 'Person'
4
+ has_and_belongs_to_many_with_deferred_save :doors
3
5
 
4
6
  def validate
5
7
  if people.size > maximum_occupancy
6
- errors.add :people, "There are too many people in this room"
8
+ errors.add :people, "This room has reached its maximum occupancy"
7
9
  end
8
10
  end
9
11
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: has_and_belongs_to_many_with_deferred_save
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Rick
@@ -54,6 +54,7 @@ files:
54
54
  - spec/db/database.yml
55
55
  - spec/db/schema.rb
56
56
  - spec/has_and_belongs_to_many_with_deferred_save_spec.rb
57
+ - spec/models/door.rb
57
58
  - spec/models/person.rb
58
59
  - spec/models/room.rb
59
60
  - spec/spec_helper.rb
@@ -87,6 +88,7 @@ signing_key:
87
88
  specification_version: 3
88
89
  summary: Make ActiveRecord defer/postpone saving the records you add to an habtm (has_and_belongs_to_many) association until you call model.save, allowing validation in the style of normal attributes.
89
90
  test_files:
91
+ - spec/models/door.rb
90
92
  - spec/models/room.rb
91
93
  - spec/models/person.rb
92
94
  - spec/has_and_belongs_to_many_with_deferred_save_spec.rb