mls 0.2.2 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.rdoc +54 -0
- data/Rakefile +20 -2
- data/lib/mls.rb +307 -78
- data/lib/mls/errors.rb +13 -4
- data/lib/mls/model.rb +36 -5
- data/lib/mls/models/account.rb +74 -66
- data/lib/mls/models/address.rb +87 -6
- data/lib/mls/models/area.rb +27 -0
- data/lib/mls/models/flyer.rb +41 -0
- data/lib/mls/models/listing.rb +180 -34
- data/lib/mls/models/photo.rb +25 -3
- data/lib/mls/models/tour_request.rb +18 -31
- data/lib/mls/parser.rb +5 -4
- data/lib/mls/properties/datetime.rb +5 -1
- data/lib/mls/properties/decimal.rb +3 -1
- data/lib/mls/properties/hash.rb +7 -0
- data/lib/mls/resource.rb +35 -5
- data/lib/rdoc/generator/template/42floors/_context.rhtml +209 -0
- data/lib/rdoc/generator/template/42floors/_head.rhtml +7 -0
- data/lib/rdoc/generator/template/42floors/class.rhtml +39 -0
- data/lib/rdoc/generator/template/42floors/file.rhtml +35 -0
- data/lib/rdoc/generator/template/42floors/index.rhtml +13 -0
- data/lib/rdoc/generator/template/42floors/resources/apple-touch-icon.png +0 -0
- data/lib/rdoc/generator/template/42floors/resources/css/github.css +129 -0
- data/lib/rdoc/generator/template/42floors/resources/css/main.css +339 -0
- data/lib/rdoc/generator/template/42floors/resources/css/panel.css +389 -0
- data/lib/rdoc/generator/template/42floors/resources/css/reset.css +48 -0
- data/lib/rdoc/generator/template/42floors/resources/favicon.ico +0 -0
- data/lib/rdoc/generator/template/42floors/resources/i/arrows.png +0 -0
- data/lib/rdoc/generator/template/42floors/resources/i/results_bg.png +0 -0
- data/lib/rdoc/generator/template/42floors/resources/i/tree_bg.png +0 -0
- data/lib/rdoc/generator/template/42floors/resources/js/highlight.pack.js +1 -0
- data/lib/rdoc/generator/template/42floors/resources/js/jquery-1.3.2.min.js +19 -0
- data/lib/rdoc/generator/template/42floors/resources/js/jquery-effect.js +593 -0
- data/lib/rdoc/generator/template/42floors/resources/js/main.js +20 -0
- data/lib/rdoc/generator/template/42floors/resources/js/searchdoc.js +442 -0
- data/lib/rdoc/generator/template/42floors/resources/panel/index.html +73 -0
- data/lib/rdoc/generator/template/42floors/se_index.rhtml +8 -0
- data/mls.gemspec +7 -4
- data/test/factories/account.rb +18 -0
- data/test/factories/address.rb +15 -0
- data/test/factories/listing.rb +30 -0
- data/test/factories/tour_request.rb +9 -0
- data/test/fixtures/flyer.pdf +68 -0
- data/test/test_helper.rb +44 -5
- data/test/units/models/test_account.rb +20 -43
- data/test/units/models/test_flyer.rb +22 -0
- data/test/units/models/test_listing.rb +119 -0
- data/test/units/models/test_photo.rb +136 -3
- data/test/units/models/test_tour_request.rb +25 -20
- data/test/units/test_errors.rb +12 -4
- data/test/units/test_mls.rb +263 -3
- data/test/units/test_resource.rb +1 -0
- metadata +78 -57
- data/lib/mls/models/user.rb +0 -7
- 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::
|
4
|
+
class MLS::Exception::UnexpectedResponse < MLS::Exception
|
5
5
|
end
|
6
6
|
|
7
|
-
class MLS::
|
7
|
+
class MLS::Exception::BadRequest < MLS::Exception
|
8
8
|
end
|
9
9
|
|
10
|
-
class MLS::
|
10
|
+
class MLS::Exception::Unauthorized < MLS::Exception
|
11
11
|
end
|
12
12
|
|
13
|
-
class MLS::
|
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 ||=
|
118
|
+
@root_element ||= root_element_string.to_sym
|
88
119
|
end
|
89
120
|
|
90
121
|
def collection_root_element
|
91
|
-
@collection_root_element ||=
|
122
|
+
@collection_root_element ||= root_element_string.pluralize.to_sym
|
92
123
|
end
|
93
124
|
|
94
125
|
end
|
data/lib/mls/models/account.rb
CHANGED
@@ -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 :
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
data/lib/mls/models/address.rb
CHANGED
@@ -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,
|
29
|
-
|
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
|