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.
- data/Gemfile +6 -6
- data/Gemfile.lock +6 -6
- data/README.md +5 -3
- data/Rakefile +2 -1
- data/VERSION +1 -1
- data/lib/flexmls_api/authentication.rb +25 -54
- data/lib/flexmls_api/authentication/api_auth.rb +100 -0
- data/lib/flexmls_api/authentication/base_auth.rb +47 -0
- data/lib/flexmls_api/authentication/oauth2.rb +219 -0
- data/lib/flexmls_api/client.rb +7 -1
- data/lib/flexmls_api/configuration.rb +5 -2
- data/lib/flexmls_api/faraday.rb +6 -2
- data/lib/flexmls_api/models.rb +2 -0
- data/lib/flexmls_api/models/base.rb +5 -1
- data/lib/flexmls_api/models/contact.rb +1 -0
- data/lib/flexmls_api/models/custom_fields.rb +2 -2
- data/lib/flexmls_api/models/finders.rb +2 -2
- data/lib/flexmls_api/models/idx_link.rb +1 -1
- data/lib/flexmls_api/models/listing.rb +31 -5
- data/lib/flexmls_api/models/market_statistics.rb +1 -1
- data/lib/flexmls_api/models/note.rb +43 -0
- data/lib/flexmls_api/models/standard_fields.rb +43 -0
- data/lib/flexmls_api/models/subresource.rb +5 -2
- data/lib/flexmls_api/models/system_info.rb +7 -0
- data/lib/flexmls_api/models/tour_of_home.rb +24 -0
- data/lib/flexmls_api/request.rb +13 -28
- data/spec/fixtures/add_note.json +11 -0
- data/spec/fixtures/agent_shared_note.json +11 -0
- data/spec/fixtures/agent_shared_note_empty.json +7 -0
- data/spec/fixtures/authentication_failure.json +7 -0
- data/spec/fixtures/count.json +10 -0
- data/spec/fixtures/errors/expired.json +7 -0
- data/spec/fixtures/generic_delete.json +1 -0
- data/spec/fixtures/generic_failure.json +5 -0
- data/spec/fixtures/oauth2_access.json +3 -0
- data/spec/fixtures/oauth2_error.json +3 -0
- data/spec/fixtures/session.json +1 -1
- data/spec/fixtures/standardfields.json +188 -0
- data/spec/fixtures/standardfields_city.json +1031 -0
- data/spec/fixtures/standardfields_nearby.json +53 -0
- data/spec/fixtures/standardfields_stateorprovince.json +36 -0
- data/spec/fixtures/tour_of_homes.json +23 -0
- data/spec/spec_helper.rb +22 -5
- data/spec/unit/flexmls_api/authentication/api_auth_spec.rb +159 -0
- data/spec/unit/flexmls_api/authentication/oauth2_spec.rb +183 -0
- data/spec/unit/flexmls_api/authentication_spec.rb +10 -2
- data/spec/unit/flexmls_api/configuration_spec.rb +2 -2
- data/spec/unit/flexmls_api/faraday_spec.rb +3 -7
- data/spec/unit/flexmls_api/models/base_spec.rb +1 -1
- data/spec/unit/flexmls_api/models/contact_spec.rb +8 -4
- data/spec/unit/flexmls_api/models/document_spec.rb +2 -5
- data/spec/unit/flexmls_api/models/listing_spec.rb +46 -9
- data/spec/unit/flexmls_api/models/note_spec.rb +90 -0
- data/spec/unit/flexmls_api/models/photo_spec.rb +2 -2
- data/spec/unit/flexmls_api/models/system_info_spec.rb +37 -3
- data/spec/unit/flexmls_api/models/tour_of_home_spec.rb +43 -0
- data/spec/unit/flexmls_api/models/video_spec.rb +2 -4
- data/spec/unit/flexmls_api/models/virtual_tour_spec.rb +2 -2
- data/spec/unit/flexmls_api/paginate_spec.rb +11 -8
- data/spec/unit/flexmls_api/request_spec.rb +31 -16
- data/spec/unit/flexmls_api/standard_fields_spec.rb +86 -0
- data/spec/unit/flexmls_api_spec.rb +6 -27
- metadata +119 -76
data/lib/flexmls_api/client.rb
CHANGED
@@ -7,13 +7,19 @@ module FlexmlsApi
|
|
7
7
|
include Authentication
|
8
8
|
include Request
|
9
9
|
|
10
|
+
attr_accessor :authenticator
|
10
11
|
attr_accessor *Configuration::VALID_OPTION_KEYS
|
11
12
|
|
12
|
-
|
13
|
+
# Constructor bootstraps the client with configuration and authorization class.
|
14
|
+
# options - see Configuration::VALID_OPTION_KEYS
|
15
|
+
# auth_klass - subclass of Authentication::BaseAuth Defaults to the original api auth system.
|
16
|
+
def initialize(options={}, auth_klass=ApiAuth)
|
13
17
|
options = FlexmlsApi.options.merge(options)
|
14
18
|
Configuration::VALID_OPTION_KEYS.each do |key|
|
15
19
|
send("#{key}=", options[key])
|
16
20
|
end
|
21
|
+
# Instanciate the authenication class passed in.
|
22
|
+
@authenticator = authentication_mode.send("new", self)
|
17
23
|
end
|
18
24
|
|
19
25
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
module FlexmlsApi
|
2
2
|
module Configuration
|
3
3
|
# valid configuration options
|
4
|
-
VALID_OPTION_KEYS = [:api_key, :api_secret, :api_user, :endpoint, :user_agent, :version, :ssl].freeze
|
5
|
-
|
4
|
+
VALID_OPTION_KEYS = [:api_key, :api_secret, :api_user, :endpoint, :user_agent, :version, :ssl, :oauth2_provider, :authentication_mode].freeze
|
5
|
+
|
6
6
|
DEFAULT_API_KEY = nil
|
7
7
|
DEFAULT_API_SECRET = nil
|
8
8
|
DEFAULT_API_USER = nil
|
@@ -10,6 +10,7 @@ module FlexmlsApi
|
|
10
10
|
DEFAULT_VERSION = 'v1'
|
11
11
|
DEFAULT_USER_AGENT = "flexmls API Ruby Gem #{VERSION}"
|
12
12
|
DEFAULT_SSL = false
|
13
|
+
DEFAULT_OAUTH2 = nil
|
13
14
|
|
14
15
|
attr_accessor *VALID_OPTION_KEYS
|
15
16
|
def configure
|
@@ -36,6 +37,8 @@ module FlexmlsApi
|
|
36
37
|
self.version = DEFAULT_VERSION
|
37
38
|
self.user_agent = DEFAULT_USER_AGENT
|
38
39
|
self.ssl = DEFAULT_SSL
|
40
|
+
self.oauth2_provider = DEFAULT_OAUTH2
|
41
|
+
self.authentication_mode = FlexmlsApi::Authentication::ApiAuth
|
39
42
|
self
|
40
43
|
end
|
41
44
|
end
|
data/lib/flexmls_api/faraday.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module FlexmlsApi
|
2
2
|
module FaradayExt
|
3
|
-
#=Flexmls API Faraday
|
3
|
+
#=Flexmls API Faraday middleware
|
4
4
|
# HTTP Response after filter to package api responses and bubble up basic api errors.
|
5
5
|
class FlexmlsMiddleware < Faraday::Response::Middleware
|
6
6
|
begin
|
@@ -17,7 +17,7 @@ module FlexmlsApi
|
|
17
17
|
# indicate a failure will raise a FlexmlsApi::ClientError exception
|
18
18
|
def self.validate_and_build_response(finished_env)
|
19
19
|
body = finished_env[:body]
|
20
|
-
FlexmlsApi.logger.debug("Response Body: #{body}")
|
20
|
+
FlexmlsApi.logger.debug("Response Body: #{body.inspect}")
|
21
21
|
unless body.is_a?(Hash) && body.key?("D")
|
22
22
|
raise InvalidResponse, "The server response could not be understood"
|
23
23
|
end
|
@@ -26,6 +26,10 @@ module FlexmlsApi
|
|
26
26
|
when 400, 409
|
27
27
|
raise BadResourceRequest.new(response.code, finished_env[:status]), response.message
|
28
28
|
when 401
|
29
|
+
# Handle the WWW-Authenticate Response Header Field if present. This can be returned by
|
30
|
+
# OAuth2 implementations and wouldn't hurt to log.
|
31
|
+
auth_header_error = finished_env[:request_headers]["WWW-Authenticate"]
|
32
|
+
FlexmlsApi.logger.warn("Authentication error #{auth_header_error}") unless auth_header_error.nil?
|
29
33
|
raise PermissionDenied.new(response.code, finished_env[:status]), response.message
|
30
34
|
when 404
|
31
35
|
raise NotFound.new(response.code, finished_env[:status]), response.message
|
data/lib/flexmls_api/models.rb
CHANGED
@@ -13,8 +13,10 @@ require File.expand_path('../models/contact', __FILE__)
|
|
13
13
|
require File.expand_path('../models/idx_link', __FILE__)
|
14
14
|
require File.expand_path('../models/market_statistics', __FILE__)
|
15
15
|
require File.expand_path('../models/video', __FILE__)
|
16
|
+
require File.expand_path('../models/tour_of_home', __FILE__)
|
16
17
|
require File.expand_path('../models/virtual_tour', __FILE__)
|
17
18
|
require File.expand_path('../models/document', __FILE__)
|
19
|
+
require File.expand_path('../models/note', __FILE__)
|
18
20
|
|
19
21
|
module FlexmlsApi
|
20
22
|
module Models
|
@@ -52,7 +52,11 @@ module FlexmlsApi
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def self.first(options={})
|
55
|
-
get(options)
|
55
|
+
get(options).first
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.count(options={})
|
59
|
+
connection.get(path, options.merge({:_pagination=>"count"}))
|
56
60
|
end
|
57
61
|
|
58
62
|
def method_missing(method_symbol, *arguments)
|
@@ -4,8 +4,8 @@ module FlexmlsApi
|
|
4
4
|
self.element_name="customfields"
|
5
5
|
|
6
6
|
|
7
|
-
def self.find_by_property_type(card_fmt,
|
8
|
-
collect(connection.get("#{self.path}/#{card_fmt}",
|
7
|
+
def self.find_by_property_type(card_fmt, arguments={})
|
8
|
+
collect(connection.get("#{self.path}/#{card_fmt}", arguments))
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
@@ -11,7 +11,6 @@ module FlexmlsApi
|
|
11
11
|
@virtual_tours = []
|
12
12
|
@documents = []
|
13
13
|
|
14
|
-
|
15
14
|
if attributes.has_key?('StandardFields')
|
16
15
|
pics, vids, tours, docs = attributes['StandardFields'].values_at('Photos','Videos', 'VirtualTours', 'Documents')
|
17
16
|
end
|
@@ -36,13 +35,12 @@ module FlexmlsApi
|
|
36
35
|
attributes['StandardFields'].delete('Documents')
|
37
36
|
end
|
38
37
|
|
39
|
-
|
40
38
|
super(attributes)
|
41
39
|
end
|
42
40
|
|
43
|
-
def self.find_by_cart_id(cart_id,
|
44
|
-
|
45
|
-
find(:all, options)
|
41
|
+
def self.find_by_cart_id(cart_id, options={})
|
42
|
+
query = {:_filter => "ListingCart Eq '#{cart_id}'"}
|
43
|
+
find(:all, options.merge(query))
|
46
44
|
end
|
47
45
|
|
48
46
|
def self.my(arguments={})
|
@@ -56,6 +54,34 @@ module FlexmlsApi
|
|
56
54
|
def self.company(arguments={})
|
57
55
|
collect(connection.get("/company/listings", arguments))
|
58
56
|
end
|
57
|
+
|
58
|
+
def tour_of_homes(arguments={})
|
59
|
+
return @tour_of_homes unless @tour_of_homes.nil?
|
60
|
+
@tour_of_homes = TourOfHome.find_by_listing_key(self.Id, arguments)
|
61
|
+
end
|
62
|
+
|
63
|
+
def my_notes
|
64
|
+
Note.build_subclass.tap do |note|
|
65
|
+
note.prefix = "/listings/#{self.ListingKey}"
|
66
|
+
note.element_name = "/my/notes"
|
67
|
+
FlexmlsApi.logger.info("Note.path: #{note.path}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# 'fore' is required when accessing an agent's shared
|
72
|
+
# notes for a specific contact. If the ApiUser /is/ the
|
73
|
+
# contact, then it can be inferred by the api, so it's
|
74
|
+
# unecessary
|
75
|
+
def shared_notes(fore=nil)
|
76
|
+
Note.build_subclass.tap do |note|
|
77
|
+
note.prefix = "/listings/#{self.ListingKey}"
|
78
|
+
if fore.nil?
|
79
|
+
note.element_name = "/shared/notes"
|
80
|
+
else
|
81
|
+
note.element_name = "/shared/notes/contacts/#{fore}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
59
85
|
|
60
86
|
private
|
61
87
|
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module FlexmlsApi
|
2
|
+
module Models
|
3
|
+
class Note < Base
|
4
|
+
extend Subresource
|
5
|
+
self.element_name = "notes" # not sure this is really of any use...
|
6
|
+
|
7
|
+
def self.get(options={})
|
8
|
+
ret = super(options)
|
9
|
+
if ret.empty?
|
10
|
+
return nil
|
11
|
+
else
|
12
|
+
return ret.first
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def save(arguments={})
|
18
|
+
begin
|
19
|
+
return save!(arguments)
|
20
|
+
rescue BadResourceRequest => e
|
21
|
+
rescue NotFound => e
|
22
|
+
# log and leave
|
23
|
+
FlexmlsApi.logger.error("Failed to save note #{self} (path: #{self.class.path}): #{e.message}")
|
24
|
+
end
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def save!(args={})
|
29
|
+
args.merge(:Notes => attributes['Note'])
|
30
|
+
results = connection.put(self.class.path, {:Note => attributes['Note']}, args)
|
31
|
+
result = results.first
|
32
|
+
attributes['ResourceUri'] = result['ResourceUri']
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete(args={})
|
37
|
+
connection.delete(self.class.path, args)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -1,7 +1,50 @@
|
|
1
1
|
module FlexmlsApi
|
2
2
|
module Models
|
3
3
|
class StandardFields < Base
|
4
|
+
extend Finders
|
4
5
|
self.element_name="standardfields"
|
6
|
+
|
7
|
+
# expand all fields passed in
|
8
|
+
def self.find_and_expand_all(fields, arguments={}, max_list_size=1000)
|
9
|
+
returns = {}
|
10
|
+
|
11
|
+
# find all standard fields, but expand only the location fields
|
12
|
+
# TODO: when _expand support is added to StandardFields API, use the following
|
13
|
+
# standard_fields = find(:all, {:ApiUser => owner, :_expand => fields.join(",")})
|
14
|
+
standard_fields = find(:all, arguments)
|
15
|
+
|
16
|
+
# filter through the list and return only the location fields found
|
17
|
+
fields.each do |field|
|
18
|
+
# search for field in the payload
|
19
|
+
if standard_fields.first.attributes.has_key?(field)
|
20
|
+
returns[field] = standard_fields.first.attributes[field]
|
21
|
+
|
22
|
+
# lookup fully _expand field, if the field has a list
|
23
|
+
if returns[field]['HasList'] && returns[field]['MaxListSize'].to_i <= max_list_size
|
24
|
+
returns[field] = connection.get("/standardfields/#{field}", arguments).first[field]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
returns
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# find_nearby: find fields nearby via lat/lon
|
35
|
+
def self.find_nearby(prop_types = ["A"], arguments={})
|
36
|
+
return_json = {"D" => {"Success" => true, "Results" => []} }
|
37
|
+
|
38
|
+
# add _expand=1 so the fields are returned
|
39
|
+
arguments.merge!({:_expand => 1})
|
40
|
+
|
41
|
+
# find and return
|
42
|
+
return_json["D"]["Results"] = connection.get("/standardfields/nearby/#{prop_types.join(',')}", arguments)
|
43
|
+
|
44
|
+
# return
|
45
|
+
return_json
|
46
|
+
end
|
47
|
+
|
5
48
|
end
|
6
49
|
end
|
7
50
|
end
|
@@ -2,11 +2,14 @@ module FlexmlsApi
|
|
2
2
|
module Models
|
3
3
|
module Subresource
|
4
4
|
|
5
|
-
def
|
6
|
-
|
5
|
+
def build_subclass
|
6
|
+
Class.new(self)
|
7
7
|
end
|
8
8
|
|
9
9
|
|
10
|
+
def find_by_listing_key(key, arguments={})
|
11
|
+
collect(connection.get("/listings/#{key}#{self.path}", arguments))
|
12
|
+
end
|
10
13
|
|
11
14
|
end
|
12
15
|
end
|
@@ -2,6 +2,13 @@ module FlexmlsApi
|
|
2
2
|
module Models
|
3
3
|
class SystemInfo < Base
|
4
4
|
self.element_name="system"
|
5
|
+
|
6
|
+
def primary_logo
|
7
|
+
logo = nil
|
8
|
+
mls_logos = attributes['Configuration'].first['MlsLogos']
|
9
|
+
logo = mls_logos.first if !mls_logos.nil? and !mls_logos.empty?
|
10
|
+
logo
|
11
|
+
end
|
5
12
|
end
|
6
13
|
end
|
7
14
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module FlexmlsApi
|
5
|
+
module Models
|
6
|
+
class TourOfHome < Base
|
7
|
+
extend Subresource
|
8
|
+
|
9
|
+
self.element_name = "tourofhomes"
|
10
|
+
|
11
|
+
def initialize(attributes={})
|
12
|
+
# Transform the date strings
|
13
|
+
unless attributes['Date'].nil?
|
14
|
+
date = Date.parse(attributes['Date'])
|
15
|
+
attributes['Date'] = date
|
16
|
+
attributes['StartTime'] = Time.parse("#{date}T#{attributes['StartTime']}") unless attributes['StartTime'].nil?
|
17
|
+
attributes['EndTime'] = Time.parse("#{date}T#{attributes['EndTime']}") unless attributes['EndTime'].nil?
|
18
|
+
end
|
19
|
+
super(attributes)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/flexmls_api/request.rb
CHANGED
@@ -58,28 +58,22 @@ module FlexmlsApi
|
|
58
58
|
|
59
59
|
# Perform an HTTP request (no data)
|
60
60
|
def request(method, path, body, options)
|
61
|
-
|
61
|
+
unless authenticated?
|
62
62
|
authenticate
|
63
63
|
end
|
64
64
|
attempts = 0
|
65
65
|
begin
|
66
|
-
request_opts = {
|
67
|
-
"AuthToken" => @session.auth_token
|
68
|
-
}
|
69
|
-
unless self.api_user.nil?
|
70
|
-
request_opts.merge!(:ApiUser => "#{api_user}")
|
71
|
-
end
|
66
|
+
request_opts = {}
|
72
67
|
request_opts.merge!(options)
|
73
68
|
post_data = body.nil? ? nil : {"D" => body }.to_json
|
74
|
-
|
75
|
-
request_path = "/#{version}#{path}?ApiSig=#{sig}#{build_url_parameters(request_opts)}"
|
76
|
-
FlexmlsApi.logger.debug("Request: #{request_path}")
|
69
|
+
request_path = "/#{version}#{path}"
|
77
70
|
start_time = Time.now
|
71
|
+
FlexmlsApi.logger.debug("#{method.to_s.upcase} Request: #{request_path}")
|
78
72
|
if post_data.nil?
|
79
|
-
response =
|
73
|
+
response = authenticator.request(method, request_path, nil, request_opts)
|
80
74
|
else
|
81
|
-
FlexmlsApi.logger.debug("Data:
|
82
|
-
response =
|
75
|
+
FlexmlsApi.logger.debug("#{method.to_s.upcase} Data: #{post_data}")
|
76
|
+
response = authenticator.request(method, request_path, post_data, request_opts)
|
83
77
|
end
|
84
78
|
request_time = Time.now - start_time
|
85
79
|
FlexmlsApi.logger.info("[#{(request_time * 1000).to_i}ms] Api: #{method.to_s.upcase} #{request_path}")
|
@@ -98,25 +92,17 @@ module FlexmlsApi
|
|
98
92
|
results = response.body.results
|
99
93
|
paging = response.body.pagination
|
100
94
|
unless paging.nil?
|
101
|
-
|
95
|
+
if request_opts[:_pagination] == "count"
|
96
|
+
results = paging['TotalRows']
|
97
|
+
else
|
98
|
+
results = paginate_response(results, paging)
|
99
|
+
end
|
102
100
|
end
|
103
101
|
results
|
104
102
|
end
|
105
103
|
|
106
|
-
# Format a hash as request parameters
|
107
|
-
#
|
108
|
-
# :returns:
|
109
|
-
# Stringized form of the parameters as needed for the http request
|
110
|
-
def build_url_parameters(parameters={})
|
111
|
-
str = ""
|
112
|
-
parameters.map do |key,value|
|
113
|
-
escaped_value = CGI.escape("#{value}")
|
114
|
-
str << "&#{key}=#{escaped_value}"
|
115
|
-
end
|
116
|
-
str
|
117
|
-
end
|
118
104
|
end
|
119
|
-
|
105
|
+
|
120
106
|
# All known response codes listed in the API
|
121
107
|
module ResponseCodes
|
122
108
|
NOT_FOUND = 404
|
@@ -149,7 +135,6 @@ module FlexmlsApi
|
|
149
135
|
class NotAllowed < ClientError; end
|
150
136
|
class BadResourceRequest < ClientError; end
|
151
137
|
|
152
|
-
|
153
138
|
# Nice and handy class wrapper for the api response hash
|
154
139
|
class ApiResponse
|
155
140
|
attr_accessor :code, :message, :results, :success, :pagination
|