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