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.
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
@@ -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
- def initialize(options={})
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
@@ -1,6 +1,6 @@
1
1
  module FlexmlsApi
2
2
  module FaradayExt
3
- #=Flexmls API Faraday middle way
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
@@ -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)[0]
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)
@@ -1,6 +1,7 @@
1
1
  module FlexmlsApi
2
2
  module Models
3
3
  class Contact < Base
4
+ extend Finders
4
5
  self.element_name="contacts"
5
6
 
6
7
  def save
@@ -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, user)
8
- collect(connection.get("#{self.path}/#{card_fmt}", :ApiUser => user))
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
@@ -36,10 +36,10 @@ module FlexmlsApi
36
36
 
37
37
  def find_single(scope, options)
38
38
  resp = connection.get("/#{element_name}/#{scope}", options)
39
- new(resp[0])
39
+ new(resp.first)
40
40
  end
41
41
 
42
42
  end
43
43
  end
44
44
  end
45
-
45
+
@@ -39,7 +39,7 @@ module FlexmlsApi
39
39
 
40
40
  def self.find_single(scope, options)
41
41
  resp = FlexmlsApi.client.get("/idxlinks/#{scope}", options)
42
- new(resp[0])
42
+ new(resp.first)
43
43
  end
44
44
 
45
45
  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, owner, options={})
44
- options.merge!({ :ApiUser => owner, :_filter => "ListingCart Eq '#{cart_id}'" })
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
 
@@ -25,7 +25,7 @@ module FlexmlsApi
25
25
  private
26
26
  def self.stat(stat_name, parameters={})
27
27
  resp = connection.get("#{path}/#{stat_name}", parameters)
28
- new(resp[0])
28
+ new(resp.first)
29
29
  end
30
30
 
31
31
  end
@@ -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 find_by_listing_key(key, user)
6
- collect(connection.get("/listings/#{key}#{self.path}", :ApiUser => user))
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
@@ -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
- if @session.nil? || @session.expired?
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
- sig = sign_token(path, request_opts, post_data)
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 = connection.send(method, request_path)
73
+ response = authenticator.request(method, request_path, nil, request_opts)
80
74
  else
81
- FlexmlsApi.logger.debug("Data: #{post_data}")
82
- response = connection.send(method, request_path, post_data)
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
- results = paginate_response(results, paging)
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