flexmls_api 0.3.6 → 0.4.5
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 +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
|