custom_fields 1.1.0.rc1 → 2.0.0.rc1

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 (41) hide show
  1. data/MIT-LICENSE +1 -1
  2. data/README.textile +14 -6
  3. data/config/locales/fr.yml +5 -1
  4. data/lib/custom_fields.rb +11 -37
  5. data/lib/custom_fields/extensions/carrierwave.rb +25 -0
  6. data/lib/custom_fields/extensions/mongoid/document.rb +12 -50
  7. data/lib/custom_fields/extensions/mongoid/factory.rb +20 -0
  8. data/lib/custom_fields/extensions/mongoid/fields.rb +29 -0
  9. data/lib/custom_fields/extensions/mongoid/fields/i18n.rb +53 -0
  10. data/lib/custom_fields/extensions/mongoid/fields/internal/localized.rb +84 -0
  11. data/lib/custom_fields/extensions/mongoid/relations/referenced/many.rb +29 -0
  12. data/lib/custom_fields/field.rb +44 -175
  13. data/lib/custom_fields/source.rb +333 -0
  14. data/lib/custom_fields/target.rb +90 -0
  15. data/lib/custom_fields/types/boolean.rb +26 -3
  16. data/lib/custom_fields/types/date.rb +36 -24
  17. data/lib/custom_fields/types/default.rb +44 -24
  18. data/lib/custom_fields/types/file.rb +35 -17
  19. data/lib/custom_fields/types/select.rb +184 -0
  20. data/lib/custom_fields/types/string.rb +25 -6
  21. data/lib/custom_fields/types/text.rb +35 -8
  22. data/lib/custom_fields/types_old/boolean.rb +13 -0
  23. data/lib/custom_fields/{types → types_old}/category.rb +0 -0
  24. data/lib/custom_fields/types_old/date.rb +49 -0
  25. data/lib/custom_fields/types_old/default.rb +44 -0
  26. data/lib/custom_fields/types_old/file.rb +27 -0
  27. data/lib/custom_fields/{types → types_old}/has_many.rb +0 -0
  28. data/lib/custom_fields/{types → types_old}/has_many/proxy_collection.rb +0 -0
  29. data/lib/custom_fields/{types → types_old}/has_many/reverse_lookup_proxy_collection.rb +0 -0
  30. data/lib/custom_fields/{types → types_old}/has_one.rb +0 -0
  31. data/lib/custom_fields/types_old/string.rb +13 -0
  32. data/lib/custom_fields/types_old/text.rb +15 -0
  33. data/lib/custom_fields/version.rb +1 -1
  34. metadata +115 -91
  35. data/lib/custom_fields/custom_fields_for.rb +0 -350
  36. data/lib/custom_fields/extensions/mongoid/relations/accessors.rb +0 -31
  37. data/lib/custom_fields/extensions/mongoid/relations/builders.rb +0 -30
  38. data/lib/custom_fields/proxy_class/base.rb +0 -112
  39. data/lib/custom_fields/proxy_class/builder.rb +0 -60
  40. data/lib/custom_fields/proxy_class/helper.rb +0 -57
  41. data/lib/custom_fields/self_metadata.rb +0 -30
@@ -1,228 +1,97 @@
1
1
  module CustomFields
2
2
 
3
3
  class Field
4
+
4
5
  include ::Mongoid::Document
5
6
  include ::Mongoid::Timestamps
6
7
 
7
- # types ##
8
- include Types::Default
9
- include Types::String
10
- include Types::Text
11
- include Types::Category
12
- include Types::Boolean
13
- include Types::Date
14
- include Types::File
15
- include Types::HasOne
16
- include Types::HasMany
8
+ ## types ##
9
+ %w(default string text date boolean file select).each do |type|
10
+ include "CustomFields::Types::#{type.classify}::Field".constantize
11
+ end
17
12
 
18
13
  ## fields ##
19
14
  field :label
20
- field :_alias
21
- field :_name
22
- field :kind
15
+ field :name
16
+ field :type
23
17
  field :hint
24
- field :position, :type => Integer, :default => 0
25
- field :required, :type => Boolean, :default => false
18
+ field :position, :type => Integer, :default => 0
19
+ field :required, :type => Boolean, :default => false
20
+ field :localized, :type => Boolean, :default => false
26
21
 
27
22
  ## validations ##
28
- validates_presence_of :label, :kind
29
- validates_exclusion_of :_alias, :in => lambda { |f| CustomFields.options[:reserved_aliases].map(&:to_s) }
30
- validates_format_of :_alias, :with => /^[a-z]([A-Za-z0-9_]+)?$/
31
- validate :uniqueness_of_label_and_alias
32
-
33
- ## other accessors ##
34
- attr_accessor :parentized_done # for performance purpose
23
+ validates_presence_of :label, :type
24
+ validates_exclusion_of :name, :in => lambda { |f| CustomFields.options[:reserved_names].map(&:to_s) }
25
+ validates_format_of :name, :with => /^[a-z]([A-Za-z0-9_]+)?$/
26
+ validate :uniqueness_of_label_and_name
35
27
 
36
28
  ## callbacks ##
37
- before_validation :set_alias
38
- before_save :invalidate_proxy_klass
29
+ before_validation :set_name
39
30
 
40
31
  ## methods ##
41
32
 
42
- # Returns the type class related to this field
33
+ # Builds the mongodb updates based on
34
+ # the new state of the field.
35
+ # Call a different method if the field has a different behaviour.
43
36
  #
44
- # @return [ Class ] The class defining the field type
37
+ # @param [ Hash ] memo Store the updates
45
38
  #
46
- def field_type
47
- self.class.field_types[self.safe_kind.to_sym]
48
- end
49
-
50
- # Enhance a document class by applying to it the information stored
51
- # in the type related to this field
52
- #
53
- # @param [ Class ] klass The document class
39
+ # @return [ Hash ] The memo object upgraded
54
40
  #
55
- def apply(klass)
56
- klass.field self._name, :type => self.field_type if self.field_type
57
-
58
- apply_method_name = :"apply_#{self.safe_kind}_type"
41
+ def collect_diff(memo)
42
+ method_name = :"collect_#{self.type}_diff"
59
43
 
60
- if self.respond_to?(apply_method_name)
61
- self.send(apply_method_name, klass)
44
+ if self.respond_to?(method_name)
45
+ self.send(method_name, memo)
62
46
  else
63
- apply_default_type(klass)
64
- end
65
-
66
- validation_method_name = :"add_#{self.safe_kind}_validation"
67
-
68
- if self.respond_to?(validation_method_name)
69
- self.send(validation_method_name, klass)
70
- else
71
- add_default_validation(klass)
47
+ collect_default_diff(memo)
72
48
  end
73
49
  end
74
50
 
75
- # Make sure it returns a valid alias
51
+ # Returns the information (name, type, required, ...etc) needed to build
52
+ # the custom class.
53
+ # That will be stored into the target instance.
76
54
  #
77
- # @return [ String ] the alias
55
+ # @return [ Hash ] The hash
78
56
  #
79
- def safe_alias
80
- self.set_alias
81
- self._alias
82
- end
83
-
84
- # Returns the kind (or type) of the current field.
85
- # Because of compatibility purpose, prior version of CustomFields used to have the value of kind in uppercase.
86
- #
87
- # @return [ String ] the kind
88
- #
89
- def safe_kind
90
- self.kind.downcase
91
- end
92
-
93
- # Returns the name of the relation binding this field and the custom_fields in the parent class
94
- #
95
- # @example:
96
- # class Company
97
- # embeds_many :employees
98
- # custom_fields_for :employees
99
- # end
100
- #
101
- # field = company.employees_custom_fields.build :label => 'His/her position', :_alias => 'position', :kind => 'string'
102
- # field.custom_fields_relation_name == 'employees'
103
- #
104
- # @return [ String ] the relation's name
105
- #
106
- def custom_fields_relation_name
107
- self.metadata.name.to_s.gsub('_custom_fields', '')
108
- end
109
-
110
- # Checks if the field is valid without running the callback which marks
111
- # the proxy class as invalidated
112
- #
113
- # @return [ Boolean ] true if the field has no errors, false otherwise
114
- def quick_valid?
115
- # true
116
- # CustomFields::Field.without_callback(:validation, :after, :invalidate_proxy_klass) do
117
- CustomFields::Field.without_callback(:save, :before, :invalidate_proxy_klass) do
118
- self.valid?
119
- end
120
- end
57
+ def to_recipe
58
+ method_name = :"#{self.type}_to_recipe"
59
+ custom_to_recipe = self.send(method_name) rescue {}
121
60
 
122
- # Destroys a field and saves the parent too in order to keep track of the changes
123
- # for the parent (for instance, the version has to be bumped).
124
- #
125
- # @param [ Hash ] options Options to pass to destroy.
126
- #
127
- # @return [ true, false ] True if successful, false if not.
128
- #
129
- def destroy(options = {})
130
- super.tap do
131
- self.mark_proxy_klass_flag_as_invalidated
132
- self._parent.save
133
- end
61
+ { 'name' => self.name, 'type' => self.type, 'required' => self.required?, 'localized' => self.localized? }.merge(custom_to_recipe)
134
62
  end
135
63
 
136
- # Collects all the important attributes of this field.
137
- # It also accepts an extra hash which will be merged with
138
- # the one built by this method (by default, this is an empty hash)
139
- #
140
- # @param [ Hash ] more The extra hash
141
- #
142
- # @return [ Hash ] the hash
143
- #
144
- def to_hash(more = {})
145
- self.fields.keys.inject({}) do |memo, meth|
146
- memo[meth] = self.send(meth.to_sym); memo
147
- end.tap do |hash|
148
- self.class.field_types.keys.each do |type|
149
- if self.respond_to?(:"#{type}_to_hash")
150
- hash.merge!(self.send(:"#{type}_to_hash"))
151
- end
152
- end
153
- end.merge({
154
- 'id' => self._id,
155
- 'new_record' => self.new_record?,
156
- 'errors' => self.errors,
157
- 'kind_name' => I18n.t("custom_fields.kind.#{self.safe_kind}")
158
- }).merge(more)
159
- end
64
+ def as_json(options = {})
65
+ method_name = :"#{self.type}_as_json"
66
+ custom_as_json = self.send(method_name) rescue {}
160
67
 
161
- # Overides the default behaviour of the to_json method by using the to_hash method
162
- #
163
- # @return [ String ] the json object
164
- #
165
- def to_json
166
- ActiveSupport::JSON.encode(self.to_hash)
68
+ super(options).merge(custom_as_json)
167
69
  end
168
70
 
169
71
  protected
170
72
 
171
- def uniqueness_of_label_and_alias
73
+ def uniqueness_of_label_and_name
172
74
  if self.siblings.any? { |f| f.label == self.label && f._id != self._id }
173
75
  self.errors.add(:label, :taken)
174
76
  end
175
77
 
176
- if self.siblings.any? { |f| f._alias == self._alias && f._id != self._id }
177
- self.errors.add(:_alias, :taken)
78
+ if self.siblings.any? { |f| f.name == self.name && f._id != self._id }
79
+ self.errors.add(:name, :taken)
178
80
  end
179
81
  end
180
82
 
181
- def set_unique_name!
182
- self._name ||= "custom_field_#{self.increment_counter!}"
183
- end
184
-
185
- def set_alias
186
- return if self.label.blank? && self._alias.blank?
83
+ def set_name
84
+ return if self.label.blank? && self.name.blank?
187
85
 
188
- if self._alias.blank?
189
- self._alias = self.label.parameterize('_').gsub('-', '_').downcase
86
+ if self.name.blank?
87
+ self.name = self.label.parameterize('_').gsub('-', '_').downcase
190
88
  end
191
89
  end
192
90
 
193
- def increment_counter!
194
- name = self.custom_fields_relation_name
195
- self._parent.bump_custom_fields_counter(name)
196
- end
197
-
198
91
  def siblings
199
92
  self._parent.send(self.metadata.name)
200
93
  end
201
94
 
202
- def parentize_with_custom_fields(object)
203
- return if self.parentized_done
204
-
205
- parentize_without_custom_fields(object)
206
-
207
- self.send(:set_unique_name!)
208
-
209
- self.parentized_done = true
210
- end
211
- alias_method_chain :parentize, :custom_fields
212
-
213
- def invalidate_proxy_klass
214
- # puts "#{self._name} / #{self._alias} _ invalidate_proxy_klass !!! #{self.flagged_for_destroy?} / #{self.destroyed?}"
215
- if self.changed? || self.flagged_for_destroy?
216
- self.mark_proxy_klass_flag_as_invalidated
217
- end
218
- end
219
-
220
- def mark_proxy_klass_flag_as_invalidated
221
- # puts "\t*** [mark_proxy_klass_flag_as_invalidated] called for '#{self._name}'"
222
- name = self.custom_fields_relation_name
223
- self._parent.mark_klass_with_custom_fields_as_invalidated(name)
224
- end
225
-
226
95
  end
227
96
 
228
97
  end
@@ -0,0 +1,333 @@
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
+ module InstanceMethods
16
+
17
+ # Determines if the relation is enhanced by the custom fields
18
+ #
19
+ # @example the Person class has somewhere in its code this: "custom_fields_for :addresses"
20
+ # person.custom_fields_for?(:addresses)
21
+ #
22
+ # @param [ String, Symbol ] name The name of the relation.
23
+ #
24
+ # @return [ true, false ] True if enhanced, false if not.
25
+ #
26
+ def custom_fields_for?(name)
27
+ self.class.custom_fields_for?(name)
28
+ end
29
+
30
+ # Returns the class enhanced by the custom fields.
31
+ # Be careful, call this method only if the source class
32
+ # has been saved with success.
33
+ #
34
+ # @param [ String, Symbol ] name The name of the relation.
35
+ #
36
+ # @return [ Class ] The modified class.
37
+ #
38
+ def klass_with_custom_fields(name)
39
+ recipe = self.custom_fields_recipe_for(name)
40
+ target = self.send(name).metadata.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' => "#{name.to_s.classify}#{self._id}",
67
+ 'rules' => self.ordered_custom_fields(name).map(&:to_recipe),
68
+ 'version' => self.custom_fields_version(name)
69
+ }
70
+ end
71
+
72
+ # Returns the number of the version for relation with custom fields
73
+ #
74
+ # @param [ String, Symbol ] name The name of the relation.
75
+ #
76
+ # @return [ Integer ] The version number
77
+ #
78
+ def custom_fields_version(name)
79
+ self.send(:"#{name}_custom_fields_version") || 0
80
+ end
81
+
82
+ # When the fields have been modified and before the object is saved,
83
+ # we bump the version.
84
+ #
85
+ # @param [ String, Symbol ] name The name of the relation.
86
+ #
87
+ def bump_custom_fields_version(name)
88
+ version = self.custom_fields_version(name) + 1
89
+ self.send(:"#{name}_custom_fields_version=", version)
90
+ end
91
+
92
+ # Change the metadata of a relation enhanced by the custom fields.
93
+ # In Mongoid, all the instances of a same document share the same metadata objects.
94
+ #
95
+ # @param [ String, Symbol ] name The name of the relation.
96
+ #
97
+ def refresh_metadata_with_custom_fields(name)
98
+ return if !self.persisted? || self.send(:"#{name}_custom_fields").blank? # do not generate a klass without all the information
99
+
100
+ old_metadata = self.send(name).metadata
101
+
102
+ # puts "old_metadata = #{old_metadata.klass.inspect} / #{old_metadata.object_id.inspect}" # DEBUG
103
+
104
+ self.send(name).metadata = old_metadata.clone.tap do |metadata|
105
+ metadata.instance_variable_set(:@klass, self.klass_with_custom_fields(name))
106
+ end
107
+
108
+ # puts "new_metadata = #{self.send(name).metadata.klass.inspect} / #{self.send(name).metadata.object_id.inspect}" # DEBUG
109
+ end
110
+
111
+ # Initializes the object tracking the modifications
112
+ # of the custom fields
113
+ #
114
+ # @param [ String, Symbol ] name The name of the relation.
115
+ #
116
+ def initialize_custom_fields_diff(name)
117
+ self._custom_field_localize_diff ||= Hash.new([])
118
+
119
+ self._custom_fields_diff ||= {}
120
+ self._custom_fields_diff[name] = { '$set' => {}, '$unset' => {}, '$rename' => {} }
121
+ end
122
+
123
+ # Collects all the modifications of the custom fields
124
+ #
125
+ # @param [ String, Symbol ] name The name of the relation.
126
+ #
127
+ # @return [ Array ] An array of hashes storing the modifications
128
+ #
129
+ def collect_custom_fields_diff(name, fields)
130
+ # puts "==> collect_custom_fields_diff for #{name}, #{fields.size}" # DEBUG
131
+
132
+ memo = self.initialize_custom_fields_diff(name)
133
+
134
+ fields.map do |field|
135
+ field.collect_diff(memo)
136
+ end
137
+
138
+ # collect fields with a modified localized field
139
+ fields.each do |field|
140
+ if field.localized_changed? && field.persisted?
141
+ self._custom_field_localize_diff[name] << { :field => field.name, :localized => field.localized? }
142
+ end
143
+ end
144
+ end
145
+
146
+ # Apply the modifications collected from the custom fields by
147
+ # updating all the documents of the relation.
148
+ # The update uses the power of mongodb to make it fully optimized.
149
+ #
150
+ # @param [ String, Symbol ] name The name of the relation.
151
+ #
152
+ def apply_custom_fields_diff(name)
153
+ # puts "==> apply_custom_fields_recipes for #{name}, #{self._custom_fields_diff[name].inspect}" # DEBUG
154
+
155
+ operations = self._custom_fields_diff[name]
156
+ operations['$set'].merge!({ 'custom_fields_recipe.version' => self.custom_fields_version(name) })
157
+ collection, selector = self.send(name).collection, self.send(name).criteria.selector
158
+
159
+ # puts "selector = #{selector.inspect}, memo = #{attributes.inspect}" # DEBUG
160
+
161
+ collection.update selector, operations, :multi => true
162
+ end
163
+
164
+ # If the localized attribute has been changed in at least one of the custom fields,
165
+ # we have to upgrade all the records enhanced by custom_fields in order to make
166
+ # the values consistent with the mongoid localize option.
167
+ #
168
+ # Ex: post.attributes[:name] = 'Hello world' => post.attributes[:name] = { :en => 'Hello world' }
169
+ #
170
+ # @param [ String, Symbol ] name The name of the relation.
171
+ #
172
+ def apply_custom_fields_localize_diff(name)
173
+ return if self._custom_field_localize_diff[name].empty?
174
+
175
+ self.send(name).all.each do |record|
176
+ updates = {}
177
+
178
+ # puts "[apply_custom_fields_localize_diff] processing: record #{record._id} / #{self._custom_field_localize_diff[name].inspect}" # DEBUG
179
+ self._custom_field_localize_diff[name].each do |changes|
180
+ if changes[:localized]
181
+ value = record.read_attribute(changes[:field].to_sym)
182
+ updates[changes[:field]] = { I18n.locale.to_s => value }
183
+ else
184
+ # the other way around
185
+ value = record.read_attribute(changes[:field].to_sym)
186
+ next if value.nil?
187
+ updates[changes[:field]] = value[I18n.locale.to_s]
188
+ end
189
+ end
190
+
191
+ next if updates.empty?
192
+
193
+ collection = self.send(name).collection
194
+ collection.update record.atomic_selector, { '$set' => updates }
195
+ end
196
+ end
197
+
198
+ end
199
+
200
+ module ClassMethods
201
+
202
+ # Determines if the relation is enhanced by the custom fields
203
+ #
204
+ # @example the Person class has somewhere in its code this: "custom_fields_for :addresses"
205
+ # Person.custom_fields_for?(:addresses)
206
+ #
207
+ # @param [ String, Symbol ] name The name of the relation.
208
+ #
209
+ # @return [ true, false ] True if enhanced, false if not.
210
+ #
211
+ def custom_fields_for?(name)
212
+ self._custom_fields_for.include?(name.to_s)
213
+ end
214
+
215
+ # Enhance a referenced collection OR the instance itself (by passing self) by providing methods to manage custom fields.
216
+ #
217
+ # @param [ String, Symbol ] name The name of the relation.
218
+ #
219
+ # @example
220
+ # class Company
221
+ # embeds_many :employees
222
+ # custom_fields_for :employees
223
+ # end
224
+ #
225
+ # class Employee
226
+ # embedded_in :company, :inverse_of => :employees
227
+ # field :name, String
228
+ # end
229
+ #
230
+ # company.employees_custom_fields.build :label => 'His/her position', :name => 'position', :kind => 'string'
231
+ # company.save
232
+ # company.employees.build :name => 'Michael Scott', :position => 'Regional manager'
233
+ #
234
+ def custom_fields_for(name)
235
+ self.declare_embedded_in_definition_in_custom_field(name)
236
+
237
+ # stores the relation name
238
+ self._custom_fields_for << name.to_s
239
+
240
+ self.extend_for_custom_fields(name)
241
+ end
242
+
243
+ protected
244
+
245
+ # Extends / Decorates the current class in order to be fully custom_fields compliant.
246
+ # it declares news fields, adds new callbacks, ...etc
247
+ #
248
+ # @param [ String, Symbol ] name The name of the relation.
249
+ #
250
+ def extend_for_custom_fields(name)
251
+ class_eval do
252
+ field :"#{name}_custom_fields_version", :type => Integer, :default => 0
253
+
254
+ embeds_many :"#{name}_custom_fields", :class_name => self.dynamic_custom_field_class_name(name) #, :cascade_callbacks => true # FIXME ?????
255
+
256
+ accepts_nested_attributes_for :"#{name}_custom_fields", :allow_destroy => true
257
+ end
258
+
259
+ class_eval <<-EOV
260
+ after_initialize :refresh_#{name}_metadata
261
+ before_update :bump_#{name}_custom_fields_version
262
+ before_update :collect_#{name}_custom_fields_diff
263
+ after_update :apply_#{name}_custom_fields_diff
264
+ after_update :apply_#{name}_custom_fields_localize_diff
265
+
266
+ def ordered_#{name}_custom_fields
267
+ self.ordered_custom_fields('#{name}')
268
+ end
269
+
270
+ protected
271
+
272
+ def refresh_#{name}_metadata
273
+ self.refresh_metadata_with_custom_fields('#{name}')
274
+ end
275
+
276
+ def bump_#{name}_custom_fields_version
277
+ self.bump_custom_fields_version('#{name}')
278
+ end
279
+
280
+ def collect_#{name}_custom_fields_diff
281
+ self.collect_custom_fields_diff(:#{name}, self.#{name}_custom_fields)
282
+ end
283
+
284
+ def apply_#{name}_custom_fields_diff
285
+ self.apply_custom_fields_diff(:#{name})
286
+ end
287
+
288
+ def apply_#{name}_custom_fields_localize_diff
289
+ self.apply_custom_fields_localize_diff(:#{name})
290
+ end
291
+
292
+ EOV
293
+ end
294
+
295
+ # Returns the class name of the custom field which is based both on the parent class name
296
+ # and the name of the relation in order to avoid name conflicts (with other classes)
297
+ #
298
+ # @param [ Metadata ] metadata The relation's old metadata.
299
+ #
300
+ # @return [ String ] The class name
301
+ #
302
+ def dynamic_custom_field_class_name(name)
303
+ "#{self.name}#{name.to_s.singularize.camelize}Field"
304
+ end
305
+
306
+ # An embedded relationship has to be defined on both side in order for it
307
+ # to work properly. But because custom_field can be embedded in different
308
+ # models that it's not aware of, we have to declare manually the definition
309
+ # once we know the target class.
310
+ #
311
+ # @param [ String, Symbol ] name The name of the relation.
312
+ #
313
+ # @return [ Field ] The new field class.
314
+ #
315
+ def declare_embedded_in_definition_in_custom_field(name)
316
+ klass_name = self.dynamic_custom_field_class_name(name).split('::').last # Use only the class, ignore the modules
317
+
318
+ source = self.parents.size > 1 ? self.parents.first : Object
319
+
320
+ unless source.const_defined?(klass_name)
321
+ (klass = Class.new(::CustomFields::Field)).class_eval <<-EOF
322
+ embedded_in :#{self.name.demodulize.underscore}, :inverse_of => :#{name}_custom_fields, :class_name => '#{self.name}'
323
+ EOF
324
+
325
+ source.const_set(klass_name, klass)
326
+ end
327
+ end
328
+
329
+ end
330
+
331
+ end
332
+
333
+ end