mongoid 2.2.6 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/CHANGELOG.md +2 -858
  2. data/Rakefile +2 -5
  3. data/lib/mongoid.rb +1 -1
  4. data/lib/mongoid/attributes.rb +68 -18
  5. data/lib/mongoid/attributes/processing.rb +4 -3
  6. data/lib/mongoid/callbacks.rb +102 -0
  7. data/lib/mongoid/collection.rb +1 -1
  8. data/lib/mongoid/components.rb +2 -1
  9. data/lib/mongoid/contexts/enumerable.rb +0 -24
  10. data/lib/mongoid/contexts/mongo.rb +2 -2
  11. data/lib/mongoid/copyable.rb +3 -1
  12. data/lib/mongoid/criteria.rb +18 -10
  13. data/lib/mongoid/criterion/complex.rb +11 -0
  14. data/lib/mongoid/criterion/inclusion.rb +38 -7
  15. data/lib/mongoid/criterion/optional.rb +2 -7
  16. data/lib/mongoid/criterion/selector.rb +1 -0
  17. data/lib/mongoid/dirty.rb +19 -0
  18. data/lib/mongoid/document.rb +16 -12
  19. data/lib/mongoid/extensions/hash/criteria_helpers.rb +1 -1
  20. data/lib/mongoid/extensions/object/checks.rb +4 -1
  21. data/lib/mongoid/extensions/object_id/conversions.rb +4 -2
  22. data/lib/mongoid/extensions/string/inflections.rb +2 -2
  23. data/lib/mongoid/factory.rb +7 -2
  24. data/lib/mongoid/fields.rb +4 -10
  25. data/lib/mongoid/fields/serializable.rb +18 -2
  26. data/lib/mongoid/fields/serializable/integer.rb +17 -5
  27. data/lib/mongoid/fields/serializable/localized.rb +41 -0
  28. data/lib/mongoid/finders.rb +5 -4
  29. data/lib/mongoid/hierarchy.rb +87 -84
  30. data/lib/mongoid/identity.rb +4 -2
  31. data/lib/mongoid/keys.rb +2 -1
  32. data/lib/mongoid/logger.rb +1 -7
  33. data/lib/mongoid/matchers/and.rb +30 -0
  34. data/lib/mongoid/matchers/in.rb +1 -1
  35. data/lib/mongoid/matchers/nin.rb +1 -1
  36. data/lib/mongoid/matchers/strategies.rb +6 -4
  37. data/lib/mongoid/multi_parameter_attributes.rb +3 -2
  38. data/lib/mongoid/named_scope.rb +3 -13
  39. data/lib/mongoid/nested_attributes.rb +1 -1
  40. data/lib/mongoid/paranoia.rb +2 -3
  41. data/lib/mongoid/persistence.rb +9 -5
  42. data/lib/mongoid/persistence/atomic/operation.rb +1 -1
  43. data/lib/mongoid/persistence/deletion.rb +1 -1
  44. data/lib/mongoid/persistence/insertion.rb +1 -1
  45. data/lib/mongoid/persistence/modification.rb +1 -1
  46. data/lib/mongoid/railtie.rb +1 -1
  47. data/lib/mongoid/railties/database.rake +9 -1
  48. data/lib/mongoid/relations.rb +1 -0
  49. data/lib/mongoid/relations/accessors.rb +1 -1
  50. data/lib/mongoid/relations/builders.rb +6 -4
  51. data/lib/mongoid/relations/builders/referenced/many.rb +1 -23
  52. data/lib/mongoid/relations/builders/referenced/one.rb +1 -1
  53. data/lib/mongoid/relations/cascading.rb +5 -3
  54. data/lib/mongoid/relations/conversions.rb +35 -0
  55. data/lib/mongoid/relations/embedded/atomic.rb +3 -3
  56. data/lib/mongoid/relations/embedded/in.rb +1 -1
  57. data/lib/mongoid/relations/embedded/many.rb +16 -13
  58. data/lib/mongoid/relations/embedded/one.rb +3 -3
  59. data/lib/mongoid/relations/metadata.rb +19 -15
  60. data/lib/mongoid/relations/proxy.rb +4 -5
  61. data/lib/mongoid/relations/referenced/in.rb +1 -1
  62. data/lib/mongoid/relations/referenced/many.rb +12 -31
  63. data/lib/mongoid/relations/referenced/many_to_many.rb +4 -5
  64. data/lib/mongoid/relations/referenced/one.rb +6 -8
  65. data/lib/mongoid/relations/synchronization.rb +3 -5
  66. data/lib/mongoid/safety.rb +34 -4
  67. data/lib/mongoid/serialization.rb +20 -6
  68. data/lib/mongoid/threaded.rb +47 -0
  69. data/lib/mongoid/timestamps.rb +1 -0
  70. data/lib/mongoid/timestamps/created.rb +1 -8
  71. data/lib/mongoid/timestamps/timeless.rb +50 -0
  72. data/lib/mongoid/timestamps/updated.rb +2 -9
  73. data/lib/mongoid/validations.rb +0 -2
  74. data/lib/mongoid/validations/associated.rb +1 -2
  75. data/lib/mongoid/validations/uniqueness.rb +89 -36
  76. data/lib/mongoid/version.rb +1 -1
  77. data/lib/mongoid/versioning.rb +5 -6
  78. data/lib/rails/generators/mongoid_generator.rb +1 -1
  79. data/lib/rails/mongoid.rb +14 -5
  80. metadata +27 -23
@@ -32,6 +32,17 @@ module Mongoid #:nodoc:
32
32
  [@key, @operator].hash
33
33
  end
34
34
 
35
+ # Create a mongo query with given value
36
+ #
37
+ # @example Create query
38
+ # criterion.to_mongo_hash(value)
39
+ #
40
+ # @params [] Whatever is a valid input for given operator
41
+ # @return [ Hash ] The query
42
+ def to_mongo_query(v)
43
+ {"$#{self.operator}" => v}
44
+ end
45
+
35
46
  # Is the criterion equal to the other?
36
47
  #
37
48
  # @example Check equality.
@@ -20,6 +20,27 @@ module Mongoid #:nodoc:
20
20
  end
21
21
  alias :all_in :all
22
22
 
23
+ # Adds a criterion to the criteria that specifies multiple expressions
24
+ # that *all* must match. This uses MongoDB's $and operator under the
25
+ # covers.
26
+ #
27
+ # @example Match all provided expressions.
28
+ # criteria.all_of(:name => value, :age.gt => 18)
29
+ #
30
+ # @param [ Array<Hash> ] Multiple hash expressions.
31
+ #
32
+ # @return [ Criteria ] The criteria object.
33
+ #
34
+ # @since 2.3.0
35
+ def all_of(*args)
36
+ clone.tap do |crit|
37
+ criterion = @selector["$and"] || []
38
+ converted = BSON::ObjectId.convert(klass, args.flatten)
39
+ expanded = converted.collect { |hash| hash.expand_complex_criteria }
40
+ crit.selector["$and"] = criterion.concat(expanded)
41
+ end
42
+ end
43
+
23
44
  # Adds a criterion to the +Criteria+ that specifies values where any can
24
45
  # be matched in order to return results. This is similar to an SQL "IN"
25
46
  # clause. The MongoDB conditional operator that will be used is "$in".
@@ -216,14 +237,24 @@ module Mongoid #:nodoc:
216
237
  BSON::ObjectId.convert(klass, selector || {}, false).expand_complex_criteria
217
238
  end
218
239
 
240
+ # @todo: Durran: 3.0.0: refactor the merging into separate strategies
241
+ # to clean this funkiness up.
219
242
  selector.each_pair do |key, value|
220
- if crit.selector.has_key?(key) &&
221
- crit.selector[key].respond_to?(:merge!) &&
222
- value.respond_to?(:merge!)
223
- crit.selector[key] =
224
- crit.selector[key].merge!(value) do |key, old, new|
225
- key == '$in' ? old & new : new
243
+ if crit.selector.has_key?(key)
244
+ if key.to_s =~ /^(|_)id$/
245
+ if crit.selector.has_key?("$and")
246
+ crit.selector["$and"] << { key => value }
247
+ else
248
+ crit.selector["$and"] = [{ key => crit.selector.delete(key) }, { key => value }]
226
249
  end
250
+ elsif crit.selector[key].respond_to?(:merge) && value.respond_to?(:merge)
251
+ crit.selector[key] =
252
+ crit.selector[key].merge(value) do |key, old, new|
253
+ key == '$in' ? old & new : new
254
+ end
255
+ else
256
+ crit.selector[key] = value
257
+ end
227
258
  else
228
259
  crit.selector[key] = value
229
260
  end
@@ -266,7 +297,7 @@ module Mongoid #:nodoc:
266
297
  #
267
298
  # @since 2.2.1
268
299
  def from_map_or_db(criteria)
269
- IdentityMap.get(klass, criteria.selector[:_id]) || criteria.one
300
+ IdentityMap.get(klass, criteria.extract_id) || criteria.one
270
301
  end
271
302
  end
272
303
  end
@@ -91,14 +91,9 @@ module Mongoid #:nodoc:
91
91
  def for_ids(*ids)
92
92
  ids.flatten!
93
93
  if ids.size > 1
94
- any_in(
95
- :_id => ::BSON::ObjectId.convert(klass, ids)
96
- )
94
+ where(:_id.in => ::BSON::ObjectId.convert(klass, ids))
97
95
  else
98
- clone.tap do |crit|
99
- crit.selector[:_id] =
100
- ::BSON::ObjectId.convert(klass, ids.first)
101
- end
96
+ where(:_id => ids.first)
102
97
  end
103
98
  end
104
99
 
@@ -30,6 +30,7 @@ module Mongoid #:nodoc:
30
30
  #
31
31
  # @since 2.0.0
32
32
  def []=(key, value)
33
+ key = "#{key}.#{::I18n.locale}" if klass.fields[key.to_s].try(:localized?)
33
34
  super(key, try_to_typecast(key, value))
34
35
  end
35
36
 
@@ -108,5 +108,24 @@ module Mongoid #:nodoc:
108
108
  return false unless changed_attributes.include?(attr)
109
109
  changed_attributes[attr] != attributes[attr]
110
110
  end
111
+
112
+ # Override Active Model's behaviour here in order to stay away from
113
+ # infinite loops on getter/setter overrides.
114
+ #
115
+ # @example Flag an attribute as changing.
116
+ # document.attribute_will_change!(:name)
117
+ #
118
+ # @param [ Symbol ] attr The attribute.
119
+ #
120
+ # @return [ Object ] The value of the attribute.
121
+ #
122
+ # @since 2.3.0
123
+ def attribute_will_change!(attr)
124
+ unless changed_attributes.include?(attr)
125
+ value = read_attribute(attr)
126
+ value = value.duplicable? ? value.clone : value
127
+ changed_attributes[attr] = value
128
+ end
129
+ end
111
130
  end
112
131
  end
@@ -119,15 +119,19 @@ module Mongoid #:nodoc:
119
119
  # Person.new(:title => "Sir")
120
120
  #
121
121
  # @param [ Hash ] attrs The attributes to set up the document with.
122
+ # @param [ Hash ] options A mass-assignment protection options. Supports
123
+ # :as and :without_protection
122
124
  #
123
125
  # @return [ Document ] A new document.
124
- def initialize(attrs = nil)
125
- building do
126
+ def initialize(attrs = nil, options = nil)
127
+ _building do
126
128
  @new_record = true
127
- @attributes = apply_default_attributes
128
- process(attrs) do
129
+ @attributes ||= {}
130
+ options ||= {}
131
+ process(attrs, options[:as] || :default, !options[:without_protection]) do
129
132
  yield self if block_given?
130
133
  identify
134
+ apply_defaults
131
135
  end
132
136
  run_callbacks(:initialize) { self }
133
137
  end
@@ -150,7 +154,7 @@ module Mongoid #:nodoc:
150
154
  end
151
155
  @attributes = {}.merge(reloaded || {})
152
156
  changed_attributes.clear
153
- apply_default_attributes
157
+ apply_defaults
154
158
  tap do
155
159
  reload_relations
156
160
  run_callbacks(:initialize)
@@ -199,13 +203,13 @@ module Mongoid #:nodoc:
199
203
  # @return [ Document ] An instance of the specified class.
200
204
  def becomes(klass)
201
205
  unless klass.include?(Mongoid::Document)
202
- raise ArgumentError, 'A class which includes Mongoid::Document is expected'
206
+ raise ArgumentError, "A class which includes Mongoid::Document is expected"
203
207
  end
204
- klass.new.tap do |became|
205
- became.instance_variable_set('@attributes', @attributes)
206
- became.instance_variable_set('@errors', @errors)
207
- became.instance_variable_set('@new_record', new_record?)
208
- became.instance_variable_set('@destroyed', destroyed?)
208
+ klass.instantiate(frozen? ? attributes.dup : attributes).tap do |became|
209
+ became.instance_variable_set(:@errors, errors)
210
+ became.instance_variable_set(:@new_record, new_record?)
211
+ became.instance_variable_set(:@destroyed, destroyed?)
212
+ became._type = klass.to_s
209
213
  end
210
214
  end
211
215
 
@@ -261,7 +265,7 @@ module Mongoid #:nodoc:
261
265
  attributes = attrs || {}
262
266
  allocate.tap do |doc|
263
267
  doc.instance_variable_set(:@attributes, attributes)
264
- doc.send(:apply_default_attributes)
268
+ doc.send(:apply_defaults)
265
269
  IdentityMap.set(doc)
266
270
  doc.run_callbacks(:initialize) { doc }
267
271
  end
@@ -20,7 +20,7 @@ module Mongoid #:nodoc:
20
20
  case k
21
21
  when Mongoid::Criterion::Complex
22
22
  hsh[k.key] ||= {}
23
- hsh[k.key].merge!({"$#{k.operator}" => v})
23
+ hsh[k.key].merge!(k.to_mongo_query(v))
24
24
  else
25
25
  hsh[k] = v
26
26
  end
@@ -14,6 +14,9 @@ module Mongoid #:nodoc:
14
14
  # @example Is the array vacant?
15
15
  # [].vacant?
16
16
  #
17
+ # @example Is the hash vacant?
18
+ # {}.vacant?
19
+ #
17
20
  # @example Is the object vacant?
18
21
  # nil.vacant?
19
22
  #
@@ -21,7 +24,7 @@ module Mongoid #:nodoc:
21
24
  #
22
25
  # @since 2.0.2
23
26
  def _vacant?
24
- is_a?(::Array) || is_a?(::String) ? empty? : !self
27
+ is_a?(::Enumerable) || is_a?(::String) ? empty? : !self
25
28
  end
26
29
  end
27
30
  end
@@ -38,7 +38,7 @@ module Mongoid #:nodoc:
38
38
  if args.unconvertable_to_bson?
39
39
  args
40
40
  else
41
- BSON::ObjectId.legal?(args) ? BSON::ObjectId.from_string(args) : args
41
+ BSON::ObjectId.from_string(args)
42
42
  end
43
43
  when ::Array
44
44
  args.delete_if { |arg| arg.blank? } if reject_blank
@@ -47,7 +47,9 @@ module Mongoid #:nodoc:
47
47
  args.tap do |hash|
48
48
  hash.each_pair do |key, value|
49
49
  next unless klass.object_id_field?(key)
50
- hash[key] = convert(klass, value, reject_blank)
50
+ begin
51
+ hash[key] = convert(klass, value, reject_blank)
52
+ rescue BSON::InvalidObjectId; end
51
53
  end
52
54
  end
53
55
  else
@@ -18,7 +18,7 @@ module Mongoid #:nodoc:
18
18
  CHAR_CONV = {
19
19
  " " => "-",
20
20
  "!" => "-excl-",
21
- "\"" => "-dblquo-",
21
+ "\"" => "-bckslsh-",
22
22
  "#" => "-hash-",
23
23
  "$" => "-dol-",
24
24
  "%" => "-perc-",
@@ -48,7 +48,7 @@ module Mongoid #:nodoc:
48
48
  "{" => "-ocurly-",
49
49
  "|" => "-pipe-",
50
50
  "}" => "-clcurly-",
51
- "~" => "-tilde-"
51
+ "~" => "-tilda-"
52
52
  }
53
53
 
54
54
  REVERSALS = {
@@ -12,11 +12,16 @@ module Mongoid #:nodoc:
12
12
  #
13
13
  # @param [ Class ] klass The class to instantiate from if _type is not present.
14
14
  # @param [ Hash ] attributes The document attributes.
15
+ # @param [ Hash ] optiosn The mass assignment scoping options.
15
16
  #
16
17
  # @return [ Document ] The instantiated document.
17
- def build(klass, attributes = {})
18
+ def build(klass, attributes = {}, options = {})
18
19
  type = (attributes || {})["_type"]
19
- (type && klass._types.include?(type)) ? type.constantize.new(attributes) : klass.new(attributes)
20
+ if type && klass._types.include?(type)
21
+ type.constantize.new(attributes, options)
22
+ else
23
+ klass.new(attributes, options)
24
+ end
20
25
  end
21
26
 
22
27
  # Builds a new +Document+ from the supplied attributes loaded from the
@@ -13,6 +13,7 @@ require "mongoid/fields/serializable/hash"
13
13
  require "mongoid/fields/serializable/integer"
14
14
  require "mongoid/fields/serializable/bignum"
15
15
  require "mongoid/fields/serializable/fixnum"
16
+ require "mongoid/fields/serializable/localized"
16
17
  require "mongoid/fields/serializable/nil_class"
17
18
  require "mongoid/fields/serializable/object"
18
19
  require "mongoid/fields/serializable/object_id"
@@ -227,20 +228,13 @@ module Mongoid #:nodoc
227
228
  # @param [ Hash ] options The hash of options.
228
229
  def add_field(name, options = {})
229
230
  meth = options.delete(:as) || name
230
- Mappings.for(
231
- options[:type], options[:identity]
232
- ).instantiate(name, options).tap do |field|
231
+ type = options[:localize] ? Fields::Serializable::Localized : options[:type]
232
+ Mappings.for(type, options[:identity]).instantiate(name, options).tap do |field|
233
233
  fields[name] = field
234
234
  defaults << name unless field.default.nil?
235
235
  create_accessors(name, meth, options)
236
236
  process_options(field)
237
-
238
- # @todo Durran: Refactor this once we can depend on at least
239
- # ActiveModel greater than 3.0.9. They finally have the ability then
240
- # to add attribute methods one at a time. This code will make class
241
- # load times extremely slow.
242
- undefine_attribute_methods
243
- define_attribute_methods(fields.keys)
237
+ define_attribute_method(name)
244
238
  end
245
239
  end
246
240
 
@@ -26,7 +26,7 @@ module Mongoid #:nodoc:
26
26
  extend ActiveSupport::Concern
27
27
 
28
28
  # Set readers for the instance variables.
29
- attr_accessor :default, :label, :name, :options
29
+ attr_accessor :default, :label, :localize, :name, :options
30
30
 
31
31
  # When reading the field do we need to cast the value? This holds true when
32
32
  # times are stored or for big decimals which are stored as strings.
@@ -89,6 +89,18 @@ module Mongoid #:nodoc:
89
89
  end
90
90
  end
91
91
 
92
+ # Is the field localized or not?
93
+ #
94
+ # @example Is the field localized?
95
+ # field.localized?
96
+ #
97
+ # @return [ true, false ] If the field is localized.
98
+ #
99
+ # @since 2.3.0
100
+ def localized?
101
+ !!@localize
102
+ end
103
+
92
104
  # Get the metadata for the field if its a foreign key.
93
105
  #
94
106
  # @example Get the metadata.
@@ -168,8 +180,12 @@ module Mongoid #:nodoc:
168
180
  allocate.tap do |field|
169
181
  field.name = name
170
182
  field.options = options
171
- field.default = options[:default]
172
183
  field.label = options[:label]
184
+ field.localize = options[:localize]
185
+ field.default = options[:default]
186
+ unless field.default
187
+ field.default = {} if field.localized?
188
+ end
173
189
  end
174
190
  end
175
191
  end
@@ -20,11 +20,23 @@ module Mongoid #:nodoc:
20
20
  # @since 2.1.0
21
21
  def serialize(object)
22
22
  return nil if object.blank?
23
- begin
24
- object.to_s =~ /(^[-+]?[0-9]+$)|(\.0+)$/ ? Integer(object) : Float(object)
25
- rescue
26
- object
27
- end
23
+ numeric(object) rescue object
24
+ end
25
+
26
+ private
27
+
28
+ # Get the numeric value for the provided object.
29
+ #
30
+ # @example Get the numeric value.
31
+ # field.numeric("1120")
32
+ #
33
+ # @param [ Object ] object The object to convert.
34
+ #
35
+ # @return [ Integer, Float ] The number.
36
+ #
37
+ # @since 2.3.0
38
+ def numeric(object)
39
+ object.to_s =~ /(^[-+]?[0-9]+$)|(\.0+)$/ ? object.to_i : Float(object)
28
40
  end
29
41
  end
30
42
  end
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Fields #:nodoc:
4
+ module Serializable #:nodoc:
5
+
6
+ # Defines the behaviour for localized string fields.
7
+ class Localized
8
+ include Serializable
9
+
10
+ # Deserialize the object based on the current locale. Will look in the
11
+ # hash for the current locale.
12
+ #
13
+ # @example Get the deserialized value.
14
+ # field.deserialize({ "en" => "testing" })
15
+ #
16
+ # @param [ Hash ] object The hash of translations.
17
+ #
18
+ # @return [ String ] The value for the current locale.
19
+ #
20
+ # @since 2.3.0
21
+ def deserialize(object)
22
+ object[::I18n.locale.to_s]
23
+ end
24
+
25
+ # Convert the provided string into a hash for the locale.
26
+ #
27
+ # @example Serialize the value.
28
+ # field.serialize("testing")
29
+ #
30
+ # @param [ String ] object The string to convert.
31
+ #
32
+ # @return [ Hash ] The locale with string translation.
33
+ #
34
+ # @since 2.3.0
35
+ def serialize(object)
36
+ { ::I18n.locale.to_s => object.try(:to_s) }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -7,10 +7,11 @@ module Mongoid #:nodoc:
7
7
 
8
8
  # Delegate to the criteria methods that are natural for creating a new
9
9
  # criteria.
10
- critera_methods = [ :all_in, :any_in, :any_of, :asc, :ascending, :avg,
11
- :desc, :descending, :excludes, :includes, :limit,
12
- :max, :min, :not_in, :only, :order_by, :skip, :sum,
13
- :without, :where, :update, :update_all, :near ]
10
+ critera_methods = [ :all_in, :all_of, :any_in, :any_of, :asc, :ascending,
11
+ :avg, :desc, :descending, :excludes,
12
+ :includes, :limit, :max, :min, :not_in, :only,
13
+ :order_by, :search, :skip, :sum, :without, :where,
14
+ :update, :update_all, :near ]
14
15
  delegate *(critera_methods.dup << { :to => :criteria })
15
16
 
16
17
  # Find all documents that match the given conditions.