couchrest_model 1.1.0.beta → 1.1.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +15 -15
- data/README.md +42 -0
- data/VERSION +1 -1
- data/couchrest_model.gemspec +1 -1
- data/history.txt +9 -0
- data/lib/couchrest/model/associations.rb +62 -50
- data/lib/couchrest/model/casted_model.rb +1 -1
- data/lib/couchrest/model/{support → core_extensions}/hash.rb +0 -0
- data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
- data/lib/couchrest/model/designs/view.rb +34 -14
- data/lib/couchrest/model/properties.rb +4 -2
- data/lib/couchrest/model/proxyable.rb +18 -13
- data/lib/couchrest/model/typecast.rb +8 -28
- data/lib/couchrest/model/validations/uniqueness.rb +20 -9
- data/lib/couchrest/model/views.rb +1 -1
- data/lib/couchrest_model.rb +3 -1
- data/spec/couchrest/assocations_spec.rb +31 -10
- data/spec/couchrest/core_extensions/time_parsing.rb +77 -0
- data/spec/couchrest/designs/view_spec.rb +60 -19
- data/spec/couchrest/property_spec.rb +1 -505
- data/spec/couchrest/proxyable_spec.rb +46 -27
- data/spec/couchrest/typecast_spec.rb +524 -0
- data/spec/couchrest/validations_spec.rb +84 -36
- data/spec/fixtures/base.rb +9 -0
- metadata +10 -6
data/Gemfile.lock
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
couchrest_model (1.1.0.
|
4
|
+
couchrest_model (1.1.0.beta2)
|
5
5
|
activemodel (~> 3.0.0)
|
6
|
-
couchrest (
|
6
|
+
couchrest (= 1.1.0.pre2)
|
7
7
|
mime-types (~> 1.15)
|
8
8
|
railties (~> 3.0.0)
|
9
9
|
tzinfo (~> 0.3.22)
|
@@ -12,9 +12,9 @@ GEM
|
|
12
12
|
remote: http://rubygems.org/
|
13
13
|
specs:
|
14
14
|
abstract (1.0.0)
|
15
|
-
actionpack (3.0.
|
16
|
-
activemodel (= 3.0.
|
17
|
-
activesupport (= 3.0.
|
15
|
+
actionpack (3.0.5)
|
16
|
+
activemodel (= 3.0.5)
|
17
|
+
activesupport (= 3.0.5)
|
18
18
|
builder (~> 2.1.2)
|
19
19
|
erubis (~> 2.6.6)
|
20
20
|
i18n (~> 0.4)
|
@@ -22,13 +22,13 @@ GEM
|
|
22
22
|
rack-mount (~> 0.6.13)
|
23
23
|
rack-test (~> 0.5.7)
|
24
24
|
tzinfo (~> 0.3.23)
|
25
|
-
activemodel (3.0.
|
26
|
-
activesupport (= 3.0.
|
25
|
+
activemodel (3.0.5)
|
26
|
+
activesupport (= 3.0.5)
|
27
27
|
builder (~> 2.1.2)
|
28
28
|
i18n (~> 0.4)
|
29
|
-
activesupport (3.0.
|
29
|
+
activesupport (3.0.5)
|
30
30
|
builder (2.1.2)
|
31
|
-
couchrest (1.0.
|
31
|
+
couchrest (1.1.0.pre2)
|
32
32
|
json (~> 1.5.1)
|
33
33
|
mime-types (~> 1.15)
|
34
34
|
rest-client (~> 1.6.1)
|
@@ -39,13 +39,13 @@ GEM
|
|
39
39
|
json (1.5.1)
|
40
40
|
mime-types (1.16)
|
41
41
|
rack (1.2.1)
|
42
|
-
rack-mount (0.6.
|
42
|
+
rack-mount (0.6.14)
|
43
43
|
rack (>= 1.0.0)
|
44
44
|
rack-test (0.5.7)
|
45
45
|
rack (>= 1.0)
|
46
|
-
railties (3.0.
|
47
|
-
actionpack (= 3.0.
|
48
|
-
activesupport (= 3.0.
|
46
|
+
railties (3.0.5)
|
47
|
+
actionpack (= 3.0.5)
|
48
|
+
activesupport (= 3.0.5)
|
49
49
|
rake (>= 0.8.7)
|
50
50
|
thor (~> 0.14.4)
|
51
51
|
rake (0.8.7)
|
@@ -60,14 +60,14 @@ GEM
|
|
60
60
|
diff-lcs (~> 1.1.2)
|
61
61
|
rspec-mocks (2.3.0)
|
62
62
|
thor (0.14.6)
|
63
|
-
tzinfo (0.3.
|
63
|
+
tzinfo (0.3.26)
|
64
64
|
|
65
65
|
PLATFORMS
|
66
66
|
ruby
|
67
67
|
|
68
68
|
DEPENDENCIES
|
69
69
|
activemodel (~> 3.0.0)
|
70
|
-
couchrest (
|
70
|
+
couchrest (= 1.1.0.pre2)
|
71
71
|
couchrest_model!
|
72
72
|
mime-types (~> 1.15)
|
73
73
|
rack-test (>= 0.5.7)
|
data/README.md
CHANGED
@@ -499,6 +499,48 @@ A really interesting use of `:proxy` and `:view` together could be where you'd l
|
|
499
499
|
|
500
500
|
Pretty cool!
|
501
501
|
|
502
|
+
## Proxy Support
|
503
|
+
|
504
|
+
CouchDB makes it really easy to create databases on the fly, so easy in fact that it is perfectly
|
505
|
+
feasable to have one database per user or per company or per whatever makes sense to split into
|
506
|
+
its own individual database. CouchRest Model now makes it really easy to support this scenario
|
507
|
+
using the proxy methods. Here's a quick example:
|
508
|
+
|
509
|
+
# Define a master company class, its children should be in their own DB
|
510
|
+
class Company < CouchRest::Model::Base
|
511
|
+
use_database COUCHDB_DATABASE
|
512
|
+
property :name
|
513
|
+
property :slug
|
514
|
+
|
515
|
+
proxy_for :invoices
|
516
|
+
|
517
|
+
def proxy_database
|
518
|
+
@proxy_database ||= COUCHDB_SERVER.database!("project_#{slug}")
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
# Invoices belong to a company
|
523
|
+
class Invoice < CouchRest::Model::Base
|
524
|
+
property :date
|
525
|
+
property :total
|
526
|
+
|
527
|
+
proxied_by :company
|
528
|
+
|
529
|
+
design do
|
530
|
+
view :by_date
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
By setting up our models like this, the invoices should be accessed via a company object:
|
535
|
+
|
536
|
+
company = Company.first
|
537
|
+
company.invoices.new # build a new invoice
|
538
|
+
company.invoices.by_date.first # find company's first invoice by date
|
539
|
+
|
540
|
+
Internally, all requests for invoices are passed through a model proxy. Aside from the
|
541
|
+
basic methods and views, it also ensures that some of the more complex queries are supported
|
542
|
+
such as validating for uniqueness and associations.
|
543
|
+
|
502
544
|
|
503
545
|
## Configuration
|
504
546
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.1.0.
|
1
|
+
1.1.0.beta2
|
data/couchrest_model.gemspec
CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |s|
|
|
23
23
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
24
24
|
s.require_paths = ["lib"]
|
25
25
|
|
26
|
-
s.add_dependency(%q<couchrest>, "
|
26
|
+
s.add_dependency(%q<couchrest>, "1.1.0.pre2")
|
27
27
|
s.add_dependency(%q<mime-types>, "~> 1.15")
|
28
28
|
s.add_dependency(%q<activemodel>, "~> 3.0.0")
|
29
29
|
s.add_dependency(%q<tzinfo>, "~> 0.3.22")
|
data/history.txt
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
== 1.1.0.beta2
|
2
|
+
|
3
|
+
* Minor enhancements:
|
4
|
+
* Time handling improved in accordance with CouchRest 1.1.0. Always set to UTC.
|
5
|
+
* Refinements to associations and uniqueness validation for proxy (based on issue found by Gleb Kanterov)
|
6
|
+
* Added :allow_nil and :allow_blank options when creating a new view
|
7
|
+
* Unique Validation now supports scopes!
|
8
|
+
* Added support for #keys with list on Design View.
|
9
|
+
|
1
10
|
== 1.1.0.beta
|
2
11
|
|
3
12
|
* Epic enhancements:
|
@@ -11,30 +11,37 @@ module CouchRest
|
|
11
11
|
module ClassMethods
|
12
12
|
|
13
13
|
# Define an association that this object belongs to.
|
14
|
-
#
|
14
|
+
#
|
15
|
+
# An attribute will be created matching the name of the attribute
|
16
|
+
# with '_id' on the end, or the foreign key (:foreign_key) provided.
|
17
|
+
#
|
18
|
+
# Searching for the assocated object is performed using a string
|
19
|
+
# (:proxy) to be evaulated in the context of the owner. Typically
|
20
|
+
# this will be set to the class name (:class_name), or determined
|
21
|
+
# automatically if the owner belongs to a proxy object.
|
22
|
+
#
|
23
|
+
# If the association owner is proxied by another model, than an attempt will
|
24
|
+
# be made to automatically determine the correct place to request
|
25
|
+
# the documents. Typically, this is a method with the pluralized name of the
|
26
|
+
# association inside owner's owner, or proxy.
|
27
|
+
#
|
28
|
+
# For example, imagine a company acts as a proxy for invoices and clients.
|
29
|
+
# If an invoice belongs to a client, the invoice will need to access the
|
30
|
+
# list of clients via the proxy. So a request to search for the associated
|
31
|
+
# client from an invoice would look like:
|
32
|
+
#
|
33
|
+
# self.company.clients
|
34
|
+
#
|
35
|
+
# If the name of the collection proxy is not the pluralized assocation name,
|
36
|
+
# it can be set with the :proxy_name option.
|
37
|
+
#
|
15
38
|
def belongs_to(attrib, *options)
|
16
|
-
opts =
|
17
|
-
:foreign_key => attrib.to_s + '_id',
|
18
|
-
:class_name => attrib.to_s.camelcase,
|
19
|
-
:proxy => nil
|
20
|
-
}
|
21
|
-
case options.first
|
22
|
-
when Hash
|
23
|
-
opts.merge!(options.first)
|
24
|
-
end
|
39
|
+
opts = merge_belongs_to_association_options(attrib, options.first)
|
25
40
|
|
26
|
-
|
27
|
-
opts[:class] = opts[:class_name].constantize
|
28
|
-
rescue NameError
|
29
|
-
raise NameError, "Unable to convert class name into Constant for #{self.name}##{attrib}"
|
30
|
-
end
|
41
|
+
property(opts[:foreign_key], opts)
|
31
42
|
|
32
|
-
|
33
|
-
|
34
|
-
create_belongs_to_getter(attrib, prop, opts)
|
35
|
-
create_belongs_to_setter(attrib, prop, opts)
|
36
|
-
|
37
|
-
prop
|
43
|
+
create_belongs_to_getter(attrib, opts)
|
44
|
+
create_belongs_to_setter(attrib, opts)
|
38
45
|
end
|
39
46
|
|
40
47
|
# Provide access to a collection of objects where the associated
|
@@ -77,44 +84,50 @@ module CouchRest
|
|
77
84
|
# frequently! Use with prudence.
|
78
85
|
#
|
79
86
|
def collection_of(attrib, *options)
|
80
|
-
opts =
|
81
|
-
|
82
|
-
:class_name => attrib.to_s.singularize.camelcase,
|
83
|
-
:proxy => nil
|
84
|
-
}
|
85
|
-
case options.first
|
86
|
-
when Hash
|
87
|
-
opts.merge!(options.first)
|
88
|
-
end
|
89
|
-
begin
|
90
|
-
opts[:class] = opts[:class_name].constantize
|
91
|
-
rescue
|
92
|
-
raise "Unable to convert class name into Constant for #{self.name}##{attrib}"
|
93
|
-
end
|
87
|
+
opts = merge_belongs_to_association_options(attrib, options.first)
|
88
|
+
opts[:foreign_key] = opts[:foreign_key].pluralize
|
94
89
|
opts[:readonly] = true
|
95
90
|
|
96
|
-
|
91
|
+
property(opts[:foreign_key], [], opts)
|
97
92
|
|
98
|
-
create_collection_of_property_setter(attrib,
|
99
|
-
create_collection_of_getter(attrib,
|
100
|
-
create_collection_of_setter(attrib,
|
101
|
-
|
102
|
-
prop
|
93
|
+
create_collection_of_property_setter(attrib, opts)
|
94
|
+
create_collection_of_getter(attrib, opts)
|
95
|
+
create_collection_of_setter(attrib, opts)
|
103
96
|
end
|
104
97
|
|
105
98
|
|
106
99
|
private
|
107
100
|
|
108
|
-
def
|
109
|
-
|
101
|
+
def merge_belongs_to_association_options(attrib, options = nil)
|
102
|
+
opts = {
|
103
|
+
:foreign_key => attrib.to_s.singularize + '_id',
|
104
|
+
:class_name => attrib.to_s.singularize.camelcase,
|
105
|
+
:proxy_name => attrib.to_s.pluralize
|
106
|
+
}
|
107
|
+
opts.merge!(options) if options.is_a?(Hash)
|
108
|
+
|
109
|
+
# Generate a string for the proxy method call
|
110
|
+
# Assumes that the proxy_owner_method from "proxyable" is available.
|
111
|
+
if opts[:proxy].to_s.empty?
|
112
|
+
opts[:proxy] = if proxy_owner_method
|
113
|
+
"self.#{proxy_owner_method}.#{opts[:proxy_name]}"
|
114
|
+
else
|
115
|
+
opts[:class_name]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
opts
|
120
|
+
end
|
121
|
+
|
122
|
+
def create_belongs_to_getter(attrib, options)
|
110
123
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
111
124
|
def #{attrib}
|
112
|
-
@#{attrib} ||= #{options[:foreign_key]}.nil? ? nil :
|
125
|
+
@#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : #{options[:proxy]}.get(self.#{options[:foreign_key]})
|
113
126
|
end
|
114
127
|
EOS
|
115
128
|
end
|
116
129
|
|
117
|
-
def create_belongs_to_setter(attrib,
|
130
|
+
def create_belongs_to_setter(attrib, options)
|
118
131
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
119
132
|
def #{attrib}=(value)
|
120
133
|
self.#{options[:foreign_key]} = value.nil? ? nil : value.id
|
@@ -125,7 +138,7 @@ module CouchRest
|
|
125
138
|
|
126
139
|
### collection_of support methods
|
127
140
|
|
128
|
-
def create_collection_of_property_setter(attrib,
|
141
|
+
def create_collection_of_property_setter(attrib, options)
|
129
142
|
# ensure CollectionOfProxy is nil, ready to be reloaded on request
|
130
143
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
131
144
|
def #{options[:foreign_key]}=(value)
|
@@ -135,18 +148,17 @@ module CouchRest
|
|
135
148
|
EOS
|
136
149
|
end
|
137
150
|
|
138
|
-
def create_collection_of_getter(attrib,
|
139
|
-
base = options[:proxy] || options[:class_name]
|
151
|
+
def create_collection_of_getter(attrib, options)
|
140
152
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
141
153
|
def #{attrib}(reload = false)
|
142
154
|
return @#{attrib} unless @#{attrib}.nil? or reload
|
143
|
-
ary = self.#{options[:foreign_key]}.collect{|i|
|
155
|
+
ary = self.#{options[:foreign_key]}.collect{|i| #{options[:proxy]}.get(i)}
|
144
156
|
@#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
|
145
157
|
end
|
146
158
|
EOS
|
147
159
|
end
|
148
160
|
|
149
|
-
def create_collection_of_setter(attrib,
|
161
|
+
def create_collection_of_setter(attrib, options)
|
150
162
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
151
163
|
def #{attrib}=(value)
|
152
164
|
@#{attrib} = ::CouchRest::CollectionOfProxy.new(value, self, '#{options[:foreign_key]}')
|
File without changes
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Model
|
3
|
+
module CoreExtensions
|
4
|
+
module TimeParsing
|
5
|
+
|
6
|
+
if RUBY_VERSION < "1.9.0"
|
7
|
+
# Overrwrite Ruby's standard new method to provide compatible support
|
8
|
+
# of 1.9.2's Time.new method.
|
9
|
+
#
|
10
|
+
# Only supports syntax like:
|
11
|
+
#
|
12
|
+
# Time.new(2011, 4, 1, 18, 50, 32, "+02:00")
|
13
|
+
# # or
|
14
|
+
# Time.new(2011, 4, 1, 18, 50, 32)
|
15
|
+
#
|
16
|
+
def new(*args)
|
17
|
+
return super() if (args.empty?)
|
18
|
+
zone = args.delete_at(6)
|
19
|
+
time = mktime(*args)
|
20
|
+
if zone =~ /([\+|\-]?)(\d{2}):?(\d{2})/
|
21
|
+
tz_difference = ("#{$1 == '-' ? '+' : '-'}#{$2}".to_i * 3600) + ($3.to_i * 60)
|
22
|
+
time + tz_difference + zone_offset(time.zone)
|
23
|
+
else
|
24
|
+
time
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Attemtps to parse a time string in ISO8601 format.
|
30
|
+
# If no match is found, the standard time parse will be used.
|
31
|
+
#
|
32
|
+
# Times, unless provided with a time zone, are assumed to be in
|
33
|
+
# UTC.
|
34
|
+
#
|
35
|
+
def parse_iso8601(string)
|
36
|
+
if (string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})(Z| ?([\+|\s|\-])?(\d{2}):?(\d{2}))?/)
|
37
|
+
# $1 = year
|
38
|
+
# $2 = month
|
39
|
+
# $3 = day
|
40
|
+
# $4 = hours
|
41
|
+
# $5 = minutes
|
42
|
+
# $6 = seconds
|
43
|
+
# $7 = UTC or Timezone
|
44
|
+
# $8 = time zone direction
|
45
|
+
# $9 = tz difference hours
|
46
|
+
# $10 = tz difference minutes
|
47
|
+
|
48
|
+
if (!$7.to_s.empty? && $7 != 'Z')
|
49
|
+
new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, "#{$8 == '-' ? '-' : '+'}#{$9}:#{$10}")
|
50
|
+
else
|
51
|
+
utc($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
parse(string)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
Time.class_eval do
|
64
|
+
extend CouchRest::Model::CoreExtensions::TimeParsing
|
65
|
+
end
|
66
|
+
|
@@ -25,7 +25,7 @@ module CouchRest
|
|
25
25
|
self.owner = parent
|
26
26
|
self.name = name.to_s
|
27
27
|
# Default options:
|
28
|
-
self.query = {
|
28
|
+
self.query = { }
|
29
29
|
elsif parent.is_a?(self.class)
|
30
30
|
self.model = (new_query.delete(:proxy) || parent.model)
|
31
31
|
self.owner = parent.owner
|
@@ -139,12 +139,6 @@ module CouchRest
|
|
139
139
|
execute['total_rows']
|
140
140
|
end
|
141
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
142
|
# Convenience wrapper to provide all the values from the route
|
149
143
|
# set without having to go through +rows+.
|
150
144
|
def values
|
@@ -181,7 +175,7 @@ module CouchRest
|
|
181
175
|
#
|
182
176
|
# Cannot be used when the +#startkey+ or +#endkey+ have been set.
|
183
177
|
def key(value)
|
184
|
-
raise "View#key cannot be used when startkey or endkey have been set" unless query[:startkey].nil? && query[:endkey].nil?
|
178
|
+
raise "View#key cannot be used when startkey or endkey have been set" unless query[:keys].nil? && query[:startkey].nil? && query[:endkey].nil?
|
185
179
|
update_query(:key => value)
|
186
180
|
end
|
187
181
|
|
@@ -193,7 +187,7 @@ module CouchRest
|
|
193
187
|
#
|
194
188
|
# Cannot be used if the key has been set.
|
195
189
|
def startkey(value)
|
196
|
-
raise "View#startkey cannot be used when key has been set" unless query[:key].nil?
|
190
|
+
raise "View#startkey cannot be used when key has been set" unless query[:key].nil? && query[:keys].nil?
|
197
191
|
update_query(:startkey => value)
|
198
192
|
end
|
199
193
|
|
@@ -210,7 +204,7 @@ module CouchRest
|
|
210
204
|
# See the +#startkey+ method for more details and the +#inclusive_end+
|
211
205
|
# option.
|
212
206
|
def endkey(value)
|
213
|
-
raise "View#endkey cannot be used when key has been set" unless query[:key].nil?
|
207
|
+
raise "View#endkey cannot be used when key has been set" unless query[:key].nil? && query[:keys].nil?
|
214
208
|
update_query(:endkey => value)
|
215
209
|
end
|
216
210
|
|
@@ -221,6 +215,22 @@ module CouchRest
|
|
221
215
|
update_query(:endkey_docid => value.is_a?(String) ? value : value.id)
|
222
216
|
end
|
223
217
|
|
218
|
+
# Keys is a special CouchDB option that will cause the view request to be POSTed
|
219
|
+
# including an array of keys. Only documents with the matching keys will be
|
220
|
+
# returned. This is much faster than sending multiple requests for a set
|
221
|
+
# non-consecutive documents.
|
222
|
+
#
|
223
|
+
# If no values are provided, this method will act as a wrapper around
|
224
|
+
# the rows result set, providing an array of keys.
|
225
|
+
def keys(*keys)
|
226
|
+
if keys.empty?
|
227
|
+
rows.map{|r| r.key}
|
228
|
+
else
|
229
|
+
raise "View#keys cannot by used when key or startkey/endkey have been set" unless query[:key].nil? && query[:startkey].nil? && query[:endkey].nil?
|
230
|
+
update_query(:keys => keys.first)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
224
234
|
|
225
235
|
# The results should be provided in descending order.
|
226
236
|
#
|
@@ -341,8 +351,6 @@ module CouchRest
|
|
341
351
|
(offset_value / limit_value) + 1
|
342
352
|
end
|
343
353
|
|
344
|
-
|
345
|
-
|
346
354
|
protected
|
347
355
|
|
348
356
|
def include_docs!
|
@@ -371,7 +379,7 @@ module CouchRest
|
|
371
379
|
def use_database
|
372
380
|
query[:database] || model.database
|
373
381
|
end
|
374
|
-
|
382
|
+
|
375
383
|
def execute
|
376
384
|
return self.result if result
|
377
385
|
raise "Database must be defined in model or view!" if use_database.nil?
|
@@ -412,6 +420,16 @@ module CouchRest
|
|
412
420
|
# The view name is the same, but three keys would be used in the
|
413
421
|
# subsecuent index.
|
414
422
|
#
|
423
|
+
# By default, a check is made on each of the view's keys to ensure they
|
424
|
+
# do not contain a nil value ('null' in javascript). This is probably what
|
425
|
+
# you want in most cases but sometimes in can be useful to create an
|
426
|
+
# index where nil is permited. Set the <tt>:allow_nil</tt> option to true to
|
427
|
+
# remove this check.
|
428
|
+
#
|
429
|
+
# Conversely, keys are not checked to see if they are empty or blank. If you'd
|
430
|
+
# like to enable this, set the <tt>:allow_blank</tt> option to false. The default
|
431
|
+
# is true, empty strings are permited in the indexes.
|
432
|
+
#
|
415
433
|
def create(model, name, opts = {})
|
416
434
|
|
417
435
|
unless opts[:map]
|
@@ -421,12 +439,14 @@ module CouchRest
|
|
421
439
|
|
422
440
|
raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?
|
423
441
|
|
442
|
+
opts[:allow_blank] = opts[:allow_blank].nil? ? true : opts[:allow_blank]
|
424
443
|
opts[:guards] ||= []
|
425
444
|
opts[:guards].push "(doc['#{model.model_type_key}'] == '#{model.to_s}')"
|
426
445
|
|
427
446
|
keys = opts[:by].map{|o| "doc['#{o}']"}
|
428
447
|
emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]"
|
429
|
-
opts[:guards] += keys.map{|k| "(#{k} != null)"}
|
448
|
+
opts[:guards] += keys.map{|k| "(#{k} != null)"} unless opts[:allow_nil]
|
449
|
+
opts[:guards] += keys.map{|k| "(#{k} != '')"} unless opts[:allow_blank]
|
430
450
|
opts[:map] = <<-EOF
|
431
451
|
function(doc) {
|
432
452
|
if (#{opts[:guards].join(' && ')}) {
|
@@ -131,8 +131,10 @@ module CouchRest
|
|
131
131
|
end
|
132
132
|
|
133
133
|
# Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
|
134
|
-
# on the document whenever saving occurs.
|
135
|
-
#
|
134
|
+
# on the document whenever saving occurs.
|
135
|
+
#
|
136
|
+
# These properties are casted as Time objects, so they should always
|
137
|
+
# be set to UTC.
|
136
138
|
def timestamps!
|
137
139
|
class_eval <<-EOS, __FILE__, __LINE__
|
138
140
|
property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
|
@@ -4,31 +4,36 @@ module CouchRest
|
|
4
4
|
module Proxyable
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
|
-
included do
|
8
|
-
attr_accessor :model_proxy
|
9
|
-
end
|
10
|
-
|
11
7
|
module ClassMethods
|
12
8
|
|
13
9
|
# Define a collection that will use the base model for the database connection
|
14
10
|
# details.
|
15
|
-
def proxy_for(
|
11
|
+
def proxy_for(assoc_name, options = {})
|
16
12
|
db_method = options[:database_method] || "proxy_database"
|
17
|
-
options[:class_name] ||=
|
13
|
+
options[:class_name] ||= assoc_name.to_s.singularize.camelize
|
18
14
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
19
|
-
def #{
|
15
|
+
def #{assoc_name}
|
20
16
|
unless respond_to?('#{db_method}')
|
21
17
|
raise "Missing ##{db_method} method for proxy"
|
22
18
|
end
|
23
|
-
@#{
|
19
|
+
@#{assoc_name} ||= CouchRest::Model::Proxyable::ModelProxy.new(::#{options[:class_name]}, self, self.class.to_s.underscore, #{db_method})
|
24
20
|
end
|
25
21
|
EOS
|
26
22
|
end
|
27
23
|
|
28
24
|
def proxied_by(model_name, options = {})
|
29
|
-
raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name)
|
25
|
+
raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name) || !proxy_owner_method.nil?
|
26
|
+
self.proxy_owner_method = model_name
|
27
|
+
attr_accessor :model_proxy
|
30
28
|
attr_accessor model_name
|
31
29
|
end
|
30
|
+
|
31
|
+
# Define an a class variable accessor ready to be inherited and unique
|
32
|
+
# for each Class using the base.
|
33
|
+
# Perhaps there is a shorter way of writing this.
|
34
|
+
def proxy_owner_method=(name); @proxy_owner_method = name; end
|
35
|
+
def proxy_owner_method; @proxy_owner_method; end
|
36
|
+
|
32
37
|
end
|
33
38
|
|
34
39
|
class ModelProxy
|
@@ -132,10 +137,10 @@ module CouchRest
|
|
132
137
|
# Update the document's proxy details, specifically, the fields that
|
133
138
|
# link back to the original document.
|
134
139
|
def proxy_update(doc)
|
135
|
-
if doc
|
136
|
-
doc.database = @database
|
137
|
-
doc.model_proxy = self
|
138
|
-
doc.send("#{owner_name}=", owner)
|
140
|
+
if doc && doc.is_a?(model)
|
141
|
+
doc.database = @database
|
142
|
+
doc.model_proxy = self
|
143
|
+
doc.send("#{owner_name}=", owner)
|
139
144
|
end
|
140
145
|
doc
|
141
146
|
end
|
@@ -1,26 +1,3 @@
|
|
1
|
-
class Time
|
2
|
-
# returns a local time value much faster than Time.parse
|
3
|
-
def self.mktime_with_offset(string)
|
4
|
-
string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})(([\+|\s|\-])*(\d{2}):?(\d{2}))?/
|
5
|
-
# $1 = year
|
6
|
-
# $2 = month
|
7
|
-
# $3 = day
|
8
|
-
# $4 = hours
|
9
|
-
# $5 = minutes
|
10
|
-
# $6 = seconds
|
11
|
-
# $8 = time zone direction
|
12
|
-
# $9 = tz difference
|
13
|
-
# utc time with wrong TZ info:
|
14
|
-
time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6)
|
15
|
-
if ($7)
|
16
|
-
tz_difference = ("#{$8 == '-' ? '+' : '-'}#{$9}".to_i * 3600)
|
17
|
-
time + tz_difference + zone_offset(time.zone)
|
18
|
-
else
|
19
|
-
time
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
1
|
module CouchRest
|
25
2
|
module Model
|
26
3
|
module Typecast
|
@@ -29,7 +6,11 @@ module CouchRest
|
|
29
6
|
return nil if value.nil?
|
30
7
|
klass = property.type_class
|
31
8
|
if value.instance_of?(klass) || klass == Object
|
32
|
-
value
|
9
|
+
if klass == Time && !value.utc?
|
10
|
+
value.utc # Ensure Time is always in UTC
|
11
|
+
else
|
12
|
+
value
|
13
|
+
end
|
33
14
|
elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
|
34
15
|
send('typecast_to_'+klass.to_s.downcase, value)
|
35
16
|
else
|
@@ -127,12 +108,11 @@ module CouchRest
|
|
127
108
|
if value.is_a?(Hash)
|
128
109
|
typecast_hash_to_time(value)
|
129
110
|
else
|
130
|
-
Time.
|
111
|
+
Time.parse_iso8601(value.to_s)
|
131
112
|
end
|
132
113
|
rescue ArgumentError
|
133
114
|
value
|
134
115
|
rescue TypeError
|
135
|
-
# After failures, resort to normal time parse
|
136
116
|
value
|
137
117
|
end
|
138
118
|
|
@@ -150,13 +130,13 @@ module CouchRest
|
|
150
130
|
# Creates a Time instance from a Hash with keys :year, :month, :day,
|
151
131
|
# :hour, :min, :sec
|
152
132
|
def typecast_hash_to_time(value)
|
153
|
-
Time.
|
133
|
+
Time.utc(*extract_time(value))
|
154
134
|
end
|
155
135
|
|
156
136
|
# Extracts the given args from the hash. If a value does not exist, it
|
157
137
|
# uses the value of Time.now.
|
158
138
|
def extract_time(value)
|
159
|
-
now
|
139
|
+
now = Time.now
|
160
140
|
[:year, :month, :day, :hour, :min, :sec].map do |segment|
|
161
141
|
typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
|
162
142
|
end
|