custom_fields 1.1.0.rc1 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|