sunspot 1.1.0 → 1.2.0
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 +10 -0
- data/Gemfile.lock +32 -0
- data/History.txt +24 -0
- data/README.rdoc +18 -5
- data/lib/sunspot.rb +40 -0
- data/lib/sunspot/dsl.rb +2 -2
- data/lib/sunspot/dsl/field_query.rb +2 -2
- data/lib/sunspot/dsl/fields.rb +0 -10
- data/lib/sunspot/dsl/restriction.rb +4 -4
- data/lib/sunspot/dsl/restriction_with_near.rb +121 -0
- data/lib/sunspot/dsl/scope.rb +55 -67
- data/lib/sunspot/dsl/standard_query.rb +11 -15
- data/lib/sunspot/field.rb +30 -29
- data/lib/sunspot/field_factory.rb +0 -18
- data/lib/sunspot/installer/solrconfig_updater.rb +0 -30
- data/lib/sunspot/query.rb +4 -3
- data/lib/sunspot/query/common_query.rb +2 -2
- data/lib/sunspot/query/composite_fulltext.rb +7 -2
- data/lib/sunspot/query/connective.rb +21 -6
- data/lib/sunspot/query/dismax.rb +1 -0
- data/lib/sunspot/query/geo.rb +53 -0
- data/lib/sunspot/query/more_like_this.rb +1 -0
- data/lib/sunspot/query/restriction.rb +5 -5
- data/lib/sunspot/query/standard_query.rb +0 -4
- data/lib/sunspot/search/abstract_search.rb +1 -7
- data/lib/sunspot/search/hit.rb +10 -10
- data/lib/sunspot/search/query_facet.rb +8 -3
- data/lib/sunspot/session.rb +10 -2
- data/lib/sunspot/session_proxy.rb +16 -0
- data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +1 -1
- data/lib/sunspot/session_proxy/sharding_session_proxy.rb +7 -0
- data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
- data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +1 -1
- data/lib/sunspot/setup.rb +1 -17
- data/lib/sunspot/type.rb +38 -6
- data/lib/sunspot/util.rb +21 -31
- data/lib/sunspot/version.rb +1 -1
- data/solr/solr/conf/solrconfig.xml +0 -4
- data/spec/api/binding_spec.rb +12 -0
- data/spec/api/indexer/attributes_spec.rb +22 -22
- data/spec/api/query/connectives_examples.rb +14 -1
- data/spec/api/query/fulltext_examples.rb +3 -3
- data/spec/api/query/geo_examples.rb +69 -0
- data/spec/api/query/scope_examples.rb +32 -13
- data/spec/api/query/standard_spec.rb +1 -1
- data/spec/api/search/faceting_spec.rb +5 -1
- data/spec/api/search/hits_spec.rb +14 -12
- data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +1 -1
- data/spec/api/session_proxy/sharding_session_proxy_spec.rb +1 -1
- data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
- data/spec/api/session_spec.rb +22 -0
- data/spec/integration/local_search_spec.rb +42 -69
- data/spec/integration/scoped_search_spec.rb +30 -0
- data/spec/mocks/connection.rb +6 -2
- data/spec/mocks/photo.rb +0 -1
- data/spec/mocks/post.rb +11 -2
- data/spec/mocks/user.rb +6 -1
- data/spec/spec_helper.rb +2 -12
- metadata +209 -177
- data/lib/sunspot/query/local.rb +0 -26
- data/solr/solr/lib/lucene-spatial-2.9.1.jar +0 -0
- data/solr/solr/lib/solr-spatial-light-0.0.6.jar +0 -0
- data/spec/api/query/local_examples.rb +0 -38
- data/tasks/gemspec.rake +0 -33
- data/tasks/rcov.rake +0 -28
- data/tasks/spec.rake +0 -24
@@ -81,7 +81,7 @@ module Sunspot
|
|
81
81
|
begin
|
82
82
|
hits = if solr_response && solr_response['docs']
|
83
83
|
solr_response['docs'].map do |doc|
|
84
|
-
Hit.new(doc, highlights_for(doc),
|
84
|
+
Hit.new(doc, highlights_for(doc), self)
|
85
85
|
end
|
86
86
|
end
|
87
87
|
maybe_will_paginate(hits || [])
|
@@ -268,12 +268,6 @@ module Sunspot
|
|
268
268
|
end
|
269
269
|
end
|
270
270
|
|
271
|
-
def distance_for(doc)
|
272
|
-
if @solr_result['distances']
|
273
|
-
@solr_result['distances'][doc['id']]
|
274
|
-
end
|
275
|
-
end
|
276
|
-
|
277
271
|
def verified_hits
|
278
272
|
@verified_hits ||= maybe_will_paginate(hits.select { |hit| hit.instance })
|
279
273
|
end
|
data/lib/sunspot/search/hit.rb
CHANGED
@@ -3,8 +3,8 @@ module Sunspot
|
|
3
3
|
#
|
4
4
|
# Hit objects represent the raw information returned by Solr for a single
|
5
5
|
# document. As well as the primary key and class name, hit objects give
|
6
|
-
# access to stored field values, keyword relevance score, and
|
7
|
-
#
|
6
|
+
# access to stored field values, keyword relevance score, and keyword
|
7
|
+
# highlighting.
|
8
8
|
#
|
9
9
|
class Hit
|
10
10
|
SPECIAL_KEYS = Set.new(%w(id type score)) #:nodoc:
|
@@ -23,17 +23,12 @@ module Sunspot
|
|
23
23
|
#
|
24
24
|
attr_reader :score
|
25
25
|
#
|
26
|
-
# For geographical searches, this is the distance between the search
|
27
|
-
# centerpoint and the document's location. Otherwise, it's nil.
|
28
|
-
#
|
29
|
-
attr_reader :distance
|
30
26
|
|
31
27
|
attr_writer :result #:nodoc:
|
32
28
|
|
33
|
-
def initialize(raw_hit, highlights,
|
29
|
+
def initialize(raw_hit, highlights, search) #:nodoc:
|
34
30
|
@class_name, @primary_key = *raw_hit['id'].match(/([^ ]+) (.+)/)[1..2]
|
35
31
|
@score = raw_hit['score']
|
36
|
-
@distance = distance
|
37
32
|
@search = search
|
38
33
|
@stored_values = raw_hit
|
39
34
|
@stored_cache = {}
|
@@ -104,7 +99,7 @@ module Sunspot
|
|
104
99
|
private
|
105
100
|
|
106
101
|
def setup
|
107
|
-
@setup ||= Sunspot::Setup.for(@class_name)
|
102
|
+
@setup ||= Sunspot::Setup.for(Util.full_const_get(@class_name))
|
108
103
|
end
|
109
104
|
|
110
105
|
def highlights_cache
|
@@ -126,7 +121,12 @@ module Sunspot
|
|
126
121
|
def stored_value(field_name, dynamic_field_name)
|
127
122
|
setup.stored_fields(field_name, dynamic_field_name).each do |field|
|
128
123
|
if value = @stored_values[field.indexed_name]
|
129
|
-
|
124
|
+
case value
|
125
|
+
when Array
|
126
|
+
return value.map { |item| field.cast(item) }
|
127
|
+
else
|
128
|
+
return field.cast(value)
|
129
|
+
end
|
130
130
|
end
|
131
131
|
end
|
132
132
|
nil
|
@@ -38,7 +38,7 @@ module Sunspot
|
|
38
38
|
private
|
39
39
|
|
40
40
|
def sort_rows!(rows)
|
41
|
-
case @options[:sort] || (:count if
|
41
|
+
case @options[:sort] || (:count if limit)
|
42
42
|
when :count
|
43
43
|
rows.sort! { |lrow, rrow| rrow.count <=> lrow.count }
|
44
44
|
when :index
|
@@ -52,11 +52,16 @@ module Sunspot
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
55
|
-
if
|
56
|
-
rows.replace(rows.first(
|
55
|
+
if limit
|
56
|
+
rows.replace(rows.first(limit))
|
57
57
|
end
|
58
58
|
rows
|
59
59
|
end
|
60
|
+
|
61
|
+
def limit
|
62
|
+
return @limit if defined?(@limit)
|
63
|
+
@limit = (@options[:limit].to_i if @options[:limit].to_i > 0)
|
64
|
+
end
|
60
65
|
end
|
61
66
|
end
|
62
67
|
end
|
data/lib/sunspot/session.rb
CHANGED
@@ -107,6 +107,14 @@ module Sunspot
|
|
107
107
|
connection.commit
|
108
108
|
end
|
109
109
|
|
110
|
+
#
|
111
|
+
# See Sunspot.optimize
|
112
|
+
#
|
113
|
+
def optimize
|
114
|
+
@adds = @deletes = 0
|
115
|
+
connection.optimize
|
116
|
+
end
|
117
|
+
|
110
118
|
#
|
111
119
|
# See Sunspot.remove
|
112
120
|
#
|
@@ -115,9 +123,9 @@ module Sunspot
|
|
115
123
|
types = objects
|
116
124
|
conjunction = Query::Connective::Conjunction.new
|
117
125
|
if types.length == 1
|
118
|
-
conjunction.
|
126
|
+
conjunction.add_positive_restriction(TypeField.instance, Query::Restriction::EqualTo, types.first)
|
119
127
|
else
|
120
|
-
conjunction.
|
128
|
+
conjunction.add_positive_restriction(TypeField.instance, Query::Restriction::AnyOf, types)
|
121
129
|
end
|
122
130
|
dsl = DSL::Scope.new(conjunction, setup_for_types(types))
|
123
131
|
Util.instance_eval_or_call(dsl, &block)
|
@@ -27,6 +27,14 @@ module Sunspot
|
|
27
27
|
module SessionProxy
|
28
28
|
NotSupportedError = Class.new(StandardError)
|
29
29
|
|
30
|
+
autoload(
|
31
|
+
:AbstractSessionProxy,
|
32
|
+
File.join(
|
33
|
+
File.dirname(__FILE__),
|
34
|
+
'session_proxy',
|
35
|
+
'abstract_session_proxy'
|
36
|
+
)
|
37
|
+
)
|
30
38
|
autoload(
|
31
39
|
:ThreadLocalSessionProxy,
|
32
40
|
File.join(
|
@@ -67,5 +75,13 @@ module Sunspot
|
|
67
75
|
'id_sharding_session_proxy'
|
68
76
|
)
|
69
77
|
)
|
78
|
+
autoload(
|
79
|
+
:SilentFailSessionProxy,
|
80
|
+
File.join(
|
81
|
+
File.dirname(__FILE__),
|
82
|
+
'session_proxy',
|
83
|
+
'silent_fail_session_proxy'
|
84
|
+
)
|
85
|
+
)
|
70
86
|
end
|
71
87
|
end
|
@@ -18,7 +18,7 @@ module Sunspot
|
|
18
18
|
attr_reader :slave_session
|
19
19
|
|
20
20
|
delegate :batch, :commit, :commit_if_delete_dirty, :commit_if_dirty,
|
21
|
-
:config, :delete_dirty?, :dirty?, :index, :index!, :remove,
|
21
|
+
:config, :delete_dirty?, :dirty?, :index, :index!, :optimize, :remove,
|
22
22
|
:remove!, :remove_all, :remove_all!, :remove_by_id,
|
23
23
|
:remove_by_id!, :to => :master_session
|
24
24
|
delegate :new_search, :search, :new_more_like_this, :more_like_this, :to => :slave_session
|
@@ -119,6 +119,13 @@ module Sunspot
|
|
119
119
|
all_sessions.each { |session| session.commit }
|
120
120
|
end
|
121
121
|
|
122
|
+
#
|
123
|
+
# Optimize all shards. See Sunspot.optimize
|
124
|
+
#
|
125
|
+
def optimize
|
126
|
+
all_sessions.each { |session| session.optimize }
|
127
|
+
end
|
128
|
+
|
122
129
|
#
|
123
130
|
# Commit all dirty sessions. Only dirty sessions will be committed.
|
124
131
|
#
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'abstract_session_proxy')
|
2
|
+
|
3
|
+
module Sunspot
|
4
|
+
module SessionProxy
|
5
|
+
class SilentFailSessionProxy < AbstractSessionProxy
|
6
|
+
|
7
|
+
attr_reader :search_session
|
8
|
+
|
9
|
+
delegate :new_search, :search, :config,
|
10
|
+
:new_more_like_this, :more_like_this,
|
11
|
+
:delete_dirty, :delete_dirty?,
|
12
|
+
:to => :search_session
|
13
|
+
|
14
|
+
def initialize(search_session = Sunspot.session)
|
15
|
+
@search_session = search_session
|
16
|
+
end
|
17
|
+
|
18
|
+
def rescued_exception(method, e)
|
19
|
+
$stderr.puts("Exception in #{method}: #{e.message}")
|
20
|
+
end
|
21
|
+
|
22
|
+
SUPPORTED_METHODS = [
|
23
|
+
:batch, :commit, :commit_if_dirty, :commit_if_delete_dirty, :dirty?,
|
24
|
+
:index!, :index, :optimize, :remove!, :remove, :remove_all!, :remove_all,
|
25
|
+
:remove_by_id!, :remove_by_id
|
26
|
+
]
|
27
|
+
|
28
|
+
SUPPORTED_METHODS.each do |method|
|
29
|
+
module_eval(<<-RUBY)
|
30
|
+
def #{method}(*args, &block)
|
31
|
+
begin
|
32
|
+
search_session.#{method}(*args, &block)
|
33
|
+
rescue => e
|
34
|
+
self.rescued_exception(:#{method}, e)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
RUBY
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -17,7 +17,7 @@ module Sunspot
|
|
17
17
|
attr_reader :config
|
18
18
|
@@next_id = 0
|
19
19
|
|
20
|
-
delegate :batch, :commit, :commit_if_delete_dirty, :commit_if_dirty, :delete_dirty?, :dirty?, :index, :index!, :new_search, :remove, :remove!, :remove_all, :remove_all!, :remove_by_id, :remove_by_id!, :search, :more_like_this, :new_more_like_this, :to => :session
|
20
|
+
delegate :batch, :commit, :commit_if_delete_dirty, :commit_if_dirty, :delete_dirty?, :dirty?, :index, :index!, :new_search, :optimize, :remove, :remove!, :remove_all, :remove_all!, :remove_by_id, :remove_by_id!, :search, :more_like_this, :new_more_like_this, :to => :session
|
21
21
|
|
22
22
|
#
|
23
23
|
# Optionally pass an existing Sunspot::Configuration object. If none is
|
data/lib/sunspot/setup.rb
CHANGED
@@ -77,15 +77,6 @@ module Sunspot
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
80
|
-
#
|
81
|
-
# The coordinates field factory is used for populating the coordinate fields
|
82
|
-
# of documents during index, but does not actually generate fields (since
|
83
|
-
# the field names used in search are static).
|
84
|
-
#
|
85
|
-
def set_coordinates_field(name = nil, &block)
|
86
|
-
@coordinates_field_factory = FieldFactory::Coordinates.new(name, &block)
|
87
|
-
end
|
88
|
-
|
89
80
|
#
|
90
81
|
# Add a document boost to documents at index time. Document boost can be
|
91
82
|
# static (the same for all documents of this class), or extracted on a per-
|
@@ -234,7 +225,6 @@ module Sunspot
|
|
234
225
|
def all_field_factories
|
235
226
|
all_field_factories = []
|
236
227
|
all_field_factories.concat(field_factories).concat(text_field_factories).concat(dynamic_field_factories)
|
237
|
-
all_field_factories << @coordinates_field_factory if @coordinates_field_factory
|
238
228
|
all_field_factories
|
239
229
|
end
|
240
230
|
|
@@ -319,13 +309,7 @@ module Sunspot
|
|
319
309
|
# Setup instance associated with the given class or its nearest ancestor
|
320
310
|
#
|
321
311
|
def for(clazz) #:nodoc:
|
322
|
-
|
323
|
-
if clazz.respond_to?(:name)
|
324
|
-
clazz.name
|
325
|
-
else
|
326
|
-
clazz
|
327
|
-
end
|
328
|
-
setups[class_name.to_sym] || self.for(clazz.superclass) if clazz
|
312
|
+
setups[clazz.name.to_sym] || self.for(clazz.superclass) if clazz
|
329
313
|
end
|
330
314
|
|
331
315
|
protected
|
data/lib/sunspot/type.rb
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
begin
|
3
|
+
require 'geohash'
|
4
|
+
rescue LoadError => e
|
5
|
+
require 'pr_geohash'
|
6
|
+
end
|
7
|
+
|
1
8
|
module Sunspot
|
2
9
|
#
|
3
10
|
# This module contains singleton objects that represent the types that can be
|
@@ -58,12 +65,7 @@ module Sunspot
|
|
58
65
|
end
|
59
66
|
|
60
67
|
class AbstractType #:nodoc:
|
61
|
-
|
62
|
-
def instance
|
63
|
-
@instance ||= new
|
64
|
-
end
|
65
|
-
private :new
|
66
|
-
end
|
68
|
+
include Singleton
|
67
69
|
|
68
70
|
def accepts_dynamic?
|
69
71
|
true
|
@@ -317,11 +319,41 @@ module Sunspot
|
|
317
319
|
true
|
318
320
|
when 'false'
|
319
321
|
false
|
322
|
+
when true, false
|
323
|
+
string
|
320
324
|
end
|
321
325
|
end
|
322
326
|
end
|
323
327
|
register BooleanType, TrueClass, FalseClass
|
324
328
|
|
329
|
+
#
|
330
|
+
# The Location type encodes geographical coordinates as a GeoHash.
|
331
|
+
# The data for this type must respond to the `lat` and `lng` methods; you
|
332
|
+
# can use Sunspot::Util::Coordinates as a wrapper if your source data does
|
333
|
+
# not follow this API.
|
334
|
+
#
|
335
|
+
# Location fields are most usefully searched using the
|
336
|
+
# Sunspot::DSL::RestrictionWithType#near method; see that method for more
|
337
|
+
# information on geographical search.
|
338
|
+
#
|
339
|
+
# ==== Example
|
340
|
+
#
|
341
|
+
# Sunspot.setup(Post) do
|
342
|
+
# location :coordinates do
|
343
|
+
# Sunspot::Util::Coordinates.new(coordinates[0], coordinates[1])
|
344
|
+
# end
|
345
|
+
# end
|
346
|
+
#
|
347
|
+
class LocationType < AbstractType
|
348
|
+
def indexed_name(name)
|
349
|
+
"#{name}_s"
|
350
|
+
end
|
351
|
+
|
352
|
+
def to_indexed(value)
|
353
|
+
GeoHash.encode(value.lat.to_f, value.lng.to_f, 12)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
325
357
|
class ClassType < AbstractType
|
326
358
|
def indexed_name(name) #:nodoc:
|
327
359
|
'class_name'
|
data/lib/sunspot/util.rb
CHANGED
@@ -108,6 +108,20 @@ module Sunspot
|
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
111
|
+
#
|
112
|
+
# When generating boosts, Solr requires that the values be in standard
|
113
|
+
# (not scientific) notation. We would like to ensure a minimum number of
|
114
|
+
# significant digits (i.e., digits that are not prefix zeros) for small
|
115
|
+
# float values.
|
116
|
+
#
|
117
|
+
def format_float(f, digits)
|
118
|
+
if f < 1
|
119
|
+
sprintf('%.*f', digits - Math.log10(f), f)
|
120
|
+
else
|
121
|
+
f.to_s
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
111
125
|
#
|
112
126
|
# Perform a deep merge of hashes, returning the result as a new hash.
|
113
127
|
# See #deep_merge_into for rules used to merge the hashes
|
@@ -182,35 +196,7 @@ module Sunspot
|
|
182
196
|
end
|
183
197
|
end
|
184
198
|
|
185
|
-
|
186
|
-
def initialize(coords)
|
187
|
-
@coords = coords
|
188
|
-
end
|
189
|
-
|
190
|
-
def lat
|
191
|
-
if @coords.respond_to?(:first)
|
192
|
-
@coords.first
|
193
|
-
elsif @coords.respond_to?(:lat)
|
194
|
-
@coords.lat
|
195
|
-
else
|
196
|
-
@coords.latitude
|
197
|
-
end.to_f
|
198
|
-
end
|
199
|
-
|
200
|
-
def lng
|
201
|
-
if @coords.respond_to?(:last)
|
202
|
-
@coords.last
|
203
|
-
elsif @coords.respond_to?(:lng)
|
204
|
-
@coords.lng
|
205
|
-
elsif @coords.respond_to?(:lon)
|
206
|
-
@coords.lon
|
207
|
-
elsif @coords.respond_to?(:long)
|
208
|
-
@coords.long
|
209
|
-
elsif @coords.respond_to?(:longitude)
|
210
|
-
@coords.longitude
|
211
|
-
end.to_f
|
212
|
-
end
|
213
|
-
end
|
199
|
+
Coordinates = Struct.new(:lat, :lng)
|
214
200
|
|
215
201
|
class ContextBoundDelegate
|
216
202
|
class <<self
|
@@ -237,12 +223,16 @@ module Sunspot
|
|
237
223
|
@__receiver__, @__calling_context__ = receiver, calling_context
|
238
224
|
end
|
239
225
|
|
226
|
+
def id
|
227
|
+
@__calling_context__.__send__(:id)
|
228
|
+
end
|
229
|
+
|
240
230
|
def method_missing(method, *args, &block)
|
241
231
|
begin
|
242
|
-
@__receiver__.
|
232
|
+
@__receiver__.__send__(method.to_sym, *args, &block)
|
243
233
|
rescue ::NoMethodError => e
|
244
234
|
begin
|
245
|
-
@__calling_context__.
|
235
|
+
@__calling_context__.__send__(method.to_sym, *args, &block)
|
246
236
|
rescue ::NoMethodError
|
247
237
|
raise(e)
|
248
238
|
end
|
data/lib/sunspot/version.rb
CHANGED
@@ -442,9 +442,6 @@
|
|
442
442
|
<str name="version">2.1</str>
|
443
443
|
-->
|
444
444
|
</lst>
|
445
|
-
<arr name="last-components">
|
446
|
-
<str>spatial</str>
|
447
|
-
</arr>
|
448
445
|
</requestHandler>
|
449
446
|
<!-- Please refer to http://wiki.apache.org/solr/SolrReplication for details on configuring replication -->
|
450
447
|
<!-- remove the <lst name="master"> section if this is just a slave -->
|
@@ -928,7 +925,6 @@
|
|
928
925
|
<healthcheck type="file">server-enabled</healthcheck>
|
929
926
|
-->
|
930
927
|
</admin>
|
931
|
-
<searchComponent name="spatial" class="me.outofti.solrspatiallight.SpatialQueryComponent"/>
|
932
928
|
<requestHandler class="solr.MoreLikeThisHandler" name="/mlt">
|
933
929
|
<lst name="defaults">
|
934
930
|
<str name="mlt.mintf">1</str>
|
data/spec/api/binding_spec.rb
CHANGED
@@ -9,6 +9,14 @@ describe "DSL bindings" do
|
|
9
9
|
value.should == 'value'
|
10
10
|
end
|
11
11
|
|
12
|
+
it 'should give access to calling context\'s id method in search DSL' do
|
13
|
+
value = nil
|
14
|
+
session.search(Post) do
|
15
|
+
value = id
|
16
|
+
end
|
17
|
+
value.should == 16
|
18
|
+
end
|
19
|
+
|
12
20
|
it 'should give access to calling context\'s methods in nested DSL block' do
|
13
21
|
value = nil
|
14
22
|
session.search(Post) do
|
@@ -35,4 +43,8 @@ describe "DSL bindings" do
|
|
35
43
|
def test_method
|
36
44
|
'value'
|
37
45
|
end
|
46
|
+
|
47
|
+
def id
|
48
|
+
16
|
49
|
+
end
|
38
50
|
end
|