couchrest_model 1.1.0.beta → 1.1.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|