mongoid 0.4.5 → 0.4.7

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.
data/.watchr CHANGED
@@ -8,4 +8,4 @@ def spec(file)
8
8
  end
9
9
 
10
10
  watch("spec/.*/*_spec\.rb") {|md| p md[0]; spec(md[0]) }
11
- watch('lib/.*/(.*)\.rb') {|md| p md[1]; spec("spec/unit/mongoid/#{md[1]}_spec.rb") }
11
+ watch('lib/(.*/.*)\.rb') {|md| p md[1]; spec("spec/unit/#{md[1]}_spec.rb") }
data/README.textile CHANGED
@@ -18,6 +18,11 @@
18
18
  Mongoid is developed against Ruby 1.8.6, 1.8.7, 1.9.1, 1.9.2
19
19
  </p>
20
20
 
21
+ <p>
22
+ Note API changes will be frequent until the 1.0 release, and do not expect
23
+ the gem to be of full production quality until that point.
24
+ </p>
25
+
21
26
  <h2>Mongoid in Action</h2>
22
27
 
23
28
  Initialize Mongoid:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.5
1
+ 0.4.7
data/lib/mongoid.rb CHANGED
@@ -25,10 +25,11 @@ gem "mongo", "0.15.1"
25
25
  gem "durran-validatable", "1.7.5"
26
26
  gem "will_paginate", "2.3.11"
27
27
 
28
+ require "delegate"
29
+ require "observer"
28
30
  require "validatable"
29
31
  require "active_support/callbacks"
30
32
  require "active_support/core_ext"
31
- require "delegate"
32
33
  require "will_paginate/collection"
33
34
  require "mongo"
34
35
  require "mongoid/associations"
@@ -37,6 +38,7 @@ require "mongoid/criteria"
37
38
  require "mongoid/extensions"
38
39
  require "mongoid/field"
39
40
  require "mongoid/document"
41
+ require "mongoid/timestamps"
40
42
 
41
43
  module Mongoid
42
44
 
@@ -2,4 +2,87 @@ require "mongoid/associations/decorator"
2
2
  require "mongoid/associations/factory"
3
3
  require "mongoid/associations/belongs_to_association"
4
4
  require "mongoid/associations/has_many_association"
5
- require "mongoid/associations/has_one_association"
5
+ require "mongoid/associations/has_one_association"
6
+
7
+ module Mongoid # :nodoc:
8
+ module Associations
9
+
10
+ # Adds the association back to the parent document. This macro is
11
+ # necessary to set the references from the child back to the parent
12
+ # document. If a child does not define this association calling
13
+ # persistence methods on the child object will cause a save to fail.
14
+ #
15
+ # Options:
16
+ #
17
+ # association_name: A +Symbol+ that matches the name of the parent class.
18
+ #
19
+ # Example:
20
+ #
21
+ # class Person < Mongoid::Document
22
+ # has_many :addresses
23
+ # end
24
+ #
25
+ # class Address < Mongoid::Document
26
+ # belongs_to :person
27
+ # end
28
+ def belongs_to(association_name)
29
+ @embedded = true
30
+ add_association(:belongs_to, association_name.to_s.classify, association_name)
31
+ end
32
+
33
+ # Adds the association from a parent document to its children. The name
34
+ # of the association needs to be a pluralized form of the child class
35
+ # name.
36
+ #
37
+ # Options:
38
+ #
39
+ # association_name: A +Symbol+ that is the plural child class name.
40
+ #
41
+ # Example:
42
+ #
43
+ # class Person < Mongoid::Document
44
+ # has_many :addresses
45
+ # end
46
+ #
47
+ # class Address < Mongoid::Document
48
+ # belongs_to :person
49
+ # end
50
+ def has_many(association_name)
51
+ add_association(:has_many, association_name.to_s.classify, association_name)
52
+ end
53
+
54
+ # Adds the association from a parent document to its child. The name
55
+ # of the association needs to be a singular form of the child class
56
+ # name.
57
+ #
58
+ # Options:
59
+ #
60
+ # association_name: A +Symbol+ that is the plural child class name.
61
+ #
62
+ # Example:
63
+ #
64
+ # class Person < Mongoid::Document
65
+ # has_many :addresses
66
+ # end
67
+ #
68
+ # class Address < Mongoid::Document
69
+ # belongs_to :person
70
+ # end
71
+ def has_one(association_name)
72
+ add_association(:has_one, association_name.to_s.titleize, association_name)
73
+ end
74
+
75
+ private
76
+ # Adds the association to the associations hash with the type as the key,
77
+ # then adds the accessors for the association.
78
+ def add_association(type, class_name, name)
79
+ define_method(name) do
80
+ Associations::Factory.create(type, name, self)
81
+ end
82
+ define_method("#{name}=") do |object|
83
+ object.parentize(self, name)
84
+ @attributes[name] = object.mongoidize
85
+ end
86
+ end
87
+ end
88
+ end
@@ -2,10 +2,12 @@ module Mongoid #:nodoc:
2
2
  module Associations #:nodoc:
3
3
  class HasManyAssociation < DelegateClass(Array) #:nodoc:
4
4
 
5
+ attr_accessor :association_name
6
+
5
7
  # Appends the object to the +Array+, setting its parent in
6
8
  # the process.
7
9
  def <<(object)
8
- object.parentize(@parent)
10
+ object.parentize(@parent, @association_name)
9
11
  @documents << object
10
12
  end
11
13
 
@@ -28,6 +30,7 @@ module Mongoid #:nodoc:
28
30
  # Returns the newly created object.
29
31
  def build(attributes)
30
32
  object = @klass.new(attributes)
33
+ object.parentize(@parent, @association_name)
31
34
  push(object)
32
35
  object
33
36
  end
@@ -49,11 +52,12 @@ module Mongoid #:nodoc:
49
52
  # essentially a proxy to an array itself.
50
53
  def initialize(association_name, document)
51
54
  @parent = document
52
- @klass = association_name.to_s.classify.constantize
53
- attributes = document.attributes[association_name]
55
+ @association_name = association_name
56
+ @klass = @association_name.to_s.classify.constantize
57
+ attributes = document.attributes[@association_name]
54
58
  @documents = attributes ? attributes.collect do |attribute|
55
59
  child = @klass.new(attribute)
56
- child.parent = document
60
+ child.parentize(@parent, @association_name)
57
61
  child
58
62
  end : []
59
63
  super(@documents)
@@ -57,15 +57,15 @@ module Mongoid #:nodoc:
57
57
 
58
58
  # Update the attributes of the +Document+. Will call save after the
59
59
  # attributes have been updated.
60
- def update_attributes(attributes = {})
61
- self.attributes = attributes; save
60
+ def update_attributes(attrs = {})
61
+ write_attributes(attrs); save
62
62
  end
63
63
 
64
64
  # Update the attributes of the +Document+. Will call save! after the
65
65
  # attributes have been updated, causing a +ValidationError+ if the
66
66
  # +Document+ failed validation.
67
- def update_attributes!(attribtues = {})
68
- self.attributes = attributes; save!
67
+ def update_attributes!(attrs = {})
68
+ write_attributes(attrs); save!
69
69
  end
70
70
 
71
71
  end
@@ -14,8 +14,23 @@ module Mongoid #:nodoc:
14
14
  #
15
15
  # <tt>criteria.execute</tt>
16
16
  class Criteria
17
+ attr_accessor :klass
17
18
  attr_reader :selector, :options, :type
18
19
 
20
+ AGGREGATE_REDUCE = "function(obj, prev) { prev.count++; }"
21
+ # Aggregate the criteria. This will take the internally built selector and options
22
+ # and pass them on to the Ruby driver's +group()+ method on the collection. The
23
+ # collection itself will be retrieved from the class provided, and once the
24
+ # query has returned it will provided a grouping of keys with counts.
25
+ #
26
+ # Example:
27
+ #
28
+ # <tt>criteria.select(:field1).where(:field1 => "Title").aggregate(Person)</tt>
29
+ def aggregate(klass = nil)
30
+ @klass = klass if klass
31
+ @klass.collection.group(@options[:fields], @selector, { :count => 0 }, AGGREGATE_REDUCE)
32
+ end
33
+
19
34
  # Adds a criterion to the +Criteria+ that specifies values that must all
20
35
  # be matched in order to return results. Similar to an "in" clause but the
21
36
  # underlying conditional logic is an "AND" and not an "OR". The MongoDB
@@ -67,9 +82,10 @@ module Mongoid #:nodoc:
67
82
  #
68
83
  # If this is a +Criteria+ to find multiple results, will return an +Array+ of
69
84
  # objects of the type of class provided.
70
- def execute(klass)
71
- return klass.new(klass.collection.find_one(@selector, @options)) if type == :first
72
- return klass.collection.find(@selector, @options).collect { |doc| klass.new(doc) }
85
+ def execute(klass = nil)
86
+ @klass = klass if klass
87
+ return @klass.new(klass.collection.find_one(@selector, @options)) if type == :first
88
+ return @klass.collection.find(@selector, @options).collect { |doc| @klass.new(doc) }
73
89
  end
74
90
 
75
91
  # Adds a criterion to the +Criteria+ that specifies additional options
@@ -88,6 +104,27 @@ module Mongoid #:nodoc:
88
104
  @options = extras; self
89
105
  end
90
106
 
107
+ GROUP_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
108
+ # Groups the criteria. This will take the internally built selector and options
109
+ # and pass them on to the Ruby driver's +group()+ method on the collection. The
110
+ # collection itself will be retrieved from the class provided, and once the
111
+ # query has returned it will provided a grouping of keys with objects.
112
+ #
113
+ # Example:
114
+ #
115
+ # <tt>criteria.select(:field1).where(:field1 => "Title").group(Person)</tt>
116
+ def group(klass = nil)
117
+ @klass = klass if klass
118
+ @klass.collection.group(
119
+ @options[:fields],
120
+ @selector,
121
+ { :group => [] },
122
+ GROUP_REDUCE
123
+ ).collect do |docs|
124
+ docs["group"] = docs["group"].collect { |attrs| @klass.new(attrs) }; docs
125
+ end
126
+ end
127
+
91
128
  # Adds a criterion to the +Criteria+ that specifies values where any can
92
129
  # be matched in order to return results. This is similar to an SQL "IN"
93
130
  # clause. The MongoDB conditional operator that will be used is "$in".
@@ -129,8 +166,9 @@ module Mongoid #:nodoc:
129
166
  # Options:
130
167
  #
131
168
  # type: One of :all, :first:, or :last
132
- def initialize(type)
133
- @selector, @options, @type = {}, {}, type
169
+ # klass: The class to execute on.
170
+ def initialize(type, klass = nil)
171
+ @selector, @options, @type, @klass = {}, {}, type, klass
134
172
  end
135
173
 
136
174
  # Adds a criterion to the +Criteria+ that specifies the maximum number of
@@ -170,6 +208,14 @@ module Mongoid #:nodoc:
170
208
  exclusions.each { |key, value| @selector[key] = { "$nin" => value } }; self
171
209
  end
172
210
 
211
+ # Returns the offset option. If a per_page option is in the list then it
212
+ # will replace it with a skip parameter and return the same value. Defaults
213
+ # to 20 if nothing was provided.
214
+ def offset
215
+ offset = @options.delete(:per_page) || 20
216
+ @options[:skip] ||= offset
217
+ end
218
+
173
219
  # Adds a criterion to the +Criteria+ that specifies the sort order of
174
220
  # the returned documents in the database. Similar to a SQL "ORDER BY".
175
221
  #
@@ -186,6 +232,12 @@ module Mongoid #:nodoc:
186
232
  @options[:sort] = params; self
187
233
  end
188
234
 
235
+ # Either returns the page option and removes it from the options, or
236
+ # returns a default value of 1.
237
+ def page
238
+ @options.delete(:page) || 1
239
+ end
240
+
189
241
  # Adds a criterion to the +Criteria+ that specifies the fields that will
190
242
  # get returned from the Document. Used mainly for list views that do not
191
243
  # require all fields to be present. This is similar to SQL "SELECT" values.
@@ -1,13 +1,11 @@
1
1
  module Mongoid #:nodoc:
2
2
  class Document
3
3
  include ActiveSupport::Callbacks
4
- include Validatable
5
- include Commands
4
+ include Commands, Observable, Validatable
5
+ extend Associations
6
6
 
7
- AGGREGATE_REDUCE = "function(obj, prev) { prev.count++; }"
8
- GROUP_BY_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
9
-
10
- attr_accessor :attributes, :parent
7
+ attr_accessor :association_name, :parent
8
+ attr_reader :attributes
11
9
 
12
10
  define_callbacks \
13
11
  :after_create,
@@ -19,37 +17,22 @@ module Mongoid #:nodoc:
19
17
 
20
18
  class << self
21
19
 
22
- # Get an aggregate count for the supplied group of fields and the
23
- # selector that is provided.
24
- def aggregate(fields, params = {})
25
- selector = params[:conditions]
26
- collection.group(fields, selector, { :count => 0 }, AGGREGATE_REDUCE)
27
- end
28
-
29
- # Adds the association back to the parent document. This macro is
30
- # necessary to set the references from the child back to the parent
31
- # document. If a child does not define this association calling
32
- # persistence methods on the child object will cause a save to fail.
20
+ # Find +Documents+ given the conditions.
33
21
  #
34
22
  # Options:
35
23
  #
36
- # association_name: A +Symbol+ that matches the name of the parent class.
37
- #
38
- # Example:
39
- #
40
- # class Person < Mongoid::Document
41
- # has_many :addresses
42
- # end
24
+ # args: A +Hash+ with a conditions key and other options
43
25
  #
44
- # class Address < Mongoid::Document
45
- # belongs_to :person
46
- # end
47
- def belongs_to(association_name)
48
- @embedded = true
49
- add_association(:belongs_to, association_name.to_s.classify, association_name)
26
+ # <tt>Person.all(:conditions => { :attribute => "value" })</tt>
27
+ def all(*args)
28
+ find(:all, *args)
50
29
  end
51
30
 
52
- # Get the Mongo::Collection associated with this Document.
31
+ # Returns the collection associated with this +Document+. If the
32
+ # document is embedded, there will be no collection associated
33
+ # with it.
34
+ #
35
+ # Returns: <tt>Mongo::Collection</tt>
53
36
  def collection
54
37
  return nil if @embedded
55
38
  @collection_name = self.to_s.demodulize.tableize
@@ -97,85 +80,57 @@ module Mongoid #:nodoc:
97
80
  Criteria.translate(*args).execute(self)
98
81
  end
99
82
 
100
- # Find a single Document given the passed selector, which is a Hash of attributes that
101
- # must match the Document in the database exactly.
83
+ # Find the first +Document+ given the conditions.
84
+ #
85
+ # Options:
86
+ #
87
+ # args: A +Hash+ with a conditions key and other options
88
+ #
89
+ # <tt>Person.first(:conditions => { :attribute => "value" })</tt>
102
90
  def first(*args)
103
91
  find(:first, *args)
104
92
  end
105
93
 
106
- # Find all Documents given the passed selector, which is a Hash of attributes that
107
- # must match the Document in the database exactly.
108
- def all(*args)
109
- find(:all, *args)
94
+ # Adds an index on the field specified. Options can be :unique => true or
95
+ # :unique => false. It will default to the latter.
96
+ def index(name, options = { :unique => false })
97
+ collection.create_index(name, options)
110
98
  end
111
99
 
112
- # Find all Documents given the supplied criteria, grouped by the fields
113
- # provided.
114
- def group_by(fields, params = {})
115
- selector = params[:condition]
116
- collection.group(fields, selector, { :group => [] }, GROUP_BY_REDUCE).collect do |docs|
117
- docs["group"] = docs["group"].collect { |attrs| new(attrs) }; docs
118
- end
100
+ # Defines the field that will be used for the id of this +Document+. This
101
+ # set the id of this +Document+ before save to a parameterized version of
102
+ # the field that was supplied. This is good for use for readable URLS in
103
+ # web applications and *MUST* be defined on documents that are embedded
104
+ # in order for proper updates in has_may associations.
105
+ def key(*fields)
106
+ @primary_key = fields
107
+ before_save :generate_key
119
108
  end
120
109
 
121
- # Adds the association from a parent document to its children. The name
122
- # of the association needs to be a pluralized form of the child class
123
- # name.
110
+ # Returns the primary key field of the +Document+
111
+ def primary_key
112
+ @primary_key
113
+ end
114
+
115
+ # Find all documents in paginated fashion given the supplied arguments.
116
+ # If no parameters are passed just default to offset 0 and limit 20.
124
117
  #
125
118
  # Options:
126
119
  #
127
- # association_name: A +Symbol+ that is the plural child class name.
120
+ # params: A +Hash+ of params to pass to the Criteria API.
128
121
  #
129
122
  # Example:
130
123
  #
131
- # class Person < Mongoid::Document
132
- # has_many :addresses
133
- # end
124
+ # <tt>Person.paginate(:conditions => { :field => "Test" }, :page => 1,
125
+ # :per_page => 20)</tt>
134
126
  #
135
- # class Address < Mongoid::Document
136
- # belongs_to :person
137
- # end
138
- def has_many(association_name)
139
- add_association(:has_many, association_name.to_s.classify, association_name)
140
- end
141
-
142
- # Create a one-to-many association between Documents.
143
- def has_one(association_name)
144
- add_association(:has_one, association_name.to_s.titleize, association_name)
145
- end
146
-
147
- # Adds timestamps on the Document in the form of the fields 'created_on'
148
- # and 'last_modified'
149
- def has_timestamps
150
- field :created_at
151
- field :last_modified
152
- class_eval do
153
- before_create \
154
- :update_created_at,
155
- :update_last_modified
156
- before_save :update_last_modified
157
- end
158
- end
159
-
160
- # Adds an index on the field specified. Options can be :unique => true or
161
- # :unique => false. It will default to the latter.
162
- def index(name, options = { :unique => false })
163
- collection.create_index(name, options)
164
- end
165
-
166
- # Find all documents in paginated fashion given the supplied arguments.
167
- # If no parameters are passed just default to offset 0 and limit 20.
127
+ # Returns paginated array of docs.
168
128
  def paginate(params = {})
169
- selector = params[:conditions]
170
- WillPaginate::Collection.create(
171
- params[:page] || 1,
172
- params[:per_page] || 20,
173
- 0) do |pager|
174
- results = collection.find(selector, { :sort => params[:sort],
175
- :limit => pager.per_page,
176
- :skip => pager.offset })
177
- pager.total_entries = results.count
178
- pager.replace(results.collect { |doc| new(doc) })
129
+ criteria = Criteria.translate(:all, params)
130
+ WillPaginate::Collection.create(criteria.page, criteria.offset, 0) do |pager|
131
+ results = criteria.execute(self)
132
+ pager.total_entries = results.size
133
+ pager.replace(results)
179
134
  end
180
135
  end
181
136
 
@@ -193,7 +148,7 @@ module Mongoid #:nodoc:
193
148
  #
194
149
  # Returns: <tt>Criteria</tt>
195
150
  def select(*args)
196
- Criteria.new(:all).select(*args)
151
+ Criteria.new(:all, self).select(*args)
197
152
  end
198
153
 
199
154
  end
@@ -219,6 +174,12 @@ module Mongoid #:nodoc:
219
174
  def initialize(attributes = {})
220
175
  @attributes = attributes.symbolize_keys if attributes
221
176
  @attributes = {} unless attributes
177
+ generate_key
178
+ end
179
+
180
+ # Return the +Document+ primary key.
181
+ def primary_key
182
+ self.class.primary_key
222
183
  end
223
184
 
224
185
  # Returns true is the Document has not been persisted to the database, false if it has.
@@ -226,27 +187,10 @@ module Mongoid #:nodoc:
226
187
  @attributes[:_id].nil?
227
188
  end
228
189
 
229
- # Returns the id of the Document
230
- def to_param
231
- id.to_s
232
- end
233
-
234
- private
235
-
236
- class << self
237
-
238
- # Adds the association to the associations hash with the type as the key,
239
- # then adds the accessors for the association.
240
- def add_association(type, class_name, name)
241
- define_method(name) do
242
- Mongoid::Associations::Factory.create(type, name, self)
243
- end
244
- define_method("#{name}=") do |object|
245
- object.parentize(self)
246
- @attributes[name] = object.mongoidize
247
- end
248
- end
249
-
190
+ # Notify observers that this Document has changed.
191
+ def notify
192
+ changed(true)
193
+ notify_observers(self)
250
194
  end
251
195
 
252
196
  # Read from the attributes hash.
@@ -255,23 +199,37 @@ module Mongoid #:nodoc:
255
199
  fields[symbol].value(@attributes[symbol])
256
200
  end
257
201
 
258
- # Update the created_at field on the Document to the current time. This is
259
- # only called on create.
260
- def update_created_at
261
- self.created_at = Time.now
202
+ # Returns the id of the Document
203
+ def to_param
204
+ id.to_s
262
205
  end
263
206
 
264
- # Update the last_modified field on the Document to the current time.
265
- # This is only called on create and on save.
266
- def update_last_modified
267
- self.last_modified = Time.now
207
+ # Update the document based on notify from child
208
+ def update(child)
209
+ @attributes.insert(child.association_name, child.attributes)
210
+ notify
268
211
  end
269
212
 
270
213
  # Write to the attributes hash.
271
214
  def write_attribute(name, value)
272
215
  symbol = name.to_sym
273
- @attributes[name.to_sym] = fields[symbol].value(value)
216
+ @attributes[name.to_sym] = value
217
+ notify
218
+ end
219
+
220
+ # Writes all the attributes of this Document, and delegate up to
221
+ # the parent.
222
+ def write_attributes(attrs)
223
+ @attributes = attrs
224
+ notify
274
225
  end
275
226
 
227
+ private
228
+ def generate_key
229
+ if primary_key
230
+ values = primary_key.collect { |key| @attributes[key] }
231
+ @attributes[:_id] = values.join(" ").parameterize.to_s
232
+ end
233
+ end
276
234
  end
277
235
  end