mls 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mls +17 -0
  3. data/lib/mls.rb +3 -1
  4. data/lib/mls/cli.rb +48 -0
  5. data/lib/mls/cli/documents.rb +71 -0
  6. data/lib/mls/cli/storage.rb +1 -0
  7. data/lib/mls/cli/storage/s3.rb +99 -0
  8. data/lib/mls/{account.rb → models/account.rb} +27 -7
  9. data/lib/mls/{accounts_region.rb → models/accounts_region.rb} +0 -0
  10. data/lib/mls/models/action.rb +44 -0
  11. data/lib/mls/{address.rb → models/address.rb} +0 -0
  12. data/lib/mls/{api_key.rb → models/api_key.rb} +0 -0
  13. data/lib/mls/{coworking_space.rb → models/coworking_space.rb} +0 -1
  14. data/lib/mls/{credit_card.rb → models/credit_card.rb} +0 -0
  15. data/lib/mls/{datum.rb → models/datum.rb} +0 -0
  16. data/lib/mls/{document.rb → models/document.rb} +0 -0
  17. data/lib/mls/{email.rb → models/email.rb} +0 -0
  18. data/lib/mls/{email_address.rb → models/email_address.rb} +0 -0
  19. data/lib/mls/models/email_digest.rb +13 -0
  20. data/lib/mls/{event.rb → models/event.rb} +0 -0
  21. data/lib/mls/{flyer.rb → models/flyer.rb} +0 -0
  22. data/lib/mls/{geometry.rb → models/geometry.rb} +0 -0
  23. data/lib/mls/{image_ordering.rb → models/image_ordering.rb} +0 -0
  24. data/lib/mls/{impression_count.rb → models/impression_count.rb} +0 -0
  25. data/lib/mls/{inquiry.rb → models/inquiry.rb} +16 -1
  26. data/lib/mls/{invoice.rb → models/invoice.rb} +1 -1
  27. data/lib/mls/models/lead.rb +26 -0
  28. data/lib/mls/{listing.rb → models/listing.rb} +20 -4
  29. data/lib/mls/{locality.rb → models/locality.rb} +0 -0
  30. data/lib/mls/{metadatum.rb → models/metadatum.rb} +0 -0
  31. data/lib/mls/{mistake.rb → models/mistake.rb} +0 -0
  32. data/lib/mls/{organization.rb → models/organization.rb} +0 -0
  33. data/lib/mls/{ownership.rb → models/ownership.rb} +0 -0
  34. data/lib/mls/{phone.rb → models/phone.rb} +0 -0
  35. data/lib/mls/{property.rb → models/property.rb} +31 -3
  36. data/lib/mls/{recommendation.rb → models/recommendation.rb} +0 -0
  37. data/lib/mls/{reference.rb → models/reference.rb} +0 -0
  38. data/lib/mls/{region.rb → models/region.rb} +8 -2
  39. data/lib/mls/models/search.rb +63 -0
  40. data/lib/mls/models/service.rb +11 -0
  41. data/lib/mls/{session.rb → models/session.rb} +0 -0
  42. data/lib/mls/models/site.rb +26 -0
  43. data/lib/mls/{slug.rb → models/slug.rb} +0 -0
  44. data/lib/mls/{source.rb → models/source.rb} +0 -0
  45. data/lib/mls/{space.rb → models/space.rb} +0 -0
  46. data/lib/mls/{stat.rb → models/stat.rb} +0 -0
  47. data/lib/mls/models/subscription.rb +19 -0
  48. data/lib/mls/models/suggestion.rb +13 -0
  49. data/lib/mls/{task.rb → models/task.rb} +0 -0
  50. data/lib/mls/{team.rb → models/team.rb} +0 -0
  51. data/lib/mls/models/tim_alert.rb +7 -0
  52. data/lib/mls/{time_log.rb → models/time_log.rb} +0 -0
  53. data/lib/mls/{use.rb → models/use.rb} +0 -0
  54. data/lib/mls/{vendor.rb → models/vendor.rb} +0 -0
  55. data/lib/mls/{view.rb → models/view.rb} +0 -0
  56. data/lib/mls/{webpage.rb → models/webpage.rb} +0 -0
  57. data/lib/mls/rack/proxy.rb +78 -0
  58. data/lib/mls/railtie.rb +32 -0
  59. data/mls.gemspec +1 -1
  60. metadata +61 -49
  61. data/lib/mls/action.rb +0 -18
  62. data/lib/mls/email_digest.rb +0 -10
  63. data/lib/mls/lead.rb +0 -6
  64. data/lib/mls/membership.rb +0 -27
  65. data/lib/mls/subscription.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d679db7d9456ed2f5924a612fd51157607a5c360
4
- data.tar.gz: 869b5a1f4d647eb414827322bc9904cb68220488
3
+ metadata.gz: '05855a1a8a8ff7514aa0d09798c336972c6443cc'
4
+ data.tar.gz: 689bc19414f3f6516d74bb8f8721d6019fb86060
5
5
  SHA512:
6
- metadata.gz: ec8233dc1a01a4341d793e112bb67540796eb3d5ec90752704213fff4075755e8ad81e7c497bce35a23c47ca81fe0f4cb496f30a0a6150249bd08fc87a088608
7
- data.tar.gz: ba2ae50b4822afac5febe29d8bcca90a6af7cd17e5a0d283f29f2ed8d717cbd8571674ae1e06554f0baca2a8b17ffa6304a613f1d6409666e1c65f0e8b18a3f3
6
+ metadata.gz: e675d1684d26d1fc2a5c64fe995bce74f185090a161844091bc24b5bfc7d6f36d723d2a6f57700ee41589dedb58d77044ab29a752ed75c3783f714fa2bdc827b
7
+ data.tar.gz: a94d1a6219a065cbc0c436e167f06a333c90662aec2160d5cf62e3a4dd1bed96100d2296e364d1ce71c9fd1b9cb6488aaaced1e66cdd0f9e60bf683534fad5ae
data/bin/mls ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path('../../lib/mls/cli', __FILE__)
3
+
4
+ case ARGV.shift.downcase.strip
5
+ when "documents:backup"
6
+ MLS::CLI.parse_args(ARGV).inspect
7
+ directory = ARGV.shift
8
+
9
+ if directory.nil? || !File.directory?(directory)
10
+ puts "Usage: mls documents:backup dir [options...]"
11
+ exit 1
12
+ end
13
+
14
+ MLS::CLI::Documents.backup(directory)
15
+ else
16
+ puts "Usage: mls documents:backup dir [options...]"
17
+ end
data/lib/mls.rb CHANGED
@@ -158,4 +158,6 @@ module MLS::Avatar
158
158
 
159
159
  end
160
160
 
161
- Dir.glob(File.join(File.dirname(__FILE__), 'mls', '*.rb'), &method(:require))
161
+ Dir.glob(File.join(File.dirname(__FILE__), 'mls', 'models', '*.rb'), &method(:require))
162
+
163
+ require 'mls/railtie' if defined?(Rails)
@@ -0,0 +1,48 @@
1
+ require File.expand_path('../../mls', __FILE__)
2
+ require 'uri'
3
+ require 'optparse'
4
+
5
+ module MLS::CLI
6
+
7
+ def self.options
8
+ if !class_variable_defined?(:@@options)
9
+ @@options = {}
10
+ end
11
+ @@options
12
+ end
13
+
14
+ def self.parse_args(args)
15
+ OptionParser.new do |opts|
16
+ opts.on("-aURL", "--auth=URL", "URL Credentials for MLS, S3 or B2") do |arg|
17
+ url = URI.parse(arg)
18
+ case url.scheme
19
+ when 's3' # ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX][?parition=4]
20
+ MLS::CLI.options[:s3] = {
21
+ access_key_id: URI.unescape(url.user),
22
+ secret_access_key: URI.unescape(url.password),
23
+ bucket: URI.unescape(url.host)
24
+ }
25
+ MLS::CLI.options[:s3][:prefix] = url.path if url.path && !url.path.empty?
26
+ url.query.split('&').each do |qp|
27
+ key, value = qp.split('=').map { |d| URI.unescape(d) }
28
+ case key
29
+ when 'partition'
30
+ MLS::CLI.options[:s3][:partition] = true
31
+ MLS::CLI.options[:s3][:partition_depth] = value.to_i
32
+ end
33
+ end
34
+ when 'b2'
35
+
36
+ when 'mls'
37
+ arg = arg.sub('mls://', 'https://')
38
+ MLS::CLI.options[:mls] = arg
39
+ MLS::Model.establish_connection(adapter: 'sunstone', url: arg)
40
+ end
41
+ end
42
+ end.parse!(args)
43
+ end
44
+
45
+ end
46
+
47
+ require File.expand_path('../cli/storage', __FILE__)
48
+ require File.expand_path('../cli/documents', __FILE__)
@@ -0,0 +1,71 @@
1
+ require 'fileutils'
2
+ require 'digest'
3
+
4
+ module MLS::CLI::Documents
5
+
6
+ def self.partition(value, depth: 5)
7
+ split = value.scan(/.{1,4}/)
8
+ split.shift(depth).join("/") + split.join("")
9
+ end
10
+
11
+ def self.calculate_digest(file)
12
+ md5_digest = Digest::MD5.new
13
+ sha1_digest = Digest::SHA1.new
14
+ sha256_digest = Digest::SHA256.new
15
+
16
+ buf = ""
17
+ file.rewind
18
+ while file.read(16384, buf)
19
+ md5_digest << buf
20
+ sha1_digest << buf
21
+ sha256_digest << buf
22
+ end
23
+
24
+ {
25
+ md5: md5_digest.hexdigest,
26
+ sha1: sha1_digest.hexdigest,
27
+ sha256: sha256_digest.hexdigest
28
+ }
29
+ end
30
+
31
+ def self.backup(dir)
32
+ last_sync_file = File.join(dir, '.last_sync')
33
+
34
+ query = if File.exist?(last_sync_file)
35
+ from_timestamp = Time.iso8601(File.read(last_sync_file))
36
+ Document.filter(created_at: {gte: from_timestamp})
37
+ else
38
+ Document.all
39
+ end
40
+
41
+ query.find_each do |document|
42
+ if document.sha256 && File.exists?(File.join(dir, partition(document.sha256)))
43
+ puts "Downloaded #{document.id}"
44
+ next
45
+ end
46
+
47
+ if document.provider.nil? || document.provider.include?('s3/hash_key')
48
+ storage_engine = MLS::CLI::Storage::S3.new(MLS::CLI.options[:s3])
49
+ key = 'hash_key'
50
+ else
51
+ raise 'unkown storage engine'
52
+ end
53
+
54
+ puts "Downloading #{document.id}"
55
+ storage_engine.copy_to_tempfile(document.send(key)) do |file|
56
+ digests = calculate_digest(file)
57
+
58
+ raise 'MD5 does not match' if digests[:md5] != document.md5
59
+ document.update!(digests.merge({provider: ['s3/hash_key']}))
60
+
61
+ FileUtils.mkdir_p(File.dirname(File.join(dir, partition(document.sha256))))
62
+ FileUtils.mv(file.path, File.join(dir, partition(document.sha256)), verbose: true)
63
+ end
64
+
65
+ File.write(last_sync_file, document.created_at.iso8601(6))
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+
@@ -0,0 +1 @@
1
+ require File.expand_path('../storage/s3', __FILE__)
@@ -0,0 +1,99 @@
1
+ require 'aws-sdk-s3'
2
+
3
+ module MLS::CLI::Storage
4
+ class S3
5
+
6
+ def initialize(configs = {})
7
+ @configs = configs
8
+ @configs[:region] ||= 'us-east-1'
9
+ @configs[:prefix] ||= ""
10
+
11
+ @client = Aws::S3::Client.new({
12
+ access_key_id: configs[:access_key_id],
13
+ secret_access_key: configs[:secret_access_key],
14
+ region: configs[:region]
15
+ })
16
+ end
17
+
18
+ def object_for(key)
19
+ Aws::S3::Object.new(@configs[:bucket], key, client: @client)
20
+ end
21
+
22
+ def local?
23
+ false
24
+ end
25
+
26
+ def url(key)
27
+ [host, destination(key)].join('/')
28
+ end
29
+
30
+ def host
31
+ h = @configs[:bucket_host_alias] || "https://s3.amazonaws.com/#{@configs[:bucket]}"
32
+ h.delete_suffix('/')
33
+ end
34
+
35
+ def destination(key)
36
+ "#{@configs[:prefix]}#{partition(key)}".gsub(/^\//, '')#delete_prefix('/')
37
+ end
38
+
39
+ def exists?(key)
40
+ object_for(destination(key)).exists?
41
+ end
42
+
43
+ # def write(key, file, meta_info)
44
+ # file = file.tempfile if file.is_a?(ActionDispatch::Http::UploadedFile)
45
+ #
46
+ # object_for(destination(key)).upload_file(file, {
47
+ # :acl => 'public-read',
48
+ # :content_disposition => "inline; filename=\"#{meta_info[:filename]}\"",
49
+ # :content_type => meta_info[:content_type]
50
+ # })
51
+ # end
52
+
53
+ def read(key, &block)
54
+ object_for(destination(key)).get()
55
+ end
56
+
57
+ def cp(key, path)
58
+ object_for(destination(key)).get({ response_target: path })
59
+ true
60
+ end
61
+
62
+ def delete(key)
63
+ object_for(destination(key)).delete
64
+ end
65
+
66
+ def copy_to_tempfile(key)
67
+ tmpfile = Tempfile.new([File.basename(key), File.extname(key)], binmode: true)
68
+ cp(key, tmpfile.path)
69
+ if block_given?
70
+ begin
71
+ yield(tmpfile)
72
+ ensure
73
+ tmpfile.close!
74
+ end
75
+ else
76
+ tmpfile
77
+ end
78
+ end
79
+
80
+ def partition(value)
81
+ return value unless @configs[:partition]
82
+ split = value.scan(/.{1,4}/)
83
+ split.shift(@configs[:partition_depth] || 3).join("/") + split.join("")
84
+ end
85
+
86
+ def md5(key)
87
+ object_for(destination(key)).etag
88
+ end
89
+
90
+ def last_modified(key)
91
+ object_for(destination(key)).last_modified
92
+ end
93
+
94
+ def mime_type(key)
95
+ object_for(destination(key)).content_type
96
+ end
97
+
98
+ end
99
+ end
@@ -4,17 +4,23 @@ class Account < MLS::Model
4
4
  include MLS::Avatar
5
5
 
6
6
  belongs_to :organization
7
- belongs_to :membership
8
-
7
+ belongs_to :cobroke_manager, class_name: "Account"
8
+
9
+ has_and_belongs_to_many :subscriptions
9
10
  has_many :tasks
10
11
  has_many :sources
11
12
  has_many :ownerships, :inverse_of => :account, :dependent => :delete_all
12
13
  has_many :assets, through: :ownerships
13
14
  has_many :coworking_spaces, through: :ownerships, source: :asset, source_type: 'CoworkingSpace'
14
15
  has_many :listings, through: :ownerships, source: :asset, source_type: 'Listing', inverse_of: :accounts
16
+ has_many :sites, through: :ownerships, source: :asset, source_type: 'Site', inverse_of: :accounts
15
17
  has_many :email_digests
16
- has_many :subscriptions, as: :subject
17
- has_many :leads
18
+ has_many :services, as: :subject
19
+ has_many :tim_alerts
20
+ has_many :references, as: :subject
21
+
22
+ has_many :searches
23
+ has_many :suggestions, foreign_key: "suggested_by_id"
18
24
 
19
25
  has_many :credit_cards
20
26
 
@@ -28,6 +34,10 @@ class Account < MLS::Model
28
34
  end
29
35
  end
30
36
  end
37
+
38
+ def deals
39
+ Search.filter([{manager_id: self.id}, "OR", {broker_id: self.id}])
40
+ end
31
41
 
32
42
  has_many :phones do
33
43
 
@@ -66,15 +76,19 @@ class Account < MLS::Model
66
76
  end
67
77
 
68
78
  def tim_alerts?
69
- self.membership&.subscriptions&.filter(started_at: true, status: {not: "closed"}, type: "tim_alerts", subject_id: self.id, subject_type: "Account")&.count.try(:>, 0)
79
+ subscriptions.map{|x| x.services.filter(status: "active", type: "tim_alerts").count}.sum > 0
70
80
  end
71
81
 
72
82
  def unlimited?
73
- self.membership&.subscriptions&.filter(started_at: true, status: {not: "closed"}, type: "unlimited", subject_id: self.id, subject_type: "Account")&.count.try(:>, 0)
83
+ subscriptions.map{|x| x.services.filter(status: "active", type: "unlimited").count}.sum > 0
84
+ end
85
+
86
+ def referral?
87
+ subscriptions.map{|x| x.services.filter(status: "active", type: "referral").count}.sum > 0
74
88
  end
75
89
 
76
90
  def paying?
77
- self.membership&.subscriptions&.filter(started_at: true, status: {not: "closed"})&.count.try(:>, 0)
91
+ subscriptions.map{|x| x.services.filter(status: "active").count}.sum > 0
78
92
  end
79
93
 
80
94
  def password_required?
@@ -153,6 +167,12 @@ class Account < MLS::Model
153
167
  req.body = {url: url}.to_json
154
168
  Account.connection.instance_variable_get(:@connection).send_request(req)
155
169
  end
170
+
171
+ def set_confirmation_token
172
+ req = Net::HTTP::Get.new("/accounts/#{self.id}/confirm")
173
+ response = Account.connection.instance_variable_get(:@connection).send_request(req)
174
+ self.confirmation_token = response.body
175
+ end
156
176
 
157
177
 
158
178
  end
@@ -0,0 +1,44 @@
1
+ class Action < MLS::Model
2
+ self.inheritance_column = nil
3
+ attr_accessor :account_id
4
+
5
+ belongs_to :event
6
+ belongs_to :subject, :polymorphic => true
7
+
8
+ has_many :mistakes
9
+ has_many :metadata, foreign_key: :event_id, primary_key: :event_id
10
+
11
+ def self.by_performer(filter)
12
+ req = Net::HTTP::Get.new("/actions/by_performer")
13
+ req.body = {
14
+ where: filter
15
+ }.to_json
16
+ JSON.parse(connection.instance_variable_get(:@connection).send_request(req).body)
17
+ end
18
+
19
+ def self.squash(attributes)
20
+ squashed_actions = []
21
+ where(nil).each do |action|
22
+ action.account_id = action.metadata.where(key: 'performed_by_id').first&.value
23
+
24
+ action.diff = action.diff.slice(*attributes) if attributes
25
+ if squashed_actions.last &&
26
+ action.account_id == squashed_actions.last.account_id &&
27
+ action.timestamp + 15.minutes > squashed_actions.last.timestamp
28
+
29
+ action.diff.each do |key, value|
30
+ next if value[0] == value[1] # filter sometimes logs even if the same
31
+ if squashed_actions.last.diff[key]
32
+ squashed_actions.last.diff[key][0] = value[0]
33
+ else
34
+ squashed_actions.last.diff[key] = value
35
+ end
36
+ end
37
+ else
38
+ squashed_actions << action
39
+ end
40
+ end
41
+ squashed_actions
42
+ end
43
+
44
+ end
@@ -4,7 +4,6 @@ class CoworkingSpace < MLS::Model
4
4
 
5
5
  belongs_to :organization
6
6
  belongs_to :property
7
- belongs_to :membership
8
7
  has_many :image_orderings, as: :subject
9
8
  has_many :photos, through: :image_orderings, source: :image
10
9
  has_many :spaces
File without changes
File without changes
@@ -0,0 +1,13 @@
1
+ class EmailDigest < MLS::Model
2
+
3
+ belongs_to :search
4
+ belongs_to :account
5
+ accepts_nested_attributes_for :account
6
+
7
+ def filter
8
+ filter_to_read = read_attribute(:filter)
9
+ filter_to_read = search.filter if filter_to_read&.empty? && search
10
+ JSON.parse (filter_to_read || {}).to_json, object_class: OpenStruct
11
+ end
12
+
13
+ end
File without changes
File without changes
@@ -1,7 +1,8 @@
1
1
  class Inquiry < MLS::Model
2
+
3
+ TERMS = %w(<1 1-2 3-5 5+ flexible)
2
4
 
3
5
  has_many :emails
4
- has_many :leads
5
6
  belongs_to :subject, polymorphic: true
6
7
  belongs_to :account
7
8
 
@@ -10,6 +11,20 @@ class Inquiry < MLS::Model
10
11
  def property
11
12
  subject.is_a? MLS::Model::Listing ? subject.property : subject
12
13
  end
14
+
15
+ def term_units(value=nil)
16
+ value ||= self.term
17
+ case value
18
+ when "<1"
19
+ "year"
20
+ when "flexible"
21
+ ""
22
+ when nil
23
+ ""
24
+ else
25
+ "years"
26
+ end
27
+ end
13
28
 
14
29
  def account_attributes=(account_attrs)
15
30
  account_attrs = account_attrs&.with_indifferent_access
@@ -1,7 +1,7 @@
1
1
  class Invoice < MLS::Model
2
2
 
3
3
  belongs_to :credit_card
4
- belongs_to :membership
4
+ belongs_to :subscription
5
5
 
6
6
  def amount
7
7
  read_attribute(:amount) / 100.0 if read_attribute(:amount)
@@ -0,0 +1,26 @@
1
+ class Lead < MLS::Model
2
+
3
+ STATUSES = %w(delivered connected touring proposals leases closed lost)
4
+
5
+ has_many :inquiries
6
+ has_many :tim_alerts
7
+
8
+ def regions
9
+ Region.where(id: region_ids)
10
+ end
11
+
12
+ def term_units(value=nil)
13
+ value ||= self.term
14
+ case value
15
+ when "<1"
16
+ "year"
17
+ when "flexible"
18
+ ""
19
+ when nil
20
+ ""
21
+ else
22
+ "years"
23
+ end
24
+ end
25
+
26
+ end
@@ -37,6 +37,10 @@ class Listing < MLS::Model
37
37
 
38
38
  accepts_nested_attributes_for :uses, :ownerships, :image_orderings
39
39
 
40
+ def is_elite?
41
+ ((property.elite_account_ids || []) & accounts.map(&:id)).length > 0
42
+ end
43
+
40
44
  def premium_property?
41
45
  Subscription.filter(started_at: true, ends_at: false, type: "premium", subject_type: "Property", subject_id: self.property_id).count > 0
42
46
  end
@@ -49,11 +53,11 @@ class Listing < MLS::Model
49
53
  end
50
54
  end
51
55
 
52
- def lead_contact
56
+ def lead_contacts
53
57
  if ownerships.loaded?
54
- @lead_contact ||= ownerships.select{|o| o.lead}.first.try(:account)
58
+ @lead_contacts ||= ownerships.select{|o| o.lead}.map(&:account)
55
59
  else
56
- @lead_contact ||= ownerships.eager_load(:account).filter(:lead => true).first.try(:account)
60
+ @lead_contacts ||= ownerships.eager_load(:account).filter(:lead => true).map(&:account)
57
61
  end
58
62
  end
59
63
 
@@ -147,7 +151,7 @@ class Listing < MLS::Model
147
151
  def name
148
152
  return "New Listing" if !self.id
149
153
  name = ""
150
- if self.type == "building"
154
+ if self.unit_type == "building"
151
155
  name += "Entire Building"
152
156
  else
153
157
  name = "Unit #{self.unit}" if self.unit.present?
@@ -158,4 +162,16 @@ class Listing < MLS::Model
158
162
  name = "Space" if name.blank?
159
163
  name
160
164
  end
165
+
166
+ def status
167
+ if self.leased_at
168
+ "Leased"
169
+ elsif self.archived
170
+ "Deleted"
171
+ elsif self.touched_at < 90.days.ago
172
+ "Expired"
173
+ else
174
+ "Active"
175
+ end
176
+ end
161
177
  end
File without changes
@@ -19,7 +19,7 @@ class Property < MLS::Model
19
19
  has_many :image_orderings, as: :subject
20
20
  has_many :data, as: :subject
21
21
  has_many :photos, through: :image_orderings, source: :image
22
- has_many :subscriptions, as: :subject
22
+ has_many :services, as: :subject
23
23
 
24
24
  has_many :uses
25
25
  # has_and_belongs_to_many :uses
@@ -48,6 +48,12 @@ class Property < MLS::Model
48
48
  def latitude
49
49
  location.y
50
50
  end
51
+
52
+ def is_elite_for_account_ids?(*account_ids)
53
+ return false unless self.elite_account_ids && self.elite_account_ids.length > 0
54
+ account_ids = Array(account_ids).flatten
55
+ (self.elite_account_ids & account_ids).length > 0
56
+ end
51
57
 
52
58
  def display_description
53
59
  return description if description.present?
@@ -122,6 +128,7 @@ class Property < MLS::Model
122
128
  region = neighborhood_region
123
129
  region ||= city_region
124
130
  region ||= market
131
+ region ||= regions.where(depth: true).sort_by(&:depth).reverse.first
125
132
  region
126
133
  end
127
134
 
@@ -134,13 +141,28 @@ class Property < MLS::Model
134
141
 
135
142
  def city_region
136
143
  return @city_region if defined? @city_region
137
- @city_region = fetch_region(:type => "City")
144
+ @city_region = fetch_region(:type => Region::CITY_TYPES)
145
+ end
146
+
147
+ def state_region
148
+ return @state_region if defined? @state_region
149
+ @state_region = fetch_region(:type => Region::STATE_TYPES)
138
150
  end
139
151
 
140
152
  def market
141
153
  return @market if defined? @market
142
154
  @market = fetch_region(:is_market => true)
143
155
  end
156
+
157
+ def human_breadcrumbs
158
+ [
159
+ neighborhood.present? ? neighborhood : neighborhood_region&.name,
160
+ city.present? ? city : (city_region&.name || regions.select{|r|
161
+ r.depth && r.depth >= 3
162
+ }.first&.name),
163
+ state.present? ? state : state_region&.slug&.split("/")&.last&.upcase
164
+ ].compact
165
+ end
144
166
 
145
167
  def flagship
146
168
  return @flagship if defined? @flagship
@@ -152,7 +174,13 @@ class Property < MLS::Model
152
174
  if params[0][0] == :query
153
175
  regions.to_a.find{|r| r.name == params[0][1]}
154
176
  else
155
- regions.to_a.find{|r| r[params[0][0]] == params[0][1]}
177
+ regions.to_a.find{|r|
178
+ if params[0][1].is_a? Array
179
+ params[0][1].include? (r[params[0][0]])
180
+ else
181
+ r[params[0][0]] == params[0][1]
182
+ end
183
+ }
156
184
  end
157
185
  end
158
186
  end
@@ -4,6 +4,8 @@ class Region < MLS::Model
4
4
 
5
5
  self.inheritance_column = nil
6
6
 
7
+ COUNTRY_TYPES = ["Monarchy", "Republic"]
8
+ STATE_TYPES = ["State", "Territory", "Commonwealth", "Province"]
7
9
  CITY_TYPES = ["City", "Municipality", "Village", "Rural Municipality", "Town", "Resort Village", "Community Government"]
8
10
 
9
11
  belongs_to :cover_photo, :class_name => 'Image'
@@ -20,8 +22,12 @@ class Region < MLS::Model
20
22
  def name
21
23
  if common_name.try(:[], 'eng')
22
24
  common_name['eng'].is_a?(Array) ? common_name['eng'].first : common_name['eng']
23
- else
25
+ elsif official_name.try(:[], 'eng')
24
26
  official_name['eng'].is_a?(Array) ? official_name['eng'].first : official_name['eng']
27
+ elsif common_name && common_name.size > 0
28
+ common_name.values.first
29
+ else
30
+ official_name.values.first
25
31
  end
26
32
  end
27
33
 
@@ -53,4 +59,4 @@ class Region < MLS::Model
53
59
  result
54
60
  end
55
61
 
56
- end
62
+ end
@@ -0,0 +1,63 @@
1
+ class Search < MLS::Model
2
+
3
+ STATUS_OPTIONS = %w(active hold cold closed)
4
+ STAGE_OPTIONS = %w(initiated contacted delivered connected toured loi signed coworking)
5
+ BUDGET_UNITS = %w(per_month per_year per_sqft_per_year)
6
+ TERMS = %w(<1 1-2 3-5 5+ flexible)
7
+ MOVE_INS = %w(<3 3-6 6-12 12+ flexible)
8
+
9
+ belongs_to :account
10
+ belongs_to :broker, class_name: "Account"
11
+ belongs_to :manager, class_name: "Account"
12
+ belongs_to :lead
13
+
14
+ has_many :suggestions
15
+ has_many :email_digests
16
+ has_many :tasks, :as => :subject
17
+
18
+ accepts_nested_attributes_for :account
19
+
20
+ def name
21
+ read_attribute(:name) || account&.company || account&.name
22
+ end
23
+
24
+ def to_json(options={})
25
+ output = super(options)
26
+ output = JSON.parse(super)
27
+ output["filter"] = read_attribute(:filter)
28
+ output.to_json
29
+ end
30
+
31
+ def filter
32
+ JSON.parse (read_attribute(:filter) || {}).to_json, object_class: OpenStruct
33
+ end
34
+
35
+ def regions
36
+ Region.where(id: self.region_ids)
37
+ end
38
+
39
+ def move_in_units(value=nil)
40
+ value ||= self.move_in
41
+ case value
42
+ when "<1"
43
+ "month"
44
+ when "flexible"
45
+ ""
46
+ else
47
+ "months"
48
+ end
49
+ end
50
+
51
+ def term_units(value=nil)
52
+ value ||= self.term
53
+ case value
54
+ when "<1"
55
+ "year"
56
+ when "flexible"
57
+ ""
58
+ else
59
+ "years"
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,11 @@
1
+ class Service < MLS::Model
2
+ self.inheritance_column = nil
3
+
4
+ belongs_to :subscription
5
+ belongs_to :subject, polymorphic: true
6
+
7
+ def name
8
+ self.type.humanize
9
+ end
10
+
11
+ end
@@ -0,0 +1,26 @@
1
+ class Site < MLS::Model
2
+
3
+ belongs_to :region
4
+ belongs_to :cover_photo, :class_name => 'Image'
5
+ belongs_to :logo, :class_name => 'Image'
6
+
7
+ has_many :ownerships, as: :asset
8
+ has_many :accounts, through: :ownerships, source: :account, inverse_of: :sites
9
+ has_many :services, as: :subject
10
+
11
+ def contacts
12
+ if ownerships.loaded?
13
+ @contacts ||= ownerships.select{|o| o.receives_inquiries }.map(&:account)
14
+ else
15
+ @contacts ||= ownerships.eager_load(:account).filter(:receives_inquiries => true).map(&:account)
16
+ end
17
+ end
18
+
19
+ def lead_contacts
20
+ if ownerships.loaded?
21
+ @lead_contacts ||= ownerships.select{|o| o.lead}.map(&:account)
22
+ else
23
+ @lead_contacts ||= ownerships.eager_load(:account).filter(:lead => true).map(&:account)
24
+ end
25
+ end
26
+ end
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,19 @@
1
+ class Subscription < MLS::Model
2
+ self.inheritance_column = nil
3
+
4
+ has_and_belongs_to_many :accounts
5
+ has_many :invoices
6
+ has_many :services
7
+ belongs_to :organization
8
+ belongs_to :billing_contact, class_name: "Account"
9
+ belongs_to :credit_card
10
+
11
+ has_and_belongs_to_many :invoice_recipients, class_name: 'EmailAddress'
12
+
13
+ accepts_nested_attributes_for :services
14
+
15
+ def cost
16
+ read_attribute(:cost) / 100.0 if read_attribute(:cost)
17
+ end
18
+
19
+ end
@@ -0,0 +1,13 @@
1
+ class Suggestion < MLS::Model
2
+
3
+ STATUS_OPTIONS = %w(proposed confirmed rejected)
4
+
5
+ belongs_to :search
6
+ belongs_to :listing
7
+ belongs_to :suggested_by, class_name: "Account"
8
+
9
+ def size
10
+ read_attribute(:size) || listing.size
11
+ end
12
+
13
+ end
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ class TimAlert < MLS::Model
2
+ self.inheritance_column = nil
3
+
4
+ belongs_to :account
5
+ belongs_to :lead
6
+
7
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,78 @@
1
+ require 'rack'
2
+ require 'rack/builder'
3
+
4
+ module MLS
5
+ module Rack
6
+ class Proxy
7
+
8
+ def self.new
9
+ app = super
10
+ ::Rack::Builder.new do
11
+ use(ActionDispatch::Cookies)
12
+ use(ActionDispatch::Session::CookieStore, {
13
+ key: Rails.application.config.session_options[:key],
14
+ path: '/',
15
+ secret: Rails.application.secrets[:secret_key_base]
16
+ })
17
+ run app
18
+ end.to_app
19
+ end
20
+
21
+ def extract_http_request_headers(env)
22
+ headers = env.reject do |k, v|
23
+ !(/^HTTP_[A-Z_]+$/ === k) || v.nil?
24
+ end.map do |k, v|
25
+ [k.sub(/^HTTP_/, "").gsub("_", "-"), v]
26
+ end.inject({}) do |hash, k_v|
27
+ k, v = k_v
28
+ hash[k] = v
29
+ hash
30
+ end
31
+
32
+ headers.delete_if {|key, value| ["HOST", "API-VERSION", "CONNECTION", "VERSION"].include?(key) }
33
+ x_forwarded_for = (headers["X-Forwarded-For"].to_s.split(/, +/) << env["REMOTE_ADDR"]).join(", ")
34
+
35
+ headers.merge!("X-Forwarded-For" => x_forwarded_for)
36
+ end
37
+
38
+ def call(env)
39
+ request = Net::HTTP.const_get(env['REQUEST_METHOD'].capitalize).new(env['PATH_INFO'] + '?' + env['QUERY_STRING'])
40
+ request.initialize_http_header(extract_http_request_headers(env))
41
+
42
+ if request.request_body_permitted?
43
+ if env['TRANSFER-ENCODING'] == 'chunked'
44
+ request.body_stream = env['rack.input']
45
+ elsif env['CONTENT_LENGTH']
46
+ request.body = env['rack.input'].read
47
+ end
48
+ end
49
+ request['Content-Type'] = env['CONTENT_TYPE']
50
+
51
+ response = MLS::Model.connection_pool.with_connection do |conn|
52
+ with_cookie_store(env) do
53
+ request_uri = "http#{conn.instance_variable_get(:@connection).use_ssl ? 's' : ''}://#{conn.instance_variable_get(:@connection).host}#{conn.instance_variable_get(:@connection).port != 80 ? (conn.instance_variable_get(:@connection).port == 443 && conn.instance_variable_get(:@connection).use_ssl ? '' : ":#{conn.instance_variable_get(:@connection).port}") : ''}#{request.path}"
54
+ request['Cookie'] = Thread.current[:sunstone_cookie_store].cookie_header_for(request_uri)
55
+ conn.instance_variable_get(:@connection).send(:request_headers).each { |k, v| request[k] = v if ['Api-Version', 'Api-Key'].include?(k) }
56
+
57
+ conn.instance_variable_get(:@connection).instance_variable_get(:@connection).request(request)
58
+ end
59
+ end
60
+ response_headers = {}
61
+ response.each_header { |k, v| response_headers[k] = v unless k == 'transfer-encoding' }
62
+
63
+ [response.code, response_headers, [response.body]]
64
+ end
65
+
66
+ def with_cookie_store(env)
67
+ store = CookieStore::HashStore.new
68
+ store.add_from_json(env['rack.session']['cookie_store']) if env['rack.session']['cookie_store']
69
+ Thread.current[:sunstone_cookie_store] = store
70
+ result = yield
71
+ Thread.current[:sunstone_cookie_store] = nil
72
+ env['rack.session']['cookie_store'] = store.to_json
73
+ result
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,32 @@
1
+ # See notes from 'active_record/railtie'
2
+ require "action_controller/railtie"
3
+
4
+ class MLS::Railtie < Rails::Railtie
5
+
6
+ config.mls = ActiveSupport::OrderedOptions.new
7
+
8
+ config.action_dispatch.rescue_responses.merge!(
9
+ "Sunstone::Exception::NotFound" => :not_found,
10
+ "Sunstone::Exception::Unauthorized" => :unauthorized,
11
+ "Sunstone::Exception::Gone" => :gone
12
+ )
13
+
14
+ initializer 'mls' do |app|
15
+
16
+ url = app.config.mls.fetch('url') { app.secrets.mls }
17
+ user_agent = []
18
+ user_agent << app.config.mls.fetch('user_agent') {
19
+ app.class.name.split('::')[0..-2].join('::')
20
+ }
21
+ user_agent << "Rails/#{Rails.version}"
22
+
23
+
24
+ MLS::Model.establish_connection({
25
+ adapter: 'sunstone',
26
+ url: url,
27
+ user_agent: user_agent.compact.join(' ')
28
+ })
29
+
30
+ end
31
+
32
+ end
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "mls"
6
- s.version = '1.5.1'
6
+ s.version = '1.6.0'
7
7
  s.authors = ["Jon Bracy", "James R. Bracy"]
8
8
  s.email = ["jon@42floors.com", "james@42floors.com"]
9
9
  s.homepage = "http://mls.42floors.com"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mls
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-04-07 00:00:00.000000000 Z
12
+ date: 2018-01-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -239,7 +239,8 @@ description: Ruby library for integrating with the 42Floors MLS
239
239
  email:
240
240
  - jon@42floors.com
241
241
  - james@42floors.com
242
- executables: []
242
+ executables:
243
+ - mls
243
244
  extensions: []
244
245
  extra_rdoc_files: []
245
246
  files:
@@ -247,53 +248,64 @@ files:
247
248
  - Gemfile
248
249
  - README.rdoc
249
250
  - Rakefile
251
+ - bin/mls
250
252
  - lib/mls.rb
251
- - lib/mls/account.rb
252
- - lib/mls/accounts_region.rb
253
- - lib/mls/action.rb
254
- - lib/mls/address.rb
255
- - lib/mls/api_key.rb
253
+ - lib/mls/cli.rb
254
+ - lib/mls/cli/documents.rb
255
+ - lib/mls/cli/storage.rb
256
+ - lib/mls/cli/storage/s3.rb
256
257
  - lib/mls/comment.rb
257
- - lib/mls/coworking_space.rb
258
- - lib/mls/credit_card.rb
259
- - lib/mls/datum.rb
260
- - lib/mls/document.rb
261
- - lib/mls/email.rb
262
- - lib/mls/email_address.rb
263
- - lib/mls/email_digest.rb
264
- - lib/mls/event.rb
265
- - lib/mls/flyer.rb
266
- - lib/mls/geometry.rb
267
- - lib/mls/image_ordering.rb
268
- - lib/mls/impression_count.rb
269
- - lib/mls/inquiry.rb
270
- - lib/mls/invoice.rb
271
- - lib/mls/lead.rb
272
- - lib/mls/listing.rb
273
- - lib/mls/locality.rb
274
- - lib/mls/membership.rb
275
- - lib/mls/metadatum.rb
276
- - lib/mls/mistake.rb
277
- - lib/mls/organization.rb
278
- - lib/mls/ownership.rb
279
- - lib/mls/phone.rb
280
- - lib/mls/property.rb
281
- - lib/mls/recommendation.rb
282
- - lib/mls/reference.rb
283
- - lib/mls/region.rb
284
- - lib/mls/session.rb
285
- - lib/mls/slug.rb
286
- - lib/mls/source.rb
287
- - lib/mls/space.rb
288
- - lib/mls/stat.rb
289
- - lib/mls/subscription.rb
290
- - lib/mls/task.rb
291
- - lib/mls/team.rb
292
- - lib/mls/time_log.rb
293
- - lib/mls/use.rb
294
- - lib/mls/vendor.rb
295
- - lib/mls/view.rb
296
- - lib/mls/webpage.rb
258
+ - lib/mls/models/account.rb
259
+ - lib/mls/models/accounts_region.rb
260
+ - lib/mls/models/action.rb
261
+ - lib/mls/models/address.rb
262
+ - lib/mls/models/api_key.rb
263
+ - lib/mls/models/coworking_space.rb
264
+ - lib/mls/models/credit_card.rb
265
+ - lib/mls/models/datum.rb
266
+ - lib/mls/models/document.rb
267
+ - lib/mls/models/email.rb
268
+ - lib/mls/models/email_address.rb
269
+ - lib/mls/models/email_digest.rb
270
+ - lib/mls/models/event.rb
271
+ - lib/mls/models/flyer.rb
272
+ - lib/mls/models/geometry.rb
273
+ - lib/mls/models/image_ordering.rb
274
+ - lib/mls/models/impression_count.rb
275
+ - lib/mls/models/inquiry.rb
276
+ - lib/mls/models/invoice.rb
277
+ - lib/mls/models/lead.rb
278
+ - lib/mls/models/listing.rb
279
+ - lib/mls/models/locality.rb
280
+ - lib/mls/models/metadatum.rb
281
+ - lib/mls/models/mistake.rb
282
+ - lib/mls/models/organization.rb
283
+ - lib/mls/models/ownership.rb
284
+ - lib/mls/models/phone.rb
285
+ - lib/mls/models/property.rb
286
+ - lib/mls/models/recommendation.rb
287
+ - lib/mls/models/reference.rb
288
+ - lib/mls/models/region.rb
289
+ - lib/mls/models/search.rb
290
+ - lib/mls/models/service.rb
291
+ - lib/mls/models/session.rb
292
+ - lib/mls/models/site.rb
293
+ - lib/mls/models/slug.rb
294
+ - lib/mls/models/source.rb
295
+ - lib/mls/models/space.rb
296
+ - lib/mls/models/stat.rb
297
+ - lib/mls/models/subscription.rb
298
+ - lib/mls/models/suggestion.rb
299
+ - lib/mls/models/task.rb
300
+ - lib/mls/models/team.rb
301
+ - lib/mls/models/tim_alert.rb
302
+ - lib/mls/models/time_log.rb
303
+ - lib/mls/models/use.rb
304
+ - lib/mls/models/vendor.rb
305
+ - lib/mls/models/view.rb
306
+ - lib/mls/models/webpage.rb
307
+ - lib/mls/rack/proxy.rb
308
+ - lib/mls/railtie.rb
297
309
  - mls.gemspec
298
310
  - test/fixtures/flyer.pdf
299
311
  - test/mls/attribute_test.rb
@@ -319,7 +331,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
319
331
  version: '0'
320
332
  requirements: []
321
333
  rubyforge_project: mls
322
- rubygems_version: 2.5.1
334
+ rubygems_version: 2.6.11
323
335
  signing_key:
324
336
  specification_version: 4
325
337
  summary: 42Floors MLS Client
@@ -1,18 +0,0 @@
1
- class Action < MLS::Model
2
- self.inheritance_column = nil
3
-
4
- belongs_to :event
5
- belongs_to :subject, :polymorphic => true
6
-
7
- has_many :mistakes
8
- has_many :metadata, foreign_key: :event_id, primary_key: :event_id
9
-
10
- def self.by_performer(filter)
11
- req = Net::HTTP::Get.new("/actions/by_performer")
12
- req.body = {
13
- where: filter
14
- }.to_json
15
- JSON.parse(connection.instance_variable_get(:@connection).send_request(req).body)
16
- end
17
-
18
- end
@@ -1,10 +0,0 @@
1
- class EmailDigest < MLS::Model
2
-
3
- belongs_to :account
4
- accepts_nested_attributes_for :account
5
-
6
- def filter
7
- (read_attribute(:filter) || {}).with_indifferent_access
8
- end
9
-
10
- end
@@ -1,6 +0,0 @@
1
- class Lead < MLS::Model
2
-
3
- belongs_to :account
4
- belongs_to :inquiry
5
-
6
- end
@@ -1,27 +0,0 @@
1
- class Membership < MLS::Model
2
- self.inheritance_column = nil
3
-
4
- has_many :accounts
5
- has_many :invoices
6
- has_many :subscriptions
7
- belongs_to :organization
8
- belongs_to :billing_contact, class_name: "Account"
9
- belongs_to :credit_card
10
- belongs_to :sourced_by, class_name: "Account"
11
- has_and_belongs_to_many :invoice_recipients, class_name: 'EmailAddress'
12
-
13
- accepts_nested_attributes_for :subscriptions
14
-
15
- def rate
16
- subscriptions.select{|x| !x.ends_at}.map(&:cost).compact.sum
17
- end
18
-
19
- def costs
20
- (read_attribute(:costs) || {}).with_indifferent_access
21
- end
22
-
23
- def value
24
- read_attribute(:value) / 100.0 if read_attribute(:value)
25
- end
26
-
27
- end
@@ -1,24 +0,0 @@
1
- class Subscription < MLS::Model
2
- self.inheritance_column = nil
3
-
4
- belongs_to :membership
5
- belongs_to :subject, polymorphic: true
6
- belongs_to :credit_card
7
-
8
- def name
9
- case self.type
10
- when "unlimited"
11
- "Unlimited Premium Listings"
12
- when "premium"
13
- "Premium Listings"
14
- when "elite"
15
- "Elite Account"
16
- when "coworking"
17
- "Coworking Space"
18
- end
19
- end
20
-
21
- def cost
22
- read_attribute(:cost) / 100.0 if read_attribute(:cost)
23
- end
24
- end