acts_has_many 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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}
|