├── log
└── .keep
├── storage
└── .keep
├── tmp
└── .keep
├── app
├── mailers
│ ├── .keep
│ └── application_mailer.rb
├── models
│ ├── .keep
│ ├── concerns
│ │ └── .keep
│ ├── transcription.rb
│ ├── answer.rb
│ ├── survey.rb
│ └── question.rb
├── assets
│ ├── images
│ │ └── .keep
│ └── config
│ │ └── manifest.js
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── questions_controller.rb
│ ├── application_controller.rb
│ ├── transcriptions_controller.rb
│ ├── answers_controller.rb
│ └── surveys_controller.rb
├── helpers
│ └── application_helper.rb
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
├── packs
│ ├── stylesheets
│ │ └── application.scss
│ └── javascript
│ │ └── application.js
├── jobs
│ └── application_job.rb
└── views
│ ├── surveys
│ └── index.html.erb
│ └── layouts
│ └── application.html.erb
├── lib
├── assets
│ └── .keep
├── tasks
│ └── .keep
├── find_next_question.rb
├── sms
│ ├── tracked_question.rb
│ ├── create_response.rb
│ └── reply_processor.rb
└── voice
│ └── create_response.rb
├── public
├── favicon.ico
├── apple-touch-icon.png
├── apple-touch-icon-precomposed.png
├── robots.txt
├── MEff241a3f1542a4b31d0ae213b96f9c59.jpg
├── 500.html
├── 422.html
└── 404.html
├── .nvmrc
├── .browserslistrc
├── config
├── locales
│ ├── en.bootstrap.yml
│ └── en.yml
├── initializers
│ ├── inflections.rb
│ ├── session_store.rb
│ ├── mime_types.rb
│ ├── application_controller_renderer.rb
│ ├── cookies_serializer.rb
│ ├── filter_parameter_logging.rb
│ ├── wrap_parameters.rb
│ ├── permissions_policy.rb
│ ├── backtrace_silencers.rb
│ ├── assets.rb
│ └── content_security_policy.rb
├── spring.rb
├── environment.rb
├── webpack
│ ├── test.js
│ ├── production.js
│ ├── development.js
│ └── environment.js
├── cable.yml
├── boot.rb
├── routes.rb
├── credentials.yml.enc
├── database.yml
├── secrets.yml
├── application.rb
├── puma.rb
├── environments
│ ├── test.rb
│ ├── development.rb
│ └── production.rb
└── webpacker.yml
├── vendor
└── assets
│ ├── javascripts
│ └── .keep
│ └── stylesheets
│ └── .keep
├── .rspec
├── spec
├── factories
│ ├── surveys.rb
│ └── questions.rb
├── spec_helper.rb
├── controllers
│ ├── questions_controller_spec.rb
│ ├── transcriptions_controller_spec.rb
│ ├── answers_controller_spec.rb
│ └── surveys_controller_spec.rb
├── lib
│ ├── find_next_question_spec.rb
│ ├── sms
│ │ ├── create_response_spec.rb
│ │ ├── tracked_question_spec.rb
│ │ └── reply_processor_spec.rb
│ └── voice
│ │ └── create_response_spec.rb
└── rails_helper.rb
├── bin
├── rake
├── rails
├── webpack
├── webpack-dev-server
├── spring
├── yarn
├── update
├── setup
└── bundle
├── config.ru
├── CONTRIBUTING.md
├── db
├── migrate
│ ├── 20160404110419_add_call_sid_to_answers.rb
│ ├── 20160324144643_create_surveys.rb
│ ├── 20160404115135_create_transcriptions.rb
│ ├── 20160324145530_create_questions.rb
│ └── 20160324154252_create_answers.rb
├── seeds.rb
└── schema.rb
├── .mergify.yml
├── Rakefile
├── .gitattributes
├── postcss.config.js
├── package.json
├── .gitignore
├── LICENSE
├── .github
├── dependabot.yml
└── workflows
│ └── build.yml
├── babel.config.js
├── Gemfile
├── README.md
├── CODE_OF_CONDUCT.md
└── Gemfile.lock
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v14.15.4
2 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | defaults
2 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/locales/en.bootstrap.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/models/transcription.rb:
--------------------------------------------------------------------------------
1 | class Transcription < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format documentation
3 | --order rand
4 | --require spec_helper
5 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | ActiveSupport::Inflector.inflections(:en) do |inflect|
2 | inflect.acronym 'SMS'
3 | end
4 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | Spring.watch(
2 | ".ruby-version",
3 | ".rbenv-vars",
4 | "tmp/restart.txt",
5 | "tmp/caching-dev.txt"
6 | )
7 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/spec/factories/surveys.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :survey do
3 | sequence(:title) { |counter| "survey #{counter}" }
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | load File.expand_path("spring", __dir__)
3 | require_relative "../config/boot"
4 | require "rake"
5 | Rake.application.run
6 |
--------------------------------------------------------------------------------
/public/MEff241a3f1542a4b31d0ae213b96f9c59.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/HEAD/public/MEff241a3f1542a4b31d0ae213b96f9c59.jpg
--------------------------------------------------------------------------------
/app/models/answer.rb:
--------------------------------------------------------------------------------
1 | class Answer < ActiveRecord::Base
2 | enum source: { voice: 0, sms: 1 }
3 |
4 | belongs_to :question
5 | has_one :transcription
6 | end
7 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/factories/questions.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :question do
3 | survey
4 | sequence(:body) { |counter| "question #{counter}" }
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/survey.rb:
--------------------------------------------------------------------------------
1 | class Survey < ActiveRecord::Base
2 | has_many :questions, dependent: :destroy
3 |
4 | def first_question
5 | questions.first
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/config/webpack/test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_automated-survey-rails_session'
4 |
--------------------------------------------------------------------------------
/config/webpack/production.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/config/webpack/development.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | load File.expand_path("spring", __dir__)
3 | APP_PATH = File.expand_path('../config/application', __dir__)
4 | require_relative "../config/boot"
5 | require "rails/commands"
6 |
--------------------------------------------------------------------------------
/app/packs/stylesheets/application.scss:
--------------------------------------------------------------------------------
1 | @import "bootstrap";
2 |
3 | footer {
4 | margin-bottom: 10px;
5 | margin-top: 20px;
6 | text-align: center;
7 |
8 | i {
9 | color:#ff0000;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Twilio
2 |
3 | All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
4 |
--------------------------------------------------------------------------------
/app/controllers/questions_controller.rb:
--------------------------------------------------------------------------------
1 | class QuestionsController < ApplicationController
2 | def show
3 | question = Question.find(params[:id])
4 | render xml: Voice::CreateResponse.for(question)
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: async
6 |
7 | production:
8 | adapter: redis
9 | url: redis://localhost:6379/1
10 | channel_prefix: receive-mms-rails_production
11 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2 |
3 | require "bundler/setup" # Set up gems listed in the Gemfile.
4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations.
5 |
--------------------------------------------------------------------------------
/db/migrate/20160404110419_add_call_sid_to_answers.rb:
--------------------------------------------------------------------------------
1 | class AddCallSidToAnswers < ActiveRecord::Migration
2 | def change
3 | add_column :answers, :call_sid, :string, default: ''
4 | add_index :answers, :call_sid
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20160324144643_create_surveys.rb:
--------------------------------------------------------------------------------
1 | class CreateSurveys < ActiveRecord::Migration
2 | def change
3 | create_table :surveys do |t|
4 | t.string :title, null: false
5 | t.timestamps null: false
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | pull_request_rules:
2 | - name: automatic merge for Dependabot pull requests
3 | conditions:
4 | - author~=^dependabot(|-preview)\[bot\]$
5 | - status-success=build
6 | actions:
7 | merge:
8 | method: squash
9 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative "config/application"
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 | end
6 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files.
2 |
3 |
4 | # Mark the yarn lockfile as having been generated.
5 | yarn.lock linguist-generated
6 |
7 | # Mark any vendored files as having been vendored.
8 | vendor/* linguist-vendored
9 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-import'),
4 | require('postcss-flexbugs-fixes'),
5 | require('postcss-preset-env')({
6 | autoprefixer: {
7 | flexbox: 'no-2009'
8 | },
9 | stage: 3
10 | })
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/app/models/question.rb:
--------------------------------------------------------------------------------
1 | class Question < ActiveRecord::Base
2 | NoQuestion = Class.new
3 |
4 | # Use type column without STI
5 | self.inheritance_column = nil
6 |
7 | enum type: { free: 0, numeric: 1, yes_no: 2 }
8 |
9 | belongs_to :survey
10 | has_many :answers, dependent: :destroy
11 | end
12 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | # Automatically retry jobs that encountered a deadlock
3 | # retry_on ActiveRecord::Deadlocked
4 |
5 | # Most jobs are safe to ignore if the underlying records are no longer available
6 | # discard_on ActiveJob::DeserializationError
7 | end
8 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | root 'surveys#index'
3 | post 'surveys/voice', to: 'surveys#voice'
4 | post 'surveys/sms', to: 'surveys#sms'
5 |
6 | resources :questions, only: [:show]
7 | resources :answers, only: [:create]
8 | resources :transcriptions, only: [:create]
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20160404115135_create_transcriptions.rb:
--------------------------------------------------------------------------------
1 | class CreateTranscriptions < ActiveRecord::Migration
2 | def change
3 | create_table :transcriptions do |t|
4 | t.belongs_to :answer, index: true
5 | t.string :text, default: ''
6 | t.timestamps null:false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [
5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
6 | ]
7 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.expect_with :rspec do |expectations|
3 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
4 | end
5 |
6 | config.mock_with :rspec do |mocks|
7 | mocks.verify_partial_doubles = true
8 | end
9 |
10 | config.shared_context_metadata_behavior = :apply_to_host_groups
11 | end
12 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "automated-survey-rails",
3 | "private": true,
4 | "dependencies": {
5 | "@rails/ujs": "^6.0.0",
6 | "@rails/webpacker": "5.4.3",
7 | "bootstrap": "^4.5.3",
8 | "jquery": "^3.5.1",
9 | "popper.js": "^1.16.1",
10 | "turbolinks": "^5.2.0"
11 | },
12 | "version": "0.1.0",
13 | "devDependencies": {
14 | "sass": "^1.32.5",
15 | "webpack-dev-server": "^4.11.1"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/db/migrate/20160324145530_create_questions.rb:
--------------------------------------------------------------------------------
1 | class CreateQuestions < ActiveRecord::Migration
2 | def change
3 | create_table :questions do |t|
4 | t.references :survey, null: false, index: true
5 | t.string :body, null: false
6 | t.integer :type, null: false, default: 0, index: true
7 | t.timestamps null: false
8 | end
9 |
10 | add_foreign_key :questions, :surveys
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Define an application-wide HTTP permissions policy. For further
2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy
3 | #
4 | # Rails.application.config.permissions_policy do |f|
5 | # f.camera :none
6 | # f.gyroscope :none
7 | # f.microphone :none
8 | # f.usb :none
9 | # f.fullscreen :self
10 | # f.payment :self, "https://secure.example.com"
11 | # end
12 |
--------------------------------------------------------------------------------
/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | H6mjhEwPEcDXfPhGXKSIIBaKVAReL3dt90szBW4IFxae7af296vfC83tf/rCU9p8F5KQmTzVCeSxHci12LYffsaORzExGpiD4dzbwECMP2XYq4ecZPmvUHYQHc+o32YpUs45lI7FPyHTd+83RxpTbWUWtkYFiVfjH/7ZouqS5W7XmBKs/yUZbQwv64fUgLUCI9Y9G3Tp4Q+0oVRCj8DG2/9WOgNdGmDtDouQ8ZNh+kNZGawh78EeRoiTWtQdnWFeZe2tciXIaKqaUZFlIqDK1bAaGba3ivk62Nw0Bw+MuHcb0lB3fBR1nxnaT1kCzW+NAuVkWWEtLKb0X8yFqi98Nto3wQECjjWUdrOgENSUMWocUxLHGo8iAKkJZ1hA2S6GsbR72qC8JukULLNmHjAqZLiiGLahXYxHfc3l--Uj+qoSoxXjWqNTuh--GIFbbasSAxfMMtpIBn2o1w==
2 |
--------------------------------------------------------------------------------
/bin/webpack:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "bundler/setup"
11 |
12 | require "webpacker"
13 | require "webpacker/webpack_runner"
14 |
15 | APP_ROOT = File.expand_path("..", __dir__)
16 | Dir.chdir(APP_ROOT) do
17 | Webpacker::WebpackRunner.run(ARGV)
18 | end
19 |
--------------------------------------------------------------------------------
/db/migrate/20160324154252_create_answers.rb:
--------------------------------------------------------------------------------
1 | class CreateAnswers < ActiveRecord::Migration
2 | def change
3 | create_table :answers do |t|
4 | t.references :question, null: false, index: true
5 | t.integer :source, null: false, default: 0, index: true
6 | t.string :content, null: false
7 | t.string :from, null: false, index: true
8 | t.timestamps null: false
9 | end
10 |
11 | add_foreign_key :answers, :questions
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/bin/webpack-dev-server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "bundler/setup"
11 |
12 | require "webpacker"
13 | require "webpacker/dev_server_runner"
14 |
15 | APP_ROOT = File.expand_path("..", __dir__)
16 | Dir.chdir(APP_ROOT) do
17 | Webpacker::DevServerRunner.run(ARGV)
18 | end
19 |
--------------------------------------------------------------------------------
/app/packs/javascript/application.js:
--------------------------------------------------------------------------------
1 | // This file is automatically compiled by Webpack, along with any other files
2 | // present in this directory. You're encouraged to place your actual application logic in
3 | // a relevant structure within app/javascript and only use these pack files to reference
4 | // that code so it'll be compiled.
5 |
6 | import Rails from "@rails/ujs"
7 | import Turbolinks from "turbolinks"
8 | import "bootstrap"
9 | import "../stylesheets/application"
10 |
11 | Rails.start()
12 | Turbolinks.start()
13 |
--------------------------------------------------------------------------------
/spec/controllers/questions_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe QuestionsController do
4 | describe '#show' do
5 | let(:survey) { create(:survey) }
6 | let(:question) { create(:question, survey: survey, body: 'question') }
7 |
8 | before { get :show, params: { id: question.id } }
9 |
10 | it 'responds with the question' do
11 | expect(response.body).to include('question')
12 | end
13 |
14 | it 'responds with ok' do
15 | expect(response).to be_ok
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | adapter: postgresql
3 | pool: 5
4 | timeout: 5000
5 |
6 | development:
7 | <<: *default
8 | database: automated_survey_development
9 |
10 | # Warning: The database defined as "test" will be erased and
11 | # re-generated from your development database when you run "rake".
12 | # Do not set this db to the same as development or production.
13 | test:
14 | <<: *default
15 | database: automated_survey_test
16 |
17 | production:
18 | <<: *default
19 | database: automated_survey_production
20 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"])
3 | gem "bundler"
4 | require "bundler"
5 |
6 | # Load Spring without loading other gems in the Gemfile, for speed.
7 | Bundler.locked_gems.specs.find { |spec| spec.name == "spring" }&.tap do |spring|
8 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
9 | gem "spring", spring.version
10 | require "spring/binstub"
11 | rescue Gem::LoadError
12 | # Ignore when Spring is not installed.
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/controllers/transcriptions_controller.rb:
--------------------------------------------------------------------------------
1 | class TranscriptionsController < ApplicationController
2 | skip_before_action :verify_authenticity_token
3 |
4 | def create
5 | text = params[:TranscriptionText]
6 | Transcription.create(answer_id: answer_for_transcription.id, text: text)
7 | head :ok
8 | end
9 |
10 | private
11 |
12 | def answer_for_transcription
13 | question_id = params[:question_id]
14 | call_sid = params[:CallSid]
15 | Answer.where(question_id: question_id, call_sid: call_sid).first
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/bin/yarn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_ROOT = File.expand_path('..', __dir__)
3 | Dir.chdir(APP_ROOT) do
4 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR).
5 | select { |dir| File.expand_path(dir) != __dir__ }.
6 | product(["yarn", "yarn.exe"]).
7 | map { |dir, file| File.expand_path(file, dir) }.
8 | find { |file| File.executable?(file) }
9 |
10 | if yarn
11 | exec yarn, *ARGV
12 | else
13 | $stderr.puts "Yarn executable was not detected in the system."
14 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
15 | exit 1
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]
9 |
--------------------------------------------------------------------------------
/config/webpack/environment.js:
--------------------------------------------------------------------------------
1 | const { environment } = require('@rails/webpacker')
2 |
3 | const webpack = require('webpack')
4 | environment.plugins.append(
5 | 'Provide',
6 | new webpack.ProvidePlugin({
7 | $: 'jquery',
8 | jQuery: 'jquery',
9 | Popper: ['popper.js', 'default']
10 | })
11 | )
12 |
13 | const sassLoader = environment.loaders.get('sass')
14 | const sassLoaderConfig = sassLoader.use.find(function(element) {
15 | return element.loader == 'sass-loader'
16 | })
17 |
18 | const options = sassLoaderConfig.options
19 | options.implementation = require('sass')
20 |
21 | module.exports = environment
22 |
--------------------------------------------------------------------------------
/app/controllers/answers_controller.rb:
--------------------------------------------------------------------------------
1 | class AnswersController < ApplicationController
2 | skip_before_action :verify_authenticity_token
3 |
4 | def create
5 | Answer.create(answer_params)
6 | current_question = Question.find(params[:question_id])
7 | next_question = FindNextQuestion.for(current_question)
8 | render xml: Voice::CreateResponse.for(next_question)
9 | end
10 |
11 | private
12 |
13 | def answer_params
14 | {
15 | from: params[:From],
16 | content: params[:RecordingUrl] || params[:Digits],
17 | question_id: params[:question_id],
18 | call_sid: params[:CallSid]
19 | }
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/views/surveys/index.html.erb:
--------------------------------------------------------------------------------
1 |
Results for <%= @survey.title %>
2 |
3 | <% @survey.questions.each do |question| %>
4 |
5 |
8 |
9 | <% question.answers.each do |answer| %>
10 | <% if question.free? && answer.voice? %>
11 |
15 | <% else %>
16 | <%= answer.content %>
17 | <% end %>
18 | <% end %>
19 |
20 |
21 | <% end %>
22 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 | # Add Yarn node_modules folder to the asset load path.
9 | Rails.application.config.assets.paths << Rails.root.join('node_modules')
10 |
11 | # Precompile additional assets.
12 | # application.js, application.css, and all non-JS/CSS in the app/assets
13 | # folder are already added.
14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
15 |
--------------------------------------------------------------------------------
/lib/find_next_question.rb:
--------------------------------------------------------------------------------
1 | class FindNextQuestion
2 | def self.for(question)
3 | new(question).find_next
4 | end
5 |
6 | def initialize(question)
7 | @question = question
8 | end
9 |
10 | def find_next
11 | question_id = next_question_id
12 | if question_id
13 | Question.find(question_id)
14 | else
15 | Question::NoQuestion
16 | end
17 | end
18 |
19 | private
20 |
21 | attr_reader :question
22 |
23 | def questions_for_survey
24 | question.survey.questions.pluck(:id)
25 | end
26 |
27 | def next_question_id
28 | questions = questions_for_survey
29 | current_question_index = questions.index(question.id)
30 | questions[current_question_index.succ]
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/controllers/surveys_controller.rb:
--------------------------------------------------------------------------------
1 | class SurveysController < ApplicationController
2 | skip_before_action :verify_authenticity_token
3 |
4 | def index
5 | @survey = Survey.includes(:questions).first
6 | end
7 |
8 | def voice
9 | survey = Survey.first
10 | render xml: welcome_message_for_voice(survey)
11 | end
12 |
13 | def sms
14 | user_response = params[:Body]
15 | from = params[:From]
16 | render xml: SMS::ReplyProcessor.process(user_response, from, cookies)
17 | end
18 |
19 | private
20 |
21 | def welcome_message_for_voice(survey)
22 | response = Twilio::TwiML::VoiceResponse.new
23 | response.say(message: "Thank you for taking the #{survey.title} survey")
24 | response.redirect question_path(survey.first_question.id), method: 'GET'
25 |
26 | response.to_s
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/lib/find_next_question_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe FindNextQuestion do
4 | describe '.for' do
5 | let(:survey) { create(:survey) }
6 | let!(:first_question) { create(:question, survey: survey, body: 'first') }
7 | let!(:last_question) { create(:question, survey: survey, body: 'last') }
8 |
9 | context 'when there are available questions' do
10 | it 'responds with the next available question' do
11 | next_question = described_class.for(first_question)
12 | expect(next_question.body).to eq('last')
13 | end
14 | end
15 |
16 | context 'when there are no more questions' do
17 | it 'responds with Question::NoQuestion' do
18 | next_question = described_class.for(last_question)
19 | expect(next_question).to eq(Question::NoQuestion)
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/controllers/transcriptions_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe TranscriptionsController do
4 | let(:survey) { create(:survey) }
5 | let(:question) { create(:question, survey: survey) }
6 | let!(:answer) { Answer.create(question: question,
7 | content: 'I like it',
8 | from: '+15555555',
9 | call_sid: '12345') }
10 |
11 | describe '#create' do
12 | it 'creates a transcription' do
13 | post :create, params: attributes_for_transcription
14 | expect(answer.transcription.text).to eq('transcription text')
15 | end
16 | end
17 |
18 | private
19 |
20 | def attributes_for_transcription
21 | {
22 | question_id: question.id,
23 | CallSid: '12345',
24 | TranscriptionText: 'transcription text'
25 | }
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a way to update your development environment automatically.
15 | # Add necessary update steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | puts "\n== Updating database =="
22 | system! 'bin/rails db:migrate'
23 |
24 | puts "\n== Removing old logs and tempfiles =="
25 | system! 'bin/rails log:clear tmp:clear'
26 |
27 | puts "\n== Restarting application server =="
28 | system! 'bin/rails restart'
29 | end
30 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 |
4 | survey = Survey.create(title: 'Twilio Developer Education')
5 |
6 | survey.questions.create(
7 | [
8 | {
9 | body: 'On a scale of 0 to 9 how would you rate this tutorial?',
10 | type: Question.types[:numeric]
11 | },
12 | {
13 | body: 'On a scale of 0 to 9 how would you rate the design of this tutorial?',
14 | type: Question.types[:numeric]
15 | },
16 | {
17 | body: 'In your own words please describe your feelings about Twilio right now?',
18 | type: Question.types[:free]
19 | },
20 | {
21 | body: 'Do you like my voice? Please be honest, I dislike liars.',
22 | type: Question.types[:yes_no]
23 | }
24 | ]
25 | )
26 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path('..', __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies
21 | system! 'bin/yarn'
22 |
23 | puts "\n== Removing old logs and tempfiles =="
24 | system! 'bin/rails log:clear tmp:clear'
25 |
26 | puts "\n== Restarting application server =="
27 | system! 'bin/rails restart'
28 | end
29 |
--------------------------------------------------------------------------------
/lib/sms/tracked_question.rb:
--------------------------------------------------------------------------------
1 | module SMS
2 | class TrackedQuestion
3 | def initialize(cookies)
4 | @cookies = cookies
5 | end
6 |
7 | def store_or_destroy(question)
8 | if question == Question::NoQuestion
9 | destroy
10 | else
11 | cookies[:question] = serialize(question)
12 | end
13 | end
14 |
15 | def fetch
16 | question = cookies.fetch(:question)
17 | deserialize(question)
18 | end
19 |
20 | def destroy
21 | cookies[:question] = nil
22 | end
23 |
24 | def empty?
25 | cookies[:question].nil? || cookies[:question].empty?
26 | end
27 |
28 | def present?
29 | !empty?
30 | end
31 |
32 | private
33 |
34 | attr_reader :cookies
35 |
36 | def serialize(question)
37 | question.serializable_hash.to_yaml
38 | end
39 |
40 | def deserialize(question)
41 | Question.new(YAML.load(question))
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 | /db/*.sqlite3-*
14 |
15 | # Ignore all logfiles and tempfiles.
16 | /log/*
17 | /tmp/*
18 | !/log/.keep
19 | !/tmp/.keep
20 |
21 | # Ignore uploaded files in development.
22 | /storage/*
23 | !/storage/.keep
24 |
25 | /public/assets
26 | .byebug_history
27 |
28 | # Ignore master key for decrypting credentials and more.
29 | /config/master.key
30 |
31 | /public/packs
32 | /public/packs-test
33 | /node_modules
34 | /yarn-error.log
35 | yarn-debug.log*
36 | .yarn-integrity
37 |
38 | /vendor
39 | .env
40 | .env.test
41 |
42 | /.vscode
43 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: c9af8cbd452dde59d7c3207a0f1545ea8cb69238fc089862b749ea2737500c8841fc30416f169a341c22cb9c744d91554e3430decbaf1806adc13bf7500cb2af
15 |
16 | test:
17 | secret_key_base: 8cc06ed4893ce1dd554176da3717ebfcfab598a122dbe9ae47ccfec2cd880e08d1e0b1448cb597974e9f0bb2e395e9c51218ce82a3378cbf8a98165738537e62
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 Twilio Inc
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Automated Surveys
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= stylesheet_pack_tag 'stylesheets/application', media: 'all', 'data-turbolinks-track': 'reload' %>
9 | <%= javascript_pack_tag 'javascript/application', 'data-turbolinks-track': 'reload' %>
10 |
11 |
12 |
13 |
23 |
24 |
25 |
26 | <%= yield %>
27 |
28 |
29 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: bundler
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | ignore:
9 | - dependency-name: sdoc
10 | versions:
11 | - 2.0.3
12 | - 2.0.4
13 | - 2.1.0
14 | - dependency-name: twilio-ruby
15 | versions:
16 | - 5.46.1
17 | - 5.47.0
18 | - 5.48.0
19 | - 5.49.0
20 | - 5.50.0
21 | - dependency-name: nokogiri
22 | versions:
23 | - 1.11.1
24 | - 1.11.2
25 | - dependency-name: pg
26 | versions:
27 | - 1.2.3
28 | - dependency-name: haml
29 | versions:
30 | - 5.2.1
31 | - dependency-name: rake
32 | versions:
33 | - 13.0.3
34 | - dependency-name: rack
35 | versions:
36 | - 1.6.13
37 | - dependency-name: puma
38 | versions:
39 | - 3.12.6
40 | - dependency-name: jbuilder
41 | versions:
42 | - 2.9.1
43 | - dependency-name: coffee-rails
44 | versions:
45 | - 4.2.2
46 | - dependency-name: loofah
47 | versions:
48 | - 2.9.0
49 | - dependency-name: sass-rails
50 | versions:
51 | - 6.0.0
52 | - dependency-name: jquery-rails
53 | versions:
54 | - 4.3.4
55 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails"
4 | # Pick the frameworks you want:
5 | require "active_model/railtie"
6 | require "active_job/railtie"
7 | require "active_record/railtie"
8 | # require "active_storage/engine"
9 | require "action_controller/railtie"
10 | # require "action_mailer/railtie"
11 | # require "action_mailbox/engine"
12 | # require "action_text/engine"
13 | require "action_view/railtie"
14 | # require "action_cable/engine"
15 | # require "sprockets/railtie"
16 | require "rails/test_unit/railtie"
17 |
18 | # Require the gems listed in Gemfile, including any gems
19 | # you've limited to :test, :development, or :production.
20 | Bundler.require(*Rails.groups)
21 |
22 | module AutomatedSurveyRails
23 | class Application < Rails::Application
24 | # Initialize configuration defaults for originally generated Rails version.
25 | config.load_defaults 6.1
26 |
27 | # Configuration for the application, engines, and railties goes here.
28 | #
29 | # These settings can be overridden in specific environments using the files
30 | # in config/environments, which are processed later.
31 | #
32 | # config.time_zone = "Central Time (US & Canada)"
33 | # config.eager_load_paths << Rails.root.join("extras")
34 | config.autoload_paths << Rails.root.join('lib')
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/sms/create_response.rb:
--------------------------------------------------------------------------------
1 | module SMS
2 | class CreateResponse
3 | INSTRUCTIONS = {
4 | 'free' => 'Reply to this message with your answer',
5 | 'numeric' => 'Reply with a number from "0" to "9" to this message',
6 | 'yes_no' => 'Reply with "1" for YES and "0" for NO to this message'
7 | }.freeze
8 |
9 | def self.for(question)
10 | new(question).response
11 | end
12 |
13 | def initialize(question)
14 | @question = question
15 | end
16 |
17 | def response
18 | return exit_message if question == Question::NoQuestion
19 |
20 | response = Twilio::TwiML::MessagingResponse.new
21 | message = Twilio::TwiML::Message.new
22 | body = Twilio::TwiML::Body.new(message_body)
23 |
24 | response.append(message)
25 | message.append(body)
26 |
27 | response.to_s
28 | end
29 |
30 | private
31 |
32 | attr_reader :question
33 |
34 | def exit_message
35 | response = Twilio::TwiML::MessagingResponse.new
36 | message = Twilio::TwiML::Message.new
37 | body = Twilio::TwiML::Body.new('Thanks for your time. Good bye')
38 |
39 | response.append(message)
40 | message.append(body)
41 |
42 | response.to_s
43 | end
44 |
45 | def message_body
46 | [question.body, INSTRUCTIONS.fetch(question.type)].join("\n\n")
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/controllers/answers_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe AnswersController do
4 | describe '#create' do
5 | let(:survey) { create(:survey) }
6 | let(:question) { first_question }
7 | let!(:first_question) { create(:question, survey: survey, body: 'first') }
8 | let!(:last_question) { create(:question, survey: survey, body: 'last') }
9 |
10 | it 'creates an answer' do
11 | expect do
12 | post :create, params: attributes_for_answer
13 | end.to change { Answer.count }.by(1)
14 | end
15 |
16 | context 'when there are more available questions' do
17 | let(:question) { first_question }
18 |
19 | it 'responds with the next question' do
20 | post :create, params: attributes_for_answer
21 | expect(response.body).to include('last')
22 | end
23 | end
24 |
25 | context 'when there are no more available questions' do
26 | let(:question) { last_question }
27 |
28 | it 'responds with the thanks message' do
29 | post :create, params: attributes_for_answer
30 | expect(response.body).to include('Thanks')
31 | end
32 | end
33 | end
34 |
35 | private
36 |
37 | def attributes_for_answer
38 | {
39 | From: '+14155559368',
40 | Digits: '1',
41 | CallSid: '12345',
42 | question_id: question.id,
43 | RecordingUrl: 'http://example.com'
44 | }
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy
4 | # For further information see the following documentation
5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
6 |
7 | # Rails.application.config.content_security_policy do |policy|
8 | # policy.default_src :self, :https
9 | # policy.font_src :self, :https, :data
10 | # policy.img_src :self, :https, :data
11 | # policy.object_src :none
12 | # policy.script_src :self, :https
13 | # policy.style_src :self, :https
14 | # # If you are using webpack-dev-server then specify webpack-dev-server host
15 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
16 |
17 | # # Specify URI for violation reports
18 | # # policy.report_uri "/csp-violation-report-endpoint"
19 | # end
20 |
21 | # If you are using UJS then enable automatic nonce generation
22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
23 |
24 | # Set the nonce only to specific directives
25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
26 |
27 | # Report CSP violations to a specified URI
28 | # For further information see the following documentation:
29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
30 | # Rails.application.config.content_security_policy_report_only = true
31 |
--------------------------------------------------------------------------------
/lib/voice/create_response.rb:
--------------------------------------------------------------------------------
1 | module Voice
2 | class CreateResponse
3 | INSTRUCTIONS = {
4 | 'free' => 'Please record your answer after the beep and then hit the pound sign',
5 | 'numeric' => 'Please press a number between 0 and 9 and then hit the pound sign',
6 | 'yes_no' => 'Please press the 1 for yes and the 0 for no and then hit the pound sign'
7 | }.freeze
8 |
9 | def self.for(question)
10 | new(question).response
11 | end
12 |
13 | def initialize(question)
14 | @question = question
15 | end
16 |
17 | def response
18 | return exit_message if question == Question::NoQuestion
19 |
20 | response = Twilio::TwiML::VoiceResponse.new
21 | response.say(message: question.body)
22 | response.say(message: INSTRUCTIONS.fetch(question.type))
23 | if question.free?
24 | response.record action: answers_path(question.id),
25 | transcribe: true,
26 | transcribe_callback: transcriptions_path(question.id)
27 | else
28 | response.gather action: answers_path(question.id)
29 | end
30 |
31 | response.to_s
32 | end
33 |
34 | private
35 |
36 | attr_reader :question
37 |
38 | def exit_message
39 | response = Twilio::TwiML::VoiceResponse.new
40 | response.say(message: 'Thanks for your time. Good bye')
41 | response.hangup
42 | response.to_s
43 | end
44 |
45 | def answers_path(question_id)
46 | "/answers?question_id=#{question_id}"
47 | end
48 |
49 | def transcriptions_path(question_id)
50 | "/transcriptions?question_id=#{question_id}"
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/sms/reply_processor.rb:
--------------------------------------------------------------------------------
1 | module SMS
2 | class ReplyProcessor
3 | def self.process(message, from, cookies)
4 | new(message, from, cookies).process
5 | end
6 |
7 | def initialize(message, from, cookies)
8 | @message = message
9 | @from = from
10 | @tracked_question = TrackedQuestion.new(cookies)
11 | end
12 |
13 | def process
14 | return process_initial_response if initial_response?
15 | process_succ_response if tracked_question.present?
16 | end
17 |
18 | private
19 |
20 | attr_reader :message, :from, :tracked_question
21 |
22 | def initial_response?
23 | tracked_question.empty?
24 | end
25 |
26 | def process_initial_response
27 | survey = Survey.first
28 | first_question = survey.first_question
29 | tracked_question.store_or_destroy(first_question)
30 | CreateResponse.for(first_question)
31 | end
32 |
33 | def process_succ_response
34 | previous_question = tracked_question.fetch
35 | Answer.create(attributes_for_answer(previous_question))
36 | next_question = FindNextQuestion.for(previous_question)
37 | tracked_question.store_or_destroy(next_question)
38 | CreateResponse.for(next_question)
39 | end
40 |
41 | def attributes_for_answer(question)
42 | {
43 | question_id: question.id,
44 | content: message,
45 | source: Answer.sources.fetch(:sms),
46 | from: from
47 | }
48 | end
49 |
50 | # Think about this case.
51 | def welcome_message_o
52 | survey = Survey.first
53 | Twilio::TwiML::Response.new do |r|
54 | r.Message do |msg|
55 | msg.Body "Thank you for taking the #{survey.title} survey"
56 | end
57 | end.to_xml
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
12 | # terminating a worker in development environments.
13 | #
14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
15 |
16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
17 | #
18 | port ENV.fetch("PORT") { 3000 }
19 |
20 | # Specifies the `environment` that Puma will run in.
21 | #
22 | environment ENV.fetch("RAILS_ENV") { "development" }
23 |
24 | # Specifies the `pidfile` that Puma will use.
25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
26 |
27 | # Specifies the number of `workers` to boot in clustered mode.
28 | # Workers are forked web server processes. If using threads and workers together
29 | # the concurrency of the application would be max `threads` * `workers`.
30 | # Workers do not work on JRuby or Windows (both of which do not support
31 | # processes).
32 | #
33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
34 |
35 | # Use the `preload_app!` method when specifying a `workers` number.
36 | # This directive tells Puma to first boot the application and load code
37 | # before forking the application. This takes advantage of Copy On Write
38 | # process behavior so workers use less memory.
39 | #
40 | # preload_app!
41 |
42 | # Allow puma to be restarted by `rails restart` command.
43 | plugin :tmp_restart
44 |
--------------------------------------------------------------------------------
/spec/lib/sms/create_response_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe SMS::CreateResponse do
4 | describe '.for' do
5 | subject { described_class.for(question) }
6 |
7 | let(:question_type) { 'free' }
8 | let(:question) do
9 | build_stubbed(:question, body: 'question?', type: question_type)
10 | end
11 |
12 | it 'creates a response with the question body' do
13 | expect(content_for('/Response/Message/Body'))
14 | .to include('question?')
15 | end
16 |
17 | context 'when the question type is "free"' do
18 | let(:question_type) { 'free' }
19 |
20 | it 'uses the instruction for free questions' do
21 | expect(content_for('/Response/Message/Body'))
22 | .to include('Reply to this message with your answer')
23 | end
24 | end
25 |
26 | context 'when the question type is "numeric"' do
27 | let(:question_type) { 'numeric' }
28 |
29 | it 'uses the instruction for numeric questions' do
30 | expect(content_for('/Response/Message/Body'))
31 | .to include('Reply with a number from "0" to "9" to this message')
32 | end
33 | end
34 |
35 | context 'when the question type is "yes_no"' do
36 | let(:question_type) { 'yes_no' }
37 |
38 | it 'uses the instruction for yes_no questions' do
39 | expect(content_for('/Response/Message/Body'))
40 | .to include('Reply with "1" for YES and "0" for NO to this message')
41 | end
42 | end
43 |
44 | context 'when the question is Question::NoQuestion' do
45 | let(:question) { Question::NoQuestion }
46 |
47 | it 'responds with a closing message' do
48 | expect(content_for('/Response/Message/Body'))
49 | .to eq('Thanks for your time. Good bye')
50 | end
51 | end
52 | end
53 |
54 | private
55 |
56 | def content_for(xpath)
57 | document = Nokogiri::XML(subject)
58 | document.at_xpath(xpath).content
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | var validEnv = ['development', 'test', 'production']
3 | var currentEnv = api.env()
4 | var isDevelopmentEnv = api.env('development')
5 | var isProductionEnv = api.env('production')
6 | var isTestEnv = api.env('test')
7 |
8 | if (!validEnv.includes(currentEnv)) {
9 | throw new Error(
10 | 'Please specify a valid `NODE_ENV` or ' +
11 | '`BABEL_ENV` environment variables. Valid values are "development", ' +
12 | '"test", and "production". Instead, received: ' +
13 | JSON.stringify(currentEnv) +
14 | '.'
15 | )
16 | }
17 |
18 | return {
19 | presets: [
20 | isTestEnv && [
21 | '@babel/preset-env',
22 | {
23 | targets: {
24 | node: 'current'
25 | }
26 | }
27 | ],
28 | (isProductionEnv || isDevelopmentEnv) && [
29 | '@babel/preset-env',
30 | {
31 | forceAllTransforms: true,
32 | useBuiltIns: 'entry',
33 | corejs: 3,
34 | modules: false,
35 | exclude: ['transform-typeof-symbol']
36 | }
37 | ]
38 | ].filter(Boolean),
39 | plugins: [
40 | 'babel-plugin-macros',
41 | '@babel/plugin-syntax-dynamic-import',
42 | isTestEnv && 'babel-plugin-dynamic-import-node',
43 | '@babel/plugin-transform-destructuring',
44 | [
45 | '@babel/plugin-proposal-class-properties',
46 | {
47 | loose: true
48 | }
49 | ],
50 | [
51 | '@babel/plugin-proposal-object-rest-spread',
52 | {
53 | useBuiltIns: true
54 | }
55 | ],
56 | [
57 | '@babel/plugin-transform-runtime',
58 | {
59 | helpers: false
60 | }
61 | ],
62 | [
63 | '@babel/plugin-transform-regenerator',
64 | {
65 | async: false
66 | }
67 | ]
68 | ].filter(Boolean)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | ruby '~> 3.0'
5 |
6 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
7 | gem 'rails', '~> 7.0.4'
8 | gem 'pg'
9 | # Use Puma as the app server
10 | gem 'puma', '~> 6.3'
11 | # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
12 | gem 'webpacker', '~> 5.4'
13 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
14 | gem 'turbolinks', '~> 5'
15 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
16 | gem 'jbuilder', '~> 2.11'
17 | # Use Active Model has_secure_password
18 | # gem 'bcrypt', '~> 3.1.7'
19 |
20 | # Reduces boot times through caching; required in config/boot.rb
21 | gem 'bootsnap', '>= 1.4.4', require: false
22 |
23 | group :development, :test do
24 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
25 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
26 | gem 'rails-controller-testing'
27 | gem 'rspec-rails'
28 | gem 'factory_bot_rails'
29 | end
30 |
31 | group :development do
32 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
33 | gem 'web-console', '>= 4.1.0'
34 | # Display performance information such as SQL time and flame graphs for each request in your browser.
35 | # Can be configured to work on production as well see: https://github.com/MiniProfiler/rack-mini-profiler/blob/master/README.md
36 | gem 'rack-mini-profiler', '~> 3.1'
37 | gem 'listen', '~> 3.8'
38 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
39 | gem 'spring'
40 | end
41 |
42 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
43 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
44 | gem "twilio-ruby", "~> 6.3"
45 | gem "bootstrap", "~> 5.2.3"
46 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | services:
14 | postgres:
15 | image: postgres
16 | env:
17 | POSTGRES_USER: postgres
18 | POSTGRES_PASSWORD: postgres
19 | options: >-
20 | --health-cmd pg_isready
21 | --health-interval 10s
22 | --health-timeout 5s
23 | --health-retries 5
24 | ports:
25 | - 5432:5432
26 |
27 | steps:
28 | - name: Checkout code
29 | uses: actions/checkout@v2
30 | - name: Install PostgreSQL Client
31 | run: sudo apt-get -yqq install libpq-dev
32 | - name: Setup Ruby
33 | uses: ruby/setup-ruby@v1
34 | with:
35 | bundler-cache: false
36 | ruby-version: '3.0'
37 | - name: Install gems
38 | run: |
39 | gem install bundler
40 | bundle config path vendor/bundle
41 | bundle install --jobs 4 --retry 3
42 | - name: Setup Node
43 | uses: actions/setup-node@v1
44 | with:
45 | node-version: 14.15.4
46 | - name: Find yarn cache location
47 | id: yarn-cache
48 | run: echo "::set-output name=dir::$(yarn cache dir)"
49 | - name: JS package cache
50 | uses: actions/cache@v1
51 | with:
52 | path: ${{ steps.yarn-cache.outputs.dir }}
53 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
54 | restore-keys: |
55 | ${{ runner.os }}-yarn-
56 | - name: Install packages
57 | run: |
58 | yarn install --pure-lockfile
59 | - name: Build App
60 | env:
61 | PGHOST: localhost
62 | PGUSER: postgres
63 | PGPASSWORD: postgres
64 | run: bundle exec rails db:setup
65 | - name: Run tests
66 | run: bundle exec rspec
67 | env:
68 | PGHOST: localhost
69 | PGUSER: postgres
70 | PGPASSWORD: postgres
71 | RAILS_ENV: test
72 |
--------------------------------------------------------------------------------
/spec/controllers/surveys_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe SurveysController do
4 | let!(:survey) { create(:survey, title: 'bees') }
5 |
6 | describe '#index' do
7 | before { get :index }
8 |
9 | it 'responds with ok' do
10 | expect(response).to be_ok
11 | end
12 |
13 | it 'renders the :index template' do
14 | expect(response).to render_template(:index)
15 | end
16 |
17 | it 'assigns @survey' do
18 | expect(assigns(:survey)).to eq(survey)
19 | end
20 | end
21 |
22 | describe '#voice' do
23 | let!(:question) { create(:question, survey: survey) }
24 |
25 | before { post :voice }
26 |
27 | it 'responds with a welcome message' do
28 | document = Nokogiri::XML(response.body)
29 | expect(document.at_xpath('/Response/Say').content)
30 | .to eq('Thank you for taking the bees survey')
31 | end
32 |
33 | it 'responds with a redirection to the first question' do
34 | document = Nokogiri::XML(response.body)
35 | expect(document.at_xpath('/Response/Redirect').content)
36 | .to eq(question_path(question.id))
37 | end
38 |
39 | it 'responds with ok' do
40 | expect(response).to be_ok
41 | end
42 | end
43 |
44 | describe '#sms' do
45 | let!(:first_question) { create(:question, survey: survey, body: 'first') }
46 | let!(:last_question) { create(:question, survey: survey, body: 'last') }
47 |
48 | context 'when the user replies to a question' do
49 | before do
50 | request.cookies[:question] = first_question.serializable_hash.to_yaml
51 | post :sms, params: { Body: 'yes', From: 'from-phone-number' }
52 | end
53 |
54 | it 'responds with the next question' do
55 | expect(content_for('/Response/Message/Body'))
56 | .to eq("last\n\nReply to this message with your answer")
57 | end
58 | end
59 |
60 | it 'responds with ok' do
61 | expect(response).to be_ok
62 | end
63 | end
64 |
65 | private
66 |
67 | def content_for(xpath)
68 | document = Nokogiri::XML(response.body)
69 | document.at_xpath(xpath).content
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | config.cache_classes = false
12 | config.action_view.cache_template_loading = true
13 |
14 | # Do not eager load code on boot. This avoids loading your whole application
15 | # just for the purpose of running a single test. If you are using a tool that
16 | # preloads Rails for running tests, you may have to set it to true.
17 | config.eager_load = false
18 |
19 | # Configure public file server for tests with Cache-Control for performance.
20 | config.public_file_server.enabled = true
21 | config.public_file_server.headers = {
22 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
23 | }
24 |
25 | # Show full error reports and disable caching.
26 | config.consider_all_requests_local = true
27 | config.action_controller.perform_caching = false
28 | config.cache_store = :null_store
29 |
30 | # Raise exceptions instead of rendering exception templates.
31 | config.action_dispatch.show_exceptions = false
32 |
33 | # Disable request forgery protection in test environment.
34 | config.action_controller.allow_forgery_protection = false
35 |
36 | # Print deprecation notices to the stderr.
37 | config.active_support.deprecation = :stderr
38 |
39 | # Raise exceptions for disallowed deprecations.
40 | config.active_support.disallowed_deprecation = :raise
41 |
42 | # Tell Active Support which deprecation messages to disallow.
43 | config.active_support.disallowed_deprecation_warnings = []
44 |
45 | # Raises error for missing translations.
46 | # config.i18n.raise_on_missing_translations = true
47 |
48 | # Annotate rendered view with file names.
49 | # config.action_view.annotate_rendered_view_with_filenames = true
50 | end
51 |
--------------------------------------------------------------------------------
/config/webpacker.yml:
--------------------------------------------------------------------------------
1 | # Note: You must restart bin/webpack-dev-server for changes to take effect
2 |
3 | default: &default
4 | source_path: app
5 | source_entry_path: packs
6 | public_root_path: public
7 | public_output_path: packs
8 | cache_path: tmp/cache/webpacker
9 | webpack_compile_output: true
10 |
11 | # Additional paths webpack should lookup modules
12 | # ['app/assets', 'engine/foo/app/assets']
13 | additional_paths: []
14 |
15 | # Reload manifest.json on all requests so we reload latest compiled packs
16 | cache_manifest: false
17 |
18 | # Extract and emit a css file
19 | extract_css: false
20 |
21 | static_assets_extensions:
22 | - .jpg
23 | - .jpeg
24 | - .png
25 | - .gif
26 | - .tiff
27 | - .ico
28 | - .svg
29 | - .eot
30 | - .otf
31 | - .ttf
32 | - .woff
33 | - .woff2
34 |
35 | extensions:
36 | - .mjs
37 | - .js
38 | - .sass
39 | - .scss
40 | - .css
41 | - .module.sass
42 | - .module.scss
43 | - .module.css
44 | - .png
45 | - .svg
46 | - .gif
47 | - .jpeg
48 | - .jpg
49 |
50 | development:
51 | <<: *default
52 | compile: true
53 |
54 | # Reference: https://webpack.js.org/configuration/dev-server/
55 | dev_server:
56 | https: false
57 | host: localhost
58 | port: 3035
59 | public: localhost:3035
60 | hmr: false
61 | # Inline should be set to true if using HMR
62 | inline: true
63 | overlay: true
64 | compress: true
65 | disable_host_check: true
66 | use_local_ip: false
67 | quiet: false
68 | pretty: false
69 | headers:
70 | 'Access-Control-Allow-Origin': '*'
71 | watch_options:
72 | ignored: '**/node_modules/**'
73 |
74 |
75 | test:
76 | <<: *default
77 | compile: true
78 |
79 | # Compile test packs to a separate directory
80 | public_output_path: packs-test
81 |
82 | production:
83 | <<: *default
84 |
85 | # Production depends on precompilation of packs prior to booting for performance.
86 | compile: false
87 |
88 | # Extract and emit a css file
89 | extract_css: true
90 |
91 | # Cache manifest.json for performance
92 | cache_manifest: true
93 |
--------------------------------------------------------------------------------
/spec/lib/sms/tracked_question_spec.rb:
--------------------------------------------------------------------------------
1 | # require_relative '../../../lib/sms/tracked_question'
2 | require 'rails_helper'
3 |
4 | describe SMS::TrackedQuestion do
5 | subject(:tracked_question) { described_class.new(cookies) }
6 |
7 | let!(:question) { build_stubbed(:question) }
8 | let!(:serialized_question) { question.serializable_hash.to_yaml }
9 |
10 | describe '#store_or_destroy' do
11 | let(:cookies) { {} }
12 |
13 | it 'stores the question' do
14 | tracked_question.store_or_destroy(question)
15 | expect(cookies[:question]).to eq(serialized_question)
16 | end
17 |
18 | context 'when the given question is Question::NoQuestion' do
19 | let(:cookies) { { question: serialized_question } }
20 |
21 | it 'destroys the question' do
22 | subject.store_or_destroy(Question::NoQuestion)
23 | expect(cookies[:question]).to be_nil
24 | end
25 | end
26 | end
27 |
28 | describe '#fetch' do
29 | context 'when there are a tracked question' do
30 | let(:cookies) { { question: serialized_question } }
31 |
32 | it 'returns the question' do
33 | expect(tracked_question.fetch).to eq(question)
34 | end
35 | end
36 | end
37 |
38 | describe '#destroy' do
39 | let(:cookies) { { question: serialized_question } }
40 |
41 | it 'destroys the question' do
42 | subject.destroy
43 | expect(cookies[:question]).to be_nil
44 | end
45 | end
46 |
47 | describe '#empty?' do
48 | context 'when there is no tracked question' do
49 | let(:cookies) { { question: '' } }
50 |
51 | it 'returns true' do
52 | expect(tracked_question.empty?).to be_truthy
53 | end
54 | end
55 |
56 | context 'when there is a tracked question' do
57 | let(:cookies) { { question: serialized_question } }
58 |
59 | it 'returns false' do
60 | expect(tracked_question.empty?).to be_falsey
61 | end
62 | end
63 | end
64 |
65 | describe '#present?' do
66 | context 'when there is a tracked question' do
67 | let(:cookies) { { question: serialized_question } }
68 |
69 | it 'returns true' do
70 | expect(tracked_question.present?).to be_truthy
71 | end
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is copied to spec/ when you run 'rails generate rspec:install'
4 | require 'spec_helper'
5 | ENV['RAILS_ENV'] ||= 'test'
6 |
7 | require File.expand_path('../config/environment', __dir__)
8 |
9 | # Prevent database truncation if the environment is production
10 | if Rails.env.production?
11 | abort('The Rails environment is running in production mode!')
12 | end
13 | require 'rspec/rails'
14 | # Add additional requires below this line. Rails is not loaded until this point!
15 |
16 | # Requires supporting ruby files with custom matchers and macros, etc, in
17 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
18 | # run as spec files by default. This means that files in spec/support that end
19 | # in _spec.rb will both be required and run as specs, causing the specs to be
20 | # run twice. It is recommended that you do not name files matching this glob to
21 | # end with _spec.rb. You can configure this pattern with the --pattern
22 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
23 | ActiveRecord::Migration.maintain_test_schema!
24 |
25 | RSpec.configure do |config|
26 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
27 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
28 |
29 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
30 | # examples within a transaction, remove the following line or assign false
31 | # instead of true.
32 | config.use_transactional_fixtures = true
33 |
34 | # RSpec Rails can automatically mix in different behaviours to your tests
35 | # based on their file location, for example enabling you to call `get` and
36 | # `post` in specs under `spec/controllers`.
37 | #
38 | # You can disable this behaviour by removing the line below, and instead
39 | # explicitly tag your specs with their type, e.g.:
40 | #
41 | # RSpec.describe UsersController, :type => :controller do
42 | # # ...
43 | # end
44 | #
45 | # The different available types are documented in the features, such as in
46 | # https://relishapp.com/rspec/rspec-rails/docs
47 | config.infer_spec_type_from_file_location!
48 |
49 | # Filter lines from Rails gems in backtraces.
50 | config.filter_rails_from_backtrace!
51 | # arbitrary gems may also be filtered via:
52 | # config.filter_gems_from_backtrace("gem name")
53 | config.include FactoryBot::Syntax::Methods
54 | end
55 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.cache_classes = false
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable/disable caching. By default caching is disabled.
18 | # Run rails dev:cache to toggle caching.
19 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
20 | config.action_controller.perform_caching = true
21 | config.action_controller.enable_fragment_cache_logging = true
22 |
23 | config.cache_store = :memory_store
24 | config.public_file_server.headers = {
25 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
26 | }
27 | else
28 | config.action_controller.perform_caching = false
29 |
30 | config.cache_store = :null_store
31 | end
32 |
33 | # Allow ngrok urls
34 | config.hosts << /[a-z0-9]+\.ngrok\.io/
35 |
36 | # Print deprecation notices to the Rails logger.
37 | config.active_support.deprecation = :log
38 |
39 | # Raise exceptions for disallowed deprecations.
40 | config.active_support.disallowed_deprecation = :raise
41 |
42 | # Tell Active Support which deprecation messages to disallow.
43 | config.active_support.disallowed_deprecation_warnings = []
44 |
45 |
46 | # Raises error for missing translations.
47 | # config.i18n.raise_on_missing_translations = true
48 |
49 | # Annotate rendered view with file names.
50 | # config.action_view.annotate_rendered_view_with_filenames = true
51 |
52 | # Use an evented file watcher to asynchronously detect changes in source code,
53 | # routes, locales, etc. This feature depends on the listen gem.
54 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
55 |
56 | Rails.logger = Logger.new(STDOUT)
57 | Rails.logger.datetime_format = '%Y-%m-%d %H:%M:%S'
58 |
59 | # log formatter
60 | Rails.logger.formatter = proc do |severity, datetime, progname, msg|
61 | "#{datetime}, #{severity}: #{progname} #{msg} \n"
62 | end
63 | config.logger = ActiveSupport::Logger.new("log/#{Rails.env}.log")
64 | end
65 |
--------------------------------------------------------------------------------
/spec/lib/sms/reply_processor_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe SMS::ReplyProcessor do
4 | describe '.process' do
5 | subject { described_class.process(message, 'from-phone-number', cookies) }
6 |
7 | let(:survey) { create(:survey, title: 'bees') }
8 |
9 | let!(:first_question) { create(:question, survey: survey, body: 'first') }
10 | let!(:last_question) { create(:question, survey: survey, body: 'last') }
11 |
12 | context 'when there are no tracked questions' do
13 | let(:message) { 'start' }
14 | let(:cookies) { {} }
15 |
16 | it 'responds with the first question' do
17 | expect(content_for('/Response/Message/Body'))
18 | .to eq("first\n\nReply to this message with your answer")
19 | end
20 |
21 | it 'track the current question' do
22 | subject
23 | expect(cookies[:question]).to include(first_question.id.to_s)
24 | end
25 | end
26 |
27 | context 'when there are a tracked question' do
28 | let(:message) { 'answer for the question' }
29 | let(:cookies) { { question: serialize_question(first_question) } }
30 |
31 | it 'creates an answer' do
32 | expect do
33 | subject
34 | end.to change { Answer.count }.by(1)
35 | end
36 |
37 | context 'when there are questions available' do
38 | it 'responds with the next available question' do
39 | expect(content_for('/Response/Message/Body'))
40 | .to eq("last\n\nReply to this message with your answer")
41 | end
42 |
43 | it 'track the current question' do
44 | subject
45 | expect(cookies[:question]).to include(last_question.id.to_s)
46 | end
47 | end
48 |
49 | context 'when there are no more questions available' do
50 | let(:cookies) { { question: serialize_question(last_question) } }
51 |
52 | it 'responds with the exit message' do
53 | expect(content_for('/Response/Message/Body'))
54 | .to eq('Thanks for your time. Good bye')
55 | end
56 |
57 | it 'untrack the current question' do
58 | subject
59 | expect(cookies[:question]).to be_nil
60 | end
61 | end
62 | end
63 | end
64 |
65 | private
66 |
67 | def content_for(xpath)
68 | document = Nokogiri::XML(subject)
69 | document.at_xpath(xpath).content
70 | end
71 |
72 | def serialize_question(question)
73 | question.serializable_hash.to_yaml
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 20160404115135) do
15 |
16 | # These are extensions that must be enabled in order to support this database
17 | enable_extension "plpgsql"
18 |
19 | create_table "answers", force: :cascade do |t|
20 | t.integer "question_id", null: false
21 | t.integer "source", default: 0, null: false
22 | t.string "content", null: false
23 | t.string "from", null: false
24 | t.datetime "created_at", null: false
25 | t.datetime "updated_at", null: false
26 | t.string "call_sid", default: ""
27 | end
28 |
29 | add_index "answers", ["call_sid"], name: "index_answers_on_call_sid", using: :btree
30 | add_index "answers", ["from"], name: "index_answers_on_from", using: :btree
31 | add_index "answers", ["question_id"], name: "index_answers_on_question_id", using: :btree
32 | add_index "answers", ["source"], name: "index_answers_on_source", using: :btree
33 |
34 | create_table "questions", force: :cascade do |t|
35 | t.integer "survey_id", null: false
36 | t.string "body", null: false
37 | t.integer "type", default: 0, null: false
38 | t.datetime "created_at", null: false
39 | t.datetime "updated_at", null: false
40 | end
41 |
42 | add_index "questions", ["survey_id"], name: "index_questions_on_survey_id", using: :btree
43 | add_index "questions", ["type"], name: "index_questions_on_type", using: :btree
44 |
45 | create_table "surveys", force: :cascade do |t|
46 | t.string "title", null: false
47 | t.datetime "created_at", null: false
48 | t.datetime "updated_at", null: false
49 | end
50 |
51 | create_table "transcriptions", force: :cascade do |t|
52 | t.integer "answer_id"
53 | t.string "text", default: ""
54 | t.datetime "created_at", null: false
55 | t.datetime "updated_at", null: false
56 | end
57 |
58 | add_index "transcriptions", ["answer_id"], name: "index_transcriptions_on_answer_id", using: :btree
59 |
60 | add_foreign_key "answers", "questions"
61 | add_foreign_key "questions", "surveys"
62 | end
63 |
--------------------------------------------------------------------------------
/spec/lib/voice/create_response_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe Voice::CreateResponse do
4 | describe '.for' do
5 | subject { described_class.for(question) }
6 |
7 | let(:question_type) { 'free' }
8 | let(:question) do
9 | build_stubbed(:question, body: 'question?', type: question_type)
10 | end
11 |
12 | it 'creates a response with the question body' do
13 | expect(content_for('/Response/Say[1]'))
14 | .to eq('question?')
15 | end
16 |
17 | context 'when the question type is "free"' do
18 | it 'uses an instruction for free questions' do
19 | expect(content_for('/Response/Say[2]'))
20 | .to eq('Please record your answer after the beep and then hit the pound sign')
21 | end
22 |
23 | it 'uses a record with an action to the given question' do
24 | expect(content_for('/Response/Record/@action'))
25 | .to eq("/answers?question_id=#{question.id}")
26 | end
27 |
28 | it 'uses a record with transcribe set to true' do
29 | expect(content_for('/Response/Record/@transcribe'))
30 | .to eq("true")
31 | end
32 |
33 | it 'uses a record with a transcribe callback' do
34 | expect(content_for('/Response/Record/@transcribeCallback'))
35 | .to eq("/transcriptions?question_id=#{question.id}")
36 | end
37 | end
38 |
39 | context 'when the question type is "numeric"' do
40 | let(:question_type) { 'numeric' }
41 |
42 | it 'uses an instruction for numeric questions' do
43 | expect(content_for('/Response/Say[2]'))
44 | .to eq('Please press a number between 0 and 9 and then hit the pound sign')
45 | end
46 |
47 | it 'uses a gather with an action to the given question' do
48 | expect(content_for('/Response/Gather/@action'))
49 | .to eq("/answers?question_id=#{question.id}")
50 | end
51 | end
52 |
53 | context 'when the question type is "yes_no"' do
54 | let(:question_type) { 'yes_no' }
55 |
56 | it 'uses an instruction for yes_no questions' do
57 | expect(content_for('/Response/Say[2]'))
58 | .to eq('Please press the 1 for yes and the 0 for no and then hit the pound sign')
59 | end
60 |
61 | it 'uses a gather with an action to the given question' do
62 | expect(content_for('/Response/Gather/@action'))
63 | .to eq("/answers?question_id=#{question.id}")
64 | end
65 | end
66 |
67 | context 'when the question is Question::NoQuestion' do
68 | let(:question) { Question::NoQuestion }
69 |
70 | it 'responds with a closing message' do
71 | expect(content_for('/Response/Say'))
72 | .to eq('Thanks for your time. Good bye')
73 | end
74 |
75 | it 'responds hanging up' do
76 | expect(content_for('/Response/Hangup')).to_not be_nil
77 | end
78 | end
79 | end
80 |
81 | private
82 |
83 | def content_for(xpath)
84 | document = Nokogiri::XML(subject)
85 | document.at_xpath(xpath).content
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Automated Surveys with Ruby on Rails and Twilio
6 |
7 | 
8 |
9 | This application demonstrates how to use Twilio and TwiML to perform automated phone surveys.
10 |
11 | [Read the full tutorial here!](https://www.twilio.com/docs/howto/walkthrough/automated-survey/ruby/rails)
12 |
13 | ## Note: protect your webhooks
14 |
15 | Twilio supports HTTP Basic and Digest Authentication. Authentication allows you to password protect your TwiML URLs on your web server so that only you and Twilio can access them.
16 |
17 | Learn more about HTTP authentication [here](https://www.twilio.com/docs/usage/security#http-authentication), which includes sample code you can use to secure your web application by validating incoming Twilio requests.
18 |
19 | ## Local development
20 |
21 | This project is built using the [Ruby on Rails](http://rubyonrails.org/) web framework and NodeJS to serve assets through Webpack.
22 |
23 | 1. First clone this repository and `cd` into it.
24 |
25 | ```bash
26 | $ git clone git@github.com:TwilioDevEd/automated-survey-rails.git
27 | $ cd automated-survey-rails
28 | ```
29 |
30 | 1. Install Rails dependencies
31 |
32 | ```bash
33 | $ bundle install
34 | ```
35 |
36 | 1. Install Node dependencies
37 |
38 | ```bash
39 | $ npm install
40 | ```
41 |
42 | 1. Create the database and run migrations.
43 |
44 | _Make sure you have installed [PostgreSQL](http://www.postgresql.org/). If on
45 | a Mac, I recommend [Postgres.app](http://postgresapp.com)_.
46 |
47 | ```bash
48 | $ bundle exec rails db:setup
49 | ```
50 |
51 | 1. Make sure the tests succeed.
52 |
53 | ```bash
54 | $ bundle exec rspec
55 | ```
56 |
57 | 1. Run the server
58 |
59 | ```bash
60 | $ bundle exec rails s
61 | ```
62 |
63 | 1. Expose your application to the wider internet using [ngrok](http://ngrok.com). You can click [here](#expose-the-application-to-the-wider-internet) for more details. This step is important because the application won't work as expected if you run it through localhost.
64 |
65 | ```bash
66 | $ ngrok http 3000
67 | ```
68 |
69 | Once ngrok is running, open up your browser and go to your ngrok URL. It will look something like this: `http://9a159ccf.ngrok.io`
70 |
71 | 1. Configure Twilio to call your webhooks
72 |
73 | You will also need to configure Twilio to call your application when calls or messages are received on your _Twilio Phone Number_.
74 |
75 | The **Voice Request URL** should look something like this:
76 |
77 | ```
78 | http://.ngrok.io/surveys/voice
79 | ```
80 |
81 | The **SMS & MMS Request URL** should look something like this:
82 |
83 | ```
84 | http://.ngrok.io/surveys/sms
85 | ```
86 |
87 | That's it!
88 |
89 | ### How To Demo
90 | _Voice Surveys_. Call your Twilio phone number and follow the instructions.
91 |
92 | _SMS Surveys_. Text your Twilio phone number with any text and follow the instructions.
93 |
94 | ## Meta
95 |
96 | * No warranty expressed or implied. Software is as is. Diggity.
97 | * [MIT License](LICENSE)
98 | * Lovingly crafted by Twilio Developer Education.
99 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at open-source@twilio.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'bundle' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "rubygems"
12 |
13 | m = Module.new do
14 | module_function
15 |
16 | def invoked_as_script?
17 | File.expand_path($0) == File.expand_path(__FILE__)
18 | end
19 |
20 | def env_var_version
21 | ENV["BUNDLER_VERSION"]
22 | end
23 |
24 | def cli_arg_version
25 | return unless invoked_as_script? # don't want to hijack other binstubs
26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27 | bundler_version = nil
28 | update_index = nil
29 | ARGV.each_with_index do |a, i|
30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31 | bundler_version = a
32 | end
33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34 | bundler_version = $1
35 | update_index = i
36 | end
37 | bundler_version
38 | end
39 |
40 | def gemfile
41 | gemfile = ENV["BUNDLE_GEMFILE"]
42 | return gemfile if gemfile && !gemfile.empty?
43 |
44 | File.expand_path("../../Gemfile", __FILE__)
45 | end
46 |
47 | def lockfile
48 | lockfile =
49 | case File.basename(gemfile)
50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
51 | else "#{gemfile}.lock"
52 | end
53 | File.expand_path(lockfile)
54 | end
55 |
56 | def lockfile_version
57 | return unless File.file?(lockfile)
58 | lockfile_contents = File.read(lockfile)
59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60 | Regexp.last_match(1)
61 | end
62 |
63 | def bundler_version
64 | @bundler_version ||=
65 | env_var_version || cli_arg_version ||
66 | lockfile_version
67 | end
68 |
69 | def bundler_requirement
70 | return "#{Gem::Requirement.default}.a" unless bundler_version
71 |
72 | bundler_gem_version = Gem::Version.new(bundler_version)
73 |
74 | requirement = bundler_gem_version.approximate_recommendation
75 |
76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0")
77 |
78 | requirement += ".a" if bundler_gem_version.prerelease?
79 |
80 | requirement
81 | end
82 |
83 | def load_bundler!
84 | ENV["BUNDLE_GEMFILE"] ||= gemfile
85 |
86 | activate_bundler
87 | end
88 |
89 | def activate_bundler
90 | gem_error = activation_error_handling do
91 | gem "bundler", bundler_requirement
92 | end
93 | return if gem_error.nil?
94 | require_error = activation_error_handling do
95 | require "bundler/version"
96 | end
97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
99 | exit 42
100 | end
101 |
102 | def activation_error_handling
103 | yield
104 | nil
105 | rescue StandardError, LoadError => e
106 | e
107 | end
108 | end
109 |
110 | m.load_bundler!
111 |
112 | if m.invoked_as_script?
113 | load Gem.bin_path("bundler", "bundle")
114 | end
115 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.cache_classes = true
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowing both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
26 |
27 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
28 | # config.asset_host = 'http://assets.example.com'
29 |
30 | # Specifies the header that your server uses for sending files.
31 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
32 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
33 |
34 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
35 | # config.force_ssl = true
36 |
37 | # Include generic and useful information about system operation, but avoid logging too much
38 | # information to avoid inadvertent exposure of personally identifiable information (PII).
39 | config.log_level = :info
40 |
41 | # Prepend all log lines with the following tags.
42 | config.log_tags = [ :request_id ]
43 |
44 | # Use a different cache store in production.
45 | # config.cache_store = :mem_cache_store
46 |
47 | # Use a real queuing backend for Active Job (and separate queues per environment).
48 | # config.active_job.queue_adapter = :resque
49 | # config.active_job.queue_name_prefix = "sample_template_rails_production"
50 |
51 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
52 | # the I18n.default_locale when a translation cannot be found).
53 | config.i18n.fallbacks = true
54 |
55 | # Send deprecation notices to registered listeners.
56 | config.active_support.deprecation = :notify
57 |
58 | # Log disallowed deprecations.
59 | config.active_support.disallowed_deprecation = :log
60 |
61 | # Tell Active Support which deprecation messages to disallow.
62 | config.active_support.disallowed_deprecation_warnings = []
63 |
64 | # Use default logging formatter so that PID and timestamp are not suppressed.
65 | config.log_formatter = ::Logger::Formatter.new
66 |
67 | # Use a different logger for distributed setups.
68 | # require "syslog/logger"
69 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
70 |
71 | if ENV["RAILS_LOG_TO_STDOUT"].present?
72 | logger = ActiveSupport::Logger.new(STDOUT)
73 | logger.formatter = config.log_formatter
74 | config.logger = ActiveSupport::TaggedLogging.new(logger)
75 | end
76 |
77 | # Inserts middleware to perform automatic connection switching.
78 | # The `database_selector` hash is used to pass options to the DatabaseSelector
79 | # middleware. The `delay` is used to determine how long to wait after a write
80 | # to send a subsequent read to the primary.
81 | #
82 | # The `database_resolver` class is used by the middleware to determine which
83 | # database is appropriate to use based on the time delay.
84 | #
85 | # The `database_resolver_context` class is used by the middleware to set
86 | # timestamps for the last write to the primary. The resolver uses the context
87 | # class timestamps to determine how long to wait before reading from the
88 | # replica.
89 | #
90 | # By default Rails will store a last write timestamp in the session. The
91 | # DatabaseSelector middleware is designed as such you can define your own
92 | # strategy for connection switching and pass that into the middleware through
93 | # these configuration options.
94 | # config.active_record.database_selector = { delay: 2.seconds }
95 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
96 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
97 | end
98 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (7.0.4.3)
5 | actionpack (= 7.0.4.3)
6 | activesupport (= 7.0.4.3)
7 | nio4r (~> 2.0)
8 | websocket-driver (>= 0.6.1)
9 | actionmailbox (7.0.4.3)
10 | actionpack (= 7.0.4.3)
11 | activejob (= 7.0.4.3)
12 | activerecord (= 7.0.4.3)
13 | activestorage (= 7.0.4.3)
14 | activesupport (= 7.0.4.3)
15 | mail (>= 2.7.1)
16 | net-imap
17 | net-pop
18 | net-smtp
19 | actionmailer (7.0.4.3)
20 | actionpack (= 7.0.4.3)
21 | actionview (= 7.0.4.3)
22 | activejob (= 7.0.4.3)
23 | activesupport (= 7.0.4.3)
24 | mail (~> 2.5, >= 2.5.4)
25 | net-imap
26 | net-pop
27 | net-smtp
28 | rails-dom-testing (~> 2.0)
29 | actionpack (7.0.4.3)
30 | actionview (= 7.0.4.3)
31 | activesupport (= 7.0.4.3)
32 | rack (~> 2.0, >= 2.2.0)
33 | rack-test (>= 0.6.3)
34 | rails-dom-testing (~> 2.0)
35 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
36 | actiontext (7.0.4.3)
37 | actionpack (= 7.0.4.3)
38 | activerecord (= 7.0.4.3)
39 | activestorage (= 7.0.4.3)
40 | activesupport (= 7.0.4.3)
41 | globalid (>= 0.6.0)
42 | nokogiri (>= 1.8.5)
43 | actionview (7.0.4.3)
44 | activesupport (= 7.0.4.3)
45 | builder (~> 3.1)
46 | erubi (~> 1.4)
47 | rails-dom-testing (~> 2.0)
48 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
49 | activejob (7.0.4.3)
50 | activesupport (= 7.0.4.3)
51 | globalid (>= 0.3.6)
52 | activemodel (7.0.4.3)
53 | activesupport (= 7.0.4.3)
54 | activerecord (7.0.4.3)
55 | activemodel (= 7.0.4.3)
56 | activesupport (= 7.0.4.3)
57 | activestorage (7.0.4.3)
58 | actionpack (= 7.0.4.3)
59 | activejob (= 7.0.4.3)
60 | activerecord (= 7.0.4.3)
61 | activesupport (= 7.0.4.3)
62 | marcel (~> 1.0)
63 | mini_mime (>= 1.1.0)
64 | activesupport (7.0.4.3)
65 | concurrent-ruby (~> 1.0, >= 1.0.2)
66 | i18n (>= 1.6, < 2)
67 | minitest (>= 5.1)
68 | tzinfo (~> 2.0)
69 | autoprefixer-rails (10.4.7.0)
70 | execjs (~> 2)
71 | bindex (0.8.1)
72 | bootsnap (1.16.0)
73 | msgpack (~> 1.2)
74 | bootstrap (5.2.3)
75 | autoprefixer-rails (>= 9.1.0)
76 | popper_js (>= 2.11.6, < 3)
77 | sassc-rails (>= 2.0.0)
78 | builder (3.2.4)
79 | byebug (11.1.3)
80 | concurrent-ruby (1.2.2)
81 | crass (1.0.6)
82 | date (3.3.3)
83 | diff-lcs (1.5.0)
84 | erubi (1.12.0)
85 | execjs (2.8.1)
86 | factory_bot (6.2.0)
87 | activesupport (>= 5.0.0)
88 | factory_bot_rails (6.2.0)
89 | factory_bot (~> 6.2.0)
90 | railties (>= 5.0.0)
91 | faraday (2.7.10)
92 | faraday-net_http (>= 2.0, < 3.1)
93 | ruby2_keywords (>= 0.0.4)
94 | faraday-net_http (3.0.2)
95 | ffi (1.15.5)
96 | ffi (1.15.5-x64-mingw32)
97 | globalid (1.1.0)
98 | activesupport (>= 5.0)
99 | i18n (1.13.0)
100 | concurrent-ruby (~> 1.0)
101 | jbuilder (2.11.5)
102 | actionview (>= 5.0.0)
103 | activesupport (>= 5.0.0)
104 | jwt (2.7.1)
105 | listen (3.8.0)
106 | rb-fsevent (~> 0.10, >= 0.10.3)
107 | rb-inotify (~> 0.9, >= 0.9.10)
108 | loofah (2.21.3)
109 | crass (~> 1.0.2)
110 | nokogiri (>= 1.12.0)
111 | mail (2.8.1)
112 | mini_mime (>= 0.1.1)
113 | net-imap
114 | net-pop
115 | net-smtp
116 | marcel (1.0.2)
117 | method_source (1.0.0)
118 | mini_mime (1.1.2)
119 | mini_portile2 (2.8.2)
120 | minitest (5.18.0)
121 | msgpack (1.6.0)
122 | net-imap (0.3.4)
123 | date
124 | net-protocol
125 | net-pop (0.1.2)
126 | net-protocol
127 | net-protocol (0.2.1)
128 | timeout
129 | net-smtp (0.3.3)
130 | net-protocol
131 | nio4r (2.5.9)
132 | nokogiri (1.15.3)
133 | mini_portile2 (~> 2.8.2)
134 | racc (~> 1.4)
135 | pg (1.5.3)
136 | popper_js (2.11.6)
137 | puma (6.3.0)
138 | nio4r (~> 2.0)
139 | racc (1.7.1)
140 | rack (2.2.7)
141 | rack-mini-profiler (3.1.0)
142 | rack (>= 1.2.0)
143 | rack-proxy (0.7.6)
144 | rack
145 | rack-test (2.1.0)
146 | rack (>= 1.3)
147 | rails (7.0.4.3)
148 | actioncable (= 7.0.4.3)
149 | actionmailbox (= 7.0.4.3)
150 | actionmailer (= 7.0.4.3)
151 | actionpack (= 7.0.4.3)
152 | actiontext (= 7.0.4.3)
153 | actionview (= 7.0.4.3)
154 | activejob (= 7.0.4.3)
155 | activemodel (= 7.0.4.3)
156 | activerecord (= 7.0.4.3)
157 | activestorage (= 7.0.4.3)
158 | activesupport (= 7.0.4.3)
159 | bundler (>= 1.15.0)
160 | railties (= 7.0.4.3)
161 | rails-controller-testing (1.0.5)
162 | actionpack (>= 5.0.1.rc1)
163 | actionview (>= 5.0.1.rc1)
164 | activesupport (>= 5.0.1.rc1)
165 | rails-dom-testing (2.0.3)
166 | activesupport (>= 4.2.0)
167 | nokogiri (>= 1.6)
168 | rails-html-sanitizer (1.6.0)
169 | loofah (~> 2.21)
170 | nokogiri (~> 1.14)
171 | railties (7.0.4.3)
172 | actionpack (= 7.0.4.3)
173 | activesupport (= 7.0.4.3)
174 | method_source
175 | rake (>= 12.2)
176 | thor (~> 1.0)
177 | zeitwerk (~> 2.5)
178 | rake (13.0.6)
179 | rb-fsevent (0.11.2)
180 | rb-inotify (0.10.1)
181 | ffi (~> 1.0)
182 | rspec-core (3.12.2)
183 | rspec-support (~> 3.12.0)
184 | rspec-expectations (3.12.3)
185 | diff-lcs (>= 1.2.0, < 2.0)
186 | rspec-support (~> 3.12.0)
187 | rspec-mocks (3.12.5)
188 | diff-lcs (>= 1.2.0, < 2.0)
189 | rspec-support (~> 3.12.0)
190 | rspec-rails (6.0.3)
191 | actionpack (>= 6.1)
192 | activesupport (>= 6.1)
193 | railties (>= 6.1)
194 | rspec-core (~> 3.12)
195 | rspec-expectations (~> 3.12)
196 | rspec-mocks (~> 3.12)
197 | rspec-support (~> 3.12)
198 | rspec-support (3.12.0)
199 | ruby2_keywords (0.0.5)
200 | sassc (2.4.0)
201 | ffi (~> 1.9)
202 | sassc (2.4.0-x64-mingw32)
203 | ffi (~> 1.9)
204 | sassc-rails (2.1.2)
205 | railties (>= 4.0.0)
206 | sassc (>= 2.0)
207 | sprockets (> 3.0)
208 | sprockets-rails
209 | tilt
210 | semantic_range (3.0.0)
211 | spring (4.1.1)
212 | sprockets (4.1.1)
213 | concurrent-ruby (~> 1.0)
214 | rack (> 1, < 3)
215 | sprockets-rails (3.4.2)
216 | actionpack (>= 5.2)
217 | activesupport (>= 5.2)
218 | sprockets (>= 3.0.0)
219 | thor (1.2.2)
220 | tilt (2.0.11)
221 | timeout (0.3.2)
222 | turbolinks (5.2.1)
223 | turbolinks-source (~> 5.2)
224 | turbolinks-source (5.2.0)
225 | twilio-ruby (6.3.0)
226 | faraday (>= 0.9, < 3.0)
227 | jwt (>= 1.5, < 3.0)
228 | nokogiri (>= 1.6, < 2.0)
229 | tzinfo (2.0.6)
230 | concurrent-ruby (~> 1.0)
231 | tzinfo-data (1.2021.1)
232 | tzinfo (>= 1.0.0)
233 | web-console (4.2.0)
234 | actionview (>= 6.0.0)
235 | activemodel (>= 6.0.0)
236 | bindex (>= 0.4.0)
237 | railties (>= 6.0.0)
238 | webpacker (5.4.4)
239 | activesupport (>= 5.2)
240 | rack-proxy (>= 0.6.1)
241 | railties (>= 5.2)
242 | semantic_range (>= 2.3.0)
243 | websocket-driver (0.7.5)
244 | websocket-extensions (>= 0.1.0)
245 | websocket-extensions (0.1.5)
246 | zeitwerk (2.6.8)
247 |
248 | PLATFORMS
249 | universal-darwin-19
250 | x64-mingw32
251 | x86_64-linux
252 |
253 | DEPENDENCIES
254 | bootsnap (>= 1.4.4)
255 | bootstrap (~> 5.2.3)
256 | byebug
257 | factory_bot_rails
258 | jbuilder (~> 2.11)
259 | listen (~> 3.8)
260 | pg
261 | puma (~> 6.3)
262 | rack-mini-profiler (~> 3.1)
263 | rails (~> 7.0.4)
264 | rails-controller-testing
265 | rspec-rails
266 | spring
267 | turbolinks (~> 5)
268 | twilio-ruby (~> 6.3)
269 | tzinfo-data
270 | web-console (>= 4.1.0)
271 | webpacker (~> 5.4)
272 |
273 | RUBY VERSION
274 | ruby 3.0.0p0
275 |
276 | BUNDLED WITH
277 | 2.2.6
278 |
--------------------------------------------------------------------------------