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