diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..e94f814 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1 @@ +defaults diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fc96f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-* + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore uploaded files in development. +/storage/* +!/storage/.keep + +/public/assets +.byebug_history + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/public/packs +/public/packs-test +/node_modules +/yarn-error.log +yarn-debug.log* +.yarn-integrity + + +## Emacs +**~ +**# + +*.lock +.ruby-version +postcss.config.js \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..40c9759 --- /dev/null +++ b/Gemfile @@ -0,0 +1,58 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby '3.0.2' + +gem 'rails', '>=6.1.3.2' +gem 'image_processing', '1.9.3' +gem 'mini_magick', '4.9.5' +gem 'active_storage_validations', '0.8.9' +gem 'bcrypt', '3.1.13' +gem 'faker', '2.11.0' +gem 'will_paginate', '3.3.0' +gem 'bootstrap-will_paginate', '1.0.0' +gem 'bootstrap-sass', '3.4.1' +gem 'puma', '5.3.1' +gem 'sass-rails', '6.0.0' +gem 'webpacker', '>=5.2.1' +gem 'turbolinks', '5.2.1' +gem 'jbuilder', '2.10.0' +gem 'bootsnap', '1.7.2', require: false + +gem 'wicked_pdf' +gem 'wkhtmltopdf-binary' +gem 'wkhtmltopdf-heroku' + +gem 'pay', '~> 2.0' +gem 'stripe', '< 6.0', '>= 2.8' +# gem 'clicksend_client', '>=5.0.0' +gem 'clicksend_client', :git => 'https://github.com/ClickSend/clicksend-ruby.git' + +group :development, :test do + gem 'sqlite3', '1.4.2' + gem 'byebug', '11.1.3', platforms: [:mri, :mingw, :x64_mingw] +end + +group :development do + gem 'web-console', '4.1.0' + gem 'rack-mini-profiler', '2.3.1' + gem 'listen', '3.4.1' + gem 'spring', '2.1.1' +end + +group :test do + gem 'capybara', '3.35.3' + gem 'selenium-webdriver', '3.142.7' + gem 'webdrivers', '4.6.0' + gem 'rails-controller-testing', '1.0.5' + gem 'minitest', '>=5.11.3' + gem 'minitest-reporters', '1.3.8' + gem 'guard', '2.16.2' + gem 'guard-minitest', '2.4.6' + gem 'rexml' +end + +group :production do + gem 'pg', '1.2.3' + gem 'aws-sdk-s3', '1.87.0', require: false +end diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..c63d1ec --- /dev/null +++ b/Guardfile @@ -0,0 +1,72 @@ +require 'active_support/core_ext/string' +# Defines the matching rules for Guard. +guard :minitest, spring: "bin/rails test", all_on_start: false do + watch(%r{^test/(.*)/?(.*)_test\.rb$}) + watch('test/test_helper.rb') { 'test' } + watch('config/routes.rb') { interface_tests } + watch(%r{app/views/layouts/*}) { interface_tests } + watch(%r{^app/models/(.*?)\.rb$}) do |matches| + ["test/models/#{matches[1]}_test.rb", + "test/integration/microposts_interface_test.rb"] + end + watch(%r{^test/fixtures/(.*?)\.yml$}) do |matches| + "test/models/#{matches[1].singularize}_test.rb" + end + watch(%r{^app/mailers/(.*?)\.rb$}) do |matches| + "test/mailers/#{matches[1]}_test.rb" + end + watch(%r{^app/views/(.*)_mailer/.*$}) do |matches| + "test/mailers/#{matches[1]}_mailer_test.rb" + end + watch(%r{^app/controllers/(.*?)_controller\.rb$}) do |matches| + resource_tests(matches[1]) + end + watch(%r{^app/views/([^/]*?)/.*\.html\.erb$}) do |matches| + ["test/controllers/#{matches[1]}_controller_test.rb"] + + integration_tests(matches[1]) + end + watch(%r{^app/helpers/(.*?)_helper\.rb$}) do |matches| + integration_tests(matches[1]) + end + watch('app/views/layouts/application.html.erb') do + 'test/integration/site_layout_test.rb' + end + watch('app/helpers/sessions_helper.rb') do + integration_tests << 'test/helpers/sessions_helper_test.rb' + end + watch('app/controllers/sessions_controller.rb') do + ['test/controllers/sessions_controller_test.rb', + 'test/integration/users_login_test.rb'] + end + watch('app/controllers/account_activations_controller.rb') do + 'test/integration/users_signup_test.rb' + end + watch(%r{app/views/users/*}) do + resource_tests('users') + + ['test/integration/microposts_interface_test.rb'] + end +end + +# Returns the integration tests corresponding to the given resource. +def integration_tests(resource = :all) + if resource == :all + Dir["test/integration/*"] + else + Dir["test/integration/#{resource}_*.rb"] + end +end + +# Returns all tests that hit the interface. +def interface_tests + integration_tests << "test/controllers" +end + +# Returns the controller tests corresponding to the given resource. +def controller_test(resource) + "test/controllers/#{resource}_controller_test.rb" +end + +# Returns all tests for the given resource. +def resource_tests(resource) + integration_tests(resource) << controller_test(resource) +end diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..474e28f --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +release: rake db:migrate +web: bundle exec puma -C config/puma.rb \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc494a8 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +VocalVoters.com +========== + + Fax, Mail, E-Mail Your Government Representatives + in 30 Seconds or Less. \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000..5918193 --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/images/vocalvoters_logos/vocalvoters-beta.png b/app/assets/images/vocalvoters_logos/vocalvoters-beta.png new file mode 100644 index 0000000..9f18713 Binary files /dev/null and b/app/assets/images/vocalvoters_logos/vocalvoters-beta.png differ diff --git a/app/assets/images/vocalvoters_logos/vocalvoters-favicon-256.png b/app/assets/images/vocalvoters_logos/vocalvoters-favicon-256.png new file mode 100644 index 0000000..590e69d Binary files /dev/null and b/app/assets/images/vocalvoters_logos/vocalvoters-favicon-256.png differ diff --git a/app/assets/images/vocalvoters_logos/vocalvoters.png b/app/assets/images/vocalvoters_logos/vocalvoters.png new file mode 100644 index 0000000..3622efa Binary files /dev/null and b/app/assets/images/vocalvoters_logos/vocalvoters.png differ diff --git a/app/assets/stylesheets/account_activations.scss b/app/assets/stylesheets/account_activations.scss new file mode 100644 index 0000000..65f3ebe --- /dev/null +++ b/app/assets/stylesheets/account_activations.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the AccountActivations controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000..fe30bc2 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_self + *= require_tree . + */ diff --git a/app/assets/stylesheets/custom.css.scss b/app/assets/stylesheets/custom.css.scss new file mode 100644 index 0000000..23c1d7b --- /dev/null +++ b/app/assets/stylesheets/custom.css.scss @@ -0,0 +1,1619 @@ + +@import "bootstrap-sprockets"; +@import "bootstrap"; + +/* mixins, variables, etc. */ + +$gray-medium-light: #eaeaea; + +@mixin box_sizing { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +/* universal */ +html { + height: 100vh; + min-height: 100vh; +} + + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + padding-top: 50px; + margin: 50px; + @media (max-width: 900px){ + padding-top: 40px; + margin: 10px; + } + height: 100vh; + min-height: 100vh; +} + +.underline-on-hover:hover { + text-decoration: underline; +} + +textarea { + resize: vertical; +} + +.center { + text-align: center; + h1 { + margin-bottom: 10px; + } +} + +/* typography */ + +h1, h2, h3, h4, h5, h6, h1_search { + line-height: 1; +} + +h1_search { + font-size: 3em; + letter-spacing: -2px; + font-weight: bold; + margin-bottom: 30px; + // color: $gray-light; +} + +h1-landing { + font-size: 6em; + letter-spacing: 2px; + margin-bottom: 30px; + text-align: left; +} + +h2-landing { + font-size: 2em; + letter-spacing: -1px; + text-align: left; + font-weight: strong; + color: $gray-light; +} + +h1 { + font-size: 3em; + letter-spacing: -2px; + margin-bottom: 30px; + text-align: center; +} + +h2 { + font-size: 2em; + letter-spacing: -1px; + text-align: center; + font-weight: normal; + color: $gray-light; +} + +p { + font-size: 1.1em; + line-height: 1.7em; +} + +a { + color: #00CCFF; +} + +.information { + p { + color: #606060; + font-size: 1.3em; + a { + color: #66CCCC; + } + } +} + +blockquote { + background: #f9f9f9; + border-left: 10px solid #ccc; + margin: 1.5em 10px; + padding: 0.5em 10px; + quotes: "\201C""\201D""\2018""\2019"; +} +blockquote:before { + color: #ccc; + content: open-quote; + font-size: 2em; + line-height: 0.1em; + margin-right: 0.25em; + vertical-align: -0.4em; +} +blockquote p { + display: inline; +} + +/* header */ +#logo { + float: left; + margin-right: 10px; + font-size: 1.7em; + color: white; + text-transform: uppercase; + letter-spacing: -1px; + padding-top: 0.25em; + padding-bottom: 0.25em; + font-weight: bold; + &:hover { + opacity: 0.7; + color: white; + text-decoration: none; + } +} + +/* footer */ + +footer { + margin-top: 45px; + padding-top: 5px; + border-top: 1px solid $gray-medium-light; + color: $gray-light; + a { + color: $gray; + &:hover { + color: $gray-darker; + } + } + small { + float: left; + } + ul { + float: right; + list-style: none; + li { + float: left; + margin-left: 15px; + } + } +} + +/* sidebar */ + +aside { + section.user_info { + margin-top: 20px; + } + section { + padding: 10px 0; + margin-top: 20px; + &:first-child { + border: 0; + padding-top: 0; + } + span { + display: block; + margin-bottom: 3px; + line-height: 1; + } + h1 { + font-size: 1.4em; + text-align: left; + letter-spacing: -1px; + margin-bottom: 3px; + margin-top: 0px; + } + } +} + +.plan { + border: 1px solid black; +} + +.selectable { + &:hover { + cursor: pointer; + } +} +.selected { + background-color: lightgray; +} +.disabled { + color: gray; +} + +.gravatar { + float: left; + margin-right: 10px; +} + +.gravatar_edit { + margin-top: 15px; +} + +/* miscellaneous */ + +.debug_dump { + clear: both; + float: left; + width: 100%; + margin-top: 45px; + @include box_sizing; +} + +/* forms */ + +input, textarea, .uneditable-input { + border: 1px solid #bbb; + width: 100%; + margin-bottom: 15px; + @include box_sizing; +} + +select { + border: 1px solid #bbb; + width: 100%; + margin-bottom: 15px; + @include box_sizing; +} + +input { + height: auto !important; +} + +#error_explanation { + color: red; + ul { + color: red; + margin: 0 0 30px 0; + } +} + +.field_with_errors { + @extend .has-error; + .form-control { + color: $state-danger-text; + } +} + +.checkbox { + margin-top: -10px; + margin-bottom: 10px; + span { + margin-left: 20px; + font-weight: normal; + } +} + +#session_remember_me { + width: auto; + margin-left: 0; +} + +/* Users index */ + +.users { + list-style: none; + margin: 0; + li { + overflow: auto; + /* padding: 10px; */ + border-bottom: 1px solid $gray-lighter; + margin-top: 10px; + } + details { + cursor:pointer; + font-size: 65%; + color: #DFDFDF; + } + .title { + color: #000000; + } + .listings { + color: #58EAFF; + } +} + +.assets-list { + list-style: none; + margin: 0; + padding: 0; + li { + overflow: auto; + padding: 0; + border-bottom: 1px solid $gray-lighter; + } + details { + cursor:pointer; + font-size: 65%; + color: #DFDFDF; + } + .title { + color: #000000; + } + .listings { + color: #58EAFF; + } +} + +/* Buttons */ + +.round-btn { + display: inline-block; + margin-top: 10px; + padding: 15px 30px; + border-radius: 50px; + font-weight: 700; + + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -ms-transition: all 0.3s; + -o-transition: all 0.3s; + transition: all 0.3s; +} + +.btn-dyn-eeg { + border: 2px solid rgb(219,34,120); + color: rgb(219,34,120); + background-image: image-url("eeg.gif"); +} +.btn-dyn-eeg:hover { + border: 2px solid #fff; + color: #fff; + background: rgb(225,45,130); +} + + +.btn-flat-white { + border: 2px solid #fff; + color: #fff; + background: rgba(255,255,255, .10); +} +.btn-flat-white:hover { + color: #fff; +} + +.btn-flat-color { + border: 2px solid rgb(255,45,130); + color: #666; +} +.btn-flat-color:hover { + color: #fff; + background: rgb(225,45,130); +} + +#changeDateButton, #start_date, #end_date, +#btnSearch, #btnClear, #btnRandom { + display: inline-block; + vertical-align: top; +} + +/*========== Loading ==========*/ + +#preloader { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; + z-index: 999; +} + +#loading-img { + position: absolute; + top: 50%; + left: 50%; + width: 128px; + height: 53px; + margin: -21px 0 0 -64px; +} + +/* Jumbotron */ +.jumbotron { + + min-height: 4vh; + height: 4vh; + line-height: 5em; + text-align: center; + vertical-align: middle; + margin-bottom: 0em; + + //background-color: #FFEDFA; + background-color: white; + + background-size: 100% 100%; + height: 100%; + -moz-background-size: 100% 100%; + -webkit-background-size: 100% 100%; + -o-background-size: 100% 100%; +} + +@media (max-width:629px) { + .spashlogo { + display: none; + } +} + +.landing { + min-height: 60vh; +} + +/*============ Images =========*/ +img { + max-width: 100%; + max-height: 100%; +} + +/*============ Loader ===========*/ +.loader, +.loader:before, +.loader:after { + /* background: #f50ca7; */ + background: #ffa3d0; + -webkit-animation: load1 1s infinite ease-in-out; + animation: load1 1s infinite ease-in-out; + width: 1em; + height: 4em; +} +.loader { + /* color: #f50ca7; */ + color: #ffa3d0; + vertical-align: middle; + text-indent: -9999em; + margin: 88px auto; + position: relative; + font-size: 11px; + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); + -webkit-animation-delay: -0.16s; + animation-delay: -0.16s; +} +.loader:before, +.loader:after { + position: absolute; + top: 0; + content: ''; +} +.loader:before { + left: -1.5em; + -webkit-animation-delay: -0.32s; + animation-delay: -0.32s; +} +.loader:after { + left: 1.5em; +} +@-webkit-keyframes load1 { + 0%, + 80%, + 100% { + box-shadow: 0 0; + height: 4em; + } + 40% { + box-shadow: 0 -2em; + height: 5em; + } +} +@keyframes load1 { + 0%, + 80%, + 100% { + box-shadow: 0 0; + height: 4em; + } + 40% { + box-shadow: 0 -2em; + height: 5em; + } +} + + +/* ====== Search Form ====== */ + +input.search-query:before { + display: block; + width: 2em; + height: 2em; + content: "\e003"; + font-family: 'Glyphicons Halflings'; + background-position: -6em 0; + position: absolute; + top:0.5em; + left:1.75em; + opacity: .5; + z-index: 1000; +} + + +input.search-query { + padding-left:2em; +} + +form.search-form { + position: relative; +} + +form.search-form:before { + display: block; + width: 2em; + height: 2em; + content: "\e003"; + font-family: 'Glyphicons Halflings'; + background-position: -6em 0; + position: absolute; + top:0.5em; + left:0.25em; + opacity: .5; + z-index: 1000; +} + +form.search-form-nav { + position: relative; +} +form.search-form-nav:before { + display: block; + content: "\e003"; + font-family: 'Glyphicons Halflings'; + background-position: -6em 0; + position: absolute; + top:0.5em; + left:1.75em; + opacity: .5; + z-index: 1000; +} + +.search-field { + box-sizing: border-box; + border: 0px solid #fff; + border-bottom: 2px solid #ccc; + display:inline-block; + background-color: white; + padding: 8px 3px 2px 3px; + + /* Remove form-control formatting */ + box-shadow: none; + -webkit-box-shadow: none; + -moz-border-radius: 0px; + -webkit-border-radius: 0px; + border-radius: 0px; +} + +.search-field:focus { + box-shadow: none; + -webkit-box-shadow: none; + border: 0px solid #fff; + padding: 8px 3px 1px 3px; + border-bottom: 3px solid #888; +} + +.btn-search { + color: #616161; + min-width: 100%; + background: #e3e3e3; + padding: 8px 25px 2px 25px; + border: 1px solid transparent; + display:inline-block; +} + +.btn-search:hover { + background: #c9c9c9; +} + +/* ===== navbar ====== */ +/* .navbar-nav { margin-bottom: -1em; } */ + +/* social nav */ +.social-nav { text-align: center;} +.social-nav a { display: inline-block; width: 40px; height: 40px; border-radius: 50%; border: 2px solid #666; margin: 0 5px; color: #666; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -ms-transition: all 0.3s; -o-transition: all 0.3s; transition: all 0.3s;} +.social-nav a:hover { color: rgb(219,34,120); border: 2px solid rgb(219,34,120);} +.social-nav span { line-height: 36px; font-size: 18px;} +.footer-nav { margin-top: 75px;} +.footer-nav a { color: #666; width: 50%; display: inline-block; margin-bottom: 5px; padding: 5px;} + +/* highcharts issue */ +svg{ + overflow:visible !important; +} +.highcharts-container{ + overflow:visible !important; +} + + +input[type="radio"]{ + vertical-align: baseline; +} + +/* help box */ +.helpbox { + position: relative; + display: inline-block; + border-bottom: 1px dotted black; +} + +.helpbox .helpboxtext { + visibility: hidden; + width: 30vh; + word-wrap: break-word; + background-color: black; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 5px 5px 5px; + position: absolute; + z-index: 1; + top: 150%; + left: 50%; + margin-left: -15vh; +} + +.helpbox .helpboxtext::after { + content: ""; + position: absolute; + bottom: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent black transparent; +} +.helpbox:hover .helpboxtext { + visibility: visible; + opacity: 0.75 +} + + +.form-signin +{ + max-width: 30em; + padding: 1em; + margin: 0 auto; +} +.form-signin .form-signin-heading, .form-signin .checkbox +{ + margin-bottom: 10px; +} +.form-signin .checkbox +{ + font-weight: normal; +} +.form-signin .form-control +{ + position: relative; + font-size: 16px; + height: auto; + padding: 10px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.form-signin .form-control:focus +{ + z-index: 2; +} +.form-signin input[type="text"] +{ + margin-bottom: -1px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +.form-signin input[type="password"] +{ + margin-bottom: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.account-wall +{ + margin-top: 10px; + padding: 10px 0px 10px 0px; + background-color: #f7f7f7; + -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); + -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); +} +.login-title +{ + color: #555; + font-size: 18px; + font-weight: 400; + display: block; +} +.profile-img +{ + width: 96px; + height: 96px; + margin: 0 auto 10px; + display: block; + -moz-border-radius: 50%; + -webkit-border-radius: 50%; + border-radius: 50%; +} +.need-help +{ + margin-top: 10px; +} +.new-account +{ + display: block; + margin-top: 10px; +} + + +/* KEY FEATURES SECTION STYLES +-------------------------------------------------*/ +.key-features-section { + padding: 140px 0 90px; + background: #eee; +} + +.single-key-feature { + padding: 0 70px; +} + +.single-key-feature:hover i { + -webkit-transform: scale(1.2); + cursor: pointer; + transform: scale(1.2); +} + +.key-features-section .col-md-4 { + border-right: 1px solid #fff; +} + +.key-features-section .col-md-4:last-child { + border-right: none; +} + +.single-key-feature > i { + font-size: 50px; + background: #fff; + height: 110px; + width: 110px; + border-radius: 100%; + line-height: 110px; + -webkit-transition: all .3s; + transition: all .3s; + -webkit-transform: scale(1); + transform: scale(1); +} + +.single-key-feature-details { +} + +.single-key-feature-details > h3 { + font-size: 20px; + font-weight: 600; + color: #222; + line-height: 32px; + margin-top: 30px; + margin-bottom: 7px; +} + +.single-key-feature-details > p { + color: #777; + font-size: 16px; + font-weight: 500; + line-height: 24px; + margin: 0; +} + +.features-section { + padding: 140px 0 90px; +} + +.demo-section { + padding: 5em 0 5em; +} + +.section-title { + margin-bottom: 50px; +} + +.section-title > h2 { + font-size: 40px; + font-weight: 300; + color: #222; + text-transform: capitalize; + margin-bottom: 17px; + margin-top: 0; +} + +.section-title > p { + font-size: 16px; + line-height: 26px; + font-weight: 400; + color: #8a8a8a; +} + +.section-description { + font-size: 16px; + line-height: 26px; + font-weight: 400; + color: #8a8a8a; +} + +.single-feature { + margin-bottom: 50px; +} + +.single-feature:hover i { + -webkit-transform: scale(1.2); + transform: scale(1.2); +} + +.single-feature > i { + font-size: 30px; + -webkit-transition: all .3s; + transition: all .3s; + -webkit-transform: scale(1); + transform: scale(1); +} + +.single-feature > h4 { + margin-left: 55px; + font-size: 22px; + font-weight: 500; + color: #484848; + margin-top: 0; +} + +.single-feature > p { + margin-left: 55px; + font-size: 15px; + line-height: 24px; + font-weight: 500; + color: #929292; +} + + +.single-key-feature > i { + color: #0cb4ce; +} + +.single-feature > i { + color: #0cb4ce; +} + + +/* . SERVICE SECTION STYLES +-------------------------------------------------*/ +.service-section { + + background-size: cover; + padding: 140px 0 70px; +} + +.single-service { + background: rgba(33,39,49,0.7); + // background: rgba(0,69,90,0.7); + padding: 40px 30px 10px; + margin-bottom: 50px; +} + +.single-service > i { + font-size: 36px; + height: 80px; + width: 80px; + color: #fff; + border-radius: 50%; + line-height: 80px; + -webkit-transition: all .3s; + transition: all .3s; + position: relative; + top: 0; + opacity: 1; +} + +.single-service:hover i { + top: -40px; + opacity: 0; +} + +.single-service:hover h4 { + top: -90px; +} + +.single-service:hover p { + top: -90px; +} + +.single-service:hover a { + top: -50px; + opacity: 1; +} + +.single-service > h4 { + color: #fff; + font-size: 20px; + text-transform: capitalize; + margin-top: 40px; + font-weight: 500; + -webkit-transition: all .3s; + transition: all .3s; + position: relative; + top: 0; +} + +.single-service > p { + font-size: 15px; + line-height: 24px; + color: #b2b8bc; + -webkit-transition: all .3s; + transition: all .3s; + position: relative; + top: 0; +} + +.single-service > a { + font-size: 14px; + text-transform: uppercase; + font-weight: 700; + background: #fff; + color: #4e4e4e; + padding: 6px 40px; + border-radius: 50px; + -webkit-transition: all .3s; + transition: all .3s; + position: relative; + top: -10px; + opacity: 0; +} + + +.single-service > i { + background: #0cb4ce; +} + +.single-service > a:hover { + background: #0cb4ce; +} + + +/* ====== Switch for Subscriptions ====== */ +.switch-field { + font-family: "Lucida Grande", Tahoma, Verdana, sans-serif; + padding: 0px; + overflow: hidden; +} + +.switch-title { + margin-bottom: 6px; +} + +.switch-field input { + position: absolute !important; + clip: rect(0, 0, 0, 0); + height: 1px; + width: 1px; + border: 0; + overflow: hidden; +} + +.switch-field label { + float: left; +} + +.switch-field label { + display: inline-block; + width: 50%; + background-color: #e4e4e4; + color: rgba(0, 0, 0, 0.6); + font-size: 14px; + font-weight: normal; + text-align: center; + text-shadow: none; + padding: 6px 6px; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px rgba(255, 255, 255, 0.1); + -webkit-transition: all 0.1s ease-in-out; + -moz-transition: all 0.1s ease-in-out; + -ms-transition: all 0.1s ease-in-out; + -o-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; +} + +.switch-field label:hover { + cursor: pointer; +} + +.switch-field input:checked + label { + /* background-color: #A5DC86; */ + color: white; + background-color: #5CB85C; + -webkit-box-shadow: none; + box-shadow: none; +} + +.switch-field label:first-of-type { + border-radius: 4px 0 0 4px; +} + +.switch-field label:last-of-type { + border-radius: 0 4px 4px 0; +} + +@media screen and (max-width: 600px) { + #tip_message { + visibility: hidden; + clear: both; + display: none; + } +} + + +/* Right align on larger screens */ +.right-align-on-sm { + text-align: left; +} +@media screen and (min-width: 768px){ + .right-align-on-sm { + text-align: right; + } +} + + +/* Landing page, Sale sign */ +#sale { + font-size: 1em; + font-weight:bold; + text-align: center; + /* Rotate div */ + /* + -ms-transform: rotate(-35deg); + -webkit-transform: rotate(-35deg); + transform: rotate(-35deg); + */ +} + +/* Right align on non-mobile */ +.full-screen-right-alignment { + text-align: right; +} +/* Any time we go below a reasonable size */ +@media (max-width: 767px) { + .full-screen-right-alignment { + text-align: center; + } +} + +/* Rotating words */ +#sentence-wrapper{ + width: 80%; + position: relative; + margin: 110px auto 0 auto; + font-family: Georgia, serif; + padding: 10px; +} +.sentence{ + margin: 0; + text-align: center; + text-shadow: 1px 1px 1px rgba(255,255,255,0.8); +} +.sentence span{ + text-align:center; + font-size: 100%; + font-weight: normal; +} +.words{ + text-align:center; + display: inline-block; + text-indent: 0px; +} +.words-rotate #words-rotate-placeholder { + position: relative; + opacity:0; + font-size:100%; +} +.words-rotate span { + position: absolute; + text-align:left; + opacity: 0; + overflow: none; + + color: #ff817a; + -webkit-animation: rotateWord 18s linear infinite 0s; + -moz-animation: rotateWord 18s linear infinite 0s; + -o-animation: rotateWord 18s linear infinite 0s; + -ms-animation: rotateWord 18s linear infinite 0s; + animation: rotateWord 18s linear infinite 0s; +} +.words-rotate span:nth-child(2) { + -webkit-animation-delay: 3s; + -moz-animation-delay: 3s; + -o-animation-delay: 3s; + -ms-animation-delay: 3s; + animation-delay: 3s; + color: #05b264; +} +.words-rotate span:nth-child(3) { + -webkit-animation-delay: 6s; + -moz-animation-delay: 6s; + -o-animation-delay: 6s; + -ms-animation-delay: 6s; + animation-delay: 6s; + color: #45d1d8; +} +.words-rotate span:nth-child(4) { + -webkit-animation-delay: 9s; + -moz-animation-delay: 9s; + -o-animation-delay: 9s; + -ms-animation-delay: 9s; + animation-delay: 9s; + color: #ff7bb6; +} +.words-rotate span:nth-child(5) { + -webkit-animation-delay: 12s; + -moz-animation-delay: 12s; + -o-animation-delay: 12s; + -ms-animation-delay: 12s; + animation-delay: 12s; + color: #d39641; +} +.words-rotate span:nth-child(6) { + -webkit-animation-delay: 15s; + -moz-animation-delay: 15s; + -o-animation-delay: 15s; + -ms-animation-delay: 15s; + animation-delay: 15s; + color: #9b6b9d; +} +@-webkit-keyframes rotateWord { + 0% { opacity: 0; } + 2% { opacity: 0; -webkit-transform: translateY(-30px); } + 5% { opacity: 1; -webkit-transform: translateY(0px);} + 17% { opacity: 1; -webkit-transform: translateY(0px); } + 20% { opacity: 0; -webkit-transform: translateY(30px); } + 80% { opacity: 0; } + 100% { opacity: 0; } +} +@-moz-keyframes rotateWord { + 0% { opacity: 0; } + 2% { opacity: 0; -moz-transform: translateY(-30px); } + 5% { opacity: 1; -moz-transform: translateY(0px);} + 17% { opacity: 1; -moz-transform: translateY(0px); } + 20% { opacity: 0; -moz-transform: translateY(30px); } + 80% { opacity: 0; } + 100% { opacity: 0; } +} +@-o-keyframes rotateWord { + 0% { opacity: 0; } + 2% { opacity: 0; -o-transform: translateY(-30px); } + 5% { opacity: 1; -o-transform: translateY(0px);} + 17% { opacity: 1; -o-transform: translateY(0px); } + 20% { opacity: 0; -o-transform: translateY(30px); } + 80% { opacity: 0; } + 100% { opacity: 0; } +} +@-ms-keyframes rotateWord { + 0% { opacity: 0; } + 2% { opacity: 0; -ms-transform: translateY(-30px); } + 5% { opacity: 1; -ms-transform: translateY(0px);} + 17% { opacity: 1; -ms-transform: translateY(0px); } + 20% { opacity: 0; -ms-transform: translateY(30px); } + 80% { opacity: 0; } + 100% { opacity: 0; } +} +@keyframes rotateWord { + 0% { opacity: 0; } + 2% { opacity: 0; transform: translateY(-30px); } + 5% { opacity: 1; transform: translateY(0px);} + 17% { opacity: 1; transform: translateY(0px); } + 20% { opacity: 0; transform: translateY(30px); } + 80% { opacity: 0; } + 100% { opacity: 0; } +} + + +/* ==== BUTTONS for SELECTION */ +button:focus { outline: none;} +::-webkit-input-placeholder { color: #fff;} +:-moz-placeholder { color: #fff;} +::-moz-placeholder { color: #fff;} +:-ms-input-placeholder { color: #fff;} + + +/*========== Buttons ==========*/ + +.background-color, +.select-icon { + background: #58e5ff; + background: -moz-linear-gradient(45deg, #027C93 0%, #2ed6f5 100%); + background: -webkit-gradient(linear, left bottom, right top, color-stop(0%,#027C93), color-stop(100%,#2ed6f5)); + background: -webkit-linear-gradient(45deg, #027C93 0%, #2ed6f5 100%); + background: -o-linear-gradient(45deg, #027C93 0%,#2ed6f5 100%); + background: -ms-linear-gradient(45deg, #027C93 0%,#2ed6f5 100%); + background: linear-gradient(45deg, #027C93 0%,#2ed6f5 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#027C93', endColorstr='#2ed6f5',GradientType=1 ); +} + +.background-color input[type="submit"] { + color: #92278f; +} +.background-color { + color: #fff; +} + +.select-icon { + width: 126px; + height: 126px; + margin: 0 auto 20px auto; + padding: 5px; + border-radius: 100%; +} + +.select-icon span { + width: 100%; + height: 100%; + border: 2px solid #fff; + color: #fff; + border-radius: 100%; + font-size: 40px; + line-height: 112px; +} +.select-icon:hover { + background: #2ed6f5; + cursor: pointer; +} + +.select-icon .selected { + background: #2ed6f5; +} + + +/* Test Navbar Styles */ + +.navbar { + border: none; + background: #fff; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-transition: all 400ms ease-in-out; + -moz-transition: all 400ms ease-in-out; + -o-transition: all 400ms ease-in-out; + -ms-transition: all 400ms ease-in-out; + transition: all 400ms ease-in-out; + padding: 20px 0 5px; + margin-bottom: 25px !important; +} + +.scroll-fixed-nav { + padding: 15px 0 12px; + box-shadow: 0 2px 30px rgba(0,0,0,0.2); +} + +.navbar-brand { + padding: 7px 15px; +} + +.navbar-brand-logo { + padding: 7px 15px; + height: auto; + width: auto; + max-width: 225px; + max-height: 50px; +} + +.navbar-brand-logo:hover { + background: rgba(0,0,0,0); +} + +.navbar-default .navbar-nav>li>a { + font-size: 15px; + font-weight: 700; + text-transform: uppercase; + color: #4C4C4C; + padding: 15px 20px; +} + +#featured-sign-up { + margin-left: 20px; + margin-right: 15px; +} + +#featured-sign-up > a { + padding: 11px 25px !important; + color: #fff !important; + -moz-border-radius: 50px; + -webkit-border-radius: 50px; + border-radius: 50px; + margin-top: 4px; +} + +a:hover,a:focus { + text-decoration: none; + outline: none; +} + +#featured-sign-up > a { + background: #0cb4ce; +} + +#featured-sign-up > a:hover { + background: #56D5E8; +} + +/* iFrame mobile */ +/* FROM: http://benmarshall.me/responsive-iframes/ */ +.iframe-container { + position: relative; + height: 0; + overflow: hidden; +} + +/* 16x9 Aspect Ratio */ +.iframe-container-16x9 { + padding-bottom: 56.25%; +} + +/* 4x3 Aspect Ratio */ +.iframe-container-4x3 { + padding-bottom: 75%; +} + +.iframe-container iframe { + position: absolute; + top:0; + left: 0; + width: 100%; + height: 100%; +} + +.recipient_card { + cursor: pointer; + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.3s; + width: 100%; + border-radius: 5px; + padding: 7px 7px 7px 7px; + margin-bottom: 25px; +} + +.recipient_card.senator_card { + box-shadow: 0 4px 8px 0 rgba(0, 40, 209, 0.2); +} + +.recipient_card.representative_card { + box-shadow: 0 4px 8px 0 rgba(209, 0, 0, 0.2); +} + +.recipient_card.governor_card { + box-shadow: 0 4px 8px 0 rgba(218, 165, 35, 0.5) +} + +.recipient_card:hover { + box-shadow: 0 10px 16px 0 rgba(0,0,0,0.2); +} + +.recipient_card.senator_card:hover { + box-shadow: 0 6px 10px 2px rgba(0, 40, 209, 0.2); +} + +.recipient_card.representative_card:hover { + box-shadow: 0 6px 10px 2px rgba(209, 0, 0, 0.2); +} + +.recipient_card.governor_card:hover { + box-shadow: 0 6px 10px 2px rgba(218, 165, 35, 0.2); +} + +.recipient_card.selected_card { + box-shadow: 0px 6px 10px 10px rgba(0,0,0,0.5); + border-style: solid; + border-width: 1px; + border-color: rgba(128, 204, 171, 1); +} + +.recipient_card.senator_card.selected_card { + border-color: rgba(0, 40, 209, 0.4); + box-shadow: 4px 8px 10px 8px rgba(0, 40, 209, 0.2); +} + +.recipient_card.representative_card.selected_card { + border-color: rgba(209, 0, 0, 0.4); + box-shadow: 4px 8px 10px 8px rgba(209, 0, 0, 0.2); +} + +.recipient_card.governor_card.selected_card { + border-color: rgba(218, 165, 32, 0.8); + box-shadow: 4px 8px 10px 8px rgba(218, 165, 32, 0.5); +} + + +.recipient_card.senator_card.selected_card:hover { + box-shadow: 2px 6px 8px 6px rgba(0, 40, 209, 0.2); +} + +.recipient_card.representative_card.selected_card:hover { + box-shadow: 2px 6px 8px 6px rgba(209, 0, 0, 0.2); +} + +.recipient_card.governor_card.selected_card:hover { + box-shadow: 2px 6px 8px 6px rgba(218, 165, 32, 0.5); +} + + +.btn.btn-info.send_button.selected_card { + box-shadow: 0px 2px 4px 4px rgba(0,0,0,0.4); + border-width: 2px; +} + + +/* Variables */ +* { + box-sizing: border-box; +} + +.payment-form { + width: 30vw; + min-width: 500px; + align-self: center; + box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1), + 0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07); + border-radius: 7px; + padding: 40px; +} + +input { + border-radius: 6px; + margin-bottom: 6px; + padding: 12px; + border: 1px solid rgba(50, 50, 93, 0.1); + height: 44px; + font-size: 16px; + width: 100%; + background: white; +} + +.result-message { + line-height: 22px; + font-size: 16px; +} + +.result-message a { + color: rgb(89, 111, 214); + font-weight: 600; + text-decoration: none; +} + +.hidden { + display: none; +} + +#card-error { + color: rgb(105, 115, 134); + text-align: left; + font-size: 13px; + line-height: 17px; + margin-top: 12px; +} + +#card-element { + border-radius: 4px 4px 0 0 ; + padding: 12px; + border: 1px solid rgba(50, 50, 93, 0.1); + height: 44px; + width: 100%; + background: white; +} + +#payment-request-button { + margin-bottom: 32px; +} + +/* Buttons and links */ +#submit { + background: #5cb85c; + color: #ffffff; + font-family: Arial, sans-serif; + border-radius: 0 0 4px 4px; + border: 0; + padding: 12px 16px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + display: block; + transition: all 0.2s ease; + box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07); + width: 100%; +} +#submit:hover { + filter: contrast(115%); +} +#submit:disabled { + opacity: 0.5; + cursor: default; +} + + +/* spinner/processing state, errors */ +.spinner, +.spinner:before, +.spinner:after { + border-radius: 50%; +} +.spinner { + color: #ffffff; + font-size: 22px; + text-indent: -99999px; + margin: 0px auto; + position: relative; + width: 20px; + height: 20px; + box-shadow: inset 0 0 0 2px; + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); +} +.spinner:before, +.spinner:after { + position: absolute; + content: ""; +} +.spinner:before { + width: 10.4px; + height: 20.4px; + background: #5cb85c; + border-radius: 20.4px 0 0 20.4px; + top: -0.2px; + left: -0.2px; + -webkit-transform-origin: 10.4px 10.2px; + transform-origin: 10.4px 10.2px; + -webkit-animation: loading 2s infinite ease 1.5s; + animation: loading 2s infinite ease 1.5s; +} +.spinner:after { + width: 10.4px; + height: 10.2px; + background: #5cb85c; + border-radius: 0 10.2px 10.2px 0; + top: -0.1px; + left: 10.2px; + -webkit-transform-origin: 0px 10.2px; + transform-origin: 0px 10.2px; + -webkit-animation: loading 2s infinite ease; + animation: loading 2s infinite ease; +} + +@-webkit-keyframes loading { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes loading { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@media only screen and (max-width: 600px) { + form { + width: 80vw; + } +} + +.moderate-risk-highlight span{ + background-color: #F6E7A3; + padding: 0.2em 0.2em; +} +.moderate-risk-highlight small{ + color: #A28910; + font-weight: bold; +} + +.high-risk-highlight span{ + background-color: #F8C5D0; + padding: 0.2em 0.2em; +} +.high-risk-highlight small{ + color: #F08199; + font-weight: bold; +} diff --git a/app/assets/stylesheets/emails.scss b/app/assets/stylesheets/emails.scss new file mode 100644 index 0000000..c3ccff7 --- /dev/null +++ b/app/assets/stylesheets/emails.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Emails controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/faxes.scss b/app/assets/stylesheets/faxes.scss new file mode 100644 index 0000000..fcc604a --- /dev/null +++ b/app/assets/stylesheets/faxes.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Faxes controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/letters.scss b/app/assets/stylesheets/letters.scss new file mode 100644 index 0000000..511c658 --- /dev/null +++ b/app/assets/stylesheets/letters.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Letters controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/password_resets.scss b/app/assets/stylesheets/password_resets.scss new file mode 100644 index 0000000..9d6c8b6 --- /dev/null +++ b/app/assets/stylesheets/password_resets.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the PasswordResets controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/payments.scss b/app/assets/stylesheets/payments.scss new file mode 100644 index 0000000..6d026a7 --- /dev/null +++ b/app/assets/stylesheets/payments.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the payments controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/pdf.scss b/app/assets/stylesheets/pdf.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/assets/stylesheets/pdf.scss @@ -0,0 +1 @@ + diff --git a/app/assets/stylesheets/posts.scss b/app/assets/stylesheets/posts.scss new file mode 100644 index 0000000..6907cec --- /dev/null +++ b/app/assets/stylesheets/posts.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Posts controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/recipients.scss b/app/assets/stylesheets/recipients.scss new file mode 100644 index 0000000..4a3b0d0 --- /dev/null +++ b/app/assets/stylesheets/recipients.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Recipients controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/scaffolds.scss b/app/assets/stylesheets/scaffolds.scss new file mode 100644 index 0000000..bb2597f --- /dev/null +++ b/app/assets/stylesheets/scaffolds.scss @@ -0,0 +1,65 @@ +body { + background-color: #fff; + color: #333; + margin: 33px; } + +body, p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; } + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; } + +a { + color: #000; } + +a:visited { + color: #666; } + +a:hover { + color: #fff; + background-color: #000; } + +th { + padding-bottom: 5px; } + +td { + padding: 0 5px 7px; } + +div.field, +div.actions { + margin-bottom: 10px; } + +#notice { + color: green; } + +.field_with_errors { + padding: 2px; + background-color: red; + display: table; } + +#error_explanation { + width: 450px; + border: 2px solid red; + padding: 7px 7px 0; + margin-bottom: 20px; + background-color: #f0f0f0; } + +#error_explanation h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px -7px 0; + background-color: #c00; + color: #fff; } + +#error_explanation ul li { + font-size: 12px; + list-style: square; } + +label { + display: block; } diff --git a/app/assets/stylesheets/send_communication.scss b/app/assets/stylesheets/send_communication.scss new file mode 100644 index 0000000..a6d11b5 --- /dev/null +++ b/app/assets/stylesheets/send_communication.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the SendCommunication controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/senders.scss b/app/assets/stylesheets/senders.scss new file mode 100644 index 0000000..6f0c212 --- /dev/null +++ b/app/assets/stylesheets/senders.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Senders controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/sessions.scss b/app/assets/stylesheets/sessions.scss new file mode 100644 index 0000000..2ee3060 --- /dev/null +++ b/app/assets/stylesheets/sessions.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Sessions controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/static_pages.scss b/app/assets/stylesheets/static_pages.scss new file mode 100644 index 0000000..2e073c0 --- /dev/null +++ b/app/assets/stylesheets/static_pages.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the StaticPages controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss new file mode 100644 index 0000000..efc2526 --- /dev/null +++ b/app/assets/stylesheets/users.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Users controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/account_activations_controller.rb b/app/controllers/account_activations_controller.rb new file mode 100644 index 0000000..0806ef6 --- /dev/null +++ b/app/controllers/account_activations_controller.rb @@ -0,0 +1,16 @@ +class AccountActivationsController < ApplicationController + + def edit + user = User.find_by(email: params[:email]) + if user && !user.activated? && user.authenticated?(:activation, params[:id]) + user.activate + log_in user + flash[:success] = "Account activated!" + redirect_to user + else + flash[:danger] = "Invalid activation link" + redirect_to root_url + end + end + +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..ce04ed1 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,14 @@ +class ApplicationController < ActionController::Base + include SessionsHelper + include UsersHelper + + # Confirms a logged-in user. + def logged_in_user + unless logged_in? + store_location + flash[:danger] = "Please log in." + redirect_to login_url + end + end + +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/emails_controller.rb b/app/controllers/emails_controller.rb new file mode 100644 index 0000000..e322789 --- /dev/null +++ b/app/controllers/emails_controller.rb @@ -0,0 +1,76 @@ +class EmailsController < ApplicationController + before_action :logged_in_user, except: [:new, :create] + before_action :logged_in_admin, except: [:new, :create] + before_action :set_email, only: [:show, :edit, :update, :destroy] + + # GET /emails + # GET /emails.json + def index + @emails = Email.all + end + + # GET /emails/1 + # GET /emails/1.json + def show + end + + # GET /emails/new + def new + @email = Email.new + end + + # GET /emails/1/edit + def edit + end + + # POST /emails + # POST /emails.json + def create + @email = Email.new(email_params) + + respond_to do |format| + if @email.save + format.html { redirect_to @email, notice: 'Email was successfully created.' } + format.json { render :show, status: :created, location: @email } + else + format.html { render :new } + format.json { render json: @email.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /emails/1 + # PATCH/PUT /emails/1.json + def update + respond_to do |format| + if @email.update(email_params) + format.html { redirect_to @email, notice: 'Email was successfully updated.' } + format.json { render :show, status: :ok, location: @email } + else + format.html { render :edit } + format.json { render json: @email.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /emails/1 + # DELETE /emails/1.json + def destroy + @email.destroy + respond_to do |format| + format.html { redirect_to emails_url, notice: 'Email was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_email + @email = Email.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def email_params + params.require(:email).permit(:email_address, :sender_id, :recipient_id, :letter_id, :payment_id) + end +end diff --git a/app/controllers/faxes_controller.rb b/app/controllers/faxes_controller.rb new file mode 100644 index 0000000..07e0704 --- /dev/null +++ b/app/controllers/faxes_controller.rb @@ -0,0 +1,76 @@ +class FaxesController < ApplicationController + before_action :logged_in_user, except: [:new, :create] + before_action :logged_in_admin, except: [:new, :create] + before_action :set_fax, only: [:show, :edit, :update, :destroy] + + # GET /faxes + # GET /faxes.json + def index + @faxes = Fax.all + end + + # GET /faxes/1 + # GET /faxes/1.json + def show + end + + # GET /faxes/new + def new + @fax = Fax.new + end + + # GET /faxes/1/edit + def edit + end + + # POST /faxes + # POST /faxes.json + def create + @fax = Fax.new(fax_params) + + respond_to do |format| + if @fax.save + format.html { redirect_to @fax, notice: 'Fax was successfully created.' } + format.json { render :show, status: :created, location: @fax } + else + format.html { render :new } + format.json { render json: @fax.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /faxes/1 + # PATCH/PUT /faxes/1.json + def update + respond_to do |format| + if @fax.update(fax_params) + format.html { redirect_to @fax, notice: 'Fax was successfully updated.' } + format.json { render :show, status: :ok, location: @fax } + else + format.html { render :edit } + format.json { render json: @fax.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /faxes/1 + # DELETE /faxes/1.json + def destroy + @fax.destroy + respond_to do |format| + format.html { redirect_to faxes_url, notice: 'Fax was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_fax + @fax = Fax.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def fax_params + params.require(:fax).permit(:number_fax, :sender_id, :recipient_id, :letter_id, :payment_id) + end +end diff --git a/app/controllers/letters_controller.rb b/app/controllers/letters_controller.rb new file mode 100644 index 0000000..bf4acdc --- /dev/null +++ b/app/controllers/letters_controller.rb @@ -0,0 +1,232 @@ +class LettersController < ApplicationController + before_action :logged_in_user, except: [:show, :find_policy] + before_action :logged_in_admin, except: [:show, :find_policy] + before_action :set_letter, only: [:show, :edit, :update, :destroy] + + # GET /letters + # GET /letters.json + def index + @letters = Letter.all + end + + # GET /letters/1 + # GET /letters/1.json + def show + + @template = false + if params.has_key?(:template) + @template = true + end + + if @letter.sentiment > 0.5 + @sentiment = 'strongly support' + elsif @letter.sentiment > 0 + @sentiment = 'support' + elsif @letter.sentiment == 0 + @sentiment = 'am indifferent to' + elsif @letter.sentiment < -0.5 + @sentiment = 'strongly oppose' + elsif @letter.sentiment < 0 + @sentiment = 'oppose' + end + + @recipient_name = "[[ insert government official name ]]" + recipient_name = nil + if params.has_key?(:recipient_name) + @recipient_name = params[:recipient_name] + recipient_name = Recipient.where(name: @recipient_name) + end + + @sender_name = "[[ insert senders name ]]" + if params.has_key?(:sender_name) + @sender_name = params[:sender_name] + end + + @sender_region = "" + + if params.has_key?(:sender_county) + @sender_region += params[:sender_county].downcase.sub(/county/, '').titleize + @sender_region += ' County' + if params.has_key?(:sender_state) + @sender_region += ", "+params[:sender_state] + end + @sender_region += '
' + end + + @recipient_position = "" + if params.has_key?(:recipient_position) + @recipient_position = params[:recipient_position].titleize + end + + recipient_district = nil + recipient_level = nil + if params.has_key?(:sender_district) + @sender_region += params[:sender_district].to_i.ordinalize + + recipient_district = Recipient.where(district: params[:sender_district]) + + region = " Congressional District" + level = "United States" + if params.has_key?(:recipient_level) \ + and params[:recipient_level] == "state" + if params.has_key?(:recipient_position) + region = " " + @recipient_position + " District" + end + level = "State" + + recipient_level = Recipient.where(level: params[:recipient_level]) + end + + @sender_region += region + end + + recipient_state = nil + if params.has_key?(:sender_state) + + recipient_state = Recipient.where(state: params[:sender_state]) + + @sender_region += ", " + params[:sender_state] + if level == "State" + level = params[:sender_state] + " " + level + end + end + + @recipient_location = "" + if @recipient_position.present? + if @recipient_position == "Senator" + @recipient_location = level + " Senate" + elsif @recipient_position == "Representative" + @recipient_location = level + " House" + end + end + + # Select recipient, if possible + recipient = Recipient + if recipient_name.present? + recipient = recipient.and(recipient_name) + end + if recipient_district.present? + recipient = recipient.and(recipient_district) + end + if recipient_level.present? + recipient = recipient.and(recipient_level) + end + if recipient_state.present? + recipient = recipient.and(recipient_state) + end + + # Ensure only one recipient for accuracy + @recipient = nil + if (not recipient.is_a?(Class)) and recipient.length == 1 + @recipient = recipient.first + end + + + @sender_region_verified = "" + if params.has_key?(:sender_verified) + @sender_region_verified = "
" + if ActiveModel::Type::Boolean.new.cast(params[:sender_verified]) + @sender_region_verified += "✔ Verified " + else + @sender_region_verified += "✖ Not Verifiably " + end + @sender_region_verified += "in District via Billing Address" + @sender_region_verified += "" + end + + respond_to do |format| + format.html + format.pdf do + render template: "letters/letter.html.erb", + pdf: "Letter ID: #{@letter.id}" + end + end + end + + # GET /letters/new + def new + @letter = Letter.new + end + + # GET /letters/1/edit + def edit + end + + # POST /letters + # POST /letters.json + def create + @letter = Letter.new(letter_params) + + respond_to do |format| + if @letter.save + format.html { redirect_to @letter, notice: 'Letter was successfully created.' } + format.json { render :show, status: :created, location: @letter } + else + format.html { render :new } + format.json { render json: @letter.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /letters/1 + # PATCH/PUT /letters/1.json + def update + respond_to do |format| + if @letter.update(letter_params) + format.html { redirect_to @letter, notice: 'Letter was successfully updated.' } + format.json { render :show, status: :ok, location: @letter } + else + format.html { render :edit } + format.json { render json: @letter.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /letters/1 + # DELETE /letters/1.json + def destroy + @letter.destroy + respond_to do |format| + format.html { redirect_to letters_url, notice: 'Letter was successfully destroyed.' } + format.json { head :no_content } + end + end + + def find_policy + category = params[:category] + sentiment = params[:sentiment] + policy_or_law = params[:policy_or_law] + + @policy_or_laws = Letter + + if category.present? + @policy_or_laws = @policy_or_laws.where( + "category LIKE lower(?)", "%#{category}%") + end + if policy_or_law.present? + @policy_or_laws = @policy_or_laws.where( + "policy_or_law LIKE lower(?)", "%#{policy_or_law}%") + end + if sentiment.present? + @policy_or_laws = @policy_or_laws.where(sentiment: sentiment.to_f) + end + + @policy_or_laws = @policy_or_laws.distinct.pluck( + :id, :sentiment, :category, :policy_or_law) + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_letter + @letter = Letter.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def letter_params + params.require(:letter) + .permit(:category, :policy_or_law, :tags, :sentiment, :body, + :target_level, :target_state) + .merge(user_id: current_user.id) + end + +end diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb new file mode 100644 index 0000000..6f94bc3 --- /dev/null +++ b/app/controllers/password_resets_controller.rb @@ -0,0 +1,68 @@ +class PasswordResetsController < ApplicationController + before_action :get_user, only: [:edit, :update] + before_action :valid_user, only: [:edit, :update] + before_action :check_expiration, only: [:edit, :update] + + def new + end + + def create + @user = User.find_by(email: params[:password_reset][:email].downcase) + if @user + @user.create_reset_digest + @user.send_password_reset_email + flash[:info] = "Email sent with password reset instructions" + redirect_to root_url + else + flash.now[:danger] = "Email address not found" + render 'new' + end + end + + def edit + end + + def update + if params[:user][:password].empty? + @user.errors.add(:password, "can't be empty") + render 'edit' + elsif @user.update(user_params) + reset_session + log_in @user + @user.update_attribute(:reset_digest, nil) + flash[:success] = "Password has been reset." + redirect_to @user + else + render 'edit' + end + end + + private + + def user_params + params.require(:user).permit(:password, :password_confirmation) + end + + # Before filters + + def get_user + @user = User.find_by(email: params[:email]) + end + + # Confirms a valid user. + def valid_user + unless (@user && @user.activated? && + @user.authenticated?(:reset, params[:id])) + redirect_to root_url + end + end + + # Checks expiration of reset token. + def check_expiration + if @user.password_reset_expired? + flash[:danger] = "Password reset has expired." + redirect_to new_password_reset_url + end + end + +end diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb new file mode 100644 index 0000000..e25a8c3 --- /dev/null +++ b/app/controllers/payments_controller.rb @@ -0,0 +1,42 @@ +class PaymentsController < ApplicationController + + # POST endpoint for stripe webhook + # If your controller accepts requests other than Stripe webhooks, + # you'll probably want to use `protect_from_forgery` to add CSRF + # protection for your application. But don't forget to exempt + # your webhook route! + protect_from_forgery except: :webhook + def webhook + payload = request.body.read + event = nil + + begin + event = Stripe::Event.construct_from( + JSON.parse(payload, symbolize_names: true) + ) + rescue JSON::ParserError => e + # Invalid payload + render status: :bad_request, nothing: true + return + end + + # Handle the event + case event.type + when 'payment_intent.succeeded' + payment_intent = event.data.object # contains a Stripe::PaymentIntent + # Then define and call a method to handle the successful payment intent. + # handle_payment_intent_succeeded(payment_intent) + require 'json' + when 'payment_method.attached' + payment_method = event.data.object # contains a Stripe::PaymentMethod + # Then define and call a method to handle the successful attachment of a PaymentMethod. + # handle_payment_method_attached(payment_method) + # ... handle other event types + else + puts "Unhandled event type: #{event.type}" + end + + render status: :ok, json: "Success: No Failures" + end + +end diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb new file mode 100644 index 0000000..180cb32 --- /dev/null +++ b/app/controllers/posts_controller.rb @@ -0,0 +1,76 @@ +class PostsController < ApplicationController + before_action :logged_in_user, except: [:new, :create] + before_action :logged_in_admin, except: [:new, :create] + before_action :set_post, only: [:show, :edit, :update, :destroy] + + # GET /posts + # GET /posts.json + def index + @posts = Post.all + end + + # GET /posts/1 + # GET /posts/1.json + def show + end + + # GET /posts/new + def new + @post = Post.new + end + + # GET /posts/1/edit + def edit + end + + # POST /posts + # POST /posts.json + def create + @post = Post.new(post_params) + + respond_to do |format| + if @post.save + format.html { redirect_to @post, notice: 'Post was successfully created.' } + format.json { render :show, status: :created, location: @post } + else + format.html { render :new } + format.json { render json: @post.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /posts/1 + # PATCH/PUT /posts/1.json + def update + respond_to do |format| + if @post.update(post_params) + format.html { redirect_to @post, notice: 'Post was successfully updated.' } + format.json { render :show, status: :ok, location: @post } + else + format.html { render :edit } + format.json { render json: @post.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /posts/1 + # DELETE /posts/1.json + def destroy + @post.destroy + respond_to do |format| + format.html { redirect_to posts_url, notice: 'Post was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_post + @post = Post.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def post_params + params.require(:post).permit(:address_line_1, :address_line_2, :address_city, :address_state, :address_zipcode, :sender_id, :recipient_id, :letter_id, :payment_id) + end +end diff --git a/app/controllers/recipients_controller.rb b/app/controllers/recipients_controller.rb new file mode 100644 index 0000000..89f8cd7 --- /dev/null +++ b/app/controllers/recipients_controller.rb @@ -0,0 +1,229 @@ +require 'net/http' +class RecipientsController < ApplicationController + before_action :logged_in_user, except: [:show, :lookup] + before_action :logged_in_admin, except: [:show, :lookup] + before_action :set_recipient, only: [:show, :edit, :update, :destroy] + + # GET /recipients + # GET /recipients.json + def index + @recipients = Recipient.all + end + + # GET /recipients/1 + # GET /recipients/1.json + def show + end + + # GET /recipients/new + def new + @recipient = Recipient.new + end + + # GET /recipients/1/edit + def edit + end + + # POST /recipients + # POST /recipients.json + def create + @recipient = Recipient.new(recipient_params) + + respond_to do |format| + if @recipient.save + format.html { redirect_to @recipient, notice: 'Recipient was successfully created.' } + format.json { render :show, status: :created, location: @recipient } + else + format.html { render :new } + format.json { render json: @recipient.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /recipients/1 + # PATCH/PUT /recipients/1.json + def update + respond_to do |format| + if @recipient.update(recipient_params) + format.html { redirect_to @recipient, notice: 'Recipient was successfully updated.' } + format.json { render :show, status: :ok, location: @recipient } + else + format.html { render :edit } + format.json { render json: @recipient.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /recipients/1 + # DELETE /recipients/1.json + def destroy + @recipient.destroy + respond_to do |format| + format.html { redirect_to recipients_url, notice: 'Recipient was successfully destroyed.' } + format.json { head :no_content } + end + end + + # GET /govlookup + # GET /govlookup.json + def lookup + + obj = get_congressional_districts() + + @address_accuracy = obj['results'][0]['accuracy'] + + if @address_accuracy > 0.7 + @accuracy_class = "" + elsif @address_accuracy > 0.5 + @accuracy_class = "moderate-risk-highlight" + else + @accuracy_class = "high-risk-highlight" + end + + district_info = obj['results'][0]['fields']['congressional_districts'][0] + district_number = district_info['district_number'] + + @sender_line_1 = '' + if obj['results'][0]['address_components'].has_key? 'number' + @sender_line_1 = obj['results'][0]['address_components']['number'] + end + if obj['results'][0]['address_components'].has_key? 'formatted_street' + @sender_line_1 += ' ' + obj['results'][0]['address_components']['formatted_street'] + end + + @address_line_2 = '' + if obj['results'][0]['address_components'].has_key? 'secondaryunit' + @address_line_2 += obj['results'][0]['address_components']['secondaryunit'] + end + if obj['results'][0]['address_components'].has_key? 'secondarynumber' + @address_line_2 += ' ' + obj['results'][0]['address_components']['secondarynumber'] + end + + @sender_city = obj['results'][0]['address_components']['city'] + @sender_state = obj['results'][0]['address_components']['state'] + @sender_zipcode = obj['results'][0]['address_components']['zip'] + @sender_country = obj['results'][0]['address_components']['country'] + + @sender_address = obj['input']['formatted_address'] + @sender_county = obj['results'][0]['address_components']['county'].sub(/ County/, '') + @sender_district_federal = district_number + + level = 'federal' + legislators = district_info['current_legislators'] + + @recipients = [] + for legislator in legislators + + position = legislator['type'] + name = legislator['bio']['first_name'] + ' ' + legislator['bio']['last_name'] + number_phone = legislator['contact']['phone'].gsub(/-/, '').to_i + contact_form = legislator['contact']['url'] + if legislator['contact']['contact_form'].present? + contact_form = legislator['contact']['contact_form'] + end + + address_line_1 = legislator['contact']['address'].split(' Washington')[0] + address_city = 'Washington' + address_state = 'DC' + address_zipcode = legislator['contact']['address'].split(' DC ')[1] + + # Find and use + recipient = Recipient.find_by( + :name => name, :position => position, :level => level, :state => @sender_state) + + # If cannot find matching record, create new record + if not recipient.present? + recipient = Recipient.create( + :name => name, + :position => position, + :level => level, + :district => district_number, + :state => @sender_state, + :number_phone => number_phone, + :contact_form => contact_form, + :address_line_1 => address_line_1, + :address_city => address_city, + :address_state => address_state, + :address_zipcode => address_zipcode) + end + + if not recipient.retired + @recipients.push(recipient) + end + end + + level = 'state' + state_district = obj['results'][0]['fields']['state_legislative_districts'] + + # Find state govenor + recipient = Recipient.find_by( + :position => 'governor', :state => @sender_state, :retired => false) + if recipient.present? + @recipients.push(recipient) + end + + if state_district.has_key? 'house' + district = state_district['house']['district_number'] + @sender_district_representative = district + recipient = Recipient.find_by( + :position => 'representative', :district => district, + :level => level, :state => @sender_state, :retired => false) + if recipient.present? + @recipients.push(recipient) + end + end + + if state_district.has_key? 'senate' + district = state_district['senate']['district_number'] + @sender_district_senate = district + recipient = Recipient.find_by( + :position => 'senator', :district => district, + :level => level, :state => @sender_state, :retired => false) + if recipient.present? + @recipients.push(recipient) + end + end + + respond_to do |format| + format.html { + if params[:layout] == 'false' + render :layout => false + end + } + format.json + end + + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_recipient + @recipient = Recipient.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def recipient_params + params.require(:recipient).permit(:name, :position, :level, :district, :state, :number_fax, :number_phone, :email_address, :contact_form, :address_line_1, :address_line_2, :address_city, :address_state, :address_zipcode, :retired) + end + + def get_congressional_districts + url = 'https://api.geocod.io/v1.6/geocode' + + address = "" + fields = "cd" # congressional district + fields += ",stateleg" # state congressional district + api_key = "" + if ENV.has_key?("GEOCODIO_API_KEY") + api_key = ENV["GEOCODIO_API_KEY"] + end + + url += '?q=' + params[:address] + url += '&fields=' + fields + url += '&api_key=' + api_key + + uri = URI(url) + obj = Net::HTTP.get(uri) + + return ActiveSupport::JSON.decode(obj) + end +end diff --git a/app/controllers/send_communication_controller.rb b/app/controllers/send_communication_controller.rb new file mode 100644 index 0000000..136b426 --- /dev/null +++ b/app/controllers/send_communication_controller.rb @@ -0,0 +1,261 @@ +require 'stripe' +require 'clicksend_client' +require 'json' + +class SendCommunicationController < ApplicationController + skip_before_action :verify_authenticity_token + + def send_communication + + Stripe.api_key = ENV['STRIPE_SECRET_KEY_VOCALVOTERS'] + + method = params['method'] + payment_id = params['payment']['id'] + payment = Stripe::PaymentIntent.retrieve(payment_id) + + # clicksend setup authorization + ClickSendClient.configure do |config| + # Configure HTTP basic authorization: BasicAuth + config.username = ENV['CLICKSEND_USERNAME'] + config.password = ENV['CLICKSEND_PASSWORD'] + end + + # If paid, then register communication + if payment['charges']['data'][0]['paid'] + billing_zipcode = payment['charges']['data'][0]['billing_details']['address']['postal_code'] + recipients = params['recipients'] # should be list of id's + + sender_line_1 = params['sender']['line_1'].presence || '' + sender_line_2 = params['sender']['line_2'].presence || '' + sender_city = params['sender']['city'].presence || '' + sender_state = params['sender']['state'].presence || '' + sender_zipcode = params['sender']['zipcode'].presence || '' + + # Create sender + sender = Sender.find_by(email: sender_params[:email]) + if not sender.present? + sender = Sender.create!(sender_params) + end + + letter_id = letter_params[:id] + sender_id = sender[:id] + verified = billing_zipcode.to_i == sender.zipcode.to_i + + # Find recipients + recipients = Recipient.find(params['recipients']) + + recipients.each { |recipient| + + # Fill letter template + letter_url = '/letters/'+letter_id.to_s+'.pdf?' + letter_url += 'recipient_name='+recipient.name+'&' + letter_url += 'recipient_position='+recipient.position+'&' + letter_url += 'recipient_level='+recipient.level+'&' + letter_url += 'sender_name='+sender.name+'&' + letter_url += 'sender_state='+sender.state+'&' + letter_url += 'sender_district='+recipient.district+'&' + letter_url += 'sender_verified='+verified.to_s + + address_zipcode = recipient[:address_zipcode] + to_fax_number = recipient[:number_fax] + + # For development, utilize the free options + if Rails.env.development? + letter_url = "https://vocalvoters.com" + letter_url + address_zipcode = "11111" + to_fax_number = "61261111111" + end + + case method + when "priority" + + letter_url += "&template=true" + return_address_id = create_return_address(sender.name, sender_line_1, + sender_line_2, sender_city, + sender_state, sender_zipcode) + + success_flag = send_post( + letter_url, recipient[:name], return_address_id, + recipient[:address_line_1], recipient[:address_line_2], + recipient[:address_city], recipient[:address_state], + address_zipcode, priority_flag=1) + + Post.create!(address_line_1: recipient[:address_line_1], + address_line_2: recipient[:address_line_2], + address_city: recipient[:address_city], + address_state: recipient[:address_state], + address_zipcode: address_zipcode, + priority: true, + sender: sender, + recipient: recipient, + letter: Letter.find_by(id: letter_id), + payment: payment_id, + success: success_flag) + + when "letter" + + letter_url += "&template=true" + return_address_id = create_return_address(sender.name, sender_line_1, + sender_line_2, sender_city, + sender_state, sender_zipcode) + + success_flag = send_post( + letter_url, recipient[:name], return_address_id, + recipient[:address_line_1], recipient[:address_line_2], + recipient[:address_city], recipient[:address_state], + address_zipcode, priority_flag=0) + + Post.create!(address_line_1: recipient[:address_line_1], + address_line_2: recipient[:address_line_2], + address_city: recipient[:address_city], + address_state: recipient[:address_state], + address_zipcode: address_zipcode, + priority: false, + sender: sender, + recipient: recipient, + letter: Letter.find_by(id: letter_id), + payment: payment_id, + success: success_flag) + + when "fax" + + from = "VocalVoters" + source_method = "rails" + from_email = sender.email + + success_flag = send_fax(letter_url, to_fax_number, from, + source_method, from_email) + + Fax.create!(number_fax: to_fax_number, + sender: sender, + recipient: recipient, + letter: Letter.find_by(id: letter_id), + payment: payment_id, + success: success_flag) + + when "email" + # Do nothing ATM + # send_email(name, email) + end + + } + + end + + render status: :ok, json: "Success: No Failures" + end + + private + + def sender_params + params.require(:sender).permit(:name, :email, :zipcode, + :county, :district, :state) + end + + def letter_params + + # "id": "1", + # "category": "bakeries", + # "sentiment": "Very Supportive", + # "policy_or_law": "deserts" + + params.require(:letter).permit(:id, :category, + :sentiment, :policy_or_law) + end + + def send_email(name, email) + # https://developers.clicksend.com/docs/rest/v3/#send-email + return false + end + + + def send_fax(letter_url, to_fax_number, from, source_method, from_email) + api_instance = ClickSendClient::FAXApi.new + + # FaxMessageCollection | FaxMessageCollection model + fax_messages = ClickSendClient::FaxMessageCollection.new( + "file_url": letter_url, + messages: [ + ClickSendClient::FaxMessage.new( + "to": to_fax_number, + "source": source_method, + "from": from, + "country": 'US', + "from_email": from_email, + "source": source_method + ) + ] + ) + + begin + # Send a fax using supplied supported file-types. + result = api_instance.fax_send_post(fax_messages) + return true + rescue ClickSendClient::ApiError => e + puts "Exception when calling FAXApi->fax_send_post: #{e.response_body}" + end + return false + end + + def create_return_address(name, line_1, line_2, city, state, zipcode) + api_instance = ClickSendClient::PostReturnAddressApi.new + + # Address | Address model + return_address = ClickSendClient::Address.new( + "address_postal_code": zipcode, + "address_country": "US", + "address_line_1": line_1, + "address_state": state, + "address_name": name, + "address_line_2": line_2, + "address_city": city + ) + + begin + # Create post return address + result = api_instance.post_return_addresses_post(return_address) + result = JSON.parse(result) + return result['data']['return_address_id'] + rescue ClickSendClient::ApiError => e + puts "Exception calling PostReturnAddressApi->post_return_addresses_post: #{e.response_body}" + end + return false + end + + def send_post(letter_url, name, return_address_id, address_line_1, address_line_2, + address_city, address_state, address_zipcode, priority_flag=0) + + # PostLetter | PostLetter model + api_instance = ClickSendClient::PostLetterApi.new + post_letter = ClickSendClient::PostLetter.new( + "file_url": letter_url, + "recipients": [ + { + "return_address_id": return_address_id, + "schedule": 0, + "address_name": name, + "address_line_1": address_line_1, + "address_line_2": address_line_2, + "address_city": address_city, + "address_state": address_state, + "address_postal_code": address_zipcode, + "address_country": "US" + } + ], + "priority_post": priority_flag, + "template_used": 1, + "duplex": 0, + "colour": 0 + ) + + begin + # Send post letter + result = api_instance.post_letters_send_post(post_letter) + return true + rescue ClickSendClient::ApiError => e + puts "Exception calling PostLetterApi->post_letters_send_post: #{e.response_body}" + end + return false + end + +end diff --git a/app/controllers/senders_controller.rb b/app/controllers/senders_controller.rb new file mode 100644 index 0000000..2bd9035 --- /dev/null +++ b/app/controllers/senders_controller.rb @@ -0,0 +1,91 @@ +class SendersController < ApplicationController + before_action :logged_in_user, except: [:new, :create] + before_action :logged_in_admin, except: [:new, :create] + before_action :set_sender, only: [:show, :edit, :update, :destroy] + + # GET /senders + # GET /senders.json + def index + @senders = Sender.all + end + + # GET /senders/1 + # GET /senders/1.json + def show + end + + # GET /senders/new + def new + @sender = Sender.new + end + + # GET /senders/1/edit + def edit + end + + # POST /senders + # POST /senders.json + def create + @sender = Sender.find_by( + :name => sender_params[:name], + :email => sender_params[:email], + :zipcode => sender_params[:zipcode], + :county => sender_params[:county], + :district => sender_params[:district], + :state => sender_params[:state]) + + if not @sender.present? + @sender = Sender.new(sender_params) + respond_to do |format| + if @sender.save + format.html { redirect_to @sender, notice: 'Sender was successfully created.' } + format.json { render :json => @sender.id } + else + format.html { render :new } + format.json { render json: @sender.errors, status: :unprocessable_entity } + end + end + else + respond_to do |format| + format.html { redirect_to @sender, notice: 'Sender already existed.' } + format.json { render :json => @sender.id } + end + end + end + + # PATCH/PUT /senders/1 + # PATCH/PUT /senders/1.json + def update + respond_to do |format| + if @sender.update(sender_params) + format.html { redirect_to @sender, notice: 'Sender was successfully updated.' } + format.json { render :show, status: :ok, location: @sender } + else + format.html { render :edit } + format.json { render json: @sender.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /senders/1 + # DELETE /senders/1.json + def destroy + @sender.destroy + respond_to do |format| + format.html { redirect_to senders_url, notice: 'Sender was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_sender + @sender = Sender.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def sender_params + params.require(:sender) + .permit(:name, :email, :zipcode, :county, :district, :state) + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..c6664e8 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,32 @@ +class SessionsController < ApplicationController + + def new + end + + def create + user = User.find_by(email: params[:session][:email].downcase) + if user && user.authenticate(params[:session][:password]) + if user.activated? + forwarding_url = session[:forwarding_url] + reset_session + params[:session][:remember_me] == '1' ? remember(user) : forget(user) + log_in user + redirect_to forwarding_url || user + else + message = "Account not activated. " + message += "Check your email for the activation link." + flash[:warning] = message + redirect_to root_url + end + else + flash.now[:danger] = 'Invalid email/password combination' + render 'new' + end + end + + def destroy + log_out if logged_in? + redirect_to root_url + end + +end diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb new file mode 100644 index 0000000..3f42354 --- /dev/null +++ b/app/controllers/static_pages_controller.rb @@ -0,0 +1,132 @@ +require 'stripe' + +class StaticPagesController < ApplicationController + skip_before_action :verify_authenticity_token, :only => [:create_payment_intent] + + def home + @stripe_pk = ENV['STRIPE_PUBLISHABLE_KEY_VOCALVOTERS'] + + @sentiment_val_map = { + -1.0 => ["Very Opposed", -1.0], + -0.5 => ["Opposed", -0.5], + 0.0 => ["Neutral", 0.0], + 0.5 => ["Support", 0.5], + 1.0 => ["Very Supportive", 1.0] + } + @sentiment_phrase_map = { + "Very Opposed" => ["Very Opposed", -1.0], + "Opposed" => ["Opposed", -0.5], + "Neutral" => ["Neutral", 0.0], + "Support" => ["Support", 0.5], + "Very Supportive" => ["Very Supportive", 1.0] + } + + @selected_category = params[:category] + @options_category = []+Letter.distinct.pluck('lower(category)') + + @selected_sentiment = nil + @options_sentiment = [] + + @selected_policy_or_law = nil + @options_policy_or_law = [] + + + # Don't bother preloading unless category selected + if @selected_category.present? + + #### DETERMINE THE SENTIMENT IF AVAILABLE ##### + + available_sentiment = Letter.where( + "category LIKE lower(?)", "%#{@selected_category}%" + ).distinct.order(sentiment: :desc).pluck(:sentiment) + available_sentiment.each { |sentiment| + @options_sentiment.append(@sentiment_val_map[sentiment]) + } + + if params.has_key? :sentiment + if is_number?(params[:sentiment]) + @selected_sentiment = params[:sentiment].to_f + elsif @sentiment_phrase_map.has_key? params[:sentiment] + @selected_sentiment = @sentiment_phrase_map[params[:sentiment]][1] + end + end + + + #### DETERMINE THE POLICY_OR_LAW IF AVAILABLE ##### + if @selected_sentiment.present? + @options_policy_or_law = Letter.where( + sentiment: @selected_sentiment + ).where( + "category LIKE lower(?)", "%#{@selected_category}%" + ).pluck(:policy_or_law, :id) + + if params.has_key? :policy_or_law + if is_number? params[:policy_or_law] + @selected_policy_or_law = params[:policy_or_law].to_i + else + @selected_policy_or_law = []+Letter.where( + sentiment: @selected_sentiment + ).where( + "category LIKE lower(?)", "%#{@selected_category}%" + ).where( + "policy_or_law LIKE lower(?)", "%#{params[:policy_or_law]}%" + ).pluck(:id) + + end + end + end + end + + end + + def help + end + + def about + end + + def contact + end + + def create_payment_intent + Stripe.api_key = ENV['STRIPE_SECRET_KEY_VOCALVOTERS'] + + # Create a PaymentIntent with amount and currency + # TODO: Make more robust + payment_intent = Stripe::PaymentIntent.create({ + amount: calculate_order_amount(params['item'], params['count']), + currency: 'usd', + payment_method_types: ['card'], + receipt_email: params['email'] + }) + + @payment_info = payment_intent['client_secret'] + + respond_to do |format| + format.json { @payment_info } + end + end + + private + + def calculate_order_amount(item, count) + + if not count.present? or not item.present? + return 0 + end + + if ['Priority Mail'].include? item + return 500 * count + elsif ['Letter', 'Letters'].include? item + return 300 * count + elsif ['Fax', 'Faxes'].include? item + return 200 * count + elsif ['Email', 'Emails'].include? item + return 100 * count + end + end + + def is_number? string + true if Float(string) rescue false + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..252a096 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,91 @@ +class UsersController < ApplicationController + before_action :logged_in_user, only: [:index, :edit, :update, :destroy] + before_action :correct_user, only: [:edit, :update] + before_action :logged_in_admin, only: [:index, :destroy] + before_action :admin_user, only: :destroy + + + def index + @users = User.where(activated: true).paginate(page: params[:page]) + end + + def show + @user = User.find(params[:id]) + redirect_to root_url and return unless @user.activated? + + respond_to do |format| + format.html + format.pdf do + render template: "users/show.html.erb", + pdf: "User ID: #{@user.id}" + end + end + + end + + def new + @user = User.new + end + + def create + @user = User.new(user_params) + if @user.save + @user.send_activation_email + flash[:info] = "Please check your email to activate your account." + redirect_to root_url + else + render 'new' + end + end + + def edit + @user = User.find(params[:id]) + end + + def update + @user = User.find(params[:id]) + if @user.update(user_params) + flash[:success] = "Profile updated" + redirect_to @user + else + render 'edit' + end + end + + def destroy + User.find(params[:id]).destroy + flash[:success] = "User deleted" + redirect_to users_url + end + + + private + + def user_params + params.require(:user).permit(:name, :email, :password, + :password_confirmation) + end + + # Before filters + + # Confirms a logged-in user. + def logged_in_user + unless logged_in? + store_location + flash[:danger] = "Please log in." + redirect_to login_url + end + end + + # Confirms the correct user. + def correct_user + @user = User.find(params[:id]) + redirect_to(root_url) unless current_user?(@user) + end + + # Confirms an admin user. + def admin_user + redirect_to(root_url) unless current_user.admin? + end + +end diff --git a/app/helpers/account_activations_helper.rb b/app/helpers/account_activations_helper.rb new file mode 100644 index 0000000..c4d5ac7 --- /dev/null +++ b/app/helpers/account_activations_helper.rb @@ -0,0 +1,2 @@ +module AccountActivationsHelper +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..24950b7 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,13 @@ +module ApplicationHelper + + # Returns the full title on a per-page basis. + def full_title(page_title = '') + base_title = "VocalVoters" + if page_title.empty? + base_title + else + page_title + " | " + base_title + end + end + +end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb new file mode 100644 index 0000000..b4dc6ec --- /dev/null +++ b/app/helpers/emails_helper.rb @@ -0,0 +1,2 @@ +module EmailsHelper +end diff --git a/app/helpers/faxes_helper.rb b/app/helpers/faxes_helper.rb new file mode 100644 index 0000000..415e266 --- /dev/null +++ b/app/helpers/faxes_helper.rb @@ -0,0 +1,2 @@ +module FaxesHelper +end diff --git a/app/helpers/letters_helper.rb b/app/helpers/letters_helper.rb new file mode 100644 index 0000000..cad8d62 --- /dev/null +++ b/app/helpers/letters_helper.rb @@ -0,0 +1,2 @@ +module LettersHelper +end diff --git a/app/helpers/password_resets_helper.rb b/app/helpers/password_resets_helper.rb new file mode 100644 index 0000000..0c9d96e --- /dev/null +++ b/app/helpers/password_resets_helper.rb @@ -0,0 +1,2 @@ +module PasswordResetsHelper +end diff --git a/app/helpers/payments_helper.rb b/app/helpers/payments_helper.rb new file mode 100644 index 0000000..c1b884f --- /dev/null +++ b/app/helpers/payments_helper.rb @@ -0,0 +1,2 @@ +module PaymentsHelper +end diff --git a/app/helpers/posts_helper.rb b/app/helpers/posts_helper.rb new file mode 100644 index 0000000..a7b8cec --- /dev/null +++ b/app/helpers/posts_helper.rb @@ -0,0 +1,2 @@ +module PostsHelper +end diff --git a/app/helpers/recipients_helper.rb b/app/helpers/recipients_helper.rb new file mode 100644 index 0000000..0595f08 --- /dev/null +++ b/app/helpers/recipients_helper.rb @@ -0,0 +1,2 @@ +module RecipientsHelper +end diff --git a/app/helpers/send_communication_helper.rb b/app/helpers/send_communication_helper.rb new file mode 100644 index 0000000..fe9efdd --- /dev/null +++ b/app/helpers/send_communication_helper.rb @@ -0,0 +1,2 @@ +module SendCommunicationHelper +end diff --git a/app/helpers/senders_helper.rb b/app/helpers/senders_helper.rb new file mode 100644 index 0000000..3a64dfb --- /dev/null +++ b/app/helpers/senders_helper.rb @@ -0,0 +1,2 @@ +module SendersHelper +end diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 0000000..023d70b --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,64 @@ +module SessionsHelper + + # Logs in the given user. + def log_in(user) + session[:user_id] = user.id + # Guard against session replay attacks. + # https://binarysolo.chapter24.blog/avoiding-session-replay-attacks-in-rails/ + session[:session_token] = user.session_token + end + + # Remembers a user in a persistent session. + def remember(user) + user.remember + cookies.permanent.encrypted[:user_id] = user.id + cookies.permanent[:remember_token] = user.remember_token + end + + + # Returns the user corresponding to the remember token cookie. + def current_user + if (user_id = session[:user_id]) + user = User.find_by(id: user_id) + if user && session[:session_token] == user.session_token + @current_user = user + end + elsif (user_id = cookies.encrypted[:user_id]) + user = User.find_by(id: user_id) + if user && user.authenticated?(:remember, cookies[:remember_token]) + log_in user + @current_user = user + end + end + end + + # Returns true if the given user is the current user. + def current_user?(user) + user && user == current_user + end + + # Returns true if the user is logged in, false otherwise. + def logged_in? + !current_user.nil? + end + + # Forgets a persistent session. + def forget(user) + user.forget + cookies.delete(:user_id) + cookies.delete(:remember_token) + end + + # Logs out the current user. + def log_out + forget(current_user) + reset_session + @current_user = nil + end + + # Stores the URL trying to be accessed. + def store_location + session[:forwarding_url] = request.original_url if request.get? + end + +end diff --git a/app/helpers/static_pages_helper.rb b/app/helpers/static_pages_helper.rb new file mode 100644 index 0000000..2d63e79 --- /dev/null +++ b/app/helpers/static_pages_helper.rb @@ -0,0 +1,2 @@ +module StaticPagesHelper +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..449f533 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,42 @@ +module UsersHelper + + # Confirms an admin user. + def admin_user + unless current_user.admin? + store_location + flash[:danger] = "Inappropriate permissions." + redirect_to root_url + end + end + + # Disables access to other users, unless site admin + # Fail with no error message + # TODO: Send to 404 page + def logged_in_admin + if params.has_key?(:id) and params[:controller] == "user" + # If has id tag, check current user or admin user, if not -> redirect + if (current_user != User.find(params[:id]) && !current_user.admin?) + redirect_to root_url + end + else + # If no id tag, check if admin, if not -> redirect + if !current_user or !current_user.admin? + flash[:warning] = "Only administrators can view that information, contact us." + redirect_back(fallback_location: root_path) + end + end + end + + # Find user by token + def find_user_from_auth_token + authenticate_with_http_token do |token, options| + api_key = ApiKey.find_by_access_token(token) + if api_key.present? + @user = User.find_by_id(api_key.user_id) + return true + end + end + return false + end + +end diff --git a/app/javascript/channels/consumer.js b/app/javascript/channels/consumer.js new file mode 100644 index 0000000..8ec3aad --- /dev/null +++ b/app/javascript/channels/consumer.js @@ -0,0 +1,6 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. + +import { createConsumer } from "@rails/actioncable" + +export default createConsumer() diff --git a/app/javascript/channels/index.js b/app/javascript/channels/index.js new file mode 100644 index 0000000..0cfcf74 --- /dev/null +++ b/app/javascript/channels/index.js @@ -0,0 +1,5 @@ +// Load all the channels within this directory and all subdirectories. +// Channel files must be named *_channel.js. + +const channels = require.context('.', true, /_channel\.js$/) +channels.keys().forEach(channels) diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js new file mode 100644 index 0000000..db8541c --- /dev/null +++ b/app/javascript/packs/application.js @@ -0,0 +1,15 @@ +// This file is automatically compiled by Webpack, along with any other files +// present in this directory. You're encouraged to place your actual application logic in +// a relevant structure within app/javascript and only use these pack files to reference +// that code so it'll be compiled. + +import Rails from "@rails/ujs" +import Turbolinks from "turbolinks" +import * as ActiveStorage from "@rails/activestorage" +import "channels" +import "jquery" +import "bootstrap" + +Rails.start() +Turbolinks.start() +ActiveStorage.start() diff --git a/app/javascript/packs/landing_page.js b/app/javascript/packs/landing_page.js new file mode 100644 index 0000000..2adbf7e --- /dev/null +++ b/app/javascript/packs/landing_page.js @@ -0,0 +1,650 @@ +function validate_email(email) { + var re = /\S+@\S+\.\S+/; + return re.test(email); +} + +function validate_form(ids_to_validate){ + safe_flag = true + for (const element of ids_to_validate){ + + containerElement = document.getElementById(element+'_container'); + containerValue = document.getElementById(element).value; + + // Check if there's no data in given field + if ((containerValue.length==0) + || (element=='email' && !validate_email(containerValue))) { + containerElement.classList.add('has-error'); + safe_flag = false + } else { + containerElement.classList.add('has-success'); + } + + containerElement.addEventListener('change', function() { + this.classList.remove('has-error'); + this.classList.remove('has-success'); + }) + + containerElement.onselect = function() { + this.classList.remove('has-error'); + this.classList.remove('has-success'); + } + } + + return safe_flag +} + +/* If enter, update */ +$('#address,#name').keyup(function(e) { + if (e.which == 13) { + form_element_ids = ['name', 'email', 'address'] + if(validate_form(form_element_ids)){ + find_legislators() + } + } +}) + +$('#legislator_button').click(function(e) { + // 'category', 'sentiment', 'policy_or_law', + form_element_ids = ['name', 'email', 'address'] + if(validate_form(form_element_ids)){ + find_legislators() + } +}) + +$("#category").change(function(e) { + + policy_or_law = $("#policy_or_law"); + if (policy_or_law.prop("selectedIndex", 0).val() != '') { + policy_or_law.empty() + }else{ + policy_or_law.find('option').not(':first').remove(); + } + + sentiment = $("#sentiment"); + if (sentiment.prop("selectedIndex", 0).val() != '') { + sentiment.empty() + }else{ + sentiment.find('option').not(':first').remove(); + } + + category = $("#category").find(":selected").text() + + update_options(category); + +}) + +$("#policy_or_law").change(function(e) { + + /* + category_val = $("#category").find(":selected").text() + policy_or_law = $("#policy_or_law").find(":selected").text() + + sentiment = $("#sentiment"); + if (sentiment.prop("selectedIndex", 0).val() != '') { + sentiment.empty() + }else{ + sentiment.find('option').not(':first').remove(); + } + + update_options(category_val, sentiment_val=null, policy_or_law); + */ + + if(generate_concerns()) { + + // Display pdf & communication section + window.location.hash = "#communications_selection"; + } + +}) + +function generate_concerns() { + + form_element_ids = ['name', 'email', 'address'] + if(validate_form(form_element_ids)){ + + category = $('#category').find(":selected").attr('value'); + sentiment = $('#sentiment').find(":selected").val(); + policy_or_law = $('#policy_or_law').find(":selected").val(); + + if(!category){ return false } + if(!sentiment){ return false } + if(!policy_or_law){ return false } + + // Potentially error + letter_id = $('#policy_or_law').find(":selected").attr('value') + if (letter_id == '') { + letter_id = $('#category').find(":selected").attr('value') + } + + sender_name = document.getElementById('name').value; + sender_state = document.querySelector('#sender_state').getAttribute('value'); + selected_cards = document.getElementsByClassName('recipient_card selected_card'); + + recipient_id = selected_cards[0].getAttribute('value'); + + district = document.querySelector('#recipient_district_'+recipient_id).getAttribute('value'); + name = document.querySelector('#recipient_name_'+recipient_id).getAttribute('value'); + position = document.querySelector('#recipient_position_'+recipient_id).getAttribute('value'); + level = document.querySelector('#recipient_level_'+recipient_id).getAttribute('value'); + update_pdf(letter_id, sender_name, sender_state, + district, name, position, level); + + // Hide bottom buffer once communications_selection displayed + $("#concerns_selection_bottom_buffer").attr('style', 'display:none'); + $('#communications_selection').attr('style', 'display:block'); + + document.getElementById("share_button").onclick = function(){ + copyLetterToClipboard(); + } + + return true + } + + return false +} + +$("#sentiment").change(function(e) { + category_val = $("#category").find(":selected").text() + sentiment_val = $("#sentiment").find(":selected").val() + + policy_or_law = $("#policy_or_law"); + if (policy_or_law.prop("selectedIndex", 0).val() != '') { + policy_or_law.empty() + }else{ + policy_or_law.find('option').not(':first').remove(); + } + + update_options(category_val, sentiment_val, policy_or_law=null); +}) + +function create_sender(){ + sender_name = document.getElementById('name').value; + sender_email = document.getElementById('email').value; + sender_zipcode = document.querySelector('#sender_zipcode').getAttribute('value'); + sender_state = document.querySelector('#sender_state').getAttribute('value'); + sender_county = document.querySelector('#sender_county').getAttribute('value'); + sender_district_federal = document.querySelector('#sender_district_federal').getAttribute('value'); + + create_sender = '/sender' + $.post({ + url: create_sender, + data: { + 'name': sender_name, + 'email': sender_email, + 'zipcode': sender_zipcode, + 'state': sender_state, + 'county': sender_county, + 'district': sender_district_federal + }, + success: function(response) { + console.log(response) + } + }) + +} + +function find_legislators(){ + sender_name = document.getElementById('name').value; + sender_email = document.getElementById('email').value; + sender_address = document.getElementById('address').value; + lookup_url = '/govlookup?'+'name='+sender_name+'&address='+sender_address + lookup_url+= '&layout=false' + + $.ajax({ + url: lookup_url, + cache: false, + success: function(html){ + + $("#legislator_selection").html(html); + document.getElementById('legislator_button').disabled = false; + document.getElementById('legislator_button').innerText = "Lookup Legislators" + + attach_stripe_checkout_on_click(); + + sender_address = document.getElementById('sender_address').getAttribute('value') + sender_state = document.getElementById('sender_state').getAttribute('value') + sender_district_federal = document.getElementById('sender_district_federal').getAttribute('value') + window.location.hash = "#legislator_selection"; + + recipient_cards = document.getElementsByClassName('recipient_card'); + for (var i=0; i < recipient_cards.length; i++) { + if (i == 0) { + id = recipient_cards[i].getAttribute('value') + recipient_name = document.getElementById('recipient_name_'+id).getAttribute('value') + recipient_position = document.getElementById('recipient_position_'+id).getAttribute('value') + recipient_level = document.getElementById('recipient_level_'+id).getAttribute('value') + } + + recipient_cards[i].onclick = function(card) { + + id = document.getElementById(event.srcElement.id).getAttribute('value') + recipient_name = document.getElementById('recipient_name_'+id).getAttribute('value') + recipient_position = document.getElementById('recipient_position_'+id).getAttribute('value') + recipient_level = document.getElementById('recipient_level_'+id).getAttribute('value') + + /* Select the recipient cards */ + if (event.srcElement.classList.contains('selected_card')) { + event.srcElement.classList.remove('selected_card') + }else{ + event.srcElement.classList.add('selected_card') + } + + disableCommunications(); + + update_prices(); + + update_checkout_price(); + + $('#concerns_selection').attr('style', 'display:block'); + + generate_concerns(); + + } + } + } + }); + + document.getElementById('legislator_button').disabled = true; + document.getElementById('legislator_button').innerText = "Please wait..." +} + +function update_pdf(letter_id, sender_name, sender_state, sender_district, + recipient_name, recipient_position, recipient_level) { + + src_url = '/letters/' + letter_id + '.pdf?sender_name=' + sender_name; + src_url += '&sender_state='+sender_state; + src_url += '&sender_district='+sender_district; + + src_url += '&recipient_name='+recipient_name; + src_url += '&recipient_level='+recipient_level; + src_url += '&recipient_position='+recipient_position; + + src_url += '&sender_verified=true' + + $('#pdf_view').attr('src', src_url); + $('#pdf_view').attr('width', '100%'); + $('#pdf_view').attr('height', '780'); + $('#pdf_view').attr('value', letter_id); + $('#iframe_container').attr('style', 'display:block'); +} + +function update_options(category=null, sentiment=null, policy_or_law=null) { + + policy_url = '/find_policy.json?' + if (!!category) { + policy_url += 'category=' + category + } + if (!!sentiment) { + policy_url += '&sentiment=' + sentiment + } + if (!!policy_or_law) { + policy_url += '&policy_or_law=' + policy_or_law + } + + sentiment_map = {} + sentiment_map[-1] = "Very Opposed" + sentiment_map[-0.5] = "Opposed" + sentiment_map[0] = "Indifferent" + sentiment_map[0.5] = "Supportive" + sentiment_map[1] = "Very Supportive" + + $.ajax({ + url: policy_url, + cache: false, + success: function(policy_list){ + + policy_or_law_options = $("#policy_or_law"); + sentiment_options = $("#sentiment"); + + if (policy_or_law === null) { + options = policy_list.map(x => [x[0], x[3]]); + $.each(options, function(key, value) { + policy_or_law_options.append( + $('').val(value[0]).html(value[1]) + ); + }); + } + + if (sentiment === null) { + options = {} + for (var i in policy_list) { + options[policy_list[i][1]] = policy_list[i] + } + + $.each(options, function(key, value) { + sentiment_options.append( + $('').val(key).html( + sentiment_map[key] + ) + ); + }); + } + } + }); +} + +function update_prices() { + recipient_count = $('.recipient_card.selected_card').length; + if(recipient_count == 0){ + document.getElementById('payment_container').style.display = 'none'; + } + + document.getElementById('email_price').innerText= recipient_count * 1; + document.getElementById('fax_price').innerText= recipient_count * 2; + document.getElementById('letter_price').innerText= recipient_count * 3; + document.getElementById('priority_price').innerText= recipient_count * 5; +} + +function attach_stripe_checkout_on_click() { + $('.send_button').click(function(e) { + + send_buttons = document.getElementsByClassName("send_button"); + for (var i = 0; i < send_buttons.length; i++) { + send_buttons[i].classList.remove('selected_card'); + } + + id = e.currentTarget.id; + e.currentTarget.classList.add('selected_card'); + count = update_checkout_price(id); + load_stripe_checkout( + document.getElementById('communication_mode').innerText, + document.getElementById('email').value, + $('.recipient_card.selected_card').length + ); + + recipient_count = $('.recipient_card.selected_card').length; + if(recipient_count == 0){ + document.getElementById('payment_container').style.display = 'none'; + } + }) +} + +function update_checkout_price(id=null) { + + if (id == null) { + id = document.getElementById('communication_mode').getAttribute('value') + } + + recipient_count = $('.recipient_card.selected_card').length; + price = 0; + single_phrasing = ""; + multiple_phrasing = ""; + + if (id == 'email') { + price = document.getElementById('email_price').innerText; + single_phrasing = "Email"; + multiple_phrasing = "Emails"; + } else if (id == 'fax') { + price = document.getElementById('fax_price').innerText; + single_phrasing = "Fax"; + multiple_phrasing = "Faxes"; + } else if (id == 'letter') { + price = document.getElementById('letter_price').innerText; + single_phrasing = "Letter"; + multiple_phrasing = "Letters"; + } else if (id == 'priority') { + price = document.getElementById('priority_price').innerText; + single_phrasing = "Priority Mail"; + multiple_phrasing = "Priority Mail"; + } + + document.getElementById('price_to_send').innerText = '$'+price; + + if (recipient_count > 1) { + document.getElementById('communication_mode').innerText = multiple_phrasing; + } else { + document.getElementById('communication_mode').innerText = single_phrasing; + } + + document.getElementById('communication_mode').setAttribute("value", id) +} + +// Disable communication modes based on the available options +var disableCommunications = function() { + selected_cards = document.getElementsByClassName('recipient_card selected_card'); + + // communication_option = ['email', 'fax', 'letter', 'priority']; + communication_option = ['fax', 'letter', 'priority']; + + for (var j = 0; j < communication_option.length; j++) { + c_option = communication_option[j]; + document.querySelector('button#'+c_option).disabled = false; + } + + + for (var i = 0; i < selected_cards.length; i++) { + id = selected_cards[i].getAttribute('value'); + + for (var j = 0; j < communication_option.length; j++) { + c_option = communication_option[j]; + if(document.getElementById( + c_option+'_'+id).getAttribute('value').length == 0) { + document.querySelector('button#'+c_option).disabled = true; + if (document.querySelector('button#'+c_option) + .classList.contains("selected_card")){ + document.querySelector('button#'+c_option) + .classList.remove("selected_card"); + document.getElementById('payment_container').style.display = 'none'; + } + } + } + } + + // Always disable email for the moment + document.querySelector('button#email').disabled = true; +} + +function load_stripe_checkout(id=null, email=null, count=null) { + + document.getElementById('payment_container').style.display = 'block'; + window.location.hash = "#payment_container"; + + // A reference to Stripe.js initialized with your real test publishable API key. + stripe_pk = document.getElementById('stripe_pk').getAttribute('value'); + var stripe = Stripe(stripe_pk); + + // The items the customer wants to buy + // TODO: Confirm email as required + var purchase = { + item: id, + email: email, + count: count + }; + + // Disable the button until we have Stripe set up on the page + document.querySelector("#submit").disabled = true; + fetch("/create-payment-intent.json", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(purchase) + }) + .then(function(result) { + return result.json(); + }) + .then(function(data) { + + var elements = stripe.elements(); + + var style = { + base: { + color: "#32325d", + fontFamily: 'Arial, sans-serif', + fontSmoothing: "antialiased", + fontSize: "16px", + "::placeholder": { + color: "#32325d" + } + }, + invalid: { + fontFamily: 'Arial, sans-serif', + color: "#fa755a", + iconColor: "#fa755a" + } + }; + + var card = elements.create("card", { style: style }); + + // Stripe injects an iframe into the DOM + card.mount("#card-element"); + + card.on("change", function (event) { + // Disable the Pay button if there are no card details in the Element + document.querySelector("#submit").disabled = event.empty; + document.querySelector("#card-error").textContent = event.error ? event.error.message : ""; + }); + + var form = document.getElementById("payment-form"); + form.addEventListener("submit", function(event) { + event.preventDefault(); + + // Complete payment when the submit button is clicked + payWithCard(stripe, card, data.clientSecret); + }); + }); + + // Calls stripe.confirmCardPayment + // If the card requires authentication Stripe shows a pop-up modal to + // prompt the user to enter authentication details without leaving your page. + var payWithCard = function(stripe, card, clientSecret) { + method = document.querySelector('.send_button.selected_card').getAttribute('id'); + loading(true); + stripe + .confirmCardPayment(clientSecret, { + payment_method: { + card: card, + billing_details: { + name: document.querySelector('#name').value, + email: document.querySelector('#email').value, + address: { + postal_code: document.querySelector('#sender_zipcode').getAttribute('value') + } + }, + metadata: { + method: method, + letter_id: document.querySelector('#pdf_view').getAttribute('value') + } + }, + receipt_email: document.querySelector('#email').value + }) + .then(function(result) { + if (result.error) { + // Show error to your customer + showError(result.error.message); + } else { + // The payment succeeded! + sendCommunication(result.paymentIntent.id, method); + orderComplete(result.paymentIntent.id, method); + } + }); + }; + + /* ------- UI helpers ------- */ + + // Shows a success message when the payment is complete + var orderComplete = function(paymentIntentId) { + loading(false); + document.querySelector(".result-message").classList.remove("hidden"); + document.querySelector("#submit").classList.add("hidden"); + document.querySelector("#card-element").classList.add("hidden"); + document.querySelector("#submit").disabled = true; + document.getElementById('share_container').style.display = 'block'; + }; + + // Show the customer the error from Stripe if their card fails to charge + var showError = function(errorMsgText) { + loading(false); + var errorMsg = document.querySelector("#card-error"); + errorMsg.textContent = errorMsgText; + setTimeout(function() { + errorMsg.textContent = ""; + }, 4000); + }; + + // Show a spinner on payment submission + var loading = function(isLoading) { + if (isLoading) { + // Disable the button and show a spinner + document.querySelector("#submit").disabled = true; + document.querySelector("#spinner").classList.remove("hidden"); + document.querySelector("#button-text").classList.add("hidden"); + } else { + document.querySelector("#submit").disabled = false; + document.querySelector("#spinner").classList.add("hidden"); + document.querySelector("#button-text").classList.remove("hidden"); + } + }; +} + +var sendCommunication = function(paymentIntentId, method) { + data = { + 'method': method, + 'sender': { + 'name': document.querySelector('#name').value, + 'email': document.querySelector('#email').value, + 'line_1': document.querySelector('#sender_line_1').getAttribute('value'), + 'line_2': document.querySelector('#sender_line_2').getAttribute('value'), + 'city': document.querySelector('#sender_city').getAttribute('value'), + 'state': document.querySelector('#sender_state').getAttribute('value'), + 'zipcode': document.querySelector('#sender_zipcode').getAttribute('value'), + 'country': document.querySelector('#sender_country').getAttribute('value'), + 'county': document.querySelector('#sender_county').getAttribute('value'), + 'district_federal': document.querySelector('#sender_district_federal').getAttribute('value'), + 'district_state_senate': document.querySelector('#sender_district_senate').getAttribute('value'), + 'district_state_representative': document.querySelector('#sender_district_representative').getAttribute('value'), + }, + 'recipients': [], + 'letter': { + 'id': document.querySelector('#pdf_view').getAttribute('value'), + 'category': $("#category").find(":selected").text(), + 'sentiment': $("#sentiment").find(":selected").text(), + 'policy_or_law': $("#policy_or_law").find(":selected").text() + }, + 'payment': { + 'id': paymentIntentId + } + } + + selected_cards = document.getElementsByClassName('selected_card') + for (const element of selected_cards){ + if(element.getAttribute('value') != null){ + data['recipients'].push(element.getAttribute('value')); + } + } + + send_communication_url = '/send_communication' + $.post({ + url: send_communication_url, + cache: false, + data: data + }) + + /* done - Sender: name, email, zipcode, county, district, state */ + /* done - Recipients: id */ + /* done - Letter: id, cat, sent, policy */ + /* done - PaymentId: log payment intent for later queries */ + /* send to URL for logging Communication: priority, letter, fax, email */ +} + +function copyLetterToClipboard(){ + + category = $('#category').find(":selected").attr('value'); + sentiment = $('#sentiment').find(":selected").text(); + policy_or_law = $('#policy_or_law').find(":selected").text(); + + var get_url = window.location; + var base_url = get_url.protocol + "//" + get_url.host + "?" + + var param_url = "" + + param_url += "category=" + encodeURIComponent(category) + "&"; + param_url += "sentiment=" + encodeURIComponent(sentiment) + "&"; + param_url += "policy_or_law=" + encodeURIComponent(policy_or_law); + + navigator.clipboard.writeText(base_url + param_url); + + document.getElementById("share_button").innerHTML = "Copied Link!" +} + + diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..9f35d6e --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'noreply@vocalvoters.com' + layout 'mailer' +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 0000000..4cd38bd --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,13 @@ +class UserMailer < ApplicationMailer + + def account_activation(user) + @user = user + mail to: user.email, subject: "Account activation" + end + + def password_reset(user) + @user = user + mail to: user.email, subject: "Password reset" + end + +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..10a4cba --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/email.rb b/app/models/email.rb new file mode 100644 index 0000000..543234a --- /dev/null +++ b/app/models/email.rb @@ -0,0 +1,6 @@ +class Email < ApplicationRecord + belongs_to :sender + belongs_to :recipient + belongs_to :letter + belongs_to :payment +end diff --git a/app/models/fax.rb b/app/models/fax.rb new file mode 100644 index 0000000..7739dd7 --- /dev/null +++ b/app/models/fax.rb @@ -0,0 +1,5 @@ +class Fax < ApplicationRecord + belongs_to :sender + belongs_to :recipient + belongs_to :letter +end diff --git a/app/models/letter.rb b/app/models/letter.rb new file mode 100644 index 0000000..d5e188b --- /dev/null +++ b/app/models/letter.rb @@ -0,0 +1,11 @@ +class Letter < ApplicationRecord + belongs_to :user + before_save :downcase_fields + + private + + def downcase_fields + self.category = category.downcase + self.tags = tags.downcase + end +end diff --git a/app/models/post.rb b/app/models/post.rb new file mode 100644 index 0000000..4d12821 --- /dev/null +++ b/app/models/post.rb @@ -0,0 +1,5 @@ +class Post < ApplicationRecord + belongs_to :sender + belongs_to :recipient + belongs_to :letter +end diff --git a/app/models/recipient.rb b/app/models/recipient.rb new file mode 100644 index 0000000..abf1da2 --- /dev/null +++ b/app/models/recipient.rb @@ -0,0 +1,2 @@ +class Recipient < ApplicationRecord +end diff --git a/app/models/sender.rb b/app/models/sender.rb new file mode 100644 index 0000000..81ba4af --- /dev/null +++ b/app/models/sender.rb @@ -0,0 +1,2 @@ +class Sender < ApplicationRecord +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..c531691 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,94 @@ +class User < ApplicationRecord + attr_accessor :remember_token, :activation_token, :reset_token + before_save :downcase_email + before_create :create_activation_digest + + validates :name, presence: true, length: { maximum: 50 } + + before_save { email.downcase! } + VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i + validates :email, presence: true, length: { maximum: 255 }, + format: { with: VALID_EMAIL_REGEX }, + uniqueness: true + has_secure_password + validates :password, presence: true, length: { minimum: 6 }, allow_nil: true + + + # Returns the hash digest of the given string. + def User.digest(string) + cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : + BCrypt::Engine.cost + BCrypt::Password.create(string, cost: cost) + end + + # Returns a random token. + def User.new_token + SecureRandom.urlsafe_base64 + end + + # Remembers a user in the database for use in persistent sessions. + def remember + self.remember_token = User.new_token + update_attribute(:remember_digest, User.digest(remember_token)) + remember_digest + end + + # Returns true if the given token matches the digest. + def authenticated?(attribute, token) + digest = send("#{attribute}_digest") + return false if digest.nil? + BCrypt::Password.new(digest).is_password?(token) + end + + # Forgets a user. + def forget + update_attribute(:remember_digest, nil) + end + + # Returns a session token to prevent session hijacking. + # We reuse the remember digest for convenience. + def session_token + remember_digest || remember + end + + # Activates an account. + def activate + update_columns(activated: true, activated_at: Time.zone.now) + end + + # Sends activation email. + def send_activation_email + UserMailer.account_activation(self).deliver_now + end + + # Sets the password reset attributes. + def create_reset_digest + self.reset_token = User.new_token + update_columns(reset_digest: User.digest(reset_token), + reset_sent_at: Time.zone.now) + end + + # Sends password reset email. + def send_password_reset_email + UserMailer.password_reset(self).deliver_now + end + + # Returns true if a password reset has expired. + def password_reset_expired? + reset_sent_at < 2.hours.ago + end + + private + + # Converts email to all lower-case. + def downcase_email + self.email = email.downcase + end + + # Creates and assigns the activation token and digest. + def create_activation_digest + self.activation_token = User.new_token + self.activation_digest = User.digest(activation_token) + end + +end diff --git a/app/views/emails/_email.json.jbuilder b/app/views/emails/_email.json.jbuilder new file mode 100644 index 0000000..0a2abd6 --- /dev/null +++ b/app/views/emails/_email.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! email, :id, :email_address, :sender_id, :recipient_id, :letter_id, :payment_id, :created_at, :updated_at +json.url email_url(email, format: :json) diff --git a/app/views/emails/_form.html.erb b/app/views/emails/_form.html.erb new file mode 100644 index 0000000..8d4ab30 --- /dev/null +++ b/app/views/emails/_form.html.erb @@ -0,0 +1,42 @@ +<%= form_with(model: email) do |form| %> + <% if email.errors.any? %> +
+

<%= pluralize(email.errors.count, "error") %> prohibited this email from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :email_address %> + <%= form.text_field :email_address %> +
+ +
+ <%= form.label :sender_id %> + <%= form.text_field :sender_id %> +
+ +
+ <%= form.label :recipient_id %> + <%= form.text_field :recipient_id %> +
+ +
+ <%= form.label :letter_id %> + <%= form.text_field :letter_id %> +
+ +
+ <%= form.label :payment_id %> + <%= form.text_field :payment_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/emails/edit.html.erb b/app/views/emails/edit.html.erb new file mode 100644 index 0000000..1431b41 --- /dev/null +++ b/app/views/emails/edit.html.erb @@ -0,0 +1,6 @@ +

Editing Email

+ +<%= render 'form', email: @email %> + +<%= link_to 'Show', @email %> | +<%= link_to 'Back', emails_path %> diff --git a/app/views/emails/index.html.erb b/app/views/emails/index.html.erb new file mode 100644 index 0000000..de7f2fb --- /dev/null +++ b/app/views/emails/index.html.erb @@ -0,0 +1,37 @@ +

<%= notice %>

+ +

Emails

+ + + + + + + + + + + + + + + + <% @emails.each do |email| %> + + + + + + + + + + + + <% end %> + +
Email addressSenderRecipientLetterPaymentSuccess
<%= email.email_address %><%= email.sender_id %><%= email.recipient_id %><%= email.letter_id %><%= email.payment %><%= email.success %><%= link_to 'Show', email %><%= link_to 'Edit', edit_email_path(email) %><%= link_to 'Destroy', email, method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to 'New Email', new_email_path %> diff --git a/app/views/emails/index.json.jbuilder b/app/views/emails/index.json.jbuilder new file mode 100644 index 0000000..70f812c --- /dev/null +++ b/app/views/emails/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @emails, partial: "emails/email", as: :email diff --git a/app/views/emails/new.html.erb b/app/views/emails/new.html.erb new file mode 100644 index 0000000..61c2c95 --- /dev/null +++ b/app/views/emails/new.html.erb @@ -0,0 +1,5 @@ +

New Email

+ +<%= render 'form', email: @email %> + +<%= link_to 'Back', emails_path %> diff --git a/app/views/emails/show.html.erb b/app/views/emails/show.html.erb new file mode 100644 index 0000000..8a3319a --- /dev/null +++ b/app/views/emails/show.html.erb @@ -0,0 +1,29 @@ +

<%= notice %>

+ +

+ Email address: + <%= @email.email_address %> +

+ +

+ Sender: + <%= @email.sender_id %> +

+ +

+ Recipient: + <%= @email.recipient_id %> +

+ +

+ Letter: + <%= @email.letter_id %> +

+ +

+ Payment: + <%= @email.payment_id %> +

+ +<%= link_to 'Edit', edit_email_path(@email) %> | +<%= link_to 'Back', emails_path %> diff --git a/app/views/emails/show.json.jbuilder b/app/views/emails/show.json.jbuilder new file mode 100644 index 0000000..ff416bd --- /dev/null +++ b/app/views/emails/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "emails/email", email: @email diff --git a/app/views/faxes/_fax.json.jbuilder b/app/views/faxes/_fax.json.jbuilder new file mode 100644 index 0000000..94b99e1 --- /dev/null +++ b/app/views/faxes/_fax.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! fax, :id, :number_fax, :sender_id, :recipient_id, :letter_id, :payment_id, :created_at, :updated_at +json.url fax_url(fax, format: :json) diff --git a/app/views/faxes/_form.html.erb b/app/views/faxes/_form.html.erb new file mode 100644 index 0000000..42d447a --- /dev/null +++ b/app/views/faxes/_form.html.erb @@ -0,0 +1,42 @@ +<%= form_with(model: fax) do |form| %> + <% if fax.errors.any? %> +
+

<%= pluralize(fax.errors.count, "error") %> prohibited this fax from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :number_fax %> + <%= form.number_field :number_fax %> +
+ +
+ <%= form.label :sender_id %> + <%= form.text_field :sender_id %> +
+ +
+ <%= form.label :recipient_id %> + <%= form.text_field :recipient_id %> +
+ +
+ <%= form.label :letter_id %> + <%= form.text_field :letter_id %> +
+ +
+ <%= form.label :payment_id %> + <%= form.text_field :payment_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/faxes/edit.html.erb b/app/views/faxes/edit.html.erb new file mode 100644 index 0000000..ab1d74a --- /dev/null +++ b/app/views/faxes/edit.html.erb @@ -0,0 +1,6 @@ +

Editing Fax

+ +<%= render 'form', fax: @fax %> + +<%= link_to 'Show', @fax %> | +<%= link_to 'Back', faxes_path %> diff --git a/app/views/faxes/index.html.erb b/app/views/faxes/index.html.erb new file mode 100644 index 0000000..4e764af --- /dev/null +++ b/app/views/faxes/index.html.erb @@ -0,0 +1,37 @@ +

<%= notice %>

+ +

Faxes

+ + + + + + + + + + + + + + + + <% @faxes.each do |fax| %> + + + + + + + + + + + + <% end %> + +
Number faxSenderRecipientLetterPaymentSuccess
<%= fax.number_fax %><%= fax.sender_id %><%= fax.recipient_id %><%= fax.letter_id %><%= fax.payment %><%= fax.success %><%= link_to 'Show', fax %><%= link_to 'Edit', edit_fax_path(fax) %><%= link_to 'Destroy', fax, method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to 'New Fax', new_fax_path %> diff --git a/app/views/faxes/index.json.jbuilder b/app/views/faxes/index.json.jbuilder new file mode 100644 index 0000000..42c75e2 --- /dev/null +++ b/app/views/faxes/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @faxes, partial: "faxes/fax", as: :fax diff --git a/app/views/faxes/new.html.erb b/app/views/faxes/new.html.erb new file mode 100644 index 0000000..6a4ed85 --- /dev/null +++ b/app/views/faxes/new.html.erb @@ -0,0 +1,5 @@ +

New Fax

+ +<%= render 'form', fax: @fax %> + +<%= link_to 'Back', faxes_path %> diff --git a/app/views/faxes/show.html.erb b/app/views/faxes/show.html.erb new file mode 100644 index 0000000..ba5681d --- /dev/null +++ b/app/views/faxes/show.html.erb @@ -0,0 +1,29 @@ +

<%= notice %>

+ +

+ Number fax: + <%= @fax.number_fax %> +

+ +

+ Sender: + <%= @fax.sender_id %> +

+ +

+ Recipient: + <%= @fax.recipient_id %> +

+ +

+ Letter: + <%= @fax.letter_id %> +

+ +

+ Payment: + <%= @fax.payment_id %> +

+ +<%= link_to 'Edit', edit_fax_path(@fax) %> | +<%= link_to 'Back', faxes_path %> diff --git a/app/views/faxes/show.json.jbuilder b/app/views/faxes/show.json.jbuilder new file mode 100644 index 0000000..16a6591 --- /dev/null +++ b/app/views/faxes/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "faxes/fax", fax: @fax diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb new file mode 100644 index 0000000..fad83f4 --- /dev/null +++ b/app/views/layouts/_footer.html.erb @@ -0,0 +1,12 @@ + diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb new file mode 100644 index 0000000..ccbc3d3 --- /dev/null +++ b/app/views/layouts/_header.html.erb @@ -0,0 +1,65 @@ + diff --git a/app/views/layouts/_shim.html.erb b/app/views/layouts/_shim.html.erb new file mode 100644 index 0000000..ecfd51d --- /dev/null +++ b/app/views/layouts/_shim.html.erb @@ -0,0 +1,4 @@ + diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..ea68839 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,39 @@ + + + + + <%= favicon_link_tag 'vocalvoters_logos/vocalvoters-favicon-256.png' %> + <%= full_title(yield(:title)) %> + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + + + + <%= stylesheet_link_tag 'application', media: 'all', + 'data-turbolinks-track': 'reload' %> + <%= javascript_pack_tag 'application', media: 'all', + 'data-turbolinks-track': 'reload' %> + + <%= render 'layouts/shim' %> + + <%= wicked_pdf_stylesheet_link_tag "pdf" %> + + + + > + <%= render 'layouts/header' %> +
+ <% flash.each do |message_type, message| %> + <%= content_tag(:div, message, class: "alert alert-#{message_type}") %> + <% end %> + <%= yield %> + <%= render 'layouts/footer' %> + <%= debug(params) if Rails.env.development? %> +
+ + + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..cbd34d2 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/layouts/pdf.html.erb b/app/views/layouts/pdf.html.erb new file mode 100644 index 0000000..05bc7f2 --- /dev/null +++ b/app/views/layouts/pdf.html.erb @@ -0,0 +1,15 @@ + + + + + <%= wicked_pdf_stylesheet_link_tag "pdf" %> + + + +
+ <%= yield %> +
+ + diff --git a/app/views/letters/_form.html.erb b/app/views/letters/_form.html.erb new file mode 100644 index 0000000..2e54344 --- /dev/null +++ b/app/views/letters/_form.html.erb @@ -0,0 +1,114 @@ +<%= form_with(model: letter) do |form| %> + <% if letter.errors.any? %> +
+

<%= pluralize(letter.errors.count, "error") %> prohibited this letter from being saved:

+ + +
+ <% end %> + +
+
+ <%= form.label :category %> + <%= form.text_field :category, class: 'form-control' %> +
+ +
+ <%= form.label :policy_or_law %> + <%= form.text_field :policy_or_law, class: 'form-control' %> +
+ +
+ <%= form.label :tags %> + <%= form.text_field :tags, class: 'form-control' %> +
+ +
+ <%= form.label :sentiment %> + <%= form.select :sentiment, + options_for_select([['Very Positive', 1.0], ['Positive', 0.5], + ['Neutral', 0],['Negative', -0.5], ['Very Negative', -1.0]], + @letter.sentiment), {}, { :class => 'form-control' } %> +
+ +
+ <%= form.label :target_level %> + <%= form.select :target_level, + options_for_select([['All', 'all'], ['Federal', 'federal'], + ['State', 'state'],['Local', 'local']], @letter.target_level), + {}, { :class => 'form-control' } %> +
+ +
+ <%= form.label :target_state %> + <%= form.select :target_state, + options_for_select([ + ['All', 'all'], + ['Alabama', 'alabama'], + ['Alaska', 'alaska'], + ['Arizona', 'arizona'], + ['Arkansas', 'arkansas'], + ['California', 'california'], + ['Colorado', 'colorado'], + ['Connecticut', 'connecticut'], + ['Delaware', 'delaware'], + ['Florida', 'florida'], + ['Georgia', 'georgia'], + ['Hawaii', 'hawaii'], + ['Idaho', 'idaho'], + ['Illinois', 'illinois'], + ['Indiana', 'indiana'], + ['Iowa', 'iowa'], + ['Kansas', 'kansas'], + ['Kentucky', 'kentucky'], + ['Louisiana', 'louisiana'], + ['Maine', 'maine'], + ['Maryland', 'maryland'], + ['Massachusetts', 'massachusetts'], + ['Michigan', 'michigan'], + ['Minnesota', 'minnesota'], + ['Mississippi', 'mississippi'], + ['Missouri', 'missouri'], + ['Montana', 'montana'], + ['Nebraska', 'nebraska'], + ['Navada', 'navada'], + ['New Hampshire', 'new hampshire'], + ['New Jersey', 'new jersey'], + ['New Mexico', 'new mexico'], + ['New York', 'new york'], + ['North Carolina', 'north carolina'], + ['North Dakota', 'north dakota'], + ['Ohio', 'ohio'], + ['Oklahoma', 'oklahoma'], + ['Oregon', 'oregon'], + ['Pennsylvania', 'pennsylvania'], + ['Rhode Island', 'rhode island'], + ['South Carolina', 'south carolina'], + ['South Dakota', 'south dakota'], + ['Tennessee', 'tennessee'], + ['Texas', 'texas'], + ['Utah', 'utah'], + ['Vermont', 'vermont'], + ['Virginia', 'virginia'], + ['Washington', 'washington'], + ['West Virginia', 'west virginia'], + ['Wisconsin', 'wisconsin'], + ['Wyoming', 'wyoming']], @letter.target_state), + {}, { :class => 'form-control' } %> +
+
+ +
+
+ <%= form.label :body %> + <%= form.text_area :body, class: 'form-control' %> +
+
+ <%= form.submit yield(:button_text), class: "btn btn-primary" %> +
+
+<% end %> diff --git a/app/views/letters/_letter.json.jbuilder b/app/views/letters/_letter.json.jbuilder new file mode 100644 index 0000000..d243d29 --- /dev/null +++ b/app/views/letters/_letter.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! letter, :id, :category, :policy_or_law, :sentiment, :body, :user_id, :created_at, :updated_at +json.url letter_url(letter, format: :json) diff --git a/app/views/letters/edit.html.erb b/app/views/letters/edit.html.erb new file mode 100644 index 0000000..8cb20bb --- /dev/null +++ b/app/views/letters/edit.html.erb @@ -0,0 +1,12 @@ +<% provide(:title, 'Edit Letter') %> +<% provide(:button_text, 'Save changes') %> +

Editing Letter

+ +
+
+ <%= render 'form', letter: @letter %> +
+
+ +<%= link_to 'Show', @letter %> | +<%= link_to 'Back', letters_path %> diff --git a/app/views/letters/find_policy.json.jbuilder b/app/views/letters/find_policy.json.jbuilder new file mode 100644 index 0000000..5a485b9 --- /dev/null +++ b/app/views/letters/find_policy.json.jbuilder @@ -0,0 +1 @@ +json.array! @policy_or_laws diff --git a/app/views/letters/index.html.erb b/app/views/letters/index.html.erb new file mode 100644 index 0000000..49db5e3 --- /dev/null +++ b/app/views/letters/index.html.erb @@ -0,0 +1,35 @@ +

<%= notice %>

+ +

Letters

+ + + + + + + + + + + + + + + <% @letters.each do |letter| %> + + + + + + + + + + + <% end %> + +
CategoryPolicy or lawSentimentBodyUser
<%= letter.category %><%= letter.policy_or_law %><%= letter.sentiment %><%= letter.body %><%= letter.user_id %><%= link_to 'Show', letter %><%= link_to 'Edit', edit_letter_path(letter) %><%= link_to 'Destroy', letter, method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to 'New Letter', new_letter_path %> diff --git a/app/views/letters/index.json.jbuilder b/app/views/letters/index.json.jbuilder new file mode 100644 index 0000000..45c84ae --- /dev/null +++ b/app/views/letters/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @letters, partial: "letters/letter", as: :letter diff --git a/app/views/letters/letter.html.erb b/app/views/letters/letter.html.erb new file mode 100644 index 0000000..aa1e86c --- /dev/null +++ b/app/views/letters/letter.html.erb @@ -0,0 +1,78 @@ + + +
+
+
+
+
+
+ <%= @recipient_position.html_safe %> <%= @recipient_name.html_safe %>
+ <%= @recipient_location.html_safe %>
+ <% if @recipient.present? %> + <%= @recipient.address_line_1 %>
+ <%= @recipient.address_line_2 %> + <%= @recipient.address_city %> <%= @recipient.address_state %>, + <%= @recipient.address_zipcode %> + <% end %> +
+
+ +
+

+ <%= @recipient_name.html_safe %>, +

+ +

+ My name is <%= @sender_name.html_safe %> and I am writing to express that I + <%= @sentiment %> <%= @letter.category %>. +

+ +

+ Particlulary, I <%= @sentiment %> <%= @letter.policy_or_law %>. +

+ + <%= simple_format(@letter.body) %> + +

+ Thank you,
+ <%= @sender_name.html_safe %>
+ <%= @sender_region.html_safe %> + <%= @sender_region_verified.html_safe %> +

+
+ + diff --git a/app/views/letters/new.html.erb b/app/views/letters/new.html.erb new file mode 100644 index 0000000..13325bf --- /dev/null +++ b/app/views/letters/new.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, 'Edit Letter') %> +<% provide(:button_text, 'Create Letter') %> +

New Letter

+ +
+
+ <%= render 'form', letter: @letter %> +
+
+ +<%= link_to 'Back', letters_path %> diff --git a/app/views/letters/show.html.erb b/app/views/letters/show.html.erb new file mode 100644 index 0000000..3381db2 --- /dev/null +++ b/app/views/letters/show.html.erb @@ -0,0 +1,35 @@ +

<%= notice %>

+ +

+ Category: + <%= @letter.category %>
+ Policy or law: + <%= @letter.policy_or_law %>
+ Target Level: + <%= @letter.target_level %>
+ Target State: + <%= @letter.target_state %>
+ Created by: + <%= User.find_by(id: @letter.user_id).name %> +

+ +

+ Sentiment: + <%= @letter.sentiment %> +

+ +

+ Body:
+ <%= @letter.body %> +

+ +

+ Letter:
+ +

+ + +<%= link_to 'Edit', edit_letter_path(@letter) %> | +<%= link_to 'Back', letters_path %> diff --git a/app/views/letters/show.json.jbuilder b/app/views/letters/show.json.jbuilder new file mode 100644 index 0000000..3cfeee2 --- /dev/null +++ b/app/views/letters/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "letters/letter", letter: @letter diff --git a/app/views/password_resets/edit.html.erb b/app/views/password_resets/edit.html.erb new file mode 100644 index 0000000..0c299a7 --- /dev/null +++ b/app/views/password_resets/edit.html.erb @@ -0,0 +1,21 @@ +<% provide(:title, 'Reset password') %> +

Reset password

+ +
+
+ <%= form_with(model: @user, url: password_reset_path(params[:id]), + local: true) do |f| %> + <%= render 'shared/error_messages' %> + + <%= hidden_field_tag :email, @user.email %> + + <%= f.label :password %> + <%= f.password_field :password, class: 'form-control' %> + + <%= f.label :password_confirmation, "Confirmation" %> + <%= f.password_field :password_confirmation, class: 'form-control' %> + + <%= f.submit "Update password", class: "btn btn-primary" %> + <% end %> +
+
diff --git a/app/views/password_resets/new.html.erb b/app/views/password_resets/new.html.erb new file mode 100644 index 0000000..f21b8f4 --- /dev/null +++ b/app/views/password_resets/new.html.erb @@ -0,0 +1,14 @@ +<% provide(:title, "Forgot password") %> +

Forgot password

+ +
+
+ <%= form_with(url: password_resets_path, scope: :password_reset, + local: true) do |f| %> + <%= f.label :email %> + <%= f.email_field :email, class: 'form-control' %> + + <%= f.submit "Submit", class: "btn btn-primary" %> + <% end %> +
+
diff --git a/app/views/posts/_form.html.erb b/app/views/posts/_form.html.erb new file mode 100644 index 0000000..9cd5c7b --- /dev/null +++ b/app/views/posts/_form.html.erb @@ -0,0 +1,62 @@ +<%= form_with(model: post) do |form| %> + <% if post.errors.any? %> +
+

<%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :address_line_1 %> + <%= form.text_field :address_line_1 %> +
+ +
+ <%= form.label :address_line_2 %> + <%= form.text_field :address_line_2 %> +
+ +
+ <%= form.label :address_city %> + <%= form.text_field :address_city %> +
+ +
+ <%= form.label :address_state %> + <%= form.text_field :address_state %> +
+ +
+ <%= form.label :address_zipcode %> + <%= form.text_field :address_zipcode %> +
+ +
+ <%= form.label :sender_id %> + <%= form.text_field :sender_id %> +
+ +
+ <%= form.label :recipient_id %> + <%= form.text_field :recipient_id %> +
+ +
+ <%= form.label :letter_id %> + <%= form.text_field :letter_id %> +
+ +
+ <%= form.label :payment_id %> + <%= form.text_field :payment_id %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/posts/_post.json.jbuilder b/app/views/posts/_post.json.jbuilder new file mode 100644 index 0000000..a4ef438 --- /dev/null +++ b/app/views/posts/_post.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! post, :id, :address_line_1, :address_line_2, :address_city, :address_state, :address_zipcode, :sender_id, :recipient_id, :letter_id, :payment_id, :created_at, :updated_at +json.url post_url(post, format: :json) diff --git a/app/views/posts/edit.html.erb b/app/views/posts/edit.html.erb new file mode 100644 index 0000000..ded33f7 --- /dev/null +++ b/app/views/posts/edit.html.erb @@ -0,0 +1,6 @@ +

Editing Post

+ +<%= render 'form', post: @post %> + +<%= link_to 'Show', @post %> | +<%= link_to 'Back', posts_path %> diff --git a/app/views/posts/index.html.erb b/app/views/posts/index.html.erb new file mode 100644 index 0000000..e5c3a52 --- /dev/null +++ b/app/views/posts/index.html.erb @@ -0,0 +1,47 @@ +

<%= notice %>

+ +

Posts

+ + + + + + + + + + + + + + + + + + + + + <% @posts.each do |post| %> + + + + + + + + + + + + + + + + + <% end %> + +
Address line 1Address line 2Address cityAddress stateAddress zipcodePrioritySenderRecipientLetterPaymentSuccess
<%= post.address_line_1 %><%= post.address_line_2 %><%= post.address_city %><%= post.address_state %><%= post.address_zipcode %><%= post.priority %><%= post.sender_id %><%= post.recipient_id %><%= post.letter_id %><%= post.payment %><%= post.success %><%= link_to 'Show', post %><%= link_to 'Edit', edit_post_path(post) %><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to 'New Post', new_post_path %> diff --git a/app/views/posts/index.json.jbuilder b/app/views/posts/index.json.jbuilder new file mode 100644 index 0000000..a3c6f4a --- /dev/null +++ b/app/views/posts/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @posts, partial: "posts/post", as: :post diff --git a/app/views/posts/new.html.erb b/app/views/posts/new.html.erb new file mode 100644 index 0000000..fb1e2a1 --- /dev/null +++ b/app/views/posts/new.html.erb @@ -0,0 +1,5 @@ +

New Post

+ +<%= render 'form', post: @post %> + +<%= link_to 'Back', posts_path %> diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb new file mode 100644 index 0000000..2036a5c --- /dev/null +++ b/app/views/posts/show.html.erb @@ -0,0 +1,49 @@ +

<%= notice %>

+ +

+ Address line 1: + <%= @post.address_line_1 %> +

+ +

+ Address line 2: + <%= @post.address_line_2 %> +

+ +

+ Address city: + <%= @post.address_city %> +

+ +

+ Address state: + <%= @post.address_state %> +

+ +

+ Address zipcode: + <%= @post.address_zipcode %> +

+ +

+ Sender: + <%= @post.sender_id %> +

+ +

+ Recipient: + <%= @post.recipient_id %> +

+ +

+ Letter: + <%= @post.letter_id %> +

+ +

+ Payment: + <%= @post.payment_id %> +

+ +<%= link_to 'Edit', edit_post_path(@post) %> | +<%= link_to 'Back', posts_path %> diff --git a/app/views/posts/show.json.jbuilder b/app/views/posts/show.json.jbuilder new file mode 100644 index 0000000..5274482 --- /dev/null +++ b/app/views/posts/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "posts/post", post: @post diff --git a/app/views/recipients/_form.html.erb b/app/views/recipients/_form.html.erb new file mode 100644 index 0000000..1554988 --- /dev/null +++ b/app/views/recipients/_form.html.erb @@ -0,0 +1,202 @@ +<%= form_with(model: recipient) do |form| %> + <% if recipient.errors.any? %> +
+

<%= pluralize(recipient.errors.count, "error") %> prohibited this recipient from being saved:

+ + +
+ <% end %> + +
+
+
+ <%= form.label :name %> + <%= form.text_field :name, class: 'form-control' %> +
+
+
+ +
+
+
+ <%= form.label :level %> + <%= form.select :level, + [['Local', 'local'], + ['State', 'state'], + ['Federal', 'federal']], + { include_blank: true }, { class: 'form-control' } %> +
+
+ +
+
+ <%= form.label :position %> + <%= form.select :position, + [['Representative', 'representative'], + ['Senator', 'senator'], + ['Governor', 'governor']], + { include_blank: true }, { class: 'form-control' } %> + +
+
+ +
+
+ <%= form.label :state %> + <%= form.select :state, + [['Alabama', 'AL'], + ['Alaska', 'AK'], + ['Arizona', 'AZ'], + ['Arkansas', 'AR'], + ['California', 'CA'], + ['Colorado', 'CO'], + ['Connecticut', 'CT'], + ['Delaware', 'DE'], + ['Florida', 'FL'], + ['Georgia', 'GA'], + ['Hawaii', 'HI'], + ['Idaho', 'ID'], + ['Illinois', 'IL'], + ['Indiana', 'IN'], + ['Iowa', 'IA'], + ['Kansas', 'KS'], + ['Kentucky', 'KY'], + ['Louisiana', 'LA'], + ['Maine', 'ME'], + ['Maryland', 'MD'], + ['Massachusetts', 'MA'], + ['Michigan', 'MI'], + ['Minnesota', 'MN'], + ['Mississippi', 'MS'], + ['Missouri', 'MO'], + ['Montana', 'MT'], + ['Nebraska', 'NE'], + ['Navada', 'NV'], + ['New Hampshire', 'NH'], + ['New Jersey', 'NJ'], + ['New Mexico', 'NM'], + ['New York', 'NY'], + ['North Carolina', 'NC'], + ['North Dakota', 'ND'], + ['Ohio', 'OH'], + ['Oklahoma', 'OK'], + ['Oregon', 'OR'], + ['Pennsylvania', 'PA'], + ['Rhode Island', 'RI'], + ['South Carolina', 'SC'], + ['South Dakota', 'SD'], + ['Tennessee', 'TN'], + ['Texas', 'TX'], + ['Utah', 'UT'], + ['Vermont', 'VT'], + ['Virginia', 'VA'], + ['Washington', 'WA'], + ['West Virginia', 'WV'], + ['Wisconsin', 'WI'], + ['Wyoming', 'WY']], + { include_blank: true }, { class: 'form-control' } %> +
+
+ +
+
+ <%= form.label :district %> + <%= form.text_field :district, class: 'form-control' %> +
+
+
+
+ <%= form.label :retirement_status %> + <%= form.select :retired, + [['Active', false], ['Retired', true]], + { include_blank: true }, { class: 'form-control' } %> +
+
+ +
+
+
+
+ +
+
+
+ <%= form.label :number_phone %> + <%= form.number_field :number_phone, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :number_fax %> + <%= form.number_field :number_fax, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :email_address %> + <%= form.text_field :email_address, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :contact_form %> + <%= form.text_field :contact_form, class: 'form-control' %> +
+
+ +
+
+
+
+ +
+
+
+ <%= form.label :address_line_1 %> + <%= form.text_field :address_line_1, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :address_line_2 %> + <%= form.text_field :address_line_2, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :address_city %> + <%= form.text_field :address_city, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :address_state %> + <%= form.text_field :address_state, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :address_zipcode %> + <%= form.text_field :address_zipcode, class: 'form-control' %> +
+
+ +
+ +
+
+ <%= form.submit yield(:button_text), class: "btn btn-primary" %> +
+
+<% end %> diff --git a/app/views/recipients/_recipient.html.erb b/app/views/recipients/_recipient.html.erb new file mode 100644 index 0000000..5157cd0 --- /dev/null +++ b/app/views/recipients/_recipient.html.erb @@ -0,0 +1,65 @@ +
+ <%= recipient.name %>, + <% if recipient.level == 'federal' %>US<% else %><%= recipient.state %><% end %> + <%= recipient.position.capitalize %> +
+ <% if recipient.level == 'federal' %> + <% if recipient.position == 'senator' %> + US + <%= recipient.position.capitalize %> from + <%= recipient.state %> + <% else %> + <%= recipient.state %> + <%= recipient.district.to_i.ordinalize %> + Congressional District + <% end %> + <% else %> + <%= recipient.state %> + <% if recipient.position != 'governor' %> + <% if recipient.position == 'senator' %> + Senator District + <% else %> + House District + <% end %> + <%= recipient.district.to_i.ordinalize %> + <% else %> + Governor + <% end %> + <% end %> +
Phone #: + <%= number_to_phone(recipient.number_phone, area_code: true) %>
+ <% if recipient.contact_form.present? %> + Contact Form: <%=link_to 'Link', + recipient.contact_form, target: "_blank" %> +
+ <% end %> + Mail: + <%= recipient.address_line_1 %>
+ <%= recipient.address_city %> + <%= recipient.address_state %> <%= recipient.address_zipcode %> + + + + + + + + + + + + +
diff --git a/app/views/recipients/_recipient.json.jbuilder b/app/views/recipients/_recipient.json.jbuilder new file mode 100644 index 0000000..bb7c9aa --- /dev/null +++ b/app/views/recipients/_recipient.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! recipient, :id, :name, :position, :level, :district, :state, :number_fax, :number_phone, :email_address, :address_line_1, :address_line_2, :address_city, :address_state, :address_zipcode, :created_at, :updated_at +json.url recipient_url(recipient, format: :json) diff --git a/app/views/recipients/edit.html.erb b/app/views/recipients/edit.html.erb new file mode 100644 index 0000000..2db17b1 --- /dev/null +++ b/app/views/recipients/edit.html.erb @@ -0,0 +1,12 @@ +<% provide(:title, 'Edit Recipient') %> +<% provide(:button_text, 'Save changes') %> +

Editing Recipient

+ +
+
+ <%= render 'form', recipient: @recipient %> +
+
+ +<%= link_to 'Show', @recipient %> | +<%= link_to 'Back', recipients_path %> diff --git a/app/views/recipients/index.html.erb b/app/views/recipients/index.html.erb new file mode 100644 index 0000000..28f9d74 --- /dev/null +++ b/app/views/recipients/index.html.erb @@ -0,0 +1,65 @@ +<% provide(:title, 'Recipients') %> + +

<%= notice %>

+ +
+

+
+ Recipients +
+
+ <%= link_to 'New Recipient', new_recipient_path %> +
+

+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + <% @recipients.each do |recipient| %> + + + + + + + + + + + + + + + + + + + + <% end %> + +
RetiredNamePositionLevelDistrictStateNumber faxNumber phoneEmail addressAddress line 1Address line 2Address cityAddress stateAddress zipcode
<%= recipient.retired %><%= recipient.name %><%= recipient.position %><%= recipient.level %><%= recipient.district %><%= recipient.state %><%= recipient.number_fax %><%= recipient.number_phone %><%= recipient.email_address %><%= recipient.address_line_1 %><%= recipient.address_line_2 %><%= recipient.address_city %><%= recipient.address_state %><%= recipient.address_zipcode %><%= link_to 'Show', recipient %><%= link_to 'Edit', edit_recipient_path(recipient) %><%= link_to 'Destroy', recipient, method: :delete, data: { confirm: 'Are you sure?' } %>
+
diff --git a/app/views/recipients/index.json.jbuilder b/app/views/recipients/index.json.jbuilder new file mode 100644 index 0000000..14437d8 --- /dev/null +++ b/app/views/recipients/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @recipients, partial: "recipients/recipient", as: :recipient diff --git a/app/views/recipients/lookup.html.erb b/app/views/recipients/lookup.html.erb new file mode 100644 index 0000000..f6e93aa --- /dev/null +++ b/app/views/recipients/lookup.html.erb @@ -0,0 +1,57 @@ +
+
+ + + + + + + + + + + + + +
+
+ + +
+
+

+
+
+
+
+
+ Select Legislator(s)
+ + <%= @sender_line_1 %> <%= @sender_line_2 %> + <%= @sender_city %>, <%= @sender_state %> <%= @sender_zipcode %> + <% if @accuracy_class.present? %> +
Review Address, Potentially Inaccurate + <% end %> +
+
+

+ <% federal_flag = false %> + <% state_flag = false %> + <% for recipient in @recipients %> + <% if recipient.level == 'federal' and not federal_flag %> +

Federal Representatives

+
+ <% federal_flag = true %> + <% elsif recipient.level != 'federal' and not state_flag %> +
+

State Representatives

+
+ <% state_flag = true %> + <% end %> +
+ <%= render recipient, :layout => false %> +
+ <% end %> +
diff --git a/app/views/recipients/lookup.json.jbuilder b/app/views/recipients/lookup.json.jbuilder new file mode 100644 index 0000000..2962137 --- /dev/null +++ b/app/views/recipients/lookup.json.jbuilder @@ -0,0 +1,11 @@ +json.sender do + json.address @sender_address + json.zipcode @sender_zipcode + json.county @sender_county + json.district @sender_district + json.state @sender_state + json.address_accuracy @address_accuracy +end +json.recipients do + json.array! @recipients, partial: "recipients/recipient", as: :recipient +end diff --git a/app/views/recipients/new.html.erb b/app/views/recipients/new.html.erb new file mode 100644 index 0000000..c624ff4 --- /dev/null +++ b/app/views/recipients/new.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, 'New Recipient') %> +<% provide(:button_text, 'Create Recipient') %> +

New Recipient

+ +
+
+ <%= render 'form', recipient: @recipient %> +
+
+ +<%= link_to 'Back', recipients_path %> diff --git a/app/views/recipients/show.html.erb b/app/views/recipients/show.html.erb new file mode 100644 index 0000000..87b44bf --- /dev/null +++ b/app/views/recipients/show.html.erb @@ -0,0 +1,80 @@ +

<%= notice %>

+ +

+ Name: + <%= @recipient.name %> +

+ +

+ Retired: + <%= @recipient.retired %> +

+ +

+ Position: + <%= @recipient.position %> +

+ +

+ Level: + <%= @recipient.level %> +

+ +

+ District: + <%= @recipient.district %> +

+ +

+ State: + <%= @recipient.state %> +

+ +

+ Number fax: + <%= @recipient.number_fax %> +

+ +

+ Number phone: + <%= @recipient.number_phone %> +

+ +

+ Email address: + <%= @recipient.email_address %> +

+ +

+ Contact form: + <%= @recipient.contact_form %> +

+ +

+ Address line 1: + <%= @recipient.address_line_1 %> +

+ +

+ Address line 2: + <%= @recipient.address_line_2 %> +

+ +

+ Address city: + <%= @recipient.address_city %> +

+ +

+ Address state: + <%= @recipient.address_state %> +

+ +

+ Address zipcode: + <%= @recipient.address_zipcode %> +

+ +<%= link_to 'Edit', edit_recipient_path(@recipient) %> | +<%= link_to 'Back', recipients_path %> | +<%= link_to 'New Recipient', new_recipient_path %> diff --git a/app/views/recipients/show.json.jbuilder b/app/views/recipients/show.json.jbuilder new file mode 100644 index 0000000..60f4498 --- /dev/null +++ b/app/views/recipients/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "recipients/recipient", recipient: @recipient diff --git a/app/views/senders/_form.html.erb b/app/views/senders/_form.html.erb new file mode 100644 index 0000000..d0be231 --- /dev/null +++ b/app/views/senders/_form.html.erb @@ -0,0 +1,66 @@ +<%= form_with(model: sender) do |form| %> + <% if sender.errors.any? %> +
+

<%= pluralize(sender.errors.count, "error") %> prohibited this sender from being saved:

+ + +
+ <% end %> + +
+
+
+ <%= form.label :name %> + <%= form.text_field :name, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :email %> + <%= form.text_field :email, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :zipcode %> + <%= form.number_field :zipcode, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :county %> + <%= form.text_field :county, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :district %> + <%= form.text_field :district, class: 'form-control' %> +
+
+ +
+
+ <%= form.label :state %> + <%= form.text_field :state, class: 'form-control' %> +
+
+ +
+ +
+
+
+ <%= form.submit yield(:button_text), class: "btn btn-primary" %> +
+
+
+<% end %> diff --git a/app/views/senders/_sender.json.jbuilder b/app/views/senders/_sender.json.jbuilder new file mode 100644 index 0000000..dd206ad --- /dev/null +++ b/app/views/senders/_sender.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! sender, :id, :name, :zipcode, :county, :district, :state, :user_id, :created_at, :updated_at +json.url sender_url(sender, format: :json) diff --git a/app/views/senders/edit.html.erb b/app/views/senders/edit.html.erb new file mode 100644 index 0000000..2bcc352 --- /dev/null +++ b/app/views/senders/edit.html.erb @@ -0,0 +1,14 @@ +<% provide(:title, 'Edit Letter') %> +<% provide(:button_text, 'Save changes') %> + +

Editing Sender

+ +
+
+ <%= render 'form', sender: @sender %> +
+
+ + +<%= link_to 'Show', @sender %> | +<%= link_to 'Back', senders_path %> diff --git a/app/views/senders/index.html.erb b/app/views/senders/index.html.erb new file mode 100644 index 0000000..01abb67 --- /dev/null +++ b/app/views/senders/index.html.erb @@ -0,0 +1,37 @@ +

<%= notice %>

+ +

Senders

+ + + + + + + + + + + + + + + + <% @senders.each do |sender| %> + + + + + + + + + + + + <% end %> + +
NameZipcodeCountyDistrictStateUser
<%= sender.name %><%= sender.zipcode %><%= sender.county %><%= sender.district %><%= sender.state %><%= sender.user_id %><%= link_to 'Show', sender %><%= link_to 'Edit', edit_sender_path(sender) %><%= link_to 'Destroy', sender, method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to 'New Sender', new_sender_path %> diff --git a/app/views/senders/index.json.jbuilder b/app/views/senders/index.json.jbuilder new file mode 100644 index 0000000..2137d86 --- /dev/null +++ b/app/views/senders/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @senders, partial: "senders/sender", as: :sender diff --git a/app/views/senders/new.html.erb b/app/views/senders/new.html.erb new file mode 100644 index 0000000..bcb547f --- /dev/null +++ b/app/views/senders/new.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, 'Edit Letter') %> +<% provide(:button_text, 'Create Sender') %> +

New Sender

+ +
+
+ <%= render 'form', sender: @sender %> +
+
+ +<%= link_to 'Back', senders_path %> diff --git a/app/views/senders/show.html.erb b/app/views/senders/show.html.erb new file mode 100644 index 0000000..0e6800a --- /dev/null +++ b/app/views/senders/show.html.erb @@ -0,0 +1,34 @@ +

<%= notice %>

+ +

+ Name: + <%= @sender.name %> +

+ +

+ Zipcode: + <%= @sender.zipcode %> +

+ +

+ County: + <%= @sender.county %> +

+ +

+ District: + <%= @sender.district %> +

+ +

+ State: + <%= @sender.state %> +

+ +

+ User: + <%= @sender.user_id %> +

+ +<%= link_to 'Edit', edit_sender_path(@sender) %> | +<%= link_to 'Back', senders_path %> diff --git a/app/views/senders/show.json.jbuilder b/app/views/senders/show.json.jbuilder new file mode 100644 index 0000000..9bff71c --- /dev/null +++ b/app/views/senders/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "senders/sender", sender: @sender diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000..98a300c --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,25 @@ +<% provide(:title, "Log in") %> +

Log in

+ +
+
+ <%= form_with(url: login_path, scope: :session, local: true) do |f| %> + + <%= f.label :email %> + <%= f.email_field :email, class: 'form-control' %> + + + <%= f.password_field :password, class: 'form-control' %> + +
+ <%= f.label :remember_me, class: "checkbox inline" do %> + <%= f.check_box :remember_me %> + Remember me + <% end %> + + <%= f.submit "Log in", class: "btn btn-primary" %> + <% end %> + +

New user? <%= link_to "Sign up now!", signup_path %>

+
+
diff --git a/app/views/shared/_error_messages.html.erb b/app/views/shared/_error_messages.html.erb new file mode 100644 index 0000000..f80053e --- /dev/null +++ b/app/views/shared/_error_messages.html.erb @@ -0,0 +1,12 @@ +<% if @user.errors.any? %> +
+
+ The form contains <%= pluralize(@user.errors.count, "error") %>. +
+ +
+<% end %> diff --git a/app/views/static_pages/about.html.erb b/app/views/static_pages/about.html.erb new file mode 100644 index 0000000..0c40015 --- /dev/null +++ b/app/views/static_pages/about.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, "About") %> + +
+
+

About

+
+
+

+ Contacting your local, state, and federal representative is far harder than it should be. Do you even know your representative? If you know your representatives, when was the last time you called or wrote? +

+

+ Representatives have two objectives: +

+

+

+ What is often overlooked is how difficult it is for representatives to know how their constituents feel about given legislation. The reality is there are always many policies and bills at the local, state and federal being simultaneously discussed. It's impossible for our representatives to survey their district / region on every topic. Worse, your representatives often engage over social media, often censored today. +

+

+ We make it easy to engage your representatives. In 30 seconds you can share your opinions with your representative. You can send a letter via post or fax (for a fee) or we provide their contact information, enabling you to call them yourself (for free). Those engaging with their representatives are far more likely to be donors and advocates, so reaching out will have a larger impact. +

+

+ Why a letter or fax? A letter is far more effective than email or contact form. Letters cannot be censored, sent to spam, and there's a physical reminder of your opinions. +

+
+
diff --git a/app/views/static_pages/contact.html.erb b/app/views/static_pages/contact.html.erb new file mode 100644 index 0000000..6441665 --- /dev/null +++ b/app/views/static_pages/contact.html.erb @@ -0,0 +1,29 @@ +<% provide(:title, "Contact") %> + +
+
+

+

Contact the Team

+
+

+

+

+ support [at sign] vocalvoters.com +
+





+

+
+
+
+

Why contact us?

+
+

+

+

+
+
diff --git a/app/views/static_pages/create_payment_intent.json.jbuilder b/app/views/static_pages/create_payment_intent.json.jbuilder new file mode 100644 index 0000000..9cc6054 --- /dev/null +++ b/app/views/static_pages/create_payment_intent.json.jbuilder @@ -0,0 +1 @@ +json.clientSecret @payment_info diff --git a/app/views/static_pages/help.html.erb b/app/views/static_pages/help.html.erb new file mode 100644 index 0000000..b9ee8c0 --- /dev/null +++ b/app/views/static_pages/help.html.erb @@ -0,0 +1,3 @@ +<% provide(:title, "Help") %> +

Help

+

This is a help page.

diff --git a/app/views/static_pages/home.html.erb b/app/views/static_pages/home.html.erb new file mode 100644 index 0000000..fa44a84 --- /dev/null +++ b/app/views/static_pages/home.html.erb @@ -0,0 +1,235 @@ +<% provide(:title, "Home") %> +
+

+ <%= image_tag 'vocalvoters_logos/vocalvoters-beta.png', + style: 'width:50%', class: "spashlogo" %> +

+

+ Share Your Opinion With Your Representatives
+ Send a Letter or Fax in 30 Seconds or Less +

+
+
+
+ <%= form_with url: govlookup_path, method: :get do |form| %> +
+
+
+ <%= form.text_field :name, + placeholder: 'Your Full Name', + class: 'form-control' %> +
+
+
+
+ <%= form.text_field :email, + placeholder: '@Email', + class: 'form-control' %> +
+
+
+
+ <%= form.text_field :address, + placeholder: 'Address: 123 Street Address, ST 11111', + style: 'margin-bottom:1px', + class: 'form-control' %> +
+
+
+ <% end %> +
+
+
+ <%= button_tag 'Contact Legislators', + id: 'legislator_button', + style: 'margin-top:1em;', + data: { disable_with: "Please wait..." }, + class: "btn btn-primary" %> +
+
+
+
+
+ +
+
+
+
+
+ + +
+ +
+ +
+ +
+ + + + +
+ +
+ + +<%= javascript_pack_tag 'landing_page' %> diff --git a/app/views/user_mailer/account_activation.html.erb b/app/views/user_mailer/account_activation.html.erb new file mode 100644 index 0000000..0f0b3b0 --- /dev/null +++ b/app/views/user_mailer/account_activation.html.erb @@ -0,0 +1,12 @@ +

VocalVoters

+ +

+ Howdy <%= @user.name %>, +

+ +

+ Welcome to the VocalVoters! Click on the link below to activate your account: +

+ +<%= link_to "Activate", edit_account_activation_url(@user.activation_token, + email: @user.email) %> diff --git a/app/views/user_mailer/account_activation.text.erb b/app/views/user_mailer/account_activation.text.erb new file mode 100644 index 0000000..dc76082 --- /dev/null +++ b/app/views/user_mailer/account_activation.text.erb @@ -0,0 +1,8 @@ +Howdy <%= @user.name %>, + +Welcome to the VocalVoters! Click on the link below to activate your account: + + +<%= link_to "Activate", edit_account_activation_url(@user.activation_token, + email: @user.email) %> + diff --git a/app/views/user_mailer/password_reset.html.erb b/app/views/user_mailer/password_reset.html.erb new file mode 100644 index 0000000..87f6d23 --- /dev/null +++ b/app/views/user_mailer/password_reset.html.erb @@ -0,0 +1,21 @@ +

Password reset

+ +

+ Howdy <%= @user.name %>, +

+ +

+ To reset your password click the link below: +

+ +<%= link_to "Reset password", edit_password_reset_url(@user.reset_token, + email: @user.email) %> + +

+ This link will expire in two hours. +

+ +

+ If you did not request your password to be reset, please ignore this email and + your password will stay as it is. +

diff --git a/app/views/user_mailer/password_reset.text.erb b/app/views/user_mailer/password_reset.text.erb new file mode 100644 index 0000000..08ac1bc --- /dev/null +++ b/app/views/user_mailer/password_reset.text.erb @@ -0,0 +1,10 @@ +Howdy <%= @user.name %>, + +To reset your password click the link below: + +<%= edit_password_reset_url(@user.reset_token, email: @user.email) %> + +This link will expire in two hours. + +If you did not request your password to be reset, please ignore this email and +your password will stay as it is. \ No newline at end of file diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb new file mode 100644 index 0000000..03dbdce --- /dev/null +++ b/app/views/users/_form.html.erb @@ -0,0 +1,17 @@ +<%= form_with(model: @user, local: true) do |f| %> + <%= render 'shared/error_messages', object: @user %> + + <%= f.label :name %> + <%= f.text_field :name, class: 'form-control' %> + + <%= f.label :email %> + <%= f.email_field :email, class: 'form-control' %> + + <%= f.label :password %> + <%= f.password_field :password, class: 'form-control' %> + + <%= f.label :password_confirmation %> + <%= f.password_field :password_confirmation, class: 'form-control' %> + + <%= f.submit yield(:button_text), class: "btn btn-primary" %> +<% end %> diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb new file mode 100644 index 0000000..e44f493 --- /dev/null +++ b/app/views/users/_user.html.erb @@ -0,0 +1,7 @@ +
  • + <%= link_to user.name, user %> + <% if current_user.admin? && !current_user?(user) %> + | <%= link_to "delete", user, method: :delete, + data: { confirm: "You sure?" } %> + <% end %> +
  • diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb new file mode 100644 index 0000000..e564b0f --- /dev/null +++ b/app/views/users/edit.html.erb @@ -0,0 +1,8 @@ +<% provide(:title, 'Edit user') %> +<% provide(:button_text, 'Save changes') %> +

    Update your profile

    +
    +
    + <%= render 'form' %> +
    +
    diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb new file mode 100644 index 0000000..5b6dbae --- /dev/null +++ b/app/views/users/index.html.erb @@ -0,0 +1,10 @@ +<% provide(:title, 'All users') %> +

    All users

    + +<%= will_paginate %> + + + +<%= will_paginate %> diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb new file mode 100644 index 0000000..5ec12ff --- /dev/null +++ b/app/views/users/new.html.erb @@ -0,0 +1,8 @@ +<% provide(:title, 'Sign up') %> +<% provide(:button_text, 'Create my account') %> +

    Sign up

    +
    +
    + <%= render 'form' %> +
    +
    diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb new file mode 100644 index 0000000..875e620 --- /dev/null +++ b/app/views/users/show.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, @user.name) %> + +
    + +
    diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..4df1949 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,70 @@ +module.exports = function(api) { + var validEnv = ['development', 'test', 'production'] + var currentEnv = api.env() + var isDevelopmentEnv = api.env('development') + var isProductionEnv = api.env('production') + var isTestEnv = api.env('test') + + if (!validEnv.includes(currentEnv)) { + throw new Error( + 'Please specify a valid `NODE_ENV` or ' + + '`BABEL_ENV` environment variables. Valid values are "development", ' + + '"test", and "production". Instead, received: ' + + JSON.stringify(currentEnv) + + '.' + ) + } + + return { + presets: [ + isTestEnv && [ + '@babel/preset-env', + { + targets: { + node: 'current' + } + } + ], + (isProductionEnv || isDevelopmentEnv) && [ + '@babel/preset-env', + { + forceAllTransforms: true, + useBuiltIns: 'entry', + corejs: 3, + modules: false, + exclude: ['transform-typeof-symbol'] + } + ] + ].filter(Boolean), + plugins: [ + 'babel-plugin-macros', + '@babel/plugin-syntax-dynamic-import', + isTestEnv && 'babel-plugin-dynamic-import-node', + '@babel/plugin-transform-destructuring', + [ + '@babel/plugin-proposal-class-properties', + { + loose: true + } + ], + [ + '@babel/plugin-proposal-object-rest-spread', + { + useBuiltIns: true + } + ], + [ + '@babel/plugin-transform-runtime', + { + helpers: false + } + ], + [ + '@babel/plugin-transform-regenerator', + { + async: false + } + ] + ].filter(Boolean) + } +} diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..a71368e --- /dev/null +++ b/bin/bundle @@ -0,0 +1,114 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../../Gemfile", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= + env_var_version || cli_arg_version || + lockfile_version + end + + def bundler_requirement + return "#{Gem::Requirement.default}.a" unless bundler_version + + bundler_gem_version = Gem::Version.new(bundler_version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + 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}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..21d3e02 --- /dev/null +++ b/bin/rails @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +load File.expand_path("spring", __dir__) +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..7327f47 --- /dev/null +++ b/bin/rake @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +load File.expand_path("spring", __dir__) +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..90700ac --- /dev/null +++ b/bin/setup @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies + system! 'bin/yarn' + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:prepare' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/spring b/bin/spring new file mode 100755 index 0000000..9675cce --- /dev/null +++ b/bin/spring @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) + gem "bundler" + require "bundler" + + # Load Spring without loading other gems in the Gemfile, for speed. + Bundler.locked_gems.specs.find { |spec| spec.name == "spring" }&.tap do |spring| + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem "spring", spring.version + require "spring/binstub" + rescue Gem::LoadError + # Ignore when Spring is not installed. + end +end diff --git a/bin/webpack b/bin/webpack new file mode 100755 index 0000000..1031168 --- /dev/null +++ b/bin/webpack @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "bundler/setup" + +require "webpacker" +require "webpacker/webpack_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::WebpackRunner.run(ARGV) +end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server new file mode 100755 index 0000000..dd96627 --- /dev/null +++ b/bin/webpack-dev-server @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "bundler/setup" + +require "webpacker" +require "webpacker/dev_server_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::DevServerRunner.run(ARGV) +end diff --git a/bin/yarn b/bin/yarn new file mode 100755 index 0000000..4700a9e --- /dev/null +++ b/bin/yarn @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + yarn = ENV["PATH"].split(File::PATH_SEPARATOR). + select { |dir| File.expand_path(dir) != __dir__ }. + product(["yarn", "yarn.exe"]). + map { |dir, file| File.expand_path(file, dir) }. + find { |file| File.executable?(file) } + + if yarn + exec yarn, *ARGV + else + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..4a3c09a --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..bdd8167 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,22 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module VocalVoters + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 6.1 + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..3cda23b --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..c98a192 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: vocalvoters_production diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000..cf4a957 --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +h/cbVyiKC+6BLQL3+y6TIRBtlRvEe6ERivyr8uqQU+AfuAGUDK6T0CWOg6RVFlLu9q1nxUnikvXd5/KgMnxAVkFrFlQg8ZeIW50xsfWZZpQHPVkArE9PZGf+nn1sXVflKaQ/egk39H+5S+QX5htLa/GpJ6mf4FUVJzI4FkTlmtmmovhy2WYXH4zXvfKlKl41xtEhM7WmF/ICUyWwh4L0Fk4RrQryJEMJtL1nQwiqjS++uH4BJNgTVEKRyFSKUaNcZt7tK3L/u5udF1h+RGy2fqQc0UH1AosN+XMADXctDhdiRxBs+/WoFwwCk1RHl3o/FUGhrzrzXwfqIZ98YkvisD/cLHqdWvnPnQ6khotWDvWiGvQLel62NIj6UwG3m4nefoKhefEGxISw7G3kMSvTVqxmzr4BYh5aYT+E--up11m3+H1hGvF+mW--HK1qITEUGPEATsy4N03R4w== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..2b37359 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,30 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: db/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: db/test.sqlite3 + +production: + adapter: postgresql + encoding: unicode + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + database: vocalvoters_production + username: vocalvoters_app + password: <%= ENV['VOCALVOTERS_DATABASE_PASSWORD'] %> + diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..977b5b3 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,92 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Host URL, local host + host = 'localhost:3000' + + # Do not fallback to assets pipeline if a precompiled asset is missed. + # config.assets.compile = false + + + # Use this on the cloud IDE. + # config.action_mailer.default_url_options = { host: host, protocol: 'https' } + + # Use this if developing on localhost. + config.action_mailer.default_url_options = { host: host, protocol: 'http' } + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Allow connections to local server + config.hosts.clear +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..2d0ef4c --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,137 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). + config.log_level = :info + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "faxgov_us_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + config.action_mailer.raise_delivery_errors = true + config.action_mailer.delivery_method = :smtp + host = 'vocalvoters.com' + config.action_mailer.default_url_options = { host: host } + ActionMailer::Base.smtp_settings = { + :address => 'smtp.sendgrid.net', + :port => '587', + :authentication => :plain, + :user_name => 'apikey', + :password => ENV['SENDGRID_API_KEY_VOCALVOTERS'], + :domain => 'heroku.com', + :enable_starttls_auto => true + } + + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Log disallowed deprecations. + config.active_support.disallowed_deprecation = :log + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..6c118ed --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,61 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + config.cache_classes = false + config.action_view.cache_template_loading = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + config.action_mailer.default_url_options = { host: 'example.com' } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true +end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000..89d2efa --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000..4b828e8 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path +# Add Yarn node_modules folder to the asset load path. +Rails.application.config.assets.paths << Rails.root.join('node_modules') + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000..33699c3 --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code +# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". +Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..1c1fd35 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,30 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +#Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https, :http +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :http, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # If you are using webpack-dev-server then specify webpack-dev-server host +# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +#end + +# If you are using UJS then enable automatic nonce generation +#Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } + +# Set the nonce only to specific directives +#Rails.application.config.content_security_policy_nonce_directives = %w(script-src) + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +#Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000..5a6a32d --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..4b34a03 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..ac033bf --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 0000000..00fa42e --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf +Mime::Type.register "application/pdf", :pdf diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb new file mode 100644 index 0000000..00f64d7 --- /dev/null +++ b/config/initializers/permissions_policy.rb @@ -0,0 +1,11 @@ +# Define an application-wide HTTP permissions policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.permissions_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure.example.com" +# end diff --git a/config/initializers/wicked_pdf.rb b/config/initializers/wicked_pdf.rb new file mode 100644 index 0000000..a84ba8b --- /dev/null +++ b/config/initializers/wicked_pdf.rb @@ -0,0 +1,36 @@ +# WickedPDF Global Configuration +# +# Use this to set up shared configuration options for your entire application. +# Any of the configuration options shown here can also be applied to single +# models by passing arguments to the `render :pdf` call. +# +# To learn more, check out the README: +# +# https://github.com/mileszs/wicked_pdf/blob/master/README.md + +WickedPdf.config = { + # Path to the wkhtmltopdf executable: This usually isn't needed if using + # one of the wkhtmltopdf-binary family of gems. + # exe_path: '/usr/local/bin/wkhtmltopdf', + # or + # exe_path: Gem.bin_path('wkhtmltopdf-binary', 'wkhtmltopdf') + + # Layout file to be used for all PDFs + # (but can be overridden in `render :pdf` calls) + # layout: 'pdf.html', + + # Using wkhtmltopdf without an X server can be achieved by enabling the + # 'use_xvfb' flag. This will wrap all wkhtmltopdf commands around the + # 'xvfb-run' command, in order to simulate an X server. + # + # use_xvfb: true, +} + +WickedPdf.config ||= {} +WickedPdf.config.merge!({ + layout: "pdf.html.erb", + orientation: "Portrait", + page_size: "A4", + zoom: 1, + dpi: 300 +}) diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000..bbfc396 --- /dev/null +++ b/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..cf9b342 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..91606ec --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,43 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +# +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { ENV['RACK_ENV'] || "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..9854792 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,38 @@ +Rails.application.routes.draw do + + get 'password_resets/new' + get 'password_resets/edit' + root 'static_pages#home' + + get '/help', to: 'static_pages#help' + get '/about', to: 'static_pages#about' + get '/contact', to: 'static_pages#contact' + + get '/signup', to: 'users#new' + + get '/login', to: 'sessions#new' + post '/login', to: 'sessions#create' + delete '/logout', to: 'sessions#destroy' + + post '/payment_system/vZ692SnI2btP1aybeJDA4TPTmEslSjOD', to: 'payments#webhook' + + resources :users + resources :account_activations, only: [:edit] + resources :password_resets, only: [:new, :create, :edit, :update] + + resources :emails + resources :posts + resources :faxes + + resources :recipients + resources :senders + resources :letters + + post 'send_communication', to: 'send_communication#send_communication' + + get 'govlookup', to: 'recipients#lookup' + get 'find_policy', to: 'letters#find_policy' + + post 'create-payment-intent', to: 'static_pages#create_payment_intent' + get 'create-payment-intent', to: 'static_pages#create_payment_intent' +end diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 0000000..db5bf13 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +Spring.watch( + ".ruby-version", + ".rbenv-vars", + "tmp/restart.txt", + "tmp/caching-dev.txt" +) diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000..d32f76e --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket + +# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/config/webpack/development.js b/config/webpack/development.js new file mode 100644 index 0000000..c5edff9 --- /dev/null +++ b/config/webpack/development.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpack/environment.js b/config/webpack/environment.js new file mode 100644 index 0000000..09ec75f --- /dev/null +++ b/config/webpack/environment.js @@ -0,0 +1,11 @@ +const { environment } = require('@rails/webpacker') + +const webpack = require('webpack') +environment.plugins.prepend('Provide', + new webpack.ProvidePlugin({ + $: 'jquery/src/jquery', + jQuery: 'jquery/src/jquery' + }) +) + +module.exports = environment diff --git a/config/webpack/production.js b/config/webpack/production.js new file mode 100644 index 0000000..be0f53a --- /dev/null +++ b/config/webpack/production.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'production' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpack/test.js b/config/webpack/test.js new file mode 100644 index 0000000..c5edff9 --- /dev/null +++ b/config/webpack/test.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpacker.yml b/config/webpacker.yml new file mode 100644 index 0000000..4b4aa3d --- /dev/null +++ b/config/webpacker.yml @@ -0,0 +1,92 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_root_path: public + public_output_path: packs + cache_path: tmp/cache/webpacker + webpack_compile_output: true + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + additional_paths: ['app/javascript/images', 'app/assets/images', 'vendor/assets'] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + # Extract and emit a css file + extract_css: false + + static_assets_extensions: + - .jpg + - .jpeg + - .png + - .gif + - .tiff + - .ico + - .svg + - .eot + - .otf + - .ttf + - .woff + - .woff2 + + extensions: + - .mjs + - .js + - .sass + - .scss + - .css + - .module.sass + - .module.scss + - .module.css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + public: localhost:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + pretty: false + headers: + 'Access-Control-Allow-Origin': '*' + watch_options: + ignored: '**/node_modules/**' + + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Extract and emit a css file + extract_css: true + + # Cache manifest.json for performance + cache_manifest: true diff --git a/db/migrate/20210603041537_create_users.rb b/db/migrate/20210603041537_create_users.rb new file mode 100644 index 0000000..e0c8121 --- /dev/null +++ b/db/migrate/20210603041537_create_users.rb @@ -0,0 +1,10 @@ +class CreateUsers < ActiveRecord::Migration[6.1] + def change + create_table :users do |t| + t.string :name + t.string :email + + t.timestamps + end + end +end diff --git a/db/migrate/20210603042417_add_index_to_users_email.rb b/db/migrate/20210603042417_add_index_to_users_email.rb new file mode 100644 index 0000000..936844c --- /dev/null +++ b/db/migrate/20210603042417_add_index_to_users_email.rb @@ -0,0 +1,5 @@ +class AddIndexToUsersEmail < ActiveRecord::Migration[6.1] + def change + add_index :users, :email, unique: true + end +end diff --git a/db/migrate/20210603042940_add_password_digest_to_users.rb b/db/migrate/20210603042940_add_password_digest_to_users.rb new file mode 100644 index 0000000..f7e2a41 --- /dev/null +++ b/db/migrate/20210603042940_add_password_digest_to_users.rb @@ -0,0 +1,5 @@ +class AddPasswordDigestToUsers < ActiveRecord::Migration[6.1] + def change + add_column :users, :password_digest, :string + end +end diff --git a/db/migrate/20210603185100_add_remember_digest_to_users.rb b/db/migrate/20210603185100_add_remember_digest_to_users.rb new file mode 100644 index 0000000..ba2b6de --- /dev/null +++ b/db/migrate/20210603185100_add_remember_digest_to_users.rb @@ -0,0 +1,5 @@ +class AddRememberDigestToUsers < ActiveRecord::Migration[6.1] + def change + add_column :users, :remember_digest, :string + end +end diff --git a/db/migrate/20210604190943_add_admin_to_users.rb b/db/migrate/20210604190943_add_admin_to_users.rb new file mode 100644 index 0000000..cd55f45 --- /dev/null +++ b/db/migrate/20210604190943_add_admin_to_users.rb @@ -0,0 +1,5 @@ +class AddAdminToUsers < ActiveRecord::Migration[6.1] + def change + add_column :users, :admin, :boolean, default: false + end +end diff --git a/db/migrate/20210604195135_add_activation_to_users.rb b/db/migrate/20210604195135_add_activation_to_users.rb new file mode 100644 index 0000000..7d7b1a6 --- /dev/null +++ b/db/migrate/20210604195135_add_activation_to_users.rb @@ -0,0 +1,7 @@ +class AddActivationToUsers < ActiveRecord::Migration[6.1] + def change + add_column :users, :activation_digest, :string + add_column :users, :activated, :boolean, default: false + add_column :users, :activated_at, :datetime + end +end diff --git a/db/migrate/20210605041256_add_reset_to_users.rb b/db/migrate/20210605041256_add_reset_to_users.rb new file mode 100644 index 0000000..dd79fee --- /dev/null +++ b/db/migrate/20210605041256_add_reset_to_users.rb @@ -0,0 +1,6 @@ +class AddResetToUsers < ActiveRecord::Migration[6.1] + def change + add_column :users, :reset_digest, :string + add_column :users, :reset_sent_at, :datetime + end +end diff --git a/db/migrate/20210609044142_create_pay_subscriptions.pay.rb b/db/migrate/20210609044142_create_pay_subscriptions.pay.rb new file mode 100644 index 0000000..2f03737 --- /dev/null +++ b/db/migrate/20210609044142_create_pay_subscriptions.pay.rb @@ -0,0 +1,18 @@ +# This migration comes from pay (originally 20170205020145) +class CreatePaySubscriptions < ActiveRecord::Migration[4.2] + def change + create_table :pay_subscriptions do |t| + # Some Billable objects use string as ID, add `type: :string` if needed + t.references :owner, polymorphic: true + t.string :name, null: false + t.string :processor, null: false + t.string :processor_id, null: false + t.string :processor_plan, null: false + t.integer :quantity, default: 1, null: false + t.datetime :trial_ends_at + t.datetime :ends_at + + t.timestamps + end + end +end diff --git a/db/migrate/20210609044143_create_pay_charges.pay.rb b/db/migrate/20210609044143_create_pay_charges.pay.rb new file mode 100644 index 0000000..9e764b6 --- /dev/null +++ b/db/migrate/20210609044143_create_pay_charges.pay.rb @@ -0,0 +1,19 @@ +# This migration comes from pay (originally 20170727235816) +class CreatePayCharges < ActiveRecord::Migration[4.2] + def change + create_table :pay_charges do |t| + # Some Billable objects use string as ID, add `type: :string` if needed + t.references :owner, polymorphic: true + t.string :processor, null: false + t.string :processor_id, null: false + t.integer :amount, null: false + t.integer :amount_refunded + t.string :card_type + t.string :card_last4 + t.string :card_exp_month + t.string :card_exp_year + + t.timestamps + end + end +end diff --git a/db/migrate/20210609044144_add_status_to_pay_subscriptions.pay.rb b/db/migrate/20210609044144_add_status_to_pay_subscriptions.pay.rb new file mode 100644 index 0000000..4286f0a --- /dev/null +++ b/db/migrate/20210609044144_add_status_to_pay_subscriptions.pay.rb @@ -0,0 +1,15 @@ +# This migration comes from pay (originally 20190816015720) +class AddStatusToPaySubscriptions < ActiveRecord::Migration[4.2] + def self.up + add_column :pay_subscriptions, :status, :string + + # Any existing subscriptions should be marked as 'active' + # This won't actually make them active if their ends_at column is expired + Pay::Subscription.reset_column_information + Pay::Subscription.update_all(status: :active) + end + + def self.down + remove_column :pay_subscriptions, :status + end +end diff --git a/db/migrate/20210609044145_add_data_to_pay_models.pay.rb b/db/migrate/20210609044145_add_data_to_pay_models.pay.rb new file mode 100644 index 0000000..a61a9f1 --- /dev/null +++ b/db/migrate/20210609044145_add_data_to_pay_models.pay.rb @@ -0,0 +1,16 @@ +# This migration comes from pay (originally 20200603134434) +class AddDataToPayModels < ActiveRecord::Migration[4.2] + def change + add_column :pay_subscriptions, :data, data_column_type + add_column :pay_charges, :data, data_column_type + end + + def data_column_type + case Pay::Adapter.current_adapter + when "postgresql" + :jsonb + else + :json + end + end +end diff --git a/db/migrate/20210609044146_add_data_to_pay_billable.pay.rb b/db/migrate/20210609044146_add_data_to_pay_billable.pay.rb new file mode 100644 index 0000000..85ded1e --- /dev/null +++ b/db/migrate/20210609044146_add_data_to_pay_billable.pay.rb @@ -0,0 +1,20 @@ +# This migration comes from pay (originally 20210309004259) +class AddDataToPayBillable < ActiveRecord::Migration[4.2] + def change + # Load all the billable models + Rails.application.eager_load! + + Pay.billable_models.each do |model| + add_column model.table_name, :pay_data, data_column_type + end + end + + def data_column_type + case Pay::Adapter.current_adapter + when "postgresql" + :jsonb + else + :json + end + end +end diff --git a/db/migrate/20210609044147_add_currency_to_pay_charges.pay.rb b/db/migrate/20210609044147_add_currency_to_pay_charges.pay.rb new file mode 100644 index 0000000..aca8b2a --- /dev/null +++ b/db/migrate/20210609044147_add_currency_to_pay_charges.pay.rb @@ -0,0 +1,6 @@ +# This migration comes from pay (originally 20210406215234) +class AddCurrencyToPayCharges < ActiveRecord::Migration[4.2] + def change + add_column :pay_charges, :currency, :string + end +end diff --git a/db/migrate/20210609044148_add_application_fee_to_pay_models.pay.rb b/db/migrate/20210609044148_add_application_fee_to_pay_models.pay.rb new file mode 100644 index 0000000..92cc74a --- /dev/null +++ b/db/migrate/20210609044148_add_application_fee_to_pay_models.pay.rb @@ -0,0 +1,8 @@ +# This migration comes from pay (originally 20210406215506) +class AddApplicationFeeToPayModels < ActiveRecord::Migration[4.2] + def change + add_column :pay_charges, :application_fee_amount, :integer + add_column :pay_subscriptions, :application_fee_percent, :decimal, precision: 8, scale: 2 + add_column :pay_charges, :pay_subscription_id, :integer + end +end diff --git a/db/migrate/20210610040024_create_letters.rb b/db/migrate/20210610040024_create_letters.rb new file mode 100644 index 0000000..e317fb2 --- /dev/null +++ b/db/migrate/20210610040024_create_letters.rb @@ -0,0 +1,14 @@ +class CreateLetters < ActiveRecord::Migration[6.1] + def change + create_table :letters do |t| + t.string :category + t.string :policy_or_law + t.string :tags + t.float :sentiment + t.text :body + t.references :user, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20210629195032_create_senders.rb b/db/migrate/20210629195032_create_senders.rb new file mode 100644 index 0000000..ae9778c --- /dev/null +++ b/db/migrate/20210629195032_create_senders.rb @@ -0,0 +1,14 @@ +class CreateSenders < ActiveRecord::Migration[6.1] + def change + create_table :senders do |t| + t.string :name + t.integer :zipcode + t.string :county + t.string :district + t.string :state + t.references :user, null: true, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20210629200322_create_recipients.rb b/db/migrate/20210629200322_create_recipients.rb new file mode 100644 index 0000000..91425a0 --- /dev/null +++ b/db/migrate/20210629200322_create_recipients.rb @@ -0,0 +1,21 @@ +class CreateRecipients < ActiveRecord::Migration[6.1] + def change + create_table :recipients do |t| + t.string :name + t.string :position + t.string :level + t.string :district + t.string :state + t.integer :number_fax + t.integer :number_phone + t.string :email_address + t.string :address_line_1 + t.string :address_line_2 + t.string :address_city + t.string :address_state + t.string :address_zipcode + + t.timestamps + end + end +end diff --git a/db/migrate/20210629202832_create_faxes.rb b/db/migrate/20210629202832_create_faxes.rb new file mode 100644 index 0000000..7b568f6 --- /dev/null +++ b/db/migrate/20210629202832_create_faxes.rb @@ -0,0 +1,14 @@ +class CreateFaxes < ActiveRecord::Migration[6.1] + def change + create_table :faxes do |t| + t.integer :number_fax + t.string :payment, null: false + t.boolean :success, default: true + t.references :sender, null: false, foreign_key: true + t.references :recipient, null: false, foreign_key: true + t.references :letter, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20210629203005_create_posts.rb b/db/migrate/20210629203005_create_posts.rb new file mode 100644 index 0000000..9cfbaf1 --- /dev/null +++ b/db/migrate/20210629203005_create_posts.rb @@ -0,0 +1,19 @@ +class CreatePosts < ActiveRecord::Migration[6.1] + def change + create_table :posts do |t| + t.string :address_line_1 + t.string :address_line_2 + t.string :address_city + t.string :address_state + t.string :address_zipcode + t.string :payment, null: false + t.boolean :priority, default: false + t.boolean :success, default: true + t.references :sender, null: false, foreign_key: true + t.references :recipient, null: false, foreign_key: true + t.references :letter, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20210629203017_create_emails.rb b/db/migrate/20210629203017_create_emails.rb new file mode 100644 index 0000000..2795da8 --- /dev/null +++ b/db/migrate/20210629203017_create_emails.rb @@ -0,0 +1,14 @@ +class CreateEmails < ActiveRecord::Migration[6.1] + def change + create_table :emails do |t| + t.string :email_address + t.boolean :success, default: true + t.references :sender, null: false, foreign_key: true + t.references :recipient, null: false, foreign_key: true + t.references :letter, null: false, foreign_key: true + t.string :payment, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20210630181252_add_contact_form_to_recipients.rb b/db/migrate/20210630181252_add_contact_form_to_recipients.rb new file mode 100644 index 0000000..924bb1c --- /dev/null +++ b/db/migrate/20210630181252_add_contact_form_to_recipients.rb @@ -0,0 +1,5 @@ +class AddContactFormToRecipients < ActiveRecord::Migration[6.1] + def change + add_column :recipients, :contact_form, :string + end +end diff --git a/db/migrate/20210630184151_add_email_to_senders.rb b/db/migrate/20210630184151_add_email_to_senders.rb new file mode 100644 index 0000000..e50c5ab --- /dev/null +++ b/db/migrate/20210630184151_add_email_to_senders.rb @@ -0,0 +1,5 @@ +class AddEmailToSenders < ActiveRecord::Migration[6.1] + def change + add_column :senders, :email, :string + end +end diff --git a/db/migrate/20210910044626_change_number_to_bigint_on_recipients.rb b/db/migrate/20210910044626_change_number_to_bigint_on_recipients.rb new file mode 100644 index 0000000..202484e --- /dev/null +++ b/db/migrate/20210910044626_change_number_to_bigint_on_recipients.rb @@ -0,0 +1,6 @@ +class ChangeNumberToBigintOnRecipients < ActiveRecord::Migration[6.1] + def change + change_column :recipients, :number_fax, :bigint + change_column :recipients, :number_phone, :bigint + end +end diff --git a/db/migrate/20210915042754_add_retirement_to_recipients.rb b/db/migrate/20210915042754_add_retirement_to_recipients.rb new file mode 100644 index 0000000..78be205 --- /dev/null +++ b/db/migrate/20210915042754_add_retirement_to_recipients.rb @@ -0,0 +1,5 @@ +class AddRetirementToRecipients < ActiveRecord::Migration[6.1] + def change + add_column :recipients, :retired, :boolean, :default => false + end +end diff --git a/db/migrate/20211021051914_add_target_level_and_target_state_to_letters.rb b/db/migrate/20211021051914_add_target_level_and_target_state_to_letters.rb new file mode 100644 index 0000000..829dc8a --- /dev/null +++ b/db/migrate/20211021051914_add_target_level_and_target_state_to_letters.rb @@ -0,0 +1,6 @@ +class AddTargetLevelAndTargetStateToLetters < ActiveRecord::Migration[6.1] + def change + add_column :letters, :target_level, :string, default: "all" + add_column :letters, :target_state, :string, default: "all" + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..4a7ca90 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,172 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2021_10_21_051914) do + + create_table "emails", force: :cascade do |t| + t.string "email_address" + t.boolean "success", default: true + t.integer "sender_id", null: false + t.integer "recipient_id", null: false + t.integer "letter_id", null: false + t.string "payment", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["letter_id"], name: "index_emails_on_letter_id" + t.index ["recipient_id"], name: "index_emails_on_recipient_id" + t.index ["sender_id"], name: "index_emails_on_sender_id" + end + + create_table "faxes", force: :cascade do |t| + t.integer "number_fax" + t.string "payment", null: false + t.boolean "success", default: true + t.integer "sender_id", null: false + t.integer "recipient_id", null: false + t.integer "letter_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["letter_id"], name: "index_faxes_on_letter_id" + t.index ["recipient_id"], name: "index_faxes_on_recipient_id" + t.index ["sender_id"], name: "index_faxes_on_sender_id" + end + + create_table "letters", force: :cascade do |t| + t.string "category" + t.string "policy_or_law" + t.string "tags" + t.float "sentiment" + t.text "body" + t.integer "user_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "target_level", default: "all" + t.string "target_state", default: "all" + t.index ["user_id"], name: "index_letters_on_user_id" + end + + create_table "pay_charges", force: :cascade do |t| + t.string "owner_type" + t.integer "owner_id" + t.string "processor", null: false + t.string "processor_id", null: false + t.integer "amount", null: false + t.integer "amount_refunded" + t.string "card_type" + t.string "card_last4" + t.string "card_exp_month" + t.string "card_exp_year" + t.datetime "created_at" + t.datetime "updated_at" + t.json "data" + t.string "currency" + t.integer "application_fee_amount" + t.integer "pay_subscription_id" + end + + create_table "pay_subscriptions", force: :cascade do |t| + t.string "owner_type" + t.integer "owner_id" + t.string "name", null: false + t.string "processor", null: false + t.string "processor_id", null: false + t.string "processor_plan", null: false + t.integer "quantity", default: 1, null: false + t.datetime "trial_ends_at" + t.datetime "ends_at" + t.datetime "created_at" + t.datetime "updated_at" + t.string "status" + t.json "data" + t.decimal "application_fee_percent", precision: 8, scale: 2 + end + + create_table "posts", force: :cascade do |t| + t.string "address_line_1" + t.string "address_line_2" + t.string "address_city" + t.string "address_state" + t.string "address_zipcode" + t.string "payment", null: false + t.boolean "priority", default: false + t.boolean "success", default: true + t.integer "sender_id", null: false + t.integer "recipient_id", null: false + t.integer "letter_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["letter_id"], name: "index_posts_on_letter_id" + t.index ["recipient_id"], name: "index_posts_on_recipient_id" + t.index ["sender_id"], name: "index_posts_on_sender_id" + end + + create_table "recipients", force: :cascade do |t| + t.string "name" + t.string "position" + t.string "level" + t.string "district" + t.string "state" + t.integer "number_fax" + t.integer "number_phone" + t.string "email_address" + t.string "address_line_1" + t.string "address_line_2" + t.string "address_city" + t.string "address_state" + t.string "address_zipcode" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "contact_form" + t.boolean "retired", default: false + end + + create_table "senders", force: :cascade do |t| + t.string "name" + t.integer "zipcode" + t.string "county" + t.string "district" + t.string "state" + t.integer "user_id" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "email" + t.index ["user_id"], name: "index_senders_on_user_id" + end + + create_table "users", force: :cascade do |t| + t.string "name" + t.string "email" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.string "password_digest" + t.string "remember_digest" + t.boolean "admin", default: false + t.string "activation_digest" + t.boolean "activated", default: false + t.datetime "activated_at" + t.string "reset_digest" + t.datetime "reset_sent_at" + t.index ["email"], name: "index_users_on_email", unique: true + end + + add_foreign_key "emails", "letters" + add_foreign_key "emails", "recipients" + add_foreign_key "emails", "senders" + add_foreign_key "faxes", "letters" + add_foreign_key "faxes", "recipients" + add_foreign_key "faxes", "senders" + add_foreign_key "letters", "users" + add_foreign_key "posts", "letters" + add_foreign_key "posts", "recipients" + add_foreign_key "posts", "senders" + add_foreign_key "senders", "users" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..ac2ffe3 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,28 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) +# Create a main sample user. +User.create!(name: "Austin Walters", + email: "austin@example.com", + password: "1password", + password_confirmation: "1password", + admin: true, + activated: true, + activated_at: Time.zone.now) + +# Generate a bunch of additional users. +99.times do |n| + name = Faker::Name.name + email = "example-#{n+1}@railstutorial.org" + password = "password" + User.create!(name: name, + email: email, + password: password, + password_confirmation: password, + activated: true, + activated_at: Time.zone.now) +end diff --git a/lib/assets/.keep b/lib/assets/.keep new file mode 100644 index 0000000..e69de29 diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..de4d826 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "vocalvoters", + "private": true, + "dependencies": { + "@rails/actioncable": "^6.0.0", + "@rails/activestorage": "^6.0.0", + "@rails/ujs": "^6.0.0", + "@rails/webpacker": "5.4.0", + "bootstrap": "3.4.1", + "jquery": "3.5.1", + "turbolinks": "^5.2.0", + "webpack": "^4.46.0", + "webpack-cli": "^3.3.12" + }, + "version": "0.1.0", + "devDependencies": { + "webpack-dev-server": "^3.11.2" + } +} diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..2be3af2 --- /dev/null +++ b/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
    +
    +

    The page you were looking for doesn't exist.

    +

    You may have mistyped the address or the page may have moved.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000..c08eac0 --- /dev/null +++ b/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
    +
    +

    The change you wanted was rejected.

    +

    Maybe you tried to change something you didn't have access to.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..78a030a --- /dev/null +++ b/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
    +
    +

    We're sorry, but something went wrong.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..e69de29 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..e69de29 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000..d19212a --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb new file mode 100644 index 0000000..800405f --- /dev/null +++ b/test/channels/application_cable/connection_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase + # test "connects with cookies" do + # cookies.signed[:user_id] = 42 + # + # connect + # + # assert_equal connection.user_id, "42" + # end +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/controllers/account_activations_controller_test.rb b/test/controllers/account_activations_controller_test.rb new file mode 100644 index 0000000..bcd2199 --- /dev/null +++ b/test/controllers/account_activations_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AccountActivationsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/emails_controller_test.rb b/test/controllers/emails_controller_test.rb new file mode 100644 index 0000000..56e1e53 --- /dev/null +++ b/test/controllers/emails_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class EmailsControllerTest < ActionDispatch::IntegrationTest + setup do + @email = emails(:one) + end + + test "should get index" do + get emails_url + assert_response :success + end + + test "should get new" do + get new_email_url + assert_response :success + end + + test "should create email" do + assert_difference('Email.count') do + post emails_url, params: { email: { email_address: @email.email_address, letter_id: @email.letter_id, payment_id: @email.payment_id, recipient_id: @email.recipient_id, sender_id: @email.sender_id } } + end + + assert_redirected_to email_url(Email.last) + end + + test "should show email" do + get email_url(@email) + assert_response :success + end + + test "should get edit" do + get edit_email_url(@email) + assert_response :success + end + + test "should update email" do + patch email_url(@email), params: { email: { email_address: @email.email_address, letter_id: @email.letter_id, payment_id: @email.payment_id, recipient_id: @email.recipient_id, sender_id: @email.sender_id } } + assert_redirected_to email_url(@email) + end + + test "should destroy email" do + assert_difference('Email.count', -1) do + delete email_url(@email) + end + + assert_redirected_to emails_url + end +end diff --git a/test/controllers/faxes_controller_test.rb b/test/controllers/faxes_controller_test.rb new file mode 100644 index 0000000..a2e35d2 --- /dev/null +++ b/test/controllers/faxes_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class FaxesControllerTest < ActionDispatch::IntegrationTest + setup do + @fax = faxes(:one) + end + + test "should get index" do + get faxes_url + assert_response :success + end + + test "should get new" do + get new_fax_url + assert_response :success + end + + test "should create fax" do + assert_difference('Fax.count') do + post faxes_url, params: { fax: { letter_id: @fax.letter_id, number_fax: @fax.number_fax, payment_id: @fax.payment_id, recipient_id: @fax.recipient_id, sender_id: @fax.sender_id } } + end + + assert_redirected_to fax_url(Fax.last) + end + + test "should show fax" do + get fax_url(@fax) + assert_response :success + end + + test "should get edit" do + get edit_fax_url(@fax) + assert_response :success + end + + test "should update fax" do + patch fax_url(@fax), params: { fax: { letter_id: @fax.letter_id, number_fax: @fax.number_fax, payment_id: @fax.payment_id, recipient_id: @fax.recipient_id, sender_id: @fax.sender_id } } + assert_redirected_to fax_url(@fax) + end + + test "should destroy fax" do + assert_difference('Fax.count', -1) do + delete fax_url(@fax) + end + + assert_redirected_to faxes_url + end +end diff --git a/test/controllers/letters_controller_test.rb b/test/controllers/letters_controller_test.rb new file mode 100644 index 0000000..320b998 --- /dev/null +++ b/test/controllers/letters_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class LettersControllerTest < ActionDispatch::IntegrationTest + setup do + @letter = letters(:letter_one) + end + + test "should get index" do + get letters_url + assert_response :success + end + + test "should get new" do + get new_letter_url + assert_response :success + end + + test "should create letter" do + assert_difference('Letter.count') do + post letters_url, params: { letter: { body: @letter.body, category: @letter.category, policy_or_law: @letter.policy_or_law, sentiment: @letter.sentiment, user_id: @letter.user_id } } + end + + assert_redirected_to letter_url(Letter.last) + end + + test "should show letter" do + get letter_url(@letter) + assert_response :success + end + + test "should get edit" do + get edit_letter_url(@letter) + assert_response :success + end + + test "should update letter" do + patch letter_url(@letter), params: { letter: { body: @letter.body, category: @letter.category, policy_or_law: @letter.policy_or_law, sentiment: @letter.sentiment, user_id: @letter.user_id } } + assert_redirected_to letter_url(@letter) + end + + test "should destroy letter" do + assert_difference('Letter.count', -1) do + delete letter_url(@letter) + end + + assert_redirected_to letters_url + end +end diff --git a/test/controllers/payments_controller_test.rb b/test/controllers/payments_controller_test.rb new file mode 100644 index 0000000..c5e86fa --- /dev/null +++ b/test/controllers/payments_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class PaymentsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/posts_controller_test.rb b/test/controllers/posts_controller_test.rb new file mode 100644 index 0000000..b7ce839 --- /dev/null +++ b/test/controllers/posts_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class PostsControllerTest < ActionDispatch::IntegrationTest + setup do + @post = posts(:one) + end + + test "should get index" do + get posts_url + assert_response :success + end + + test "should get new" do + get new_post_url + assert_response :success + end + + test "should create post" do + assert_difference('Post.count') do + post posts_url, params: { post: { address_city: @post.address_city, address_line_1: @post.address_line_1, address_line_2: @post.address_line_2, address_state: @post.address_state, address_zipcode: @post.address_zipcode, letter_id: @post.letter_id, payment_id: @post.payment_id, recipient_id: @post.recipient_id, sender_id: @post.sender_id } } + end + + assert_redirected_to post_url(Post.last) + end + + test "should show post" do + get post_url(@post) + assert_response :success + end + + test "should get edit" do + get edit_post_url(@post) + assert_response :success + end + + test "should update post" do + patch post_url(@post), params: { post: { address_city: @post.address_city, address_line_1: @post.address_line_1, address_line_2: @post.address_line_2, address_state: @post.address_state, address_zipcode: @post.address_zipcode, letter_id: @post.letter_id, payment_id: @post.payment_id, recipient_id: @post.recipient_id, sender_id: @post.sender_id } } + assert_redirected_to post_url(@post) + end + + test "should destroy post" do + assert_difference('Post.count', -1) do + delete post_url(@post) + end + + assert_redirected_to posts_url + end +end diff --git a/test/controllers/recipients_controller_test.rb b/test/controllers/recipients_controller_test.rb new file mode 100644 index 0000000..9c9b1f7 --- /dev/null +++ b/test/controllers/recipients_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class RecipientsControllerTest < ActionDispatch::IntegrationTest + setup do + @recipient = recipients(:one) + end + + test "should get index" do + get recipients_url + assert_response :success + end + + test "should get new" do + get new_recipient_url + assert_response :success + end + + test "should create recipient" do + assert_difference('Recipient.count') do + post recipients_url, params: { recipient: { address_city: @recipient.address_city, address_line_1: @recipient.address_line_1, address_line_2: @recipient.address_line_2, address_state: @recipient.address_state, address_zipcode: @recipient.address_zipcode, district: @recipient.district, email_address: @recipient.email_address, level: @recipient.level, name: @recipient.name, number_fax: @recipient.number_fax, number_phone: @recipient.number_phone, position: @recipient.position, state: @recipient.state } } + end + + assert_redirected_to recipient_url(Recipient.last) + end + + test "should show recipient" do + get recipient_url(@recipient) + assert_response :success + end + + test "should get edit" do + get edit_recipient_url(@recipient) + assert_response :success + end + + test "should update recipient" do + patch recipient_url(@recipient), params: { recipient: { address_city: @recipient.address_city, address_line_1: @recipient.address_line_1, address_line_2: @recipient.address_line_2, address_state: @recipient.address_state, address_zipcode: @recipient.address_zipcode, district: @recipient.district, email_address: @recipient.email_address, level: @recipient.level, name: @recipient.name, number_fax: @recipient.number_fax, number_phone: @recipient.number_phone, position: @recipient.position, state: @recipient.state } } + assert_redirected_to recipient_url(@recipient) + end + + test "should destroy recipient" do + assert_difference('Recipient.count', -1) do + delete recipient_url(@recipient) + end + + assert_redirected_to recipients_url + end +end diff --git a/test/controllers/send_communication_controller_test.rb b/test/controllers/send_communication_controller_test.rb new file mode 100644 index 0000000..7f9fbd6 --- /dev/null +++ b/test/controllers/send_communication_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class SendCommunicationControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/senders_controller_test.rb b/test/controllers/senders_controller_test.rb new file mode 100644 index 0000000..84ac455 --- /dev/null +++ b/test/controllers/senders_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class SendersControllerTest < ActionDispatch::IntegrationTest + setup do + @sender = senders(:one) + end + + test "should get index" do + get senders_url + assert_response :success + end + + test "should get new" do + get new_sender_url + assert_response :success + end + + test "should create sender" do + assert_difference('Sender.count') do + post senders_url, params: { sender: { county: @sender.county, district: @sender.district, name: @sender.name, state: @sender.state, user_id: @sender.user_id, zipcode: @sender.zipcode } } + end + + assert_redirected_to sender_url(Sender.last) + end + + test "should show sender" do + get sender_url(@sender) + assert_response :success + end + + test "should get edit" do + get edit_sender_url(@sender) + assert_response :success + end + + test "should update sender" do + patch sender_url(@sender), params: { sender: { county: @sender.county, district: @sender.district, name: @sender.name, state: @sender.state, user_id: @sender.user_id, zipcode: @sender.zipcode } } + assert_redirected_to sender_url(@sender) + end + + test "should destroy sender" do + assert_difference('Sender.count', -1) do + delete sender_url(@sender) + end + + assert_redirected_to senders_url + end +end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb new file mode 100644 index 0000000..811a08d --- /dev/null +++ b/test/controllers/sessions_controller_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class SessionsControllerTest < ActionDispatch::IntegrationTest + test "should get new" do + get login_path + assert_response :success + end +end diff --git a/test/controllers/static_pages_controller_test.rb b/test/controllers/static_pages_controller_test.rb new file mode 100644 index 0000000..b63391b --- /dev/null +++ b/test/controllers/static_pages_controller_test.rb @@ -0,0 +1,33 @@ +require "test_helper" + +class StaticPagesControllerTest < ActionDispatch::IntegrationTest + + def setup + @base_title = "VocalVoters" + end + + test "should get root" do + get root_url + assert_response :success + assert_select "title", "Home | #{@base_title}" + end + + test "should get help" do + get help_path + assert_response :success + assert_select "title", "Help | #{@base_title}" + end + + test "should get about" do + get about_path + assert_response :success + assert_select "title", "About | #{@base_title}" + end + + test "should get contact" do + get contact_path + assert_response :success + assert_select "title", "Contact | #{@base_title}" + end + +end diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb new file mode 100644 index 0000000..ae7f999 --- /dev/null +++ b/test/controllers/users_controller_test.rb @@ -0,0 +1,73 @@ +require "test_helper" + +class UsersControllerTest < ActionDispatch::IntegrationTest + + def setup + @user = users(:michael) + @other_user = users(:archer) + end + + test "should get new" do + get signup_path + assert_response :success + end + + test "should redirect index when not logged in" do + get users_path + assert_redirected_to login_url + end + + test "should redirect edit when not logged in" do + get edit_user_path(@user) + assert_not flash.empty? + assert_redirected_to login_url + end + + test "should redirect update when not logged in" do + patch user_path(@user), params: { user: { name: @user.name, + email: @user.email } } + assert_not flash.empty? + assert_redirected_to login_url + end + + test "should not allow the admin attribute to be edited via the web" do + log_in_as(@other_user) + assert_not @other_user.admin? + patch user_path(@other_user), params: { + user: { password: "password", + password_confirmation: "password", + admin: true } } + assert_not @other_user.admin? + end + + test "should redirect edit when logged in as wrong user" do + log_in_as(@other_user) + get edit_user_path(@user) + assert flash.empty? + assert_redirected_to root_url + end + + test "should redirect update when logged in as wrong user" do + log_in_as(@other_user) + patch user_path(@user), params: { user: { name: @user.name, + email: @user.email } } + assert flash.empty? + assert_redirected_to root_url + end + + test "should redirect destroy when not logged in" do + assert_no_difference 'User.count' do + delete user_path(@user) + end + assert_redirected_to login_url + end + + test "should redirect destroy when logged in as a non-admin" do + log_in_as(@other_user) + assert_no_difference 'User.count' do + delete user_path(@user) + end + assert_redirected_to root_url + end + +end diff --git a/test/fixtures/emails.yml b/test/fixtures/emails.yml new file mode 100644 index 0000000..4744bb3 --- /dev/null +++ b/test/fixtures/emails.yml @@ -0,0 +1,15 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + email_address: MyString + sender: one + recipient: one + letter: one + payment: one + +two: + email_address: MyString + sender: two + recipient: two + letter: two + payment: two diff --git a/test/fixtures/faxes.yml b/test/fixtures/faxes.yml new file mode 100644 index 0000000..f7da01f --- /dev/null +++ b/test/fixtures/faxes.yml @@ -0,0 +1,15 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + number_fax: 1 + sender: one + recipient: one + letter: one + payment: one + +two: + number_fax: 1 + sender: two + recipient: two + letter: two + payment: two diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/letters.yml b/test/fixtures/letters.yml new file mode 100644 index 0000000..d4d9c07 --- /dev/null +++ b/test/fixtures/letters.yml @@ -0,0 +1,26 @@ + +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +letter_one: + category: Guns + policy_or_law: 13A-11-7 + tags: guns + sentiment: 1.0 + body: This is my gun. Come and take it. + user: one + +letter_two: + category: Guns + policy_or_law: 14A-12-5 + tags: guns + sentiment: 0.5 + body: I don't like guns, but support peoples right. + user: two + +letter_three: + category: Voting + policy_or_law: HR1 + tags: voting + sentiment: 1.0 + body: I'm pro voting year round! + user: two diff --git a/test/fixtures/posts.yml b/test/fixtures/posts.yml new file mode 100644 index 0000000..f5131cf --- /dev/null +++ b/test/fixtures/posts.yml @@ -0,0 +1,23 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + address_line_1: MyString + address_line_2: MyString + address_city: MyString + address_state: MyString + address_zipcode: MyString + sender: one + recipient: one + letter: one + payment: one + +two: + address_line_1: MyString + address_line_2: MyString + address_city: MyString + address_state: MyString + address_zipcode: MyString + sender: two + recipient: two + letter: two + payment: two diff --git a/test/fixtures/recipients.yml b/test/fixtures/recipients.yml new file mode 100644 index 0000000..b8201fe --- /dev/null +++ b/test/fixtures/recipients.yml @@ -0,0 +1,31 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + position: MyString + level: MyString + district: MyString + state: MyString + number_fax: 1 + number_phone: 1 + email_address: MyString + address_line_1: MyString + address_line_2: MyString + address_city: MyString + address_state: MyString + address_zipcode: MyString + +two: + name: MyString + position: MyString + level: MyString + district: MyString + state: MyString + number_fax: 1 + number_phone: 1 + email_address: MyString + address_line_1: MyString + address_line_2: MyString + address_city: MyString + address_state: MyString + address_zipcode: MyString diff --git a/test/fixtures/senders.yml b/test/fixtures/senders.yml new file mode 100644 index 0000000..48be94b --- /dev/null +++ b/test/fixtures/senders.yml @@ -0,0 +1,17 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + zipcode: 1 + county: MyString + district: MyString + state: MyString + user: one + +two: + name: MyString + zipcode: 1 + county: MyString + district: MyString + state: MyString + user: two diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000..0dbf1dc --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,44 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +michael: + name: Michael Example + email: michael@example.com + password_digest: <%= User.digest('password') %> + admin: true + activated: true + activated_at: <%= Time.zone.now %> + +archer: + name: Sterling Archer + email: duchess@example.gov + password_digest: <%= User.digest('password') %> + activated: true + activated_at: <%= Time.zone.now %> + +inactive: + name: Inactive User + email: inactive@example.com + password_digest: <%= User.digest('password') %> + admin: false + activated: false + +one: + name: name1 + email: name1@email.com + activated: true + activated_at: <%= Time.zone.now %> + +two: + name: name2 + email: name2@email.com + activated: true + activated_at: <%= Time.zone.now %> + +<% 30.times do |n| %> +user_<%= n %>: + name: <%= "User #{n}" %> + email: <%= "user-#{n}@example.com" %> + password_digest: <%= User.digest('password') %> + activated: true + activated_at: <%= Time.zone.now %> +<% end %> \ No newline at end of file diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb new file mode 100644 index 0000000..32b6fe7 --- /dev/null +++ b/test/helpers/application_helper_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class ApplicationHelperTest < ActionView::TestCase + test "full title helper" do + assert_equal full_title, "VocalVoters" + assert_equal full_title("Help"), "Help | VocalVoters" + end +end diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/integration/password_resets_test.rb b/test/integration/password_resets_test.rb new file mode 100644 index 0000000..a342962 --- /dev/null +++ b/test/integration/password_resets_test.rb @@ -0,0 +1,80 @@ +require "test_helper" + +class PasswordResetsTest < ActionDispatch::IntegrationTest + + def setup + ActionMailer::Base.deliveries.clear + @user = users(:michael) + end + + test "password resets" do + get new_password_reset_path + assert_template 'password_resets/new' + assert_select 'input[name=?]', 'password_reset[email]' + # Invalid email + post password_resets_path, params: { password_reset: { email: "" } } + assert_not flash.empty? + assert_template 'password_resets/new' + # Valid email + post password_resets_path, + params: { password_reset: { email: @user.email } } + assert_not_equal @user.reset_digest, @user.reload.reset_digest + assert_equal 1, ActionMailer::Base.deliveries.size + assert_not flash.empty? + assert_redirected_to root_url + # Password reset form + user = assigns(:user) + # Wrong email + get edit_password_reset_path(user.reset_token, email: "") + assert_redirected_to root_url + # Inactive user + user.toggle!(:activated) + get edit_password_reset_path(user.reset_token, email: user.email) + assert_redirected_to root_url + user.toggle!(:activated) + # Right email, wrong token + get edit_password_reset_path('wrong token', email: user.email) + assert_redirected_to root_url + # Right email, right token + get edit_password_reset_path(user.reset_token, email: user.email) + assert_template 'password_resets/edit' + assert_select "input[name=email][type=hidden][value=?]", user.email + # Invalid password & confirmation + patch password_reset_path(user.reset_token), + params: { email: user.email, + user: { password: "foobaz", + password_confirmation: "barquux" } } + assert_select 'div#error_explanation' + # Empty password + patch password_reset_path(user.reset_token), + params: { email: user.email, + user: { password: "", + password_confirmation: "" } } + assert_select 'div#error_explanation' + # Valid password & confirmation + patch password_reset_path(user.reset_token), + params: { email: user.email, + user: { password: "foobaz", + password_confirmation: "foobaz" } } + assert is_logged_in? + assert_not flash.empty? + assert_redirected_to user + end + + test "expired token" do + get new_password_reset_path + post password_resets_path, + params: { password_reset: { email: @user.email } } + + @user = assigns(:user) + @user.update_attribute(:reset_sent_at, 3.hours.ago) + patch password_reset_path(@user.reset_token), + params: { email: @user.email, + user: { password: "foobar", + password_confirmation: "foobar" } } + assert_response :redirect + follow_redirect! + assert_match /expired/i, response.body + end + +end diff --git a/test/integration/site_layout_test.rb b/test/integration/site_layout_test.rb new file mode 100644 index 0000000..996579d --- /dev/null +++ b/test/integration/site_layout_test.rb @@ -0,0 +1,16 @@ +require "test_helper" + +class SiteLayoutTest < ActionDispatch::IntegrationTest + + test "layout links" do + get root_path + assert_template 'static_pages/home' + assert_select "a[href=?]", root_path, count: 2 + assert_select "a[href=?]", help_path + assert_select "a[href=?]", about_path + assert_select "a[href=?]", contact_path + get contact_path + assert_select "title", full_title("Contact") + end + +end diff --git a/test/integration/users_edit_test.rb b/test/integration/users_edit_test.rb new file mode 100644 index 0000000..1c57c35 --- /dev/null +++ b/test/integration/users_edit_test.rb @@ -0,0 +1,55 @@ +require 'test_helper' + +class UsersEditTest < ActionDispatch::IntegrationTest + + def setup + @user = users(:michael) + end + + test "unsuccessful edit" do + log_in_as(@user) + get edit_user_path(@user) + assert_template 'users/edit' + patch user_path(@user), params: { user: { name: "", + email: "foo@invalid", + password: "foo", + password_confirmation: "bar" } } + + assert_template 'users/edit' + end + + test "successful edit" do + log_in_as(@user) + get edit_user_path(@user) + assert_template 'users/edit' + name = "Foo Bar" + email = "foo@bar.com" + patch user_path(@user), params: { user: { name: name, + email: email, + password: "", + password_confirmation: "" } } + assert_not flash.empty? + assert_redirected_to @user + @user.reload + assert_equal name, @user.name + assert_equal email, @user.email + end + + test "successful edit with friendly forwarding" do + get edit_user_path(@user) + log_in_as(@user) + assert_redirected_to edit_user_url(@user) + name = "Foo Bar" + email = "foo@bar.com" + patch user_path(@user), params: { user: { name: name, + email: email, + password: "", + password_confirmation: "" } } + assert_not flash.empty? + assert_redirected_to @user + @user.reload + assert_equal name, @user.name + assert_equal email, @user.email + end + +end diff --git a/test/integration/users_index_test.rb b/test/integration/users_index_test.rb new file mode 100644 index 0000000..cbafd90 --- /dev/null +++ b/test/integration/users_index_test.rb @@ -0,0 +1,35 @@ +require "test_helper" + +class UsersIndexTest < ActionDispatch::IntegrationTest + + def setup + @admin = users(:michael) + @non_admin = users(:archer) + end + + test "index as admin including pagination and delete links" do + log_in_as(@admin) + first_page_of_users = User.paginate(page: 1) + first_page_of_users.first.toggle!(:activated) + get users_path + assert_template 'users/index' + assert_select 'div.pagination' + assigns(:users).each do |user| + assert user.activated? + assert_select 'a[href=?]', user_path(user), text: user.name + unless user == @admin + assert_select 'a[href=?]', user_path(user), text: 'delete' + end + end + assert_difference 'User.count', -1 do + delete user_path(@non_admin) + end + end + + test "index as non-admin" do + log_in_as(@non_admin) + get users_path + assert_select 'a', text: 'delete', count: 0 + end + +end diff --git a/test/integration/users_login_test.rb b/test/integration/users_login_test.rb new file mode 100644 index 0000000..28a3648 --- /dev/null +++ b/test/integration/users_login_test.rb @@ -0,0 +1,56 @@ +require "test_helper" + +class UsersLoginTest < ActionDispatch::IntegrationTest + + def setup + @user = users(:michael) + end + + test "login with valid email/invalid password" do + get login_path + assert_template 'sessions/new' + post login_path, params: { session: { email: @user.email, + password: "invalid" } } + assert_not is_logged_in? + assert_template 'sessions/new' + assert_not flash.empty? + get root_path + assert flash.empty? + end + + test "login with valid information followed by logout" do + get login_path + post login_path, params: { session: { email: @user.email, + password: 'password' } } + assert is_logged_in? + assert_redirected_to @user + follow_redirect! + assert_template 'users/show' + assert_select "a[href=?]", login_path, count: 0 + assert_select "a[href=?]", logout_path + assert_select "a[href=?]", user_path(@user) + delete logout_path + assert_not is_logged_in? + assert_redirected_to root_url + # Simulate a user clicking logout in a second window. + delete logout_path + follow_redirect! + assert_select "a[href=?]", login_path + assert_select "a[href=?]", logout_path, count: 0 + assert_select "a[href=?]", user_path(@user), count: 0 + end + + test "login with remembering" do + log_in_as(@user, remember_me: '1') + assert_not cookies[:remember_token].blank? + end + + test "login without remembering" do + # Log in to set the cookie. + log_in_as(@user, remember_me: '1') + # Log in again and verify that the cookie is deleted. + log_in_as(@user, remember_me: '0') + assert cookies[:remember_token].blank? + end + +end diff --git a/test/integration/users_show_test.rb b/test/integration/users_show_test.rb new file mode 100644 index 0000000..6777616 --- /dev/null +++ b/test/integration/users_show_test.rb @@ -0,0 +1,21 @@ +require 'test_helper' + +class UsersShowTest < ActionDispatch::IntegrationTest + + def setup + @inactive_user = users(:inactive) + @activated_user = users(:archer) + end + + test "should redirect when user not activated" do + get user_path(@inactive_user) + assert_response :redirect + assert_redirected_to root_url + end + + test "should display user when activated" do + get user_path(@activated_user) + assert_response :success + assert_template 'users/show' + end +end diff --git a/test/integration/users_signup_test.rb b/test/integration/users_signup_test.rb new file mode 100644 index 0000000..2e40259 --- /dev/null +++ b/test/integration/users_signup_test.rb @@ -0,0 +1,51 @@ +require "test_helper" + +class UsersSignupTest < ActionDispatch::IntegrationTest + + + def setup + ActionMailer::Base.deliveries.clear + end + + test "invalid signup information" do + get signup_path + assert_no_difference 'User.count' do + post users_path, params: { user: { name: "", + email: "user@invalid", + password: "foo", + password_confirmation: "bar" } } + end + assert_template 'users/new' + assert_select 'div#error_explanation' + assert_select 'div.alert-danger' + end + + test "valid signup information with account activation" do + get signup_path + assert_difference 'User.count', 1 do + post users_path, params: { user: { name: "Example User", + email: "user@example.com", + password: "password", + password_confirmation: "password" } } + end + assert_equal 1, ActionMailer::Base.deliveries.size + user = assigns(:user) + assert_not user.activated? + # Try to log in before activation. + log_in_as(user) + assert_not is_logged_in? + # Invalid activation token + get edit_account_activation_path("invalid token", email: user.email) + assert_not is_logged_in? + # Valid token, wrong email + get edit_account_activation_path(user.activation_token, email: 'wrong') + assert_not is_logged_in? + # Valid activation token + get edit_account_activation_path(user.activation_token, email: user.email) + assert user.reload.activated? + follow_redirect! + assert_template 'users/show' + assert is_logged_in? + end + +end diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb new file mode 100644 index 0000000..e787797 --- /dev/null +++ b/test/mailers/previews/user_mailer_preview.rb @@ -0,0 +1,20 @@ +# Preview all emails at http://localhost:3000/rails/mailers/user_mailer +class UserMailerPreview < ActionMailer::Preview + + # Preview this email at + # http://localhost:3000/rails/mailers/user_mailer/account_activation + def account_activation + user = User.first + user.activation_token = User.new_token + UserMailer.account_activation(user) + end + + # Preview this email at + # http://localhost:3000/rails/mailers/user_mailer/password_reset + def password_reset + user = User.first + user.reset_token = User.new_token + UserMailer.password_reset(user) + end + +end diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb new file mode 100644 index 0000000..0db08c4 --- /dev/null +++ b/test/mailers/user_mailer_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class UserMailerTest < ActionMailer::TestCase + + test "account_activation" do + user = users(:michael) + user.activation_token = User.new_token + mail = UserMailer.account_activation(user) + assert_equal "Account activation", mail.subject + assert_equal [user.email], mail.to + assert_equal ["noreply@vocalvoters.com"], mail.from + assert_match user.name, mail.body.encoded + assert_match user.activation_token, mail.body.encoded + assert_match CGI.escape(user.email), mail.body.encoded + end + + test "password_reset" do + user = users(:michael) + user.reset_token = User.new_token + mail = UserMailer.password_reset(user) + assert_equal "Password reset", mail.subject + assert_equal [user.email], mail.to + assert_equal ["noreply@vocalvoters.com"], mail.from + assert_match user.reset_token, mail.body.encoded + assert_match CGI.escape(user.email), mail.body.encoded + end + +end diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/models/email_test.rb b/test/models/email_test.rb new file mode 100644 index 0000000..a07b515 --- /dev/null +++ b/test/models/email_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class EmailTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/fax_test.rb b/test/models/fax_test.rb new file mode 100644 index 0000000..765dd4f --- /dev/null +++ b/test/models/fax_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class FaxTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/letter_test.rb b/test/models/letter_test.rb new file mode 100644 index 0000000..d2fcfec --- /dev/null +++ b/test/models/letter_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class LetterTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/post_test.rb b/test/models/post_test.rb new file mode 100644 index 0000000..ff155c4 --- /dev/null +++ b/test/models/post_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class PostTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/recipient_test.rb b/test/models/recipient_test.rb new file mode 100644 index 0000000..2bcf5d6 --- /dev/null +++ b/test/models/recipient_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class RecipientTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/sender_test.rb b/test/models/sender_test.rb new file mode 100644 index 0000000..a0c5c85 --- /dev/null +++ b/test/models/sender_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class SenderTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 0000000..bd3b01a --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,79 @@ +require "test_helper" + +class UserTest < ActiveSupport::TestCase + + def setup + @user = User.new(name: "Example User", email: "user@example.com", + password: "foobar", password_confirmation: "foobar") + end + + test "should be valid" do + assert @user.valid? + end + + test "name should be present" do + @user.name = " " + assert_not @user.valid? + end + + test "email should be present" do + @user.email = " " + assert_not @user.valid? + end + + test "name should not be too long" do + @user.name = "a" * 51 + assert_not @user.valid? + end + + test "email should not be too long" do + @user.email = "a" * 244 + "@example.com" + assert_not @user.valid? + end + + test "email validation should accept valid addresses" do + valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org + first.last@foo.jp alice+bob@baz.cn] + valid_addresses.each do |valid_address| + @user.email = valid_address + assert @user.valid?, "#{valid_address.inspect} should be valid" + end + end + + test "email validation should reject invalid addresses" do + invalid_addresses = %w[user@example,com user_at_foo.org user.name@example. + foo@bar_baz.com foo@bar+baz.com] + invalid_addresses.each do |invalid_address| + @user.email = invalid_address + assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" + end + end + + test "email addresses should be unique" do + duplicate_user = @user.dup + @user.save + assert_not duplicate_user.valid? + end + + test "email addresses should be saved as lower-case" do + mixed_case_email = "Foo@ExAMPle.CoM" + @user.email = mixed_case_email + @user.save + assert_equal mixed_case_email.downcase, @user.reload.email + end + + test "password should be present (nonblank)" do + @user.password = @user.password_confirmation = " " * 6 + assert_not @user.valid? + end + + test "password should have a minimum length" do + @user.password = @user.password_confirmation = "a" * 5 + assert_not @user.valid? + end + + test "authenticated? should return false for a user with nil digest" do + assert_not @user.authenticated?(:remember, '') + end + +end diff --git a/test/system/.keep b/test/system/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/system/emails_test.rb b/test/system/emails_test.rb new file mode 100644 index 0000000..bb1576d --- /dev/null +++ b/test/system/emails_test.rb @@ -0,0 +1,51 @@ +require "application_system_test_case" + +class EmailsTest < ApplicationSystemTestCase + setup do + @email = emails(:one) + end + + test "visiting the index" do + visit emails_url + assert_selector "h1", text: "Emails" + end + + test "creating a Email" do + visit emails_url + click_on "New Email" + + fill_in "Email address", with: @email.email_address + fill_in "Letter", with: @email.letter_id + fill_in "Payment", with: @email.payment_id + fill_in "Recipient", with: @email.recipient_id + fill_in "Sender", with: @email.sender_id + click_on "Create Email" + + assert_text "Email was successfully created" + click_on "Back" + end + + test "updating a Email" do + visit emails_url + click_on "Edit", match: :first + + fill_in "Email address", with: @email.email_address + fill_in "Letter", with: @email.letter_id + fill_in "Payment", with: @email.payment_id + fill_in "Recipient", with: @email.recipient_id + fill_in "Sender", with: @email.sender_id + click_on "Update Email" + + assert_text "Email was successfully updated" + click_on "Back" + end + + test "destroying a Email" do + visit emails_url + page.accept_confirm do + click_on "Destroy", match: :first + end + + assert_text "Email was successfully destroyed" + end +end diff --git a/test/system/faxes_test.rb b/test/system/faxes_test.rb new file mode 100644 index 0000000..e4aa35f --- /dev/null +++ b/test/system/faxes_test.rb @@ -0,0 +1,51 @@ +require "application_system_test_case" + +class FaxesTest < ApplicationSystemTestCase + setup do + @fax = faxes(:one) + end + + test "visiting the index" do + visit faxes_url + assert_selector "h1", text: "Faxes" + end + + test "creating a Fax" do + visit faxes_url + click_on "New Fax" + + fill_in "Letter", with: @fax.letter_id + fill_in "Number fax", with: @fax.number_fax + fill_in "Payment", with: @fax.payment_id + fill_in "Recipient", with: @fax.recipient_id + fill_in "Sender", with: @fax.sender_id + click_on "Create Fax" + + assert_text "Fax was successfully created" + click_on "Back" + end + + test "updating a Fax" do + visit faxes_url + click_on "Edit", match: :first + + fill_in "Letter", with: @fax.letter_id + fill_in "Number fax", with: @fax.number_fax + fill_in "Payment", with: @fax.payment_id + fill_in "Recipient", with: @fax.recipient_id + fill_in "Sender", with: @fax.sender_id + click_on "Update Fax" + + assert_text "Fax was successfully updated" + click_on "Back" + end + + test "destroying a Fax" do + visit faxes_url + page.accept_confirm do + click_on "Destroy", match: :first + end + + assert_text "Fax was successfully destroyed" + end +end diff --git a/test/system/letters_test.rb b/test/system/letters_test.rb new file mode 100644 index 0000000..d8b7c35 --- /dev/null +++ b/test/system/letters_test.rb @@ -0,0 +1,51 @@ +require "application_system_test_case" + +class LettersTest < ApplicationSystemTestCase + setup do + @letter = letters(:letter_one) + end + + test "visiting the index" do + visit letters_url + assert_selector "h1", text: "Letters" + end + + test "creating a Letter" do + visit letters_url + click_on "New Letter" + + fill_in "Body", with: @letter.body + fill_in "Category", with: @letter.category + fill_in "Policy or law", with: @letter.policy_or_law + fill_in "Sentiment", with: @letter.sentiment + fill_in "User", with: @letter.user_id + click_on "Create Letter" + + assert_text "Letter was successfully created" + click_on "Back" + end + + test "updating a Letter" do + visit letters_url + click_on "Edit", match: :first + + fill_in "Body", with: @letter.body + fill_in "Category", with: @letter.category + fill_in "Policy or law", with: @letter.policy_or_law + fill_in "Sentiment", with: @letter.sentiment + fill_in "User", with: @letter.user_id + click_on "Update Letter" + + assert_text "Letter was successfully updated" + click_on "Back" + end + + test "destroying a Letter" do + visit letters_url + page.accept_confirm do + click_on "Destroy", match: :first + end + + assert_text "Letter was successfully destroyed" + end +end diff --git a/test/system/posts_test.rb b/test/system/posts_test.rb new file mode 100644 index 0000000..e3d0519 --- /dev/null +++ b/test/system/posts_test.rb @@ -0,0 +1,59 @@ +require "application_system_test_case" + +class PostsTest < ApplicationSystemTestCase + setup do + @post = posts(:one) + end + + test "visiting the index" do + visit posts_url + assert_selector "h1", text: "Posts" + end + + test "creating a Post" do + visit posts_url + click_on "New Post" + + fill_in "Address city", with: @post.address_city + fill_in "Address line 1", with: @post.address_line_1 + fill_in "Address line 2", with: @post.address_line_2 + fill_in "Address state", with: @post.address_state + fill_in "Address zipcode", with: @post.address_zipcode + fill_in "Letter", with: @post.letter_id + fill_in "Payment", with: @post.payment_id + fill_in "Recipient", with: @post.recipient_id + fill_in "Sender", with: @post.sender_id + click_on "Create Post" + + assert_text "Post was successfully created" + click_on "Back" + end + + test "updating a Post" do + visit posts_url + click_on "Edit", match: :first + + fill_in "Address city", with: @post.address_city + fill_in "Address line 1", with: @post.address_line_1 + fill_in "Address line 2", with: @post.address_line_2 + fill_in "Address state", with: @post.address_state + fill_in "Address zipcode", with: @post.address_zipcode + fill_in "Letter", with: @post.letter_id + fill_in "Payment", with: @post.payment_id + fill_in "Recipient", with: @post.recipient_id + fill_in "Sender", with: @post.sender_id + click_on "Update Post" + + assert_text "Post was successfully updated" + click_on "Back" + end + + test "destroying a Post" do + visit posts_url + page.accept_confirm do + click_on "Destroy", match: :first + end + + assert_text "Post was successfully destroyed" + end +end diff --git a/test/system/recipients_test.rb b/test/system/recipients_test.rb new file mode 100644 index 0000000..5a1d5ba --- /dev/null +++ b/test/system/recipients_test.rb @@ -0,0 +1,67 @@ +require "application_system_test_case" + +class RecipientsTest < ApplicationSystemTestCase + setup do + @recipient = recipients(:one) + end + + test "visiting the index" do + visit recipients_url + assert_selector "h1", text: "Recipients" + end + + test "creating a Recipient" do + visit recipients_url + click_on "New Recipient" + + fill_in "Address city", with: @recipient.address_city + fill_in "Address line 1", with: @recipient.address_line_1 + fill_in "Address line 2", with: @recipient.address_line_2 + fill_in "Address state", with: @recipient.address_state + fill_in "Address zipcode", with: @recipient.address_zipcode + fill_in "District", with: @recipient.district + fill_in "Email address", with: @recipient.email_address + fill_in "Level", with: @recipient.level + fill_in "Name", with: @recipient.name + fill_in "Number fax", with: @recipient.number_fax + fill_in "Number phone", with: @recipient.number_phone + fill_in "Position", with: @recipient.position + fill_in "State", with: @recipient.state + click_on "Create Recipient" + + assert_text "Recipient was successfully created" + click_on "Back" + end + + test "updating a Recipient" do + visit recipients_url + click_on "Edit", match: :first + + fill_in "Address city", with: @recipient.address_city + fill_in "Address line 1", with: @recipient.address_line_1 + fill_in "Address line 2", with: @recipient.address_line_2 + fill_in "Address state", with: @recipient.address_state + fill_in "Address zipcode", with: @recipient.address_zipcode + fill_in "District", with: @recipient.district + fill_in "Email address", with: @recipient.email_address + fill_in "Level", with: @recipient.level + fill_in "Name", with: @recipient.name + fill_in "Number fax", with: @recipient.number_fax + fill_in "Number phone", with: @recipient.number_phone + fill_in "Position", with: @recipient.position + fill_in "State", with: @recipient.state + click_on "Update Recipient" + + assert_text "Recipient was successfully updated" + click_on "Back" + end + + test "destroying a Recipient" do + visit recipients_url + page.accept_confirm do + click_on "Destroy", match: :first + end + + assert_text "Recipient was successfully destroyed" + end +end diff --git a/test/system/senders_test.rb b/test/system/senders_test.rb new file mode 100644 index 0000000..b8fb6ba --- /dev/null +++ b/test/system/senders_test.rb @@ -0,0 +1,53 @@ +require "application_system_test_case" + +class SendersTest < ApplicationSystemTestCase + setup do + @sender = senders(:one) + end + + test "visiting the index" do + visit senders_url + assert_selector "h1", text: "Senders" + end + + test "creating a Sender" do + visit senders_url + click_on "New Sender" + + fill_in "County", with: @sender.county + fill_in "District", with: @sender.district + fill_in "Name", with: @sender.name + fill_in "State", with: @sender.state + fill_in "User", with: @sender.user_id + fill_in "Zipcode", with: @sender.zipcode + click_on "Create Sender" + + assert_text "Sender was successfully created" + click_on "Back" + end + + test "updating a Sender" do + visit senders_url + click_on "Edit", match: :first + + fill_in "County", with: @sender.county + fill_in "District", with: @sender.district + fill_in "Name", with: @sender.name + fill_in "State", with: @sender.state + fill_in "User", with: @sender.user_id + fill_in "Zipcode", with: @sender.zipcode + click_on "Update Sender" + + assert_text "Sender was successfully updated" + click_on "Back" + end + + test "destroying a Sender" do + visit senders_url + page.accept_confirm do + click_on "Destroy", match: :first + end + + assert_text "Sender was successfully destroyed" + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..c5c4897 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,35 @@ +ENV['RAILS_ENV'] ||= 'test' +require_relative "../config/environment" +require "rails/test_help" +require "minitest/reporters" +Minitest::Reporters.use! + +class ActiveSupport::TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + include ApplicationHelper + + # Returns true if a test user is logged in. + def is_logged_in? + !session[:user_id].nil? + end + + # Log in as a particular user. + def log_in_as(user) + session[:user_id] = user.id + end + +end + +class ActionDispatch::IntegrationTest + + # Log in as a particular user. + def log_in_as(user, password: 'password', remember_me: '1') + post login_path, params: { session: { email: user.email, + password: password, + remember_me: remember_me } } + end +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29