samflores-couch_surfer 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'extlib'
3
+ require 'validatable'
4
+
5
+ module CouchSurfer
6
+ module Validations
7
+ module ClassMethods
8
+ def validates_uniqueness_of *args
9
+ # add view, validation and before callbacks
10
+ options = args.last.is_a?(Hash) ? args.pop : {}
11
+ field = args.first
12
+ class_eval do
13
+ #view_by *args
14
+ validates_true_for args.first, :logic => lambda { is_unique?(field, options) }, :message => options[:message] || "is taken"
15
+ end
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ def is_unique?(field, options)
21
+ if options[:view]
22
+ view_name = options[:view]
23
+ query = options[:query].is_a?(Proc) ? self.instance_eval(&options[:query]) : nil
24
+ end
25
+ view_name ||= "by_#{field}"
26
+ query ||= {:key => self.send(field)}
27
+ result = self.class.send(view_name, query)
28
+ if result.blank?
29
+ return true
30
+ else
31
+ return !id.blank? && (id == result.first.id)
32
+ end
33
+ end
34
+
35
+ def validate_instance
36
+ throw(:halt, false) unless valid?
37
+ end
38
+ end
39
+
40
+ def self.included(receiver)
41
+ receiver.extend ClassMethods
42
+ receiver.send :include, Validatable
43
+ receiver.send :include, InstanceMethods
44
+ receiver.class_eval do
45
+ [:save].each do |method|
46
+ before method, :validate_instance
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ This is an example README file.
2
+
3
+ More of the README, whee.
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Test</title>
5
+ </head>
6
+ <body>
7
+ <p>
8
+ Test
9
+ </p>
10
+ </body>
11
+ </html>
@@ -0,0 +1,3 @@
1
+ function globalLib() {
2
+ return "fixture";
3
+ };
@@ -0,0 +1,3 @@
1
+ function justThisView() {
2
+ return "fixture";
3
+ };
@@ -0,0 +1,4 @@
1
+ function(doc) {
2
+ //include-lib
3
+ emit(null, null);
4
+ };
@@ -0,0 +1,3 @@
1
+ function(doc) {
2
+ emit(null, null);
3
+ };
@@ -0,0 +1,3 @@
1
+ function(ks,vs,co) {
2
+ return vs.length;
3
+ };
@@ -0,0 +1,167 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ class Account
4
+ include CouchSurfer::Model
5
+ include CouchSurfer::Associations
6
+
7
+ key_accessor :name
8
+
9
+ has_many :employees,
10
+ :view => :by_account_id_and_role,
11
+ :query => lambda{ {:startkey => [id, nil], :endkey => [id, {}]} }
12
+
13
+ has_many :programmers, :class_name => :employee,
14
+ :view => :by_account_id_and_role,
15
+ :query => lambda{ {:startkey => [id, "Programmer"], :endkey => [id, "Programmer"]} }
16
+
17
+ has_many :projects
18
+
19
+ end
20
+
21
+ class Project
22
+ include CouchSurfer::Model
23
+ include CouchSurfer::Associations
24
+
25
+ key_accessor :name, :account_id
26
+
27
+ belongs_to :account
28
+ has_many :members, :through => :memberships, :class_name => :employee
29
+
30
+ view_by :account_id
31
+ view_by :id
32
+ end
33
+
34
+ class Membership
35
+ include CouchSurfer::Model
36
+ include CouchSurfer::Associations
37
+
38
+ key_accessor :project_id, :employee_id
39
+ belongs_to :project
40
+ belongs_to :employee
41
+
42
+ view_by :project_id
43
+ view_by :employee_id
44
+ end
45
+
46
+
47
+ class Employee
48
+ include CouchSurfer::Model
49
+ include CouchSurfer::Associations
50
+
51
+ key_accessor :email, :account_id, :role
52
+
53
+ belongs_to :employer, :class_name => :account
54
+ has_many :shirts, :inline => true
55
+ has_many :projects, :through => :memberships
56
+
57
+ view_by :account_id, :role
58
+
59
+ end
60
+
61
+ class Shirt
62
+ include CouchSurfer::Model
63
+ include CouchSurfer::Associations
64
+
65
+ key_accessor :color
66
+ end
67
+
68
+ describe CouchSurfer::Associations do
69
+ before(:all) do
70
+ db = CouchRest.database!('couch_surfer-test')
71
+ db.delete!
72
+ CouchSurfer::Model.default_database = CouchRest.database!('http://127.0.0.1:5984/couch_surfer-test')
73
+ @account = Account.create(:name => "My Account")
74
+ 2.times do |i|
75
+ Employee.create(:email => "foo#{i+1}@bar.com", :account_id => @account.id, :role => "Programmer")
76
+ Employee.create(:email => "foo#{i+3}@bar.com", :account_id => @account.id, :role => "Engineer")
77
+ Project.create(:name => "Project No. #{i}", :account_id => @account.id)
78
+ end
79
+ @employee = @account.employees.first
80
+ @project = @account.projects.first
81
+ end
82
+
83
+ describe "belongs_to" do
84
+ it "should return it's parent" do
85
+ @project.account.should == @account
86
+ end
87
+
88
+ it "should take the class_name into consideration" do
89
+ @employee.employer.should == @account
90
+ end
91
+ end
92
+
93
+ describe "has_many" do
94
+ describe "vanilla" do
95
+ it "should return it's children" do
96
+ @account.employees.length.should == 4
97
+ @account.employees.map{|employee| employee.email}.sort.should == ["foo1@bar.com", "foo2@bar.com", "foo3@bar.com", "foo4@bar.com"]
98
+ end
99
+ end
100
+
101
+ describe ":class_name" do
102
+ it "should return it's children" do
103
+ @account.programmers.length.should == 2
104
+ end
105
+ end
106
+ describe ":inline" do
107
+ before(:all) do
108
+ @employee.shirts.clear
109
+ @blue_shirt = Shirt.create(:color => "White")
110
+ @pink_shirt = Shirt.create(:color => "Pink")
111
+ @employee.shirts << {:color => "Blue"}
112
+ @employee.shirts << @blue_shirt
113
+ @employee.save
114
+ @employee = Employee.get(@employee.id)
115
+ @employee.shirts << @pink_shirt
116
+ @employee.save
117
+ end
118
+ it "should return it's children" do
119
+ employee = Employee.get(@employee.id)
120
+ employee.shirts.each do |shirt|
121
+ shirt.should be_kind_of(Shirt)
122
+ end
123
+ employee.shirts.map{|shirt| shirt.color}.should == %w(Blue White Pink)
124
+ end
125
+
126
+ it "should have a delete method" do
127
+ employee = Employee.get(@employee.id)
128
+ employee.shirts.delete(@pink_shirt)
129
+ employee.save
130
+ employee = Employee.get(@employee.id)
131
+ employee.shirts.map{|shirt| shirt.color}.should == %w(Blue White)
132
+ end
133
+
134
+ it "should have an append method" do
135
+ employee = Employee.get(@employee.id)
136
+ employee.shirts << {:color => "Black"}
137
+ employee.save
138
+ employee = Employee.get(@employee.id)
139
+ employee.shirts.map{|shirt| shirt.color}.should == %w(Blue White Black)
140
+ end
141
+
142
+ end
143
+
144
+ describe ":through" do
145
+ it "should return it's 'through' children" do
146
+ @employee.memberships.should be_empty
147
+ end
148
+
149
+ it "should return it's children" do
150
+ 3.times do |i|
151
+ p = Project.create(:name => "Project with Invitations No. #{i}", :account_id => @account.id)
152
+ Membership.create(:employee_id => @employee.id, :project_id => p.id)
153
+ end
154
+
155
+ 7.times do |i|
156
+ e = Employee.create(:email => "bar#{i}@bar.com", :account_id => @account.id)
157
+ Membership.create(:employee_id => e.id, :project_id => @project.id)
158
+ end
159
+
160
+ @employee.memberships.size.should == 3
161
+ @employee.projects.size.should == 3
162
+ @project.memberships.size.should == 7
163
+ @project.members.size.should == 7
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,893 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ class Basic
4
+ include CouchSurfer::Model
5
+ end
6
+
7
+ class BasicWithValidation
8
+ include CouchSurfer::Model
9
+
10
+ before :create, :validate
11
+ before :update, :change_name
12
+ key_accessor :name
13
+
14
+ def validate
15
+ throw(:halt, false) unless name
16
+ end
17
+
18
+ def change_name
19
+ self.name = "Bar"
20
+ end
21
+ end
22
+
23
+ class Article
24
+ include CouchSurfer::Model
25
+ #use_database CouchRest.database!('http://127.0.0.1:5984/couchrest-model-test')
26
+ unique_id :slug
27
+
28
+ view_by :date, :descending => true
29
+ view_by :user_id, :date
30
+
31
+ view_by :tags,
32
+ :map =>
33
+ "function(doc) {
34
+ if (doc['couchrest-type'] == 'Article' && doc.tags) {
35
+ doc.tags.forEach(function(tag){
36
+ emit(tag, 1);
37
+ });
38
+ }
39
+ }",
40
+ :reduce =>
41
+ "function(keys, values, rereduce) {
42
+ return sum(values);
43
+ }"
44
+
45
+ key_writer :date
46
+ key_reader :slug #, :created_at, :updated_at
47
+ key_accessor :title, :tags
48
+
49
+ timestamps!
50
+
51
+ before(:save, :generate_slug_from_title)
52
+ def generate_slug_from_title
53
+ self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new_document?
54
+ end
55
+ end
56
+
57
+ class WithTemplateAndUniqueID
58
+ include CouchSurfer::Model
59
+
60
+ unique_id do |model|
61
+ model['important-field']
62
+ end
63
+ set_default({
64
+ :preset => 'value',
65
+ 'more-template' => [1,2,3]
66
+ })
67
+ key_accessor :preset
68
+ key_accessor :has_no_default
69
+ end
70
+
71
+ class Question
72
+ include CouchSurfer::Model
73
+
74
+ key_accessor :q, :a
75
+ end
76
+
77
+ class Person
78
+ include CouchSurfer::Model
79
+
80
+ key_accessor :name
81
+ def last_name
82
+ name.last
83
+ end
84
+ end
85
+
86
+ class Course
87
+ include CouchSurfer::Model
88
+
89
+ key_accessor :title
90
+ cast :questions, :as => ['Question']
91
+ cast :professor, :as => 'Person'
92
+ cast :final_test_at, :as => 'Time'
93
+ view_by :title
94
+ view_by :dept, :ducktype => true
95
+ end
96
+
97
+ class Event
98
+ include CouchSurfer::Model
99
+
100
+ key_accessor :subject, :occurs_at
101
+
102
+ cast :occurs_at, :as => 'Time', :send => 'parse'
103
+ end
104
+
105
+ describe CouchSurfer::Model do
106
+ before(:all) do
107
+ @cr = CouchRest.new(COUCHHOST)
108
+ @db = @cr.database(TESTDB)
109
+ @db.delete! rescue nil
110
+ @db = @cr.create_db(TESTDB) rescue nil
111
+ @adb = @cr.database('couchrest-model-test')
112
+ @adb.delete! rescue nil
113
+ CouchSurfer::Model.default_database = CouchRest.database!('http://127.0.0.1:5984/couch_surfer-test')
114
+ end
115
+
116
+ describe "setting the database" do
117
+ it "should use the default database" do
118
+ Basic.database.info['db_name'].should == 'couch_surfer-test'
119
+ end
120
+
121
+ it "should be able to overwrite the default" do
122
+ Basic.use_database CouchRest.database!('http://127.0.0.1:5984/couch_surfer-custom')
123
+ Basic.database.info['db_name'].should == 'couch_surfer-custom'
124
+ end
125
+ end
126
+
127
+ describe "a new model" do
128
+ it "should be a new_record" do
129
+ Basic.new.should be_a_new_record
130
+ end
131
+ end
132
+
133
+ describe "a model with key_accessors" do
134
+ it "should allow reading keys" do
135
+ @art = Article.new(:title => 'My Article Title')
136
+ @art.title.should == 'My Article Title'
137
+ end
138
+ it "should allow setting keys" do
139
+ @art = Article.new(:title => 'My Article Title')
140
+ @art.title = 'My New Article Title'
141
+ @art.title.should == 'My New Article Title'
142
+ end
143
+ end
144
+
145
+ describe "a model with key_writers" do
146
+ it "should allow setting keys" do
147
+ @art = Article.new
148
+ t = Time.now
149
+ @art.date = t
150
+ @art.attributes[:date].should == t
151
+ end
152
+ it "should not allow reading keys" do
153
+ @art = Article.new
154
+ t = Time.now
155
+ @art.date = t
156
+ lambda{@art.date}.should raise_error
157
+ end
158
+ end
159
+
160
+ describe "a model with key_readers" do
161
+ it "should allow reading keys" do
162
+ @art = Article.new(:slug => "my-slug")
163
+ @art.slug.should == 'my-slug'
164
+ end
165
+ it "should not allow setting keys" do
166
+ @art = Article.new
167
+ lambda{@art.slug = 'My Article Title'}.should raise_error
168
+ end
169
+ end
170
+
171
+ describe "update attributes without saving" do
172
+ before(:each) do
173
+ a = Article.get "big-bad-danger" rescue nil
174
+ a.destroy if a
175
+ @art = Article.new(:title => "big bad danger")
176
+ @art.save
177
+ end
178
+ it "should work for attribute= methods" do
179
+ @art['title'].should == "big bad danger"
180
+ @art.update_attributes(:date => Time.now, :title => "super danger")
181
+ @art['title'].should == "super danger"
182
+ end
183
+
184
+ it "should flip out if an attribute= method is missing" do
185
+ lambda {
186
+ @art.update_attributes('slug' => "new-slug", :title => "super danger")
187
+ }.should raise_error
188
+ end
189
+
190
+ it "should not change other attributes if there is an error" do
191
+ lambda {
192
+ @art.update_attributes('slug' => "new-slug", :title => "super danger")
193
+ }.should raise_error
194
+ @art['title'].should == "big bad danger"
195
+ end
196
+ end
197
+
198
+ describe "update attributes" do
199
+ before(:each) do
200
+ a = Article.get "big-bad-danger" rescue nil
201
+ a.destroy if a
202
+ @art = Article.new(:title => "big bad danger")
203
+ @art.save
204
+ end
205
+ it "should save" do
206
+ @art['title'].should == "big bad danger"
207
+ @art.update_attributes('date' => Time.now, :title => "super danger")
208
+ loaded = Article.get @art.id
209
+ loaded['title'].should == "super danger"
210
+ end
211
+ end
212
+
213
+ describe "a model with template values" do
214
+ before(:all) do
215
+ @tmpl = WithTemplateAndUniqueID.new
216
+ @tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'important-field' => '1')
217
+ end
218
+ it "should have fields set when new" do
219
+ @tmpl.preset.should == 'value'
220
+ end
221
+ it "shouldn't override explicitly set values" do
222
+ @tmpl2.preset.should == 'not_value'
223
+ end
224
+ it "shouldn't override existing documents" do
225
+ @tmpl2.save
226
+ tmpl2_reloaded = WithTemplateAndUniqueID.get(@tmpl2.id)
227
+ @tmpl2.preset.should == 'not_value'
228
+ tmpl2_reloaded.preset.should == 'not_value'
229
+ end
230
+
231
+ it "shouldn't fill in existing documents" do
232
+ @tmpl2.save
233
+ # If user adds a new default value, shouldn't be retroactively applied to
234
+ # documents upon fetching
235
+ WithTemplateAndUniqueID.set_default({:has_no_default => 'giraffe'})
236
+
237
+ tmpl2_reloaded = WithTemplateAndUniqueID.get(@tmpl2.id)
238
+ @tmpl2.has_no_default.should be_nil
239
+ tmpl2_reloaded.has_no_default.should be_nil
240
+ WithTemplateAndUniqueID.new.has_no_default.should == 'giraffe'
241
+ end
242
+ end
243
+
244
+ describe "getting a model" do
245
+ before(:all) do
246
+ @art = Article.new(:title => 'All About Getting')
247
+ @art.save
248
+ end
249
+ it "should load and instantiate it" do
250
+ foundart = Article.get @art.id
251
+ foundart.title.should == "All About Getting"
252
+ end
253
+ end
254
+
255
+ describe "getting a model with a subobjects array" do
256
+ before(:all) do
257
+ course_doc = {
258
+ "title" => "Metaphysics 200",
259
+ "questions" => [
260
+ {
261
+ "q" => "Carve the ___ of reality at the ___.",
262
+ "a" => ["beast","joints"]
263
+ },{
264
+ "q" => "Who layed the smack down on Leibniz's Law?",
265
+ "a" => "Willard Van Orman Quine"
266
+ }
267
+ ]
268
+ }
269
+ r = Course.database.save course_doc
270
+ @course = Course.get r['id']
271
+ end
272
+ it "should load the course" do
273
+ @course.title.should == "Metaphysics 200"
274
+ end
275
+ it "should instantiate them as such" do
276
+ @course["questions"][0].a[0].should == "beast"
277
+ end
278
+ end
279
+
280
+ describe "finding all instances of a model" do
281
+ before(:all) do
282
+ WithTemplateAndUniqueID.new('important-field' => '1').save
283
+ WithTemplateAndUniqueID.new('important-field' => '2').save
284
+ WithTemplateAndUniqueID.new('important-field' => '3').save
285
+ WithTemplateAndUniqueID.new('important-field' => '4').save
286
+ end
287
+ it "should make the design doc" do
288
+ WithTemplateAndUniqueID.all
289
+ WithTemplateAndUniqueID.all
290
+ d = WithTemplateAndUniqueID.design_doc
291
+ d['views']['all']['map'].should include('WithTemplateAndUniqueID')
292
+ end
293
+ it "should find all" do
294
+ rs = WithTemplateAndUniqueID.all
295
+ rs.length.should == 4
296
+ end
297
+ end
298
+
299
+ describe "finding the first instance of a model" do
300
+ before(:each) do
301
+ @db = reset_test_db!
302
+ WithTemplateAndUniqueID.new('important-field' => '1').save
303
+ WithTemplateAndUniqueID.new('important-field' => '2').save
304
+ WithTemplateAndUniqueID.new('important-field' => '3').save
305
+ WithTemplateAndUniqueID.new('important-field' => '4').save
306
+ end
307
+ it "should make the design doc" do
308
+ WithTemplateAndUniqueID.all
309
+ d = WithTemplateAndUniqueID.design_doc
310
+ d['views']['all']['map'].should include('WithTemplateAndUniqueID')
311
+ end
312
+ it "should find first" do
313
+ rs = WithTemplateAndUniqueID.first
314
+ rs['important-field'].should == "1"
315
+ end
316
+ it "should return nil if no instances are found" do
317
+ WithTemplateAndUniqueID.all.each {|obj| obj.destroy }
318
+ WithTemplateAndUniqueID.first.should be_nil
319
+ end
320
+ end
321
+
322
+ describe "getting a model with a subobject field" do
323
+ before(:all) do
324
+ course_doc = {
325
+ "title" => "Metaphysics 410",
326
+ "professor" => {
327
+ "name" => ["Mark", "Hinchliff"]
328
+ },
329
+ "final_test_at" => "2008/12/19 13:00:00 +0800"
330
+ }
331
+ r = Course.database.save course_doc
332
+ @course = Course.get r['id']
333
+ end
334
+ it "should load the course" do
335
+ @course["professor"]["name"][1].should == "Hinchliff"
336
+ end
337
+ it "should instantiate the professor as a person" do
338
+ @course['professor'].last_name.should == "Hinchliff"
339
+ end
340
+ it "should instantiate the final_test_at as a Time" do
341
+ @course['final_test_at'].should == Time.parse("2008/12/19 13:00:00 +0800")
342
+ end
343
+ end
344
+
345
+ describe "cast keys to any type" do
346
+ before(:all) do
347
+ event_doc = { :subject => "Some event", :occurs_at => Time.now }
348
+ e = Event.database.save event_doc
349
+
350
+ @event = Event.get e['id']
351
+ end
352
+ it "should cast created_at to Time" do
353
+ @event.occurs_at.should be_an_instance_of(Time)
354
+ end
355
+ end
356
+
357
+ describe "saving a model" do
358
+ before(:all) do
359
+ @obj = Basic.new(:foo => "Bar")
360
+ @obj.save.should == true
361
+ end
362
+
363
+ it "should save the doc" do
364
+ doc = @obj.database.get @obj.id
365
+ doc['_id'].should == @obj.id
366
+ end
367
+
368
+ it "should be set for resaving" do
369
+ rev = @obj.rev
370
+ @obj['another-key'] = "some value"
371
+ @obj.save
372
+ @obj.rev.should_not == rev
373
+ end
374
+
375
+ it "should set the id" do
376
+ @obj.id.should be_an_instance_of(String)
377
+ end
378
+
379
+ it "should set the type" do
380
+ @obj['couchrest-type'].should == 'Basic'
381
+ end
382
+ end
383
+
384
+ describe "creating a model" do
385
+ before(:all) do
386
+ @obj = Basic.create(:foo => "Bar")
387
+ end
388
+
389
+ it "should save the doc" do
390
+ doc = @obj.database.get @obj.id
391
+ doc['_id'].should == @obj.id
392
+ end
393
+
394
+ it "should be set for resaving" do
395
+ rev = @obj.rev
396
+ @obj['another-key'] = "some value"
397
+ @obj.save
398
+ @obj.rev.should_not == rev
399
+ end
400
+
401
+ it "should set the id" do
402
+ @obj.id.should be_an_instance_of(String)
403
+ end
404
+
405
+ it "should set the type" do
406
+ @obj['couchrest-type'].should == 'Basic'
407
+ end
408
+ end
409
+
410
+ describe "saving a model with validation hooks added as extlib" do
411
+ before(:all) do
412
+ @obj = BasicWithValidation.new
413
+ end
414
+
415
+ it "save should return false is the model doesn't save as expected" do
416
+ @obj.save.should be_false
417
+ end
418
+
419
+ it "save! should raise and exception if the model doesn't save" do
420
+ lambda{ @obj.save!}.should raise_error("#{@obj.inspect} failed to save")
421
+ end
422
+ end
423
+
424
+ describe "updating a model with a hook added as extlib" do
425
+
426
+ it "should run the hook method" do
427
+ @obj = BasicWithValidation.new(:name => "Foo")
428
+ @obj.save.should be_true
429
+ @obj.save
430
+ @obj.name.should == "Bar"
431
+ end
432
+
433
+ end
434
+
435
+ describe "saving a model with a unique_id configured" do
436
+ before(:each) do
437
+ @art = Article.new
438
+ @old = Article.database.get('this-is-the-title') rescue nil
439
+ Article.database.delete(@old) if @old
440
+ end
441
+
442
+ it "should be a new document" do
443
+ @art.should be_a_new_document
444
+ @art.title.should be_nil
445
+ end
446
+
447
+ it "should require the title" do
448
+ lambda{@art.save}.should raise_error
449
+ @art.title = 'This is the title'
450
+ @art.save.should == true
451
+ end
452
+
453
+ it "should not change the slug on update" do
454
+ @art.title = 'This is the title'
455
+ @art.save.should == true
456
+ @art.title = 'new title'
457
+ @art.save.should == true
458
+ @art.slug.should == 'this-is-the-title'
459
+ end
460
+
461
+ it "should raise an error when the slug is taken" do
462
+ @art.title = 'This is the title'
463
+ @art.save.should == true
464
+ @art2 = Article.new(:title => 'This is the title!')
465
+ lambda{@art2.save}.should raise_error
466
+ end
467
+
468
+ it "should set the slug" do
469
+ @art.title = 'This is the title'
470
+ @art.save.should == true
471
+ @art.slug.should == 'this-is-the-title'
472
+ end
473
+
474
+ it "should set the id" do
475
+ @art.title = 'This is the title'
476
+ @art.save.should == true
477
+ @art.id.should == 'this-is-the-title'
478
+ end
479
+ end
480
+
481
+ describe "saving a model with a unique_id lambda" do
482
+ before(:each) do
483
+ @templated = WithTemplateAndUniqueID.new
484
+ @old = WithTemplateAndUniqueID.get('very-important') rescue nil
485
+ @old.destroy if @old
486
+ end
487
+
488
+ it "should require the field" do
489
+ lambda{@templated.save}.should raise_error
490
+ @templated['important-field'] = 'very-important'
491
+ @templated.save.should == true
492
+ end
493
+
494
+ it "should save with the id" do
495
+ @templated['important-field'] = 'very-important'
496
+ @templated.save.should == true
497
+ t = WithTemplateAndUniqueID.get('very-important')
498
+ t.should == @templated
499
+ end
500
+
501
+ it "should not change the id on update" do
502
+ @templated['important-field'] = 'very-important'
503
+ @templated.save.should == true
504
+ @templated['important-field'] = 'not-important'
505
+ @templated.save.should == true
506
+ t = WithTemplateAndUniqueID.get('very-important')
507
+ t.should == @templated
508
+ end
509
+
510
+ it "should raise an error when the id is taken" do
511
+ @templated['important-field'] = 'very-important'
512
+ @templated.save.should == true
513
+ lambda{WithTemplateAndUniqueID.new('important-field' => 'very-important').save}.should raise_error
514
+ end
515
+
516
+ it "should set the id" do
517
+ @templated['important-field'] = 'very-important'
518
+ @templated.save.should == true
519
+ @templated.id.should == 'very-important'
520
+ end
521
+ end
522
+
523
+ describe "a model with timestamps" do
524
+ before(:each) do
525
+ oldart = Article.get "saving-this" rescue nil
526
+ oldart.destroy if oldart
527
+ @art = Article.new(:title => "Saving this")
528
+ @art.save
529
+ end
530
+ it "should set the time on create" do
531
+ (Time.now - @art.created_at).should < 2
532
+ foundart = Article.get @art.id
533
+ foundart.created_at.should == foundart.updated_at
534
+ end
535
+ it "should set the time on update" do
536
+ sleep 1 # HACK!! Sometimes takes less than a second to call save the second time. Really should mock this!
537
+ @art.save
538
+ @art.created_at.should < @art.updated_at
539
+ end
540
+
541
+ it "should return both created_at and updated_at as instances of Time" do
542
+ @art.created_at.should be_kind_of(Time)
543
+ @art.updated_at.should be_kind_of(Time)
544
+ end
545
+ it "when retrieved should return both created_at and updated_at as instances of Time" do
546
+ foundart = Article.get @art.id
547
+ foundart.created_at.should be_kind_of(Time)
548
+ foundart.updated_at.should be_kind_of(Time)
549
+ end
550
+ end
551
+
552
+ describe "a model with simple views and a default param" do
553
+ before(:all) do
554
+ written_at = Time.now - 24 * 3600 * 7
555
+ @titles = ["this and that", "also interesting", "more fun", "some junk"]
556
+ @titles.each do |title|
557
+ a = Article.new(:title => title)
558
+ a.date = written_at
559
+ a.save
560
+ written_at += 24 * 3600
561
+ end
562
+ end
563
+
564
+ it "should have a design doc" do
565
+ Article.design_doc["views"]["by_date"].should_not be_nil
566
+ end
567
+
568
+ it "should save the design doc" do
569
+ Article.by_date #rescue nil
570
+ doc = Article.database.get Article.design_doc.id
571
+ doc['views']['by_date'].should_not be_nil
572
+ end
573
+
574
+ it "should return the matching raw view result" do
575
+ view = Article.by_date :raw => true
576
+ view['rows'].length.should == 4
577
+ end
578
+
579
+ it "should not include non-Articles" do
580
+ Article.database.save({"date" => 1})
581
+ view = Article.by_date :raw => true
582
+ view['rows'].length.should == 4
583
+ end
584
+
585
+ it "should return the matching objects (with default argument :descending => true)" do
586
+ articles = Article.by_date
587
+ articles.collect{|a|a.title}.should == @titles.reverse
588
+ end
589
+
590
+ it "should allow you to override default args" do
591
+ articles = Article.by_date :descending => false
592
+ articles.collect{|a|a.title}.should == @titles
593
+ end
594
+ end
595
+
596
+ describe "another model with a simple view" do
597
+ before(:all) do
598
+ Course.database.delete! rescue nil
599
+ @db = @cr.create_db(TESTDB) rescue nil
600
+ %w{aaa bbb ddd eee}.each do |title|
601
+ Course.new(:title => title).save
602
+ end
603
+ end
604
+ it "should make the design doc upon first query" do
605
+ Course.by_title
606
+ doc = Course.design_doc
607
+ doc['views']['all']['map'].should include('Course')
608
+ end
609
+ it "should can query via view" do
610
+ # register methods with method-missing, for local dispatch. method
611
+ # missing lookup table, no heuristics.
612
+ view = Course.view :by_title
613
+ designed = Course.by_title
614
+ view.should == designed
615
+ end
616
+ it "should get them" do
617
+ rs = Course.by_title
618
+ rs.length.should == 4
619
+ end
620
+ it "should yield" do
621
+ courses = []
622
+ rs = Course.by_title # remove me
623
+ Course.view(:by_title) do |course|
624
+ courses << course
625
+ end
626
+ courses[0]["doc"]["title"].should =='aaa'
627
+ end
628
+ end
629
+
630
+ describe "a ducktype view" do
631
+ before(:all) do
632
+ @id = @db.save({:dept => true})['id']
633
+ end
634
+ it "should setup" do
635
+ duck = Course.get(@id) # from a different db
636
+ duck["dept"].should == true
637
+ end
638
+ it "should make the design doc" do
639
+ @as = Course.by_dept
640
+ @doc = Course.design_doc
641
+ @doc["views"]["by_dept"]["map"].should_not include("couchrest")
642
+ end
643
+ it "should not look for class" do |variable|
644
+ @as = Course.by_dept
645
+ @as[0]['_id'].should == @id
646
+ end
647
+ end
648
+
649
+ describe "a model with a compound key view" do
650
+ before(:all) do
651
+ written_at = Time.now - 24 * 3600 * 7
652
+ @titles = ["uniq one", "even more interesting", "less fun", "not junk"]
653
+ @user_ids = ["quentin", "aaron"]
654
+ @titles.each_with_index do |title,i|
655
+ u = i % 2
656
+ a = Article.new(:title => title, :user_id => @user_ids[u])
657
+ a.date = written_at
658
+ a.save
659
+ written_at += 24 * 3600
660
+ end
661
+ end
662
+ it "should create the design doc" do
663
+ Article.by_user_id_and_date rescue nil
664
+ doc = Article.design_doc
665
+ doc['views']['by_date'].should_not be_nil
666
+ end
667
+ it "should sort correctly" do
668
+ articles = Article.by_user_id_and_date
669
+ articles.collect{|a|a['user_id']}.should == ['aaron', 'aaron', 'quentin',
670
+ 'quentin']
671
+ articles[1].title.should == 'not junk'
672
+ end
673
+ it "should be queryable with couchrest options" do
674
+ articles = Article.by_user_id_and_date :limit => 1, :startkey => 'quentin'
675
+ articles.length.should == 1
676
+ articles[0].title.should == "even more interesting"
677
+ end
678
+ end
679
+
680
+ describe "with a custom view" do
681
+ before(:all) do
682
+ @titles = ["very uniq one", "even less interesting", "some fun",
683
+ "really junk", "crazy bob"]
684
+ @tags = ["cool", "lame"]
685
+ @titles.each_with_index do |title,i|
686
+ u = i % 2
687
+ a = Article.new(:title => title, :tags => [@tags[u]])
688
+ a.save
689
+ end
690
+ end
691
+ it "should be available raw" do
692
+ view = Article.by_tags :raw => true
693
+ view['rows'].length.should == 5
694
+ end
695
+
696
+ it "should be default to :reduce => false" do
697
+ ars = Article.by_tags
698
+ ars.first.tags.first.should == 'cool'
699
+ end
700
+
701
+ it "should be raw when reduce is true" do
702
+ view = Article.by_tags :reduce => true, :group => true
703
+ view['rows'].find{|r|r['key'] == 'cool'}['value'].should == 3
704
+ end
705
+ end
706
+
707
+ # TODO: moved to Design, delete
708
+ describe "adding a view" do
709
+ before(:each) do
710
+ Article.by_date
711
+ @design_docs = Article.database.documents :startkey => "_design/",
712
+ :endkey => "_design/\u9999"
713
+ end
714
+ it "should not create a design doc on view definition" do
715
+ Article.view_by :created_at
716
+ newdocs = Article.database.documents :startkey => "_design/",
717
+ :endkey => "_design/\u9999"
718
+ newdocs["rows"].length.should == @design_docs["rows"].length
719
+ end
720
+ it "should create a new design document on view access" do
721
+ Article.view_by :updated_at
722
+ Article.by_updated_at
723
+ newdocs = Article.database.documents :startkey => "_design/",
724
+ :endkey => "_design/\u9999"
725
+ # puts @design_docs.inspect
726
+ # puts newdocs.inspect
727
+ newdocs["rows"].length.should == @design_docs["rows"].length + 1
728
+ end
729
+ end
730
+
731
+ describe "with a lot of designs left around" do
732
+ before(:each) do
733
+ Article.by_date
734
+ Article.view_by :field
735
+ Article.by_field
736
+ end
737
+ it "should clean them up" do
738
+ Article.view_by :stream
739
+ Article.by_stream
740
+ ddocs = Article.all_design_doc_versions
741
+ ddocs["rows"].length.should > 1
742
+ Article.cleanup_design_docs!
743
+ ddocs = Article.all_design_doc_versions
744
+ ddocs["rows"].length.should == 1
745
+ end
746
+ end
747
+
748
+ describe "destroying an instance" do
749
+ before(:each) do
750
+ @obj = Basic.new
751
+ @obj.save.should == true
752
+ end
753
+ it "should return true" do
754
+ result = @obj.destroy
755
+ result.should == true
756
+ end
757
+ it "should be resavable" do
758
+ @obj.destroy
759
+ @obj.rev.should be_nil
760
+ @obj.id.should be_nil
761
+ @obj.save.should == true
762
+ end
763
+ it "should make it go away" do
764
+ @obj.destroy
765
+ lambda{Basic.get(@obj.id)}.should raise_error
766
+ end
767
+ end
768
+
769
+ describe "#has_attachment?" do
770
+ before(:each) do
771
+ @obj = Basic.new
772
+ @obj.save.should == true
773
+ @file = File.open(FIXTURE_PATH + '/attachments/test.html')
774
+ @attachment_name = 'my_attachment'
775
+ @obj.create_attachment(:file => @file, :name => @attachment_name)
776
+ end
777
+
778
+ it 'should return false if there is no attachment' do
779
+ @obj.has_attachment?('bogus').should be_false
780
+ end
781
+
782
+ it 'should return true if there is an attachment' do
783
+ @obj.has_attachment?(@attachment_name).should be_true
784
+ end
785
+
786
+ it 'should return true if an object with an attachment is reloaded' do
787
+ @obj.save.should be_true
788
+ reloaded_obj = Basic.get(@obj.id)
789
+ reloaded_obj.has_attachment?(@attachment_name).should be_true
790
+ end
791
+
792
+ it 'should return false if an attachment has been removed' do
793
+ @obj.delete_attachment(@attachment_name)
794
+ @obj.has_attachment?(@attachment_name).should be_false
795
+ end
796
+ end
797
+
798
+ describe "creating an attachment" do
799
+ before(:each) do
800
+ @obj = Basic.new
801
+ @obj.save.should == true
802
+ @file_ext = File.open(FIXTURE_PATH + '/attachments/test.html')
803
+ @file_no_ext = File.open(FIXTURE_PATH + '/attachments/README')
804
+ @attachment_name = 'my_attachment'
805
+ @content_type = 'media/mp3'
806
+ end
807
+
808
+ it "should create an attachment from file with an extension" do
809
+ @obj.create_attachment(:file => @file_ext, :name => @attachment_name)
810
+ @obj.save.should == true
811
+ reloaded_obj = Basic.get(@obj.id)
812
+ reloaded_obj['_attachments'][@attachment_name].should_not be_nil
813
+ end
814
+
815
+ it "should create an attachment from file without an extension" do
816
+ @obj.create_attachment(:file => @file_no_ext, :name => @attachment_name)
817
+ @obj.save.should == true
818
+ reloaded_obj = Basic.get(@obj.id)
819
+ reloaded_obj['_attachments'][@attachment_name].should_not be_nil
820
+ end
821
+
822
+ it 'should raise ArgumentError if :file is missing' do
823
+ lambda{ @obj.create_attachment(:name => @attachment_name) }.should raise_error
824
+ end
825
+
826
+ it 'should raise ArgumentError if :name is missing' do
827
+ lambda{ @obj.create_attachment(:file => @file_ext) }.should raise_error
828
+ end
829
+
830
+ it 'should set the content-type if passed' do
831
+ @obj.create_attachment(:file => @file_ext, :name => @attachment_name, :content_type => @content_type)
832
+ @obj['_attachments'][@attachment_name]['content-type'].should == @content_type
833
+ end
834
+ end
835
+
836
+ describe 'reading, updating, and deleting an attachment' do
837
+ before(:each) do
838
+ @obj = Basic.new
839
+ @file = File.open(FIXTURE_PATH + '/attachments/test.html')
840
+ @attachment_name = 'my_attachment'
841
+ @obj.create_attachment(:file => @file, :name => @attachment_name)
842
+ @obj.save.should == true
843
+ @file.rewind
844
+ @content_type = 'media/mp3'
845
+ end
846
+
847
+ it 'should read an attachment that exists' do
848
+ @obj.read_attachment(@attachment_name).should == @file.read
849
+ end
850
+
851
+ it 'should update an attachment that exists' do
852
+ file = File.open(FIXTURE_PATH + '/attachments/README')
853
+ @file.should_not == file
854
+ @obj.update_attachment(:file => file, :name => @attachment_name)
855
+ @obj.save
856
+ reloaded_obj = Basic.get(@obj.id)
857
+ file.rewind
858
+ reloaded_obj.read_attachment(@attachment_name).should_not == @file.read
859
+ reloaded_obj.read_attachment(@attachment_name).should == file.read
860
+ end
861
+
862
+ it 'should se the content-type if passed' do
863
+ file = File.open(FIXTURE_PATH + '/attachments/README')
864
+ @file.should_not == file
865
+ @obj.update_attachment(:file => file, :name => @attachment_name, :content_type => @content_type)
866
+ @obj['_attachments'][@attachment_name]['content-type'].should == @content_type
867
+ end
868
+
869
+ it 'should delete an attachment that exists' do
870
+ @obj.delete_attachment(@attachment_name)
871
+ @obj.save
872
+ lambda{Basic.get(@obj.id).read_attachment(@attachment_name)}.should raise_error
873
+ end
874
+ end
875
+
876
+ describe "#attachment_url" do
877
+ before(:each) do
878
+ @obj = Basic.new
879
+ @file = File.open(FIXTURE_PATH + '/attachments/test.html')
880
+ @attachment_name = 'my_attachment'
881
+ @obj.create_attachment(:file => @file, :name => @attachment_name)
882
+ @obj.save.should == true
883
+ end
884
+
885
+ it 'should return nil if attachment does not exist' do
886
+ @obj.attachment_url('bogus').should be_nil
887
+ end
888
+
889
+ it 'should return the attachment URL as specified by CouchDB HttpDocumentApi' do
890
+ @obj.attachment_url(@attachment_name).should == "#{Basic.database}/#{@obj.id}/#{@attachment_name}"
891
+ end
892
+ end
893
+ end