├── .browserslistrc ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── .mergify.yml ├── .nvmrc ├── .rspec ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ └── images │ │ └── .keep ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── answers_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ ├── questions_controller.rb │ ├── surveys_controller.rb │ └── transcriptions_controller.rb ├── helpers │ └── application_helper.rb ├── jobs │ └── application_job.rb ├── mailers │ ├── .keep │ └── application_mailer.rb ├── models │ ├── .keep │ ├── answer.rb │ ├── concerns │ │ └── .keep │ ├── question.rb │ ├── survey.rb │ └── transcription.rb ├── packs │ ├── javascript │ │ └── application.js │ └── stylesheets │ │ └── application.scss └── views │ ├── layouts │ └── application.html.erb │ └── surveys │ └── index.html.erb ├── babel.config.js ├── bin ├── bundle ├── rails ├── rake ├── setup ├── spring ├── update ├── webpack ├── webpack-dev-server └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── permissions_policy.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ ├── en.bootstrap.yml │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml ├── spring.rb ├── webpack │ ├── development.js │ ├── environment.js │ ├── production.js │ └── test.js └── webpacker.yml ├── db ├── migrate │ ├── 20160324144643_create_surveys.rb │ ├── 20160324145530_create_questions.rb │ ├── 20160324154252_create_answers.rb │ ├── 20160404110419_add_call_sid_to_answers.rb │ └── 20160404115135_create_transcriptions.rb ├── schema.rb └── seeds.rb ├── lib ├── assets │ └── .keep ├── find_next_question.rb ├── sms │ ├── create_response.rb │ ├── reply_processor.rb │ └── tracked_question.rb ├── tasks │ └── .keep └── voice │ └── create_response.rb ├── log └── .keep ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── 404.html ├── 422.html ├── 500.html ├── MEff241a3f1542a4b31d0ae213b96f9c59.jpg ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── spec ├── controllers │ ├── answers_controller_spec.rb │ ├── questions_controller_spec.rb │ ├── surveys_controller_spec.rb │ └── transcriptions_controller_spec.rb ├── factories │ ├── questions.rb │ └── surveys.rb ├── lib │ ├── find_next_question_spec.rb │ ├── sms │ │ ├── create_response_spec.rb │ │ ├── reply_processor_spec.rb │ │ └── tracked_question_spec.rb │ └── voice │ │ └── create_response_spec.rb ├── rails_helper.rb └── spec_helper.rb ├── storage └── .keep ├── tmp └── .keep ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.15.4 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | --order rand 4 | --require spec_helper 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | # Automated Surveys with Ruby on Rails and Twilio 6 | 7 | ![](https://github.com/TwilioDevEd/automated-survey-rails/actions/workflows/build.yml/badge.svg) 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 | -------------------------------------------------------------------------------- /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/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/app/assets/images/.keep -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/app/mailers/.keep -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/app/models/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/app/models/concerns/.keep -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /app/models/transcription.rb: -------------------------------------------------------------------------------- 1 | class Transcription < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |
Made with ❤️ by your pals 30 | @twilio
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /app/views/surveys/index.html.erb: -------------------------------------------------------------------------------- 1 |

Results for <%= @survey.title %>

2 | 3 | <% @survey.questions.each do |question| %> 4 |
5 |
6 | <%= question.body %> 7 |
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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | H6mjhEwPEcDXfPhGXKSIIBaKVAReL3dt90szBW4IFxae7af296vfC83tf/rCU9p8F5KQmTzVCeSxHci12LYffsaORzExGpiD4dzbwECMP2XYq4ecZPmvUHYQHc+o32YpUs45lI7FPyHTd+83RxpTbWUWtkYFiVfjH/7ZouqS5W7XmBKs/yUZbQwv64fUgLUCI9Y9G3Tp4Q+0oVRCj8DG2/9WOgNdGmDtDouQ8ZNh+kNZGawh78EeRoiTWtQdnWFeZe2tciXIaKqaUZFlIqDK1bAaGba3ivk62Nw0Bw+MuHcb0lB3fBR1nxnaT1kCzW+NAuVkWWEtLKb0X8yFqi98Nto3wQECjjWUdrOgENSUMWocUxLHGo8iAKkJZ1hA2S6GsbR72qC8JukULLNmHjAqZLiiGLahXYxHfc3l--Uj+qoSoxXjWqNTuh--GIFbbasSAxfMMtpIBn2o1w== 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport::Inflector.inflections(:en) do |inflect| 2 | inflect.acronym 'SMS' 3 | end 4 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /config/locales/en.bootstrap.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/config/locales/en.bootstrap.yml -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/lib/assets/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/lib/tasks/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/log/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /public/MEff241a3f1542a4b31d0ae213b96f9c59.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/public/MEff241a3f1542a4b31d0ae213b96f9c59.jpg -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/factories/surveys.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :survey do 3 | sequence(:title) { |counter| "survey #{counter}" } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/storage/.keep -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/tmp/.keep -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/automated-survey-rails/d05b2497bfd41cc72cfe3d931b7de402bea4271c/vendor/assets/stylesheets/.keep --------------------------------------------------------------------------------