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