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.
- data/MIT-LICENSE +1 -1
- data/README.textile +14 -6
- data/config/locales/fr.yml +5 -1
- data/lib/custom_fields.rb +11 -37
- data/lib/custom_fields/extensions/carrierwave.rb +25 -0
- data/lib/custom_fields/extensions/mongoid/document.rb +12 -50
- data/lib/custom_fields/extensions/mongoid/factory.rb +20 -0
- data/lib/custom_fields/extensions/mongoid/fields.rb +29 -0
- data/lib/custom_fields/extensions/mongoid/fields/i18n.rb +53 -0
- data/lib/custom_fields/extensions/mongoid/fields/internal/localized.rb +84 -0
- data/lib/custom_fields/extensions/mongoid/relations/referenced/many.rb +29 -0
- data/lib/custom_fields/field.rb +44 -175
- data/lib/custom_fields/source.rb +333 -0
- data/lib/custom_fields/target.rb +90 -0
- data/lib/custom_fields/types/boolean.rb +26 -3
- data/lib/custom_fields/types/date.rb +36 -24
- data/lib/custom_fields/types/default.rb +44 -24
- data/lib/custom_fields/types/file.rb +35 -17
- data/lib/custom_fields/types/select.rb +184 -0
- data/lib/custom_fields/types/string.rb +25 -6
- data/lib/custom_fields/types/text.rb +35 -8
- data/lib/custom_fields/types_old/boolean.rb +13 -0
- data/lib/custom_fields/{types → types_old}/category.rb +0 -0
- data/lib/custom_fields/types_old/date.rb +49 -0
- data/lib/custom_fields/types_old/default.rb +44 -0
- data/lib/custom_fields/types_old/file.rb +27 -0
- data/lib/custom_fields/{types → types_old}/has_many.rb +0 -0
- data/lib/custom_fields/{types → types_old}/has_many/proxy_collection.rb +0 -0
- data/lib/custom_fields/{types → types_old}/has_many/reverse_lookup_proxy_collection.rb +0 -0
- data/lib/custom_fields/{types → types_old}/has_one.rb +0 -0
- data/lib/custom_fields/types_old/string.rb +13 -0
- data/lib/custom_fields/types_old/text.rb +15 -0
- data/lib/custom_fields/version.rb +1 -1
- metadata +115 -91
- data/lib/custom_fields/custom_fields_for.rb +0 -350
- data/lib/custom_fields/extensions/mongoid/relations/accessors.rb +0 -31
- data/lib/custom_fields/extensions/mongoid/relations/builders.rb +0 -30
- data/lib/custom_fields/proxy_class/base.rb +0 -112
- data/lib/custom_fields/proxy_class/builder.rb +0 -60
- data/lib/custom_fields/proxy_class/helper.rb +0 -57
- data/lib/custom_fields/self_metadata.rb +0 -30
data/lib/custom_fields/field.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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 :
|
21
|
-
field :
|
22
|
-
field :kind
|
15
|
+
field :name
|
16
|
+
field :type
|
23
17
|
field :hint
|
24
|
-
field :position,
|
25
|
-
field :required,
|
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, :
|
29
|
-
validates_exclusion_of :
|
30
|
-
validates_format_of :
|
31
|
-
validate :
|
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 :
|
38
|
-
before_save :invalidate_proxy_klass
|
29
|
+
before_validation :set_name
|
39
30
|
|
40
31
|
## methods ##
|
41
32
|
|
42
|
-
#
|
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
|
-
# @
|
37
|
+
# @param [ Hash ] memo Store the updates
|
45
38
|
#
|
46
|
-
|
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
|
56
|
-
|
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?(
|
61
|
-
self.send(
|
44
|
+
if self.respond_to?(method_name)
|
45
|
+
self.send(method_name, memo)
|
62
46
|
else
|
63
|
-
|
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
|
-
#
|
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 [
|
55
|
+
# @return [ Hash ] The hash
|
78
56
|
#
|
79
|
-
def
|
80
|
-
self.
|
81
|
-
self.
|
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
|
-
|
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
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
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
|
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.
|
177
|
-
self.errors.add(:
|
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
|
182
|
-
self.
|
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.
|
189
|
-
self.
|
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
|