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
@@ -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