gnuside-custom_fields 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.textile +70 -0
  4. data/config/locales/de.yml +15 -0
  5. data/config/locales/en.yml +21 -0
  6. data/config/locales/fr.yml +25 -0
  7. data/config/locales/pt-BR.yml +9 -0
  8. data/config/locales/ru.yml +15 -0
  9. data/lib/custom_fields/extensions/active_support.rb +28 -0
  10. data/lib/custom_fields/extensions/carrierwave.rb +25 -0
  11. data/lib/custom_fields/extensions/mongoid/document.rb +21 -0
  12. data/lib/custom_fields/extensions/mongoid/factory.rb +20 -0
  13. data/lib/custom_fields/extensions/mongoid/fields/i18n.rb +55 -0
  14. data/lib/custom_fields/extensions/mongoid/fields/localized.rb +39 -0
  15. data/lib/custom_fields/extensions/mongoid/fields.rb +31 -0
  16. data/lib/custom_fields/extensions/mongoid/relations/referenced/in.rb +22 -0
  17. data/lib/custom_fields/extensions/mongoid/relations/referenced/many.rb +34 -0
  18. data/lib/custom_fields/extensions/mongoid/validations/collection_size.rb +43 -0
  19. data/lib/custom_fields/extensions/mongoid/validations/macros.rb +25 -0
  20. data/lib/custom_fields/extensions/origin/smash.rb +33 -0
  21. data/lib/custom_fields/field.rb +106 -0
  22. data/lib/custom_fields/source.rb +347 -0
  23. data/lib/custom_fields/target.rb +99 -0
  24. data/lib/custom_fields/target_helpers.rb +192 -0
  25. data/lib/custom_fields/types/belongs_to.rb +65 -0
  26. data/lib/custom_fields/types/boolean.rb +55 -0
  27. data/lib/custom_fields/types/date.rb +97 -0
  28. data/lib/custom_fields/types/date_time.rb +97 -0
  29. data/lib/custom_fields/types/default.rb +103 -0
  30. data/lib/custom_fields/types/email.rb +60 -0
  31. data/lib/custom_fields/types/file.rb +74 -0
  32. data/lib/custom_fields/types/float.rb +52 -0
  33. data/lib/custom_fields/types/has_many.rb +74 -0
  34. data/lib/custom_fields/types/integer.rb +54 -0
  35. data/lib/custom_fields/types/many_to_many.rb +75 -0
  36. data/lib/custom_fields/types/money.rb +146 -0
  37. data/lib/custom_fields/types/relationship_default.rb +44 -0
  38. data/lib/custom_fields/types/select.rb +217 -0
  39. data/lib/custom_fields/types/string.rb +55 -0
  40. data/lib/custom_fields/types/tags.rb +35 -0
  41. data/lib/custom_fields/types/text.rb +65 -0
  42. data/lib/custom_fields/version.rb +6 -0
  43. data/lib/custom_fields.rb +74 -0
  44. metadata +244 -0
@@ -0,0 +1,75 @@
1
+ module CustomFields
2
+
3
+ module Types
4
+
5
+ module ManyToMany
6
+
7
+ module Field
8
+
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+
13
+ def many_to_many_to_recipe
14
+ { 'class_name' => self.class_name, 'inverse_of' => self.inverse_of, 'order_by' => self.order_by }
15
+ end
16
+
17
+ def many_to_many_is_relationship?
18
+ self.type == 'many_to_many'
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
25
+ module Target
26
+
27
+ extend ActiveSupport::Concern
28
+
29
+ module ClassMethods
30
+
31
+ # Adds a many_to_many relationship between 2 mongoid models
32
+ #
33
+ # @param [ Class ] klass The class to modify
34
+ # @param [ Hash ] rule It contains the name of the relation and if it is required or not
35
+ #
36
+ def apply_many_to_many_custom_field(klass, rule)
37
+ # puts "#{klass.inspect}.many_to_many #{rule['name'].inspect}, class_name: #{rule['class_name'].inspect} / #{rule['order_by']}" # DEBUG
38
+
39
+ klass.has_and_belongs_to_many rule['name'], class_name: rule['class_name'], inverse_of: rule['inverse_of'], order: rule['order_by'] do
40
+
41
+ # def at_least_one_element?
42
+ # (base.send(metadata.key.to_sym).try(:size) || 0) > 0
43
+ # end
44
+
45
+ def filtered(conditions = {}, order_by = nil)
46
+ list = conditions.empty? ? self : self.where(conditions)
47
+
48
+ if order_by
49
+ list.order_by(order_by)
50
+ else
51
+ # use the natural order given by the initial array (ex: project_ids).
52
+ # Warning: it returns an array and not a criteria object meaning it breaks the chain
53
+ ids = base.send(metadata.key.to_sym)
54
+ list.entries.sort { |a, b| ids.index(a.id) <=> ids.index(b.id) }
55
+ end
56
+ end
57
+
58
+ alias :ordered :filtered # backward compatibility + semantic purpose
59
+
60
+ end
61
+
62
+ if rule['required']
63
+ klass.validates_collection_size_of rule['name'], minimum: 1, message: :at_least_one_element
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,146 @@
1
+ module CustomFields
2
+ module Types
3
+ module Money
4
+
5
+ module Field
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # allow_currency_from_symbol means that the user is able
10
+ # to provide another currency instead of the the default
11
+ # e.g. default is 'EUR' and User sets '100.11 USD'
12
+ field :default_currency
13
+ field :allow_currency_from_symbol, type: ::Boolean, default: false
14
+
15
+ before_validation :set_default
16
+
17
+ protected
18
+
19
+ def set_default
20
+ self.default_currency ||= CustomFields.options[:default_currency]
21
+ end
22
+
23
+ def check_currency
24
+ ::Money::Currency.find( self.default_currency )
25
+ end
26
+
27
+ end # included
28
+
29
+ def money_to_recipe
30
+ { 'default_currency' => self.default_currency,
31
+ 'allow_currency_from_symbol' => self.allow_currency_from_symbol }
32
+ end
33
+
34
+ def money_as_json(options = {})
35
+ money_to_recipe
36
+ end
37
+
38
+ end # module Field
39
+
40
+
41
+ module Target
42
+ extend ActiveSupport::Concern
43
+
44
+ module ClassMethods
45
+
46
+ # Adds a Money field
47
+ #
48
+ # uses the money gem (https://github.com/RubyMoney/money)
49
+ #
50
+ # @param [ Class ] klass The class to modify
51
+ # @param [ Hash ] rule It contains the name of the field and if it is required or not
52
+ #
53
+ # if allow_currency_from_symbol is set the formatted_name_field will return the amount
54
+ # and the currency
55
+
56
+ def apply_money_custom_field(klass, rule)
57
+
58
+ # the field names
59
+ name = rule['name']
60
+ names = {
61
+ name: name.to_sym,
62
+ cents_field: "#{name}_cents".to_sym,
63
+ currency_field: "#{name}_currency".to_sym,
64
+ formatted_name_field: "formatted_#{name}".to_sym,
65
+ allow_currency_from_symbol: "#{name}_allow_currency_from_symbol".to_sym,
66
+ default_currency: "#{name}_default_currency".to_sym
67
+ }
68
+
69
+ # fields
70
+ klass.field names[:cents_field], type: ::Integer, localize: false
71
+ klass.field names[:currency_field], type: ::String, localize: false
72
+
73
+ # getters and setters
74
+ klass.send( :define_method, name ) { _get_money(names) }
75
+ klass.send( :define_method, :"#{name}=" ) { |value| _set_money(value, names) }
76
+
77
+ klass.send( :define_method, names[:formatted_name_field] ) { _get_formatted_money(names) }
78
+ klass.send( :define_method, :"#{names[:formatted_name_field]}=" ) { |value| _set_money(value, names) }
79
+
80
+ klass.send( :define_method, names[:allow_currency_from_symbol] ) { rule['allow_currency_from_symbol'] }
81
+ klass.send( :define_method, names[:default_currency] ) { rule['default_currency'] }
82
+
83
+ # validations
84
+ klass.validate { _check_money( names ) } if rule['required']
85
+ klass.validates_presence_of( names[:cents_field], names[:currency_field] ) if rule['required']
86
+ klass.validates_numericality_of names[:cents_field], only_integer: true, if: names[:cents_field]
87
+
88
+ end
89
+
90
+ def money_attribute_get(instance, name)
91
+ if value = instance.send(:"formatted_#{name}")
92
+ { name => value, "formatted_#{name}" => value }
93
+ else
94
+ {}
95
+ end
96
+ end
97
+
98
+ def money_attribute_set(instance, name, attributes)
99
+ return unless attributes.key?(name) || attributes.key?("formatted_#{name}")
100
+ value = attributes[name] || attributes["formatted_#{name}"]
101
+ instance.send(:"formatted_#{name}=", value)
102
+ end
103
+
104
+
105
+ end
106
+
107
+ protected
108
+
109
+ def _set_money_defaults( names )
110
+ ::Money.assume_from_symbol = self.send( names[:allow_currency_from_symbol] )
111
+ ::Money.default_currency = self.send( names[:default_currency] )
112
+ end
113
+
114
+ def _get_money( names )
115
+ _set_money_defaults( names )
116
+ ::Money.new( self.read_attribute( names[:cents_field] ), self.read_attribute( names[:currency_field] ) || ::Money.default_currency )
117
+ end
118
+
119
+ def _check_money( names )
120
+ if [nil, ''].include? self.read_attribute.names[:cents_field]
121
+ raise ArgumentError.new 'Unrecognized amount'
122
+ end
123
+ _get_money( names )
124
+ rescue
125
+ self.errors.add( names[:name], "#{$!}" )
126
+ false
127
+ end
128
+
129
+ def _set_money( _money, names )
130
+ return if _money.blank?
131
+ _set_money_defaults( names )
132
+ money = _money.kind_of?( Money ) ? _money : ::Money.parse( _money )
133
+ self.write_attribute( names[:cents_field], money.cents )
134
+ self.write_attribute( names[:currency_field], money.currency.iso_code )
135
+ rescue
136
+ self.errors.add( names[:name], "#{$!}" )
137
+ end
138
+
139
+ def _get_formatted_money( names )
140
+ _get_money( names ).format( symbol: self.send( names[:allow_currency_from_symbol] ), no_cents_if_whole: true ) rescue nil
141
+ end
142
+
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,44 @@
1
+ module CustomFields
2
+
3
+ module Types
4
+
5
+ module RelationshipDefault
6
+
7
+ module Field
8
+
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+
13
+ field :class_name
14
+ field :inverse_of
15
+ field :order_by
16
+
17
+ validates_presence_of :class_name, if: :is_relationship?
18
+ validate :ensure_class_name_security, if: :is_relationship?
19
+
20
+ def is_relationship?
21
+ method_name = :"#{self.type}_is_relationship?"
22
+ self.respond_to?(method_name) && self.send(method_name)
23
+ end
24
+
25
+ protected
26
+
27
+ def ensure_class_name_security
28
+ true
29
+ # FIXME: to be overridden in the target application
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+
43
+
44
+
@@ -0,0 +1,217 @@
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
+ def ordered_select_options
39
+ self.select_options.sort { |a, b| (a.position || 0) <=> (b.position || 0) }.to_a
40
+ end
41
+
42
+ def select_to_recipe
43
+ {
44
+ 'select_options' => self.ordered_select_options.map do |option|
45
+ { '_id' => option._id, 'name' => option.name_translations }
46
+ end
47
+ }
48
+ end
49
+
50
+ def select_as_json(options = {})
51
+ { 'select_options' => self.ordered_select_options.map(&:as_json) }
52
+ end
53
+
54
+ end
55
+
56
+ module Target
57
+
58
+ extend ActiveSupport::Concern
59
+
60
+ module ClassMethods
61
+
62
+ # Adds a select field
63
+ #
64
+ # @param [ Class ] klass The class to modify
65
+ # @param [ Hash ] rule It contains the name of the field and if it is required or not
66
+ #
67
+ def apply_select_custom_field(klass, rule)
68
+ name, base_collection_name = rule['name'], "#{rule['name']}_options".to_sym
69
+
70
+ klass.field :"#{name}_id", type: Moped::BSON::ObjectId, localize: rule['localized'] || false
71
+
72
+ klass.cattr_accessor "_raw_#{base_collection_name}"
73
+ klass.send :"_raw_#{base_collection_name}=", rule['select_options'].sort { |a, b| a['position'] <=> b['position'] }
74
+
75
+ # other methods
76
+ klass.send(:define_method, name.to_sym) { _get_select_option(name) }
77
+ klass.send(:define_method, :"#{name}=") { |value| _set_select_option(name, value) }
78
+
79
+ klass.class_eval <<-EOV
80
+
81
+ def self.#{base_collection_name}
82
+ self._select_options('#{name}')
83
+ end
84
+
85
+ EOV
86
+
87
+ if rule['required']
88
+ klass.validates_presence_of name
89
+ end
90
+ end
91
+
92
+ # Build a hash storing the values (id and option name) for
93
+ # a select custom field of an instance.
94
+ #
95
+ # @param [ Object ] instance An instance of the class enhanced by the custom_fields
96
+ # @param [ String ] name The name of the select custom field
97
+ #
98
+ # @return [ Hash ] fields: <name>: option name, <name>_id: id of the option
99
+ #
100
+ def select_attribute_get(instance, name)
101
+ if value = instance.send(name.to_sym)
102
+ {
103
+ name => value,
104
+ "#{name}_id" => instance.send(:"#{name}_id")
105
+ }
106
+ else
107
+ {}
108
+ end
109
+ end
110
+
111
+ # Set the value for the instance and the select field specified by
112
+ # the 2 params.
113
+ #
114
+ # @param [ Object ] instance An instance of the class enhanced by the custom_fields
115
+ # @param [ String ] name The name of the select custom field
116
+ # @param [ Hash ] attributes The attributes used to fetch the values
117
+ #
118
+ def select_attribute_set(instance, name, attributes)
119
+ id_or_name = attributes[name] || attributes["#{name}_id"]
120
+
121
+ return if id_or_name.nil?
122
+
123
+ option = _select_options(name).detect do |option|
124
+ [option['_id'], option['name']].include?(id_or_name)
125
+ end
126
+
127
+ instance.send(:"#{name}_id=", option.try(:[], '_id'))
128
+ end
129
+
130
+ # Returns a list of documents groupes by select values defined in the custom fields recipe
131
+ #
132
+ # @param [ Class ] klass The class to modify
133
+ # @return [ Array ] An array of hashes (keys: select option and related documents)
134
+ #
135
+ def group_by_select_option(name, order_by = nil)
136
+ name_id = "#{name}_id"
137
+ groups = self.each.group_by{|x| x.send(name_id)}.map do |(k, v)|
138
+ {name_id => k, "group" => v}
139
+ end
140
+
141
+ _select_options(name).map do |option|
142
+ group = groups.detect { |g| g[name_id].to_s == option['_id'].to_s }
143
+ list = group ? group['group'] : []
144
+
145
+ groups.delete(group) if group
146
+
147
+ { name: option['name'], entries: self._order_select_entries(list, order_by) }.with_indifferent_access
148
+ end.tap do |array|
149
+ if not groups.empty? # orphan entries ?
150
+ empty = { name: nil, entries: [] }.with_indifferent_access
151
+ groups.each do |group|
152
+ empty[:entries] += group['group']
153
+ end
154
+ empty[:entries] = self._order_select_entries(empty[:entries], order_by)
155
+ array << empty
156
+ end
157
+ end
158
+ end
159
+
160
+ def _select_options(name)
161
+ self.send(:"_raw_#{name}_options").map do |option|
162
+
163
+ locale = Mongoid::Fields::I18n.locale.to_s
164
+
165
+ name = if !option['name'].respond_to?(:merge)
166
+ option['name']
167
+ elsif Mongoid::Fields::I18n.fallbacks?
168
+ option['name'][Mongoid::Fields::I18n.fallbacks[locale.to_sym].map(&:to_s).find { |loc| !option['name'][loc].nil? }]
169
+ else
170
+ option['name'][locale.to_s]
171
+ end
172
+
173
+ { '_id' => option['_id'], 'name' => name }
174
+ end
175
+ end
176
+
177
+ def _order_select_entries(list, order_by = nil)
178
+ return list if order_by.nil?
179
+
180
+ column, direction = order_by.flatten
181
+
182
+ list = list.sort { |a, b| (a.send(column) && b.send(column)) ? (a.send(column) || 0) <=> (b.send(column) || 0) : 0 }
183
+
184
+ direction == 'asc' ? list : list.reverse
185
+
186
+ list
187
+ end
188
+
189
+ end
190
+
191
+ def _select_option_id(name)
192
+ self.send(:"#{name}_id")
193
+ end
194
+
195
+ def _find_select_option(name, id_or_name)
196
+ self.class._select_options(name).detect do |option|
197
+ option['name'] == id_or_name || option['_id'].to_s == id_or_name.to_s
198
+ end
199
+ end
200
+
201
+ def _get_select_option(name)
202
+ option = self._find_select_option(name, self._select_option_id(name))
203
+ option ? option['name'] : nil
204
+ end
205
+
206
+ def _set_select_option(name, value)
207
+ option = self._find_select_option(name, value)
208
+ self.send(:"#{name}_id=", option ? option['_id'] : nil)
209
+ end
210
+
211
+ end
212
+
213
+ end
214
+
215
+ end
216
+
217
+ end
@@ -0,0 +1,55 @@
1
+ module CustomFields
2
+
3
+ module Types
4
+
5
+ module String
6
+
7
+ module Field; end
8
+
9
+ module Target
10
+
11
+ extend ActiveSupport::Concern
12
+
13
+ module ClassMethods
14
+
15
+ # Add a string 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_string_custom_field(klass, rule)
21
+ apply_custom_field(klass, rule)
22
+ end
23
+
24
+ # Build a hash storing the raw value for
25
+ # a string custom field of an instance.
26
+ #
27
+ # @param [ Object ] instance An instance of the class enhanced by the custom_fields
28
+ # @param [ String ] name The name of the string custom field
29
+ #
30
+ # @return [ Hash ] field name => raw value
31
+ #
32
+ def string_attribute_get(instance, name)
33
+ self.default_attribute_get(instance, name)
34
+ end
35
+
36
+ # Set the value for the instance and the string field specified by
37
+ # the 2 params.
38
+ #
39
+ # @param [ Object ] instance An instance of the class enhanced by the custom_fields
40
+ # @param [ String ] name The name of the string custom field
41
+ # @param [ Hash ] attributes The attributes used to fetch the values
42
+ #
43
+ def string_attribute_set(instance, name, attributes)
44
+ self.default_attribute_set(instance, name, attributes)
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,35 @@
1
+ module CustomFields
2
+ module Types
3
+ module Tags
4
+ module Field; end
5
+
6
+ module Target
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+
11
+ def apply_tags_custom_field(klass, rule)
12
+ klass.field rule['name'], localize: rule['localized'] || false, type: Array
13
+
14
+ klass.class_eval do
15
+ define_method("#{rule['name']}=") do |val|
16
+ #FIXME I would use is_a?(), but it doesn't work in my machine!
17
+ val = val.split(/ *, */) if val.class.to_s == "String"
18
+ super(val)
19
+ end
20
+ end
21
+ end
22
+
23
+ def tags_attribute_get(instance, name)
24
+ self.default_attribute_get(instance, name)
25
+ end
26
+
27
+ def tags_attribute_set(instance, name, attributes)
28
+ self.default_attribute_set(instance, name, attributes)
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,65 @@
1
+ module CustomFields
2
+
3
+ module Types
4
+
5
+ module Text
6
+
7
+ module Field
8
+
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+
13
+ field :text_formatting, default: 'html'
14
+
15
+ end
16
+
17
+ end
18
+
19
+ module Target
20
+
21
+ extend ActiveSupport::Concern
22
+
23
+ module ClassMethods
24
+
25
+ # Adds a text field (simply a string field)
26
+ #
27
+ # @param [ Class ] klass The class to modify
28
+ # @param [ Hash ] rule It contains the name of the field and if it is required or not
29
+ #
30
+ def apply_text_custom_field(klass, rule)
31
+ apply_custom_field(klass, rule)
32
+ end
33
+
34
+ # Build a hash storing the raw value for
35
+ # a string custom field of an instance.
36
+ #
37
+ # @param [ Object ] instance An instance of the class enhanced by the custom_fields
38
+ # @param [ String ] name The name of the string custom field
39
+ #
40
+ # @return [ Hash ] field name => raw value
41
+ #
42
+ def text_attribute_get(instance, name)
43
+ default_attribute_get(instance, name)
44
+ end
45
+
46
+ # Set the value for the instance and the text field specified by
47
+ # the 2 params.
48
+ #
49
+ # @param [ Object ] instance An instance of the class enhanced by the custom_fields
50
+ # @param [ String ] name The name of the text custom field
51
+ # @param [ Hash ] attributes The attributes used to fetch the values
52
+ #
53
+ def text_attribute_set(instance, name, attributes)
54
+ self.default_attribute_set(instance, name, attributes)
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+ module CustomFields #:nodoc
3
+
4
+ VERSION = '2.3.1'
5
+
6
+ end