flexmls_api 0.3.6 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/Gemfile +6 -6
  2. data/Gemfile.lock +6 -6
  3. data/README.md +5 -3
  4. data/Rakefile +2 -1
  5. data/VERSION +1 -1
  6. data/lib/flexmls_api/authentication.rb +25 -54
  7. data/lib/flexmls_api/authentication/api_auth.rb +100 -0
  8. data/lib/flexmls_api/authentication/base_auth.rb +47 -0
  9. data/lib/flexmls_api/authentication/oauth2.rb +219 -0
  10. data/lib/flexmls_api/client.rb +7 -1
  11. data/lib/flexmls_api/configuration.rb +5 -2
  12. data/lib/flexmls_api/faraday.rb +6 -2
  13. data/lib/flexmls_api/models.rb +2 -0
  14. data/lib/flexmls_api/models/base.rb +5 -1
  15. data/lib/flexmls_api/models/contact.rb +1 -0
  16. data/lib/flexmls_api/models/custom_fields.rb +2 -2
  17. data/lib/flexmls_api/models/finders.rb +2 -2
  18. data/lib/flexmls_api/models/idx_link.rb +1 -1
  19. data/lib/flexmls_api/models/listing.rb +31 -5
  20. data/lib/flexmls_api/models/market_statistics.rb +1 -1
  21. data/lib/flexmls_api/models/note.rb +43 -0
  22. data/lib/flexmls_api/models/standard_fields.rb +43 -0
  23. data/lib/flexmls_api/models/subresource.rb +5 -2
  24. data/lib/flexmls_api/models/system_info.rb +7 -0
  25. data/lib/flexmls_api/models/tour_of_home.rb +24 -0
  26. data/lib/flexmls_api/request.rb +13 -28
  27. data/spec/fixtures/add_note.json +11 -0
  28. data/spec/fixtures/agent_shared_note.json +11 -0
  29. data/spec/fixtures/agent_shared_note_empty.json +7 -0
  30. data/spec/fixtures/authentication_failure.json +7 -0
  31. data/spec/fixtures/count.json +10 -0
  32. data/spec/fixtures/errors/expired.json +7 -0
  33. data/spec/fixtures/generic_delete.json +1 -0
  34. data/spec/fixtures/generic_failure.json +5 -0
  35. data/spec/fixtures/oauth2_access.json +3 -0
  36. data/spec/fixtures/oauth2_error.json +3 -0
  37. data/spec/fixtures/session.json +1 -1
  38. data/spec/fixtures/standardfields.json +188 -0
  39. data/spec/fixtures/standardfields_city.json +1031 -0
  40. data/spec/fixtures/standardfields_nearby.json +53 -0
  41. data/spec/fixtures/standardfields_stateorprovince.json +36 -0
  42. data/spec/fixtures/tour_of_homes.json +23 -0
  43. data/spec/spec_helper.rb +22 -5
  44. data/spec/unit/flexmls_api/authentication/api_auth_spec.rb +159 -0
  45. data/spec/unit/flexmls_api/authentication/oauth2_spec.rb +183 -0
  46. data/spec/unit/flexmls_api/authentication_spec.rb +10 -2
  47. data/spec/unit/flexmls_api/configuration_spec.rb +2 -2
  48. data/spec/unit/flexmls_api/faraday_spec.rb +3 -7
  49. data/spec/unit/flexmls_api/models/base_spec.rb +1 -1
  50. data/spec/unit/flexmls_api/models/contact_spec.rb +8 -4
  51. data/spec/unit/flexmls_api/models/document_spec.rb +2 -5
  52. data/spec/unit/flexmls_api/models/listing_spec.rb +46 -9
  53. data/spec/unit/flexmls_api/models/note_spec.rb +90 -0
  54. data/spec/unit/flexmls_api/models/photo_spec.rb +2 -2
  55. data/spec/unit/flexmls_api/models/system_info_spec.rb +37 -3
  56. data/spec/unit/flexmls_api/models/tour_of_home_spec.rb +43 -0
  57. data/spec/unit/flexmls_api/models/video_spec.rb +2 -4
  58. data/spec/unit/flexmls_api/models/virtual_tour_spec.rb +2 -2
  59. data/spec/unit/flexmls_api/paginate_spec.rb +11 -8
  60. data/spec/unit/flexmls_api/request_spec.rb +31 -16
  61. data/spec/unit/flexmls_api/standard_fields_spec.rb +86 -0
  62. data/spec/unit/flexmls_api_spec.rb +6 -27
  63. metadata +119 -76
@@ -0,0 +1,53 @@
1
+ {
2
+ "D": {
3
+ "Success": true,
4
+ "Results": [{
5
+ "City": {
6
+ "ResourceUri": "/v1/standardfields/nearby/A/City",
7
+ "HasList": true,
8
+ "FieldList": [{
9
+ "Name": "Fargo",
10
+ "Value": "'Fargo'"
11
+ },
12
+ {
13
+ "Name": "Moorhead",
14
+ "Value": "'Moorhead'"
15
+ }],
16
+ "Searchable": true,
17
+ "Type": "Character"
18
+ },
19
+ "PostalCode": {
20
+ "ResourceUri": "/v1/standardfields/nearby/A/PostalCode",
21
+ "HasList": true,
22
+ "FieldList": [{
23
+ "Name": "56560",
24
+ "Value": "'56560'"
25
+ },
26
+ {
27
+ "Name": "58102",
28
+ "Value": "'58102'"
29
+ },
30
+ {
31
+ "Name": "58103",
32
+ "Value": "'58103'"
33
+ }],
34
+ "Searchable": true,
35
+ "Type": "Character"
36
+ },
37
+ "StateOrProvince": {
38
+ "ResourceUri": "/v1/standardfields/nearby/A/StateOrProvince",
39
+ "HasList": true,
40
+ "FieldList": [{
41
+ "Name": "MN",
42
+ "Value": "'MN'"
43
+ },
44
+ {
45
+ "Name": "ND",
46
+ "Value": "'ND'"
47
+ }],
48
+ "Searchable": true,
49
+ "Type": "Character"
50
+ }
51
+ }]
52
+ }
53
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "D": {
3
+ "Success": true,
4
+ "Results": [{
5
+ "StateOrProvince": {
6
+ "ResourceUri": "/v1/standardfields/StateOrProvince",
7
+ "FieldList": [{
8
+ "Name": "IA",
9
+ "Applies To": ["A", "B", "G", "I", "J", "K", "M"],
10
+ "Value": "IA"
11
+ },
12
+ {
13
+ "Name": "MN",
14
+ "Value": "MN"
15
+ },
16
+ {
17
+ "Name": "ND",
18
+ "Value": "ND"
19
+ },
20
+ {
21
+ "Name": "SD",
22
+ "Applies To": ["A", "B", "G", "I", "J", "K", "M"],
23
+ "Value": "SD"
24
+ },
25
+ {
26
+ "Name": "WI",
27
+ "Applies To": ["A", "B", "G", "I", "J", "K", "M"],
28
+ "Value": "WI"
29
+ }],
30
+ "HasList": true,
31
+ "Searchable": true,
32
+ "Type": "Character"
33
+ }
34
+ }]
35
+ }
36
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "D": {
3
+ "Results": [
4
+ {"ResourceUri":"/listings/20060725224713296297000000/tourofhomes/20101127153422574618000000",
5
+ "Id": "20101127153422574618000000",
6
+ "Date": "10/01/2010",
7
+ "StartTime": "09:00 AM",
8
+ "EndTime": "12:00 PM",
9
+ "Comments": "Wonderful home; must see!",
10
+ "AdditionalInfo": [{"Hosted By": "Joe Smith"}, {"Host Phone": "123-456-7890"}, {"Tour Area": "North-Central"}]
11
+ },
12
+ {"ResourceUri":"/listings/20060725224713296297000000/tourofhomes/20101127153422174618000000",
13
+ "Id": "20101127153422174618000000",
14
+ "Date": "10/08/2010",
15
+ "StartTime": "09:00 AM",
16
+ "EndTime": "12:00 PM",
17
+ "Comments": "Wonderful home; must see!",
18
+ "AdditionalInfo": [{"Hosted By": "Joe Smith"}, {"Host Phone": "123-456-7890"}, {"Tour Area": "North-Central"}]
19
+ }
20
+ ],
21
+ "Success": true
22
+ }
23
+ }
data/spec/spec_helper.rb CHANGED
@@ -36,9 +36,23 @@ def mock_session()
36
36
  FlexmlsApi::Authentication::Session.new("AuthToken" => "1234", "Expires" => (Time.now + 3600).to_s, "Roles" => "['idx']")
37
37
  end
38
38
 
39
+ def mock_oauth_session()
40
+ FlexmlsApi::Authentication::OAuthSession.new("access_token" => "1234", "expires_in" => 3600, "scope" => nil, "refresh_token"=> "1000refresh")
41
+ end
39
42
 
40
43
  class MockClient < FlexmlsApi::Client
41
- attr_accessor :connection, :session
44
+ attr_accessor :connection
45
+
46
+ def connection(ssl = false)
47
+ @connection
48
+ end
49
+ end
50
+
51
+ class MockApiAuthenticator < FlexmlsApi::Authentication::ApiAuth
52
+ # Sign a request
53
+ def sign(sig)
54
+ "SignedToken"
55
+ end
42
56
  end
43
57
 
44
58
  def mock_client(stubs)
@@ -60,19 +74,22 @@ def test_connection(stubs)
60
74
  end
61
75
  end
62
76
 
63
-
64
77
  def stub_auth_request()
65
78
  stub_request(:post, "https://api.flexmls.com/#{FlexmlsApi.version}/session").
66
79
  with(:query => {:ApiKey => "", :ApiSig => "806737984ab19be2fd08ba36030549ac"}).
67
80
  to_return(:body => fixture("session.json"))
68
81
  end
69
82
 
70
-
71
-
72
-
73
83
  def fixture(file)
74
84
  File.new(File.expand_path("../fixtures", __FILE__) + '/' + file)
75
85
  end
76
86
 
87
+ def reset_config()
88
+ FlexmlsApi.reset
89
+ FlexmlsApi.configure do |config|
90
+ config.api_user = "foobar"
91
+ end
92
+ end
93
+ reset_config
77
94
 
78
95
  include FlexmlsApi::Models
@@ -0,0 +1,159 @@
1
+ require './spec/spec_helper'
2
+
3
+ describe FlexmlsApi::Authentication::ApiAuth do
4
+ subject {FlexmlsApi::Authentication::ApiAuth.new(nil) }
5
+ describe "build_param_hash" do
6
+ it "should return a blank string when passed nil" do
7
+ subject.build_param_string(nil).should be_empty
8
+ end
9
+ it "should return a correct param string for one item" do
10
+ subject.build_param_string({:foo => "bar"}).should match("foobar")
11
+ end
12
+ it "should alphabatize the param names by key first, then by value" do
13
+ subject.build_param_string({:zoo => "zar", :ooo => "car"}).should match("ooocarzoozar")
14
+ subject.build_param_string({:Akey => "aValue", :aNotherkey => "AnotherValue"}).should
15
+ match "AkeyaValueaNotherkeyAnotherValue"
16
+ end
17
+ end
18
+
19
+ describe "authenticate" do
20
+ let(:client) { FlexmlsApi::Client.new({:api_key => "my_key", :api_secret => "my_secret"}) }
21
+ subject do
22
+ s = FlexmlsApi::Authentication::ApiAuth.new(client)
23
+ client.authenticator = s
24
+ s
25
+ end
26
+ it "should authenticate the api credentials" do
27
+ stub_request(:post, "https://api.flexmls.com/#{FlexmlsApi.version}/session").
28
+ with(:query => {:ApiKey => "my_key", :ApiSig => "c731cf2455fbc7a4ef937b2301108d7a"}).
29
+ to_return(:body => fixture("session.json"))
30
+ subject.authenticate()
31
+ end
32
+ it "should raise an error when api credentials are invalid" do
33
+ stub_request(:post, "https://api.flexmls.com/#{FlexmlsApi.version}/session").
34
+ with(:query => {:ApiKey => "my_key", :ApiSig => "c731cf2455fbc7a4ef937b2301108d7a"}).
35
+ to_return(:body => fixture("authentication_failure.json"), :status=>401)
36
+ expect {subject.authenticate()}.to raise_error(FlexmlsApi::ClientError){ |e| e.status.should == 401 }
37
+ end
38
+ end
39
+
40
+ describe "authenticated?" do
41
+ let(:session) { Object.new }
42
+ it "should return true when session is active" do
43
+ subject.session = session
44
+ session.stub(:expired?) { false }
45
+ subject.authenticated?.should eq(true)
46
+ end
47
+ it "should return false when session is expired" do
48
+ subject.session = session
49
+ session.stub(:expired?) { true }
50
+ subject.authenticated?.should eq(false)
51
+ end
52
+ it "should return false when session is uninitialized" do
53
+ subject.authenticated?.should eq(false)
54
+ end
55
+ end
56
+
57
+ describe "logout" do
58
+ let(:session) { mock_session }
59
+ let(:client) { Object.new }
60
+ subject {FlexmlsApi::Authentication::ApiAuth.new(client) }
61
+ it "should logout when there is an active session" do
62
+ logged_out = false
63
+ subject.session = session
64
+ client.stub(:delete).with("/session/1234") { logged_out = true }
65
+ subject.logout
66
+ subject.session.should eq(nil)
67
+ logged_out.should eq(true)
68
+ end
69
+ it "should skip logging out when there is no active session information" do
70
+ client.stub(:delete) { raise "Should not be called" }
71
+ subject.logout.should eq(nil)
72
+ end
73
+ end
74
+
75
+ # Since the request method is overly complex, the following tests just go through the whole stack
76
+ # with some semi realistic requests. Performing this type of test here should allow us to safely
77
+ # mock out authentication for the rest of our unit tests and still have some decent coverage.
78
+ describe "request" do
79
+ let(:client) { FlexmlsApi::Client.new({:api_key => "my_key", :api_secret => "my_secret"}) }
80
+ let(:session) { mock_session }
81
+ subject do
82
+ s = FlexmlsApi::Authentication::ApiAuth.new(client)
83
+ client.authenticator = s
84
+ s.session = session
85
+ s
86
+ end
87
+ it "should handle a get request" do
88
+ stub_auth_request
89
+ args = {
90
+ :ApiUser => "foobar",
91
+ :_limit => '10',
92
+ :_page => '1',
93
+ :_pagination => '1'
94
+ }
95
+ stub_request(:get, "#{FlexmlsApi.endpoint}/#{FlexmlsApi.version}/listings").
96
+ with(:query => {
97
+ :ApiSig => "1cb789831f8f4c6925dc708c93762a2c",
98
+ :AuthToken => "1234"}.merge(args)).
99
+ to_return(:body => fixture("listing_no_subresources.json"))
100
+ subject.session = session
101
+ subject.request(:get, "/#{FlexmlsApi.version}/listings", nil, args).status.should eq(200)
102
+ end
103
+ it "should handle a post request" do
104
+ stub_auth_request
105
+ args = {:ApiUser => "foobar"}
106
+ contact = '{"D":{"Contacts":[{"DisplayName":"Contact Four","PrimaryEmail":"contact4@fbsdata.com"}]}}'
107
+ stub_request(:post, "#{FlexmlsApi.endpoint}/#{FlexmlsApi.version}/contacts").
108
+ with(:query => {
109
+ :ApiSig => "82898ef88d22e1b31bd2e2ea6bb8efe7",
110
+ :AuthToken => "1234"}.merge(args),
111
+ :body => contact
112
+ ).
113
+ to_return(:body => '{"D": {
114
+ "Success": true,
115
+ "Results": [
116
+ {
117
+ "ResourceUri":"/v1/contacts/20101230223226074204000000"
118
+ }]}
119
+ }',
120
+ :status=>201)
121
+ subject.request(:post, "/#{FlexmlsApi.version}/contacts", contact, args).status.should eq(201)
122
+ end
123
+ end
124
+
125
+ describe "sign" do
126
+ it "should sign the auth parameters correctly" do
127
+ sign_token = "my_secretApiKeymy_key"
128
+ subject.sign(sign_token).should eq("c731cf2455fbc7a4ef937b2301108d7a")
129
+ end
130
+ end
131
+
132
+ context "when the server says the session is expired (even if we disagree)" do
133
+ it "should reset the session and reauthenticate" do
134
+ count = 0
135
+ # Make sure the auth request goes out twice.
136
+ stub_request(:post, "https://api.flexmls.com/#{FlexmlsApi.version}/session").
137
+ with(:query => {:ApiKey => "", :ApiSig => "806737984ab19be2fd08ba36030549ac"}).
138
+ to_return do |r|
139
+ count += 1
140
+ {:body => fixture("session.json")}
141
+ end
142
+ # Fail the first time, but then return the correct value after reauthentication
143
+ stub_request(:get, "#{FlexmlsApi.endpoint}/#{FlexmlsApi.version}/listings/1234").
144
+ with(:query => {
145
+ :ApiSig => "554b6e2a3efec8719b782647c19d238d",
146
+ :AuthToken => "c401736bf3d3f754f07c04e460e09573",
147
+ :ApiUser => "foobar",
148
+ :_expand => "Documents"
149
+ }).
150
+ to_return(:body => fixture('errors/expired.json'), :status => 401).times(1).then.
151
+ to_return(:body => fixture('listing_with_documents.json'))
152
+ l = Listing.find('1234', :_expand => "Documents")
153
+
154
+ count.should eq(2)
155
+ FlexmlsApi.client.session.expired?.should eq(false)
156
+ end
157
+ end
158
+
159
+ end
@@ -0,0 +1,183 @@
1
+ require './spec/spec_helper'
2
+
3
+ # Lightweight example of an oauth2 provider used by the ruby client.
4
+ class TestOAuth2Provider < FlexmlsApi::Authentication::BaseOAuth2Provider
5
+
6
+ def initialize
7
+ @authorization_uri = "https://test.fbsdata.com/r/oauth2"
8
+ @access_uri = "https://api.test.fbsdata.com/v1/oauth2/grant"
9
+ @redirect_uri = "https://exampleapp.fbsdata.com/oauth-callback"
10
+ @client_id="example-id"
11
+ @client_secret="example-password"
12
+ @session_cache = {}
13
+ end
14
+
15
+ def redirect(url)
16
+ # User redirected to url, signs in, and gets code sent to callback
17
+ self.code="my_code"
18
+ end
19
+
20
+ def load_session()
21
+ @session_cache["test_user_session"]
22
+ end
23
+
24
+ def save_session(session)
25
+ @session_cache["test_user_session"] = session
26
+ nil
27
+ end
28
+
29
+ def session_timeout; 7200; end
30
+
31
+ end
32
+
33
+ describe FlexmlsApi::Authentication::OAuth2 do
34
+ let(:provider) { TestOAuth2Provider.new() }
35
+ let(:client) { FlexmlsApi::Client.new({:authentication_mode => FlexmlsApi::Authentication::OAuth2,:oauth2_provider => provider}) }
36
+ subject {client.authenticator }
37
+ # Make sure the client boostraps the right plugin based on configuration.
38
+ describe "plugin" do
39
+ it "should load the oauth2 authenticator" do
40
+ client.authenticator.class.should eq(FlexmlsApi::Authentication::OAuth2)
41
+ end
42
+ end
43
+ describe "authenticate" do
44
+ it "should authenticate the api credentials" do
45
+ stub_request(:post, provider.access_uri).
46
+ with(:query => {
47
+ :client_id => provider.client_id,
48
+ :client_secret => provider.client_secret,
49
+ :grant_type => "authorization_code",
50
+ :redirect_uri => provider.redirect_uri,
51
+ :code => "my_code"
52
+ }
53
+ ).
54
+ to_return(:body => fixture("oauth2_access.json"), :status=>200)
55
+ subject.authenticate.access_token.should eq("04u7h-4cc355-70k3n")
56
+ subject.authenticate.expires_in.should eq(7200)
57
+ end
58
+
59
+ it "should raise an error when api credentials are invalid" do
60
+ stub_request(:post, provider.access_uri).
61
+ with(:query => {
62
+ :client_id => provider.client_id,
63
+ :client_secret => provider.client_secret,
64
+ :grant_type => "authorization_code",
65
+ :redirect_uri => provider.redirect_uri,
66
+ :code => "my_code"
67
+ }
68
+ ).
69
+ to_return(:body => fixture("oauth2_error.json"), :status=>400)
70
+ expect {subject.authenticate()}.to raise_error(FlexmlsApi::ClientError){ |e| e.status.should == 400 }
71
+ end
72
+
73
+ end
74
+
75
+ describe "authenticated?" do
76
+ let(:session) { Object.new }
77
+ it "should return true when session is active" do
78
+ subject.session = session
79
+ session.stub(:expired?) { false }
80
+ subject.authenticated?.should eq(true)
81
+ end
82
+ it "should return false when session is expired" do
83
+ subject.session = session
84
+ session.stub(:expired?) { true }
85
+ subject.authenticated?.should eq(false)
86
+ end
87
+ it "should return false when session is uninitialized" do
88
+ subject.authenticated?.should eq(false)
89
+ end
90
+ end
91
+
92
+ describe "logout" do
93
+ let(:session) { mock_oauth_session }
94
+ it "should logout when there is an active session" do
95
+ subject.session = session
96
+ subject.logout
97
+ subject.session.should eq(nil)
98
+ end
99
+ it "should skip logging out when there is no active session information" do
100
+ client.stub(:delete) { raise "Should not be called" }
101
+ subject.logout.should eq(nil)
102
+ end
103
+ end
104
+
105
+ describe "request" do
106
+ let(:session) { mock_oauth_session }
107
+ it "should handle a get request" do
108
+ subject.session = session
109
+ args = {
110
+ :_limit => '10',
111
+ :_page => '1',
112
+ :_pagination => '1'
113
+ }
114
+ c = stub_request(:get, "https://api.flexmls.com/#{FlexmlsApi.version}/listings").
115
+ with(:query => {:access_token => "1234"}.merge(args)).
116
+ to_return(:body => fixture("listing_no_subresources.json"))
117
+ subject.session = session
118
+ subject.request(:get, "/#{FlexmlsApi.version}/listings", nil, args).status.should eq(200)
119
+ end
120
+ it "should handle a post request" do
121
+ subject.session = session
122
+ args = {}
123
+ contact = '{"D":{"Contacts":[{"DisplayName":"Contact Four","PrimaryEmail":"contact4@fbsdata.com"}]}}'
124
+ stub_request(:post, "https://api.flexmls.com/#{FlexmlsApi.version}/contacts").
125
+ with(:query => {:access_token => "1234"}.merge(args),
126
+ :body => contact
127
+ ).
128
+ to_return(:body => '{"D": {
129
+ "Success": true,
130
+ "Results": [
131
+ {
132
+ "ResourceUri":"/v1/contacts/20101230223226074204000000"
133
+ }]}
134
+ }',
135
+ :status=>201)
136
+ subject.request(:post, "/#{FlexmlsApi.version}/contacts", contact, args).status.should eq(201)
137
+ end
138
+ end
139
+
140
+ context "when the server says the session is expired (even if we disagree)" do
141
+ it "should reset the session and reauthenticate" do
142
+ count = 0
143
+ stub_request(:post, provider.access_uri).
144
+ with(:query => {
145
+ :client_id => provider.client_id,
146
+ :client_secret => provider.client_secret,
147
+ :grant_type => "authorization_code",
148
+ :redirect_uri => provider.redirect_uri,
149
+ :code => "my_code"
150
+ }
151
+ ).
152
+ to_return do
153
+ count += 1
154
+ {:body => fixture("oauth2_access.json"), :status=>200}
155
+ end
156
+ # Make sure the auth request goes out twice.
157
+ # Fail the first time, but then return the correct value after reauthentication
158
+ stub_request(:get, "https://api.flexmls.com/#{FlexmlsApi.version}/listings/1234").
159
+ with(:query => {:access_token => "04u7h-4cc355-70k3n"}).
160
+ to_return(:body => fixture('errors/expired.json'), :status => 401).times(1).then.
161
+ to_return(:body => fixture('listing_with_documents.json'))
162
+
163
+ client.get("/listings/1234")
164
+ count.should eq(2)
165
+ client.session.expired?.should eq(false)
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ describe FlexmlsApi::Authentication::BaseOAuth2Provider do
172
+ context "session_timeout" do
173
+ it "should provide a default" do
174
+ subject.session_timeout.should eq(3600)
175
+ end
176
+ describe TestOAuth2Provider do
177
+ subject { TestOAuth2Provider.new }
178
+ it "should be able to override the session timeout" do
179
+ subject.session_timeout.should eq(7200)
180
+ end
181
+ end
182
+ end
183
+ end