deferred_associations 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +21 -0
- data/Rakefile +26 -0
- data/Readme.markdown +70 -0
- data/VERSION +1 -0
- data/has_and_belongs_to_many_with_deferred_save.gemspec +63 -0
- data/init.rb +3 -0
- data/install.rb +1 -0
- data/lib/array_to_association_wrapper.rb +67 -0
- data/lib/has_and_belongs_to_many_with_deferred_save.rb +144 -0
- data/lib/has_many_with_deferred_save.rb +102 -0
- data/spec/.gitignore +2 -0
- data/spec/db/database.yml +21 -0
- data/spec/db/schema.rb +40 -0
- data/spec/has_and_belongs_to_many_with_deferred_save_spec.rb +214 -0
- data/spec/has_many_with_deferred_save_spec.rb +77 -0
- data/spec/models/chair.rb +3 -0
- data/spec/models/door.rb +3 -0
- data/spec/models/person.rb +3 -0
- data/spec/models/room.rb +51 -0
- data/spec/models/table.rb +4 -0
- data/spec/spec_helper.rb +44 -0
- data/uninstall.rb +1 -0
- metadata +101 -0
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
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,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
|
data/spec/models/door.rb
ADDED
data/spec/models/room.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|