samflores-couch_surfer 0.0.6

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