acts_has_many 0.1.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +46 -67
- data/lib/acts_has_many/active_record/acts_has_many/child.rb +143 -0
- data/lib/acts_has_many/active_record/acts_has_many/parent.rb +106 -0
- data/lib/acts_has_many/active_record/acts_has_many.rb +100 -0
- data/lib/acts_has_many/version.rb +1 -1
- data/lib/acts_has_many.rb +5 -2
- data/spec/has_many_spec.rb +157 -190
- data/spec/has_many_through_spec.rb +102 -102
- data/spec/helper.rb +1 -0
- metadata +5 -3
- data/lib/acts_has_many/active_record/acts/has_many.rb +0 -358
data/README.md
CHANGED
@@ -33,9 +33,9 @@ end
|
|
33
33
|
# OR
|
34
34
|
|
35
35
|
class Company < ActiveRecord::Base
|
36
|
-
acts_has_many :users # list necessary relations
|
37
|
-
|
38
36
|
has_many :users
|
37
|
+
|
38
|
+
acts_has_many :users # list necessary relations
|
39
39
|
end
|
40
40
|
|
41
41
|
# acts_has_many options:
|
@@ -60,13 +60,12 @@ company.actuale? :users # => false ( exclude 1 record of current relation)
|
|
60
60
|
|
61
61
|
company # => <Company id: 1, title: "Microsoft">
|
62
62
|
|
63
|
-
|
63
|
+
update_record, delete_record = company.has_many_update({ title: 'Google'}, :users)
|
64
64
|
# or
|
65
|
-
#
|
65
|
+
# update_record, delete_record = company.update_with_users({ title: 'Google'})
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
company # => <Company id: 1, title: "Google">
|
67
|
+
update_record # => <Company id: 1, title: "Google">
|
68
|
+
delete_record # => nil
|
70
69
|
|
71
70
|
user2 = User.create do |user|
|
72
71
|
user.company = company
|
@@ -80,11 +79,11 @@ company.actuale? :users # => true
|
|
80
79
|
# user2.destroy # => user will be destroyed, company will rollback (because company is used by other user)
|
81
80
|
|
82
81
|
company # => <Company id: 1, title: "Google">
|
83
|
-
|
84
|
-
|
85
|
-
|
82
|
+
update_record, delete_id = company.has_many_update({ title: 'Apple'}, :users)
|
83
|
+
update_record # => <Company id: 2, title: "Apple">
|
84
|
+
delete_record # => nil
|
86
85
|
|
87
|
-
user2.company =
|
86
|
+
user2.company = update_record
|
88
87
|
user2.save
|
89
88
|
|
90
89
|
# if you want to destroy user now
|
@@ -95,65 +94,17 @@ Company.all # [#<Company id: 1, title: "Google">, #<Company id: 2, title: "Apple
|
|
95
94
|
company.destroy # => false (because company is used)
|
96
95
|
|
97
96
|
companu # => <Company id: 1, title: "Google">
|
98
|
-
|
99
|
-
|
100
|
-
|
97
|
+
update_record, delete_record = company.has_many_update({ title: 'Apple'}, :users)
|
98
|
+
update_record # => <Company id: 2, title: "Apple">
|
99
|
+
delete_record # => <Company id: 1, title: "Google">
|
101
100
|
|
102
|
-
user2.company =
|
101
|
+
user2.company = update_record
|
103
102
|
user2.save
|
104
103
|
|
105
104
|
company.destroy # => true
|
106
105
|
|
107
|
-
# this situation with
|
108
|
-
|
109
|
-
# if you need update element and you don't care where it uses you can skip relation
|
110
|
-
Company.first.has_many_update({title: "IBM"})
|
111
|
-
# in this case check all relation but if you use this gem you don't have unused record
|
112
|
-
# that why this exampl is equal Company.first_or_create({title: "IBM"})
|
113
|
-
|
106
|
+
# this situation with delete_record best way is "company.delete" because you miss unnecessary check actuality
|
114
107
|
|
115
|
-
```
|
116
|
-
### Also you can use has_many_update!
|
117
|
-
```ruby
|
118
|
-
|
119
|
-
user = User.first
|
120
|
-
user.company.has_many_update!({title: "Google"}, user)
|
121
|
-
|
122
|
-
user.reload # becaus variable user have old data
|
123
|
-
user.company # <Company id: 3, title: "Google">
|
124
|
-
# you don't need update user
|
125
|
-
```
|
126
|
-
### Use acts_has_many_for with acts_has_many
|
127
|
-
When use acts_has_many_for you don't need set relation and give parent record in case with has_many_update!
|
128
|
-
```ruby
|
129
|
-
|
130
|
-
class User < ActiveRecord::Base
|
131
|
-
belongs_to :company, dependent: :destroy
|
132
|
-
acts_has_many_for :company
|
133
|
-
end
|
134
|
-
|
135
|
-
class Company < ActiveRecord::Base
|
136
|
-
has_many :users
|
137
|
-
|
138
|
-
acts_has_many # after necessary relation
|
139
|
-
end
|
140
|
-
|
141
|
-
# in this case
|
142
|
-
|
143
|
-
user = User.first
|
144
|
-
company = user.company
|
145
|
-
company.has_many_update!({title: "Google"}) # don't give record user
|
146
|
-
|
147
|
-
user.reload # becaus variable user have old data
|
148
|
-
user.company # <Company id: 3, title: "Google">
|
149
|
-
|
150
|
-
new_id, del_id = user.company.has_many_update({title: "Google"}) # don't give relation
|
151
|
-
|
152
|
-
# in this case has_many_update doesn't know about relation and the same as if you skip relation(see above)
|
153
|
-
new_id, del_id = Company.first.has_many_update({title: "something else"})
|
154
|
-
|
155
|
-
# not work becous has_many_update! doesn't know about parent record
|
156
|
-
Company.first.has_many_update!({title: "something else"})
|
157
108
|
```
|
158
109
|
|
159
110
|
### has_many_update_through used with has_many :through and get array parameters
|
@@ -169,9 +120,9 @@ class UserCompany < ActiveRecord::Base
|
|
169
120
|
|
170
121
|
class Company < ActiveRecord::Base
|
171
122
|
has_many :users, :through => :user_company
|
172
|
-
acts_has_many :through => true
|
173
|
-
|
174
123
|
has_many :user_company
|
124
|
+
|
125
|
+
acts_has_many :users, :through => true
|
175
126
|
end
|
176
127
|
|
177
128
|
class User < ActiveRecord::Base
|
@@ -186,6 +137,35 @@ user.companies = new_rows # update user companies
|
|
186
137
|
Company.delete(delete_ids) # for delete_ids from has_many_update_through best way is to use "delete" and miss unnecessary check
|
187
138
|
```
|
188
139
|
|
140
|
+
### Use acts_has_many_for with acts_has_many
|
141
|
+
When use acts_has_many_for you can user attributes
|
142
|
+
```ruby
|
143
|
+
|
144
|
+
user = User.create name: 'Bill', company_attributes: { title: 'Microsoft' }
|
145
|
+
|
146
|
+
or
|
147
|
+
|
148
|
+
user.company_attributes = { title: 'Google' }
|
149
|
+
|
150
|
+
or
|
151
|
+
|
152
|
+
user.company_attributes = Company.first
|
153
|
+
user.save
|
154
|
+
|
155
|
+
# if you use has_many_through you can use collection
|
156
|
+
|
157
|
+
user = User.create name: 'Bob', companies_collection: [{title: 'MS'}, {title: 'Google'}]
|
158
|
+
|
159
|
+
or
|
160
|
+
|
161
|
+
user.companies_collection = [{title: 'MS'}, {title: 'Google'}]
|
162
|
+
|
163
|
+
or
|
164
|
+
|
165
|
+
user.companies_collection = Company.all
|
166
|
+
user.save
|
167
|
+
|
168
|
+
```
|
189
169
|
|
190
170
|
Contributing
|
191
171
|
------------
|
@@ -193,7 +173,6 @@ You can help improve this project.
|
|
193
173
|
|
194
174
|
Here are some ways *you* can contribute:
|
195
175
|
|
196
|
-
* by using alpha, beta, and prerelease versions
|
197
176
|
* by reporting bugs
|
198
177
|
* by suggesting new features
|
199
178
|
* by writing or editing documentation
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ActsHasMany
|
3
|
+
|
4
|
+
# Added:
|
5
|
+
# class methods:
|
6
|
+
# +has_many_through_update+
|
7
|
+
#
|
8
|
+
# instance methods:
|
9
|
+
# +has_many_update+
|
10
|
+
# +update_with_<relation>+
|
11
|
+
# +actuale?+
|
12
|
+
|
13
|
+
module Child
|
14
|
+
|
15
|
+
def self.included base
|
16
|
+
|
17
|
+
# Generate methods:
|
18
|
+
# update_with_<relation>(data)
|
19
|
+
|
20
|
+
base.dependent_relations.each do |relation|
|
21
|
+
define_method("update_with_#{relation}") do |data|
|
22
|
+
has_many_update data, relation
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# +has_many_update+ (return: array) - [new_record, delete_record]
|
29
|
+
# options: (Hash is deprecated, list parameters)
|
30
|
+
# data (type: hash) - data for updte
|
31
|
+
# relation (type: str, symbol) - current relation for check
|
32
|
+
#
|
33
|
+
|
34
|
+
def has_many_update data, relation
|
35
|
+
data = { model.compare => ''}.merge data
|
36
|
+
|
37
|
+
if relation.blank?
|
38
|
+
warn "[ARRGUMENT MISSING]: 'has_many_update' don't know about current relation, and check all relations"
|
39
|
+
end
|
40
|
+
|
41
|
+
has_many_cleaner data.symbolize_keys, relation
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# +actuale?+ - check the acutuality of element in has_many table
|
46
|
+
# options:
|
47
|
+
# relation (String, Symbol) - for exclude one record from current relation
|
48
|
+
#
|
49
|
+
|
50
|
+
def actuale? opt = ""
|
51
|
+
relation = opt.to_s.tableize
|
52
|
+
|
53
|
+
actuale = false
|
54
|
+
model.dependent_relations.each do |dependent_relation|
|
55
|
+
tmp = self.send dependent_relation
|
56
|
+
if relation == dependent_relation
|
57
|
+
actuale ||= tmp.all.size > 1
|
58
|
+
else
|
59
|
+
actuale ||= tmp.exists?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
actuale
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
#
|
69
|
+
# +destroy_filter+ - use with before_filter, and protect actuale records
|
70
|
+
#
|
71
|
+
|
72
|
+
def destroy_filter
|
73
|
+
not actuale?
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# +has_many_cleaner+ - base mothod
|
78
|
+
#
|
79
|
+
|
80
|
+
def has_many_cleaner data, relation
|
81
|
+
compare = { model.compare => data[model.compare] }
|
82
|
+
|
83
|
+
new_record = self
|
84
|
+
del_record = nil
|
85
|
+
|
86
|
+
if actuale? relation
|
87
|
+
new_record = model.where(compare).first_or_create data
|
88
|
+
else
|
89
|
+
object_tmp = model.where(compare).first
|
90
|
+
unless object_tmp.nil?
|
91
|
+
del_record = (new_record.id == object_tmp.id) ? nil : new_record
|
92
|
+
new_record = object_tmp
|
93
|
+
else
|
94
|
+
if new_record.id.nil?
|
95
|
+
new_record = model.where(compare).first_or_create data
|
96
|
+
else
|
97
|
+
if data[model.compare].blank?
|
98
|
+
del_record, new_record = new_record, nil
|
99
|
+
else
|
100
|
+
update_attributes data
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
[new_record, del_record]
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
module ChildThrough
|
110
|
+
|
111
|
+
#
|
112
|
+
# +has_many_through_update+ (return array) [ 1 - array new records, 2 - array delete records ]
|
113
|
+
# options
|
114
|
+
# :update (array) - data for update (id and data)
|
115
|
+
# :new (array) - data for create record (data)
|
116
|
+
#
|
117
|
+
# +for delete records need use method destroy !!!+
|
118
|
+
#
|
119
|
+
|
120
|
+
def has_many_through_update(options)
|
121
|
+
record_add = []
|
122
|
+
record_del = []
|
123
|
+
|
124
|
+
options[:update].each do |id, data|
|
125
|
+
add, del = find(id).has_many_update data, options[:relation]
|
126
|
+
record_add << add unless add.nil?
|
127
|
+
record_del << del unless del.nil?
|
128
|
+
end unless options[:update].nil?
|
129
|
+
|
130
|
+
unless options[:new].nil?
|
131
|
+
options[:new].uniq!
|
132
|
+
options[:new].each do |data|
|
133
|
+
data = data.symbolize_keys
|
134
|
+
record_add << where(compare => data[compare]).first_or_create(data)
|
135
|
+
record_del.delete record_add.last
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
[record_add, record_del]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ActsHasMany
|
3
|
+
|
4
|
+
#
|
5
|
+
# Add class methods:
|
6
|
+
# +<relation>_attributes=+
|
7
|
+
# or
|
8
|
+
# +<relation>_collection=+
|
9
|
+
#
|
10
|
+
|
11
|
+
module Parent
|
12
|
+
|
13
|
+
def self.included base
|
14
|
+
|
15
|
+
#
|
16
|
+
# Generate class methods
|
17
|
+
# +<relation>_attributes=+ - set one element (use with belogns_to)
|
18
|
+
# options:
|
19
|
+
# data (type: Hash, existing recod, NilClass)
|
20
|
+
#
|
21
|
+
# Create or update child record and set to parent when `data` is Hash
|
22
|
+
# Change data with delating or leaving child record when `data` is exists record, or NilClass
|
23
|
+
#
|
24
|
+
# =>------------------------------------------------------------------
|
25
|
+
#
|
26
|
+
# +<relation>_collection=+ - set many elements (use with has_many :through)
|
27
|
+
# options:
|
28
|
+
# data (type: Array with (exist records or Hash)
|
29
|
+
#
|
30
|
+
# Create or select exists records and set to parent, unused record try destory
|
31
|
+
#
|
32
|
+
|
33
|
+
base.dependent_relations.each do |relation|
|
34
|
+
unless base.reflect_on_association relation.to_sym
|
35
|
+
raise ArgumentError, "No association found for name `#{relation}`. Has it been defined yet?"
|
36
|
+
end
|
37
|
+
|
38
|
+
relation = relation.to_s
|
39
|
+
unless base.reflect_on_association(relation.to_sym).collection?
|
40
|
+
base.class_eval <<-EOV, __FILE__ , __LINE__
|
41
|
+
def #{relation}_attributes= data
|
42
|
+
self.tmp_garbage ||= {}
|
43
|
+
current = self.#{relation}
|
44
|
+
|
45
|
+
if data.is_a? Hash
|
46
|
+
if current
|
47
|
+
new, del = current.has_many_update data, '#{base.name.tableize}'
|
48
|
+
else
|
49
|
+
new, del = #{relation.classify}.new.has_many_update data, '#{base.name.tableize}'
|
50
|
+
end
|
51
|
+
|
52
|
+
self.#{relation} = new
|
53
|
+
self.tmp_garbage.merge!({ #{relation}: del }) if del
|
54
|
+
else
|
55
|
+
self.#{relation} = data
|
56
|
+
self.tmp_garbage.merge!({ #{relation}: current }) if current
|
57
|
+
end
|
58
|
+
end
|
59
|
+
EOV
|
60
|
+
else
|
61
|
+
base.class_eval <<-EOV, __FILE__ , __LINE__
|
62
|
+
def #{relation}_collection= data
|
63
|
+
self.tmp_garbage ||= {}
|
64
|
+
|
65
|
+
if data.is_a? Array
|
66
|
+
if data.first.is_a? Hash
|
67
|
+
new, del = #{relation.classify}.has_many_through_update new: data, relation: '#{base.name.tableize}'
|
68
|
+
elsif data.first.is_a? #{relation.classify}
|
69
|
+
new, del = data, []
|
70
|
+
elsif data.empty?
|
71
|
+
new, del = [],[]
|
72
|
+
else
|
73
|
+
raise ArgumentError, "Array with (Hash or " + #{relation.classify}.inspect + ") expected, got Array with " + data.first.inspect
|
74
|
+
end
|
75
|
+
|
76
|
+
#{relation}.each{ |t| del << t if not new.include? t }
|
77
|
+
self.#{relation} = new
|
78
|
+
self.tmp_garbage.merge!({ #{relation}: del }) if del.present?
|
79
|
+
else
|
80
|
+
raise ArgumentError, "Array expected, got " + data.inspect
|
81
|
+
end
|
82
|
+
end
|
83
|
+
EOV
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
#
|
91
|
+
# +clear_garbage+ work with after_save and try destroied records from tmp_garbage
|
92
|
+
#
|
93
|
+
|
94
|
+
def clear_garbage
|
95
|
+
self.tmp_garbage.each do |relation, record|
|
96
|
+
if record.is_a? Array
|
97
|
+
record.each { |r| r.destroy }
|
98
|
+
else
|
99
|
+
record.destroy
|
100
|
+
end
|
101
|
+
end if self.tmp_garbage.present?
|
102
|
+
self.tmp_garbage = {}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ActsHasMany
|
3
|
+
|
4
|
+
#
|
5
|
+
# Add class methods: (for use in your model)
|
6
|
+
# +acts_has_many_for+
|
7
|
+
# +acts_has_many+
|
8
|
+
#
|
9
|
+
|
10
|
+
def self.included base
|
11
|
+
base.extend ClassMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
# Add class methods:
|
17
|
+
# +dependent_relations+
|
18
|
+
# +compare+
|
19
|
+
# +model+
|
20
|
+
# +has_many_through_update+
|
21
|
+
#
|
22
|
+
# Add instance mothods:
|
23
|
+
# +actuale?+
|
24
|
+
# +has_many_update+
|
25
|
+
# +update_with_<relation>+
|
26
|
+
#
|
27
|
+
# Set:
|
28
|
+
# validates for <compare_element> (uniqueness: true, presence: true)
|
29
|
+
# before destroy filter
|
30
|
+
#
|
31
|
+
|
32
|
+
def acts_has_many *opt
|
33
|
+
options = { compare: :title, through: false }
|
34
|
+
options.update opt.extract_options!
|
35
|
+
options.assert_valid_keys :compare, :through
|
36
|
+
options[:relations] = opt
|
37
|
+
|
38
|
+
options[:relations] = self.reflect_on_all_associations(:has_many)
|
39
|
+
.map(&:name) if options[:relations].blank?
|
40
|
+
|
41
|
+
dependent_relations = []
|
42
|
+
options[:relations].each do |relation|
|
43
|
+
if reflect_on_association relation.to_sym
|
44
|
+
dependent_relations << relation.to_s.tableize
|
45
|
+
else
|
46
|
+
raise ArgumentError, "No association found for name `#{relation}'. Has it been defined yet?"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class_eval <<-EOV, __FILE__ , __LINE__
|
51
|
+
def self.dependent_relations
|
52
|
+
#{dependent_relations}
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.compare
|
56
|
+
'#{options[:compare]}'.to_sym
|
57
|
+
end
|
58
|
+
|
59
|
+
include ActiveRecord::ActsHasMany::Child
|
60
|
+
#{'extend ActiveRecord::ActsHasMany::ChildThrough' if options[:through]}
|
61
|
+
|
62
|
+
def model
|
63
|
+
#{self}
|
64
|
+
end
|
65
|
+
|
66
|
+
validates :#{options[:compare]}, uniqueness: true, presence: true
|
67
|
+
before_destroy :destroy_filter
|
68
|
+
EOV
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Add class methods:
|
73
|
+
# +dependent_relations+
|
74
|
+
#
|
75
|
+
# +<relation>_attributes=+
|
76
|
+
# or
|
77
|
+
# +<relation>_collection=+
|
78
|
+
#
|
79
|
+
# Set:
|
80
|
+
# after save filter
|
81
|
+
# attribut accessor tmp_garbage
|
82
|
+
#
|
83
|
+
|
84
|
+
def acts_has_many_for *relations
|
85
|
+
class_eval <<-EOV, __FILE__ , __LINE__
|
86
|
+
def self.dependent_relations
|
87
|
+
#{relations}
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_accessor :tmp_garbage
|
91
|
+
|
92
|
+
include ActiveRecord::ActsHasMany::Parent
|
93
|
+
|
94
|
+
after_save :clear_garbage
|
95
|
+
EOV
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/acts_has_many.rb
CHANGED
@@ -1,2 +1,5 @@
|
|
1
|
-
require 'acts_has_many/active_record/
|
2
|
-
|
1
|
+
require 'acts_has_many/active_record/acts_has_many'
|
2
|
+
require 'acts_has_many/active_record/acts_has_many/child'
|
3
|
+
require 'acts_has_many/active_record/acts_has_many/parent'
|
4
|
+
|
5
|
+
ActiveRecord::Base.class_eval { include ActiveRecord::ActsHasMany}
|