mls 0.2.2 → 0.2.3

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 (57) hide show
  1. data/.gitignore +1 -0
  2. data/README.rdoc +54 -0
  3. data/Rakefile +20 -2
  4. data/lib/mls.rb +307 -78
  5. data/lib/mls/errors.rb +13 -4
  6. data/lib/mls/model.rb +36 -5
  7. data/lib/mls/models/account.rb +74 -66
  8. data/lib/mls/models/address.rb +87 -6
  9. data/lib/mls/models/area.rb +27 -0
  10. data/lib/mls/models/flyer.rb +41 -0
  11. data/lib/mls/models/listing.rb +180 -34
  12. data/lib/mls/models/photo.rb +25 -3
  13. data/lib/mls/models/tour_request.rb +18 -31
  14. data/lib/mls/parser.rb +5 -4
  15. data/lib/mls/properties/datetime.rb +5 -1
  16. data/lib/mls/properties/decimal.rb +3 -1
  17. data/lib/mls/properties/hash.rb +7 -0
  18. data/lib/mls/resource.rb +35 -5
  19. data/lib/rdoc/generator/template/42floors/_context.rhtml +209 -0
  20. data/lib/rdoc/generator/template/42floors/_head.rhtml +7 -0
  21. data/lib/rdoc/generator/template/42floors/class.rhtml +39 -0
  22. data/lib/rdoc/generator/template/42floors/file.rhtml +35 -0
  23. data/lib/rdoc/generator/template/42floors/index.rhtml +13 -0
  24. data/lib/rdoc/generator/template/42floors/resources/apple-touch-icon.png +0 -0
  25. data/lib/rdoc/generator/template/42floors/resources/css/github.css +129 -0
  26. data/lib/rdoc/generator/template/42floors/resources/css/main.css +339 -0
  27. data/lib/rdoc/generator/template/42floors/resources/css/panel.css +389 -0
  28. data/lib/rdoc/generator/template/42floors/resources/css/reset.css +48 -0
  29. data/lib/rdoc/generator/template/42floors/resources/favicon.ico +0 -0
  30. data/lib/rdoc/generator/template/42floors/resources/i/arrows.png +0 -0
  31. data/lib/rdoc/generator/template/42floors/resources/i/results_bg.png +0 -0
  32. data/lib/rdoc/generator/template/42floors/resources/i/tree_bg.png +0 -0
  33. data/lib/rdoc/generator/template/42floors/resources/js/highlight.pack.js +1 -0
  34. data/lib/rdoc/generator/template/42floors/resources/js/jquery-1.3.2.min.js +19 -0
  35. data/lib/rdoc/generator/template/42floors/resources/js/jquery-effect.js +593 -0
  36. data/lib/rdoc/generator/template/42floors/resources/js/main.js +20 -0
  37. data/lib/rdoc/generator/template/42floors/resources/js/searchdoc.js +442 -0
  38. data/lib/rdoc/generator/template/42floors/resources/panel/index.html +73 -0
  39. data/lib/rdoc/generator/template/42floors/se_index.rhtml +8 -0
  40. data/mls.gemspec +7 -4
  41. data/test/factories/account.rb +18 -0
  42. data/test/factories/address.rb +15 -0
  43. data/test/factories/listing.rb +30 -0
  44. data/test/factories/tour_request.rb +9 -0
  45. data/test/fixtures/flyer.pdf +68 -0
  46. data/test/test_helper.rb +44 -5
  47. data/test/units/models/test_account.rb +20 -43
  48. data/test/units/models/test_flyer.rb +22 -0
  49. data/test/units/models/test_listing.rb +119 -0
  50. data/test/units/models/test_photo.rb +136 -3
  51. data/test/units/models/test_tour_request.rb +25 -20
  52. data/test/units/test_errors.rb +12 -4
  53. data/test/units/test_mls.rb +263 -3
  54. data/test/units/test_resource.rb +1 -0
  55. metadata +78 -57
  56. data/lib/mls/models/user.rb +0 -7
  57. data/lib/mls/version.rb +0 -3
data/lib/mls/errors.rb CHANGED
@@ -1,14 +1,23 @@
1
1
  class MLS::Exception < Exception
2
2
  end
3
3
 
4
- class MLS::BadRequest < MLS::Exception
4
+ class MLS::Exception::UnexpectedResponse < MLS::Exception
5
5
  end
6
6
 
7
- class MLS::Unauthorized < MLS::Exception
7
+ class MLS::Exception::BadRequest < MLS::Exception
8
8
  end
9
9
 
10
- class MLS::NotFound < MLS::Exception
10
+ class MLS::Exception::Unauthorized < MLS::Exception
11
11
  end
12
12
 
13
- class MLS::ApiVersionUnsupported < MLS::Exception
13
+ class MLS::Exception::NotFound < MLS::Exception
14
+ end
15
+
16
+ class MLS::Exception::ApiVersionUnsupported < MLS::Exception
17
+ end
18
+
19
+ class MLS::Exception::RecordInvalid < MLS::Exception
20
+ end
21
+
22
+ class MLS::Exception::ServiceUnavailable < MLS::Exception
14
23
  end
data/lib/mls/model.rb CHANGED
@@ -1,23 +1,51 @@
1
1
  module MLS::Model
2
2
 
3
- def self.extended(model)
3
+ def self.extended(model) #:nodoc:
4
4
  model.instance_variable_set(:@properties, {})
5
5
  model.instance_variable_set(:@associations, {})
6
6
  end
7
7
 
8
+ # Creates an object and saves it to the MLS. The resulting object is returned
9
+ # whether or no the object was saved successfully to the MLS or not.
10
+ #
11
+ # ==== Examples
12
+ # #!ruby
13
+ # # Create a single new object
14
+ # User.create(:first_name => 'Jamie')
15
+ #
16
+ # # Create a single object and pass it into a block to set other attributes.
17
+ # User.create(:first_name => 'Jamie') do |u|
18
+ # u.is_admin = false
19
+ # end
20
+ def create(attributes={}, &block) # TODO: testme
21
+ model = self.new(attributes)
22
+ yield(model) if block_given?
23
+ model.save
24
+ model
25
+ end
26
+
8
27
  # Properties ===================================================================================================
9
-
28
+
10
29
  def property(name, type, options = {})
11
30
  klass = MLS::Property.determine_class(type)
12
31
  raise NotImplementedError, "#{type} is not supported" unless klass
13
32
 
14
33
  property = klass.new(name, options)
15
34
  @properties[property.name] = property
35
+ @properties_excluded_from_comparison = []
16
36
 
17
37
  create_reader_for(property)
18
38
  create_writer_for(property)
19
39
  end
20
40
 
41
+ def exclude_from_comparison(*properties)
42
+ @properties_excluded_from_comparison |= properties
43
+ end
44
+
45
+ def properties_excluded_from_comparison
46
+ @properties_excluded_from_comparison
47
+ end
48
+
21
49
  def properties
22
50
  @properties
23
51
  end
@@ -82,13 +110,16 @@ module MLS::Model
82
110
  end
83
111
 
84
112
  # used for parser
85
-
113
+ def root_element_string
114
+ ActiveSupport::Inflector.demodulize(self).underscore
115
+ end
116
+
86
117
  def root_element
87
- @root_element ||= ActiveSupport::Inflector.demodulize(self).downcase.to_sym
118
+ @root_element ||= root_element_string.to_sym
88
119
  end
89
120
 
90
121
  def collection_root_element
91
- @collection_root_element ||= ActiveSupport::Inflector.demodulize(self).downcase.pluralize.to_sym
122
+ @collection_root_element ||= root_element_string.pluralize.to_sym
92
123
  end
93
124
 
94
125
  end
@@ -4,6 +4,7 @@ class MLS::Account < MLS::Resource
4
4
  DEFAULTS = {:role => 'user'}
5
5
 
6
6
  property :id, Fixnum
7
+ property :type, String, :default => 'User'
7
8
  property :role, String, :default => 'user'
8
9
  property :name, String
9
10
  property :title, String
@@ -12,6 +13,7 @@ class MLS::Account < MLS::Resource
12
13
  property :password_confirmation, String, :serialize => :if_present
13
14
  property :perishable_token, String
14
15
  property :perishable_token_set_at, String
16
+ property :ghost, Boolean, :serialize => false, :default => false
15
17
 
16
18
  property :phone, String
17
19
  property :system_phone, String
@@ -21,7 +23,7 @@ class MLS::Account < MLS::Resource
21
23
  property :twitter, String
22
24
  property :facebook, String
23
25
  property :web, String
24
- property :mls_number, String
26
+ property :system_phone, String
25
27
 
26
28
  property :city, String
27
29
  property :state, String
@@ -30,36 +32,33 @@ class MLS::Account < MLS::Resource
30
32
  property :auth_key, String
31
33
 
32
34
  property :funding, String
33
- property :message, String
34
35
  property :population, String
35
36
  property :growing, Boolean
36
37
  property :move_in, String
37
38
  property :extra_info, String
38
-
39
- def update!
40
- MLS.put('/account', to_hash) do |code, response|
41
- case code
42
- when 400
43
- @errors = MLS.parse(response.body)[:errors]
44
- return false
45
- else
46
- MLS.handle_response(response)
47
- MLS::Account::Parser.update(self, response.body)
48
- end
39
+
40
+ exclude_from_comparison :password, :password_confirmation
41
+
42
+ attr_accessor :password_required
43
+
44
+ attr_writer :favorites
45
+
46
+ def update
47
+ MLS.put('/account', to_hash, 400) do |response, code|
48
+ MLS::Account::Parser.update(self, response.body)
49
+ code == 200
49
50
  end
50
51
  end
51
52
 
52
- def create!
53
- Rails.logger.warn(to_hash)
54
- MLS.post('/account', to_hash) do |code, response|
55
- case code
56
- when 400
57
- @errors = MLS.parse(response.body)[:errors]
58
- return false
59
- else
60
- MLS.handle_response(response)
61
- MLS::Account::Parser.update(self, response.body)
62
- end
53
+ # Save the Account to the MLS. @errors will be set on the account if there
54
+ # are any errors. @persisted will also be set to +true+ if the Account was
55
+ # succesfully created
56
+ def create
57
+ MLS.post('/account', to_hash, 400) do |response, code|
58
+ raise MLS::Exception::UnexpectedResponse if ![201, 400].include?(code)
59
+ MLS::Account::Parser.update(self, response.body)
60
+ @persisted = true
61
+ code == 201
63
62
  end
64
63
  end
65
64
 
@@ -68,77 +67,82 @@ class MLS::Account < MLS::Resource
68
67
  end
69
68
 
70
69
  def favorites
70
+ return @favorites if @favorites
71
71
  response = MLS.get('/account/favorites')
72
- MLS::Listing::Parser.parse_collection(response.body)
72
+ @favorites = MLS::Listing::Parser.parse_collection(response.body, {:collection_root_element => :favorites})
73
73
  end
74
74
 
75
- def favorite(listing_id)
76
- params_hash = {:id => listing_id}
77
- Rails.logger.warn(params_hash)
78
- MLS.post('/account/favorites', params_hash) do |code, response|
79
- case code
80
- when 400
81
- @errors = MLS.parse(response.body)[:errors]
82
- return false
83
- else
84
- MLS.handle_response(response)
85
- return true
86
- end
75
+ def favorited?(listing)
76
+ favorites.include?(listing)
77
+ end
78
+
79
+ def favorite(listing) # TODO: test me, i don't work on failures
80
+ params_hash = {:id => listing.is_a?(MLS::Listing) ? listing.id : listing }
81
+ MLS.post('/account/favorites', params_hash) do |response, code|
82
+ @favorites = nil
83
+ true
87
84
  end
88
85
  end
89
86
 
90
- def unfavorite(listing_id)
91
- MLS.delete("/account/favorites/#{listing_id}") do |code, response|
92
- case code
93
- when 400
94
- @errors = MLS.parse(response.body)[:errors]
95
- return false
96
- else
97
- MLS.handle_response(response)
98
- return true
99
- end
87
+ def unfavorite(listing_id) # TODO: test me, i don't work on failures
88
+ listing_id = listing_id.is_a?(MLS::Listing) ? listing_id.id : listing_id
89
+ MLS.delete("/account/favorites/#{listing_id}") do |response, code|
90
+ @favorites = nil
91
+ true
100
92
  end
101
93
  end
102
94
 
103
- class << self
95
+ def to_hash
96
+ hash = super
97
+ hash[:password_required] = password_required unless password_required.nil?
98
+ hash
99
+ end
104
100
 
101
+ class << self
102
+
105
103
  def current
106
104
  response = MLS.get('/account')
107
105
  MLS::Account::Parser.parse(response.body)
108
106
  end
109
107
 
108
+ # Authenticate and Account via <tt>email</tt> and <tt>password</tt>. Returns
109
+ # the <tt>Account</tt> object if successfully authenticated. Returns <tt>nil</tt>
110
+ # if the account could not be found, password was incorrect, or the account
111
+ # was revoked
112
+ #
113
+ # ==== Examples
114
+ # #!ruby
115
+ # Account.authenticate(:email => 'jon@does.net', :password => 'opensesame') # => #<Account>
116
+ #
117
+ # Account.authenticate('jon@does.net', 'opensesame') # => #<Account>
118
+ #
119
+ # Account.authenticate('jon@does.net', 'wrong') # => nil
110
120
  def authenticate(attrs_or_email, password=nil)
111
121
  email = attrs_or_email.is_a?(Hash) ? attrs_or_email[:email] : attrs_or_email
112
122
  password = attrs_or_email.is_a?(Hash) ? attrs_or_email[:password] : password
113
123
 
114
124
  response = MLS.get('/account', {:email => email, :password => password})
115
125
  MLS::Account::Parser.parse(response.body)
116
- rescue MLS::Unauthorized => response
126
+ rescue MLS::Exception::Unauthorized => response
117
127
  nil
118
128
  end
119
129
 
120
130
  def reset_password!(email)
121
- params_hash = {:email => email}
122
- Rails.logger.warn(params_hash)
123
- MLS.post('/accounts/password/reset', params_hash) do |code, response|
124
- case code
125
- when 400
126
- @errors = MLS.parse(response.body)[:errors]
127
- return false
128
- else
129
- MLS.handle_response(response)
130
- return true
131
- end
131
+ MLS.put('/account/reset_password', {:email => email}, 400) do |response, code|
132
+ MLS::Account::Parser.update(self, response.body)
133
+ code == 200
132
134
  end
133
135
  end
134
136
 
135
137
  def update_password!(params_hash)
136
- Rails.logger.warn(params_hash)
137
- response = MLS.put('/accounts/password', params_hash)
138
- MLS::Account::Parser.parse(response)
139
- rescue MLS::BadRequest => response
140
- @errors = MLS.parse(response.message)
141
- return false
138
+ MLS.put('/account/update_password', params_hash, 400) do |response, code|
139
+ MLS::Account::Parser.parse(response.body)
140
+ end
141
+ end
142
+
143
+ def search(terms)
144
+ response = MLS.get('/account/search', :query => terms)
145
+ MLS::Account::Parser.parse_collection(response.body)
142
146
  end
143
147
 
144
148
  end
@@ -147,4 +151,8 @@ end
147
151
 
148
152
  class MLS::Account::Parser < MLS::Parser
149
153
 
154
+ def favorites=(favorites)
155
+ @object.favorites = favorites.map {|a| MLS::Listing::Parser.build(a) }
156
+ end
157
+
150
158
  end
@@ -1,8 +1,8 @@
1
1
  class MLS::Address < MLS::Resource
2
2
 
3
- property :id, Fixnum
4
- property :name, String
5
- property :slug, String
3
+ property :id, Fixnum, :serialize => :if_present
4
+ property :name, String, :serialize => :false
5
+ property :slug, String, :serialize => :if_present
6
6
 
7
7
  property :latitude, Decimal
8
8
  property :longitude, Decimal
@@ -16,6 +16,61 @@ class MLS::Address < MLS::Resource
16
16
  property :state, String
17
17
  property :country, String
18
18
  property :postal_code, String
19
+ property :min_rate_per_year, Decimal, :serialize => :if_present
20
+ property :max_rate_per_year, Decimal, :serialize => :if_present
21
+ property :max_size, Fixnum, :serialize => :if_present
22
+ property :min_size, Fixnum, :serialize => :if_present
23
+ property :comments, String
24
+ property :year_built, Fixnum
25
+ property :total_size, Fixnum
26
+ property :floors, Fixnum
27
+ property :internet_speed, Decimal
28
+ property :parking_garage, Boolean
29
+ property :lobby_attendant, Boolean
30
+ property :gym, Boolean
31
+ property :leed_certification, String
32
+
33
+ # Counter caches
34
+ property :listings_count, Fixnum, :serialize => :false
35
+ property :leased_listings_count, Fixnum, :serialize => :false
36
+ property :hidden_listings_count, Fixnum, :serialize => :false
37
+ property :import_listings_count, Fixnum, :serialize => :false
38
+ property :active_listings_count, Fixnum, :serialize => :false
39
+
40
+ attr_accessor :listings, :listing_kinds, :photos
41
+
42
+ # should include an optional use address or no_image image
43
+ def avatar(size='100x200', protocol='http')
44
+ params = {
45
+ :size => size,
46
+ :format => 'png',
47
+ :sensor => false,
48
+ :location => [formatted_address],
49
+ :fov => 120
50
+ }
51
+
52
+ "#{protocol}://maps.googleapis.com/maps/api/streetview?" + params.map{|k,v| k.to_s + '=' + URI.escape(v.to_s) }.join('&')
53
+ end
54
+
55
+ def to_param
56
+ [state, city, name].map(&:parameterize).join('/')
57
+ end
58
+
59
+ def url
60
+ if defined? Rails
61
+ case Rails.env
62
+ when "production"
63
+ host = "42floors.com"
64
+ when "staging"
65
+ host = "staging.42floors.com"
66
+ when "development","test"
67
+ host = "spire.dev"
68
+ end
69
+ else
70
+ host = "42floors.com"
71
+ end
72
+ "http://#{host}/#{slug}"
73
+ end
19
74
 
20
75
  class << self
21
76
 
@@ -25,8 +80,19 @@ class MLS::Address < MLS::Resource
25
80
  end
26
81
 
27
82
  # Bounds is passed as 'n,e,s,w' or [n, e, s, w]
28
- def box_cluster(bounds, zoom, filters={})
29
- response = MLS.get('/addresses/box_cluster', :bounds => bounds, :zoom => zoom, :filters => filters)
83
+ def box_cluster(bounds, zoom, where={})
84
+ MLS.get('/addresses/box_cluster', :bounds => bounds, :zoom => zoom, :where => where)
85
+ end
86
+
87
+ def find(id)
88
+ response = MLS.get("/addresses/#{id}")
89
+ MLS::Address::Parser.parse(response.body)
90
+ end
91
+
92
+ # currently supported options are :include, :where, :limit, :offset
93
+ def all(options={})
94
+ response = MLS.get('/addresses', options)
95
+ MLS::Address::Parser.parse_collection(response.body)
30
96
  end
31
97
 
32
98
  end
@@ -35,5 +101,20 @@ end
35
101
 
36
102
 
37
103
  class MLS::Address::Parser < MLS::Parser
38
-
104
+
105
+ def listings=(listings)
106
+ @object.listings = listings.map { |data|
107
+ listing = MLS::Listing::Parser.build(data)
108
+ listing.address = @object
109
+ listing
110
+ }
111
+ end
112
+
113
+ def listing_kinds=(listing_kinds)
114
+ @object.listing_kinds = listing_kinds
115
+ end
116
+
117
+ def photos=(photos)
118
+ @object.photos = photos.map {|d| MLS::Photo.new({:digest => d})}
119
+ end
39
120
  end
@@ -0,0 +1,27 @@
1
+ class MLS::Area < MLS::Resource
2
+
3
+ property :id, Fixnum, :serialize => :if_present
4
+ property :name, String, :serialize => :if_present
5
+ property :level, Fixnum, :serialize => :if_present
6
+ property :type, String, :serialize => :if_present
7
+ property :source, String, :serialize => :if_present
8
+ property :geometry, Hash, :serialize => false
9
+
10
+ # Counter caches
11
+ property :listings_count, Fixnum, :serialize => :false
12
+
13
+ class << self
14
+
15
+ def find(id)
16
+ response = MLS.get("/areas/#{id}")
17
+ MLS::Area::Parser.parse(response.body)
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+
24
+
25
+ class MLS::Area::Parser < MLS::Parser
26
+
27
+ end