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.
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