gnuside-custom_fields 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.textile +70 -0
  4. data/config/locales/de.yml +15 -0
  5. data/config/locales/en.yml +21 -0
  6. data/config/locales/fr.yml +25 -0
  7. data/config/locales/pt-BR.yml +9 -0
  8. data/config/locales/ru.yml +15 -0
  9. data/lib/custom_fields/extensions/active_support.rb +28 -0
  10. data/lib/custom_fields/extensions/carrierwave.rb +25 -0
  11. data/lib/custom_fields/extensions/mongoid/document.rb +21 -0
  12. data/lib/custom_fields/extensions/mongoid/factory.rb +20 -0
  13. data/lib/custom_fields/extensions/mongoid/fields/i18n.rb +55 -0
  14. data/lib/custom_fields/extensions/mongoid/fields/localized.rb +39 -0
  15. data/lib/custom_fields/extensions/mongoid/fields.rb +31 -0
  16. data/lib/custom_fields/extensions/mongoid/relations/referenced/in.rb +22 -0
  17. data/lib/custom_fields/extensions/mongoid/relations/referenced/many.rb +34 -0
  18. data/lib/custom_fields/extensions/mongoid/validations/collection_size.rb +43 -0
  19. data/lib/custom_fields/extensions/mongoid/validations/macros.rb +25 -0
  20. data/lib/custom_fields/extensions/origin/smash.rb +33 -0
  21. data/lib/custom_fields/field.rb +106 -0
  22. data/lib/custom_fields/source.rb +347 -0
  23. data/lib/custom_fields/target.rb +99 -0
  24. data/lib/custom_fields/target_helpers.rb +192 -0
  25. data/lib/custom_fields/types/belongs_to.rb +65 -0
  26. data/lib/custom_fields/types/boolean.rb +55 -0
  27. data/lib/custom_fields/types/date.rb +97 -0
  28. data/lib/custom_fields/types/date_time.rb +97 -0
  29. data/lib/custom_fields/types/default.rb +103 -0
  30. data/lib/custom_fields/types/email.rb +60 -0
  31. data/lib/custom_fields/types/file.rb +74 -0
  32. data/lib/custom_fields/types/float.rb +52 -0
  33. data/lib/custom_fields/types/has_many.rb +74 -0
  34. data/lib/custom_fields/types/integer.rb +54 -0
  35. data/lib/custom_fields/types/many_to_many.rb +75 -0
  36. data/lib/custom_fields/types/money.rb +146 -0
  37. data/lib/custom_fields/types/relationship_default.rb +44 -0
  38. data/lib/custom_fields/types/select.rb +217 -0
  39. data/lib/custom_fields/types/string.rb +55 -0
  40. data/lib/custom_fields/types/tags.rb +35 -0
  41. data/lib/custom_fields/types/text.rb +65 -0
  42. data/lib/custom_fields/version.rb +6 -0
  43. data/lib/custom_fields.rb +74 -0
  44. 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