mls 1.5.1 → 1.6.0

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