sugarcrm 0.5.3 → 0.6.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/README.rdoc +15 -10
- data/Rakefile +4 -1
- data/VERSION +1 -1
- data/lib/sugarcrm.rb +107 -119
- data/lib/sugarcrm/api/get_available_modules.rb +17 -0
- data/lib/sugarcrm/api/get_document_revision.rb +14 -0
- data/lib/sugarcrm/{get_entries.rb → api/get_entries.rb} +2 -2
- data/lib/sugarcrm/api/get_entries_count.rb +20 -0
- data/lib/sugarcrm/{get_entry.rb → api/get_entry.rb} +5 -8
- data/lib/sugarcrm/{get_entry_list.rb → api/get_entry_list.rb} +2 -2
- data/lib/sugarcrm/{get_module_fields.rb → api/get_module_fields.rb} +6 -3
- data/lib/sugarcrm/api/get_note_attachment.rb +15 -0
- data/lib/sugarcrm/{get_relationships.rb → api/get_relationship.rb} +9 -4
- data/lib/sugarcrm/api/get_report_entries.rb +19 -0
- data/lib/sugarcrm/api/get_server_info.rb +7 -0
- data/lib/sugarcrm/api/get_user_id.rb +13 -0
- data/lib/sugarcrm/api/get_user_team_id.rb +14 -0
- data/lib/sugarcrm/api/login.rb +18 -0
- data/lib/sugarcrm/api/logout.rb +15 -0
- data/lib/sugarcrm/api/seamless_login.rb +13 -0
- data/lib/sugarcrm/api/search_by_module.rb +24 -0
- data/lib/sugarcrm/api/set_campaign_merge.rb +15 -0
- data/lib/sugarcrm/api/set_document_revision.rb +15 -0
- data/lib/sugarcrm/api/set_entries.rb +15 -0
- data/lib/sugarcrm/api/set_entry.rb +15 -0
- data/lib/sugarcrm/api/set_note_attachment.rb +3 -0
- data/lib/sugarcrm/api/set_relationship.rb +18 -0
- data/lib/sugarcrm/api/set_relationships.rb +22 -0
- data/lib/sugarcrm/connection.rb +112 -0
- data/lib/sugarcrm/exceptions.rb +16 -0
- data/lib/sugarcrm/module.rb +11 -0
- data/lib/sugarcrm/request.rb +28 -0
- data/lib/sugarcrm/response.rb +29 -0
- data/test/helper.rb +5 -0
- data/test/test_connection.rb +60 -0
- data/test/test_response.rb +36 -0
- data/test/test_sugarcrm.rb +31 -45
- metadata +56 -17
- data/lib/net/http_digest_auth.rb +0 -44
- data/lib/stdlib/array.rb +0 -14
- data/lib/stdlib/hash.rb +0 -17
- data/lib/stdlib/object.rb +0 -5
- data/test/test_json_to_obj.rb +0 -48
@@ -0,0 +1,29 @@
|
|
1
|
+
module SugarCRM
|
2
|
+
# takes a raw JSON response and turns it into a REAL object
|
3
|
+
class Response
|
4
|
+
|
5
|
+
attr :response, false
|
6
|
+
attr :module, false
|
7
|
+
attr :attributes, false
|
8
|
+
attr :object, false
|
9
|
+
attr :id, false
|
10
|
+
|
11
|
+
def initialize(json)
|
12
|
+
@response = json
|
13
|
+
@module = @response["entry_list"][0]["module_name"].singularize
|
14
|
+
@attributes = flatten(@response["entry_list"][0]["name_value_list"])
|
15
|
+
@object = SugarCRM.const_get(@module).new(@attributes) if SugarCRM.const_get(@module)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Takes a hash like { "first_name" => {"name" => "first_name", "value" => "John"}}
|
19
|
+
# And flattens it into {"first_name" => "John"}
|
20
|
+
def flatten(list)
|
21
|
+
raise ArgumentError, 'method parameter must respond to #each_pair' unless list.respond_to? :each_pair
|
22
|
+
flat_list = {}
|
23
|
+
list.each_pair do |k,v|
|
24
|
+
flat_list[k.to_sym] = v["value"]
|
25
|
+
end
|
26
|
+
flat_list
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/test/helper.rb
CHANGED
@@ -7,4 +7,9 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
7
7
|
require 'sugarcrm'
|
8
8
|
|
9
9
|
class Test::Unit::TestCase
|
10
|
+
# Replace these with your test instance
|
11
|
+
URL = "http://valet/sugarcrm6"
|
12
|
+
USER = "admin"
|
13
|
+
PASS = 'letmein'
|
14
|
+
CRM = SugarCRM::Base.establish_connection(URL, USER, PASS, {:debug => true})
|
10
15
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require "test/unit"
|
3
|
+
require "pp"
|
4
|
+
|
5
|
+
class TestSugarcrm < Test::Unit::TestCase
|
6
|
+
context "A SugarCRM::Connection instance" do
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@connection = SugarCRM::Connection.new(URL, USER, PASS, false)
|
10
|
+
end
|
11
|
+
|
12
|
+
should "login and set session id" do
|
13
|
+
assert_not_nil @connection.session
|
14
|
+
end
|
15
|
+
|
16
|
+
should "retrieve the list of available modules" do
|
17
|
+
assert_instance_of Array, @connection.modules
|
18
|
+
end
|
19
|
+
|
20
|
+
should "create sub-classes by module name" do
|
21
|
+
assert SugarCRM.const_defined? "User"
|
22
|
+
end
|
23
|
+
|
24
|
+
should "return a single entry when sent #get_entry." do
|
25
|
+
response = @connection.get_entry(
|
26
|
+
"Users",
|
27
|
+
1,
|
28
|
+
{:fields => ["first_name", "last_name"]}
|
29
|
+
)
|
30
|
+
assert response.response.key? "entry_list"
|
31
|
+
end
|
32
|
+
|
33
|
+
should "return a list of entries when sent #get_entries." do
|
34
|
+
response = @connection.get_entries(
|
35
|
+
"Users",
|
36
|
+
[1],
|
37
|
+
{:fields => ["first_name", "last_name"]}
|
38
|
+
)
|
39
|
+
assert response.key? "entry_list"
|
40
|
+
end
|
41
|
+
|
42
|
+
should "return a list of entries when sent #get_entry_list." do
|
43
|
+
response = @connection.get_entry_list(
|
44
|
+
"Users",
|
45
|
+
"users.user_name = \'#{USER}\'",
|
46
|
+
{
|
47
|
+
:fields => ["first_name", "last_name"],
|
48
|
+
:link_fields => [
|
49
|
+
{
|
50
|
+
"name" => "accounts",
|
51
|
+
"value" => ["id", "name"]
|
52
|
+
}
|
53
|
+
]
|
54
|
+
}
|
55
|
+
)
|
56
|
+
assert response.key? "entry_list"
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require "test/unit"
|
3
|
+
require "pp"
|
4
|
+
|
5
|
+
class TestSugarcrm < Test::Unit::TestCase
|
6
|
+
context "A SugarCRM::Response instance" do
|
7
|
+
setup do
|
8
|
+
@json = {"entry_list"=> [{
|
9
|
+
"name_value_list"=> {
|
10
|
+
"address_city" => {"name"=>"address_city", "value"=>""},
|
11
|
+
"receive_notifications" => {"name"=>"receive_notifications", "value"=>"1"},
|
12
|
+
"is_group" => {"name"=>"is_group", "value"=>"0"},
|
13
|
+
"pwd_last_changed" => {"name"=>"pwd_last_changed", "value"=>"never"}
|
14
|
+
},
|
15
|
+
"id"=>"1",
|
16
|
+
"module_name"=>"Users"
|
17
|
+
}],
|
18
|
+
"relationship_list"=>[]}
|
19
|
+
|
20
|
+
@response = SugarCRM::Response.new(@json)
|
21
|
+
end
|
22
|
+
|
23
|
+
should "set the module name" do
|
24
|
+
assert_equal "User", @response.module
|
25
|
+
end
|
26
|
+
|
27
|
+
should "flatten the name_value_list into an attributes hash" do
|
28
|
+
assert_equal "never", @response.attributes[:pwd_last_changed]
|
29
|
+
end
|
30
|
+
|
31
|
+
should "return an instance of a SugarCRM Module when #object" do
|
32
|
+
assert_instance_of SugarCRM::User, @response.object
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/test/test_sugarcrm.rb
CHANGED
@@ -2,61 +2,47 @@ require 'helper'
|
|
2
2
|
require "test/unit"
|
3
3
|
require "pp"
|
4
4
|
|
5
|
-
# Replace these with your test instance
|
6
|
-
URL = "http://valet/sugarcrm"
|
7
|
-
USER = "admin"
|
8
|
-
PASS = 'letmein'
|
9
|
-
|
10
5
|
class TestSugarcrm < Test::Unit::TestCase
|
11
|
-
context "A SugarCRM::
|
12
|
-
|
13
|
-
|
6
|
+
context "A SugarCRM::Module instance" do
|
7
|
+
|
8
|
+
should "return the module name" do
|
9
|
+
assert_equal "Users", SugarCRM::User.module_name
|
14
10
|
end
|
15
11
|
|
16
|
-
should "
|
17
|
-
|
18
|
-
|
19
|
-
"Users",
|
20
|
-
1,
|
21
|
-
{:fields => ["first_name", "last_name"]}
|
22
|
-
)
|
23
|
-
assert_kind_of Hash, response
|
12
|
+
should "respond to self.connection" do
|
13
|
+
assert_respond_to SugarCRM::User, :connection
|
14
|
+
assert_instance_of SugarCRM::Connection, SugarCRM::User.connection
|
24
15
|
end
|
25
16
|
|
26
|
-
should "
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
{:fields => ["first_name", "last_name"]}
|
31
|
-
)
|
32
|
-
assert_respond_to 'response', :entry_list
|
17
|
+
should "respond to self.register_module_fields" do
|
18
|
+
assert_respond_to SugarCRM::User, :register_module_fields
|
19
|
+
SugarCRM::User.register_module_fields
|
20
|
+
assert SugarCRM::User.module_fields.length > 0
|
33
21
|
end
|
34
22
|
|
35
|
-
should "
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
assert_respond_to 'response', :entry_list
|
23
|
+
should "respond to self.connection.logged_in?" do
|
24
|
+
assert SugarCRM::User.connection.logged_in?
|
25
|
+
end
|
26
|
+
|
27
|
+
should "return an instance of itself when #new" do
|
28
|
+
assert_instance_of SugarCRM::User, SugarCRM::User.new
|
42
29
|
end
|
43
30
|
|
44
|
-
should "
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
"value" => ["id", "name"]
|
54
|
-
}
|
55
|
-
]
|
56
|
-
}
|
57
|
-
)
|
58
|
-
assert_respond_to 'response', :entry_list
|
31
|
+
should "define instance level attributes when #new" do
|
32
|
+
u = SugarCRM::User.new
|
33
|
+
assert SugarCRM::User.attribute_methods_generated
|
34
|
+
end
|
35
|
+
|
36
|
+
should "respond to attributes derived from module_fields" do
|
37
|
+
u = SugarCRM::User.new
|
38
|
+
u.last_name = "Test"
|
39
|
+
assert_equal "Test", u.last_name
|
59
40
|
end
|
60
41
|
|
42
|
+
should "return an an instance of itself when sent #find(id)" do
|
43
|
+
u = SugarCRM::User.find(1)
|
44
|
+
assert_instance_of SugarCRM::User, u
|
45
|
+
end
|
61
46
|
end
|
47
|
+
|
62
48
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sugarcrm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 7
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 6
|
9
|
+
- 0
|
10
|
+
version: 0.6.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Carl Hicks
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-08-08 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -46,7 +46,24 @@ dependencies:
|
|
46
46
|
version: "0"
|
47
47
|
type: :runtime
|
48
48
|
version_requirements: *id002
|
49
|
-
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: activesupport
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
63
|
+
description: |-
|
64
|
+
I've implemented all of the basic API calls that SugarCRM supports, and am actively building an abstraction layer
|
65
|
+
on top of the basic API methods. The end result will be to provide ActiveRecord style finders and first class
|
66
|
+
objects. Some of this functionality is included today.
|
50
67
|
email: carl.hicks@gmail.com
|
51
68
|
executables: []
|
52
69
|
|
@@ -62,18 +79,39 @@ files:
|
|
62
79
|
- README.rdoc
|
63
80
|
- Rakefile
|
64
81
|
- VERSION
|
65
|
-
- lib/net/http_digest_auth.rb
|
66
|
-
- lib/stdlib/array.rb
|
67
|
-
- lib/stdlib/hash.rb
|
68
|
-
- lib/stdlib/object.rb
|
69
82
|
- lib/sugarcrm.rb
|
70
|
-
- lib/sugarcrm/
|
71
|
-
- lib/sugarcrm/
|
72
|
-
- lib/sugarcrm/
|
73
|
-
- lib/sugarcrm/
|
74
|
-
- lib/sugarcrm/
|
83
|
+
- lib/sugarcrm/api/get_available_modules.rb
|
84
|
+
- lib/sugarcrm/api/get_document_revision.rb
|
85
|
+
- lib/sugarcrm/api/get_entries.rb
|
86
|
+
- lib/sugarcrm/api/get_entries_count.rb
|
87
|
+
- lib/sugarcrm/api/get_entry.rb
|
88
|
+
- lib/sugarcrm/api/get_entry_list.rb
|
89
|
+
- lib/sugarcrm/api/get_module_fields.rb
|
90
|
+
- lib/sugarcrm/api/get_note_attachment.rb
|
91
|
+
- lib/sugarcrm/api/get_relationship.rb
|
92
|
+
- lib/sugarcrm/api/get_report_entries.rb
|
93
|
+
- lib/sugarcrm/api/get_server_info.rb
|
94
|
+
- lib/sugarcrm/api/get_user_id.rb
|
95
|
+
- lib/sugarcrm/api/get_user_team_id.rb
|
96
|
+
- lib/sugarcrm/api/login.rb
|
97
|
+
- lib/sugarcrm/api/logout.rb
|
98
|
+
- lib/sugarcrm/api/seamless_login.rb
|
99
|
+
- lib/sugarcrm/api/search_by_module.rb
|
100
|
+
- lib/sugarcrm/api/set_campaign_merge.rb
|
101
|
+
- lib/sugarcrm/api/set_document_revision.rb
|
102
|
+
- lib/sugarcrm/api/set_entries.rb
|
103
|
+
- lib/sugarcrm/api/set_entry.rb
|
104
|
+
- lib/sugarcrm/api/set_note_attachment.rb
|
105
|
+
- lib/sugarcrm/api/set_relationship.rb
|
106
|
+
- lib/sugarcrm/api/set_relationships.rb
|
107
|
+
- lib/sugarcrm/connection.rb
|
108
|
+
- lib/sugarcrm/exceptions.rb
|
109
|
+
- lib/sugarcrm/module.rb
|
110
|
+
- lib/sugarcrm/request.rb
|
111
|
+
- lib/sugarcrm/response.rb
|
75
112
|
- test/helper.rb
|
76
|
-
- test/
|
113
|
+
- test/test_connection.rb
|
114
|
+
- test/test_response.rb
|
77
115
|
- test/test_sugarcrm.rb
|
78
116
|
has_rdoc: true
|
79
117
|
homepage: http://github.com/chicks/sugarcrm
|
@@ -111,5 +149,6 @@ specification_version: 3
|
|
111
149
|
summary: Ruby based REST client for SugarCRM
|
112
150
|
test_files:
|
113
151
|
- test/helper.rb
|
114
|
-
- test/
|
152
|
+
- test/test_connection.rb
|
153
|
+
- test/test_response.rb
|
115
154
|
- test/test_sugarcrm.rb
|
data/lib/net/http_digest_auth.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
# net_http_digest_auth.rb
|
2
|
-
require 'digest/md5'
|
3
|
-
require 'net/http'
|
4
|
-
|
5
|
-
module Net
|
6
|
-
module HTTPHeader
|
7
|
-
@@nonce_count = -1
|
8
|
-
CNONCE = Digest::MD5.new.update("%x" % (Time.now.to_i + rand(65535))).hexdigest
|
9
|
-
def digest_auth(user, password, response)
|
10
|
-
# based on http://segment7.net/projects/ruby/snippets/digest_auth.rb
|
11
|
-
@@nonce_count += 1
|
12
|
-
|
13
|
-
response['www-authenticate'] =~ /^(\w+) (.*)/
|
14
|
-
|
15
|
-
params = {}
|
16
|
-
$2.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
|
17
|
-
|
18
|
-
a_1 = "#{user}:#{params['realm']}:#{password}"
|
19
|
-
a_2 = "#{@method}:#{@path}"
|
20
|
-
request_digest = ''
|
21
|
-
request_digest << Digest::MD5.new.update(a_1).hexdigest
|
22
|
-
request_digest << ':' << params['nonce']
|
23
|
-
request_digest << ':' << ('%08x' % @@nonce_count)
|
24
|
-
request_digest << ':' << CNONCE
|
25
|
-
request_digest << ':' << params['qop']
|
26
|
-
request_digest << ':' << Digest::MD5.new.update(a_2).hexdigest
|
27
|
-
|
28
|
-
header = []
|
29
|
-
header << "Digest username=\"#{user}\""
|
30
|
-
header << "realm=\"#{params['realm']}\""
|
31
|
-
|
32
|
-
header << "qop=#{params['qop']}"
|
33
|
-
|
34
|
-
header << "algorithm=MD5"
|
35
|
-
header << "uri=\"#{@path}\""
|
36
|
-
header << "nonce=\"#{params['nonce']}\""
|
37
|
-
header << "nc=#{'%08x' % @@nonce_count}"
|
38
|
-
header << "cnonce=\"#{CNONCE}\""
|
39
|
-
header << "response=\"#{Digest::MD5.new.update(request_digest).hexdigest}\""
|
40
|
-
|
41
|
-
@header['Authorization'] = header
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
data/lib/stdlib/array.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
# A fancy way of iterating over an array and converting hashes to objects
|
2
|
-
class Array
|
3
|
-
def to_obj
|
4
|
-
# Make a deep copy of the array
|
5
|
-
a = Marshal.load(Marshal.dump(self))
|
6
|
-
a.each do |i|
|
7
|
-
case i.class.to_s
|
8
|
-
when "Hash" then i = i.to_obj
|
9
|
-
when "Array" then i = i.to_obj
|
10
|
-
end
|
11
|
-
end
|
12
|
-
a
|
13
|
-
end
|
14
|
-
end
|
data/lib/stdlib/hash.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
# A fancy way of iterating over a hash and converting hashes to objects
|
2
|
-
class Hash
|
3
|
-
def to_obj
|
4
|
-
o = Object.new
|
5
|
-
self.each do |k,v|
|
6
|
-
# If we're looking at a hash or array, we need to look through them and convert any hashes to objects as well
|
7
|
-
case v.class.to_s
|
8
|
-
when "Hash" then v = v.to_obj
|
9
|
-
when "Array" then v = v.to_obj
|
10
|
-
end
|
11
|
-
o.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair
|
12
|
-
o.class.send(:define_method, k, proc{o.instance_variable_get("@#{k}")}) ## create the getter that returns the instance variable
|
13
|
-
o.class.send(:define_method, "#{k}=", proc{|v| o.instance_variable_set("@#{k}", v)}) ## create the setter that sets the instance variable
|
14
|
-
end
|
15
|
-
o
|
16
|
-
end
|
17
|
-
end
|
data/lib/stdlib/object.rb
DELETED
data/test/test_json_to_obj.rb
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
class TestJson2Object < Test::Unit::TestCase
|
4
|
-
def test_object
|
5
|
-
json = {"string" => "value"}.to_json
|
6
|
-
obj = JSON.parse(json).to_obj
|
7
|
-
assert_equal("value", obj.string)
|
8
|
-
end
|
9
|
-
|
10
|
-
def test_nested_object
|
11
|
-
json = {"dogs" => {"retriever" => "sparky", "basset" => "jennie", "pinscher" => "carver"}}.to_json
|
12
|
-
obj = JSON.parse(json).to_obj
|
13
|
-
assert_equal("sparky", obj.dogs.retriever)
|
14
|
-
end
|
15
|
-
|
16
|
-
def test_array_of_objects
|
17
|
-
json = [{"retriever" => "sparky"}, {"basset" => "jennie"}, {"pinscher" => "carver"}].to_json
|
18
|
-
obj = JSON.parse(json).to_obj
|
19
|
-
assert_equal("sparky", obj[0].retriever)
|
20
|
-
end
|
21
|
-
|
22
|
-
def test_deep_nest_mixed
|
23
|
-
json = {"kennels" => [
|
24
|
-
{"dallas" => [
|
25
|
-
{"name" => "north"},
|
26
|
-
{"name" => "east"},
|
27
|
-
]},
|
28
|
-
{"frisco" => [
|
29
|
-
{"name" => "south"},
|
30
|
-
{"name" => "west"}
|
31
|
-
],
|
32
|
-
"company" => "Doggie Daze"
|
33
|
-
}
|
34
|
-
]}.to_json
|
35
|
-
obj = JSON.parse(json).to_obj
|
36
|
-
assert_equal("west", obj.kennels[1].frisco[0].name)
|
37
|
-
end
|
38
|
-
|
39
|
-
def test_deep_nest_hash
|
40
|
-
json = {"kennels" => {
|
41
|
-
"kennel" => {
|
42
|
-
"dallas" => ["north", "south"],
|
43
|
-
"frisco" => ["east", "west"]}}
|
44
|
-
}.to_json
|
45
|
-
obj = JSON.parse(json).to_obj
|
46
|
-
assert_equal("north", obj.kennels.kennel.dallas[0])
|
47
|
-
end
|
48
|
-
end
|