mls 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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