has_and_belongs_to_many_with_deferred_save 0.1.0 → 0.2.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/.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