gnuside-custom_fields 2.3.1
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.
- 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
|