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.
Files changed (41) hide show
  1. data/MIT-LICENSE +1 -1
  2. data/README.textile +14 -6
  3. data/config/locales/fr.yml +5 -1
  4. data/lib/custom_fields.rb +11 -37
  5. data/lib/custom_fields/extensions/carrierwave.rb +25 -0
  6. data/lib/custom_fields/extensions/mongoid/document.rb +12 -50
  7. data/lib/custom_fields/extensions/mongoid/factory.rb +20 -0
  8. data/lib/custom_fields/extensions/mongoid/fields.rb +29 -0
  9. data/lib/custom_fields/extensions/mongoid/fields/i18n.rb +53 -0
  10. data/lib/custom_fields/extensions/mongoid/fields/internal/localized.rb +84 -0
  11. data/lib/custom_fields/extensions/mongoid/relations/referenced/many.rb +29 -0
  12. data/lib/custom_fields/field.rb +44 -175
  13. data/lib/custom_fields/source.rb +333 -0
  14. data/lib/custom_fields/target.rb +90 -0
  15. data/lib/custom_fields/types/boolean.rb +26 -3
  16. data/lib/custom_fields/types/date.rb +36 -24
  17. data/lib/custom_fields/types/default.rb +44 -24
  18. data/lib/custom_fields/types/file.rb +35 -17
  19. data/lib/custom_fields/types/select.rb +184 -0
  20. data/lib/custom_fields/types/string.rb +25 -6
  21. data/lib/custom_fields/types/text.rb +35 -8
  22. data/lib/custom_fields/types_old/boolean.rb +13 -0
  23. data/lib/custom_fields/{types → types_old}/category.rb +0 -0
  24. data/lib/custom_fields/types_old/date.rb +49 -0
  25. data/lib/custom_fields/types_old/default.rb +44 -0
  26. data/lib/custom_fields/types_old/file.rb +27 -0
  27. data/lib/custom_fields/{types → types_old}/has_many.rb +0 -0
  28. data/lib/custom_fields/{types → types_old}/has_many/proxy_collection.rb +0 -0
  29. data/lib/custom_fields/{types → types_old}/has_many/reverse_lookup_proxy_collection.rb +0 -0
  30. data/lib/custom_fields/{types → types_old}/has_one.rb +0 -0
  31. data/lib/custom_fields/types_old/string.rb +13 -0
  32. data/lib/custom_fields/types_old/text.rb +15 -0
  33. data/lib/custom_fields/version.rb +1 -1
  34. metadata +115 -91
  35. data/lib/custom_fields/custom_fields_for.rb +0 -350
  36. data/lib/custom_fields/extensions/mongoid/relations/accessors.rb +0 -31
  37. data/lib/custom_fields/extensions/mongoid/relations/builders.rb +0 -30
  38. data/lib/custom_fields/proxy_class/base.rb +0 -112
  39. data/lib/custom_fields/proxy_class/builder.rb +0 -60
  40. data/lib/custom_fields/proxy_class/helper.rb +0 -57
  41. 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
- extend ActiveSupport::Concern
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
- extend ActiveSupport::Concern
7
+ module Field; end
6
8
 
7
- included do
8
- register_type :date, ::Date
9
- end
9
+ module Target
10
10
 
11
- module InstanceMethods
11
+ extend ActiveSupport::Concern
12
12
 
13
- def apply_date_type(klass)
13
+ module ClassMethods
14
14
 
15
- klass.class_eval <<-EOF
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
- def #{self.safe_alias}
18
- self.#{self._name}
19
- end
23
+ klass.field name, :type => ::Date, :localize => rule['localized'] || false
20
24
 
21
- def #{self.safe_alias}=(value)
22
- if value.is_a?(::String) && !value.blank?
23
- date = ::Date._strptime(value, I18n.t('date.formats.default'))
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
- self.#{self._name} = value
29
+ if rule['required']
30
+ klass.validates_presence_of name, :"formatted_#{name}"
28
31
  end
32
+ end
29
33
 
30
- def formatted_#{self.safe_alias}
31
- self.#{self._name}.strftime(I18n.t('date.formats.default')) rescue nil
32
- end
34
+ end
33
35
 
34
- alias formatted_#{self.safe_alias}= #{self.safe_alias}=
35
- EOF
36
+ module InstanceMethods
36
37
 
37
- def add_date_validation(klass)
38
- if self.required?
39
- klass.validates_presence_of self.safe_alias.to_sym, :"formatted_#{self.safe_alias}"
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
- included do
7
- cattr_accessor :field_types
8
- end
7
+ module Field
9
8
 
10
- module InstanceMethods
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
- def apply_default_type(klass)
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
- def add_default_validation(klass)
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 ClassMethods
35
+ module Target
29
36
 
30
- def register_type(kind, klass = ::String)
31
- self.field_types ||= {}
32
- self.field_types[kind.to_sym] = klass unless klass.nil?
37
+ extend ActiveSupport::Concern
33
38
 
34
- self.class_eval <<-EOF
35
- def #{kind.to_s}?
36
- self.kind.downcase == '#{kind}' rescue false
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
- EOF
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
- extend ActiveSupport::Concern
6
-
7
- included do
8
- register_type :file, nil # do not create the default field
9
- end
10
-
11
- module InstanceMethods
12
-
13
- def apply_file_type(klass)
14
-
15
- klass.mount_uploader self._name, FileUploader
16
-
17
- self.apply_default_type(klass)
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