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 +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
|
+
|