-
Notifications
You must be signed in to change notification settings - Fork 92
/
accessible_associations.rb
179 lines (145 loc) · 6.26 KB
/
accessible_associations.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
module Hobo
module AccessibleAssociations
extend self
def prepare_has_many_assignment(association, association_name, array_or_hash)
owner = association.proxy_owner
array = params_hash_to_array(array_or_hash)
array.map! do |record_hash_or_string|
finder = association.member_class.scoped :conditions => association.conditions
find_or_create_and_update(owner, association_name, finder, record_hash_or_string) do |id|
# The block is required to either locate find an existing record in the collection, or build a new one
if id
# TODO: We don't really want to find these one by one
association.find(id)
else
association.build
end
end
end
array.compact
end
def find_or_create_and_update(owner, association_name, finder, record_hash_or_string)
if record_hash_or_string.is_a?(String)
return nil if record_hash_or_string.blank?
# An ID or a name - the passed block will find the record
record = find_by_name_or_id(finder, record_hash_or_string)
elsif record_hash_or_string.is_a?(Hash)
# A hash of attributes
hash = record_hash_or_string
# Remove completely blank hashes
return nil if hash.values.all?(&:blank?)
id = hash.delete(:id)
record = yield id
record.attributes = hash
if owner.new_record? && record.new_record?
# work around
# https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/3510-has_many-build-does-not-set-reverse-reflection
# https://hobo.lighthouseapp.com/projects/8324/tickets/447-validation-problems-with-has_many-accessible-true
reverse = owner.class.reverse_reflection(association_name)
if reverse && reverse.macro==:belongs_to
method = "#{reverse.name}=".to_sym
record.send(method, owner) if record.respond_to? method
end
else
owner.include_in_save(association_name, record)
end
else
# It's already a record
record = record_hash_or_string
end
record
end
def params_hash_to_array(array_or_hash)
if array_or_hash.is_a?(Hash)
array = array_or_hash.get(*array_or_hash.keys.sort_by(&:to_i))
elsif array_or_hash.is_a?(String)
# Due to the way that rails works, there's no good way to tell
# the difference between an empty array and a params hash that
# just isn't making any updates to the array. So we're
# hacking this in: if you pash an empty string where an array
# is expected, we assume you wanted an empty array.
[]
else
array_or_hash
end
end
def find_by_name_or_id(finder, id_or_name)
if id_or_name =~ /^@(.*)/
id = $1
finder.find(id)
else
finder.named(id_or_name)
end
end
def finder_for_belongs_to(record, name)
refl = record.class.reflections[name]
conditions = ActiveRecord::Associations::BelongsToAssociation.new(record, refl).conditions
finder = refl.klass.scoped(:conditions => conditions)
end
end
classy_module(AccessibleAssociations) do
include Hobo::IncludeInSave
# --- has_many mass assignment support --- #
def self.has_many_with_accessible(name, options={}, &block)
has_many_without_accessible(name, options, &block)
if options[:accessible]
class_eval %{
def #{name}_with_accessible=(array_or_hash)
__items = Hobo::AccessibleAssociations.prepare_has_many_assignment(#{name}, :#{name}, array_or_hash)
self.#{name}_without_accessible = __items
# ensure the loaded array contains any changed records
self.#{name}.proxy_target[0..-1] = __items
end
}, __FILE__, __LINE__ - 7
alias_method_chain :"#{name}=", :accessible
end
end
metaclass.alias_method_chain :has_many, :accessible
# --- belongs_to assignment support --- #
def self.belongs_to_with_accessible(name, options={}, &block)
belongs_to_without_accessible(name, options, &block)
if options[:accessible]
class_eval %{
def #{name}_with_accessible=(record_hash_or_string)
finder = Hobo::AccessibleAssociations.finder_for_belongs_to(self, :#{name})
record = Hobo::AccessibleAssociations.find_or_create_and_update(self, :#{name}, finder, record_hash_or_string) do |id|
if id
raise ArgumentError, "attempted to update the wrong record in belongs_to association #{self}##{name}" unless
#{name} && id.to_s == self.#{name}.id.to_s
#{name}
else
finder.new
end
end
self.#{name}_without_accessible = record
end
}, __FILE__, __LINE__ - 15
alias_method_chain :"#{name}=", :accessible
else
# Not accessible - but finding by name and ID is still supported
class_eval %{
def #{name}_with_finder=(record_or_string)
record = if record_or_string.is_a?(String)
finder = Hobo::AccessibleAssociations.finder_for_belongs_to(self, :#{name})
Hobo::AccessibleAssociations.find_by_name_or_id(finder, record_or_string)
else # it is a record
record_or_string
end
self.#{name}_without_finder = record
end
}, __FILE__, __LINE__ - 12
alias_method_chain :"#{name}=", :finder
end
end
metaclass.alias_method_chain :belongs_to, :accessible
# Add :accessible to the valid keys so AR doesn't complain
def self.valid_keys_for_has_many_association_with_accessible
valid_keys_for_has_many_association_without_accessible + [:accessible]
end
metaclass.alias_method_chain :valid_keys_for_has_many_association, :accessible
def self.valid_keys_for_belongs_to_association_with_accessible
valid_keys_for_belongs_to_association_without_accessible + [:accessible]
end
metaclass.alias_method_chain :valid_keys_for_belongs_to_association, :accessible
end
end