couchrest_model 1.0.0 → 1.1.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -1
- data/Gemfile.lock +19 -20
- data/README.md +145 -20
- data/VERSION +1 -1
- data/couchrest_model.gemspec +2 -3
- data/history.txt +14 -0
- data/lib/couchrest/model/associations.rb +4 -4
- data/lib/couchrest/model/base.rb +5 -0
- data/lib/couchrest/model/callbacks.rb +1 -2
- data/lib/couchrest/model/collection.rb +1 -1
- data/lib/couchrest/model/designs/view.rb +486 -0
- data/lib/couchrest/model/designs.rb +81 -0
- data/lib/couchrest/model/document_queries.rb +1 -1
- data/lib/couchrest/model/persistence.rb +25 -16
- data/lib/couchrest/model/properties.rb +5 -1
- data/lib/couchrest/model/property.rb +2 -2
- data/lib/couchrest/model/proxyable.rb +152 -0
- data/lib/couchrest/model/typecast.rb +1 -1
- data/lib/couchrest/model/validations/casted_model.rb +3 -1
- data/lib/couchrest/model/validations/locale/en.yml +1 -1
- data/lib/couchrest/model/validations/uniqueness.rb +6 -7
- data/lib/couchrest/model/validations.rb +1 -0
- data/lib/couchrest/model/views.rb +11 -9
- data/lib/couchrest_model.rb +3 -0
- data/spec/couchrest/assocations_spec.rb +2 -2
- data/spec/couchrest/base_spec.rb +15 -1
- data/spec/couchrest/casted_model_spec.rb +30 -12
- data/spec/couchrest/class_proxy_spec.rb +2 -2
- data/spec/couchrest/collection_spec.rb +89 -0
- data/spec/couchrest/designs/view_spec.rb +766 -0
- data/spec/couchrest/designs_spec.rb +110 -0
- data/spec/couchrest/persistence_spec.rb +36 -7
- data/spec/couchrest/property_spec.rb +15 -0
- data/spec/couchrest/proxyable_spec.rb +329 -0
- data/spec/couchrest/{validations.rb → validations_spec.rb} +1 -3
- data/spec/couchrest/view_spec.rb +19 -91
- data/spec/fixtures/base.rb +8 -6
- data/spec/fixtures/more/article.rb +1 -1
- data/spec/fixtures/more/course.rb +4 -2
- metadata +21 -76
- data/lib/couchrest/model/view.rb +0 -190
@@ -0,0 +1,486 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Model
|
3
|
+
module Designs
|
4
|
+
|
5
|
+
#
|
6
|
+
# A proxy class that allows view queries to be created using
|
7
|
+
# chained method calls. After each call a new instance of the method
|
8
|
+
# is created based on the original in a similar fashion to ruby's Sequel
|
9
|
+
# library, or Rails 3's Arel.
|
10
|
+
#
|
11
|
+
# CouchDB views have inherent limitations, so joins and filters as used in
|
12
|
+
# a normal relational database are not possible.
|
13
|
+
#
|
14
|
+
class View
|
15
|
+
include Enumerable
|
16
|
+
|
17
|
+
attr_accessor :owner, :model, :name, :query, :result
|
18
|
+
|
19
|
+
# Initialize a new View object. This method should not be called from
|
20
|
+
# outside CouchRest Model.
|
21
|
+
def initialize(parent, new_query = {}, name = nil)
|
22
|
+
if parent.is_a?(Class) && parent < CouchRest::Model::Base
|
23
|
+
raise "Name must be provided for view to be initialized" if name.nil?
|
24
|
+
self.model = parent
|
25
|
+
self.owner = parent
|
26
|
+
self.name = name.to_s
|
27
|
+
# Default options:
|
28
|
+
self.query = { :reduce => false }
|
29
|
+
elsif parent.is_a?(self.class)
|
30
|
+
self.model = (new_query.delete(:proxy) || parent.model)
|
31
|
+
self.owner = parent.owner
|
32
|
+
self.name = parent.name
|
33
|
+
self.query = parent.query.dup
|
34
|
+
else
|
35
|
+
raise "View cannot be initialized without a parent Model or View"
|
36
|
+
end
|
37
|
+
query.update(new_query)
|
38
|
+
super()
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# == View Execution Methods
|
43
|
+
#
|
44
|
+
# Request to the CouchDB database using the current query values.
|
45
|
+
|
46
|
+
# Return each row wrapped in a ViewRow object. Unlike the raw
|
47
|
+
# CouchDB request, this will provide an empty array if there
|
48
|
+
# are no results.
|
49
|
+
def rows
|
50
|
+
return @rows if @rows
|
51
|
+
if execute && result['rows']
|
52
|
+
@rows ||= result['rows'].map{|v| ViewRow.new(v, model)}
|
53
|
+
else
|
54
|
+
[ ]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Fetch all the documents the view can access. If the view has
|
59
|
+
# not already been prepared for including documents in the query,
|
60
|
+
# it will be added automatically and reset any previously cached
|
61
|
+
# results.
|
62
|
+
def all
|
63
|
+
include_docs!
|
64
|
+
docs
|
65
|
+
end
|
66
|
+
|
67
|
+
# Provide all the documents from the view. If the view has not been
|
68
|
+
# prepared with the +include_docs+ option, each document will be
|
69
|
+
# loaded individually.
|
70
|
+
def docs
|
71
|
+
@docs ||= rows.map{|r| r.doc}
|
72
|
+
end
|
73
|
+
|
74
|
+
# If another request has been made on the view, this will return
|
75
|
+
# the first document in the set. If not, a new query object will be
|
76
|
+
# generated with a limit of 1 so that only the first document is
|
77
|
+
# loaded.
|
78
|
+
def first
|
79
|
+
result ? all.first : limit(1).all.first
|
80
|
+
end
|
81
|
+
|
82
|
+
# Same as first but will order the view in descending order. This
|
83
|
+
# does not however reverse the search keys or the offset, so if you
|
84
|
+
# are using a +startkey+ and +endkey+ you might end up with
|
85
|
+
# unexpected results.
|
86
|
+
#
|
87
|
+
# If in doubt, don't use this method!
|
88
|
+
#
|
89
|
+
def last
|
90
|
+
result ? all.last : limit(1).descending.all.last
|
91
|
+
end
|
92
|
+
|
93
|
+
# Perform a count operation based on the current view. If the view
|
94
|
+
# can be reduced, the reduce will be performed and return the first
|
95
|
+
# value. This is okay for most simple queries, but may provide
|
96
|
+
# unexpected results if your reduce method does not calculate
|
97
|
+
# the total number of documents in a result set.
|
98
|
+
#
|
99
|
+
# Trying to use this method with the group option will raise an error.
|
100
|
+
#
|
101
|
+
# If no reduce function is defined, a query will be performed
|
102
|
+
# to return the total number of rows, this is the equivalant of:
|
103
|
+
#
|
104
|
+
# view.limit(0).total_rows
|
105
|
+
#
|
106
|
+
def count
|
107
|
+
raise "View#count cannot be used with group options" if query[:group]
|
108
|
+
if can_reduce?
|
109
|
+
row = reduce.rows.first
|
110
|
+
row.nil? ? 0 : row.value
|
111
|
+
else
|
112
|
+
limit(0).total_rows
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Check to see if the array of documents is empty. This *will*
|
117
|
+
# perform the query and return all documents ready to use, if you don't
|
118
|
+
# want to load anything, use +#total_rows+ or +#count+ instead.
|
119
|
+
def empty?
|
120
|
+
all.empty?
|
121
|
+
end
|
122
|
+
|
123
|
+
# Run through each document provided by the +#all+ method.
|
124
|
+
# This is also used by the Enumerator mixin to provide all the standard
|
125
|
+
# ruby collection directly on the view.
|
126
|
+
def each(&block)
|
127
|
+
all.each(&block)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Wrapper for the results offset. As per the CouchDB API,
|
131
|
+
# this may be nil if groups are used.
|
132
|
+
def offset
|
133
|
+
execute['offset']
|
134
|
+
end
|
135
|
+
|
136
|
+
# Wrapper for the total_rows value provided by the query. As per the
|
137
|
+
# CouchDB API, this may be nil if groups are used.
|
138
|
+
def total_rows
|
139
|
+
execute['total_rows']
|
140
|
+
end
|
141
|
+
|
142
|
+
# Convenience wrapper around the rows result set. This will provide
|
143
|
+
# and array of keys.
|
144
|
+
def keys
|
145
|
+
rows.map{|r| r.key}
|
146
|
+
end
|
147
|
+
|
148
|
+
# Convenience wrapper to provide all the values from the route
|
149
|
+
# set without having to go through +rows+.
|
150
|
+
def values
|
151
|
+
rows.map{|r| r.value}
|
152
|
+
end
|
153
|
+
|
154
|
+
# Accept requests as if the view was an array. Used for backwards compatibity
|
155
|
+
# with older queries:
|
156
|
+
#
|
157
|
+
# Model.all(:raw => true, :limit => 0)['total_rows']
|
158
|
+
#
|
159
|
+
# In this example, the raw option will be ignored, and the total rows
|
160
|
+
# will still be accessible.
|
161
|
+
#
|
162
|
+
def [](value)
|
163
|
+
execute[value]
|
164
|
+
end
|
165
|
+
|
166
|
+
# No yet implemented. Eventually this will provide a raw hash
|
167
|
+
# of the information CouchDB holds about the view.
|
168
|
+
def info
|
169
|
+
raise "Not yet implemented"
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
# == View Filter Methods
|
174
|
+
#
|
175
|
+
# View filters return a copy of the view instance with the query
|
176
|
+
# modified appropriatly. Errors will be raised if the methods
|
177
|
+
# are combined in an incorrect fashion.
|
178
|
+
#
|
179
|
+
|
180
|
+
# Find all entries in the index whose key matches the value provided.
|
181
|
+
#
|
182
|
+
# Cannot be used when the +#startkey+ or +#endkey+ have been set.
|
183
|
+
def key(value)
|
184
|
+
raise "View#key cannot be used when startkey or endkey have been set" unless query[:startkey].nil? && query[:endkey].nil?
|
185
|
+
update_query(:key => value)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Find all index keys that start with the value provided. May or may
|
189
|
+
# not be used in conjunction with the +endkey+ option.
|
190
|
+
#
|
191
|
+
# When the +#descending+ option is used (not the default), the start
|
192
|
+
# and end keys should be reversed, as per the CouchDB API.
|
193
|
+
#
|
194
|
+
# Cannot be used if the key has been set.
|
195
|
+
def startkey(value)
|
196
|
+
raise "View#startkey cannot be used when key has been set" unless query[:key].nil?
|
197
|
+
update_query(:startkey => value)
|
198
|
+
end
|
199
|
+
|
200
|
+
# The result set should start from the position of the provided document.
|
201
|
+
# The value may be provided as an object that responds to the +#id+ call
|
202
|
+
# or a string.
|
203
|
+
def startkey_doc(value)
|
204
|
+
update_query(:startkey_docid => value.is_a?(String) ? value : value.id)
|
205
|
+
end
|
206
|
+
|
207
|
+
# The opposite of +#startkey+, finds all index entries whose key is before
|
208
|
+
# the value specified.
|
209
|
+
#
|
210
|
+
# See the +#startkey+ method for more details and the +#inclusive_end+
|
211
|
+
# option.
|
212
|
+
def endkey(value)
|
213
|
+
raise "View#endkey cannot be used when key has been set" unless query[:key].nil?
|
214
|
+
update_query(:endkey => value)
|
215
|
+
end
|
216
|
+
|
217
|
+
# The result set should end at the position of the provided document.
|
218
|
+
# The value may be provided as an object that responds to the +#id+
|
219
|
+
# call or a string.
|
220
|
+
def endkey_doc(value)
|
221
|
+
update_query(:endkey_docid => value.is_a?(String) ? value : value.id)
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
# The results should be provided in descending order.
|
226
|
+
#
|
227
|
+
# Descending is false by default, this method will enable it and cannot
|
228
|
+
# be undone.
|
229
|
+
def descending
|
230
|
+
update_query(:descending => true)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Limit the result set to the value supplied.
|
234
|
+
def limit(value)
|
235
|
+
update_query(:limit => value)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Skip the number of entries in the index specified by value. This would be
|
239
|
+
# the equivilent of an offset in SQL.
|
240
|
+
#
|
241
|
+
# The CouchDB documentation states that the skip option should not be used
|
242
|
+
# with large data sets as it is inefficient. Use the +startkey_doc+ method
|
243
|
+
# instead to skip ranges efficiently.
|
244
|
+
def skip(value = 0)
|
245
|
+
update_query(:skip => value)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Use the reduce function on the view. If none is available this method
|
249
|
+
# will fail.
|
250
|
+
def reduce
|
251
|
+
raise "Cannot reduce a view without a reduce method" unless can_reduce?
|
252
|
+
update_query(:reduce => true, :include_docs => nil)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Control whether the reduce function reduces to a set of distinct keys
|
256
|
+
# or to a single result row.
|
257
|
+
#
|
258
|
+
# By default the value is false, and can only be set when the view's
|
259
|
+
# +#reduce+ option has been set.
|
260
|
+
def group
|
261
|
+
raise "View#reduce must have been set before grouping is permitted" unless query[:reduce]
|
262
|
+
update_query(:group => true)
|
263
|
+
end
|
264
|
+
|
265
|
+
# Will set the level the grouping should be performed to. As per the
|
266
|
+
# CouchDB API, it only makes sense when the index key is an array.
|
267
|
+
#
|
268
|
+
# This will automatically set the group option.
|
269
|
+
def group_level(value)
|
270
|
+
group.update_query(:group_level => value.to_i)
|
271
|
+
end
|
272
|
+
|
273
|
+
def include_docs
|
274
|
+
update_query.include_docs!
|
275
|
+
end
|
276
|
+
|
277
|
+
### Special View Filter Methods
|
278
|
+
|
279
|
+
# Specify the database the view should use. If not defined,
|
280
|
+
# an attempt will be made to load its value from the model.
|
281
|
+
def database(value)
|
282
|
+
update_query(:database => value)
|
283
|
+
end
|
284
|
+
|
285
|
+
# Set the view's proxy that will be used instead of the model
|
286
|
+
# for any future searches. As soon as this enters the
|
287
|
+
# new object's initializer it will be removed and replace
|
288
|
+
# the model object.
|
289
|
+
#
|
290
|
+
# See the Proxyable mixin for more details.
|
291
|
+
#
|
292
|
+
def proxy(value)
|
293
|
+
update_query(:proxy => value)
|
294
|
+
end
|
295
|
+
|
296
|
+
# Return any cached values to their nil state so that any queries
|
297
|
+
# requested later will have a fresh set of data.
|
298
|
+
def reset!
|
299
|
+
self.result = nil
|
300
|
+
@rows = nil
|
301
|
+
@docs = nil
|
302
|
+
end
|
303
|
+
|
304
|
+
# == Kaminari compatible pagination support
|
305
|
+
#
|
306
|
+
# Based on the really simple support for scoped pagination in the
|
307
|
+
# the Kaminari gem, we provide compatible methods here to perform
|
308
|
+
# the same actions you'd expect.
|
309
|
+
#
|
310
|
+
|
311
|
+
def page(page)
|
312
|
+
limit(owner.default_per_page).skip(owner.default_per_page * ([page.to_i, 1].max - 1))
|
313
|
+
end
|
314
|
+
|
315
|
+
def per(num)
|
316
|
+
raise "View#page must be called before #per!" if limit_value.nil? || offset_value.nil?
|
317
|
+
if (n = num.to_i) <= 0
|
318
|
+
self
|
319
|
+
else
|
320
|
+
limit(num).skip(offset_value / limit_value * n)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def total_count
|
325
|
+
@total_count ||= limit(nil).skip(nil).count
|
326
|
+
end
|
327
|
+
|
328
|
+
def offset_value
|
329
|
+
query[:skip]
|
330
|
+
end
|
331
|
+
|
332
|
+
def limit_value
|
333
|
+
query[:limit]
|
334
|
+
end
|
335
|
+
|
336
|
+
def num_pages
|
337
|
+
(total_count.to_f / limit_value).ceil
|
338
|
+
end
|
339
|
+
|
340
|
+
def current_page
|
341
|
+
(offset_value / limit_value) + 1
|
342
|
+
end
|
343
|
+
|
344
|
+
|
345
|
+
|
346
|
+
protected
|
347
|
+
|
348
|
+
def include_docs!
|
349
|
+
raise "Cannot include documents in view that has been reduced!" if query[:reduce]
|
350
|
+
reset! if result && !include_docs?
|
351
|
+
query[:include_docs] = true
|
352
|
+
self
|
353
|
+
end
|
354
|
+
|
355
|
+
def include_docs?
|
356
|
+
!!query[:include_docs]
|
357
|
+
end
|
358
|
+
|
359
|
+
def update_query(new_query = {})
|
360
|
+
self.class.new(self, new_query)
|
361
|
+
end
|
362
|
+
|
363
|
+
def design_doc
|
364
|
+
model.design_doc
|
365
|
+
end
|
366
|
+
|
367
|
+
def can_reduce?
|
368
|
+
!design_doc['views'][name]['reduce'].blank?
|
369
|
+
end
|
370
|
+
|
371
|
+
def use_database
|
372
|
+
query[:database] || model.database
|
373
|
+
end
|
374
|
+
|
375
|
+
def execute
|
376
|
+
return self.result if result
|
377
|
+
raise "Database must be defined in model or view!" if use_database.nil?
|
378
|
+
retryable = true
|
379
|
+
# Remove the reduce value if its not needed
|
380
|
+
query.delete(:reduce) unless can_reduce?
|
381
|
+
begin
|
382
|
+
self.result = model.design_doc.view_on(use_database, name, query.reject{|k,v| v.nil?})
|
383
|
+
rescue RestClient::ResourceNotFound => e
|
384
|
+
if retryable
|
385
|
+
model.save_design_doc(use_database)
|
386
|
+
retryable = false
|
387
|
+
retry
|
388
|
+
else
|
389
|
+
raise e
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
# Class Methods
|
395
|
+
class << self
|
396
|
+
|
397
|
+
# Simplified view creation. A new view will be added to the
|
398
|
+
# provided model's design document using the name and options.
|
399
|
+
#
|
400
|
+
# If the view name starts with "by_" and +:by+ is not provided in
|
401
|
+
# the options, the new view's map method will be interpretted and
|
402
|
+
# generated automatically. For example:
|
403
|
+
#
|
404
|
+
# View.create(Meeting, "by_date_and_name")
|
405
|
+
#
|
406
|
+
# Will create a view that searches by the date and name properties.
|
407
|
+
# Explicity setting the attributes to use is possible using the
|
408
|
+
# +:by+ option. For example:
|
409
|
+
#
|
410
|
+
# View.create(Meeting, "by_date_and_name", :by => [:date, :firstname, :lastname])
|
411
|
+
#
|
412
|
+
# The view name is the same, but three keys would be used in the
|
413
|
+
# subsecuent index.
|
414
|
+
#
|
415
|
+
def create(model, name, opts = {})
|
416
|
+
|
417
|
+
unless opts[:map]
|
418
|
+
if opts[:by].nil? && name.to_s =~ /^by_(.+)/
|
419
|
+
opts[:by] = $1.split(/_and_/)
|
420
|
+
end
|
421
|
+
|
422
|
+
raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?
|
423
|
+
|
424
|
+
opts[:guards] ||= []
|
425
|
+
opts[:guards].push "(doc['#{model.model_type_key}'] == '#{model.to_s}')"
|
426
|
+
|
427
|
+
keys = opts[:by].map{|o| "doc['#{o}']"}
|
428
|
+
emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]"
|
429
|
+
opts[:guards] += keys.map{|k| "(#{k} != null)"}
|
430
|
+
opts[:map] = <<-EOF
|
431
|
+
function(doc) {
|
432
|
+
if (#{opts[:guards].join(' && ')}) {
|
433
|
+
emit(#{emit}, 1);
|
434
|
+
}
|
435
|
+
}
|
436
|
+
EOF
|
437
|
+
opts[:reduce] = <<-EOF
|
438
|
+
function(key, values, rereduce) {
|
439
|
+
return sum(values);
|
440
|
+
}
|
441
|
+
EOF
|
442
|
+
end
|
443
|
+
|
444
|
+
model.design_doc['views'] ||= {}
|
445
|
+
view = model.design_doc['views'][name.to_s] = { }
|
446
|
+
view['map'] = opts[:map]
|
447
|
+
view['reduce'] = opts[:reduce] if opts[:reduce]
|
448
|
+
view
|
449
|
+
end
|
450
|
+
|
451
|
+
end
|
452
|
+
|
453
|
+
end
|
454
|
+
|
455
|
+
# A special wrapper class that provides easy access to the key
|
456
|
+
# fields in a result row.
|
457
|
+
class ViewRow < Hash
|
458
|
+
attr_reader :model
|
459
|
+
def initialize(hash, model)
|
460
|
+
@model = model
|
461
|
+
replace(hash)
|
462
|
+
end
|
463
|
+
def id
|
464
|
+
self["id"]
|
465
|
+
end
|
466
|
+
def key
|
467
|
+
self["key"]
|
468
|
+
end
|
469
|
+
def value
|
470
|
+
self['value']
|
471
|
+
end
|
472
|
+
def raw_doc
|
473
|
+
self['doc']
|
474
|
+
end
|
475
|
+
# Send a request for the linked document either using the "id" field's
|
476
|
+
# value, or the ["value"]["_id"] used for linked documents.
|
477
|
+
def doc
|
478
|
+
return model.build_from_database(self['doc']) if self['doc']
|
479
|
+
doc_id = (value.is_a?(Hash) && value['_id']) ? value['_id'] : self.id
|
480
|
+
doc_id ? model.get(doc_id) : nil
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
|
2
|
+
#### NOTE Work in progress! Not yet used!
|
3
|
+
|
4
|
+
module CouchRest
|
5
|
+
module Model
|
6
|
+
|
7
|
+
# A design block in CouchRest Model groups together the functionality of CouchDB's
|
8
|
+
# design documents in a simple block definition.
|
9
|
+
#
|
10
|
+
# class Person < CouchRest::Model::Base
|
11
|
+
# property :name
|
12
|
+
# timestamps!
|
13
|
+
#
|
14
|
+
# design do
|
15
|
+
# view :by_name
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
module Designs
|
20
|
+
extend ActiveSupport::Concern
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
|
24
|
+
# Add views and other design document features
|
25
|
+
# to the current model.
|
26
|
+
def design(*args, &block)
|
27
|
+
mapper = DesignMapper.new(self)
|
28
|
+
mapper.create_view_method(:all)
|
29
|
+
|
30
|
+
mapper.instance_eval(&block) if block_given?
|
31
|
+
|
32
|
+
req_design_doc_refresh
|
33
|
+
end
|
34
|
+
|
35
|
+
# Override the default page pagination value:
|
36
|
+
#
|
37
|
+
# class Person < CouchRest::Model::Base
|
38
|
+
# paginates_per 10
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
def paginates_per(val)
|
42
|
+
@_default_per_page = val
|
43
|
+
end
|
44
|
+
|
45
|
+
# The models number of documents to return
|
46
|
+
# by default when performing pagination.
|
47
|
+
# Returns 25 unless explicitly overridden via <tt>paginates_per</tt>
|
48
|
+
def default_per_page
|
49
|
+
@_default_per_page || 25
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
class DesignMapper
|
56
|
+
|
57
|
+
attr_accessor :model
|
58
|
+
|
59
|
+
def initialize(model)
|
60
|
+
self.model = model
|
61
|
+
end
|
62
|
+
|
63
|
+
# Define a view and generate a method that will provide a new
|
64
|
+
# View instance when requested.
|
65
|
+
def view(name, opts = {})
|
66
|
+
View.create(model, name, opts)
|
67
|
+
create_view_method(name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_view_method(name)
|
71
|
+
model.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
72
|
+
def self.#{name}(opts = {})
|
73
|
+
CouchRest::Model::Designs::View.new(self, opts, '#{name}')
|
74
|
+
end
|
75
|
+
EOS
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -3,7 +3,7 @@ module CouchRest
|
|
3
3
|
module Persistence
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
-
# Create the document. Validation is enabled by default and will return
|
6
|
+
# Create the document. Validation is enabled by default and will return
|
7
7
|
# false if the document is not valid. If all goes well, the document will
|
8
8
|
# be returned.
|
9
9
|
def create(options = {})
|
@@ -16,13 +16,13 @@ module CouchRest
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
# Creates the document in the db. Raises an exception
|
21
21
|
# if the document is not created properly.
|
22
22
|
def create!
|
23
23
|
self.class.fail_validate!(self) unless self.create
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
# Trigger the callbacks (before, after, around)
|
27
27
|
# only if the document isn't new
|
28
28
|
def update(options = {})
|
@@ -35,12 +35,12 @@ module CouchRest
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
# Trigger the callbacks (before, after, around) and save the document
|
40
40
|
def save(options = {})
|
41
41
|
self.new? ? create(options) : update(options)
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
# Saves the document to the db using save. Raises an exception
|
45
45
|
# if the document is not saved properly.
|
46
46
|
def save!
|
@@ -65,7 +65,6 @@ module CouchRest
|
|
65
65
|
# Update the document's attributes and save. For example:
|
66
66
|
#
|
67
67
|
# doc.update_attributes :name => "Fred"
|
68
|
-
#
|
69
68
|
# Is the equivilent of doing the following:
|
70
69
|
#
|
71
70
|
# doc.attributes = { :name => "Fred" }
|
@@ -76,7 +75,17 @@ module CouchRest
|
|
76
75
|
save
|
77
76
|
end
|
78
77
|
|
79
|
-
|
78
|
+
# Reloads the attributes of this object from the database.
|
79
|
+
# It doesn't override custom instance variables.
|
80
|
+
#
|
81
|
+
# Returns self.
|
82
|
+
def reload
|
83
|
+
merge!(self.class.get(id))
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
protected
|
80
89
|
|
81
90
|
def perform_validations(options = {})
|
82
91
|
perform_validation = case options
|
@@ -93,16 +102,16 @@ module CouchRest
|
|
93
102
|
|
94
103
|
# Creates a new instance, bypassing attribute protection
|
95
104
|
#
|
96
|
-
#
|
97
105
|
# ==== Returns
|
98
106
|
# a document instance
|
99
|
-
|
107
|
+
#
|
108
|
+
def build_from_database(doc = {})
|
100
109
|
base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
|
101
|
-
base.new(doc, :directly_set_attributes => true)
|
110
|
+
base.new(doc, :directly_set_attributes => true)
|
102
111
|
end
|
103
112
|
|
104
|
-
# Defines an instance and save it directly to the database
|
105
|
-
#
|
113
|
+
# Defines an instance and save it directly to the database
|
114
|
+
#
|
106
115
|
# ==== Returns
|
107
116
|
# returns the reloaded document
|
108
117
|
def create(attributes = {})
|
@@ -110,9 +119,9 @@ module CouchRest
|
|
110
119
|
instance.create
|
111
120
|
instance
|
112
121
|
end
|
113
|
-
|
114
|
-
# Defines an instance and save it directly to the database
|
115
|
-
#
|
122
|
+
|
123
|
+
# Defines an instance and save it directly to the database
|
124
|
+
#
|
116
125
|
# ==== Returns
|
117
126
|
# returns the reloaded document or raises an exception
|
118
127
|
def create!(attributes = {})
|
@@ -148,7 +157,7 @@ module CouchRest
|
|
148
157
|
raise Errors::Validations.new(document)
|
149
158
|
end
|
150
159
|
end
|
151
|
-
|
160
|
+
|
152
161
|
|
153
162
|
end
|
154
163
|
end
|
@@ -156,7 +156,11 @@ module CouchRest
|
|
156
156
|
type = Class.new(Hash) do
|
157
157
|
include CastedModel
|
158
158
|
end
|
159
|
-
|
159
|
+
if block.arity == 1 # Traditional, with options
|
160
|
+
type.class_eval { yield type }
|
161
|
+
else
|
162
|
+
type.instance_exec(&block)
|
163
|
+
end
|
160
164
|
type = [type] # inject as an array
|
161
165
|
end
|
162
166
|
property = Property.new(name, type, options)
|