diff --git a/Gemfile b/Gemfile index 8045c187c..7113e8343 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,9 @@ gem 'rails', '~> 4.2.0' # must use this version of mysql2 for rails 4.0.0 gem 'mysql2', '~> 0.3.18' -gem 'validates_overlap' +gem 'redis' # ephemeral storage. used for expiring wit.ai contexts + +gem 'validates_overlap' # to ensure we don't double book people gem 'mail', '2.6.3' @@ -136,6 +138,9 @@ gem 'aasm' # cron jobs for backups and sending reminders gem 'whenever', require: false +# natural language processing API +gem 'wit' + group :testing do # mock tests w/mocha gem 'mocha', require: false @@ -162,6 +167,9 @@ group :testing do # webrick is slow, capybara will use puma instead gem 'puma' + + # in memory redis for testing only + gem 'mock_redis' end group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index de2541ff3..5994202b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -224,6 +224,7 @@ GEM minitest (5.8.4) mocha (1.1.0) metaclass (~> 0.0.1) + mock_redis (0.16.1) multi_json (1.11.2) multi_xml (0.5.5) multipart-post (2.0.0) @@ -307,6 +308,7 @@ GEM rb-fsevent (0.9.7) rb-inotify (0.9.5) ffi (>= 0.5.0) + redis (3.3.0) ref (2.0.0) responders (2.1.1) railties (>= 4.2.0, < 5.1) @@ -449,6 +451,7 @@ GEM will_paginate (3.1.0) will_paginate-bootstrap (0.2.5) will_paginate (>= 3.0.3) + wit (3.3.1) wuparty (1.2.6) httparty (>= 0.6.1) mime-types (~> 1.16) @@ -497,6 +500,7 @@ DEPENDENCIES memory_profiler memory_test_fix mocha + mock_redis mysql2 (~> 0.3.18) newrelic_rpm phony_rails @@ -507,6 +511,7 @@ DEPENDENCIES quiet_assets rack-mini-profiler rails (~> 4.2.0) + redis rspec-rails (~> 3.0) rspec-retry rubocop @@ -533,7 +538,8 @@ DEPENDENCIES whenever will_paginate will_paginate-bootstrap (~> 0.2.5) + wit wuparty BUNDLED WITH - 1.12.2 + 1.12.3 diff --git a/Vagrantfile b/Vagrantfile index 95cc59c37..d5fed0a54 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -12,7 +12,7 @@ Vagrant.configure(2) do |config| # Every Vagrant development environment requires a box. You can search for # boxes at https://atlas.hashicorp.com/search. - config.vm.box = "ubuntu/trusty64" + config.vm.box = 'ubuntu/trusty64' # config.vm.network "forwarded_port", guest: 80, host: 3000 @@ -23,7 +23,7 @@ Vagrant.configure(2) do |config| config.cache.scope = :box config.cache.enable :generic - if !Gem.win_platform? + unless Gem.win_platform? # passwordless nfs # https://gist.github.com/cromulus/5044b9558319769aaf0b config.cache.synced_folder_opts = { @@ -33,7 +33,7 @@ Vagrant.configure(2) do |config| end else puts "please run 'vagrant plugin install vagrant-cachier'" - puts "It will make vagrant substantially faster" + puts 'It will make vagrant substantially faster' end if Vagrant.has_plugin?('vagrant-hostmanager') @@ -52,7 +52,7 @@ Vagrant.configure(2) do |config| config.hostmanager.manage_host = true else puts "run 'vagrant plugin install vagrant-hostmanager'" - puts "It will help you find your dev environment!" + puts 'It will help you find your dev environment!' end # Provider-specific configuration so you can fine-tune various @@ -60,7 +60,7 @@ Vagrant.configure(2) do |config| # Example for VirtualBox: # - config.vm.provider "virtualbox" do |vb, override| + config.vm.provider 'virtualbox' do |vb, override| # Don't display the VirtualBox GUI when booting the machine vb.gui = false @@ -74,7 +74,7 @@ Vagrant.configure(2) do |config| # https://gist.github.com/cromulus/5044b9558319769aaf0b # also this one: https://gist.github.com/GUI/2864683 override.vm.synced_folder '.', '/vagrant', type: 'nfs' - override.vm.network "private_network", ip: "192.168.33.124" + override.vm.network 'private_network', ip: '192.168.33.124' end # View the documentation for the provider you are using for more @@ -91,8 +91,8 @@ Vagrant.configure(2) do |config| # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the # documentation for more information about their specific syntax and use. - #splitting shell provisioning for caching benefits. - config.vm.provision "shell", privileged: false, inline: %[ + # splitting shell provisioning for caching benefits. + config.vm.provision :shell, privileged: false, inline: %( sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password password' sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password' @@ -121,24 +121,24 @@ Vagrant.configure(2) do |config| sudo service elasticsearch start; # automatically cd to /vagrant/ echo 'if [ -d /vagrant/ ]; then cd /vagrant/; fi' >> /home/vagrant/.bashrc - ] + ) - config.vm.provision :shell, privileged: false, inline: %[ + config.vm.provision :shell, privileged: false, inline: %( echo 'gem: --no-rdoc --no-ri' | sudo tee /etc/gemrc; # rvm install is idempotent curl -sSL https://rvm.io/mpapis.asc | gpg --import - curl -sSL https://get.rvm.io | bash -s stable --auto-dotfiles source ~/.profile - ] + ) - config.vm.provision :shell, privileged: false, inline: %[ + config.vm.provision :shell, privileged: false, inline: %( # cleanup and install the appropriate ruby version source ~/.profile rvm reload rvm use --default install `cat /vagrant/.ruby-version` rvm cleanup all - ] - config.vm.provision :shell, privileged: false, inline: %[ + ) + config.vm.provision :shell, privileged: false, inline: %( # setup our particular rails app source ~/.profile cd /vagrant/ @@ -156,6 +156,5 @@ Vagrant.configure(2) do |config| sudo mkdir -p /var/run/nginx/tmp sudo chown -R www-data:www-data /var/run/nginx/ sudo service nginx restart - ] - + ) end diff --git a/app/controllers/twilio_messages_controller.rb b/app/controllers/twilio_messages_controller.rb index 50690e859..eb9241959 100644 --- a/app/controllers/twilio_messages_controller.rb +++ b/app/controllers/twilio_messages_controller.rb @@ -93,11 +93,13 @@ def new @twilio_message = TwilioMessage.new end + # this is the callback from twilio about the message and it's delivery # POST /twilio_messages/updatestatus def updatestatus this_message = TwilioMessage.find_by message_sid: params['MessageSid'] this_message.status = params['MessageStatus'] this_message.error_code = params['ErrorCode'] + this_message.error_message = params['ErrorMessage'] this_message.save end diff --git a/app/controllers/v2/event_invitations_controller.rb b/app/controllers/v2/event_invitations_controller.rb index 2474fe7ee..ae47be542 100644 --- a/app/controllers/v2/event_invitations_controller.rb +++ b/app/controllers/v2/event_invitations_controller.rb @@ -78,7 +78,8 @@ def send_email(person, event) end def send_sms(person, event) - ::EventInvitationSms.new(to: person, event: event).send + # we send a bunch at once, delay it. Plus this has extra logic + Delayed::Job.enqueue SendEventInvitationsSmsJob.new(person, event) end # TODO: add a nested :event diff --git a/app/controllers/v2/sms_reservations_controller.rb b/app/controllers/v2/sms_reservations_controller.rb index 137fb6e27..66fd7ddba 100644 --- a/app/controllers/v2/sms_reservations_controller.rb +++ b/app/controllers/v2/sms_reservations_controller.rb @@ -1,5 +1,4 @@ -# FIXME: Refactor and re-enable cop -# rubocop:disable ClassLength +# FIXME: Refactor class V2::SmsReservationsController < ApplicationController skip_before_action :verify_authenticity_token, only: [:create] skip_before_action :authenticate_user! @@ -13,18 +12,11 @@ def create # should do sms verification here if unverified # FIXME: this needs a refactor badly. - if letters_and_numbers_only? # they are trying to accept! - reservation = V2::Reservation.new(generate_reservation_params) - if reservation.save - send_new_reservation_notifications(person, reservation) - else - resend_available_slots(person, event) - end - elsif remove? + if remove? # do the remove people thing. person.deactivate! elsif declined? # currently not used. - send_decline_notifications(person, event) + #send_decline_notifications(person, event) elsif confirm? # confirmation for the days reservations if person.v2_reservations.for_today_and_tomorrow.size > 0 person.v2_reservations.for_today_and_tomorrow.each(&:confirm!) @@ -46,7 +38,14 @@ def create elsif calendar? ::ReservationReminderSms.new(to: person, reservations: person.v2_reservations.for_today_and_tomorrow).send else - send_error_notification && return + str_context = Redis.current.get("wit_context:#{person.id}") + + # we don't know what event_id we're talking about here + send_error_notification && return if str_context.nil? + context = JSON.parse(str_context) + new_context = ::WitClient.run_actions "#{person.id}_#{context['event_id']}", message, context + Redis.current.set("wit_context:#{person.id}", new_context.to_json) + Redis.current.expire("wit_context:#{person.id}", 3600) end render text: 'OK' end @@ -62,21 +61,6 @@ def person @person ||= Person.find_by(phone_number: sms_params[:From]) end - # TODO: need to handle more than 26 slots - def selection - slot_letter = message.downcase.delete('^a-z') - # "a".ord - ("A".ord + 32) == 0 - # "b".ord - ("A".ord + 32) == 1 - # (0 + 97).chr == a - # (1 + 97).chr == b - slot_letter.ord - ('A'.ord + 32) - end - - def event - event_id = message.delete('^0-9').to_i - @event ||= V2::Event.includes(:event_invitation, :user, :time_slots).find_by(id: event_id) - end - def event_invitation @event_invitation ||= event.event_invitation end @@ -85,18 +69,6 @@ def user @user ||= event_invitation.user end - def time_slot - @event.time_slots[selection] - end - - def generate_reservation_params - { user: user, - person: person, - event: event, - event_invitation: event_invitation, - time_slot: time_slot } - end - def send_new_reservation_notifications(person, reservation) ::ReservationSms.new(to: person, reservation: reservation).send ReservationNotifier.notify(email_address: reservation.user.email, reservation: reservation).deliver_later @@ -107,7 +79,9 @@ def send_decline_notifications(person, event) end def send_error_notification - ::InvalidOptionSms.new(to: sms_params[:From]).send + # awkward, yes, but see application_sms to understand why + phone_struct = Struct.new(:phone_number).new(sms_params[:From]) + ::InvalidOptionSms.new(to: phone_struct).send render text: 'OK' end @@ -142,12 +116,6 @@ def remove? message.downcase.include?('remove') end - def letters_and_numbers_only? - # this is for accepting only. many messages now won't pass. - # up to 10k events - message.downcase =~ /\b\d{1,5}[a-z]\b/ - end - def sms_params params.permit(:From, :Body) end @@ -155,17 +123,14 @@ def sms_params def twilio_params res = {} params.permit(:From, :To, :Body, :MessageSid, :DateCreated, :DateUpdated, :DateSent, :AccountSid, :WufooFormid, :SmsStatus, :FromZip, :FromCity, - :FromState, :ErrorCode, :ErrorMessage).to_unsafe_hash.keys do |k, v| - # behold the horror + :FromState, :ErrorCode, :ErrorMessage, :Direction, :AccountSid).to_unsafe_hash.keys do |k, v| + # behold the horror of translating twilio params to rails attributes res[k.gsub!(/(.)([A-Z])/, '\1_\2').downcase] = v end res end def save_twilio_message - tm = TwilioMessage.new(twilio_params) - tm.direction = 'twiml-incoming' - tm.save + TwilioMessage.create(twilio_params) end end -# rubocop:enable ClassLength diff --git a/app/jobs/send_event_invitations_sms_job.rb b/app/jobs/send_event_invitations_sms_job.rb new file mode 100644 index 000000000..20ec7182b --- /dev/null +++ b/app/jobs/send_event_invitations_sms_job.rb @@ -0,0 +1,75 @@ +# app/jobs/twilio/send_messages.rb +# +# module TwilioSender +# Send twilio messages to a list of phone numbers +# +# FIXME: Refactor and re-enable cop +# rubocop:disable Style/StructInheritance +# +class SendEventInvitationsSmsJob < Struct.new(:to, :event) + + def enqueue(job) + # job.delayed_reference_id = + # job.delayed_reference_type = '' + Rails.logger.info '[TwilioSender] job enqueued' + job.save! + end + + def max_attempts + 1 + end + + def perform + # step 1: check to see if we already have a context for the person + # yes: get ttl and re-enque for after ttl + # no: go to step 2 + # step 2: check if we are after hours + # yes: requeue for 8:30am + # no: set context with expire and send! + + + # context = Redis.current.get("wit_context:#{to.id}") + # if context.nil? && !time_requeue? # no context, free to send + # context is symbols here, but will be string keys after json. + context = { person_id: to.id, event_id: event.id, state: 'yes_no' }.to_json + Redis.current.set("wit_context:#{to.id}", context) + Redis.current.expire("wit_context:#{to.id}", 7200) # two hours + EventInvitationSms.new(to: to, event: event).send + # elsif time_requeue? + # Delayed::Job.enqueue(SendEventInvitationsSmsJob.new(to, event), run_at: run_in_business_hours) + # else # we have a context, wait till we time out. + # ttl = Redis.current.ttl("wit_context:#{to.id}") # ttl is in seconds + # requeue_at = Time.current + ttl.seconds + # Delayed::Job.enqueue(SendEventInvitationsSmsJob.new(to, event), run_at: requeue_at) + # end + sleep(1) # twilio rate limiting. + end + + def before(job) + end + + def after(job) + end + + def success(job) + end + + private + + def time_requeue? + # yes if before 8:30am and yes if after 8pm + return true if Time.current < DateTime.current.change({ hour: 8, minute: 30 }) + return true if Time.current > DateTime.current.change({ hour: 20, minute: 0 }) + + false + end + + def run_in_business_hours # different run_at times + if Time.current > Time.zone.parse('20:00') + DateTime.tomorrow.change({ hour: 8, minute: 30 }) + elsif Time.current < Time.zone.parse('8:00') + DateTime.current.change({ hour: 8, minute: 30 }) + end + end +end +# rubocop:enable Style/StructInheritance diff --git a/app/jobs/send_twilio_messages_job.rb b/app/jobs/send_twilio_messages_job.rb index e83fdd36b..bcf194b02 100644 --- a/app/jobs/send_twilio_messages_job.rb +++ b/app/jobs/send_twilio_messages_job.rb @@ -48,7 +48,7 @@ def perform @outgoing.body = message @outgoing.from = ENV['TWILIO_SURVEY_NUMBER'].gsub('+1', '').delete('-') @outgoing.wufoo_formid = smsCampaign - # @incoming.direction = "incoming-twiml" + @outgoint.direction = 'outgoing-survey' @outgoing.save phone_number = '+1' + phone_number.strip.gsub('+1', '').delete('-') @@ -68,7 +68,7 @@ def perform Rails.logger.warn("[Twilio][SendTwilioMessagesJob] had a problem. Full error: #{@outgoing.error_message}") end end - sleep(1) + sleep(1) # for twilio rate limiting, I presume. end end end diff --git a/app/models/concerns/calendarable.rb b/app/models/concerns/calendarable.rb index 946ab9796..d2d894289 100644 --- a/app/models/concerns/calendarable.rb +++ b/app/models/concerns/calendarable.rb @@ -41,6 +41,10 @@ def slot_time_human "#{start_datetime.strftime('%l:%M%p')}-#{end_datetime.strftime('%l:%M%p, %a %b')} #{start_datetime.strftime('%d').to_i.ordinalize}" end + def bot_duration + "#{start_datetime.strftime('%A')} from #{start_datetime.strftime('%l:%M%p')} to #{end_datetime.strftime('%l:%M%p')}" + end + private def cal_description diff --git a/app/models/concerns/neighborhoods.rb b/app/models/concerns/neighborhoods.rb new file mode 100644 index 000000000..ff2c4aa64 --- /dev/null +++ b/app/models/concerns/neighborhoods.rb @@ -0,0 +1,152 @@ +module Neighborhoods + + CHICAGO_NEIGHBORHOODS = { + 'Cathedral District' => [60611], + 'Central Station' => [60605], + 'Dearborn Park' => [60605], + 'Gold Coast' => [60610, 60611], + 'Loop' => [60601, 60602, 60603, 60604, 60605, 60606, 60607, 60616], + 'Magnificent Mile' => [60611], + 'Museum Campus' => [60605], + 'Near North Side' => [60610, 60611, 60642, 60654], + 'Near West Side' => [60606, 60607, 60608, 60610, 60612, 60661], + 'New East Side' => [60601], + 'Noble Square' => [60622], + 'Old Town' => [60610], + 'Printers Row' => [60605], + 'Randolph Market' => [60607, 60661], + 'River East' => [60611], + 'River North' => [60611, 60654], + 'River West' => [60622, 60610], + 'South Loop' => [60605, 60607, 60608, 60616], + 'Streeterville' => [60611], + 'Tri-Taylor' => [60612], + 'Ukrainian Village' => [60622, 60612], + 'West Loop' => [60607], + 'West Town' => [60612, 60622, 60642, 60647], + 'Wicker Park' => [60622], + 'Alta Vista Terrace' => [60613], + 'Belmont Harbor' => [60657], + 'Boys Town' => [60613, 60657], + 'Bucktown' => [60647, 60622, 60614], + 'DePaul' => [60614], + 'Lakeview' => [60657, 60613], + 'Lakeview Central' => [60657], + 'Lakeview East' => [60657, 60613], + 'Lincoln Park' => [60614, 60610], + 'Lincoln Square' => [60625], + 'North Center' => [60618, 60613], + 'North Halsted' => [60613, 60657], + 'Old Town Triangle' => [60614], + 'Park West' => [60614], + 'Ranch Triangle' => [60614], + 'Roscoe Village' => [60618, 60657], + 'Sheffield' => [60614], + 'Uptown' => [60640], + 'West DePaul' => [60614], + 'Wrightwood Neighbors' => [60614], + 'Wrigleyville' => [60613], + 'Andersonville' => [60640], + 'Budlong Woods' => [60625], + 'Buena Park' => [60613, 60640], + 'East Ravenswood' => [60613, 60640], + 'Edgewater' => [60640, 60660], + 'Edgewater Glen' => [60660, 60640], + 'Edison Park' => [60631], + 'Middle Edgebrook' => [60630, 60646], + 'North Edgebrook' => [60630, 60646], + 'Old Irving Park' => [60641], + 'Peterson Park' => [60659], + 'Ravenswood' => [60640, 60625, 60613], + 'West Ridge' => [60645, 60659], + 'West Rogers Park' => [60645, 60659, 60660], + 'Albany Park' => [60625], + 'Avondale' => [60618], + 'Belmont Gardens' => [60641, 60639], + 'Forest Glen' => [60630], + 'Irving Park' => [60618], + 'Jefferson Park' => [60630], + 'Mayfair' => [60630], + 'North Park' => [60625], + 'Norwood Park' => [60631], + 'Old Edgebrook' => [60646], + 'Old Norwood Park' => [60631], + 'Portage Park' => [60634, 60641], + 'Ravenswood Manor' => [60625], + 'Sauganash' => [60646, 60630], + 'Sauganash Woods' => [60630], + 'Schorsch Forest View' => [60656], + 'South Edgebrook' => [60646], + 'Union Ridge' => [60656] + }.freeze + + NYC_NEIGHBORHOODS = { + 'Central Bronx' => [10453, 10457, 10460], + 'Bronx Park and Fordham' => [10458, 10467, 10468], + 'High Bridge and Morrisania' => [10451, 10452, 10456], + 'Hunts Point and Mott Haven' => [10454, 10455, 10459, 10474], + 'Kingsbridge and Riverdale' => [10463, 10471], + 'Northeast Bronx' => [10466, 10469, 10470, 10475], + 'Southeast Bronx' => [10461, 10462, 10464, 10465, 10472, 10473], + 'Central Brooklyn' => [11213, 11216, 11238], + 'Southwest Brooklyn' => [11209, 11214, 11228], + 'Borough Park' => [11204, 11218, 11219, 11230], + 'Canarsie' => [11234, 11236], + 'Southern Brooklyn' => [11223, 11224, 11229, 11235], + 'Northwest Brooklyn' => [11201, 11205, 11215, 11217, 11231], + 'Flatbush' => [11203, 11210, 11225, 11226], + 'Brownsville' => [11233, 11212], + 'East New York and New Lots' => [11207, 11208, 11239], + 'Greenpoint' => [11222], + 'Sunset Park' => [11220, 11232], + 'Williamsburg' => [11211], + 'Bushwick' => [11206, 11221, 11237], + 'Central Harlem' => [10026, 10027, 10030, 10037, 10039], + 'Chelsea and Clinton' => [10001, 10011, 10018, 10019, 10020, 10036], + 'East Harlem' => [10029, 10035], + 'Gramercy Park and Murray Hill' => [10010, 10016, 10017, 10022], + 'Greenwich Village and Soho' => [10012, 10013, 10014], + 'Lower Manhattan' => [10004, 10005, 10006, 10007, 10038, 10280], + 'Lower East Side' => [10002, 10003, 10009], + 'Upper East Side' => [10021, 10028, 10044, 10065, 10075, 10128], + 'Upper West Side' => [10023, 10024, 10025], + 'Inwood and Washington Heights' => [10031, 10032, 10033, 10034, 10040], + 'Northeast Queens' => [11361, 11362, 11363, 11364], + 'North Queens' => [11354, 11355, 11356, 11357, 11358, 11359, 11360], + 'Central Queens' => [11365, 11366, 11367], + 'Jamaica' => [11412, 11423, 11432, 11433, 11434, 11435, 11436], + 'Northwest Queens' => [11101, 11102, 11103, 11104, 11105, 11106], + 'West Central Queens' => [11374, 11375, 11379, 11385], + 'Rockaways' => [11691, 11692, 11693, 11694, 11695, 11697], + 'Southeast Queens' => [11004, 11005, 11411, 11413, 11422, 11426, 11427, 11428, 11429], + 'Southwest Queens' => [11414, 11415, 11416, 11417, 11418, 11419, 11420, 11421], + 'West Queens' => [11368, 11369, 11370, 11372, 11373, 11377, 11378], + 'Port Richmond' => [10302, 10303, 10310], + 'South Shore' => [10306, 10307, 10308, 10309, 10312], + 'Stapleton and St. George' => [10301, 10304, 10305], + 'Mid-Island' => [10314] }.freeze + + def zip_to_neighborhood(zip) + res = select_neighborhood_mapping.select { |k, v| k if v.include?(zip.to_i) } + return res.keys[0] if res + end + + def neighborhood_to_zip(neighborhood) + res = select_neighborhood_mapping.select { |k, _v| k == neighborhood } + res.values[0] if res + end + + private + + def select_neighborhood_mapping + case ENV['NEIGHBORHOOD'] + when 'CHICAGO' + CHICAGO_NEIGHBORHOODS + when 'NEW_YORK' + NYC_NEIGHBORHOODS + else + NYC_NEIGHBORHOODS + end + end + +end diff --git a/app/models/person.rb b/app/models/person.rb index 45808d70e..6699bcaf9 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -39,6 +39,7 @@ class Person < ActiveRecord::Base default_scope { where(active: true) } # quick deactivations include Searchable include ExternalDataMappings + include Neighborhoods phony_normalize :phone_number, default_country_code: 'US' phony_normalized_method :phone_number, default_country_code: 'US' @@ -316,7 +317,7 @@ def deactivate(method = nil) end def update_neighborhood - n = zip_to_neighborhood(postal_code) + n = Neighborhood.zip_to_neighborhood(postal_code) unless n.blank? self.neighborhood = n save diff --git a/app/sms/application_sms.rb b/app/sms/application_sms.rb index 221184286..37102bef1 100644 --- a/app/sms/application_sms.rb +++ b/app/sms/application_sms.rb @@ -1,8 +1,11 @@ class ApplicationSms - attr_reader :client, :application_number + attr_reader :client, :application_number, :to - include Rails.application.routes.url_helpers # need to generate urls here. + # need to generate urls here. + include Rails.application.routes.url_helpers + + # how we figure out the right host host = ENV["#{Rails.env.upcase}_SERVER"].blank? ? 'localhost' : ENV["#{Rails.env.upcase}_SERVER"] Rails.application.routes.default_url_options[:host] = host @@ -14,9 +17,33 @@ def initialize(*) @application_number = ENV['TWILIO_SCHEDULING_NUMBER'] end - def slot_id_to_char(id) - raise ArgumentError if id >= 26 - (id + 97).chr + def send + res = @client.messages.create( + from: @application_number, + to: @to.phone_number, + body: body + ) + + # log this outgoing message. + # sms spec currently doesn't match twilio's return object + TwilioMessage.create(twilio_params(res)) if Rails.env != 'test' + end + + def body + # this must be implemented in every subclass + raise 'SubclassResponsibility' end + private + + def twilio_params(res) + { from: @application_number, + to: @to.phone_number, + body: body, + direction: "outbound-api", + date_sent: Time.current, + message_sid: res.sid, + account_sid: res.account_sid, + status: res.status } + end end diff --git a/app/sms/decline_invitation_sms.rb b/app/sms/decline_invitation_sms.rb index cb529e7fe..f62ea07e4 100644 --- a/app/sms/decline_invitation_sms.rb +++ b/app/sms/decline_invitation_sms.rb @@ -9,14 +9,6 @@ def initialize(to:, event:) @event = event end - def send - client.messages.create( - from: application_number, - to: to.phone_number, - body: body - ) - end - private def body diff --git a/app/sms/event_invitation_sms.rb b/app/sms/event_invitation_sms.rb index b10b537f0..95bdb5e94 100644 --- a/app/sms/event_invitation_sms.rb +++ b/app/sms/event_invitation_sms.rb @@ -10,25 +10,12 @@ def initialize(to:, event:) @event = event end - def send - client.messages.create( - from: application_number, - to: to.phone_number, - body: body - ) + # TODO: Chunk this into 160 characters and send individualls + def body + body = "#{event.description}\n" + body << "If you're available for #{event.duration / 60} minutes" + body << ' during that time please' + body << " text back 'Yes'. If not, 'No'\n" + body << "You can text 'remove me' to unsubscribe" end - - private - - # TODO: Chunk this into 160 characters and send individualls - def body - body = "#{event.description}\n" - body << "If you're available please " - body << " text back the number and letter of the time?\n\n" - event.available_time_slots(to).each_with_index do |slot, i| - body << "'#{event.id}#{slot_id_to_char(i)}' for #{slot.start_datetime_human}\n" - end - body << "If none of these times work, just ignore this.\n" - body << "text 'remove me' to unsubscribe" - end end diff --git a/app/sms/invalid_option_sms.rb b/app/sms/invalid_option_sms.rb index 27e546673..1b03e7085 100644 --- a/app/sms/invalid_option_sms.rb +++ b/app/sms/invalid_option_sms.rb @@ -8,11 +8,7 @@ def initialize(to:) @to = to end - def send - client.messages.create( - from: application_number, - to: to, - body: "Sorry, I didn't understand that! I'm just a computer..." - ) + def body + "Sorry, I didn't understand that! I'm just a computer..." end end diff --git a/app/sms/reservation_cancel_sms.rb b/app/sms/reservation_cancel_sms.rb index 4ee7bbcf7..3966e5e1e 100644 --- a/app/sms/reservation_cancel_sms.rb +++ b/app/sms/reservation_cancel_sms.rb @@ -9,20 +9,15 @@ def initialize(to:, reservation:) @reservation = reservation end - def send - client.messages.create( - from: application_number, - to: to.phone_number, - body: "The #{duration} minute interview for #{selected_time}, with #{reservation.user.name} has been cancelled") + def body + "The #{duration} minute interview for #{selected_time}, with #{reservation.user.name} has been cancelled" end - private - - def selected_time - reservation.time_slot.start_datetime_human - end + def selected_time + reservation.time_slot.start_datetime_human + end - def duration - reservation.duration / 60 - end + def duration + reservation.duration / 60 + end end diff --git a/app/sms/reservation_confirm_sms.rb b/app/sms/reservation_confirm_sms.rb index 6d2a4f5cc..c60bbaee4 100644 --- a/app/sms/reservation_confirm_sms.rb +++ b/app/sms/reservation_confirm_sms.rb @@ -9,20 +9,15 @@ def initialize(to:, reservation:) @reservation = reservation end - def send - client.messages.create( - from: application_number, - to: to.phone_number, - body: "You have confirmed a #{duration} minute interview for #{selected_time}, with #{reservation.user.name}. \nTheir number is #{reservation.user.phone_number}") + def body + "You have confirmed a #{duration} minute interview for #{selected_time}, with #{reservation.user.name}. \nTheir number is #{reservation.user.phone_number}" end - private - - def selected_time - reservation.time_slot.start_datetime_human - end + def selected_time + reservation.time_slot.start_datetime_human + end - def duration - reservation.duration / 60 - end + def duration + reservation.duration / 60 + end end diff --git a/app/sms/reservation_reminder_sms.rb b/app/sms/reservation_reminder_sms.rb index 8e6ebde1d..d4bb9db72 100644 --- a/app/sms/reservation_reminder_sms.rb +++ b/app/sms/reservation_reminder_sms.rb @@ -9,43 +9,33 @@ def initialize(to:, reservations:) @reservations = reservations end - def send - client.messages.create( - from: application_number, - to: to.phone_number, - body: body - ) - end - - private - - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def generate_res_msgs - msg = "You have #{res_count} reservation#{res_count > 1 ? 's': ''} soon.\n" - reservations.each do|r| - next if r.end_datetime < Time.current # don't remind people of past events - msg += "#{r.description} on #{r.start_datetime_human} for #{r.duration / 60} minutes with #{r.user.name} tel: #{r.user.phone_number} \n" - end - msg += "Reply 'Confirm' to confirm them all\n" - msg += "Reply 'Cancel' to cancel them all\n" - msg += "Reply 'Change' to request to reschedule\n" - msg += "Reply 'Calendar' to see your schedule\n" - msg += "You can always check online here:\n " - msg += "#{calendar_url(token: to.token)}\n" - msg += 'Thanks!' + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def generate_res_msgs + msg = "You have #{res_count} reservation#{res_count > 1 ? 's': ''} soon.\n" + reservations.each do|r| + next if r.end_datetime < Time.current # don't remind people of past events + msg += "#{r.description} on #{r.start_datetime_human} for #{r.duration / 60} minutes with #{r.user.name} tel: #{r.user.phone_number} \n" end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + msg += "Reply 'Confirm' to confirm them all\n" + msg += "Reply 'Cancel' to cancel them all\n" + msg += "Reply 'Change' to request to reschedule\n" + msg += "Reply 'Calendar' to see your schedule\n" + msg += "You can always check online here:\n " + msg += "#{calendar_url(token: to.token)}\n" + msg += 'Thanks!' + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - def res_count - @reservations.size - end + def res_count + @reservations.size + end - def body - if @reservations.blank? - %(You have no reservations for today or tomorrow! ) - else - generate_res_msgs - end + def body + if @reservations.blank? + %(You have no reservations for today or tomorrow! ) + else + generate_res_msgs end + end end diff --git a/app/sms/reservation_reschedule_sms.rb b/app/sms/reservation_reschedule_sms.rb index 5dbf569c4..11b9b08de 100644 --- a/app/sms/reservation_reschedule_sms.rb +++ b/app/sms/reservation_reschedule_sms.rb @@ -9,11 +9,8 @@ def initialize(to:, reservation:) @reservation = reservation end - def send - client.messages.create( - from: application_number, - to: to.phone_number, - body: "It looks like we'll need to reschedule the #{duration} minute interview for #{selected_time}.\n#{reservation.user.name} will get in touch with you soon.\nTheir number is #{reservation.user.phone_number}") + def body + "It looks like we'll need to reschedule the #{duration} minute interview for #{selected_time}.\n#{reservation.user.name} will get in touch with you soon.\nTheir number is #{reservation.user.phone_number}" end private diff --git a/app/sms/reservation_sms.rb b/app/sms/reservation_sms.rb index ca2c0cf4d..e84befcb5 100644 --- a/app/sms/reservation_sms.rb +++ b/app/sms/reservation_sms.rb @@ -9,14 +9,6 @@ def initialize(to:, reservation:) @reservation = reservation end - def send - client.messages.create( - from: application_number, - to: to.phone_number, - body: body - ) - end - private def body @@ -30,4 +22,5 @@ def selected_time def duration reservation.duration / 60 end + end diff --git a/app/sms/time_slot_not_available_sms.rb b/app/sms/time_slot_not_available_sms.rb index 0f52be29a..37452c39c 100644 --- a/app/sms/time_slot_not_available_sms.rb +++ b/app/sms/time_slot_not_available_sms.rb @@ -9,14 +9,6 @@ def initialize(to:, event:) @event = event end - def send - client.messages.create( - from: application_number, - to: to.phone_number, - body: body - ) - end - def body body = "Sorry, that time is not longer available for: \n" body << "#{event.description}\n" diff --git a/app/sms/wit_sms.rb b/app/sms/wit_sms.rb new file mode 100644 index 000000000..180a7b30c --- /dev/null +++ b/app/sms/wit_sms.rb @@ -0,0 +1,15 @@ +# TODO: needs a spec. The spec for SmsReservationsController covers it, +# but a unit test would make coverage more robust +class WitSms < ApplicationSms + attr_reader :to, :body + + def initialize(to:, msg:) + super + @to = to + @msg = msg + end + + def body + @msg + end +end diff --git a/config/application.rb b/config/application.rb index 20ff22140..fc8f8cc6d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -5,14 +5,16 @@ # Assets should be precompiled for production (so we don't need the gems loaded then) Bundler.require(*Rails.groups(assets: %w(development test))) -# this enables us to know who created a user or updated a user, I beleive. -require './lib/with_user' -# this does zip <-> neighborhood -require './lib/neighborhood_mapping' + + + module Logan class Application < Rails::Application + # this enables us to know who created a user or updated a user + require "#{config.root}/lib/extensions/with_user" + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. diff --git a/config/initializers/delayed_job_config.rb b/config/initializers/delayed_job_config.rb index 06bbc6336..62c2ab49c 100644 --- a/config/initializers/delayed_job_config.rb +++ b/config/initializers/delayed_job_config.rb @@ -1,6 +1,6 @@ # config/initializers/delayed_job_config.rb Delayed::Worker.destroy_failed_jobs = false -Delayed::Worker.sleep_delay = 60 +Delayed::Worker.sleep_delay = 1 # sms needs to be speedy Delayed::Worker.max_attempts = 3 Delayed::Worker.max_run_time = 5.minutes Delayed::Worker.read_ahead = 10 diff --git a/config/initializers/redis.rb b/config/initializers/redis.rb new file mode 100644 index 000000000..abe877bca --- /dev/null +++ b/config/initializers/redis.rb @@ -0,0 +1 @@ +Redis.current = Redis.new(:host => 'localhost', :port => 6379) diff --git a/config/initializers/wit_ai.rb b/config/initializers/wit_ai.rb new file mode 100644 index 000000000..7d8559420 --- /dev/null +++ b/config/initializers/wit_ai.rb @@ -0,0 +1,127 @@ +require 'wit' + +access_token = ENV['WIT_ACCESS_TOKEN'] + +# note, context: it's a hash with string keys. allways string keys +# that's because we save it in json in redis for reuse +actions = { + say: lambda do |_session_id, context, msg| + # this is where we text. + person = Person.find_by(id: context['person_id']) + ::WitSms.new(to: person, msg: msg).send + end, + merge: lambda do |_session_id, context, entities, _msg| + # not sure what else we'd do here. + # maybe some content munging to make things easier? + # like make our dates nice and pretty etc. + + # state is usefull for setting up the initial yes_or_no + context.delete('state') if context['state'] + return context.merge(entities) + end, + error: lambda do |_session_id, _context, error| + p error.message + end, + get_event: lambda do |_session_id, context| + # session ID and context will have event id in it. + ei = V2::EventInvitation.find_by(id: context['event_id']) + + context.merge({ 'reference_time' => ei.start_datetime, + 'reference_time_slot' => ei.bot_duration }) + end, + select_slot_in_time: lambda do |_session_id, context| + # currently unused + context.merge({ 'first_slot_in_time' => 'friday at 2pm' }) + end, + reserve_slot: lambda do |_session_id, context| + # datetime is the time the person chose. + + pp context + ei = V2::EventInvitation.find_by(id: context['event_id']) + person = Person.find_by(id: context['person_id']) + + times = get_times(context, ei.end_datetime) + if times + slots = ei.event.available_time_slots(person) + slot = find_slot_given_times(slots,times) + r = nil + unless slot.blank? + r = V2::Reservation.new(person: person, + time_slot: slot, + user: ei.user, + event: ei.event, + event_invitation: ei) + end + + if r && r.save + duration = r.duration / 60 + selected_time = r.start_datetime_human + msg = "A #{duration} minute interview has been booked for:\n#{selected_time}\nWith: #{r.user.name}, \nTel: #{r.user.phone_number}\n.You'll get a reminder that morning." + else + msg = 'It appears there are no more times available. There will be more opportunities soon!' + end + Redis.current.del("wit_context:#{person.id}") + context.merge({ 'response_msg' => msg }) + else + context.merge({ 'response_msg' => "I'm sorry, I'm just a silly robot. I didn't understand that. Please respond with something that looks like: #{context['reference_time_slot']}" }) + end + end, + confirm_reservation: lambda do |_session_id, context| + # person confirms reservation + context + end, + cancel_reservation: lambda do |_session_id, context| + # person cancels reservation + context + end, + change_reservation: lambda do |_session_id, context| + # person changes reservation + context + end, + calendar: lambda do |_session_id, context| + # a person's calendar for the next few days + context + end, + decline_invitation: lambda do |_session_id, context| + # probably destroy the context and turn end the current session + context + end +} + +def find_slot_given_times(slots,times) + slots.find do |s| + times.find do |t| # iterating through all the times given + next unless t[:confidence] > 0.7 + s.start_datetime > t[:from] && s.end_datetime < t[:untill] + end + end +end + +def get_times(context, default_end) + return nil if context['datetime'].blank? + res = [] + + context['datetime'].each do | datetime| + if datetime['type'] == 'value' + from = Time.zone.parse(datetime['value']) + untill = default_end + else + from = Time.zone.parse(datetime['from']['value']) + # wit.ai uses the end of a duration. we want the start. + # for them 1pm to 5pm == 1pm untill 6pm, including the whole of 5pm + untill = Time.zone.parse(datetime['to']['value']) - 1.hour + end + res << { from: from, untill: untill, confidence: datetime['confidence'] } + end + res # not yet ready to deal with multiple time windows +end + + +# key contexts: +# session_id: it's the person_id and the event_id, how we know what we are talking about +# reference time: the start of the event +# reference_time_slot: string of the start and end time of event +# response_msg: either you got a slot in your time or you didn't +# +::WitClient = Wit.new access_token, actions + diff --git a/config/sample.local_env.yml b/config/sample.local_env.yml index d3de205c1..e5e023684 100644 --- a/config/sample.local_env.yml +++ b/config/sample.local_env.yml @@ -39,3 +39,5 @@ VERIFICATION_SMS_MESSAGE: 'Thank you for verifying your account. We will mail yo SIGNUP_EMAIL_MESSAGE: "You are now signed up for CUTGroup! Your $5 gift card will be in the mail. When new tests come up, you'll receive an email from smarziano@cct.org with details." SIGNUP_SMS_MESSAGE: "You are now signed up for CUTGroup! Your $5 gift card will be in the mail. When new tests come up, you'll receive a text from 773-747-6239 with more details." SIGNUP_ERROR_MESSAGE: "If you are having trouble email smarziano@cct.org or text '98765' and you will be contacted later." +WIT_ACCESS_TOKEN: '' +WIT_APP_ID: '' diff --git a/conversation_management_options b/conversation_management_options new file mode 100644 index 000000000..eb49bcd2a --- /dev/null +++ b/conversation_management_options @@ -0,0 +1,77 @@ +conversation management: how to + +reservation changes: +created is changed to "attendable" +We have a new state "scheduled". It's the default for people using the web UI to create a reservation at a "scheduled" state. + +the user can convert an "attendable" -> "scheduled" and it notifies the person. +when the time slot pops up for the user, they see all of the people who are "available" during that time. + + +logic for "attendable" state overlap checking: +1) Doesn't prevent other people from booking that slot +2) doesn't present as an option to user if person has booked a reservation for tim + + +Conversation Management: + +Option 1 + all sms goes out through delayed_job, every outbound sms is logged. + + event_id and person_id in a redis hash. (could be mysql table, but seems like overkill) + + all communication between person and app is about the last event. + * this enables us to do things like: when are you available? + no + + events time out after a certan period of time. + + this is using wit.ai for almost everything... I think this is the right path. + +Option 2 + after the invitation goes out, we know it's the last invitation to be sent. + When a person responds with a yes or a no, we send another text asking when they are available. We use the context of the the last invitation sent for parsing the response. + + This will totally break if two people get invitations in short order. + + + examples: + happy path: + bot: are you avaiable for a 15 minute call friday between 9am and 6pm? Click here to use the web + person: yes + bot: When are you available? please text back the date and time, like "friday between 2pm and 5pm" + person: friday between 1pm and 4pm + bot: Right. Friday, the 27th, from 1:00pm to 4:00pm. Did I get that right? + person: yes + bot: great, we'll be in touch soon with a specific time then. + + reject offer: + bot: are you avaiable for a 15 minute call friday between 9am and 6pm? Click here to use the web + person: no + bot: sorry to hear that! We'll have more opportunities soon. If you don't want to receive messages from us, reply: 'remove me' + + time parse fail: + bot: are you avaiable for a 15 minute call friday between 9am and 6pm? Click here to use the web + person: yes + bot: When are you available? please text back the date and time, like "friday between 2pm and 5pm" + person: whenever is good for you + bot: I'm sorry. I didn't understand 'whenver is good for you'. Please respond with the date and times you are free. Like: 'friday between 11am and 2pm.' + person: friday at 4pm + bot: Right. Friday, the 27th, from 4:00pm to 6:00pm. Did I get that right? + person: no + bot: Sorry, please try texting a date with the time you start being available untill the time you are not available, like 'friday from 1pm to 4pm' + person: friday from 4pm to 5pm + bot: Right. Friday, the 27th, from 4:00pm to 5:00pm. Did I get that right? + person: yes + bot: great, we'll be in touch soon with a specific time then. + +Flow: + text comes in: + 0) save it. + 1) is it 'remove me'? -> do remove + 2) is it confirm/cancel/change/calendar? -> do those things & send response + 4) do we have an outstanding invitation? -> context from redis -> do wit.ai + 5) error message + +save context in redis by person_id. set it to expire in 30 minutes? every time we get an update, the expiration is extended. (expire key) + diff --git a/lib/with_user.rb b/lib/extensions/with_user.rb similarity index 100% rename from lib/with_user.rb rename to lib/extensions/with_user.rb diff --git a/lib/neighborhood_mapping.rb b/lib/neighborhood_mapping.rb deleted file mode 100644 index 8d98f744c..000000000 --- a/lib/neighborhood_mapping.rb +++ /dev/null @@ -1,146 +0,0 @@ -CHICAGO_NEIGHBORHOODS = { - 'Cathedral District' => [60611], - 'Central Station' => [60605], - 'Dearborn Park' => [60605], - 'Gold Coast' => [60610, 60611], - 'Loop' => [60601, 60602, 60603, 60604, 60605, 60606, 60607, 60616], - 'Magnificent Mile' => [60611], - 'Museum Campus' => [60605], - 'Near North Side' => [60610, 60611, 60642, 60654], - 'Near West Side' => [60606, 60607, 60608, 60610, 60612, 60661], - 'New East Side' => [60601], - 'Noble Square' => [60622], - 'Old Town' => [60610], - 'Printers Row' => [60605], - 'Randolph Market' => [60607, 60661], - 'River East' => [60611], - 'River North' => [60611, 60654], - 'River West' => [60622, 60610], - 'South Loop' => [60605, 60607, 60608, 60616], - 'Streeterville' => [60611], - 'Tri-Taylor' => [60612], - 'Ukrainian Village' => [60622, 60612], - 'West Loop' => [60607], - 'West Town' => [60612, 60622, 60642, 60647], - 'Wicker Park' => [60622], - 'Alta Vista Terrace' => [60613], - 'Belmont Harbor' => [60657], - 'Boys Town' => [60613, 60657], - 'Bucktown' => [60647, 60622, 60614], - 'DePaul' => [60614], - 'Lakeview' => [60657, 60613], - 'Lakeview Central' => [60657], - 'Lakeview East' => [60657, 60613], - 'Lincoln Park' => [60614, 60610], - 'Lincoln Square' => [60625], - 'North Center' => [60618, 60613], - 'North Halsted' => [60613, 60657], - 'Old Town Triangle' => [60614], - 'Park West' => [60614], - 'Ranch Triangle' => [60614], - 'Roscoe Village' => [60618, 60657], - 'Sheffield' => [60614], - 'Uptown' => [60640], - 'West DePaul' => [60614], - 'Wrightwood Neighbors' => [60614], - 'Wrigleyville' => [60613], - 'Andersonville' => [60640], - 'Budlong Woods' => [60625], - 'Buena Park' => [60613, 60640], - 'East Ravenswood' => [60613, 60640], - 'Edgewater' => [60640, 60660], - 'Edgewater Glen' => [60660, 60640], - 'Edison Park' => [60631], - 'Middle Edgebrook' => [60630, 60646], - 'North Edgebrook' => [60630, 60646], - 'Old Irving Park' => [60641], - 'Peterson Park' => [60659], - 'Ravenswood' => [60640, 60625, 60613], - 'West Ridge' => [60645, 60659], - 'West Rogers Park' => [60645, 60659, 60660], - 'Albany Park' => [60625], - 'Avondale' => [60618], - 'Belmont Gardens' => [60641, 60639], - 'Forest Glen' => [60630], - 'Irving Park' => [60618], - 'Jefferson Park' => [60630], - 'Mayfair' => [60630], - 'North Park' => [60625], - 'Norwood Park' => [60631], - 'Old Edgebrook' => [60646], - 'Old Norwood Park' => [60631], - 'Portage Park' => [60634, 60641], - 'Ravenswood Manor' => [60625], - 'Sauganash' => [60646, 60630], - 'Sauganash Woods' => [60630], - 'Schorsch Forest View' => [60656], - 'South Edgebrook' => [60646], - 'Union Ridge' => [60656] -}.freeze - -NYC_NEIGHBORHOODS = { - 'Central Bronx' => [10453, 10457, 10460], - 'Bronx Park and Fordham' => [10458, 10467, 10468], - 'High Bridge and Morrisania' => [10451, 10452, 10456], - 'Hunts Point and Mott Haven' => [10454, 10455, 10459, 10474], - 'Kingsbridge and Riverdale' => [10463, 10471], - 'Northeast Bronx' => [10466, 10469, 10470, 10475], - 'Southeast Bronx' => [10461, 10462, 10464, 10465, 10472, 10473], - 'Central Brooklyn' => [11213, 11216, 11238], - 'Southwest Brooklyn' => [11209, 11214, 11228], - 'Borough Park' => [11204, 11218, 11219, 11230], - 'Canarsie' => [11234, 11236], - 'Southern Brooklyn' => [11223, 11224, 11229, 11235], - 'Northwest Brooklyn' => [11201, 11205, 11215, 11217, 11231], - 'Flatbush' => [11203, 11210, 11225, 11226], - 'Brownsville' => [11233, 11212], - 'East New York and New Lots' => [11207, 11208, 11239], - 'Greenpoint' => [11222], - 'Sunset Park' => [11220, 11232], - 'Williamsburg' => [11211], - 'Bushwick' => [11206, 11221, 11237], - 'Central Harlem' => [10026, 10027, 10030, 10037, 10039], - 'Chelsea and Clinton' => [10001, 10011, 10018, 10019, 10020, 10036], - 'East Harlem' => [10029, 10035], - 'Gramercy Park and Murray Hill' => [10010, 10016, 10017, 10022], - 'Greenwich Village and Soho' => [10012, 10013, 10014], - 'Lower Manhattan' => [10004, 10005, 10006, 10007, 10038, 10280], - 'Lower East Side' => [10002, 10003, 10009], - 'Upper East Side' => [10021, 10028, 10044, 10065, 10075, 10128], - 'Upper West Side' => [10023, 10024, 10025], - 'Inwood and Washington Heights' => [10031, 10032, 10033, 10034, 10040], - 'Northeast Queens' => [11361, 11362, 11363, 11364], - 'North Queens' => [11354, 11355, 11356, 11357, 11358, 11359, 11360], - 'Central Queens' => [11365, 11366, 11367], - 'Jamaica' => [11412, 11423, 11432, 11433, 11434, 11435, 11436], - 'Northwest Queens' => [11101, 11102, 11103, 11104, 11105, 11106], - 'West Central Queens' => [11374, 11375, 11379, 11385], - 'Rockaways' => [11691, 11692, 11693, 11694, 11695, 11697], - 'Southeast Queens' => [11004, 11005, 11411, 11413, 11422, 11426, 11427, 11428, 11429], - 'Southwest Queens' => [11414, 11415, 11416, 11417, 11418, 11419, 11420, 11421], - 'West Queens' => [11368, 11369, 11370, 11372, 11373, 11377, 11378], - 'Port Richmond' => [10302, 10303, 10310], - 'South Shore' => [10306, 10307, 10308, 10309, 10312], - 'Stapleton and St. George' => [10301, 10304, 10305], - 'Mid-Island' => [10314] }.freeze - -def zip_to_neighborhood(zip) - res = select_neighborhood_mapping.select { |k, v| k if v.include?(zip.to_i) } - return res.keys[0] if res -end - -def neighborhood_to_zip(neighborhood) - res = select_neighborhood_mapping.select { |k, _v| k == neighborhood } - res.values[0] if res -end - -def select_neighborhood_mapping - case ENV['NEIGHBORHOOD'] - when 'CHICAGO' - CHICAGO_NEIGHBORHOODS - when 'NEW_YORK' - NYC_NEIGHBORHOODS - else - NYC_NEIGHBORHOODS - end -end diff --git a/lib/tasks/bulk_contact_method.rake b/lib/tasks/bulk_contact_method.rake index 4b5814e36..ccdc99f51 100644 --- a/lib/tasks/bulk_contact_method.rake +++ b/lib/tasks/bulk_contact_method.rake @@ -1,15 +1,15 @@ namespace :bulk_contact_method do desc "Bulk set blank preferred contact method as EMAIL" task :bulkAllBlank => :environment do - + Person.find_each do |person| if person.preferred_contact_method.blank? puts person - person.preferred_contact_method = "EMAIL" + person.preferred_contact_method = "EMAIL" person.save end end end - + end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 508185964..f0806409a 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -8,9 +8,12 @@ require 'support/helpers' require 'sms_spec' require 'timecop' +require 'mock_redis' SmsSpec.driver = :'twilio-ruby' +Redis.current = MockRedis.new # mocking out redis for our tests + ActiveRecord::Migration.maintain_test_schema! Shoulda::Matchers.configure do |config| @@ -56,6 +59,7 @@ config.before(:each) do DatabaseCleaner.strategy = :transaction + Redis.current.flushdb end config.before(:each) do