gnuside-custom_fields 2.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +70 -0
- data/config/locales/de.yml +15 -0
- data/config/locales/en.yml +21 -0
- data/config/locales/fr.yml +25 -0
- data/config/locales/pt-BR.yml +9 -0
- data/config/locales/ru.yml +15 -0
- data/lib/custom_fields/extensions/active_support.rb +28 -0
- data/lib/custom_fields/extensions/carrierwave.rb +25 -0
- data/lib/custom_fields/extensions/mongoid/document.rb +21 -0
- data/lib/custom_fields/extensions/mongoid/factory.rb +20 -0
- data/lib/custom_fields/extensions/mongoid/fields/i18n.rb +55 -0
- data/lib/custom_fields/extensions/mongoid/fields/localized.rb +39 -0
- data/lib/custom_fields/extensions/mongoid/fields.rb +31 -0
- data/lib/custom_fields/extensions/mongoid/relations/referenced/in.rb +22 -0
- data/lib/custom_fields/extensions/mongoid/relations/referenced/many.rb +34 -0
- data/lib/custom_fields/extensions/mongoid/validations/collection_size.rb +43 -0
- data/lib/custom_fields/extensions/mongoid/validations/macros.rb +25 -0
- data/lib/custom_fields/extensions/origin/smash.rb +33 -0
- data/lib/custom_fields/field.rb +106 -0
- data/lib/custom_fields/source.rb +347 -0
- data/lib/custom_fields/target.rb +99 -0
- data/lib/custom_fields/target_helpers.rb +192 -0
- data/lib/custom_fields/types/belongs_to.rb +65 -0
- data/lib/custom_fields/types/boolean.rb +55 -0
- data/lib/custom_fields/types/date.rb +97 -0
- data/lib/custom_fields/types/date_time.rb +97 -0
- data/lib/custom_fields/types/default.rb +103 -0
- data/lib/custom_fields/types/email.rb +60 -0
- data/lib/custom_fields/types/file.rb +74 -0
- data/lib/custom_fields/types/float.rb +52 -0
- data/lib/custom_fields/types/has_many.rb +74 -0
- data/lib/custom_fields/types/integer.rb +54 -0
- data/lib/custom_fields/types/many_to_many.rb +75 -0
- data/lib/custom_fields/types/money.rb +146 -0
- data/lib/custom_fields/types/relationship_default.rb +44 -0
- data/lib/custom_fields/types/select.rb +217 -0
- data/lib/custom_fields/types/string.rb +55 -0
- data/lib/custom_fields/types/tags.rb +35 -0
- data/lib/custom_fields/types/text.rb +65 -0
- data/lib/custom_fields/version.rb +6 -0
- data/lib/custom_fields.rb +74 -0
- metadata +244 -0
@@ -0,0 +1,347 @@
|
|
1
|
+
module CustomFields
|
2
|
+
|
3
|
+
module Source
|
4
|
+
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
cattr_accessor :_custom_fields_for
|
9
|
+
self._custom_fields_for = []
|
10
|
+
|
11
|
+
attr_accessor :_custom_fields_diff
|
12
|
+
attr_accessor :_custom_field_localize_diff
|
13
|
+
end
|
14
|
+
|
15
|
+
# Determines if the relation is enhanced by the custom fields
|
16
|
+
#
|
17
|
+
# @example the Person class has somewhere in its code this: "custom_fields_for :addresses"
|
18
|
+
# person.custom_fields_for?(:addresses)
|
19
|
+
#
|
20
|
+
# @param [ String, Symbol ] name The name of the relation.
|
21
|
+
#
|
22
|
+
# @return [ true, false ] True if enhanced, false if not.
|
23
|
+
#
|
24
|
+
def custom_fields_for?(name)
|
25
|
+
self.class.custom_fields_for?(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the class enhanced by the custom fields.
|
29
|
+
# Be careful, call this method only if the source class
|
30
|
+
# has been saved with success.
|
31
|
+
#
|
32
|
+
# @param [ String, Symbol ] name The name of the relation.
|
33
|
+
#
|
34
|
+
# @return [ Class ] The modified class.
|
35
|
+
#
|
36
|
+
def klass_with_custom_fields(name)
|
37
|
+
# Rails.logger.debug "[CustomFields] klass_with_custom_fields #{self.send(name).metadata.klass} / #{self.send(name).metadata[:old_klass]}" if defined?(Rails) # DEBUG
|
38
|
+
recipe = self.custom_fields_recipe_for(name)
|
39
|
+
_metadata = self.send(name).metadata
|
40
|
+
target = _metadata[:original_klass] || _metadata.klass # avoid to use an already enhanced klass
|
41
|
+
target.klass_with_custom_fields(recipe)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the ordered list of custom fields for a relation
|
45
|
+
#
|
46
|
+
# @example the Person class has somewhere in its code this: "custom_fields_for :addresses"
|
47
|
+
# person.ordered_custom_fields(:addresses)
|
48
|
+
#
|
49
|
+
# @param [ String, Symbol ] name The name of the relation.
|
50
|
+
#
|
51
|
+
# @return [ Collection ] The ordered list.
|
52
|
+
#
|
53
|
+
def ordered_custom_fields(name)
|
54
|
+
self.send(:"#{name}_custom_fields").sort { |a, b| (a.position || 0) <=> (b.position || 0) }
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the recipe (meaning all the rules) needed to
|
58
|
+
# build the custom klass
|
59
|
+
#
|
60
|
+
# @param [ String, Symbol ] name The name of the relation.
|
61
|
+
#
|
62
|
+
# @return [ Array ] An array of hashes
|
63
|
+
#
|
64
|
+
def custom_fields_recipe_for(name)
|
65
|
+
{
|
66
|
+
'name' => "#{self.relations[name.to_s].class_name.demodulize}#{self._id}",
|
67
|
+
'rules' => self.ordered_custom_fields(name).map(&:to_recipe),
|
68
|
+
'version' => self.custom_fields_version(name),
|
69
|
+
'model_name' => self.relations[name.to_s].class_name.constantize.model_name
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns the number of the version for relation with custom fields
|
74
|
+
#
|
75
|
+
# @param [ String, Symbol ] name The name of the relation.
|
76
|
+
#
|
77
|
+
# @return [ Integer ] The version number
|
78
|
+
#
|
79
|
+
def custom_fields_version(name)
|
80
|
+
self.send(:"#{name}_custom_fields_version") || 0
|
81
|
+
end
|
82
|
+
|
83
|
+
# When the fields have been modified and before the object is saved,
|
84
|
+
# we bump the version.
|
85
|
+
#
|
86
|
+
# @param [ String, Symbol ] name The name of the relation.
|
87
|
+
#
|
88
|
+
def bump_custom_fields_version(name)
|
89
|
+
version = self.custom_fields_version(name) + 1
|
90
|
+
self.send(:"#{name}_custom_fields_version=", version)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Change the metadata of a relation enhanced by the custom fields.
|
94
|
+
# In Mongoid, all the instances of a same document share the same metadata objects.
|
95
|
+
#
|
96
|
+
# @param [ String, Symbol ] name The name of the relation.
|
97
|
+
#
|
98
|
+
def refresh_metadata_with_custom_fields(name)
|
99
|
+
return if !self.persisted? || self.send(:"#{name}_custom_fields").blank? # do not generate a klass without all the information
|
100
|
+
|
101
|
+
old_metadata = self.send(name).metadata
|
102
|
+
|
103
|
+
# puts "old_metadata = #{old_metadata.klass.inspect} / #{old_metadata.object_id.inspect}" # DEBUG
|
104
|
+
|
105
|
+
# puts "[CustomFields] refresh_metadata_with_custom_fields, #{name.inspect}, self = #{self.inspect}"
|
106
|
+
|
107
|
+
self.send(name).metadata = old_metadata.clone.tap do |metadata|
|
108
|
+
# Rails.logger.debug "[CustomFields] refresh_metadata_with_custom_fields #{metadata.klass}" if defined?(Rails) # DEBUG
|
109
|
+
|
110
|
+
# backup the current klass
|
111
|
+
metadata[:original_klass] ||= metadata.klass
|
112
|
+
|
113
|
+
metadata.instance_variable_set(:@klass, self.klass_with_custom_fields(name))
|
114
|
+
end
|
115
|
+
set_attribute_localization(name)
|
116
|
+
# puts "new_metadata = #{self.send(name).metadata.klass.inspect} / #{self.send(name).metadata.object_id.inspect}" # DEBUG
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_attribute_localization(name)
|
120
|
+
klass_name = name.singularize.to_sym
|
121
|
+
self.send(:"#{name}_custom_fields").each do |cf|
|
122
|
+
I18n.backend.store_translations ::I18n.locale,
|
123
|
+
{mongoid: { attributes: {klass_name => {cf.name.to_sym => cf.label}}}}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Initializes the object tracking the modifications
|
128
|
+
# of the custom fields
|
129
|
+
#
|
130
|
+
# @param [ String, Symbol ] name The name of the relation.
|
131
|
+
#
|
132
|
+
def initialize_custom_fields_diff(name)
|
133
|
+
self._custom_field_localize_diff ||= Hash.new([])
|
134
|
+
|
135
|
+
self._custom_fields_diff ||= {}
|
136
|
+
self._custom_fields_diff[name] = { '$set' => {}, '$unset' => {}, '$rename' => {} }
|
137
|
+
end
|
138
|
+
|
139
|
+
# Collects all the modifications of the custom fields
|
140
|
+
#
|
141
|
+
# @param [ String, Symbol ] name The name of the relation.
|
142
|
+
#
|
143
|
+
# @return [ Array ] An array of hashes storing the modifications
|
144
|
+
#
|
145
|
+
def collect_custom_fields_diff(name, fields)
|
146
|
+
# puts "==> collect_custom_fields_diff for #{name}, #{fields.size}" # DEBUG
|
147
|
+
|
148
|
+
memo = self.initialize_custom_fields_diff(name)
|
149
|
+
|
150
|
+
fields.map do |field|
|
151
|
+
field.collect_diff(memo)
|
152
|
+
end
|
153
|
+
|
154
|
+
# collect fields with a modified localized field
|
155
|
+
fields.each do |field|
|
156
|
+
if field.localized_changed? && field.persisted?
|
157
|
+
self._custom_field_localize_diff[name] << { field: field.name, localized: field.localized? }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Apply the modifications collected from the custom fields by
|
163
|
+
# updating all the documents of the relation.
|
164
|
+
# The update uses the power of mongodb to make it fully optimized.
|
165
|
+
#
|
166
|
+
# @param [ String, Symbol ] name The name of the relation.
|
167
|
+
#
|
168
|
+
def apply_custom_fields_diff(name)
|
169
|
+
# puts "==> apply_custom_fields_recipes for #{name}, #{self._custom_fields_diff[name].inspect}" # DEBUG
|
170
|
+
|
171
|
+
operations = self._custom_fields_diff[name]
|
172
|
+
operations['$set'].merge!({ 'custom_fields_recipe.version' => self.custom_fields_version(name) })
|
173
|
+
collection, selector = self.send(name).collection, self.send(name).criteria.selector
|
174
|
+
|
175
|
+
# puts "selector = #{selector.inspect}, memo = #{attributes.inspect}" # DEBUG
|
176
|
+
|
177
|
+
collection.find(selector).update operations, multi: true
|
178
|
+
end
|
179
|
+
|
180
|
+
# If the localized attribute has been changed in at least one of the custom fields,
|
181
|
+
# we have to upgrade all the records enhanced by custom_fields in order to make
|
182
|
+
# the values consistent with the mongoid localize option.
|
183
|
+
#
|
184
|
+
# Ex: post.attributes[:name] = 'Hello world' => post.attributes[:name] = { en: 'Hello world' }
|
185
|
+
#
|
186
|
+
# @param [ String, Symbol ] name The name of the relation.
|
187
|
+
#
|
188
|
+
def apply_custom_fields_localize_diff(name)
|
189
|
+
return if self._custom_field_localize_diff[name].empty?
|
190
|
+
|
191
|
+
self.send(name).all.each do |record|
|
192
|
+
updates = {}
|
193
|
+
|
194
|
+
# puts "[apply_custom_fields_localize_diff] processing: record #{record._id} / #{self._custom_field_localize_diff[name].inspect}" # DEBUG
|
195
|
+
self._custom_field_localize_diff[name].each do |changes|
|
196
|
+
if changes[:localized]
|
197
|
+
value = record.read_attribute(changes[:field].to_sym)
|
198
|
+
updates[changes[:field]] = { Mongoid::Fields::I18n.locale.to_s => value }
|
199
|
+
else
|
200
|
+
# the other way around
|
201
|
+
value = record.read_attribute(changes[:field].to_sym)
|
202
|
+
next if value.nil?
|
203
|
+
updates[changes[:field]] = value[Mongoid::Fields::I18n.locale.to_s]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
next if updates.empty?
|
208
|
+
|
209
|
+
collection = self.send(name).collection
|
210
|
+
collection.find(record.atomic_selector).update({ '$set' => updates })
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
module ClassMethods
|
215
|
+
|
216
|
+
# Determines if the relation is enhanced by the custom fields
|
217
|
+
#
|
218
|
+
# @example the Person class has somewhere in its code this: "custom_fields_for :addresses"
|
219
|
+
# Person.custom_fields_for?(:addresses)
|
220
|
+
#
|
221
|
+
# @param [ String, Symbol ] name The name of the relation.
|
222
|
+
#
|
223
|
+
# @return [ true, false ] True if enhanced, false if not.
|
224
|
+
#
|
225
|
+
def custom_fields_for?(name)
|
226
|
+
self._custom_fields_for.include?(name.to_s)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Enhance a referenced collection OR the instance itself (by passing self) by providing methods to manage custom fields.
|
230
|
+
#
|
231
|
+
# @param [ String, Symbol ] name The name of the relation.
|
232
|
+
#
|
233
|
+
# @example
|
234
|
+
# class Company
|
235
|
+
# embeds_many :employees
|
236
|
+
# custom_fields_for :employees
|
237
|
+
# end
|
238
|
+
#
|
239
|
+
# class Employee
|
240
|
+
# embedded_in :company, inverse_of: :employees
|
241
|
+
# field :name, String
|
242
|
+
# end
|
243
|
+
#
|
244
|
+
# company.employees_custom_fields.build label: 'His/her position', name: 'position', kind: 'string'
|
245
|
+
# company.save
|
246
|
+
# company.employees.build name: 'Michael Scott', position: 'Regional manager'
|
247
|
+
#
|
248
|
+
def custom_fields_for(name)
|
249
|
+
self.declare_embedded_in_definition_in_custom_field(name)
|
250
|
+
|
251
|
+
# stores the relation name
|
252
|
+
self._custom_fields_for << name.to_s
|
253
|
+
|
254
|
+
self.extend_for_custom_fields(name)
|
255
|
+
end
|
256
|
+
|
257
|
+
protected
|
258
|
+
|
259
|
+
# Extends / Decorates the current class in order to be fully custom_fields compliant.
|
260
|
+
# it declares news fields, adds new callbacks, ...etc
|
261
|
+
#
|
262
|
+
# @param [ String, Symbol ] name The name of the relation.
|
263
|
+
#
|
264
|
+
def extend_for_custom_fields(name)
|
265
|
+
class_eval do
|
266
|
+
field :"#{name}_custom_fields_version", type: ::Integer, default: 0
|
267
|
+
|
268
|
+
embeds_many :"#{name}_custom_fields", class_name: self.dynamic_custom_field_class_name(name) #, cascade_callbacks: true # FIXME ?????
|
269
|
+
|
270
|
+
accepts_nested_attributes_for :"#{name}_custom_fields", allow_destroy: true
|
271
|
+
end
|
272
|
+
|
273
|
+
class_eval <<-EOV
|
274
|
+
after_initialize :refresh_#{name}_metadata
|
275
|
+
before_update :bump_#{name}_custom_fields_version
|
276
|
+
before_update :collect_#{name}_custom_fields_diff
|
277
|
+
after_update :apply_#{name}_custom_fields_diff
|
278
|
+
after_update :apply_#{name}_custom_fields_localize_diff
|
279
|
+
|
280
|
+
def ordered_#{name}_custom_fields
|
281
|
+
self.ordered_custom_fields('#{name}')
|
282
|
+
end
|
283
|
+
|
284
|
+
protected
|
285
|
+
|
286
|
+
def refresh_#{name}_metadata
|
287
|
+
self.refresh_metadata_with_custom_fields('#{name}')
|
288
|
+
end
|
289
|
+
|
290
|
+
def bump_#{name}_custom_fields_version
|
291
|
+
self.bump_custom_fields_version('#{name}')
|
292
|
+
end
|
293
|
+
|
294
|
+
def collect_#{name}_custom_fields_diff
|
295
|
+
self.collect_custom_fields_diff(:#{name}, self.#{name}_custom_fields)
|
296
|
+
end
|
297
|
+
|
298
|
+
def apply_#{name}_custom_fields_diff
|
299
|
+
self.apply_custom_fields_diff(:#{name})
|
300
|
+
end
|
301
|
+
|
302
|
+
def apply_#{name}_custom_fields_localize_diff
|
303
|
+
self.apply_custom_fields_localize_diff(:#{name})
|
304
|
+
end
|
305
|
+
|
306
|
+
EOV
|
307
|
+
end
|
308
|
+
|
309
|
+
# Returns the class name of the custom field which is based both on the parent class name
|
310
|
+
# and the name of the relation in order to avoid name conflicts (with other classes)
|
311
|
+
#
|
312
|
+
# @param [ Metadata ] metadata The relation's old metadata.
|
313
|
+
#
|
314
|
+
# @return [ String ] The class name
|
315
|
+
#
|
316
|
+
def dynamic_custom_field_class_name(name)
|
317
|
+
"#{self.name}#{name.to_s.singularize.camelize}Field"
|
318
|
+
end
|
319
|
+
|
320
|
+
# An embedded relationship has to be defined on both side in order for it
|
321
|
+
# to work properly. But because custom_field can be embedded in different
|
322
|
+
# models that it's not aware of, we have to declare manually the definition
|
323
|
+
# once we know the target class.
|
324
|
+
#
|
325
|
+
# @param [ String, Symbol ] name The name of the relation.
|
326
|
+
#
|
327
|
+
# @return [ Field ] The new field class.
|
328
|
+
#
|
329
|
+
def declare_embedded_in_definition_in_custom_field(name)
|
330
|
+
klass_name = self.dynamic_custom_field_class_name(name).split('::').last # Use only the class, ignore the modules
|
331
|
+
|
332
|
+
source = self.parents.size > 1 ? self.parents.first : Object
|
333
|
+
|
334
|
+
unless source.const_defined?(klass_name)
|
335
|
+
(klass = Class.new(::CustomFields::Field)).class_eval <<-EOF
|
336
|
+
embedded_in :#{self.name.demodulize.underscore}, inverse_of: :#{name}_custom_fields, class_name: '#{self.name}'
|
337
|
+
EOF
|
338
|
+
|
339
|
+
source.const_set(klass_name, klass)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
end
|
344
|
+
|
345
|
+
end
|
346
|
+
|
347
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module CustomFields
|
2
|
+
|
3
|
+
module Target
|
4
|
+
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
|
9
|
+
## types ##
|
10
|
+
%w(default string text email date date_time boolean file select float integer money
|
11
|
+
belongs_to has_many many_to_many tags).each do |type|
|
12
|
+
include "CustomFields::Types::#{type.camelize}::Target".constantize
|
13
|
+
end
|
14
|
+
|
15
|
+
include ::CustomFields::TargetHelpers
|
16
|
+
|
17
|
+
## fields ##
|
18
|
+
field :custom_fields_recipe, type: Hash
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
# A document with custom fields always returns true.
|
24
|
+
#
|
25
|
+
# @return [ Boolean ] True
|
26
|
+
#
|
27
|
+
def with_custom_fields?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
# Builds the custom klass by sub-classing it
|
32
|
+
# from its parent and by applying a recipe
|
33
|
+
#
|
34
|
+
# @param [ Hash ] recipe The recipe describing the fields to add
|
35
|
+
#
|
36
|
+
# @return [ Class ] the anonymous custom klass
|
37
|
+
#
|
38
|
+
def build_klass_with_custom_fields(recipe)
|
39
|
+
name = recipe['name']
|
40
|
+
# puts "CREATING #{name}, #{recipe.inspect}" # DEBUG
|
41
|
+
parent.const_set(name, Class.new(self)).tap do |klass|
|
42
|
+
klass.cattr_accessor :version
|
43
|
+
|
44
|
+
klass.version = recipe['version']
|
45
|
+
|
46
|
+
# copy scopes from the parent class (scopes does not inherit automatically from the parents in mongoid)
|
47
|
+
# FIXME (Did): not needed anymore ?
|
48
|
+
# klass.write_inheritable_attribute(:scopes, self.scopes)
|
49
|
+
|
50
|
+
recipe['rules'].each do |rule|
|
51
|
+
self.send(:"apply_#{rule['type']}_custom_field", klass, rule)
|
52
|
+
end
|
53
|
+
recipe_model_name = recipe['model_name']
|
54
|
+
model_name = Proc.new do
|
55
|
+
if recipe_model_name.is_a?(ActiveModel::Name)
|
56
|
+
recipe_model_name
|
57
|
+
else
|
58
|
+
recipe_model_name.constantize.model_name
|
59
|
+
end
|
60
|
+
end
|
61
|
+
klass.send :define_method, :model_name, model_name
|
62
|
+
klass.send :define_singleton_method, :model_name, model_name
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns a custom klass always up-to-date. If it does not
|
67
|
+
# exist or if the version is out-dates then build a new custom klass.
|
68
|
+
# The recipe also contains the name which will be assigned to the
|
69
|
+
# custom klass.
|
70
|
+
#
|
71
|
+
# @param [ Hash ] recipe The recipe describing the fields to add
|
72
|
+
#
|
73
|
+
# @return [ Class ] the custom klass
|
74
|
+
#
|
75
|
+
def klass_with_custom_fields(recipe)
|
76
|
+
return self if recipe.blank? # no recipe provided
|
77
|
+
|
78
|
+
name = recipe['name']
|
79
|
+
|
80
|
+
(modules = self.name.split('::')).pop
|
81
|
+
|
82
|
+
parent = modules.empty? ? Object : modules.join('::').constantize
|
83
|
+
|
84
|
+
klass = parent.const_defined?(name) ? parent.const_get(name) : nil
|
85
|
+
|
86
|
+
if klass.nil? || klass.version != recipe['version'] # no klass or out-dated klass
|
87
|
+
parent.send(:remove_const, name) if klass
|
88
|
+
|
89
|
+
klass = build_klass_with_custom_fields(recipe)
|
90
|
+
end
|
91
|
+
|
92
|
+
klass
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module CustomFields
|
2
|
+
|
3
|
+
module TargetHelpers
|
4
|
+
|
5
|
+
# Return the list of the getters dynamically based on the
|
6
|
+
# custom_fields recipe in order to get the formatted values
|
7
|
+
# of the custom fields.
|
8
|
+
# If a block is passed, then the list will be filtered accordingly with
|
9
|
+
# the following logic. If the block is evaluated as true, then the method
|
10
|
+
# will be kept in the list, otherwise it will be removed.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# # keep all the methods except for the field named 'foo'
|
14
|
+
# project.custom_fields_methods do |rule|
|
15
|
+
# rule['name] != 'foo'
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# @return [ List ] a list of method names (string)
|
19
|
+
#
|
20
|
+
def custom_fields_methods(&filter)
|
21
|
+
self.custom_fields_recipe['rules'].map do |rule|
|
22
|
+
method = self.custom_fields_getters_for rule['name'], rule['type']
|
23
|
+
if block_given?
|
24
|
+
filter.call(rule) ? method : nil
|
25
|
+
else
|
26
|
+
method
|
27
|
+
end
|
28
|
+
end.compact.flatten
|
29
|
+
end
|
30
|
+
|
31
|
+
# List all the setters that are used by the custom_fields
|
32
|
+
# in order to get updated thru a html form for instance.
|
33
|
+
#
|
34
|
+
# @return [ List ] a list of method names (string)
|
35
|
+
#
|
36
|
+
def custom_fields_safe_setters
|
37
|
+
self.custom_fields_recipe['rules'].map do |rule|
|
38
|
+
case rule['type'].to_sym
|
39
|
+
when :date, :date_time, :money then "formatted_#{rule['name']}"
|
40
|
+
when :file then [rule['name'], "remove_#{rule['name']}"]
|
41
|
+
when :select, :belongs_to then ["#{rule['name']}_id", "position_in_#{rule['name']}"]
|
42
|
+
when :has_many, :many_to_many then nil
|
43
|
+
else
|
44
|
+
rule['name']
|
45
|
+
end
|
46
|
+
end.compact.flatten
|
47
|
+
end
|
48
|
+
|
49
|
+
# Build a hash for all the non-relationship fields
|
50
|
+
# meaning string, text, date, boolean, select, file types.
|
51
|
+
# This hash stores their name and their value.
|
52
|
+
#
|
53
|
+
# @return [ Hash ] Field name / formatted value
|
54
|
+
#
|
55
|
+
def custom_fields_basic_attributes
|
56
|
+
{}.tap do |hash|
|
57
|
+
self.non_relationship_custom_fields.each do |rule|
|
58
|
+
name, type = rule['name'], rule['type'].to_sym
|
59
|
+
|
60
|
+
# method of the custom getter
|
61
|
+
method_name = "#{type}_attribute_get"
|
62
|
+
|
63
|
+
hash.merge!(self.class.send(method_name, self, name))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Set the values (and their related fields) for all the non-relationship fields
|
69
|
+
# meaning string, text, date, boolean, select, file types.
|
70
|
+
#
|
71
|
+
# @param [ Hash ] The attributes for the custom fields and their related fields.
|
72
|
+
#
|
73
|
+
def custom_fields_basic_attributes=(attributes)
|
74
|
+
self.non_relationship_custom_fields.each do |rule|
|
75
|
+
name, type = rule['name'], rule['type'].to_sym
|
76
|
+
|
77
|
+
# method of the custom getter
|
78
|
+
method_name = "#{type}_attribute_set"
|
79
|
+
|
80
|
+
self.class.send(method_name, self, name, attributes)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Check if the rule defined by the name is a "many" relationship kind.
|
85
|
+
# A "many" relationship includes "has_many" and "many_to_many"
|
86
|
+
#
|
87
|
+
# @param [ String ] name The name of the rule
|
88
|
+
#
|
89
|
+
# @return [ Boolean ] True if the rule is a "many" relationship kind.
|
90
|
+
#
|
91
|
+
def is_a_custom_field_many_relationship?(name)
|
92
|
+
rule = self.custom_fields_recipe['rules'].detect do |rule|
|
93
|
+
rule['name'] == name && _custom_field_many_relationship?(rule['type'])
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return the rules of the custom fields which do not describe a relationship.
|
98
|
+
#
|
99
|
+
# @return [ Array ] List of rules (Hash)
|
100
|
+
#
|
101
|
+
def non_relationship_custom_fields
|
102
|
+
self.custom_fields_recipe['rules'].find_all do |rule|
|
103
|
+
!%w(belongs_to has_many many_to_many).include?(rule['type'])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Return the rules of the custom fields which describe a relationship.
|
108
|
+
#
|
109
|
+
# @return [ Array ] List of rules (Hash)
|
110
|
+
#
|
111
|
+
def relationship_custom_fields
|
112
|
+
self.custom_fields_recipe['rules'].find_all do |rule|
|
113
|
+
%w(belongs_to has_many many_to_many).include?(rule['type'])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Return the names of all the select fields of this object
|
118
|
+
def select_custom_fields
|
119
|
+
group_custom_fields 'select'
|
120
|
+
end
|
121
|
+
|
122
|
+
# Return the names of all the file custom_fields of this object
|
123
|
+
#
|
124
|
+
# @return [ Array ] List of names
|
125
|
+
#
|
126
|
+
def file_custom_fields
|
127
|
+
group_custom_fields 'file'
|
128
|
+
end
|
129
|
+
|
130
|
+
# Return the names of all the belongs_to custom_fields of this object
|
131
|
+
#
|
132
|
+
# @return [ Array ] List of names
|
133
|
+
#
|
134
|
+
def belongs_to_custom_fields
|
135
|
+
group_custom_fields 'belongs_to'
|
136
|
+
end
|
137
|
+
|
138
|
+
# Return the names of all the has_many custom_fields of this object
|
139
|
+
#
|
140
|
+
# @return [ Array ] Array of array [name, inverse_of]
|
141
|
+
#
|
142
|
+
def has_many_custom_fields
|
143
|
+
group_custom_fields('has_many') { |rule| [rule['name'], rule['inverse_of']] }
|
144
|
+
end
|
145
|
+
|
146
|
+
# Return the names of all the many_to_many custom_fields of this object.
|
147
|
+
# It also adds the property used to set/get the target ids.
|
148
|
+
#
|
149
|
+
# @return [ Array ] Array of array [name, <name in singular>_ids]
|
150
|
+
#
|
151
|
+
def many_to_many_custom_fields
|
152
|
+
group_custom_fields('many_to_many') { |rule| [rule['name'], "#{rule['name'].singularize}_ids"] }
|
153
|
+
end
|
154
|
+
|
155
|
+
protected
|
156
|
+
|
157
|
+
# Get the names of the getter methods for a field.
|
158
|
+
# The names depend on the field type.
|
159
|
+
#
|
160
|
+
# @param [ String ] name Name of the field
|
161
|
+
# @param [ String ] type Type of the field
|
162
|
+
#
|
163
|
+
# @return [ Object ] A string or an array of names
|
164
|
+
#
|
165
|
+
def custom_fields_getters_for(name, type)
|
166
|
+
case type.to_sym
|
167
|
+
when :select then [name, "#{name}_id"]
|
168
|
+
when :date, :date_time, :money then "formatted_#{name}"
|
169
|
+
when :file then "#{name}_url"
|
170
|
+
when :belongs_to then "#{name}_id"
|
171
|
+
else
|
172
|
+
name
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
#:nodoc:
|
177
|
+
def _custom_field_many_relationship?(type)
|
178
|
+
%w(has_many many_to_many).include?(type)
|
179
|
+
end
|
180
|
+
|
181
|
+
#:nodoc:
|
182
|
+
def group_custom_fields(type, &block)
|
183
|
+
unless block_given?
|
184
|
+
block = lambda { |rule| rule['name'] }
|
185
|
+
end
|
186
|
+
|
187
|
+
self.custom_fields_recipe['rules'].find_all { |rule| rule['type'] == type }.map(&block)
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module CustomFields
|
2
|
+
|
3
|
+
module Types
|
4
|
+
|
5
|
+
module BelongsTo
|
6
|
+
|
7
|
+
module Field
|
8
|
+
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
|
13
|
+
def belongs_to_to_recipe
|
14
|
+
{ 'class_name' => self.class_name }
|
15
|
+
end
|
16
|
+
|
17
|
+
def belongs_to_is_relationship?
|
18
|
+
self.type == 'belongs_to'
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
module Target
|
26
|
+
|
27
|
+
extend ActiveSupport::Concern
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
|
31
|
+
# Adds a belongs_to relationship between 2 models
|
32
|
+
#
|
33
|
+
# @param [ Class ] klass The class to modify
|
34
|
+
# @param [ Hash ] rule It contains the name of the field and if it is required or not
|
35
|
+
#
|
36
|
+
def apply_belongs_to_custom_field(klass, rule)
|
37
|
+
# puts "#{klass.inspect}.belongs_to #{rule['name'].inspect}, class_name: #{rule['class_name'].inspect}" # DEBUG
|
38
|
+
|
39
|
+
position_name = "position_in_#{rule['name'].underscore}"
|
40
|
+
|
41
|
+
# puts "#{klass.inspect}.field :#{position_name}" # DEBUG
|
42
|
+
|
43
|
+
klass.field position_name, type: ::Integer, default: 0
|
44
|
+
|
45
|
+
klass.belongs_to rule['name'].to_sym, class_name: rule['class_name']
|
46
|
+
|
47
|
+
if rule['required']
|
48
|
+
klass.validates_presence_of rule['name'].to_sym
|
49
|
+
end
|
50
|
+
|
51
|
+
klass.before_create do |object|
|
52
|
+
position = (object.class.max(position_name.to_sym) || 0) + 1
|
53
|
+
object.send(:"#{position_name}=", position)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|