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
@@ -0,0 +1,90 @@
|
|
1
|
+
module CustomFields
|
2
|
+
|
3
|
+
module Target
|
4
|
+
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
|
9
|
+
## types ##
|
10
|
+
%w(default string text date boolean file select).each do |type|
|
11
|
+
include "CustomFields::Types::#{type.classify}::Target".constantize
|
12
|
+
end
|
13
|
+
|
14
|
+
## fields ##
|
15
|
+
field :custom_fields_recipe, :type => Hash
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
|
25
|
+
# A document with custom fields always returns true.
|
26
|
+
#
|
27
|
+
# @return [ Boolean ] True
|
28
|
+
#
|
29
|
+
def with_custom_fields?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
# Builds the custom klass by sub-classing it
|
34
|
+
# from its parent and by applying a recipe
|
35
|
+
#
|
36
|
+
# @param [ Hash ] recipe The recipe describing the fields to add
|
37
|
+
#
|
38
|
+
# @return [ Class] the anonymous custom klass
|
39
|
+
#
|
40
|
+
def build_klass_with_custom_fields(recipe)
|
41
|
+
# puts "CREATING new '#{name}' / #{recipe.inspect}" # DEBUG
|
42
|
+
Class.new(self).tap do |klass|
|
43
|
+
klass.cattr_accessor :version
|
44
|
+
|
45
|
+
klass.version = recipe['version']
|
46
|
+
|
47
|
+
# copy scopes from the parent class (scopes does not inherit automatically from the parents in mongoid)
|
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
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a custom klass always up-to-date. If it does not
|
57
|
+
# exist or if the version is out-dates then build a new custom klass.
|
58
|
+
# The recipe also contains the name which will be assigned to the
|
59
|
+
# custom klass.
|
60
|
+
#
|
61
|
+
# @param [ Hash ] recipe The recipe describing the fields to add
|
62
|
+
#
|
63
|
+
# @return [ Class ] the custom klass
|
64
|
+
#
|
65
|
+
def klass_with_custom_fields(recipe)
|
66
|
+
name = recipe['name']
|
67
|
+
|
68
|
+
(modules = self.name.split('::')).pop
|
69
|
+
|
70
|
+
parent = modules.empty? ? Object : modules.join('::').constantize
|
71
|
+
|
72
|
+
klass = parent.const_defined?(name) ? parent.const_get(name) : nil
|
73
|
+
|
74
|
+
if klass.nil? || klass.version != recipe['version'] # no klass or out-dated klass
|
75
|
+
parent.send(:remove_const, name) if klass
|
76
|
+
|
77
|
+
klass = build_klass_with_custom_fields(recipe)
|
78
|
+
|
79
|
+
parent.const_set(name, klass)
|
80
|
+
end
|
81
|
+
|
82
|
+
klass
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -1,13 +1,36 @@
|
|
1
1
|
module CustomFields
|
2
|
+
|
2
3
|
module Types
|
4
|
+
|
3
5
|
module Boolean
|
4
6
|
|
5
|
-
|
7
|
+
module Field; end
|
8
|
+
|
9
|
+
module Target
|
10
|
+
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
# Adds a boolean field
|
16
|
+
#
|
17
|
+
# @param [ Class ] klass The class to modify
|
18
|
+
# @param [ Hash ] rule It contains the name of the field and if it is required or not
|
19
|
+
#
|
20
|
+
def apply_boolean_custom_field(klass, rule)
|
21
|
+
klass.field rule['name'], :type => ::Boolean, :localize => rule['localized'] || false
|
22
|
+
|
23
|
+
if rule['required']
|
24
|
+
klass.validates_presence_of rule['name']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
6
29
|
|
7
|
-
included do
|
8
|
-
register_type :boolean, ::Boolean
|
9
30
|
end
|
10
31
|
|
11
32
|
end
|
33
|
+
|
12
34
|
end
|
35
|
+
|
13
36
|
end
|
@@ -1,43 +1,53 @@
|
|
1
1
|
module CustomFields
|
2
|
+
|
2
3
|
module Types
|
4
|
+
|
3
5
|
module Date
|
4
6
|
|
5
|
-
|
7
|
+
module Field; end
|
6
8
|
|
7
|
-
|
8
|
-
register_type :date, ::Date
|
9
|
-
end
|
9
|
+
module Target
|
10
10
|
|
11
|
-
|
11
|
+
extend ActiveSupport::Concern
|
12
12
|
|
13
|
-
|
13
|
+
module ClassMethods
|
14
14
|
|
15
|
-
|
15
|
+
# Adds a date field
|
16
|
+
#
|
17
|
+
# @param [ Class ] klass The class to modify
|
18
|
+
# @param [ Hash ] rule It contains the name of the field and if it is required or not
|
19
|
+
#
|
20
|
+
def apply_date_custom_field(klass, rule)
|
21
|
+
name = rule['name']
|
16
22
|
|
17
|
-
|
18
|
-
self.#{self._name}
|
19
|
-
end
|
23
|
+
klass.field name, :type => ::Date, :localize => rule['localized'] || false
|
20
24
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
value = ::Date.new(date[:year], date[:mon], date[:mday])
|
25
|
-
end
|
25
|
+
# other methods
|
26
|
+
klass.send(:define_method, :"formatted_#{name}") { _get_formatted_date(name) }
|
27
|
+
klass.send(:define_method, :"formatted_#{name}=") { |value| _set_formatted_date(name, value) }
|
26
28
|
|
27
|
-
|
29
|
+
if rule['required']
|
30
|
+
klass.validates_presence_of name, :"formatted_#{name}"
|
28
31
|
end
|
32
|
+
end
|
29
33
|
|
30
|
-
|
31
|
-
self.#{self._name}.strftime(I18n.t('date.formats.default')) rescue nil
|
32
|
-
end
|
34
|
+
end
|
33
35
|
|
34
|
-
|
35
|
-
EOF
|
36
|
+
module InstanceMethods
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
protected
|
39
|
+
|
40
|
+
def _set_formatted_date(name, value)
|
41
|
+
if value.is_a?(::String) && !value.blank?
|
42
|
+
date = ::Date._strptime(value, I18n.t('date.formats.default'))
|
43
|
+
value = ::Date.new(date[:year], date[:mon], date[:mday])
|
40
44
|
end
|
45
|
+
|
46
|
+
self.send(:"#{name}=", value)
|
47
|
+
end
|
48
|
+
|
49
|
+
def _get_formatted_date(name)
|
50
|
+
self.send(name.to_sym).strftime(I18n.t('date.formats.default')) rescue nil
|
41
51
|
end
|
42
52
|
|
43
53
|
end
|
@@ -45,5 +55,7 @@ module CustomFields
|
|
45
55
|
end
|
46
56
|
|
47
57
|
end
|
58
|
+
|
48
59
|
end
|
60
|
+
|
49
61
|
end
|
@@ -1,44 +1,64 @@
|
|
1
1
|
module CustomFields
|
2
|
+
|
2
3
|
module Types
|
4
|
+
|
3
5
|
module Default
|
4
|
-
extend ActiveSupport::Concern
|
5
6
|
|
6
|
-
|
7
|
-
cattr_accessor :field_types
|
8
|
-
end
|
7
|
+
module Field
|
9
8
|
|
10
|
-
|
9
|
+
# Build the mongodb updates based on
|
10
|
+
# the new state of the field
|
11
|
+
#
|
12
|
+
# @param [ Hash ] memo Store the updates
|
13
|
+
#
|
14
|
+
# @return [ Hash ] The memo object upgraded
|
15
|
+
#
|
16
|
+
def collect_default_diff(memo)
|
17
|
+
if self.persisted?
|
18
|
+
if self.destroyed?
|
19
|
+
memo['$unset'][self.name] = 1
|
20
|
+
elsif self.changed?
|
21
|
+
if self.changes.key?(:name)
|
22
|
+
old_name, new_name = self.changes[:name]
|
23
|
+
memo['$rename'][old_name] = new_name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
11
27
|
|
12
|
-
|
13
|
-
klass.class_eval <<-EOF
|
14
|
-
alias :#{self.safe_alias} :#{self._name}
|
15
|
-
alias :#{self.safe_alias}= :#{self._name}=
|
16
|
-
EOF
|
17
|
-
end
|
28
|
+
(memo['$set']['custom_fields_recipe.rules'] ||= []) << self.to_recipe
|
18
29
|
|
19
|
-
|
20
|
-
# add validation if required field
|
21
|
-
if self.required?
|
22
|
-
klass.validates_presence_of self.safe_alias.to_sym
|
23
|
-
end
|
30
|
+
memo
|
24
31
|
end
|
25
32
|
|
26
33
|
end
|
27
34
|
|
28
|
-
module
|
35
|
+
module Target
|
29
36
|
|
30
|
-
|
31
|
-
self.field_types ||= {}
|
32
|
-
self.field_types[kind.to_sym] = klass unless klass.nil?
|
37
|
+
extend ActiveSupport::Concern
|
33
38
|
|
34
|
-
|
35
|
-
|
36
|
-
|
39
|
+
module ClassMethods
|
40
|
+
|
41
|
+
# Modify the target class according to the rule.
|
42
|
+
# By default, it declares the field and a validator
|
43
|
+
# if specified by the rule
|
44
|
+
#
|
45
|
+
# @param [ Class ] klass The class to modify
|
46
|
+
# @param [ Hash ] rule It contains the name of the field and if it is required or not
|
47
|
+
#
|
48
|
+
def apply_custom_field(klass, rule)
|
49
|
+
klass.field rule['name'], :localize => rule['localized'] || false
|
50
|
+
|
51
|
+
if rule['required']
|
52
|
+
klass.validates_presence_of rule['name']
|
37
53
|
end
|
38
|
-
|
54
|
+
end
|
55
|
+
|
39
56
|
end
|
40
57
|
|
41
58
|
end
|
59
|
+
|
42
60
|
end
|
61
|
+
|
43
62
|
end
|
63
|
+
|
44
64
|
end
|
@@ -1,27 +1,45 @@
|
|
1
1
|
module CustomFields
|
2
|
+
|
2
3
|
module Types
|
4
|
+
|
3
5
|
module File
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
klass
|
16
|
-
|
17
|
-
|
6
|
+
|
7
|
+
module Field; end
|
8
|
+
|
9
|
+
module Target
|
10
|
+
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
# Adds a file field (using carrierwave)
|
16
|
+
#
|
17
|
+
# @param [ Class ] klass The class to modify
|
18
|
+
# @param [ Hash ] rule It contains the name of the field and if it is required or not
|
19
|
+
#
|
20
|
+
def apply_file_custom_field(klass, rule)
|
21
|
+
name = rule['name']
|
22
|
+
|
23
|
+
klass.mount_uploader name, FileUploader
|
24
|
+
|
25
|
+
if rule['localized'] == true
|
26
|
+
klass.replace_field name, ::String, true
|
27
|
+
end
|
28
|
+
|
29
|
+
if rule['required']
|
30
|
+
klass.validates_presence_of name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
18
34
|
end
|
19
|
-
|
35
|
+
|
20
36
|
end
|
21
|
-
|
37
|
+
|
22
38
|
class FileUploader < ::CarrierWave::Uploader::Base
|
23
39
|
end
|
24
|
-
|
40
|
+
|
25
41
|
end
|
42
|
+
|
26
43
|
end
|
44
|
+
|
27
45
|
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
module CustomFields
|
2
|
+
|
3
|
+
module Types
|
4
|
+
|
5
|
+
module Select
|
6
|
+
|
7
|
+
class Option
|
8
|
+
|
9
|
+
include Mongoid::Document
|
10
|
+
|
11
|
+
field :name, :localize => true
|
12
|
+
field :position, :type => Integer, :default => 0
|
13
|
+
|
14
|
+
embedded_in :custom_field, :inverse_of => :select_options
|
15
|
+
|
16
|
+
validates_presence_of :name
|
17
|
+
|
18
|
+
def as_json(options = nil)
|
19
|
+
super :methods => %w(_id name position)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
module Field
|
25
|
+
|
26
|
+
extend ActiveSupport::Concern
|
27
|
+
|
28
|
+
included do
|
29
|
+
|
30
|
+
embeds_many :select_options, :class_name => 'CustomFields::Types::Select::Option'
|
31
|
+
|
32
|
+
validates_associated :select_options
|
33
|
+
|
34
|
+
accepts_nested_attributes_for :select_options, :allow_destroy => true
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
module InstanceMethods
|
39
|
+
|
40
|
+
def ordered_select_options
|
41
|
+
self.select_options.sort { |a, b| (a.position || 0) <=> (b.position || 0) }.to_a
|
42
|
+
end
|
43
|
+
|
44
|
+
def select_to_recipe
|
45
|
+
{
|
46
|
+
'select_options' => self.ordered_select_options.map do |option|
|
47
|
+
{ '_id' => option._id, 'name' => option.name_translations }
|
48
|
+
end
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def select_as_json(options = {})
|
53
|
+
{ 'select_options' => self.ordered_select_options.map(&:as_json) }
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
module Target
|
61
|
+
|
62
|
+
extend ActiveSupport::Concern
|
63
|
+
|
64
|
+
module ClassMethods
|
65
|
+
|
66
|
+
# Adds a select field
|
67
|
+
#
|
68
|
+
# @param [ Class ] klass The class to modify
|
69
|
+
# @param [ Hash ] rule It contains the name of the field and if it is required or not
|
70
|
+
#
|
71
|
+
def apply_select_custom_field(klass, rule)
|
72
|
+
name, base_collection_name = rule['name'], "#{rule['name']}_options".to_sym
|
73
|
+
|
74
|
+
klass.field :"#{name}_id", :type => BSON::ObjectId, :localize => rule['localized'] || false
|
75
|
+
|
76
|
+
klass.cattr_accessor "_raw_#{base_collection_name}"
|
77
|
+
klass.send :"_raw_#{base_collection_name}=", rule['select_options']
|
78
|
+
|
79
|
+
# other methods
|
80
|
+
klass.send(:define_method, name.to_sym) { _get_select_option(name) }
|
81
|
+
klass.send(:define_method, :"#{name}=") { |value| _set_select_option(name, value) }
|
82
|
+
|
83
|
+
klass.class_eval <<-EOV
|
84
|
+
|
85
|
+
def self.#{base_collection_name}
|
86
|
+
self._select_options('#{name}')
|
87
|
+
end
|
88
|
+
|
89
|
+
EOV
|
90
|
+
|
91
|
+
if rule['required']
|
92
|
+
klass.validates_presence_of name
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns a list of documents groupes by select values defined in the custom fields recipe
|
97
|
+
#
|
98
|
+
# @param [ Class ] klass The class to modify
|
99
|
+
# @return [ Array ] An array of hashes (keys: select option and related documents)
|
100
|
+
#
|
101
|
+
def group_by_select_option(name, order_by = nil)
|
102
|
+
groups = self.only(:"#{name}_id").group
|
103
|
+
|
104
|
+
_select_options(name).map do |option|
|
105
|
+
group = groups.detect { |g| g["#{name}_id"].to_s == option['_id'].to_s }
|
106
|
+
list = group ? group['group'] : []
|
107
|
+
|
108
|
+
groups.delete(group) if group
|
109
|
+
|
110
|
+
{ :name => option['name'], :entries => self._order_select_entries(list, order_by) }.with_indifferent_access
|
111
|
+
end.tap do |array|
|
112
|
+
if not groups.empty? # orphan entries ?
|
113
|
+
empty = { :name => nil, :entries => [] }.with_indifferent_access
|
114
|
+
groups.each do |group|
|
115
|
+
empty[:entries] += group['group']
|
116
|
+
end
|
117
|
+
empty[:entries] = self._order_select_entries(empty[:entries], order_by)
|
118
|
+
array << empty
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def _select_options(name)
|
124
|
+
self.send(:"_raw_#{name}_options").map do |option|
|
125
|
+
|
126
|
+
locale = Mongoid::Fields::I18n.locale.to_s
|
127
|
+
|
128
|
+
name = if !option['name'].respond_to?(:merge)
|
129
|
+
option['name']
|
130
|
+
elsif Mongoid::Fields::I18n.fallbacks?
|
131
|
+
option['name'][Mongoid::Fields::I18n.fallbacks[locale.to_sym].map(&:to_s).find { |loc| !option['name'][loc].nil? }]
|
132
|
+
else
|
133
|
+
option['name'][locale.to_s]
|
134
|
+
end
|
135
|
+
|
136
|
+
{ '_id' => option['_id'], 'name' => name }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def _order_select_entries(list, order_by = nil)
|
141
|
+
return list if order_by.nil?
|
142
|
+
|
143
|
+
column, direction = order_by.flatten
|
144
|
+
|
145
|
+
list = list.sort { |a, b| (a.send(column) && b.send(column)) ? (a.send(column) || 0) <=> (b.send(column) || 0) : 0 }
|
146
|
+
|
147
|
+
direction == 'asc' ? list : list.reverse
|
148
|
+
|
149
|
+
list
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
module InstanceMethods
|
155
|
+
|
156
|
+
def _select_option_id(name)
|
157
|
+
self.send(:"#{name}_id")
|
158
|
+
end
|
159
|
+
|
160
|
+
def _find_select_option(name, id_or_name)
|
161
|
+
self.class._select_options(name).detect do |option|
|
162
|
+
option['name'] == id_or_name || option['_id'].to_s == id_or_name.to_s
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def _get_select_option(name)
|
167
|
+
option = self._find_select_option(name, self._select_option_id(name))
|
168
|
+
option ? option['name'] : nil
|
169
|
+
end
|
170
|
+
|
171
|
+
def _set_select_option(name, value)
|
172
|
+
option = self._find_select_option(name, value)
|
173
|
+
self.send(:"#{name}_id=", option ? option['_id'] : nil)
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|