diff --git a/.env.example b/.env.example index 7c05f6e69..a25b320af 100644 --- a/.env.example +++ b/.env.example @@ -63,6 +63,8 @@ amazon_authorize_key= # Third party integration (required) congress_forms_url=http://phantomdc.example.com +google_civic_api_url=https://www.googleapis.com/civicinfo/v2/representatives/ +google_civic_api_key= smarty_streets_id= smarty_streets_token= diff --git a/.env.test b/.env.test index 5c939e805..d479be1ac 100644 --- a/.env.test +++ b/.env.test @@ -8,8 +8,8 @@ call_tool_api_key=xyz storage=s3 amazon_region=us-west-1 -amazon_bucket=actioncenter-staging -amazon_bucket_url=actioncentertest.s3-us-west-1.amazonaws.com +amazon_bucket=actioncenter-test +amazon_bucket_url=actioncenter-testurl.s3-us-west-1.amazonaws.com supporters_api_key=xyz supporters_host=https://civicrm.test diff --git a/.gitignore b/.gitignore index 156f76604..a3e9e0020 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ public/webshims # Ignore dotenv file /.env +/public/uploads diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..6c97cdaa1 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,26 @@ +stages: + - build + +variables: + IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + +before_script: + - env + - docker info + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY + +build: + stage: build + script: + - docker build --pull --label "repo=$CI_PROJECT_URL" -t $IMAGE_NAME . + - docker push $IMAGE_NAME + - | + if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then + docker tag "$IMAGE_NAME" "$CI_REGISTRY_IMAGE:latest" + docker push "$CI_REGISTRY_IMAGE:latest" + fi + # Run this job in a branch where a Dockerfile exists + rules: + - if: $CI_COMMIT_BRANCH + exists: + - Dockerfile diff --git a/.rubocop.yml b/.rubocop.yml index 7933b1521..cf8cb7555 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,18 +1,78 @@ +require: + - rubocop-rails + - rubocop-performance + inherit_gem: rubocop-github: - config/default.yml - config/rails.yml AllCops: - TargetRailsVersion: 4.2 - TargetRubyVersion: 2.3 + TargetRailsVersion: 7.0 + TargetRubyVersion: 3.0 + NewCops: enable + SuggestExtensions: false Exclude: - 'db/**/*' - 'config/**/*' - 'bin/**/*' - 'vendor/**/*' + - 'features/**/*' + +# Handle cops not enabled by default +# Should probably be checked on rubocop version upgrades +# Last checked 02/09/21 +# Disables cops marked as unsafe in docs + +Layout/SpaceAroundMethodCallOperator: + Enabled: true + +Lint/RaiseException: + Enabled: true + +Lint/StructNewOverride: + Enabled: true + +Style/ExponentialNotation: + Enabled: true + +Style/HashEachMethods: + Enabled: false -### Override rubcop-github ### +Style/HashTransformKeys: + Enabled: false + +Style/HashTransformValues: + Enabled: false + +Performance/AncestorsInclude: + Enabled: false + +Performance/BigDecimalWithNumericArgument: + Enabled: true + +Performance/RedundantSortBlock: + Enabled: true + +Performance/RedundantStringChars: + Enabled: true + +Performance/ReverseFirst: + Enabled: true + +Performance/SortReverse: + Enabled: true + +Performance/Squeeze: + Enabled: true + +Performance/StringInclude: + Enabled: true + +Bundler/OrderedGems: + Enabled: false + +### Override rubocop-github ### Style/FrozenStringLiteralComment: # We aren't upgrading to Ruby 3.0, so this is just noise @@ -30,9 +90,6 @@ GitHub/RailsControllerRenderPathsExist: # Doesn't seem to work; can't find templates that exist Enabled: false -GitHub/RailsApplicationRecord: - # Doesn't apply to < Rails 5.0, and ignores TargetRailsVersion - Enabled: false Lint/Void: Exclude: @@ -43,7 +100,7 @@ Lint/Debugger: Exclude: - 'features/step_definitions/debug_steps.rb' -Style/BlockComments: +Lint/AmbiguousBlockAssociation: Exclude: - 'spec/**/*' @@ -65,7 +122,30 @@ Layout/MultilineHashBraceLayout: Layout/SpaceAroundOperators: Enabled: true +Naming/PredicateName: + Exclude: + - 'spec/**/*' + Security/JSONLoad: Enabled: true Exclude: - 'spec/**/*' + +Rails/HttpStatus: + EnforcedStyle: 'numeric' + +Style/Alias: + Enabled: false + +Style/BlockComments: + Exclude: + - 'spec/**/*' + +Style/ClassAndModuleChildren: + EnforcedStyle: 'compact' + +Style/Documentation: + Enabled: false + +Style/NumericLiterals: + MinDigits: 6 diff --git a/.ruby-version b/.ruby-version index 0cadbc1e3..fa7adc7ac 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.5.5 +3.3.5 diff --git a/.travis.yml b/.travis.yml index b4a0ed27c..8ffc83c25 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ before_script: - npm install - cp config/database.yml.travis config/database.yml - psql -c 'create database travis_ci_test;' -U postgres - - RAILS_ENV=test bundle exec rails webdrivers:chromedriver:update + - google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost & script: - bundle exec rubocop - $(npm bin)/sass-lint -vq diff --git a/Dockerfile b/Dockerfile index ea4308c07..844a91e70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,10 @@ -FROM ruby:2.5-stretch +FROM ruby:3.3-slim RUN mkdir /opt/actioncenter WORKDIR /opt/actioncenter +COPY db/global-bundle.pem /opt/actioncenter/vendor/assets/certificates/ + RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ @@ -13,24 +15,10 @@ RUN apt-get update && \ postgresql-client \ cron \ gnupg \ - libssl-dev - -RUN set -x; \ - curl -sL https://deb.nodesource.com/setup_6.x -o nodesource_setup.sh \ - && chmod +x nodesource_setup.sh \ - && ./nodesource_setup.sh \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ + libssl-dev \ + shared-mime-info \ nodejs \ - npm \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ - && apt-get update \ - && apt-get install \ - yarn + npm COPY package.json package-lock.json ./ RUN npm install @@ -65,11 +53,10 @@ RUN bundle exec rake assets:precompile \ SECRET_KEY_BASE=noop \ devise_secret_key=noop \ amazon_region=noop \ + amazon_bucket=noop \ DATABASE_URL=postgres://noop -RUN bundle exec rake webshims:update_public -RUN mkdir /opt/actioncenter/log \ - /var/www +RUN mkdir /var/www RUN chown -R www-data /opt/actioncenter/public \ /opt/actioncenter/db \ /opt/actioncenter/tmp \ diff --git a/Gemfile b/Gemfile index e30d333e3..9d57b22e3 100644 --- a/Gemfile +++ b/Gemfile @@ -1,31 +1,29 @@ source "https://rubygems.org" -gem "rails", "~> 5.0" +gem "rails", "~> 7.0.0" -#Database +# Database gem "pg", "~> 1.1" -gem "pg_search" +gem "pg_search", "~> 2" # Hosting-related -gem "aws-sdk", "~> 2.3" -gem "aws-sdk-rails", "~> 1" +gem "aws-sdk-rails", "~> 2" +gem "aws-sdk-s3", "~> 1" gem "dotenv-rails", "~> 2" gem "rack-attack", "~> 5" -gem "rails_12factor", group: :production # Loads "rails_serve_static_assets" and "rails_stdout_logging" -gem "rails_response_headers", "~> 0" +gem "rails_response_headers", git: "https://github.com/EFForg/rails_response_headers.git" # Frontend/assets gem "bootstrap-daterangepicker-rails", "~> 3" gem "bootstrap-sass", "~> 3.4" gem "bourbon", "~> 3" gem "bundler", ">= 1.8.4" # needed for rails-assets -gem "fontello_rails_converter", "~> 0" gem "react-rails", "~> 1" gem "redcarpet", "~> 3" # Markdown -gem "sass-rails", "~> 5.0" +gem "sass-rails", "< 5.1" gem "select2-rails" # Autocomplete select menus gem "uglifier", ">= 1.3.0" # compressor for JavaScript assets -gem "webshims-rails", "~> 1" + source "https://rails-assets.org" do gem "rails-assets-chartjs", "~> 2" gem "rails-assets-congress-images-102x125" @@ -44,17 +42,17 @@ source "https://rails-assets.org" do end # File upload -gem "paperclip", "~> 5.2" +gem "carrierwave", "~> 3.0" +gem "fog-aws" # Email preformatting gem "nokogiri", "~> 1" # Required for premailer-rails gem "premailer-rails", "~> 1" # Inline styles for emails -# Optimization -gem "sprockets-image_compressor", "~> 0" # Optimizes png/jpg - # Analytics -gem "ahoy_matey", "~> 1.6" # Analytics +gem "ahoy_matey", "~> 4.0" +# required for ahoy_matey 2.0 with activerecordstore +gem "uuidtools", "~> 2" gem "chartkick", "~> 3" gem "eff_matomo", "~> 0.2.4", require: "matomo" gem "groupdate", "~> 2" @@ -64,16 +62,15 @@ gem "daemons", "~> 1" gem "delayed_job_active_record", "~> 4" # Exception monitoring -gem "sentry-raven", "~> 0.15" +gem "sentry-raven", "~> 3.1.2" # Fancy counter caches gem "counter_culture", "~> 2.0" # Other -gem "activerecord-session_store", "~> 1" -gem "acts_as_paranoid", git: "https://github.com/ActsAsParanoid/acts_as_paranoid.git" +gem "activerecord-session_store", "~> 2.1.0" +gem "acts_as_paranoid", "~> 0.7" gem "cocoon", "~> 1" # Dynamically add and remove nested associations from forms -gem "descriptive_statistics", "~> 2" # Used for calculating percentiles gem "devise", "~> 4.7" gem "ejs", "~> 1" # Embedded javascript gem "email_validator", "~> 1" @@ -84,17 +81,19 @@ gem "gravatar-ultimate", "~> 2" gem "http_accept_language", "~> 2" # Detect HTTP language header gem "invisible_captcha", "~> 0" # Prevent form submissions by bots gem "iso_country_codes", "~> 0" -gem "jbuilder", "~> 1.2" # JSON APIs +gem "jbuilder", "~> 2" gem "oauth", "~> 0" gem "rest-client", "~> 2" -gem "sanitize", "~> 4" # Sanitize user input -gem "warden", "1.2.4" # This dep of devise has a bug in 1.2.5 so am avaoiding +gem "sanitize", "~> 6" # Sanitize user input gem "whenever", "~> 0", require: false # Cron jobs gem "will_paginate", "~> 3.0" -gem "xmlrpc" +gem "xmlrpc", "~> 0.3" # For creating many records, quickly -gem "fast_inserter", "~> 0.1" +gem "fast_inserter", "~> 2.0" + +# Pin psych to below version 4 until we're on rails 7 and ruby 3.1 +gem "psych", "< 4" group :doc do # bundle exec rake doc:rails generates the API under doc/api @@ -103,31 +102,23 @@ end group :development do gem "better_errors", "~> 2" - gem "binding_of_caller", "~> 0" - gem "rails-dev-tweaks", "~> 1.1" - gem "rb-fchange", "~> 0", require: false - gem "rb-fsevent", "~> 0", require: false - gem "rb-inotify", "~> 0", require: false end group :test do - gem "webmock", "~> 2" + gem "webmock", "~> 3" + gem "selenium-devtools" end group :development, :test do gem "byebug" - gem "capybara", "~> 3.26" - gem "cucumber-rails", "1.6.0", require: false - gem "database_cleaner", "~> 1" - gem "factory_girl_rails", "~> 4" - gem "poltergeist", "~> 1" + gem "capybara", "~> 3" + gem "factory_bot_rails", "~> 6.2" gem "rails-controller-testing" - gem "rspec-core", "~> 3" - gem "rspec-rails", "~> 3" - gem "rubocop", "0.52.0" - gem "rubocop-github", "0.9.0" - gem "selenium-webdriver", "~> 3" - gem "webdrivers", "~> 4" + gem "rspec-rails", "~> 6.1" + gem "rubocop" + gem "rubocop-github", "~> 0.16" + gem "rubocop-performance", require: false + gem "rubocop-rails", require: false end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 259f5127b..06c40396b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,518 +1,591 @@ GIT - remote: https://github.com/ActsAsParanoid/acts_as_paranoid.git - revision: b58c5149d32f06ea729ad1c8bd3ac26d8cde50ce + remote: https://github.com/EFForg/rails_response_headers.git + revision: 472d93ad39876c350cfb388cf6f7d68433793dc1 specs: - acts_as_paranoid (0.6.0) - activerecord (>= 4.2, < 6.0) - activesupport (>= 4.2, < 6.0) + rails_response_headers (0.3.0) GEM - remote: https://rubygems.org/ remote: https://rails-assets.org/ specs: - actioncable (5.0.7.2) - actionpack (= 5.0.7.2) - nio4r (>= 1.2, < 3.0) - websocket-driver (~> 0.6.1) - actionmailer (5.0.7.2) - actionpack (= 5.0.7.2) - actionview (= 5.0.7.2) - activejob (= 5.0.7.2) + rails-assets-EpicEditor (0.2.3) + rails-assets-chartjs (2.9.4) + rails-assets-congress-images-102x125 (0.1.2) + rails-assets-html5shiv (3.7.2) + rails-assets-ionicons (2.0.1) + rails-assets-jquery (2.1.3) + rails-assets-jquery-cookie (1.4.1) + rails-assets-jquery (>= 1.2) + rails-assets-jquery-timeago (1.6.7) + rails-assets-jquery (>= 1.4) + rails-assets-jquery-ujs (1.0.3) + rails-assets-jquery (> 1.8) + rails-assets-lodash (3.7.0) + rails-assets-moment (2.9.0) + rails-assets-respond (1.4.2) + rails-assets-roboto-webfont (0.1.1) + rails-assets-sweetalert (1.0.1) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.0.8.6) + actionpack (= 7.0.8.6) + activesupport (= 7.0.8.6) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (7.0.8.6) + actionpack (= 7.0.8.6) + activejob (= 7.0.8.6) + activerecord (= 7.0.8.6) + activestorage (= 7.0.8.6) + activesupport (= 7.0.8.6) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.8.6) + actionpack (= 7.0.8.6) + actionview (= 7.0.8.6) + activejob (= 7.0.8.6) + activesupport (= 7.0.8.6) mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp rails-dom-testing (~> 2.0) - actionpack (5.0.7.2) - actionview (= 5.0.7.2) - activesupport (= 5.0.7.2) - rack (~> 2.0) - rack-test (~> 0.6.3) + actionpack (7.0.8.6) + actionview (= 7.0.8.6) + activesupport (= 7.0.8.6) + rack (~> 2.0, >= 2.2.4) + rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.7.2) - activesupport (= 5.0.7.2) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.8.6) + actionpack (= 7.0.8.6) + activerecord (= 7.0.8.6) + activestorage (= 7.0.8.6) + activesupport (= 7.0.8.6) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.8.6) + activesupport (= 7.0.8.6) builder (~> 3.1) - erubis (~> 2.7.0) + erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.0.7.2) - activesupport (= 5.0.7.2) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.8.6) + activesupport (= 7.0.8.6) globalid (>= 0.3.6) - activemodel (5.0.7.2) - activesupport (= 5.0.7.2) - activerecord (5.0.7.2) - activemodel (= 5.0.7.2) - activesupport (= 5.0.7.2) - arel (~> 7.0) - activerecord-session_store (1.1.3) - actionpack (>= 4.0) - activerecord (>= 4.0) + activemodel (7.0.8.6) + activesupport (= 7.0.8.6) + activerecord (7.0.8.6) + activemodel (= 7.0.8.6) + activesupport (= 7.0.8.6) + activerecord-session_store (2.1.0) + actionpack (>= 6.1) + activerecord (>= 6.1) + cgi (>= 0.3.6) multi_json (~> 1.11, >= 1.11.2) - rack (>= 1.5.2, < 3) - railties (>= 4.0) - activesupport (5.0.7.2) + rack (>= 2.0.8, < 4) + railties (>= 6.1) + activestorage (7.0.8.6) + actionpack (= 7.0.8.6) + activejob (= 7.0.8.6) + activerecord (= 7.0.8.6) + activesupport (= 7.0.8.6) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (7.0.8.6) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - addressable (2.6.0) - public_suffix (>= 2.0.2, < 4.0) - after_commit_action (1.1.0) - activerecord (>= 3.0.0) - activesupport (>= 3.0.0) - ahoy_matey (1.6.1) - addressable - browser (~> 2.0) - geocoder - rack-attack (< 6) - railties - referer-parser (>= 0.3.0) - request_store - safely_block (>= 0.1.1) - user_agent_parser - uuidtools - arel (7.1.4) - ast (2.4.0) - autoprefixer-rails (9.5.1) - execjs - aws-eventstream (1.0.3) - aws-sdk (2.11.264) - aws-sdk-resources (= 2.11.264) - aws-sdk-core (2.11.264) - aws-sigv4 (~> 1.0) - jmespath (~> 1.0) - aws-sdk-rails (1.0.1) - aws-sdk-resources (~> 2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + acts_as_paranoid (0.10.2) + activerecord (>= 6.1, < 8) + activesupport (>= 6.1, < 8) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ahoy_matey (4.2.1) + activesupport (>= 5.2) + device_detector + safely_block (>= 0.2.1) + ast (2.4.2) + autoprefixer-rails (10.4.19.0) + execjs (~> 2) + aws-eventstream (1.3.0) + aws-partitions (1.1001.0) + aws-sdk-core (3.211.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.95.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-rails (2.1.0) + aws-sdk-ses (~> 1) railties (>= 3) - aws-sdk-resources (2.11.264) - aws-sdk-core (= 2.11.264) - aws-sigv4 (1.1.0) - aws-eventstream (~> 1.0, >= 1.0.2) + aws-sdk-s3 (1.169.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sdk-ses (1.76.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) + aws-eventstream (~> 1, >= 1.0.2) babel-source (5.8.35) babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) - backports (3.14.0) - bcrypt (3.1.13) - better_errors (2.5.1) - coderay (>= 1.0.0) + base64 (0.2.0) + bcrypt (3.1.20) + better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) - binding_of_caller (0.8.0) - debug_inspector (>= 0.0.1) - bootstrap-daterangepicker-rails (3.0.3) - railties (>= 4.0, < 5.3) + rouge (>= 1.0.0) + bigdecimal (3.1.8) + bootstrap-daterangepicker-rails (3.0.4) + railties (>= 4.0) bootstrap-sass (3.4.1) autoprefixer-rails (>= 5.2.1) sassc (>= 2.0.0) bourbon (3.2.4) sass (~> 3.2) thor - browser (2.5.3) - builder (3.2.3) - byebug (11.0.1) - capybara (3.26.0) + builder (3.3.0) + byebug (11.1.3) + capybara (3.40.0) addressable + matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) - regexp_parser (~> 1.5) + regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - chartkick (3.3.0) - childprocess (2.0.0) - rake (< 13.0) + carrierwave (3.0.7) + activemodel (>= 6.0.0) + activesupport (>= 6.0.0) + addressable (~> 2.6) + image_processing (~> 1.1) + marcel (~> 1.0.0) + ssrf_filter (~> 1.0) + cgi (0.4.1) + chartkick (3.4.2) chronic (0.10.2) - climate_control (0.2.0) - cliver (0.3.2) - cocoon (1.2.12) - coderay (1.1.2) - concurrent-ruby (1.1.5) - connection_pool (2.2.2) - counter_culture (2.2.3) + cocoon (1.2.15) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + counter_culture (2.9.0) activerecord (>= 4.2) activesupport (>= 4.2) - after_commit_action (~> 1.0) - crack (0.4.3) - safe_yaml (~> 1.0.0) - crass (1.0.5) - css_parser (1.7.0) + crack (1.0.0) + bigdecimal + rexml + crass (1.0.6) + css_parser (1.19.1) addressable - cucumber (3.1.2) - builder (>= 2.1.2) - cucumber-core (~> 3.2.0) - cucumber-expressions (~> 6.0.1) - cucumber-wire (~> 0.0.1) - diff-lcs (~> 1.3) - gherkin (~> 5.1.0) - multi_json (>= 1.7.5, < 2.0) - multi_test (>= 0.1.2) - cucumber-core (3.2.1) - backports (>= 3.8.0) - cucumber-tag_expressions (~> 1.1.0) - gherkin (~> 5.0) - cucumber-expressions (6.0.1) - cucumber-rails (1.6.0) - capybara (>= 1.1.2, < 4) - cucumber (>= 3.0.2, < 4) - mime-types (>= 1.17, < 4) - nokogiri (~> 1.8) - railties (>= 4, < 6) - cucumber-tag_expressions (1.1.1) - cucumber-wire (0.0.1) - daemons (1.3.1) - database_cleaner (1.7.0) - debug_inspector (0.0.3) - delayed_job (4.1.5) - activesupport (>= 3.0, < 5.3) - delayed_job_active_record (4.1.3) - activerecord (>= 3.0, < 5.3) + csv (3.3.0) + daemons (1.4.1) + date (3.4.0) + delayed_job (4.1.12) + activesupport (>= 3.0, < 8.0) + delayed_job_active_record (4.1.10) + activerecord (>= 3.0, < 8.0) delayed_job (>= 3.0, < 5) - descriptive_statistics (2.5.1) - devise (4.7.1) + device_detector (1.1.3) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.3) - domain_name (0.5.20180417) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.2) - dotenv-rails (2.7.2) - dotenv (= 2.7.2) - railties (>= 3.2, < 6.1) + diff-lcs (1.5.1) + domain_name (0.6.20240107) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) + railties (>= 3.2) eff_matomo (0.2.4) activesupport httparty ejs (1.1.1) email_validator (1.6.0) activemodel - errbase (0.1.1) - erubi (1.8.0) - erubis (2.7.0) - execjs (2.7.0) - factory_girl (4.9.0) - activesupport (>= 3.0.0) - factory_girl_rails (4.9.0) - factory_girl (~> 4.9.0) - railties (>= 3.0.0) - faraday (0.15.4) - multipart-post (>= 1.2, < 3) - fast_inserter (0.1.6) + erubi (1.13.0) + excon (1.1.1) + execjs (2.10.0) + factory_bot (6.5.0) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) + railties (>= 5.0.0) + faraday (2.12.0) + faraday-net_http (>= 2.0, < 3.4) + json + logger + faraday-net_http (3.3.0) + net-http + fast_inserter (2.0.0) activerecord (>= 4.1.0) - fastly (2.3.0) - ffi (1.10.0) - fontello_rails_converter (0.4.6) - activesupport - launchy - rest-client - rubyzip (~> 1.0) - friendly_id (5.2.5) + fastly (2.5.3) + ffi (1.17.0-aarch64-linux-gnu) + ffi (1.17.0-aarch64-linux-musl) + ffi (1.17.0-arm-linux-gnu) + ffi (1.17.0-arm-linux-musl) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86-linux-gnu) + ffi (1.17.0-x86-linux-musl) + ffi (1.17.0-x86_64-darwin) + ffi (1.17.0-x86_64-linux-gnu) + ffi (1.17.0-x86_64-linux-musl) + fog-aws (3.29.0) + base64 (~> 0.2.0) + fog-core (~> 2.6) + fog-json (~> 1.1) + fog-xml (~> 0.1) + fog-core (2.6.0) + builder + excon (~> 1.0) + formatador (>= 0.2, < 2.0) + mime-types + fog-json (1.2.0) + fog-core + multi_json (~> 1.10) + fog-xml (0.1.4) + fog-core + nokogiri (>= 1.5.11, < 2.0.0) + formatador (1.1.0) + friendly_id (5.5.1) activerecord (>= 4.0.0) - geocoder (1.5.1) - gherkin (5.1.0) - globalid (0.4.2) - activesupport (>= 4.2.0) + globalid (1.2.1) + activesupport (>= 6.1) going_postal (0.1.6) gravatar-ultimate (2.0.0) activesupport (>= 2.3.14) rack groupdate (2.5.3) activesupport (>= 3) - hashdiff (0.3.9) + hashdiff (1.1.1) + hashie (5.0.0) htmlentities (4.3.4) - http-cookie (1.0.3) + http-accept (1.7.0) + http-cookie (1.0.7) domain_name (~> 0.5) http_accept_language (2.1.1) - httparty (0.17.0) - mime-types (~> 3.0) + httparty (0.22.0) + csv + mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.6.0) + i18n (1.14.6) concurrent-ruby (~> 1.0) - invisible_captcha (0.12.0) + image_processing (1.13.0) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) + invisible_captcha (0.13.0) rails (>= 3.2.0) iso_country_codes (0.7.8) - jbuilder (1.5.3) - activesupport (>= 3.0.0) - multi_json (>= 1.2.0) - jmespath (1.4.0) - launchy (2.4.3) - addressable (~> 2.3) - loofah (2.3.1) + jbuilder (2.13.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + jmespath (1.6.2) + json (2.7.5) + language_server-protocol (3.17.0.3) + logger (1.6.1) + loofah (2.23.1) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) - method_source (0.9.2) - mime-types (3.2.2) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + method_source (1.1.0) + mime-types (3.6.0) + logger mime-types-data (~> 3.2015) - mime-types-data (3.2019.0331) - mimemagic (0.3.10) - nokogiri (~> 1) - rake - mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.11.3) - multi_json (1.13.1) - multi_test (0.1.2) - multi_xml (0.6.0) - multipart-post (2.0.0) + mime-types-data (3.2024.1001) + mini_magick (4.13.2) + mini_mime (1.1.5) + minitest (5.25.1) + multi_json (1.15.0) + multi_xml (0.7.1) + bigdecimal (~> 3.1) + net-http (0.4.1) + uri + net-imap (0.5.0) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol netrc (0.11.0) - nio4r (2.3.1) - nokogiri (1.10.8) - mini_portile2 (~> 2.4.0) - nokogumbo (1.5.0) - nokogiri - oauth (0.5.5) + nio4r (2.7.4) + nokogiri (1.16.7-aarch64-linux) + racc (~> 1.4) + nokogiri (1.16.7-arm-linux) + racc (~> 1.4) + nokogiri (1.16.7-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86-linux) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-linux) + racc (~> 1.4) + oauth (0.6.2) + snaky_hash (~> 2.0) + version_gem (~> 1.1) orm_adapter (0.5.0) - paperclip (5.3.0) - activemodel (>= 4.2.0) - activesupport (>= 4.2.0) - mime-types - mimemagic (~> 0.3.0) - terrapin (~> 0.6.0) - parallel (1.17.0) - parser (2.6.3.0) - ast (~> 2.4.0) - pg (1.1.4) - pg_search (2.1.7) - activerecord (>= 4.2) - activesupport (>= 4.2) - poltergeist (1.18.1) - capybara (>= 2.1, < 4) - cliver (~> 0.3.1) - websocket-driver (>= 0.2.0) - powerpack (0.1.2) - premailer (1.11.1) + parallel (1.26.3) + parser (3.3.5.1) + ast (~> 2.4.1) + racc + pg (1.5.9) + pg_search (2.3.7) + activerecord (>= 6.1) + activesupport (>= 6.1) + premailer (1.27.0) addressable - css_parser (>= 1.6.0) + css_parser (>= 1.19.0) htmlentities (>= 4.0.0) - premailer-rails (1.10.2) - actionmailer (>= 3, < 6) + premailer-rails (1.12.0) + actionmailer (>= 3) + net-smtp premailer (~> 1.7, >= 1.7.9) - public_suffix (3.1.1) - puma (3.12.4) - rack (2.0.8) + psych (3.3.4) + public_suffix (6.0.1) + puma (3.12.6) + racc (1.8.1) + rack (2.2.10) rack-attack (5.4.2) rack (>= 1.0, < 3) - rack-test (0.6.3) - rack (>= 1.0) - rails (5.0.7.2) - actioncable (= 5.0.7.2) - actionmailer (= 5.0.7.2) - actionpack (= 5.0.7.2) - actionview (= 5.0.7.2) - activejob (= 5.0.7.2) - activemodel (= 5.0.7.2) - activerecord (= 5.0.7.2) - activesupport (= 5.0.7.2) - bundler (>= 1.3.0) - railties (= 5.0.7.2) - sprockets-rails (>= 2.0.0) - rails-assets-EpicEditor (0.2.3) - rails-assets-chartjs (2.8.0) - rails-assets-congress-images-102x125 (0.1.2) - rails-assets-html5shiv (3.7.2) - rails-assets-ionicons (2.0.1) - rails-assets-jquery (2.1.3) - rails-assets-jquery-cookie (1.4.1) - rails-assets-jquery (>= 1.2) - rails-assets-jquery-timeago (1.6.7) - rails-assets-jquery (>= 1.4) - rails-assets-jquery-ujs (1.0.3) - rails-assets-jquery (> 1.8) - rails-assets-lodash (3.7.0) - rails-assets-moment (2.9.0) - rails-assets-respond (1.4.2) - rails-assets-roboto-webfont (0.1.1) - rails-assets-sweetalert (1.0.1) - rails-controller-testing (1.0.4) - actionpack (>= 5.0.1.x) - actionview (>= 5.0.1.x) - activesupport (>= 5.0.1.x) - rails-dev-tweaks (1.2.0) - actionpack (>= 3.1) - railties (>= 3.1) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rack-test (2.1.0) + rack (>= 1.3) + rails (7.0.8.6) + actioncable (= 7.0.8.6) + actionmailbox (= 7.0.8.6) + actionmailer (= 7.0.8.6) + actionpack (= 7.0.8.6) + actiontext (= 7.0.8.6) + actionview (= 7.0.8.6) + activejob (= 7.0.8.6) + activemodel (= 7.0.8.6) + activerecord (= 7.0.8.6) + activestorage (= 7.0.8.6) + activesupport (= 7.0.8.6) + bundler (>= 1.15.0) + railties (= 7.0.8.6) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.2.0) - loofah (~> 2.2, >= 2.2.2) - rails_12factor (0.0.3) - rails_serve_static_assets - rails_stdout_logging - rails_response_headers (0.1.0) - rails_serve_static_assets (0.0.5) - rails_stdout_logging (0.0.5) - railties (5.0.7.2) - actionpack (= 5.0.7.2) - activesupport (= 5.0.7.2) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.0.8.6) + actionpack (= 7.0.8.6) + activesupport (= 7.0.8.6) method_source - rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) - rainbow (3.0.0) - rake (12.3.3) - rb-fchange (0.0.6) - ffi - rb-fsevent (0.10.3) - rb-inotify (0.10.0) + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rainbow (3.1.1) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) ffi (~> 1.0) - rdoc (6.1.1) + rdoc (6.3.4.1) react-rails (1.11.0) babel-transpiler (>= 0.7.0) connection_pool execjs railties (>= 3.2) tilt - redcarpet (3.4.0) - referer-parser (0.3.0) - regexp_parser (1.6.0) - request_store (1.4.1) - rack (>= 1.4) - responders (3.0.0) - actionpack (>= 5.0) - railties (>= 5.0) - rest-client (2.0.2) + redcarpet (3.6.0) + regexp_parser (2.9.2) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rspec-core (3.8.0) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.3) + rexml (3.3.9) + rouge (4.4.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-rails (3.8.2) - actionpack (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.0) - rubocop (0.52.0) + rspec-support (~> 3.13.0) + rspec-rails (6.1.5) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.1) + rubocop (1.68.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 2.4.0.2, < 3.0) - powerpack (~> 0.1) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.4, < 3.0) + rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - rubocop-github (0.9.0) - rubocop (~> 0.51) - ruby-progressbar (1.10.0) - rubyzip (1.3.0) - safe_yaml (1.0.5) - safely_block (0.2.1) - errbase - sanitize (4.6.6) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.33.0) + parser (>= 3.3.1.0) + rubocop-github (0.20.0) + rubocop (>= 1.37) + rubocop-performance (>= 1.15) + rubocop-rails (>= 2.17) + rubocop-performance (1.22.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.27.0) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.52.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (1.13.0) + ruby-vips (2.2.2) + ffi (~> 1.12) + logger + rubyzip (2.3.2) + safely_block (0.4.1) + sanitize (6.1.3) crass (~> 1.0.2) - nokogiri (>= 1.4.4) - nokogumbo (~> 1.4) + nokogiri (>= 1.12.0) sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.0.7) - railties (>= 4.0.0, < 6) + sass-rails (5.0.8) + railties (>= 5.2.0) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sassc (2.0.1) + sassc (2.4.0) ffi (~> 1.9) - rake - sdoc (1.0.0) + sdoc (2.6.1) rdoc (>= 5.0) - select2-rails (4.0.3) - thor (~> 0.14) - selenium-webdriver (3.142.4) - childprocess (>= 0.5, < 3.0) - rubyzip (~> 1.2, >= 1.2.2) - sentry-raven (0.15.6) - faraday (>= 0.7.6) - sprockets (3.7.2) + select2-rails (4.0.13) + selenium-devtools (0.130.0) + selenium-webdriver (~> 4.2) + selenium-webdriver (4.26.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + sentry-raven (3.1.2) + faraday (>= 1.0) + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) + sprockets (3.7.5) + base64 concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-image_compressor (0.3.0) - sprockets - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - terrapin (0.6.0) - climate_control (>= 0.0.3, < 1.0) - thor (0.20.3) - thread_safe (0.3.6) - tilt (2.0.9) - tzinfo (1.2.5) - thread_safe (~> 0.1) - uglifier (4.1.20) + ssrf_filter (1.1.2) + thor (1.3.2) + tilt (2.4.0) + timeout (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uglifier (4.2.1) execjs (>= 0.3.0, < 3) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.6) - unicode-display_width (1.5.0) - user_agent_parser (2.6.0) - uuidtools (2.1.5) - warden (1.2.4) - rack (>= 1.0) - webdrivers (4.1.2) - nokogiri (~> 1.6) - rubyzip (~> 1.0) - selenium-webdriver (>= 3.0, < 4.0) - webmock (2.3.2) - addressable (>= 2.3.6) + unicode-display_width (2.6.0) + uri (0.13.1) + uuidtools (2.2.0) + version_gem (1.1.4) + warden (1.2.9) + rack (>= 2.0.9) + webmock (3.24.0) + addressable (>= 2.8.0) crack (>= 0.3.2) - hashdiff - webshims-rails (1.16.0) - rails (> 3.1.0) - websocket-driver (0.6.5) + hashdiff (>= 0.4.0, < 2.0.0) + webrick (1.9.0) + websocket (1.2.11) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) + websocket-extensions (0.1.5) whenever (0.11.0) chronic (>= 0.6.3) - will_paginate (3.1.7) - xmlrpc (0.3.0) + will_paginate (3.3.1) + xmlrpc (0.3.3) + webrick xpath (3.2.0) nokogiri (~> 1.8) + zeitwerk (2.7.1) PLATFORMS - ruby + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES - activerecord-session_store (~> 1) - acts_as_paranoid! - ahoy_matey (~> 1.6) - aws-sdk (~> 2.3) - aws-sdk-rails (~> 1) + activerecord-session_store (~> 2.1.0) + acts_as_paranoid (~> 0.7) + ahoy_matey (~> 4.0) + aws-sdk-rails (~> 2) + aws-sdk-s3 (~> 1) better_errors (~> 2) - binding_of_caller (~> 0) bootstrap-daterangepicker-rails (~> 3) bootstrap-sass (~> 3.4) bourbon (~> 3) bundler (>= 1.8.4) byebug - capybara (~> 3.26) + capybara (~> 3) + carrierwave (~> 3.0) chartkick (~> 3) cocoon (~> 1) counter_culture (~> 2.0) - cucumber-rails (= 1.6.0) daemons (~> 1) - database_cleaner (~> 1) delayed_job_active_record (~> 4) - descriptive_statistics (~> 2) devise (~> 4.7) dotenv-rails (~> 2) eff_matomo (~> 0.2.4) ejs (~> 1) email_validator (~> 1) - factory_girl_rails (~> 4) - fast_inserter (~> 0.1) + factory_bot_rails (~> 6.2) + fast_inserter (~> 2.0) fastly (~> 2) - fontello_rails_converter (~> 0) + fog-aws friendly_id (~> 5.0) going_postal (~> 0) gravatar-ultimate (~> 2) @@ -520,17 +593,16 @@ DEPENDENCIES http_accept_language (~> 2) invisible_captcha (~> 0) iso_country_codes (~> 0) - jbuilder (~> 1.2) + jbuilder (~> 2) nokogiri (~> 1) oauth (~> 0) - paperclip (~> 5.2) pg (~> 1.1) - pg_search - poltergeist (~> 1) + pg_search (~> 2) premailer-rails (~> 1) + psych (< 4) puma (~> 3) rack-attack (~> 5) - rails (~> 5.0) + rails (~> 7.0.0) rails-assets-EpicEditor (~> 0)! rails-assets-chartjs (~> 2)! rails-assets-congress-images-102x125! @@ -546,34 +618,27 @@ DEPENDENCIES rails-assets-roboto-webfont (~> 0)! rails-assets-sweetalert (= 1.0.1)! rails-controller-testing - rails-dev-tweaks (~> 1.1) - rails_12factor - rails_response_headers (~> 0) - rb-fchange (~> 0) - rb-fsevent (~> 0) - rb-inotify (~> 0) + rails_response_headers! react-rails (~> 1) redcarpet (~> 3) rest-client (~> 2) - rspec-core (~> 3) - rspec-rails (~> 3) - rubocop (= 0.52.0) - rubocop-github (= 0.9.0) - sanitize (~> 4) - sass-rails (~> 5.0) + rspec-rails (~> 6.1) + rubocop + rubocop-github (~> 0.16) + rubocop-performance + rubocop-rails + sanitize (~> 6) + sass-rails (< 5.1) sdoc select2-rails - selenium-webdriver (~> 3) - sentry-raven (~> 0.15) - sprockets-image_compressor (~> 0) + selenium-devtools + sentry-raven (~> 3.1.2) uglifier (>= 1.3.0) - warden (= 1.2.4) - webdrivers (~> 4) - webmock (~> 2) - webshims-rails (~> 1) + uuidtools (~> 2) + webmock (~> 3) whenever (~> 0) will_paginate (~> 3.0) - xmlrpc + xmlrpc (~> 0.3) BUNDLED WITH - 2.0.1 + 2.5.22 diff --git a/README.md b/README.md index 526985ac8..94891733d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,10 @@ Follow these instructions to run the Action Center using Docker (recommended). T * Allows users to submit e-messages to congress * [Call Congress](https://github.com/EFForg/call-congress) url and API key * Connects calls between citizens and their congress person using the Twilio API +* [Google Civic Information API](https://developers.google.com/civic-information) url and API key + * Representative information powered by the Civic Information API + * We use this when we need to give a user the ability to find their representatives to complete a state-level email action + * Some key limitations: https://developers.google.com/civic-information/docs/data_guidelines?hl=en e.g. "Developer’s using the API should make every effort to ensure all users are met with the same experience. We do not allow holdbacks, A/B testing, or similar experiments." ## Using the Action Center diff --git a/Rakefile b/Rakefile index 506551c8a..4afe39994 100644 --- a/Rakefile +++ b/Rakefile @@ -1,14 +1,14 @@ # 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 File.expand_path("../config/application", __FILE__) +require File.expand_path("config/application", __dir__) Actioncenter::Application.load_tasks -if %w(development test).include? Rails.env +if %w[development test].include? Rails.env require "rubocop/rake_task" RuboCop::RakeTask.new task(:default).clear - task default: [:sass_lint, :rubocop, :spec, :cucumber] + task default: %i[sass_lint rubocop spec cucumber] end diff --git a/public/images/email-top-banner.png b/app/assets/images/email-top-banner.png similarity index 100% rename from public/images/email-top-banner.png rename to app/assets/images/email-top-banner.png diff --git a/public/images/share-on-email-thin.png b/app/assets/images/share-on-email-thin.png similarity index 100% rename from public/images/share-on-email-thin.png rename to app/assets/images/share-on-email-thin.png diff --git a/public/images/share-on-fb-thin.png b/app/assets/images/share-on-fb-thin.png similarity index 100% rename from public/images/share-on-fb-thin.png rename to app/assets/images/share-on-fb-thin.png diff --git a/public/images/share-on-fb.png b/app/assets/images/share-on-fb.png similarity index 100% rename from public/images/share-on-fb.png rename to app/assets/images/share-on-fb.png diff --git a/public/images/share-on-g-thin.png b/app/assets/images/share-on-g-thin.png similarity index 100% rename from public/images/share-on-g-thin.png rename to app/assets/images/share-on-g-thin.png diff --git a/public/images/share-on-g.png b/app/assets/images/share-on-g.png similarity index 100% rename from public/images/share-on-g.png rename to app/assets/images/share-on-g.png diff --git a/public/images/share-on-tw-thin.png b/app/assets/images/share-on-tw-thin.png similarity index 100% rename from public/images/share-on-tw-thin.png rename to app/assets/images/share-on-tw-thin.png diff --git a/public/images/share-on-tw.png b/app/assets/images/share-on-tw.png similarity index 100% rename from public/images/share-on-tw.png rename to app/assets/images/share-on-tw.png diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index a3cf8d238..118f8643c 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -8,6 +8,7 @@ //= require admin/gallery //= require admin/action_pages //= require admin/action_pages/petition-targets +//= require admin/action_pages/email //= require admin/analytics //= require_tree ./admin/components diff --git a/app/assets/javascripts/admin/action_pages/email.js b/app/assets/javascripts/admin/action_pages/email.js new file mode 100644 index 000000000..fdb5167cc --- /dev/null +++ b/app/assets/javascripts/admin/action_pages/email.js @@ -0,0 +1,15 @@ +$(document).ready(function() { + var stateLevelTarget = $("#action_page_email_campaign_attributes_state"); + var stateLevelTargetSelection = $("#state-level-target-selection"); + + if (stateLevelTarget.val() === "") + stateLevelTargetSelection.hide(); + + stateLevelTarget.on("change", function() { + if (stateLevelTarget.val() !== "") + stateLevelTargetSelection.show(); + else + stateLevelTargetSelection.hide(); + }); +}); + diff --git a/app/assets/javascripts/application/gallery.js b/app/assets/javascripts/application/gallery.js index 92ead348d..1e0699e6a 100644 --- a/app/assets/javascripts/application/gallery.js +++ b/app/assets/javascripts/application/gallery.js @@ -24,9 +24,9 @@ $(function() { }); } - attachGalleryImage('#image-gallery', '#action_page_featured_image', '#attached-featured_image'); - attachGalleryImage('#image-gallery', '#action_page_background_image', '#attached-background_image'); - attachGalleryImage('#image-gallery', '#action_page_og_image', '#attached-og_image'); + attachGalleryImage('#image-gallery', '#action_page_remote_featured_image_url', '#attached-featured_image'); + attachGalleryImage('#image-gallery', '#action_page_remote_background_image_url', '#attached-background_image'); + attachGalleryImage('#image-gallery', '#action_page_remote_og_image_url', '#attached-og_image'); $('.tweet-target').each(function(i, target) { attachGalleryImage('#image-gallery', $(target).find('.image-input'), target); diff --git a/app/assets/javascripts/application/tools/congress_message.js b/app/assets/javascripts/application/tools/congress_message.js index c1c155291..7539e0b33 100644 --- a/app/assets/javascripts/application/tools/congress_message.js +++ b/app/assets/javascripts/application/tools/congress_message.js @@ -105,23 +105,4 @@ $(document).on("ready", function() { e.preventDefault(); $('#customize-message .notice').addClass('down'); }); - - function show_progress_bars() { - $(".progress-striped").show(); - $("#tools :submit").hide(); - $("#tools input,textarea,button,select", $(this)).attr("disabled", "disabled"); - } - - function show_error(error, form) { - $(".progress-striped").hide(); - form.find(":submit").show(); - form.find(".alert-danger").remove(); - $("#errors").append($('
').text(error)); - $("#tools input,textarea,button,select", form).removeAttr("disabled"); - } - - function update_tabs(from, to) { - $(".page-indicator div.page" + from).removeClass('active'); - $(".page-indicator div.page" + to).addClass('active'); - } }); diff --git a/app/assets/javascripts/application/tools/email.js.erb b/app/assets/javascripts/application/tools/email.js.erb index 7159ed624..9a240425a 100644 --- a/app/assets/javascripts/application/tools/email.js.erb +++ b/app/assets/javascripts/application/tools/email.js.erb @@ -8,8 +8,29 @@ $(document).on('ready', function() { dnt = true; }, 0); }); + $('#target-email button').on('click', function() { $('.thank-you').show(); $('#email-tool').hide(); }); + + $(".state-rep-email").hide(); + + $("form.state-rep-lookup").on("ajax:complete", function(e, xhr, status) { + var $form = $(this); + var data = xhr.responseJSON; + $('.state-rep-lookup').hide(); + $('.state-reps').replaceWith(data.content); + if (status == "success") { + $form.remove(); + $(".state-reps").html(data); + + if ($("#action-content").length) { + $(window).scrollTop( $("#action-content").offset().top ); // go to top of page if on action center site + } + update_tabs(1, 2); + } else { + show_error("Something went wrong. Please try again later.", $form); + } + }); }); diff --git a/app/assets/javascripts/application/tools/shared.js b/app/assets/javascripts/application/tools/shared.js new file mode 100644 index 000000000..97d07e676 --- /dev/null +++ b/app/assets/javascripts/application/tools/shared.js @@ -0,0 +1,18 @@ +function show_progress_bars() { + $(".progress-striped").show(); + $("#tools :submit").hide(); + $("#tools input,textarea,button,select", $(this)).attr("disabled", "disabled"); +} + +function show_error(error, form) { + $(".progress-striped").hide(); + form.find(":submit").show(); + form.find(".alert-danger").remove(); + $("#errors").append($('
').text(error)); + $("#tools input,textarea,button,select", form).removeAttr("disabled"); +} + +function update_tabs(from, to) { + $(".page-indicator div.page" + from).removeClass('active'); + $(".page-indicator div.page" + to).addClass('active'); +} \ No newline at end of file diff --git a/app/assets/stylesheets/action_page.scss b/app/assets/stylesheets/action_page.scss index 6ace3a726..7a30dde8c 100644 --- a/app/assets/stylesheets/action_page.scss +++ b/app/assets/stylesheets/action_page.scss @@ -1236,25 +1236,6 @@ html.js #affiliations { } } -.partner-logos { - display: flex; - align-items: center; - justify-content: space-evenly; - flex-wrap: wrap; - @media screen and (min-width: $lg) { - flex-wrap: nowrap; - } - img { - width: 100%; - height: auto; - max-width: 200px; - padding: 1em; - @media screen and (min-width: $md) { - padding: 0 1em; - } - } -} - #congress-message-create { position: relative; diff --git a/app/controllers/action_page_controller.rb b/app/controllers/action_page_controller.rb index 8daf812f5..d1612a1aa 100644 --- a/app/controllers/action_page_controller.rb +++ b/app/controllers/action_page_controller.rb @@ -5,15 +5,15 @@ class ActionPageController < ApplicationController :protect_unpublished, :redirect_to_specified_url, :redirect_from_archived_to_active_action, - only: [:show, :show_by_institution, :embed_iframe, - :signature_count, :filter] + only: %i[show show_by_institution embed_iframe + signature_count filter] before_action :redirect_to_cannonical_slug, only: [:show] - before_action :set_institution, only: [:show_by_institution, :filter] - before_action :set_action_display_variables, only: [:show, - :show_by_institution, - :embed_iframe, - :signature_count, - :filter] + before_action :set_institution, only: %i[show_by_institution filter] + before_action :set_action_display_variables, only: %i[show + show_by_institution + embed_iframe + signature_count + filter] skip_before_action :verify_authenticity_token, only: :embed @@ -26,9 +26,9 @@ def show end def index - @actionPages = ActionPage.where(published: true, archived: false, victory: false). - paginate(page: params[:page], per_page: 9). - order(created_at: :desc) + @actionPages = ActionPage.where(published: true, archived: false, victory: false) + .paginate(page: params[:page], per_page: 9) + .order(created_at: :desc) @actionPages = @actionPages.categorized(params[:category]) if params[:category].present? @@ -51,10 +51,10 @@ def embed_iframe def signature_count @actionPage = ActionPage.friendly.find(params[:id]) - if petition = @actionPage.petition - render text: petition.signatures.count + if @actionPage.petition + render body: @actionPage.petition.signatures.count else - render text: "0" + render body: "0" end end @@ -83,18 +83,14 @@ def protect_unpublished end def redirect_to_specified_url - if @actionPage.enable_redirect - redirect_to @actionPage.redirect_url, status: 301 - end + redirect_to @actionPage.redirect_url, status: 301, allow_other_host: true if @actionPage.enable_redirect end def redirect_from_archived_to_active_action - if @actionPage.redirect_from_archived_to_active_action? - # Users can access actions they've taken in the past as a historical record - unless current_user and (current_user.taken_action? @actionPage or current_user.admin?) - redirect_to @actionPage.active_action_page_for_redirect - end - end + return unless @actionPage.redirect_from_archived_to_active_action? + return if current_user&.can_view_archived?(@actionPage) + + redirect_to @actionPage.active_action_page_for_redirect end def redirect_to_cannonical_slug diff --git a/app/controllers/admin/action_pages_controller.rb b/app/controllers/admin/action_pages_controller.rb index 5045b8f7a..7f11f6eed 100644 --- a/app/controllers/admin/action_pages_controller.rb +++ b/app/controllers/admin/action_pages_controller.rb @@ -2,34 +2,32 @@ class Admin::ActionPagesController < Admin::ApplicationController include DateRange include ActionPageDisplay - before_action :set_action_page, only: [ - :edit, - :update, - :destroy, - :events, - :events_table, - :duplicate, - :preview, - :status, - :edit_partners + before_action :set_action_page, only: %i[ + edit + update + destroy + events + events_table + duplicate + preview + status + edit_partners ] - before_action :set_petition_targets, only: %i(new edit duplicate) - before_action :set_partners, only: %i(new edit duplicate) - before_action :set_source_files, only: %i(new edit create update duplicate) + before_action :set_petition_targets, only: %i[new edit duplicate] + before_action :set_partners, only: %i[new edit duplicate] + before_action :set_source_files, only: %i[new edit create update duplicate] - after_action :purge_cache, only: [:update, :publish] + after_action :purge_cache, only: %i[update publish] allow_collaborators_to :index, :edit def index - @categories = Category.all.order(:title) + @categories = Category.order(:title) @authors = User.authors.order(:last_name) @actionPages = filter_action_pages - if request.xhr? - render partial: "admin/action_pages/index" - end + render partial: "admin/action_pages/index" if request.xhr? end def new @@ -64,26 +62,19 @@ def edit @actionPage.email_campaign ||= EmailCampaign.new @actionPage.congress_message_campaign ||= CongressMessageCampaign.new 10.times { @actionPage.affiliation_types.build } - if @actionPage.enable_petition && @actionPage.petition.enable_affiliations - @target_category = @actionPage.institutions.first.category - end + @target_category = @actionPage.institutions.first.category if @actionPage.enable_petition && @actionPage.petition.enable_affiliations end - def status - end + def status; end def edit_partners @partners = Partner.order(name: :desc) end def update - @actionPage.background_image = nil if params[:destroy_background_image] - @actionPage.featured_image = nil if params[:destroy_featured_image] - @actionPage.og_image = nil if params[:destroy_og_image] - - @actionPage.update_attributes(action_page_params) + @actionPage.update(action_page_params) if (institutions_params[:reset] && institutions_params[:reset] == "1") || - (institutions_params[:category] && @actionPage.institutions.empty?) + (institutions_params[:category] && @actionPage.institutions.empty?) ActionInstitution.add(action_page: @actionPage, **institutions_params.to_h.symbolize_keys) end @@ -96,12 +87,11 @@ def destroy redirect_to admin_action_pages_path, notice: "Deleted action page: #{@actionPage.title}" end - def preview @actionPage.attributes = action_page_params if @actionPage.enable_redirect - redirect_to @actionPage.redirect_url, status: 301 + redirect_to @actionPage.redirect_url, status: 301 return end @@ -132,9 +122,9 @@ def events end format.json do render json: @events.chart_data( - type: params[:type], - range: @start_date..@end_date - ) + type: params[:type], + range: @start_date..@end_date + ) end end end @@ -144,9 +134,7 @@ def events_table @events = @actionPage.events.in_range(@start_date, @end_date) @counts = @events.table_data @summary = @events.summary - if @actionPage.enable_congress_message? - @fills = @actionPage.congress_message_campaign.date_fills(@start_date, @end_date) - end + @fills = @actionPage.congress_message_campaign.date_fills(@start_date, @end_date) if @actionPage.enable_congress_message? end def homepage @@ -183,25 +171,26 @@ def set_partners def action_page_params params.require(:action_page).permit( - :title, :summary, :description, :category_id, :related_content_url, :featured_image, + :title, :summary, :description, :category_id, :related_content_url, :remote_featured_image_url, :enable_call, :enable_petition, :enable_email, :enable_tweet, - :enable_congress_message, :og_title, :og_image, :share_message, :published, + :enable_congress_message, :og_title, :remote_og_image_url, :share_message, :published, :call_campaign_id, :what_to_say, :redirect_url, :email_text, :enable_redirect, :victory, :victory_message, :archived_redirect_action_page_id, :archived, :status, partner_ids: [], - action_page_images_attributes: [:id, :action_page_image], - call_campaign_attributes: [:id, :title, :message, :call_campaign_id], - petition_attributes: [:id, :title, :description, :goal, :enable_affiliations], - affiliation_types_attributes: [:id, :name], + action_page_images_attributes: %i[id action_page_image], + call_campaign_attributes: %i[id title message call_campaign_id], + petition_attributes: %i[id title description goal enable_affiliations], + affiliation_types_attributes: %i[id name], tweet_attributes: [ :id, :target, :target_house, :target_senate, :message, :cta, :bioguide_id, - tweet_targets_attributes: [:id, :_destroy, :twitter_id, :image] + { tweet_targets_attributes: %i[id _destroy twitter_id image] } ], - email_campaign_attributes: [ - :id, :message, :subject, :target_house, :target_senate, :target_email, - :email_addresses, :target_bioguide_id, :bioguide_id, :alt_text_email_your_rep, - :alt_text_look_up_your_rep, :alt_text_extra_fields_explain, :topic_category_id, - :alt_text_look_up_helper, :alt_text_customize_message_helper, :campaign_tag + email_campaign_attributes: %i[ + id message subject state target_state_lower_chamber target_state_upper_chamber + target_governor target_email email_addresses target_bioguide_id + bioguide_id alt_text_email_your_rep alt_text_look_up_your_rep + alt_text_extra_fields_explain topic_category_id alt_text_look_up_helper + alt_text_customize_message_helper campaign_tag ], congress_message_campaign_attributes: [ :id, :message, :subject, :target_house, :target_senate, { target_bioguide_list: [] }, @@ -210,18 +199,19 @@ def action_page_params :alt_text_customize_message_helper, :campaign_tag, :enable_customization_notice ], - partnerships_attributes: [:id, :enable_mailings] + partnerships_attributes: %i[id enable_mailings] ) end def institutions_params - return {} unless params.has_key? :institutions - params.require(:institutions).permit(%i(category reset)) + return {} unless params.key? :institutions + + params.require(:institutions).permit(%i[category reset]) end def filter_params params.permit(:q, :date_range, :utf8, - action_filters: %i(type status author category)) + action_filters: %i[type status author category]) end def purge_cache diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 3eedde808..fee652024 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -6,25 +6,19 @@ def manifest self.class.manifest || "admin" end - protected - + # FLAG_AS_UNUSED def self.allow_collaborators_to(*actions) skip_before_action :must_be_admin, only: actions before_action :must_be_admin_or_collaborator, only: actions end def must_be_admin - unless user_signed_in? && current_user.admin? - raise ActiveRecord::RecordNotFound - end + raise ActiveRecord::RecordNotFound unless user_signed_in? && current_user.admin? end def must_be_admin_or_collaborator - unless user_signed_in? && (current_user.admin? || current_user.collaborator?) - raise ActiveRecord::RecordNotFound - end + raise ActiveRecord::RecordNotFound unless user_signed_in? && (current_user.admin? || current_user.collaborator?) end - def images - end + def images; end end diff --git a/app/controllers/admin/congress_message_campaigns_controller.rb b/app/controllers/admin/congress_message_campaigns_controller.rb index 492ac7fd0..78360652d 100644 --- a/app/controllers/admin/congress_message_campaigns_controller.rb +++ b/app/controllers/admin/congress_message_campaigns_controller.rb @@ -3,8 +3,7 @@ class Admin::CongressMessageCampaignsController < Admin::ApplicationController allow_collaborators_to :congress_tabulation, :staffer_report - def congress_tabulation - end + def congress_tabulation; end def staffer_report @bioguide_id = params[:bioguide_id] diff --git a/app/controllers/admin/events_controller.rb b/app/controllers/admin/events_controller.rb index 3bd605300..97b29d105 100644 --- a/app/controllers/admin/events_controller.rb +++ b/app/controllers/admin/events_controller.rb @@ -6,12 +6,12 @@ def index @events = Ahoy::Event.all.in_range(@start_date, @end_date) respond_to do |format| format.html { @summary = @events.summary } - format.json { + format.json do render json: @events.chart_data( - type: params[:type], - range: @start_date..@end_date - ) - } + type: params[:type], + range: @start_date..@end_date + ) + end end end end diff --git a/app/controllers/admin/images_controller.rb b/app/controllers/admin/images_controller.rb index d89edf4a7..774771fc9 100644 --- a/app/controllers/admin/images_controller.rb +++ b/app/controllers/admin/images_controller.rb @@ -1,4 +1,3 @@ class Admin::ImagesController < Admin::ApplicationController - def index - end + def index; end end diff --git a/app/controllers/admin/institutions_controller.rb b/app/controllers/admin/institutions_controller.rb index 6b6c0a563..ca2783305 100644 --- a/app/controllers/admin/institutions_controller.rb +++ b/app/controllers/admin/institutions_controller.rb @@ -1,13 +1,11 @@ class Admin::InstitutionsController < Admin::ApplicationController - before_action :set_institution, only: %i(destroy edit update) - before_action :set_categories, only: %i(new edit upload index) + before_action :set_institution, only: %i[destroy edit update] + before_action :set_categories, only: %i[new edit upload index] def index - @institutions = Institution.includes(:action_pages).all.order(created_at: :desc) + @institutions = Institution.includes(:action_pages).order(created_at: :desc) @institutions = @institutions.search(params[:q]) if params[:q].present? - if params[:category].present? && params[:category] != "All" - @institutions = @institutions.where(category: params[:category]) - end + @institutions = @institutions.where(category: params[:category]) if params[:category].present? && params[:category] != "All" @institutions = @institutions.paginate(page: params[:page], per_page: 20) end @@ -42,11 +40,7 @@ def import if names.empty? redirect_to action: "upload", notice: "Import failed. Please check CSV formatting" else - category = if import_params[:new_category].blank? - import_params[:category] - else - import_params[:new_category] - end + category = import_params[:new_category].presence || import_params[:category] Institution.delay.import(category, names) redirect_to action: "index", notice: "Successfully imported #{names.length} targets" end diff --git a/app/controllers/admin/partners_controller.rb b/app/controllers/admin/partners_controller.rb index d3bb7080b..3fce9ce28 100644 --- a/app/controllers/admin/partners_controller.rb +++ b/app/controllers/admin/partners_controller.rb @@ -1,7 +1,7 @@ class Admin::PartnersController < Admin::ApplicationController layout "admin" - before_action :set_partner, only: %i(edit update show destroy) + before_action :set_partner, only: %i[edit update show destroy] # GET /partners # GET /partners.json @@ -22,16 +22,15 @@ def create respond_to do |format| if @partner.save format.html { redirect_to @partner, notice: "Partner was successfully created." } - format.json { render "show", status: :created, location: @partner } + format.json { render "show", status: 201, location: @partner } else format.html { render "new" } - format.json { render json: @partner.errors, status: :unprocessable_entity } + format.json { render json: @partner.errors, status: 422 } end end end - def edit - end + def edit; end def update if @partner.update(partner_params) diff --git a/app/controllers/admin/petitions_controller.rb b/app/controllers/admin/petitions_controller.rb index 331e767dd..1fde4420d 100644 --- a/app/controllers/admin/petitions_controller.rb +++ b/app/controllers/admin/petitions_controller.rb @@ -1,5 +1,5 @@ -include PetitionHelper class Admin::PetitionsController < Admin::ApplicationController + include PetitionHelper before_action :set_petition allow_collaborators_to :show, :destroy_signatures @@ -25,8 +25,8 @@ def affiliation_csv signatures = @petition.signatures if params[:institution_id].present? - signatures = signatures.joins(affiliations: :institution). - where(institutions: { id: params[:institution_id] }) + signatures = signatures.joins(affiliations: :institution) + .where(institutions: { id: params[:institution_id] }) end send_data signatures.to_affiliation_csv, @@ -36,9 +36,7 @@ def affiliation_csv def destroy_signatures @petition.signatures.where(id: params[:signature_ids]).delete_all - if params[:page].to_i > filtered_signatures.total_pages - params[:page] = filtered_signatures.total_pages - end + params[:page] = filtered_signatures.total_pages if params[:page].to_i > filtered_signatures.total_pages redirect_to admin_action_page_petition_path(@petition.action_page, @petition, search_params) @@ -51,10 +49,10 @@ def set_petition end def filtered_signatures - @petition.signatures. - filter(params[:query]). - order(created_at: :desc). - paginate(page: params[:page], per_page: params[:per_page] || 10) + @petition.signatures + .search(params[:query]) + .order(created_at: :desc) + .paginate(page: params[:page], per_page: params[:per_page] || 10) end def search_params diff --git a/app/controllers/admin/s3_uploads_controller.rb b/app/controllers/admin/s3_uploads_controller.rb index 78bca4f4b..5e80050c1 100644 --- a/app/controllers/admin/s3_uploads_controller.rb +++ b/app/controllers/admin/s3_uploads_controller.rb @@ -2,11 +2,11 @@ class Admin::S3UploadsController < Admin::ApplicationController # GET /admin/source_files # GET /admin/source_files.json def index - if params[:f].present? - source_files = SourceFile.where("LOWER(file_name) LIKE ?", "%#{params[:f]}%".downcase) - else - source_files = SourceFile.limit(3) - end + source_files = if params[:f].present? + SourceFile.where("LOWER(file_name) LIKE ?", "%#{params[:f]}%".downcase) + else + SourceFile.limit(3) + end source_files = source_files.order(created_at: :desc) @@ -22,15 +22,15 @@ def create @source_file = SourceFile.new(parameters) respond_to do |format| if @source_file.save - format.html { + format.html do render json: @source_file.to_jq_upload, - content_type: "text/html", - layout: false - } - format.json { render json: @source_file.to_jq_upload, status: :created } + content_type: "text/html", + layout: false + end + format.json { render json: @source_file.to_jq_upload, status: 201 } else format.html { render "new" } - format.json { render json: @source_file.errors, status: :unprocessable_entity } + format.json { render json: @source_file.errors, status: 422 } end end end @@ -52,7 +52,7 @@ def destroy # for /admin/action_page/new # GET /admin/source_files/generate_key def generate_key - uid = SecureRandom.uuid.gsub(/-/, "") + uid = SecureRandom.uuid.delete("-") render json: { key: "uploads/#{uid}/#{params[:filename]}", diff --git a/app/controllers/admin/topic_categories_controller.rb b/app/controllers/admin/topic_categories_controller.rb index 7242e9818..111bd1d8c 100644 --- a/app/controllers/admin/topic_categories_controller.rb +++ b/app/controllers/admin/topic_categories_controller.rb @@ -14,18 +14,16 @@ def create end def destroy - begin - TopicCategory.destroy(params[:id]) - render json: { id: params[:id] } - rescue => e - render text: e.message, status: 500 - end + TopicCategory.destroy(params[:id]) + render json: { id: params[:id] } + rescue StandardError => e + render body: e.message, status: 500 end def update topic_category = TopicCategory.find(params[:id]) - if topic_category.update_attributes(topic_category_params) + if topic_category.update(topic_category_params) render json: topic_category else render json: topic_category.errors, status: 500 diff --git a/app/controllers/admin/topic_sets_controller.rb b/app/controllers/admin/topic_sets_controller.rb index 6ca7cb6db..7dab61a0b 100644 --- a/app/controllers/admin/topic_sets_controller.rb +++ b/app/controllers/admin/topic_sets_controller.rb @@ -1,16 +1,14 @@ class Admin::TopicSetsController < Admin::ApplicationController def index - topic_sets = TopicSet.all.order(:tier) + topic_sets = TopicSet.order(:tier) render json: topic_sets end def destroy - begin - TopicSet.destroy(params[:id]) - render json: { id: params[:id] } - rescue => e - render text: e.message, status: 500 - end + TopicSet.destroy(params[:id]) + render json: { id: params[:id] } + rescue StandardError => e + render body: e.message, status: 500 end def create @@ -28,7 +26,7 @@ def create def update topic_set = TopicSet.find(params[:id]) - if topic_set.update_attributes(topic_set_params) + if topic_set.update(topic_set_params) render json: topic_set else render json: topic_set.errors, status: 500 diff --git a/app/controllers/admin/topics_controller.rb b/app/controllers/admin/topics_controller.rb index a635cbb3f..ba41bb634 100644 --- a/app/controllers/admin/topics_controller.rb +++ b/app/controllers/admin/topics_controller.rb @@ -2,12 +2,10 @@ class Admin::TopicsController < Admin::ApplicationController layout "admin" def destroy - begin - Topic.destroy(params[:id]) - render json: { id: params[:id] } - rescue => e - render text: e.message, status: 500 - end + Topic.destroy(params[:id]) + render json: { id: params[:id] } + rescue StandardError => e + render body: e.message, status: 500 end def create diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index eb37ff0bb..38453b79f 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -6,7 +6,7 @@ def index def update user = User.find(params[:id]) - if user.update_attributes(user_params) + if user.update(user_params) flash[:notice] = "#{user.email} was updated" else flash[:error] = "Could not update #{user.email}" diff --git a/app/controllers/ahoy_controller.rb b/app/controllers/ahoy_controller.rb index 83a56264c..8a6d70b83 100644 --- a/app/controllers/ahoy_controller.rb +++ b/app/controllers/ahoy_controller.rb @@ -1,15 +1,14 @@ class AhoyController < ApplicationController before_action :set_ahoy_cookies before_action :track_ahoy_visit - before_action :set_ahoy_request_store def visit action_type = params.require(:action_type) action_page_id = params.require(:action_page_id) ahoy.track "View", - { type: "action", actionType: action_type, actionPageId: action_page_id }, - action_page_id: action_page_id + { type: "action", actionType: action_type, actionPageId: action_page_id }, + action_page_id: action_page_id send_data image_asset, content_type: "image/gif" end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0c4c4f5c2..71ebe9d3d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,23 +11,17 @@ class ApplicationController < ActionController::Base before_action :set_locale before_action :user_conditional_logic - skip_before_action :set_ahoy_cookies skip_before_action :track_ahoy_visit - skip_before_action :set_ahoy_request_store def user_conditional_logic - if user_signed_in? - lock_users_with_expired_passwords! unless user_is_being_told_to_reset_pass_or_is_resetting_pass? - end + lock_users_with_expired_passwords! if user_signed_in? && !user_is_being_told_to_reset_pass_or_is_resetting_pass? end # This method seems to check if the request is coming from a domain listed in # `cors_allowed_domains` in application.yml, and if it is, the response gets # a header allowing the requesting domain to use this app's CRUD def cors - if Actioncenter::Application.config.cors_allowed_domains.include? request.env["HTTP_ORIGIN"] or Actioncenter::Application.config.cors_allowed_domains.include? "*" - response.headers["Access-Control-Allow-Origin"] = request.env["HTTP_ORIGIN"] - end + response.headers["Access-Control-Allow-Origin"] = request.env["HTTP_ORIGIN"] if Actioncenter::Application.config.cors_allowed_domains.include?(request.env["HTTP_ORIGIN"]) || Actioncenter::Application.config.cors_allowed_domains.include?("*") end def self.manifest(value = nil) @@ -60,7 +54,7 @@ def user_is_being_told_to_reset_pass_or_is_resetting_pass? def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, - keys: [:record_activity, :subscribe]) + keys: %i[record_activity subscribe]) end def set_locale diff --git a/app/controllers/concerns/action_page_display.rb b/app/controllers/concerns/action_page_display.rb index f31507472..ca926275d 100644 --- a/app/controllers/concerns/action_page_display.rb +++ b/app/controllers/concerns/action_page_display.rb @@ -11,22 +11,25 @@ def set_action_display_variables @congress_message_campaign = @actionPage.congress_message_campaign # Shows a mailing list if no tools enabled - @no_tools = [:tweet, :petition, :call, :email, :congress_message].none? do |tool| + @no_tools = %i[tweet petition call email congress_message].none? do |tool| @actionPage.send "enable_#{tool}".to_sym end set_signatures - if @actionPage.petition and @actionPage.petition.enable_affiliations - @top_institutions = @actionPage.institutions.top(300, first: @institution.try(:id)) + if @actionPage.petition&.enable_affiliations + @top_institutions = [ + @institution, + TopInstitutionsQuery.run(action_page: @actionPage, + limit: 300, + exclude: [@institution]) + ].flatten @institutions = @actionPage.institutions.order(:name) @institution_category = @institutions.first.category end @topic_category = nil - if @email_campaign and !@email_campaign.topic_category.nil? - @topic_category = @email_campaign.topic_category.as_2d_array - end + @topic_category = @email_campaign.topic_category.as_2d_array if @email_campaign && !@email_campaign.topic_category.nil? # Initialize a temporary signature object for form auto-population current_zipcode = params[:zipcode] || current_user.try(:zipcode) @@ -53,18 +56,17 @@ def set_signatures @institution_signature_count = @signatures.pretty_count elsif @petition.enable_affiliations @signatures = @petition.signatures - .includes(affiliations: [:institution, :affiliation_type]) + .includes(affiliations: %i[institution affiliation_type]) else @signatures = @petition.signatures end @signatures = @signatures - .paginate(page: params[:page], per_page: 9) - .order(created_at: :desc) + .paginate(page: params[:page], per_page: 9) + .order(created_at: :desc) @signature_count = @petition.signatures.pretty_count @require_location = !@petition.enable_affiliations end end - end diff --git a/app/controllers/concerns/date_range.rb b/app/controllers/concerns/date_range.rb index ccefc3f4f..eae82f3f3 100644 --- a/app/controllers/concerns/date_range.rb +++ b/app/controllers/concerns/date_range.rb @@ -7,10 +7,9 @@ def set_dates def process_dates(date_range_text: nil, date_text: nil, **_) return parse_date_range(date_range_text) if date_range_text.present? - return [1.month.ago, Time.zone.now] unless date_text.present? - if date_text == "Action lifetime" && @actionPage.present? - return [@actionPage.created_at, Time.zone.now] - end + return [1.month.ago, Time.zone.now] if date_text.blank? + return [@actionPage.created_at, Time.zone.now] if date_text == "Action lifetime" && @actionPage.present? + [parse_time_ago(date_text), Time.zone.now] end @@ -21,12 +20,14 @@ def parse_date_range(date_range_string) # Convert Last X (days|weeks|months) to a time def parse_time_ago(string) _, count, unit = string.split(" ") - return Time.zone.now - 1.month unless %w(days weeks months years).include? unit + return Time.zone.now - 1.month unless %w[days weeks months years].include? unit + Time.zone.now - count.to_i.send(unit) end def date_range_string return "" unless @start_date && @end_date + format = "%Y-%m-%d" "#{@start_date.strftime(format)} - #{@end_date.strftime(format)}" end diff --git a/app/controllers/concerns/logged_invisible_captcha.rb b/app/controllers/concerns/logged_invisible_captcha.rb index 13885594d..a2b48f633 100644 --- a/app/controllers/concerns/logged_invisible_captcha.rb +++ b/app/controllers/concerns/logged_invisible_captcha.rb @@ -3,12 +3,12 @@ module LoggedInvisibleCaptcha def on_spam(options = {}) log_failure - super options + super(options) end def on_timestamp_spam(options = {}) log_failure - super options + super(options) end def log_failure diff --git a/app/controllers/concerns/tooling.rb b/app/controllers/concerns/tooling.rb index e7b0cbef3..3a4605e16 100644 --- a/app/controllers/concerns/tooling.rb +++ b/app/controllers/concerns/tooling.rb @@ -5,17 +5,14 @@ module Tooling def create_partner_subscription return unless @action_page + @action_page.partners.each do |partner| - if params["#{partner.code}_subscribe"] == "1" - Subscription.create(partner_signup_params.merge(partner: partner)) - end + Subscription.create(partner_signup_params.merge(partner: partner)) if params["#{partner.code}_subscribe"] == "1" end end def deliver_thanks_message @email ||= current_user.try(:email) || params[:email] || params.dig(:subscription, :email) - if @email.present? - UserMailer.thanks_message(@email, @action_page, user: @user, name: @name).deliver_now - end + UserMailer.thanks_message(@email, @action_page, user: @user, name: @name).deliver_now if @email.present? end end diff --git a/app/controllers/congress_messages_controller.rb b/app/controllers/congress_messages_controller.rb index 0bc6ab8aa..6b8976753 100644 --- a/app/controllers/congress_messages_controller.rb +++ b/app/controllers/congress_messages_controller.rb @@ -22,7 +22,7 @@ def new end forms, @links = CongressForms::Form.find(bioguide_ids) @defunct_members, @members = @members.partition do |m| - @links.keys.include? m.bioguide_id + @links.key?(m.bioguide_id) end @message = CongressMessage.new_from_lookup(location, @campaign, forms) render partial: "form" @@ -37,16 +37,16 @@ def create else params[:forms][:bioguide_ids] end - @message.forms, _ = CongressForms::Form.find(bioguide_ids) + @message.forms, = CongressForms::Form.find(bioguide_ids) - if EmailValidator.valid?(user_params[:email]) && @message.background_submit(params[:test]) + if EmailValidator.valid?(user_params[:email]) && @message.background_submit(test: params[:test]) @name = user_params[:first_name] # for deliver_thanks_message @email = user_params[:email] # for deliver_thanks_message track_action unless params[:test] deliver_thanks_message unless subscribe_user render partial: "tools/share" else - render plain: I18n.t(:invalid_submission, scope: :congress_forms), status: :bad_request + render plain: I18n.t(:invalid_submission, scope: :congress_forms), status: 400 end end @@ -84,35 +84,33 @@ def partner_signup_params end def update_user - if params[:update_user_data] == "yes" - current_user.update(user_params.except(:email)) - end + current_user.update(user_params.except(:email)) if params[:update_user_data] == "yes" end def subscribe_user create_partner_subscription if params[:subscribe] == "1" - source = "action center congress message :: " + @action_page.title + source = "action center congress message :: #{@action_page.title}" user = User.find_or_initialize_by(email: user_params[:email]) user.attributes = user_params - user.subscribe!(opt_in = false, source = source)["requires_confirmation"] + user.subscribe!(opt_in: false, source: source)["requires_confirmation"] end end def track_action customized_message = params[:message] != @campaign.message ahoy.track "Action", - { type: "action", actionType: "congress_message", actionPageId: params[:action_id], - customizedMessage: customized_message }, - action_page: @action_page + { type: "action", actionType: "congress_message", actionPageId: params[:action_id], + customizedMessage: customized_message }, + action_page: @action_page end def address_not_found - render plain: I18n.t(:address_lookup_failed, scope: :congress_forms), status: :bad_request + render plain: I18n.t(:address_lookup_failed, scope: :congress_forms), status: 400 end def congress_forms_request_failed - render plain: I18n.t(:request_failed, scope: :congress_forms), status: :internal_server_error + render plain: I18n.t(:request_failed, scope: :congress_forms), status: 500 end end diff --git a/app/controllers/partners_controller.rb b/app/controllers/partners_controller.rb index 515f87f8b..ccb77b04d 100644 --- a/app/controllers/partners_controller.rb +++ b/app/controllers/partners_controller.rb @@ -6,9 +6,9 @@ class PartnersController < ApplicationController # GET /partners/1 # GET /partners/1.json def show - @subscriptions = @partner.subscriptions. - paginate(page: params[:page], per_page: 10). - order("id desc") + @subscriptions = @partner.subscriptions + .paginate(page: params[:page], per_page: 10) + .order("id desc") end def csv @@ -24,24 +24,22 @@ def update format.json { head :no_content } else format.html { render "edit" } - format.json { render json: @partner.errors, status: :unprocessable_entity } + format.json { render json: @partner.errors, status: 422 } end end end def add_user - user = User.find_by_email(params[:email]) + user = User.find_by(email: params[:email]) if user.nil? flash[:notice] = "Couldn't find a user by email #{params[:email]}" + elsif user.partner == @partner + flash[:notice] = "That user is already linked to #{@partner.name}" + elsif user.partner.nil? + user.partner = @partner + user.save else - if user.partner.nil? - user.partner = @partner - user.save - elsif user.partner == @partner - flash[:notice] = "That user is already linked to #{@partner.name}" - else - flash[:notice] = "That user is linked to another partner: #{user.partner.name}" - end + flash[:notice] = "That user is linked to another partner: #{user.partner.name}" end redirect_to @partner end @@ -66,9 +64,7 @@ def set_partner def authenticate authenticate_user! - unless current_user.admin? - raise ActiveRecord::RecordNotFound if current_user.partner != @partner - end + raise ActiveRecord::RecordNotFound if !current_user.admin? && (current_user.partner != @partner) end # Never trust parameters from the scary internet, only allow the white list through. diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 1254ffa85..1ea3f7de9 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -21,16 +21,14 @@ def update private def handle_nonunique_email - resource.errors.delete(:email) + resource.errors.delete(:email) unless resource.errors[:email].size > 1 if resource.errors.empty? - existing = User.find_by_email(resource.email) + existing = User.find_by(email: resource.email) existing.send_email_taken_notice # Allow unconfirmed users to set a new password by re-registering. - if !existing.confirmed? - existing.update_attributes(sign_up_params) - end + existing.update(sign_up_params) unless existing.confirmed? if resource.persisted? resource.update_attribute(:unconfirmed_email, account_update_params[:email]) diff --git a/app/controllers/robots_controller.rb b/app/controllers/robots_controller.rb index be90d584c..208100dda 100644 --- a/app/controllers/robots_controller.rb +++ b/app/controllers/robots_controller.rb @@ -1,9 +1,9 @@ class RobotsController < ApplicationController def show - if Rails.env.development? or Rails.application.secrets.enable_basic_auth == "true" - render text: "User-agent: *\nDisallow: /" + if Rails.env.development? || (Rails.application.secrets.enable_basic_auth == "true") + render body: "User-agent: *\nDisallow: /" else - render text: "" + render body: "" end end @@ -11,9 +11,9 @@ def show # for load balancing/ database connection detecting def heartbeat if User.count >= 0 - render text: "Application Heart Beating OK" + render body: "Application Heart Beating OK" else - render text: "There's something odd about the database, probably disconnected...", status: 500 + render body: "There's something odd about the database, probably disconnected...", status: 500 end end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index c86631eb7..e2f0fc452 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -3,7 +3,7 @@ class SessionsController < Devise::SessionsController before_action :unset_logged_in, only: :destroy def set_logged_in - if (user_signed_in?) + if user_signed_in? # Sets a "permanent" cookie (which expires in 20 years from now). # This is exclusively used to never cache content for logged in users cookies.permanent[:logged_in] = "I <3 EFF" diff --git a/app/controllers/smarty_streets_controller.rb b/app/controllers/smarty_streets_controller.rb index 3377aed18..6645415db 100644 --- a/app/controllers/smarty_streets_controller.rb +++ b/app/controllers/smarty_streets_controller.rb @@ -9,10 +9,13 @@ def street_address render json: get_data_on_address_zip(params), status: 200 end - # This endpoint appears unused - def suggest - render json: get_suggestions_for_address(params), status: 200 - end + # This endpoint appears unused (TheNotary 1/16) + # Lets remove it and see if anything breaks! (jparr 4/23) + # todo: remove if not used + + # def suggest + # render json: get_suggestions_for_address(params), status: 200 + # end private @@ -38,10 +41,8 @@ def authorize_query(params) end def proxy_request(url) - begin - return RestClient.get url, accept: :json, 'X-Include-Invalid': "true" - rescue => e - logger.error e - end + RestClient.get url, accept: :json, 'X-Include-Invalid': "true" + rescue StandardError => e + logger.error e end end diff --git a/app/controllers/sns_controller.rb b/app/controllers/sns_controller.rb index 00eb8ffb5..191704e3c 100644 --- a/app/controllers/sns_controller.rb +++ b/app/controllers/sns_controller.rb @@ -40,7 +40,7 @@ def set_context def set_message body = JSON.parse(request.body.read) - return JSON.parse(body["Message"]) + JSON.parse(body["Message"]) end def log_request diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb index 95dcbe585..7554a5200 100644 --- a/app/controllers/subscriptions_controller.rb +++ b/app/controllers/subscriptions_controller.rb @@ -9,14 +9,14 @@ class SubscriptionsController < ApplicationController def create email = params[:subscription][:email] - if !EmailValidator.valid?(email) + unless EmailValidator.valid?(email) render json: { message: "Bad news, something went wrong with your email address. Please check it for typos and try again." }, status: 400 return end update_user_data(email: email) params[:subscription][:opt_in] = params[:subscription][:opt_in] || false - subscription = CiviCRM::subscribe params[:subscription] + subscription = Civicrm.subscribe params[:subscription] if subscription["error"] render json: { message: subscription["error_message"] }, status: 500 else @@ -27,7 +27,7 @@ def create def edit civicrm_url = current_user.manage_subscription_url! if civicrm_url - redirect_to civicrm_url + redirect_to civicrm_url, allow_other_host: true else flash.now[:error] = I18n.t "subscriptions.edit_error" redirect_to "/account" diff --git a/app/controllers/tools_controller.rb b/app/controllers/tools_controller.rb index d5fe9158d..eec3eca24 100644 --- a/app/controllers/tools_controller.rb +++ b/app/controllers/tools_controller.rb @@ -10,10 +10,10 @@ class ToolsController < ApplicationController # Put an invisible captcha on forms are easy to submit programmatically and # create email subscriptions. - invisible_captcha only: [:email, :petition] - before_action :create_newsletter_subscription, only: [:email, :call] - before_action :create_partner_subscription, only: [:email, :call, :petition, :message_congress] - after_action :deliver_thanks_message, only: [:email, :call, :petition, :message_congress] + invisible_captcha only: %i[email petition] + before_action :create_newsletter_subscription, only: %i[email call] + before_action :create_partner_subscription, only: %i[email call petition message_congress] + after_action :deliver_thanks_message, only: %i[email call petition message_congress] skip_after_action :deliver_thanks_message, if: :signature_has_errors # See https://github.com/EFForg/action-center-platform/wiki/Deployment-Notes#csrf-protection @@ -22,14 +22,12 @@ class ToolsController < ApplicationController def call ahoy.track "Action", - { type: "action", actionType: "call", actionPageId: params[:action_id] }, - action_page: @action_page + { type: "action", actionType: "call", actionPageId: params[:action_id] }, + action_page: @action_page @name = current_user.try :name - if params[:update_user_data] == "yes" - update_user_data(call_params) - end + update_user_data(call_params) if params[:update_user_data] == "yes" CallTool.campaign_call(params[:call_campaign_id], phone: params[:phone], @@ -56,12 +54,11 @@ def petition @action_page = Petition.find(params[:signature][:petition_id]).action_page @signature = Signature.new(signature_params.merge(user_id: @user.id)) - if @signature.zipcode.present? && @signature.country_code.blank? - @signature.country_code = "US" - end + @signature.country_code = "US" if @signature.zipcode.present? && @signature.country_code.blank? if @signature.country_code == "US" && !Rails.application.secrets.smarty_streets_id.nil? - if city_state = SmartyStreets.get_city_state(@signature.zipcode) + city_state = SmartyStreets.get_city_state(@signature.zipcode) + if city_state @signature.city = city_state["city"] @signature.state = city_state["state"] end @@ -76,28 +73,24 @@ def petition :zipcode, :country_code, :phone ) - @source = "action center petition :: " + @action_page.title - @user.subscribe!(opt_in = true, source = @source) + @source = "action center petition :: #{@action_page.title}" + @user.subscribe!(opt_in: true, source: @source) end - if params[:update_user_data] - update_user_data(signature_params) - end + update_user_data(signature_params) if params[:update_user_data] ahoy.track "Action", - { type: "action", actionType: "signature", actionPageId: @action_page.id }, - action_page: @action_page + { type: "action", actionType: "signature", actionPageId: @action_page.id }, + action_page: @action_page respond_to do |format| format.json { render json: { success: true }, status: 200 } format.html do - begin - url = URI.parse(request.referrer) - url.query = [url.query.presence, "thankyou=1"].join("&") - redirect_to url.to_s - rescue - redirect_to welcome_index_path - end + url = URI.parse(request.referrer) + url.query = [url.query.presence, "thankyou=1"].join("&") + redirect_to url.to_s + rescue StandardError + redirect_to welcome_index_path end end else @@ -107,23 +100,49 @@ def petition def tweet ahoy.track "Action", - { type: "action", actionType: "tweet", actionPageId: params[:action_id] }, - action_page: @action_page + { type: "action", actionType: "tweet", actionPageId: params[:action_id] }, + action_page: @action_page render json: { success: true }, status: 200 end def email - unless (@user and @user.events.emails.find_by_action_page_id(params[:action_id])) or params[:dnt] == "true" + unless @user&.taken_action?(@action_page) || params[:dnt] == "true" ahoy.track "Action", - { type: "action", actionType: "email", actionPageId: params[:action_id] }, - action_page: @action_page + { type: "action", actionType: "email", actionPageId: params[:action_id] }, + action_page: @action_page end if params[:service] == "copy" @actionPage = @action_page render "email_target" + elsif params[:state_rep_email] + redirect_to @action_page.email_campaign.service_uri(params[:service], { email: params[:state_rep_email] }), allow_other_host: true else - redirect_to @action_page.email_campaign.service_uri(params[:service]) + redirect_to @action_page.email_campaign.service_uri(params[:service]), allow_other_host: true + end + end + + # GET /tools/state_reps + # + # This endpoint is hit by the js for state legislator lookup-by-address actions. + # It renders json containing html markup for presentation on the view + def state_reps + @email_campaign = EmailCampaign.find(params[:email_campaign_id]) + @actionPage = @email_campaign.action_page + # TODO: strong params this + address = "#{params[:street_address]} #{params[:zipcode]}" + civic_api_response = CivicApi.state_rep_search(address, @email_campaign.leg_level) + @state_reps = JSON.parse(civic_api_response.body)["officials"] + state_rep_emails = [] + @state_reps.each do |sr| + state_rep_emails << sr["emails"] unless sr["emails"].nil? + end + # single-rep lookup only + @state_rep_email = state_rep_emails.flatten.first + if @state_reps.present? + render json: { content: render_to_string(partial: "action_page/state_reps") }, status: 200 + else + render json: { error: "No representatives found" }, status: 200 end end @@ -162,7 +181,7 @@ def set_user end def set_action_page - @action_page ||= ActionPage.find_by_id(params[:action_id]) + @action_page ||= ActionPage.find_by(id: params[:action_id]) end def create_newsletter_subscription @@ -170,7 +189,7 @@ def create_newsletter_subscription source = "action center #{@action_page.class.name.downcase} :: " + @action_page.title params[:subscription][:opt_in] = true params[:subscription][:source] = source - CiviCRM::subscribe params[:subscription] + Civicrm.subscribe params[:subscription] end end @@ -179,9 +198,9 @@ def signature_has_errors end def partner_signup_params - attributes = %i(first_name last_name email) + attributes = %i[first_name last_name email] # Partner signup params might come through the main form or a nested subscription form. - %i(signature subscription).each do |model| + %i[signature subscription].each do |model| return params.require(model).permit(*attributes) if params[model].present? end params.permit(*attributes) @@ -191,8 +210,8 @@ def signature_params params.require(:signature).permit( :first_name, :last_name, :email, :petition_id, :user_id, :street_address, :city, :state, :country_code, :zipcode, :anonymous, - affiliations_attributes: [ - :id, :institution_id, :affiliation_type_id + affiliations_attributes: %i[ + id institution_id affiliation_type_id ] ) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 38f7c87f2..99a9bd6a7 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -7,11 +7,11 @@ def show end def update - if current_user.update_attributes(user_params) - flash[:notice] = "You updated your account successfully." - else - flash[:notice] = "Could not update your account." - end + flash[:notice] = if current_user.update(user_params) + "You updated your account successfully." + else + "Could not update your account." + end if request.xhr? render json: {}, status: 200 diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index 8e018e532..269572d32 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -1,10 +1,10 @@ class WelcomeController < ApplicationController manifest :welcome def index - @actionPages = FeaturedActionPage.includes(:action_page). - order("weight desc"). - map(&:action_page). - compact + @actionPages = FeaturedActionPage.includes(:action_page) + .order("weight desc") + .map(&:action_page) + .compact @featuredActionPage = @actionPages.pop @actionPages = @actionPages.reverse end diff --git a/app/helpers/action_page_helper.rb b/app/helpers/action_page_helper.rb index 1e78245f3..2f322a71d 100644 --- a/app/helpers/action_page_helper.rb +++ b/app/helpers/action_page_helper.rb @@ -4,9 +4,7 @@ def twitter_share_url(action_page) action_page.share_message, action_page_url(action_page) ].map(&:presence).compact.join(" ") - suffix = if Rails.application.config.twitter_handle - " via @#{Rails.application.config.twitter_handle}" - end + suffix = (" via @#{Rails.application.config.twitter_handle}" if Rails.application.config.twitter_handle) message += suffix if action_page.share_message.to_s.length + suffix.length <= 117 related = Rails.application.config.twitter_related.to_a.join(",") @@ -14,7 +12,6 @@ def twitter_share_url(action_page) "https://twitter.com/intent/tweet?text=#{u message}&related=#{related}" end - def tweet_url(target, message) message = [target, message].compact.join(" ") related = Rails.application.config.twitter_related.to_a.join(",") @@ -22,10 +19,8 @@ def tweet_url(target, message) end def facebook_share_url(action_page) - "https://www.facebook.com/sharer/sharer.php?" + { - u: action_page_url(action_page), - display: "popup" - }.to_param + fb_params = { u: action_page_url(action_page), display: "popup" }.to_param + "https://www.facebook.com/sharer/sharer.php?#{fb_params}" end def email_friends_url(action_page) @@ -70,16 +65,17 @@ def parse_email_text(options = {}) title = @actionPage.title name = html_escape(options[:name]) - email_text. - gsub(/\$TITLE/, title). - gsub(/\$URL/, url). - gsub(/\$NAME/, name) + email_text + .gsub("$TITLE", title) + .gsub("$URL", url) + .gsub("$NAME", name) end def visible_partners mailings_enabled = @actionPage.partners.includes(:partnerships) .where(partnerships: { enable_mailings: true }) return mailings_enabled if params[:partner].blank? + mailings_enabled.where(code: params[:partner]) end end diff --git a/app/helpers/admin/action_pages_helper.rb b/app/helpers/admin/action_pages_helper.rb index dfc8b35dc..d10cd02a5 100644 --- a/app/helpers/admin/action_pages_helper.rb +++ b/app/helpers/admin/action_pages_helper.rb @@ -1,47 +1,43 @@ -module Admin - module ActionPagesHelper - def call_campaign_options_for_select - if CallTool.enabled? - CallTool.campaigns.map do |campaign| - ["#{campaign['name']} (#{campaign['status']})", campaign["id"]] - end.sort_by(&:last).reverse - else - [] - end - rescue SystemCallError, RestClient::Exception +module Admin::ActionPagesHelper + def call_campaign_options_for_select + if CallTool.enabled? + CallTool.campaigns.map do |campaign| + ["#{campaign['name']} (#{campaign['status']})", campaign["id"]] + end.sort_by(&:last).reverse + else [] end + rescue SystemCallError, RestClient::Exception + [] + end - def congress_member_options_for_select(campaign) - selected = (campaign.target_bioguide_ids || "").split(/\s*,\s*/) + def congress_member_options_for_select(campaign) + selected = (campaign.target_bioguide_ids || "").split(/\s*,\s*/) - state_names = Places.us_state_codes.invert + state_names = Places.us_state_codes.invert - congressional_bioguides = [] - grouped_reps = CongressMember.all.group_by do |rep| - congressional_bioguides << rep.bioguide_id - state_names[rep.state] - end + congressional_bioguides = [] + grouped_reps = CongressMember.all.group_by do |rep| + congressional_bioguides << rep.bioguide_id + state_names[rep.state] + end - grouped_reps.each do |state, state_reps| - grouped_reps[state] = state_reps.sort_by(&:last_name).map do |rep| - label = "#{rep.full_name} (#{rep.bioguide_id})" - [label, rep.bioguide_id] - end + grouped_reps.each do |state, state_reps| + grouped_reps[state] = state_reps.sort_by(&:last_name).map do |rep| + label = "#{rep.full_name} (#{rep.bioguide_id})" + [label, rep.bioguide_id] end + end - grouped_reps = grouped_reps.keys.sort.map { |k| [k, grouped_reps[k]] } + grouped_reps = grouped_reps.keys.sort.map { |k| [k, grouped_reps[k]] } - non_congressional_bioguides = [] - CongressMessageCampaign.targets_bioguide_ids.pluck(:target_bioguide_ids).each do |targets| - non_congressional_bioguides.concat(targets.split(/\s*,\s*/) - congressional_bioguides) - end + non_congressional_bioguides = [] + CongressMessageCampaign.targets_bioguide_ids.pluck(:target_bioguide_ids).each do |targets| + non_congressional_bioguides.concat(targets.split(/\s*,\s*/) - congressional_bioguides) + end - if non_congressional_bioguides.present? - grouped_reps.unshift(["Non-congressional", non_congressional_bioguides.sort]) - end + grouped_reps.unshift(["Non-congressional", non_congressional_bioguides.sort]) if non_congressional_bioguides.present? - grouped_options_for_select(grouped_reps, selected) - end + grouped_options_for_select(grouped_reps, selected) end end diff --git a/app/helpers/admin/topics_helper.rb b/app/helpers/admin/topics_helper.rb index cb93a029c..9308f1c89 100644 --- a/app/helpers/admin/topics_helper.rb +++ b/app/helpers/admin/topics_helper.rb @@ -1,19 +1,17 @@ -module Admin - module TopicsHelper - def topic_category_props(topic_category) - topic_sets = topic_category.topic_sets.map do |topic_set| - { - id: topic_set.id, - tier: topic_set.tier, - topics: topic_set.topics.map { |topic| topic.attributes.slice("id", "name") } - } - end - +module Admin::TopicsHelper + def topic_category_props(topic_category) + topic_sets = topic_category.topic_sets.map do |topic_set| { - topicCategoryId: topic_category.id, - topicCategoryName: topic_category.name, - topicSets: topic_sets + id: topic_set.id, + tier: topic_set.tier, + topics: topic_set.topics.map { |topic| topic.attributes.slice("id", "name") } } end + + { + topicCategoryId: topic_category.id, + topicCategoryName: topic_category.name, + topicSets: topic_sets + } end end diff --git a/app/helpers/ahoy_helper.rb b/app/helpers/ahoy_helper.rb index 2bc4f3676..b1ef23a17 100644 --- a/app/helpers/ahoy_helper.rb +++ b/app/helpers/ahoy_helper.rb @@ -1,8 +1,8 @@ module AhoyHelper def ahoy_track_tag(action_type, action_page_id) - content_tag(:div, style: "height: 0; overflow: hidden") { + content_tag(:div, style: "height: 0; overflow: hidden") do params = { action_type: action_type, action_page_id: action_page_id, format: :gif } image_tag(ahoy_visit_url(params), style: "border: 0", alt: "") - } + end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 12ff78db6..ae639d71b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,15 +1,15 @@ module ApplicationHelper def page_title - t("page_title", scope: [controller_path.gsub("/", "_"), action_name], - default: [@title, I18n.t("site_title")].compact.join(" | ")) + t("page_title", scope: [controller_path.tr("/", "_"), action_name], + default: [@title, I18n.t("site_title")].compact.join(" | ")) end def escape_page_title - URI.escape(page_title , Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) + ERB::Util.url_encode page_title end def twitter_handle - "@" + Rails.application.config.twitter_handle.to_s + "@#{Rails.application.config.twitter_handle}" end def markdown(blogtext) @@ -21,7 +21,7 @@ def markdown(blogtext) end def substitute_keywords(blogtext) - if @actionPage and @actionPage.description and @petition + if @actionPage&.description && @petition blogtext.gsub("$SIGNATURECOUNT", @petition.signatures.pretty_count) else blogtext @@ -71,24 +71,26 @@ def update_user_data(params = {}) if user_signed_in? p = params.clone p.delete(:email) - current_user.update_attributes p + current_user.update p end end + # REFACTOR_FLAG + # use ActiveStorage::Filename#sanitized after upgrading to 5.2 def sanitize_filename(filename) # Split the name when finding a period which is preceded by some # character, and is followed by some character other than a period, # if there is no following period that is followed by something # other than a period (yeah, confusing, I know) - fn = filename.split /(?<=.)\.(?=[^.])(?!.*\.[^.])/m + fn = filename.split(/(?<=.)\.(?=[^.])(?!.*\.[^.])/m) # We now have one or two parts (depending on whether we could find # a suitable period). For each of these parts, replace any unwanted # sequence of characters with an underscore - fn.map! { |s| s.gsub /[^a-z0-9\-]+/i, "_" } + fn.map! { |s| s.gsub(/[^a-z0-9\-]+/i, "_") } # Finally, join the parts with a period and return the result - return fn.join "." + fn.join "." end def can?(ability) @@ -110,18 +112,20 @@ def messages def percentage(x, y, precision: 0) return "-" unless y > 0 + number_to_percentage((x / y.to_f) * 100, precision: precision) end private def user_session_data_whitelist - [:email, :last_name, :first_name, :street_address, :city, :state, :zipcode, - :country_code, :phone] + %i[email last_name first_name street_address city state zipcode + country_code phone] end def current_user_data(field) return nil unless user_session_data_whitelist.include? field + current_user.try(field) end end diff --git a/app/helpers/congress_message_helper.rb b/app/helpers/congress_message_helper.rb index 12659b644..7fb8e7497 100644 --- a/app/helpers/congress_message_helper.rb +++ b/app/helpers/congress_message_helper.rb @@ -1,8 +1,7 @@ module CongressMessageHelper def congress_forms_prefills(campaign, field) - if field.value == "$TOPIC" && campaign.topic_category.present? - return campaign.topic_category.best_match(field.options_hash) - end + return campaign.topic_category.best_match(field.options_hash) if field.value == "$TOPIC" && campaign.topic_category.present? + { "$NAME_FIRST" => current_first_name, "$NAME_LAST" => current_last_name, @@ -10,16 +9,16 @@ def congress_forms_prefills(campaign, field) "$PHONE" => number_to_phone(current_user.try(:phone)), "$ADDRESS_STREET" => current_street_address, "$ADDRESS_CITY" => current_city, - "$SUBJECT" => campaign.subject, + "$SUBJECT" => campaign.subject }[field.value] end def congress_forms_field(field, campaign, message_attributes, bioguide_id = nil) - if bioguide_id - name = "member_attributes[#{bioguide_id}][#{field.value}]" - else - name = "common_attributes[#{field.value}]" - end + name = if bioguide_id + "member_attributes[#{bioguide_id}][#{field.value}]" + else + "common_attributes[#{field.value}]" + end # Try to guess the input based on saved info about the campaign + user. prefill = congress_forms_prefills(campaign, field) @@ -30,11 +29,11 @@ def congress_forms_field(field, campaign, message_attributes, bioguide_id = nil) elsif field.value == "$PHONE" telephone_field_tag name, prefill, congress_forms_field_defaults(field) .merge({ - class: "form-control bfh-phone", - "data-format": "ddd-ddd-dddd", - pattern: "^((5\\d[123467890])|(5[123467890]\\d)|([2346789]\\d\\d))-\\d\\d\\d-\\d\\d\\d\\d$", - title: "Must be a valid US phone number entered in 555-555-5555 format" - }) + class: "form-control bfh-phone", + "data-format": "ddd-ddd-dddd", + pattern: "^((5\\d[123467890])|(5[123467890]\\d)|([2346789]\\d\\d))-\\d\\d\\d-\\d\\d\\d\\d$", + title: "Must be a valid US phone number entered in 555-555-5555 format" + }) elsif field.value == "$EMAIL" email_field_tag name, prefill, congress_forms_field_defaults(field) elsif field.value.include?("ADDRESS") && !field.is_select? @@ -44,7 +43,7 @@ def congress_forms_field(field, campaign, message_attributes, bioguide_id = nil) text_field_tag name, prefill, congress_forms_field_defaults(field, placeholder: address_label, "aria-label": address_label) elsif field.is_select? select_tag name, options_for_select(field.options_hash, prefill), - class: "form-control", "aria-label": field.label, include_blank: field.label, required: true + class: "form-control", "aria-label": field.label, include_blank: field.label, required: true else text_field_tag name, prefill, congress_forms_field_defaults(field) end diff --git a/app/helpers/devise_helper.rb b/app/helpers/devise_helper.rb index ff4a7e3ce..c4bfed8b0 100644 --- a/app/helpers/devise_helper.rb +++ b/app/helpers/devise_helper.rb @@ -2,28 +2,22 @@ module DeviseHelper # Customized to add model error instead of flashing the error. def devise_error_messages! flash_alerts = [] - error_key = "errors.messages.not_saved" flash_alerts.push("This account was locked due to too many failed login attempts. Check your email for a link to unlock.") if locked_account? - if !flash.empty? + unless flash.empty? flash_alerts.push(flash[:error]) if flash[:error] flash_alerts.push(flash[:alert]) if flash[:alert] flash_alerts.push(flash[:notice]) if flash[:notice] - error_key = "devise.failure.invalid" end return "" if resource.errors.empty? && flash_alerts.empty? + @hasErrorMessages = true errors = resource.errors.empty? ? flash_alerts : resource.errors.full_messages messages = errors.map { |msg| content_tag(:p, msg) }.join - sentence = I18n.t(error_key, count: errors.count, - resource: resource.class.model_name.human.downcase) - - if !flash[:notice] | flash[:alert] - panel_title = "

Error

" - end + panel_title = "

Error

" if !flash[:notice] | flash[:alert] html = <<-HTML
@@ -42,7 +36,7 @@ def devise_error_messages? end def locked_account? - u = User.find_by_email(@user.email) - u && u.access_locked? + u = User.find_by(email: @user.email) + u&.access_locked? end end diff --git a/app/helpers/email_campaign_helper.rb b/app/helpers/email_campaign_helper.rb new file mode 100644 index 000000000..4f41b9d5c --- /dev/null +++ b/app/helpers/email_campaign_helper.rb @@ -0,0 +1,14 @@ +module EmailCampaignHelper + def legislative_level_from_state_representative_info(legislator_info) + case legislator_info + when "legislatorLowerBody" + "state representative of the lower chamber" + when "legislatorUpperBody" + "state representative of the upper chamber" + when "headOfGovernment" + "Governor of the State" + else + "Invalid info" + end + end +end diff --git a/lib/action_cloner.rb b/app/lib/action_cloner.rb similarity index 52% rename from lib/action_cloner.rb rename to app/lib/action_cloner.rb index 45a63f02b..253d73fb4 100644 --- a/lib/action_cloner.rb +++ b/app/lib/action_cloner.rb @@ -7,13 +7,9 @@ def self.run(original) clone_tools(original, clone) end - private - def self.clone_tools(original, clone) - %i(tweet email_campaign petition congress_message_campaign call_campaign).each do |tool_sym| - if original.send("#{tool_sym}_id").present? - clone.send("#{tool_sym}=", original.send(tool_sym).dup) - end + %i[tweet email_campaign petition congress_message_campaign call_campaign].each do |tool_sym| + clone.send("#{tool_sym}=", original.send(tool_sym).dup) if original.send("#{tool_sym}_id").present? end clone end diff --git a/lib/call_tool.rb b/app/lib/call_tool.rb similarity index 69% rename from lib/call_tool.rb rename to app/lib/call_tool.rb index 9bb969e40..0714715ec 100644 --- a/lib/call_tool.rb +++ b/app/lib/call_tool.rb @@ -2,23 +2,19 @@ module CallTool def self.campaign_call(campaign, phone:, location:, user_id:, action_id:, callback_url:) - unless [campaign, phone, location, action_id, callback_url].all? - raise ArgumentError.new("required argument is nil") - end + raise ArgumentError, "required argument is nil" unless [campaign, phone, location, action_id, callback_url].all? get "/call/create", { campaignId: campaign.to_param, - userPhone: phone, + userPhone: phone, userCountry: "US", userLocation: location, callback_url: callback_url, - - # TODO - Settle on the schema of the private meta data meta: { - user_id: user_id, - action_id: action_id, + user_id: user_id, + action_id: action_id, action_type: "call" - }.to_json, + }.to_json } end @@ -32,7 +28,7 @@ def self.campaigns api_response = { "total_pages" => 1, "page" => 0 } until api_response["page"] >= api_response["total_pages"] - api_response = JSON.parse(get "/api/campaign", { api_key: api_key, page: api_response["page"] + 1 }) + api_response = JSON.parse(get("/api/campaign", { api_key: api_key, page: api_response["page"] + 1 })) campaigns.concat(api_response["objects"].map { |campaign| campaign.slice("id", "name", "allow_call_in", "phone_numbers", "status") }) end @@ -44,19 +40,13 @@ def self.enabled? Rails.application.secrets.fetch_values(:call_tool_url, :call_tool_api_key).all? end - private - def self.get(action, params = {}) RestClient.get endpoint(action), params: params rescue RestClient::BadRequest => e - begin - error = JSON.parse(e.http_body)["error"] - rescue - raise - end + error = JSON.parse(e.http_body)["error"] # Don't raise for twilio error 13224: number invalid - unless error.match(/^13224:/) + unless error.match?(/^13224:/) if Rails.application.secrets.sentry_dsn.nil? raise error else @@ -66,8 +56,8 @@ def self.get(action, params = {}) end def self.endpoint(action) - base = Rails.application.config.call_tool_url.sub(/\/$/, "") - action = action.sub(/^\//, "") + base = Rails.application.config.call_tool_url.sub(%r{/$}, "") + action = action.sub(%r{^/}, "") "#{base}/#{action}" end diff --git a/app/lib/civic_api.rb b/app/lib/civic_api.rb new file mode 100644 index 000000000..3f28306de --- /dev/null +++ b/app/lib/civic_api.rb @@ -0,0 +1,57 @@ +require "rest_client" + +# From Google, the API provider (September 2022): +# Reference link: https://developers.google.com/civic-information/docs/data_guidelines?hl=en +# "Developer’s using the API should make every effort to ensure all users are met with the same experience. We +# do not allow holdbacks, A/B testing, or similar experiments." + +module CivicApi + VALID_ROLES = %w[legislatorLowerBody legislatorUpperBody headOfGovernment].freeze + + def self.state_rep_search(address, roles) + raise ArgumentError, "required argument is nil" unless [address, roles].all? + + raise ArgumentError, "Invalid role for Civic API #{roles}" unless VALID_ROLES.include?(roles) + + # `includeOffices` param is needed in order to get officials list + # `administrativeArea1` param restricts the search to state-level legislators (and governors) + params = { address: address, includeOffices: true, levels: "administrativeArea1", roles: roles, key: civic_api_key } + + get params + end + + def self.all_state_reps_for_role(state, roles) + raise ArgumentError, "required argument is nil" unless [state, roles].all? + + # need to append division information to API route + path_params = { ocdId: "ocd-division%2Fcountry%3Aus%2Fstate%3A#{state.downcase}" } + # `administrativeArea1` param restricts the search to state-level legislators (and governors) + query_params = { levels: "administrativeArea1", recursive: true, roles: roles, key: civic_api_key } + + params = { path_params: path_params, query_params: query_params } + + get params + end + + def self.civic_api_key + Rails.application.secrets.google_civic_api_key + end + + def self.endpoint + Rails.application.config.google_civic_api_url + end + + def self.get(params = {}) + if params[:path_params].nil? + url = endpoint + else + ocd_encpoint = endpoint.clone + url = ocd_encpoint.concat(params[:path_params][:ocdId]) + params = params[:query_params] + end + RestClient.get url, params: params + rescue RestClient::BadRequest => e + error = JSON.parse(e.http_body)["error"] + raise error + end +end diff --git a/lib/civicrm.rb b/app/lib/civicrm.rb similarity index 60% rename from lib/civicrm.rb rename to app/lib/civicrm.rb index 7773669b7..2a8a081d3 100644 --- a/lib/civicrm.rb +++ b/app/lib/civicrm.rb @@ -1,5 +1,5 @@ require "rest_client" -module CiviCRM +module Civicrm module UserMethods def contact_attributes attributes.symbolize_keys.slice( @@ -8,34 +8,39 @@ def contact_attributes ) end - def subscribe!(opt_in = false, source = "action center") - return nil if CiviCRM.skip_crm? - res = CiviCRM::subscribe contact_attributes.merge(opt_in: opt_in, source: source) - update_attributes(contact_id: res["contact_id"]) if (res && res["contact_id"]) + def subscribe!(opt_in: false, source: "action center") + return nil if Civicrm.skip_crm? + + res = Civicrm.subscribe contact_attributes.merge(opt_in: opt_in, source: source) + update(contact_id: res["contact_id"]) if res && res["contact_id"] res || {} end def contact_id! - return nil if CiviCRM.skip_crm? - res = CiviCRM::import_contact contact_attributes - update_attributes(contact_id: res["contact_id"]) if (res && res["contact_id"]) + return nil if Civicrm.skip_crm? + + res = Civicrm.import_contact contact_attributes + update(contact_id: res["contact_id"]) if res && res["contact_id"] contact_id end def add_civicrm_activity!(action_page_id) - return nil if CiviCRM.skip_crm? - if contact_id && action_page = ActionPage.find_by_id(action_page_id) - CiviCRM::add_activity( - contact_id: contact_id, - subject: "Took Action #{action_page.id}: #{action_page.title}" - ) - end + return nil if Civicrm.skip_crm? + + action_page = ActionPage.find_by(id: action_page_id) + return unless contact_id && action_page + + Civicrm.add_activity( + contact_id: contact_id, + subject: "Took Action #{action_page.id}: #{action_page.title}" + ) end def manage_subscription_url! - checksum = CiviCRM::get_checksum(contact_id) + checksum = Civicrm.get_checksum(contact_id) return nil unless checksum - "#{Rails.application.secrets.supporters['host']}/update-your-preferences?" + { + + "#{Rails.application.secrets.supporters[:host]}/update-your-preferences?" + { cid1: contact_id, cs: checksum }.to_param @@ -43,16 +48,18 @@ def manage_subscription_url! end def self.skip_crm? - Rails.application.secrets.supporters["api_key"].nil? + Rails.application.secrets.supporters[:api_key].nil? end def self.subscribe(params) return {} if skip_crm? - self.import_contact params.merge(subscribe: true) + + import_contact params.merge(subscribe: true) end def self.import_contact(params) return {} if skip_crm? + post base_params.merge( method: "import_contact", data: { @@ -73,6 +80,7 @@ def self.import_contact(params) def self.add_activity(params) return nil if skip_crm? + post base_params.merge( method: "add_activity", data: params.slice(:contact_id, :subject, :activity_type_id).to_json @@ -80,21 +88,18 @@ def self.add_activity(params) end def self.supporters_api_url - "#{Rails.application.secrets.supporters['host']}/#{Rails.application.secrets.supporters['path']}" + "#{Rails.application.secrets.supporters[:host]}/#{Rails.application.secrets.supporters[:path]}" end - private - def self.post(params) - begin - res = JSON.parse RestClient.post(supporters_api_url, params) - raise res["error_message"] if res["error"] - return res - rescue => e - Raven.capture_exception(e) - Rails.logger.error "#{ e } (#{ e.class })!" - return false - end + res = JSON.parse RestClient.post(supporters_api_url, params) + raise res["error_message"] if res["error"] + + res + rescue StandardError => e + Raven.capture_exception(e) + Rails.logger.error "#{e} (#{e.class})!" + false end def self.send_email_template_data(params) @@ -116,6 +121,7 @@ def self.find_contact_by_email_data(params) def self.get_checksum(contact_id) return nil if skip_crm? + # Valid for 24 hours res = post base_params.merge( method: "generate_checksum", @@ -125,6 +131,6 @@ def self.get_checksum(contact_id) end def self.base_params - { site_key: Rails.application.secrets.supporters["api_key"] } + { site_key: Rails.application.secrets.supporters[:api_key] } end end diff --git a/lib/congress_forms.rb b/app/lib/congress_forms.rb similarity index 78% rename from lib/congress_forms.rb rename to app/lib/congress_forms.rb index a31c483cf..9a6b549cd 100644 --- a/lib/congress_forms.rb +++ b/app/lib/congress_forms.rb @@ -7,7 +7,8 @@ class Form def self.find(bioguide_ids) raw_data = CongressForms.post("/retrieve-form-elements/", { bio_ids: bioguide_ids }) raise CongressForms::RequestFailed if raw_data.empty? - links, forms = raw_data.partition { |id, raw| raw["defunct"] } + + links, forms = raw_data.partition { |_id, raw| raw["defunct"] } [ forms.map { |id, raw| Form.new(id, raw["required_actions"]) }, links.map { |id, raw| [id, raw["contact_url"]] }.to_h @@ -21,7 +22,7 @@ def initialize(bioguide_id, fields) end def order_fields - order = %w($NAME_PREFIX $NAME_FIRST $NAME_LAST $PHONE $EMAIL $SUBJECT $TOPIC) + order = %w[$NAME_PREFIX $NAME_FIRST $NAME_LAST $PHONE $EMAIL $SUBJECT $TOPIC] @fields = @fields.sort_by { |f| order.index(f.value) || Float::INFINITY } end @@ -46,6 +47,7 @@ def validate(input) return false if input.nil? return false if max_length && input.length > max_length return false unless options.nil? || options.include?(input) + true end @@ -72,9 +74,10 @@ def hash end def options_hash - if @value == "$ADDRESS_STATE_POSTAL_ABBREV" + case @value + when "$ADDRESS_STATE_POSTAL_ABBREV" Places.us_state_codes - elsif @value == "$ADDRESS_STATE_FULL" + when "$ADDRESS_STATE_FULL" Places.us_states else @options_hash @@ -83,6 +86,7 @@ def options_hash def options return options_hash.values if options_hash.is_a?(Hash) + options_hash end end @@ -95,7 +99,7 @@ def self.date_fills_path(campaign_tag = nil, start_date = nil, end_date = nil, b params = { date_start: start_date, date_end: end_date, - campaign_tag: campaign_tag, + campaign_tag: campaign_tag }.compact data_path("/successful-fills-by-date/", params, bioguide_id) end @@ -110,30 +114,26 @@ def self.date_fills(*args) def self.data_path(base_path, params = {}, bioguide_id = nil) base_path += bioguide_id unless bioguide_id.nil? - base_path += "?" + { - debug_key: Rails.application.secrets.congress_forms_debug_key, + "#{base_path}?" + { + debug_key: Rails.application.secrets.congress_forms_debug_key }.merge(params).to_query end def self.get(path) - begin - JSON.parse RestClient.get(base_url + path) - rescue RestClient::ExceptionWithResponse => e - Raven.capture_exception(e) - Rails.logger.error e - return {} - end + JSON.parse RestClient.get(base_url + path) + rescue RestClient::ExceptionWithResponse => e + Raven.capture_exception(e) + Rails.logger.error e + {} end def self.post(path, body = {}) - begin - JSON.parse RestClient.post(base_url + path, body.to_json, - { content_type: :json, accept: :json }) - rescue RestClient::ExceptionWithResponse => e - Raven.capture_exception(e) - Rails.logger.error e - raise RequestFailed - end + JSON.parse RestClient.post(base_url + path, body.to_json, + { content_type: :json, accept: :json }) + rescue RestClient::ExceptionWithResponse => e + Raven.capture_exception(e) + Rails.logger.error e + raise RequestFailed end def self.base_url diff --git a/lib/places.rb b/app/lib/places.rb similarity index 53% rename from lib/places.rb rename to app/lib/places.rb index 4809d8342..d29c96107 100644 --- a/lib/places.rb +++ b/app/lib/places.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Metrics/CollectionLiteralLength module Places def self.us_state_codes { "Alabama" => "AL", @@ -69,193 +70,193 @@ def self.us_states def self.country_codes [ - ["Afghanistan", "AF"], + %w[Afghanistan AF], ["Aland Islands", "AX"], - ["Albania", "AL"], - ["Algeria", "DZ"], + %w[Albania AL], + %w[Algeria DZ], ["American Samoa", "AS"], - ["Andorra", "AD"], - ["Angola", "AO"], - ["Anguilla", "AI"], - ["Antarctica", "AQ"], + %w[Andorra AD], + %w[Angola AO], + %w[Anguilla AI], + %w[Antarctica AQ], ["Antigua and Barbuda", "AG"], - ["Argentina", "AR"], - ["Armenia", "AM"], - ["Aruba", "AW"], + %w[Argentina AR], + %w[Armenia AM], + %w[Aruba AW], ["Ascension Island", "AC"], - ["Australia", "AU"], - ["Austria", "AT"], - ["Azerbaijan", "AZ"], - ["Bahamas", "BS"], - ["Bahrain", "BH"], - ["Bangladesh", "BD"], - ["Barbados", "BB"], - ["Belarus", "BY"], - ["Belgium", "BE"], - ["Belize", "BZ"], - ["Benin", "BJ"], - ["Bermuda", "BM"], - ["Bhutan", "BT"], + %w[Australia AU], + %w[Austria AT], + %w[Azerbaijan AZ], + %w[Bahamas BS], + %w[Bahrain BH], + %w[Bangladesh BD], + %w[Barbados BB], + %w[Belarus BY], + %w[Belgium BE], + %w[Belize BZ], + %w[Benin BJ], + %w[Bermuda BM], + %w[Bhutan BT], ["Bolivia, Plurinational State of", "BO"], ["Bonaire, Sint Eustatius and Saba", "BQ"], ["Bosnia and Herzegovina", "BA"], - ["Botswana", "BW"], + %w[Botswana BW], ["Bouvet Island", "BV"], - ["Brazil", "BR"], + %w[Brazil BR], ["British Indian Ocean Territory", "IO"], ["Brunei Darussalam", "BN"], - ["Bulgaria", "BG"], + %w[Bulgaria BG], ["Burkina Faso", "BF"], - ["Burundi", "BI"], - ["Cambodia", "KH"], - ["Cameroon", "CM"], - ["Canada", "CA"], + %w[Burundi BI], + %w[Cambodia KH], + %w[Cameroon CM], + %w[Canada CA], ["Cape Verde", "CV"], ["Cayman Islands", "KY"], ["Central African Republic", "CF"], - ["Chad", "TD"], - ["Chile", "CL"], - ["China", "CN"], + %w[Chad TD], + %w[Chile CL], + %w[China CN], ["Christmas Island", "CX"], ["Cocos (Keeling) Islands", "CC"], - ["Colombia", "CO"], - ["Comoros", "KM"], - ["Congo", "CG"], + %w[Colombia CO], + %w[Comoros KM], + %w[Congo CG], ["Congo, the Democratic Republic of the", "CD"], ["Cook Islands", "CK"], ["Costa Rica", "CR"], ["Cote d'Ivoire", "CI"], - ["Croatia", "HR"], - ["Cuba", "CU"], - ["Curacao", "CW"], - ["Cyprus", "CY"], + %w[Croatia HR], + %w[Cuba CU], + %w[Curacao CW], + %w[Cyprus CY], ["Czech Republic", "CZ"], - ["Denmark", "DK"], - ["Djibouti", "DJ"], - ["Dominica", "DM"], + %w[Denmark DK], + %w[Djibouti DJ], + %w[Dominica DM], ["Dominican Republic", "DO"], - ["Ecuador", "EC"], - ["Egypt", "EG"], + %w[Ecuador EC], + %w[Egypt EG], ["El Salvador", "SV"], ["Equatorial Guinea", "GQ"], - ["Eritrea", "ER"], - ["Estonia", "EE"], - ["Ethiopia", "ET"], + %w[Eritrea ER], + %w[Estonia EE], + %w[Ethiopia ET], ["Falkland Islands (Malvinas)", "FK"], ["Faroe Islands", "FO"], - ["Fiji", "FJ"], - ["Finland", "FI"], - ["France", "FR"], + %w[Fiji FJ], + %w[Finland FI], + %w[France FR], ["French Guiana", "GF"], ["French Polynesia", "PF"], ["French Southern Territories", "TF"], - ["Gabon", "GA"], - ["Gambia", "GM"], - ["Georgia", "GE"], - ["Germany", "DE"], - ["Ghana", "GH"], - ["Gibraltar", "GI"], - ["Greece", "GR"], - ["Greenland", "GL"], - ["Grenada", "GD"], - ["Guadeloupe", "GP"], - ["Guam", "GU"], - ["Guatemala", "GT"], - ["Guernsey", "GG"], - ["Guinea", "GN"], - ["Guinea-Bissau", "GW"], - ["Guyana", "GY"], - ["Haiti", "HT"], + %w[Gabon GA], + %w[Gambia GM], + %w[Georgia GE], + %w[Germany DE], + %w[Ghana GH], + %w[Gibraltar GI], + %w[Greece GR], + %w[Greenland GL], + %w[Grenada GD], + %w[Guadeloupe GP], + %w[Guam GU], + %w[Guatemala GT], + %w[Guernsey GG], + %w[Guinea GN], + %w[Guinea-Bissau GW], + %w[Guyana GY], + %w[Haiti HT], ["Heard Island and McDonald Islands", "HM"], ["Holy See (Vatican City State)", "VA"], - ["Honduras", "HN"], + %w[Honduras HN], ["Hong Kong", "HK"], - ["Hungary", "HU"], - ["Iceland", "IS"], - ["India", "IN"], - ["Indonesia", "ID"], + %w[Hungary HU], + %w[Iceland IS], + %w[India IN], + %w[Indonesia ID], ["Iran, Islamic Republic of", "IR"], - ["Iraq", "IQ"], - ["Ireland", "IE"], + %w[Iraq IQ], + %w[Ireland IE], ["Isle of Man", "IM"], - ["Israel", "IL"], - ["Italy", "IT"], - ["Jamaica", "JM"], - ["Japan", "JP"], - ["Jersey", "JE"], - ["Jordan", "JO"], - ["Kazakhstan", "KZ"], - ["Kenya", "KE"], - ["Kiribati", "KI"], + %w[Israel IL], + %w[Italy IT], + %w[Jamaica JM], + %w[Japan JP], + %w[Jersey JE], + %w[Jordan JO], + %w[Kazakhstan KZ], + %w[Kenya KE], + %w[Kiribati KI], ["Korea, Democratic People's Republic of", "KP"], ["Korea, Republic of", "KR"], - ["Kosovo", "KV"], - ["Kuwait", "KW"], - ["Kyrgyzstan", "KG"], + %w[Kosovo KV], + %w[Kuwait KW], + %w[Kyrgyzstan KG], ["Lao People's Democratic Republic", "LA"], - ["Latvia", "LV"], - ["Lebanon", "LB"], - ["Lesotho", "LS"], - ["Liberia", "LR"], - ["Libya", "LY"], - ["Liechtenstein", "LI"], - ["Lithuania", "LT"], - ["Luxembourg", "LU"], - ["Macao", "MO"], + %w[Latvia LV], + %w[Lebanon LB], + %w[Lesotho LS], + %w[Liberia LR], + %w[Libya LY], + %w[Liechtenstein LI], + %w[Lithuania LT], + %w[Luxembourg LU], + %w[Macao MO], ["Macedonia, The Former Yugoslav Republic Of", "MK"], - ["Madagascar", "MG"], - ["Malawi", "MW"], - ["Malaysia", "MY"], - ["Maldives", "MV"], - ["Mali", "ML"], - ["Malta", "MT"], + %w[Madagascar MG], + %w[Malawi MW], + %w[Malaysia MY], + %w[Maldives MV], + %w[Mali ML], + %w[Malta MT], ["Marshall Islands", "MH"], - ["Martinique", "MQ"], - ["Mauritania", "MR"], - ["Mauritius", "MU"], - ["Mayotte", "YT"], - ["Mexico", "MX"], + %w[Martinique MQ], + %w[Mauritania MR], + %w[Mauritius MU], + %w[Mayotte YT], + %w[Mexico MX], ["Micronesia, Federated States of", "FM"], ["Moldova, Republic of", "MD"], - ["Monaco", "MC"], - ["Mongolia", "MN"], - ["Montenegro", "ME"], - ["Montserrat", "MS"], - ["Morocco", "MA"], - ["Mozambique", "MZ"], - ["Myanmar", "MM"], - ["Namibia", "NA"], - ["Nauru", "NR"], - ["Nepal", "NP"], - ["Netherlands", "NL"], + %w[Monaco MC], + %w[Mongolia MN], + %w[Montenegro ME], + %w[Montserrat MS], + %w[Morocco MA], + %w[Mozambique MZ], + %w[Myanmar MM], + %w[Namibia NA], + %w[Nauru NR], + %w[Nepal NP], + %w[Netherlands NL], ["Netherlands Antilles", "AN"], ["New Caledonia", "NC"], ["New Zealand", "NZ"], - ["Nicaragua", "NI"], - ["Niger", "NE"], - ["Nigeria", "NG"], - ["Niue", "NU"], + %w[Nicaragua NI], + %w[Niger NE], + %w[Nigeria NG], + %w[Niue NU], ["Norfolk Island", "NF"], ["Northern Mariana Islands", "MP"], - ["Norway", "NO"], - ["Oman", "OM"], - ["Pakistan", "PK"], - ["Palau", "PW"], + %w[Norway NO], + %w[Oman OM], + %w[Pakistan PK], + %w[Palau PW], ["Palestinian Territory, Occupied", "PS"], - ["Panama", "PA"], + %w[Panama PA], ["Papua New Guinea", "PG"], - ["Paraguay", "PY"], - ["Peru", "PE"], - ["Philippines", "PH"], - ["Pitcairn", "PN"], - ["Poland", "PL"], - ["Portugal", "PT"], + %w[Paraguay PY], + %w[Peru PE], + %w[Philippines PH], + %w[Pitcairn PN], + %w[Poland PL], + %w[Portugal PT], ["Puerto Rico", "PR"], - ["Qatar", "QA"], - ["Reunion", "RE"], - ["Romania", "RO"], + %w[Qatar QA], + %w[Reunion RE], + %w[Romania RO], ["Russian Federation", "RU"], - ["Rwanda", "RW"], + %w[Rwanda RW], ["Saint Barthelemy", "BL"], ["Saint Helena, Ascension and Tristan da Cunha", "SH"], ["Saint Kitts and Nevis", "KN"], @@ -263,66 +264,66 @@ def self.country_codes ["Saint Martin (French part)", "MF"], ["Saint Pierre and Miquelon", "PM"], ["Saint Vincent and the Grenadines", "VC"], - ["Samoa", "WS"], + %w[Samoa WS], ["San Marino", "SM"], ["Sao Tome and Principe", "ST"], ["Saudi Arabia", "SA"], - ["Senegal", "SN"], - ["Serbia", "RS"], - ["Seychelles", "SC"], + %w[Senegal SN], + %w[Serbia RS], + %w[Seychelles SC], ["Sierra Leone", "SL"], - ["Singapore", "SG"], + %w[Singapore SG], ["Sint Maarten (Dutch part)", "SX"], - ["Slovakia", "SK"], - ["Slovenia", "SI"], + %w[Slovakia SK], + %w[Slovenia SI], ["Solomon Islands", "SB"], - ["Somalia", "SO"], + %w[Somalia SO], ["South Africa", "ZA"], ["South Georgia and the South Sandwich Islands", "GS"], ["South Sudan, Republic of", "SS"], - ["Spain", "ES"], + %w[Spain ES], ["Sri Lanka", "LK"], - ["Sudan", "SD"], - ["Suriname", "SR"], + %w[Sudan SD], + %w[Suriname SR], ["Svalbard and Jan Mayen", "SJ"], - ["Swaziland", "SZ"], - ["Sweden", "SE"], - ["Switzerland", "CH"], + %w[Swaziland SZ], + %w[Sweden SE], + %w[Switzerland CH], ["Syrian Arab Republic", "SY"], - ["Taiwan", "TW"], - ["Tajikistan", "TJ"], + %w[Taiwan TW], + %w[Tajikistan TJ], ["Tanzania, United Republic of", "TZ"], - ["Thailand", "TH"], - ["Timor-Leste", "TL"], - ["Togo", "TG"], - ["Tokelau", "TK"], - ["Tonga", "TO"], + %w[Thailand TH], + %w[Timor-Leste TL], + %w[Togo TG], + %w[Tokelau TK], + %w[Tonga TO], ["Trinidad and Tobago", "TT"], ["Tristan da Cunha", "TA"], - ["Tunisia", "TN"], - ["Turkey", "TR"], - ["Turkmenistan", "TM"], + %w[Tunisia TN], + %w[Turkey TR], + %w[Turkmenistan TM], ["Turks and Caicos Islands", "TC"], - ["Tuvalu", "TV"], - ["Uganda", "UG"], - ["Ukraine", "UA"], + %w[Tuvalu TV], + %w[Uganda UG], + %w[Ukraine UA], ["United Arab Emirates", "AE"], ["United Kingdom", "GB"], ["United States", "US"], ["United States Minor Outlying Islands", "UM"], - ["Uruguay", "UY"], - ["Uzbekistan", "UZ"], - ["Vanuatu", "VU"], + %w[Uruguay UY], + %w[Uzbekistan UZ], + %w[Vanuatu VU], ["Venezuela, Bolivarian Republic of", "VE"], ["Viet Nam", "VN"], ["Virgin Islands, British", "VG"], ["Virgin Islands, U.S.", "VI"], ["Wallis and Futuna", "WF"], ["Western Sahara", "EH"], - ["Yemen", "YE"], - ["Zambia", "ZM"], - ["Zimbabwe", "ZW"] + %w[Yemen YE], + %w[Zambia ZM], + %w[Zimbabwe ZW] ].freeze end - end +# rubocop:enable Metrics/CollectionLiteralLength diff --git a/app/lib/quotes.rb b/app/lib/quotes.rb new file mode 100644 index 000000000..fff4641d2 --- /dev/null +++ b/app/lib/quotes.rb @@ -0,0 +1,11 @@ +module Quotes + def self.get + quotes.sample + end + + def self.quotes + @quotes ||= YAML.load_file("config/custom_quotes.yml") + rescue Errno::ENOENT + @quotes ||= YAML.load_file("config/quotes.yml") + end +end diff --git a/lib/related_content.rb b/app/lib/related_content.rb similarity index 98% rename from lib/related_content.rb rename to app/lib/related_content.rb index ab5cd05a6..6e3fc3066 100644 --- a/lib/related_content.rb +++ b/app/lib/related_content.rb @@ -5,11 +5,12 @@ def initialize(url) def load return if url.blank? + begin open_page @loaded_successfully = true rescue OpenURI::HTTPError - return + nil end end @@ -23,6 +24,7 @@ def title def image return @image if @image + og_url = page.css("meta[property='og:image']") @image = if og_url.blank? "" diff --git a/lib/smarty_streets.rb b/app/lib/smarty_streets.rb similarity index 75% rename from lib/smarty_streets.rb rename to app/lib/smarty_streets.rb index b7d004434..a9a65d283 100644 --- a/lib/smarty_streets.rb +++ b/app/lib/smarty_streets.rb @@ -3,15 +3,13 @@ module SmartyStreets def self.get_city_state(zipcode) url = "https://us-zipcode.api.smartystreets.com/lookup" res = post(url, base_params.merge(zipcode: zipcode)) - if res && !res.empty? - res.first["city_states"].try :first - end + res.first["city_states"].try :first if res.present? end def self.get_location(street, zipcode) url = "https://api.smartystreets.com/street-address" res = post(url, base_params.merge(street: street, zipcode: zipcode)) - raise AddressNotFound if !res || res.empty? + raise AddressNotFound if res.blank? location = OpenStruct.new location.street = street @@ -21,7 +19,7 @@ def self.get_location(street, zipcode) location.state = res[0]["components"]["state_abbreviation"] location.district = res[0]["metadata"]["congressional_district"] location.district = "0" if location.district == "AL" - return location + location end def self.get_congressional_district(street, zipcode) @@ -31,17 +29,12 @@ def self.get_congressional_district(street, zipcode) class AddressNotFound < StandardError; end - private - def self.post(url, params) - begin - res = JSON.parse RestClient.get("#{url}?#{params.to_query}") - return res - rescue => e - Raven.capture_exception(e) - Rails.logger.error "#{ e } (#{ e.class })!" - return false - end + JSON.parse RestClient.get("#{url}?#{params.to_query}") + rescue StandardError => e + Raven.capture_exception(e) + Rails.logger.error "#{e} (#{e.class})!" + false end def self.base_params diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 185fde7cd..7609415fe 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -12,7 +12,7 @@ def thanks_message(email, actionPage, options = {}) mail(to: email, subject: "Thanks for taking action") end - def signup_attempt_with_existing_email(user, options = {}) + def signup_attempt_with_existing_email(user, _options = {}) @user = user @email = user.email @token = user.reset_password_token @@ -22,8 +22,6 @@ def signup_attempt_with_existing_email(user, options = {}) private def check_bounces - unless Bounce.find_by_email(@email.downcase).nil? - mail.perform_deliveries = false - end + mail.perform_deliveries = false unless Bounce.find_by(email: @email.downcase).nil? end end diff --git a/app/models/action_institution.rb b/app/models/action_institution.rb index d1491f79d..cd57fab1f 100644 --- a/app/models/action_institution.rb +++ b/app/models/action_institution.rb @@ -1,10 +1,11 @@ -class ActionInstitution < ActiveRecord::Base +class ActionInstitution < ApplicationRecord belongs_to :institution belongs_to :action_page def self.add(action_page:, category:, reset: false) return unless action_page.enable_petition && - action_page.petition.enable_affiliations + action_page.petition.enable_affiliations + action_page.action_institutions.delete_all if reset == "1" institution_ids = Institution.where(category: category).pluck(:id) fast_create(action_page_id: action_page.id, institution_ids: institution_ids) @@ -33,5 +34,5 @@ def self.fast_create(action_page_id:, institution_ids:) # * Check if there are any duplicates in production # * If so, remove them in a way that doesn't break anything else # * Uncomment the following line - #validates_uniqueness_of :institution_id, scope: :action_page_id + # validates_uniqueness_of :institution_id, scope: :action_page_id end diff --git a/app/models/action_page.rb b/app/models/action_page.rb index 104a5149a..3c1de0d97 100644 --- a/app/models/action_page.rb +++ b/app/models/action_page.rb @@ -1,28 +1,28 @@ -class ActionPage < ActiveRecord::Base - extend FriendlyId, AmazonCredentials +class ActionPage < ApplicationRecord + extend FriendlyId - include PgSearch + include PgSearch::Model pg_search_scope :search, - against: [ - :title, - :slug, - :summary, - :description, - :email_text, + against: %i[ + title + slug + summary + description + email_text ], associated_against: { - call_campaign: [:title, :message], - congress_message_campaign: [:subject, :message, :campaign_tag], - email_campaign: [:subject, :message], - petition: [:title, :description], - tweet: [:target, :message, :cta] + call_campaign: %i[title message], + congress_message_campaign: %i[subject message campaign_tag], + email_campaign: %i[subject message], + petition: %i[title description], + tweet: %i[target message cta] }, using: { tsearch: { prefix: true } } - friendly_id :title, use: [:slugged, :history] + friendly_id :title, use: %i[slugged history] scope :published, -> { where(published: true) } - has_many :events, class_name: Ahoy::Event + has_many :events, class_name: "Ahoy::Event" has_many :partnerships has_many :partners, through: :partnerships has_many :action_institutions @@ -36,25 +36,19 @@ class ActionPage < ActiveRecord::Base belongs_to :call_campaign belongs_to :category, optional: true belongs_to :active_action_page_for_redirect, class_name: "ActionPage", - foreign_key: "archived_redirect_action_page_id" + foreign_key: "archived_redirect_action_page_id" + belongs_to :author, class_name: "User", foreign_key: :user_id, optional: true + accepts_nested_attributes_for :tweet, :petition, :email_campaign, - :call_campaign, :congress_message_campaign, :affiliation_types, :partnerships, - reject_if: :all_blank - - has_attached_file :featured_image, amazon_credentials.merge(default_url: "missing.png") - has_attached_file :background_image, amazon_credentials - has_attached_file :og_image, amazon_credentials - validates_media_type_spoof_detection :featured_image, - if: -> { featured_image.present? && featured_image_file_name_came_from_user? } - validates_media_type_spoof_detection :background_image, - if: -> { background_image.present? && background_image_file_name_came_from_user? } - validates_media_type_spoof_detection :og_image, - if: -> { og_image.present? && og_image_file_name_came_from_user? } - do_not_validate_attachment_file_type [:featured_image, :background_image, :og_image] - - #validates_length_of :og_title, maximum: 65 + :call_campaign, :congress_message_campaign, :affiliation_types, :partnerships, + reject_if: :all_blank + + mount_uploader :featured_image, ActionPageImageUploader, mount_on: :featured_image_file_name + mount_uploader :background_image, ActionPageImageUploader, mount_on: :background_image_file_name + mount_uploader :og_image, ActionPageImageUploader, mount_on: :og_image_file_name + after_save :no_drafts_on_homepage after_save :set_congress_tag, if: -> { enable_congress_message } @@ -62,18 +56,16 @@ class ActionPage < ActiveRecord::Base def self.type(*types) scopes = Array(types).flatten.map do |t| - unless %w(call congress_message email petition tweet redirect).include?(t) - raise ArgumentError, "unrecognized type #{t}" - end + raise ArgumentError, "unrecognized type #{t}" unless %w[call congress_message email petition tweet redirect].include?(t) - where(:"enable_#{t}" => true) + where("enable_#{t}": true) end scopes.inject(:or) || all end def action_type - %w(call congress_message email petition tweet redirect).each do |type| + %w[call congress_message email petition tweet redirect].each do |type| return type.titleize if self[:"enable_#{type}"] end @@ -81,9 +73,8 @@ def action_type end def self.status(status) - unless %w(archived victory live draft).include?(status) - raise ArgumentError, "unrecognized status #{status}" - end + raise ArgumentError, "unrecognized status #{status}" unless %w[archived victory live draft].include?(status) + case status when "live" where(published: true, archived: false, victory: false) @@ -106,14 +97,14 @@ def should_generate_new_friendly_id? def call_tool_title call_campaign && - call_campaign.title.length > 0 && + !call_campaign.title.empty? && call_campaign.title || "Call Your Legislators" end def message_rendered # TODO: just write a test for this and rename this to .to_md - call_campaign && call_campaign.message || "" + call_campaign&.message || "" end def verb @@ -151,6 +142,7 @@ def image def actions_taken_percent return 0 if view_count == 0 + @percent ||= (action_count / view_count.to_f) * 100 end @@ -195,7 +187,8 @@ def no_drafts_on_homepage end def set_congress_tag - return unless congress_message_campaign.campaign_tag.blank? + return if congress_message_campaign.campaign_tag.present? + congress_message_campaign.update(campaign_tag: slug) end end diff --git a/app/models/affiliation.rb b/app/models/affiliation.rb index 39a8b3c7f..3034772e2 100644 --- a/app/models/affiliation.rb +++ b/app/models/affiliation.rb @@ -1,4 +1,4 @@ -class Affiliation < ActiveRecord::Base +class Affiliation < ApplicationRecord belongs_to :action_page belongs_to :signature belongs_to :affiliation_type diff --git a/app/models/affiliation_type.rb b/app/models/affiliation_type.rb index 3a83e66b7..9a084ab4a 100644 --- a/app/models/affiliation_type.rb +++ b/app/models/affiliation_type.rb @@ -1,4 +1,4 @@ -class AffiliationType < ActiveRecord::Base +class AffiliationType < ApplicationRecord belongs_to :action_page has_many :affiliations end diff --git a/app/models/ahoy/event.rb b/app/models/ahoy/event.rb index 942bfae6e..fe7322748 100644 --- a/app/models/ahoy/event.rb +++ b/app/models/ahoy/event.rb @@ -1,17 +1,16 @@ module Ahoy - class Event < ActiveRecord::Base + class Event < ApplicationRecord self.table_name = "ahoy_events" belongs_to :visit belongs_to :user belongs_to :action_page counter_culture :action_page, column_name: proc { |record| - if record.name == "Action" + case record.name + when "Action" "action_count" - elsif record.name == "View" + when "View" "view_count" - else - nil end } @@ -22,25 +21,23 @@ class Event < ActiveRecord::Base scope :calls, -> { where("properties ->> 'actionType' = 'call'") } scope :signatures, -> { where("properties ->> 'actionType' = 'signature'") } scope :tweets, -> { where("properties ->> 'actionType' = 'tweet'") } - scope :on_page, -> (id) { where(action_page_id: id) } - scope :in_range, ->(start_date, end_date) { + scope :on_page, ->(id) { where(action_page_id: id) } + scope :in_range, lambda { |start_date, end_date| where(time: start_date..end_date.tomorrow) } - before_save :user_opt_out - before_save :anonymize_views after_create :record_civicrm - TYPES = %i(views emails tweets calls signatures congress_messages).freeze + TYPES = %i[views emails tweets calls signatures congress_messages].freeze def self.action_types(action_page = nil) TYPES.dup.tap do |t| if action_page.present? - t.delete(:calls) if !action_page.enable_call - t.delete(:congress_messages) if !action_page.enable_congress_message - t.delete(:emails) if !action_page.enable_email - t.delete(:signatures) if !action_page.enable_petition - t.delete(:tweets) if !action_page.enable_tweet + t.delete(:calls) unless action_page.enable_call + t.delete(:congress_messages) unless action_page.enable_congress_message + t.delete(:emails) unless action_page.enable_email + t.delete(:signatures) unless action_page.enable_petition + t.delete(:tweets) unless action_page.enable_tweet end end end @@ -86,20 +83,9 @@ def self.summary { view: views.count, action: actions.count } end - def user_opt_out - if user - user_id = nil unless user.record_activity? - end - end - def record_civicrm - if name == "Action" && user && action_page_id - user.add_civicrm_activity! action_page_id - end - end - - def anonymize_views - self.user_id = nil if name == "View" + return unless name == "Action" && user && action_page_id + user.add_civicrm_activity! action_page_id end end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 000000000..10a4cba84 --- /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/bounce.rb b/app/models/bounce.rb index a3deb6c0a..c1780fdaa 100644 --- a/app/models/bounce.rb +++ b/app/models/bounce.rb @@ -1,2 +1,2 @@ -class Bounce < ActiveRecord::Base +class Bounce < ApplicationRecord end diff --git a/app/models/call_campaign.rb b/app/models/call_campaign.rb index 37c8f7d9d..dd8c55198 100644 --- a/app/models/call_campaign.rb +++ b/app/models/call_campaign.rb @@ -1,3 +1,3 @@ -class CallCampaign < ActiveRecord::Base +class CallCampaign < ApplicationRecord has_one :action_page end diff --git a/app/models/category.rb b/app/models/category.rb index e3960204f..aa92bc0c4 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -1,4 +1,4 @@ -class Category < ActiveRecord::Base - validates_presence_of :title - validates_format_of :title, with: /\A[\w\s&]+\Z/ +class Category < ApplicationRecord + validates :title, presence: true + validates :title, format: { with: /\A[\w\s&]+\Z/ } end diff --git a/app/models/complaint.rb b/app/models/complaint.rb index b0904e938..3b14e25b0 100644 --- a/app/models/complaint.rb +++ b/app/models/complaint.rb @@ -1,2 +1,2 @@ -class Complaint < ActiveRecord::Base +class Complaint < ApplicationRecord end diff --git a/app/models/congress_member.rb b/app/models/congress_member.rb index 43317fa2f..83fc6d3dc 100644 --- a/app/models/congress_member.rb +++ b/app/models/congress_member.rb @@ -1,19 +1,19 @@ -class CongressMember < ActiveRecord::Base - validates_uniqueness_of :bioguide_id +class CongressMember < ApplicationRecord + validates :bioguide_id, uniqueness: true - scope :current, -> { where("? <= term_end", Time.now) } + scope :current, -> { where("? <= term_end", Time.zone.now) } - scope :filter, ->(f) do + scope :search, lambda { |f| if f.present? fields = "first_name || ' ' || last_name || ' ' || full_name || ' ' || bioguide_id" where("LOWER(#{fields}) LIKE ?", "%#{f.downcase}%") else all end - end + } def current? - Time.now <= term_end + Time.zone.now <= term_end end def senate? diff --git a/app/models/congress_message.rb b/app/models/congress_message.rb index b67398c5e..29d382fef 100644 --- a/app/models/congress_message.rb +++ b/app/models/congress_message.rb @@ -7,11 +7,16 @@ class CongressMessage attr_accessor :forms, :campaign attr_writer :common_attributes, :member_attributes - ALWAYS_COMMON = %w($NAME_FIRST $NAME_LAST $ADDRESS_CITY $ADDRESS_STATE - $ADDRESS_STREET $ADDRESS_ZIP5 $EMAIL).freeze + ALWAYS_COMMON = %w[$NAME_FIRST $NAME_LAST $ADDRESS_CITY $ADDRESS_STATE + $ADDRESS_STREET $ADDRESS_ZIP5 $EMAIL].freeze - def common_attributes() @common_attributes || {}; end - def member_attributes() @member_attributes || {}; end + def common_attributes + @common_attributes || {} + end + + def member_attributes + @member_attributes || {} + end def self.new_from_lookup(location, campaign, forms) common_attributes = { @@ -20,13 +25,13 @@ def self.new_from_lookup(location, campaign, forms) } if location common_attributes.merge!({ - "$ADDRESS_STREET" => location.street, - "$ADDRESS_CITY" => location.city, - "$ADDRESS_ZIP4" => location.zip4, - "$ADDRESS_ZIP5" => location.zipcode, - "$ADDRESS_STATE" => location.state, - "$ADDRESS_STATE_POSTAL_ABBREV" => location.state - }) + "$ADDRESS_STREET" => location.street, + "$ADDRESS_CITY" => location.city, + "$ADDRESS_ZIP4" => location.zip4, + "$ADDRESS_ZIP5" => location.zipcode, + "$ADDRESS_STATE" => location.state, + "$ADDRESS_STATE_POSTAL_ABBREV" => location.state + }) end new({ common_attributes: common_attributes, forms: forms, campaign: campaign }) end @@ -45,7 +50,7 @@ def forms_minus_common_fields @forms.map do |form| form_minus = form.dup form_minus.fields = form.fields - .reject { |x| common_fields.include?(x) } + .reject { |x| common_fields.include?(x) } form_minus end end @@ -56,12 +61,15 @@ def targets def attributes_for(bioguide_id) return common_attributes unless member_attributes[bioguide_id] + common_attributes.merge(member_attributes[bioguide_id]) end - def background_submit(test = false) - if valid? - @forms.each { |f| f.delay.fill(attributes_for(f.bioguide_id), campaign.campaign_tag, test) } + def background_submit(test: false) + return unless valid? + + @forms.each do |f| + f.delay.fill(attributes_for(f.bioguide_id), campaign.campaign_tag, test) end end @@ -77,9 +85,7 @@ def attributes_satisfy_forms @forms.each do |form| attributes = attributes_for(form.bioguide_id) form.fields.each do |field| - if !field.validate(attributes[field.value]) - errors.add(:base, "Invalid input for #{field.value}") - end + errors.add(:base, "Invalid input for #{field.value}") unless field.validate(attributes[field.value]) end end end diff --git a/app/models/congress_message_campaign.rb b/app/models/congress_message_campaign.rb index 3ec85b171..3e2ee674e 100644 --- a/app/models/congress_message_campaign.rb +++ b/app/models/congress_message_campaign.rb @@ -1,4 +1,4 @@ -class CongressMessageCampaign < ActiveRecord::Base +class CongressMessageCampaign < ApplicationRecord belongs_to :topic_category has_one :action_page @@ -65,7 +65,7 @@ def target_specific_legislators private def target_bioguide_text_or_default(custom_text, default) - if !target_bioguide_ids or custom_text.blank? + if !target_bioguide_ids || custom_text.blank? default else custom_text diff --git a/app/models/congress_scorecard.rb b/app/models/congress_scorecard.rb index 092f922c8..ecca65aeb 100644 --- a/app/models/congress_scorecard.rb +++ b/app/models/congress_scorecard.rb @@ -2,7 +2,7 @@ # petition action that drew in a signature from one of their constituents # So calling #counter on a model will display the number of signatures from # that member's constituents -class CongressScorecard < ActiveRecord::Base +class CongressScorecard < ApplicationRecord belongs_to :action_page def increment! diff --git a/app/models/email_campaign.rb b/app/models/email_campaign.rb index 604935b9b..fceb74b87 100644 --- a/app/models/email_campaign.rb +++ b/app/models/email_campaign.rb @@ -1,7 +1,10 @@ -class EmailCampaign < ActiveRecord::Base +class EmailCampaign < ApplicationRecord belongs_to :topic_category has_one :action_page + # No DC + STATES = %w[AK AL AR AZ CA CO CT DE FL GA HI IA ID IL IN KS KY LA MA MD ME MI MN MO MS MT NC ND NE NH NJ NM NV NY OH OK OR PA RI SC SD TN TX UT VA VT WA WI WV WY].freeze + def email_your_rep_text(default) target_bioguide_text_or_default alt_text_email_your_rep, default end @@ -22,19 +25,40 @@ def extra_fields_explain_text(default) target_bioguide_text_or_default alt_text_extra_fields_explain, default end + def leg_level + return "legislatorLowerBody" if target_state_lower_chamber + return "legislatorUpperBody" if target_state_upper_chamber + return "headOfGovernment" if target_governor + + "" + end + include ERB::Util - def service_uri(service) - mailto_addresses = email_addresses.split(/\s*,\s*/).map do |email| - u(email.gsub(" ", "")).gsub("%40", "@") + def service_uri(service, opts = {}) + mailto_addresses = opts[:email] + mailto_addresses ||= email_addresses + # look for custom email addresses set on the back end if there is no email param from the front-end, + # as is the case when we send state-level emails -- we cannot store these email address in our db, + # reason below: + + # https://developers.google.com/terms#e_prohibitions_on_content + # Section 5.e.1., as of December 2022 + # e. Prohibitions on Content + # Unless expressly permitted by the content owner or by applicable law, you will not, and will not permit your end users or others acting on your behalf to, do the following with content returned from the APIs: + # Scrape, build databases, or otherwise create permanent copies of such content, or keep cached copies longer than permitted by the cache header; + + # results in comma-separated string of email addresses + default_mailto_addresses ||= mailto_addresses.split(/\s*,\s*/).map do |email| + u(email.delete(" ")).gsub("%40", "@").gsub("%2B", "+") end.join(",") { - default: "mailto:#{mailto_addresses}?#{query(body: message, subject: subject)}", + default: "mailto:#{default_mailto_addresses}?#{query(body: message, subject: subject)}", - gmail: "https://mail.google.com/mail/?view=cm&fs=1&#{{ to: email_addresses, body: message, su: subject }.to_query}", + gmail: "https://mail.google.com/mail/?view=cm&fs=1&#{{ to: mailto_addresses, body: message, su: subject }.to_query}", - hotmail: "https://outlook.live.com/default.aspx?rru=compose&#{{ to: email_addresses, body: message, subject: subject }.to_query}#page=Compose" + hotmail: "https://outlook.live.com/default.aspx?rru=compose&#{{ to: mailto_addresses, body: message, subject: subject }.to_query}#page=Compose" }.fetch(service.to_sym) end @@ -48,7 +72,7 @@ def query(hash) end def target_bioguide_text_or_default(custom_text, default) - if !target_bioguide_id or custom_text.blank? + if !target_bioguide_id || custom_text.blank? default else custom_text diff --git a/app/models/featured_action_page.rb b/app/models/featured_action_page.rb index 78a0c3bae..60deafad2 100644 --- a/app/models/featured_action_page.rb +++ b/app/models/featured_action_page.rb @@ -1,6 +1,6 @@ -class FeaturedActionPage < ActiveRecord::Base +class FeaturedActionPage < ApplicationRecord belongs_to :action_page - validates_presence_of :action_page, :weight + validates :action_page, :weight, presence: true def initialize(attributes = {}) super(attributes.reverse_merge(weight: 0)) @@ -9,7 +9,8 @@ def initialize(attributes = {}) def self.load_for_edit existing = order(:weight).preload(:action_page) return existing unless existing.length < 4 + weights_to_create = (1..4).to_a - existing.map(&:weight) - existing += weights_to_create.map { |w| new(weight: w) } + existing + weights_to_create.map { |w| new(weight: w) } end end diff --git a/app/models/institution.rb b/app/models/institution.rb index c81c09f8f..ef6a8d1ee 100644 --- a/app/models/institution.rb +++ b/app/models/institution.rb @@ -1,14 +1,14 @@ -class Institution < ActiveRecord::Base +class Institution < ApplicationRecord require "csv" extend FriendlyId - friendly_id :name, use: [:slugged, :history] + friendly_id :name, use: %i[slugged history] - include PgSearch + include PgSearch::Model pg_search_scope :search, - against: [ - :name, - :category + against: %i[ + name + category ], using: { tsearch: { prefix: true } } @@ -16,15 +16,16 @@ class Institution < ActiveRecord::Base has_many :action_pages, through: :action_institutions has_many :affiliations - validates_presence_of :name - validates_uniqueness_of :name - validates_presence_of :category + validates :name, presence: true + validates :name, uniqueness: true + validates :category, presence: true def self.process_csv(csv_file) [].tap do |names| CSV.foreach(csv_file.path, headers: true) do |row| row = row.to_hash return [] unless row["name"] + names << row["name"] end end @@ -39,22 +40,12 @@ def self.import(category, names) end def self.categories - all.distinct.pluck(:category) - end - - # Sort institutions by most popular. - # Put `first` at the top of the list if it exists. - def self.top(n, first: 0) - select("institutions.*, COUNT(signatures.id) AS s_count") - .joins("LEFT OUTER JOIN affiliations ON institutions.id = affiliations.institution_id") - .joins("LEFT OUTER JOIN signatures ON affiliations.signature_id = signatures.id") - .group("institutions.id") - .order("institutions.id = #{first.to_i} desc", "s_count DESC", "institutions.name") - .limit(n) + distinct.pluck(:category) end def included_in_active_actions? return false if action_pages.empty? + action_pages.map(&:status).any? "live" end end diff --git a/app/models/partner.rb b/app/models/partner.rb index 4f665124f..703d73799 100644 --- a/app/models/partner.rb +++ b/app/models/partner.rb @@ -1,17 +1,11 @@ -class Partner < ActiveRecord::Base - extend AmazonCredentials +class Partner < ApplicationRecord acts_as_paranoid has_many :subscriptions has_many :users has_many :partnerships has_many :action_pages, through: :partnerships - has_attached_file :logo, amazon_credentials - - validates_media_type_spoof_detection :logo, - if: -> { logo.present? && logo_file_name_came_from_user? } - do_not_validate_attachment_file_type [:logo] - validates_uniqueness_of :code + validates :code, uniqueness: true def to_csv(options = {}) column_names = %w[first_name last_name email created_at] diff --git a/app/models/partnership.rb b/app/models/partnership.rb index 7f832d22b..0576af137 100644 --- a/app/models/partnership.rb +++ b/app/models/partnership.rb @@ -1,4 +1,4 @@ -class Partnership < ActiveRecord::Base +class Partnership < ApplicationRecord belongs_to :partner belongs_to :action_page end diff --git a/app/models/petition.rb b/app/models/petition.rb index 6c864dc1b..6869f85fe 100644 --- a/app/models/petition.rb +++ b/app/models/petition.rb @@ -1,20 +1,21 @@ -class Petition < ActiveRecord::Base +class Petition < ApplicationRecord has_one :action_page has_many :signatures after_initialize :set_goal def percent_complete return 0 if goal == 0 - [signatures.count.to_f / goal.to_f, 1].min * 100 + + [signatures.count / goal.to_f, 1].min * 100 end def recent_signatures(num) recent = [] - signatures.last(num).reverse.each do |s| + signatures.last(num).reverse_each do |s| if s.anonymous - recent.push(s.as_json(only: [], methods: [:time_ago, :location])) + recent.push(s.as_json(only: [], methods: %i[time_ago location])) else - recent.push(s.as_json(only: [:first_name, :last_name, :city], methods: [:time_ago, :location])) + recent.push(s.as_json(only: %i[first_name last_name city], methods: %i[time_ago location])) end end recent @@ -22,7 +23,7 @@ def recent_signatures(num) def signatures_by_institution(institution) signatures.includes(affiliations: :institution) - .where(institutions: { id: institution }) + .where(institutions: { id: institution }) end def location_required? @@ -30,14 +31,12 @@ def location_required? end def to_s - "#{title}-exported_on-#{DateTime.now.strftime("%Y-%m-%d")}" + "#{title}-exported_on-#{DateTime.now.strftime('%Y-%m-%d')}" end private def set_goal - if new_record? - goal = 100 - end + self.goal = 100 if new_record? end end diff --git a/app/models/signature.rb b/app/models/signature.rb index 7d797c471..ff7a34449 100644 --- a/app/models/signature.rb +++ b/app/models/signature.rb @@ -1,16 +1,16 @@ -include GoingPostal +class Signature < ApplicationRecord + include GoingPostal -class Signature < ActiveRecord::Base - belongs_to :user + belongs_to :user, optional: true belongs_to :petition has_many :affiliations before_validation :format_zipcode - before_save :sanitize_input - validates_presence_of :first_name, :last_name, :petition_id, - message: "This can't be blank." + before_validation :sanitize_input + validates :first_name, :last_name, :petition_id, + presence: { message: "This can't be blank." } - validates_presence_of :country_code, if: :location_required? + validates :country_code, presence: { if: :location_required? } validates :email, email: true validates :email, uniqueness: { scope: :petition_id, @@ -20,15 +20,15 @@ class Signature < ActiveRecord::Base accepts_nested_attributes_for :affiliations, reject_if: :all_blank - scope :filter, ->(f) do + scope :search, lambda { |f| if f.present? - where("LOWER(email) LIKE ? " + + where("LOWER(email) LIKE ? " \ "OR LOWER(first_name || ' ' || last_name) LIKE ?", "%#{f}%".downcase, "%#{f}%".downcase) else all end - end + } include ActionView::Helpers::DateHelper @@ -38,7 +38,7 @@ def self.to_csv(options = {}) CSV.generate(options) do |csv| csv << column_names - all.each do |sub| + find_each do |sub| csv << sub.attributes.values_at(*column_names) end end @@ -47,22 +47,22 @@ def self.to_csv(options = {}) def self.to_presentable_csv(options = {}) column_names = %w[full_name email city state country] - CSV.generate(options) do |csv| + CSV.generate(**options) do |csv| csv << column_names - all.each do |signature| + find_each do |signature| csv << signature.to_csv_line end end end def self.to_affiliation_csv(options = {}) - column_names = %w[full_name, institution, affiliation_type] + column_names = %w[full_name institution affiliation_type] CSV.generate(options) do |csv| csv << column_names - all.each do |s| + find_each do |s| affiliation = s.affiliations.first or next csv << [ @@ -75,18 +75,18 @@ def self.to_affiliation_csv(options = {}) end def self.institutions - joins(affiliations: :institution). - distinct.pluck("institutions.name, institutions.id").sort + joins(affiliations: :institution) + .distinct.pluck("institutions.name, institutions.id").sort end def self.pretty_count - ActiveSupport::NumberHelper::number_to_delimited(self.count, delimiter: ",") + ActiveSupport::NumberHelper.number_to_delimited(count, delimiter: ",") end def arbitrary_opinion_of_country_string_validity - if country_code.present? and full_country_name.nil? - errors.add(:country_code, "Country Code might come from a spam bot.") - end + return unless country_code.present? && full_country_name.nil? + + errors.add(:country_code, "Country Code might come from a spam bot.") end def name @@ -118,17 +118,15 @@ def location_required? end def full_country_name - begin - IsoCountryCodes.find(country_code).name - rescue IsoCountryCodes::UnknownCodeError - nil - end + IsoCountryCodes.find(country_code).name + rescue IsoCountryCodes::UnknownCodeError + nil end private def format_zipcode - zipcode = GoingPostal.format_zipcode(zipcode, country_code) || zipcode + self.zipcode = GoingPostal.format_zipcode(zipcode, country_code) || zipcode end def sanitize_input @@ -143,8 +141,8 @@ def sanitize_input end def validate_zipcode - unless GoingPostal.valid_zipcode?(zipcode, country_code) - errors.add(:zipcode, "Invalid zip/postal code for country") - end + return if GoingPostal.valid_zipcode?(zipcode, country_code) + + errors.add(:zipcode, "Invalid zip/postal code for country") end end diff --git a/app/models/source_file.rb b/app/models/source_file.rb index 63b1022ca..8574ae639 100644 --- a/app/models/source_file.rb +++ b/app/models/source_file.rb @@ -1,7 +1,7 @@ -class SourceFile < ActiveRecord::Base +class SourceFile < ApplicationRecord include Rails.application.routes.url_helpers - validates_presence_of :file_name, :file_content_type, :file_size, :key, :bucket + validates :file_name, :file_content_type, :file_size, :key, :bucket, presence: true delegate :secrets, to: "Rails.application".to_sym @@ -23,8 +23,16 @@ class SourceFile < ActiveRecord::Base def pull_down_s3_object_attributes Rails.logger.debug "Trying to validate S3 object." self.file_name = key.split("/").last if key - self.file_size ||= s3_object.content_length rescue nil - self.file_content_type ||= s3_object.content_type rescue nil + self.file_size ||= begin + s3_object.content_length + rescue StandardError + nil + end + self.file_content_type ||= begin + s3_object.content_type + rescue StandardError + nil + end false end @@ -33,7 +41,7 @@ def full_url if ENV["amazon_bucket_url"] "https://#{ENV['amazon_bucket_url']}/#{key}" else # we have to build the url up from amazon information - "https://#{ENV['amazon_bucket']}.#{AmazonCredentials.build_s3_host_name}/#{key}" + "https://#{ENV['amazon_bucket']}.s3-#{Rails.application.secrets.amazon_region}.amazonaws.com/#{key}" end end @@ -43,7 +51,7 @@ def to_jq_upload "name" => file_name, "size" => file_size, "full_url" => full_url, - "image" => self.is_image?, + "image" => is_image?, "delete_url" => Rails.application.routes.url_helpers.admin_source_file_path(self, format: :json) } end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 81cf3cbf4..36322681b 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,4 +1,4 @@ -class Subscription < ActiveRecord::Base +class Subscription < ApplicationRecord belongs_to :partner, counter_cache: true validates :email, presence: true, email: true validates :email, uniqueness: { scope: :partner_id } diff --git a/app/models/topic.rb b/app/models/topic.rb index 3e69c04c2..667c86dd5 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1,3 +1,3 @@ -class Topic < ActiveRecord::Base +class Topic < ApplicationRecord belongs_to :topic_set end diff --git a/app/models/topic_category.rb b/app/models/topic_category.rb index fd08268bc..a6c8aff5b 100644 --- a/app/models/topic_category.rb +++ b/app/models/topic_category.rb @@ -1,19 +1,17 @@ -class TopicCategory < ActiveRecord::Base +class TopicCategory < ApplicationRecord has_many :topic_sets, dependent: :destroy has_many :email_campaigns has_many :congress_message_campaigns def as_2d_array - arr = [] - topic_sets.order(:tier).each do |ts| - arr.push ts.topics.map { |t| t.name } + arr = topic_sets.order(:tier).map do |ts| + ts.topics.map(&:name) end - arr end def best_match(options) topics = topic_sets.order(:tier).reduce([]) do |arr, ts| - arr += ts.topics + arr + ts.topics end topics.each do |topic| diff --git a/app/models/topic_set.rb b/app/models/topic_set.rb index 916375ee2..ecda608f8 100644 --- a/app/models/topic_set.rb +++ b/app/models/topic_set.rb @@ -1,4 +1,4 @@ -class TopicSet < ActiveRecord::Base +class TopicSet < ApplicationRecord belongs_to :topic_category has_many :topics, dependent: :destroy end diff --git a/app/models/tweet.rb b/app/models/tweet.rb index ae5d52be7..f94782e25 100644 --- a/app/models/tweet.rb +++ b/app/models/tweet.rb @@ -1,9 +1,9 @@ -class Tweet < ActiveRecord::Base +class Tweet < ApplicationRecord has_one :action_page has_many :tweet_targets alias :targets :tweet_targets accepts_nested_attributes_for :tweet_targets, reject_if: :all_blank, - allow_destroy: true + allow_destroy: true def target_congress? target_house? || target_senate? diff --git a/app/models/tweet_target.rb b/app/models/tweet_target.rb index 57b154db4..bdb5d43bc 100644 --- a/app/models/tweet_target.rb +++ b/app/models/tweet_target.rb @@ -1,34 +1,7 @@ -class TweetTarget < ActiveRecord::Base - extend AmazonCredentials - require "open-uri" - +class TweetTarget < ApplicationRecord belongs_to :tweet - has_attached_file :image, amazon_credentials - validates_media_type_spoof_detection :image, if: ->() { image_file_name.present? } - do_not_validate_attachment_file_type :image - after_save :attach_twitter_image def url - "https://twitter.com/" + twitter_id - end - - def image_url - image.url - end - - def attach_twitter_image - self.delay.attach_twitter_image_without_delay if image_file_name.nil? and Twitter.has_api_keys? - end - - def attach_twitter_image_without_delay - access_token = Twitter.prepare_access_token Rails.application.secrets.twitter_oauth_token, Rails.application.secrets.twitter_oauth_token_secret - - # ref: https://dev.twitter.com/overview/general/user-profile-images-and-banners - response = access_token.request(:get, "https://api.twitter.com/1.1/users/show.json?screen_name=" + twitter_id) - user_info = JSON.parse response.body - user_image_url = user_info["profile_image_url_https"].gsub(/_normal\./, "_bigger.") - - self.image = URI.parse(user_image_url) - self.save + "https://twitter.com/#{twitter_id}" end end diff --git a/app/models/twitter.rb b/app/models/twitter.rb index 117123ec9..0ea83eaac 100644 --- a/app/models/twitter.rb +++ b/app/models/twitter.rb @@ -1,21 +1,22 @@ class Twitter def self.has_api_keys? - !Rails.application.secrets.twitter_api_key.blank? && - !Rails.application.secrets.twitter_api_secret.blank? && - !Rails.application.secrets.twitter_oauth_token.blank? && - !Rails.application.secrets.twitter_oauth_token_secret.blank? + Rails.application.secrets.twitter_api_key.present? && + Rails.application.secrets.twitter_api_secret.present? && + Rails.application.secrets.twitter_oauth_token.present? && + Rails.application.secrets.twitter_oauth_token_secret.present? end # ref: https://dev.twitter.com/oauth/overview/single-user def self.prepare_access_token(oauth_token, oauth_token_secret) consumer = OAuth::Consumer.new( - Rails.application.secrets.twitter_api_key, - Rails.application.secrets.twitter_api_secret, - { site: "https://api.twitter.com", scheme: :header }) + Rails.application.secrets.twitter_api_key, + Rails.application.secrets.twitter_api_secret, + { site: "https://api.twitter.com", scheme: :header } + ) # now create the access token object from passed values token_hash = { oauth_token: oauth_token, oauth_token_secret: oauth_token_secret } - access_token = OAuth::AccessToken.from_hash(consumer, token_hash) + OAuth::AccessToken.from_hash(consumer, token_hash) end end diff --git a/app/models/user.rb b/app/models/user.rb index 526b66a5c..0a11d5889 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,9 +1,9 @@ require "civicrm" -class User < ActiveRecord::Base - include CiviCRM::UserMethods +class User < ApplicationRecord + include Civicrm::UserMethods - include PgSearch - pg_search_scope :search, against: [:email, :first_name, :last_name] + include PgSearch::Model + pg_search_scope :search, against: %i[email first_name last_name] # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable @@ -12,7 +12,7 @@ class User < ActiveRecord::Base remember_for: 90.days has_many :signatures has_many :user_preferences - has_many :events, class_name: Ahoy::Event + has_many :events, class_name: "Ahoy::Event" belongs_to :partner validates :email, email: true validate :password_complexity @@ -25,7 +25,7 @@ class User < ActiveRecord::Base alias :preferences :user_preferences - scope :authors, ->() { joins(:action_pages).distinct } + scope :authors, -> { joins(:action_pages).distinct } def self.group_created_in_range(start_date, end_date) if start_date == end_date @@ -42,11 +42,13 @@ def invalidate_password_reset_tokens end def email_taken? - errors.added? :email, :taken + return false unless errors.include? :email + + errors.details[:email].any? { |x| x.value?(:taken) } end def send_email_taken_notice - if self.confirmed? + if confirmed? UserMailer.signup_attempt_with_existing_email(self).deliver_now else send_confirmation_instructions @@ -54,9 +56,7 @@ def send_email_taken_notice end def password_complexity - if admin? && password.present? and password.length < 30 - errors.add :password, "must be at least 30 (try choosing 6 memorable words)" - end + errors.add :password, "must be at least 30 (try choosing 6 memorable words)" if admin? && password.present? && (password.length < 30) end def name @@ -64,21 +64,9 @@ def name end def display_name - return name unless name.blank? - email - end - - def percentile_rank - user_action_counts = Rails.cache.fetch("user_action_counts", expires_in: 24.hours) { - User.select("users.id, count(ahoy_events.id) AS events_count") - .joins("LEFT OUTER JOIN ahoy_events ON ahoy_events.user_id = users.id") - .where("ahoy_events.name IS null OR ahoy_events.name = ?", "Action") - .group("users.id") - .map { |u| u.events_count } - } + return name if name.present? - user_count = events.actions.count - percentile = user_action_counts.percentile_rank(user_count - 1).round(0) + email end def signed?(petition) @@ -124,19 +112,25 @@ def privileged_role? # This is here for collission avoidance when generating new user names in tests def self.next_id - self.last.nil? ? 1 : self.last.id + 1 + last.nil? ? 1 : last.id + 1 end # We're allowing unconfirmed users to reset their passwords by # re-registering. In that case, they shouldn't get a password reset # notification. def send_password_change_notification? - self.confirmed? && super + confirmed? && super + end + + def can_view_archived?(action_page) + return true if admin? + + taken_action? action_page end protected def after_confirmation - subscribe!(opt_in = true) if self.subscribe? + subscribe!(opt_in: true) if subscribe? end end diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 9e7860cb4..200d8ccf9 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -1,5 +1,5 @@ -class UserPreference < ActiveRecord::Base +class UserPreference < ApplicationRecord belongs_to :user - validates_presence_of :name - validates_presence_of :value + validates :name, presence: true + validates :value, presence: true end diff --git a/app/models/visit.rb b/app/models/visit.rb index 6bb47fed0..9bcf891d7 100644 --- a/app/models/visit.rb +++ b/app/models/visit.rb @@ -1,4 +1,4 @@ -class Visit < ActiveRecord::Base +class Visit < ApplicationRecord has_many :ahoy_events, class_name: "Ahoy::Event" belongs_to :user end diff --git a/app/queries/action_page_filters.rb b/app/queries/action_page_filters.rb index 98d379cd9..d0ba042d3 100644 --- a/app/queries/action_page_filters.rb +++ b/app/queries/action_page_filters.rb @@ -14,6 +14,7 @@ def run filters.each do |f, val| next unless valid_query?(f, val) + @relation = if NAMED_SCOPES.include? f relation.send(f, val) else @@ -25,8 +26,8 @@ def run private - NAMED_SCOPES = %i(type status).freeze - VALID_FILTERS = %i(type status author category).freeze + NAMED_SCOPES = %i[type status].freeze + VALID_FILTERS = %i[type status author category].freeze attr_accessor :relation, :filters @@ -35,7 +36,8 @@ def empty_value?(val) end def process_date_range - return unless filters[:date_range].present? + return if filters[:date_range].blank? + start_date, end_date = parse_date_range @relation = relation.where(created_at: start_date..(end_date + 1.day)) end @@ -50,8 +52,6 @@ def valid_query?(f, val) end def validate_filter_name(f) - unless VALID_FILTERS.include? f - raise ArgumentError, "unrecognized filter #{f}" - end + raise ArgumentError, "unrecognized filter #{f}" unless VALID_FILTERS.include? f end end diff --git a/app/queries/top_institutions_query.rb b/app/queries/top_institutions_query.rb new file mode 100644 index 000000000..10215ec51 --- /dev/null +++ b/app/queries/top_institutions_query.rb @@ -0,0 +1,34 @@ +class TopInstitutionsQuery + def self.run(**args) + new(**args).run + end + + def initialize(action_page:, limit: 300, exclude: []) + @action_page = action_page + @limit = limit + @exclusions = exclude.compact.map(&:id) + end + + def run + action_page.institutions + .where.not(id: exclusions) + .left_outer_joins(affiliations: [:signature]) + .distinct + # TODO: break up / document + .select("institutions.*, COUNT(signatures.id) AS sig_count") + .group("institutions.id") + .order("sig_count DESC", "institutions.name") + .limit(limit) + end + + private + + attr_reader :action_page, :limit, :exclusions + + def source_collection + @source_collection ||= action_page.institutions + return @source_collection if exclusions.empty? + + @source_collection = @source_collection.where.not(id: exclusions) + end +end diff --git a/app/uploaders/action_page_image_uploader.rb b/app/uploaders/action_page_image_uploader.rb new file mode 100644 index 000000000..43a5ffa3d --- /dev/null +++ b/app/uploaders/action_page_image_uploader.rb @@ -0,0 +1,27 @@ +class ActionPageImageUploader < CarrierWave::Uploader::Base + storage :fog + # cache_storage :file + + def default_url(*args) + "missing.png" + end + + def store_dir + "#{model.class.to_s.pluralize.underscore}/#{mounted_as.to_s.pluralize}/#{id_partition}/#{style}" + end + + # we could set this here or config/initializers/carrier_wave.rb + def asset_host + Rails.application.secrets.amazon_bucket_url.present? ? "https://#{Rails.application.secrets.amazon_bucket_url}" : super + end + + private + + def style + version_name || "original" + end + + def id_partition # mimics paperclips mapping function + ("%09d" % model.id).scan(/\d{3}/).join("/") + end +end diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb index 676a65476..874cce619 100644 --- a/app/validators/email_validator.rb +++ b/app/validators/email_validator.rb @@ -1,7 +1,5 @@ class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i - record.errors[attribute] << (options[:message] || "is not an email") - end + record.errors[attribute] << (options[:message] || "is not an email") unless /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value) end end diff --git a/app/views/action_page/_meta_tags.html.erb b/app/views/action_page/_meta_tags.html.erb index 382706cc6..5136ff61d 100644 --- a/app/views/action_page/_meta_tags.html.erb +++ b/app/views/action_page/_meta_tags.html.erb @@ -1,7 +1,7 @@ <% summary = strip_tags stripdown(@actionPage.summary) -%> <% - image_url = if @actionPage.image.try(:exists?) - image_url(@actionPage.image.to_s) + image_url = if @actionPage.image.present? + image_url(@actionPage.image.url) else image_url("og-image-default.png") end diff --git a/app/views/action_page/_partner_logos.html.erb b/app/views/action_page/_partner_logos.html.erb deleted file mode 100644 index 27b862cff..000000000 --- a/app/views/action_page/_partner_logos.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<% if @actionPage.partners.present? && @actionPage.partners.any? { |p| p.logo.present? } %> -

Who We Are

-
- <%= image_tag "EFF_Monogram-red" %> - <% @actionPage.partners.each do |partner| %> - <%= image_tag partner.logo if partner.logo.present? %> - <% end %> -
-<% end %> diff --git a/app/views/action_page/_state_reps.html.erb b/app/views/action_page/_state_reps.html.erb new file mode 100644 index 000000000..58740e8d3 --- /dev/null +++ b/app/views/action_page/_state_reps.html.erb @@ -0,0 +1,17 @@ +
+ <%= "This action is for the #{legislative_level_from_state_representative_info(@email_campaign.leg_level)}." %> + <% @state_reps.each do |sr| %> +
+ <%= "Your representative is #{sr['name']}" %>. + <% if sr["emails"].present? %> + <%= "They can be reached at: #{sr['emails'].join(', ')}" %> + <% else %> + <%= "We could not find their email address." %> + <% end %> +
+ <% end %> +
+ +
+ <%= render 'tools/send_email' %> +
diff --git a/app/views/action_page/index.atom.builder b/app/views/action_page/index.atom.builder index cf969cdc4..6b300b7ae 100644 --- a/app/views/action_page/index.atom.builder +++ b/app/views/action_page/index.atom.builder @@ -1,19 +1,22 @@ atom_feed do |feed| - feed.title(t :site_title) - feed.subtitle(t :summary) - feed.updated(@actionPages[0].created_at) if @actionPages.length > 0 + feed.title(t(:site_title)) + feed.subtitle(t(:summary)) + feed.updated(@actionPages[0].created_at) unless @actionPages.empty? @actionPages.each do |actionPage| feed.entry(actionPage) do |entry| - entry.link(rel: "enclosure", type: actionPage.featured_image.content_type || "image/png", - href: URI.join(root_url, image_path(actionPage.featured_image))) + entry.link( + rel: "enclosure", + type: (actionPage.featured_image.content_type.presence || "image/png"), + href: URI.join(root_url, image_path(actionPage.featured_image.url)) + ) entry.title(actionPage.title) entry.summary(markdown(actionPage.summary), type: "html") entry.content(markdown(actionPage.description), type: "html") entry.author do |author| - author.name(t :organization_name) + author.name(t(:organization_name)) end end end diff --git a/app/views/action_page/index.html.erb b/app/views/action_page/index.html.erb index ecfd70701..321061b8d 100644 --- a/app/views/action_page/index.html.erb +++ b/app/views/action_page/index.html.erb @@ -7,7 +7,7 @@
<%= form_tag action_page_index_path, method: :get, id: "action-page-filter" do %> <%= select_tag :category, - options_from_collection_for_select(Category.all.order(:title).uniq, + options_from_collection_for_select(Category.all.order(:title).distinct, :title, :title, params[:category]), include_blank: "All campaigns" %> <% end %> @@ -18,7 +18,7 @@
<%= link_to action_page_path(actionPage) do%>
- <%= image_tag(actionPage.featured_image) %> + <%= image_tag(actionPage.featured_image.url) %>
<% end %> diff --git a/app/views/action_page/index.json.jbuilder b/app/views/action_page/index.json.jbuilder index 761f1f25b..db98d0fbc 100644 --- a/app/views/action_page/index.json.jbuilder +++ b/app/views/action_page/index.json.jbuilder @@ -4,7 +4,7 @@ json.array! @actionPages do |actionPage| json.summary markdown actionPage.summary if actionPage.featured_image_file_name json.featured_image do - json.alt image_alt actionPage.featured_image_file_name + json.alt actionPage.featured_image_file_name.titleize json.url image_url actionPage.featured_image end end diff --git a/app/views/action_page/show.html.erb b/app/views/action_page/show.html.erb index c025665bd..52fdca997 100644 --- a/app/views/action_page/show.html.erb +++ b/app/views/action_page/show.html.erb @@ -10,11 +10,9 @@
<%= @actionPage.category.title -%>
<% end %>

<%= @actionPage.title -%>

- <%= image_tag(@actionPage.featured_image) %> + <%= image_tag(@actionPage.featured_image.url) %> <%= markdown @actionPage.summary -%> - <%= render "action_page/partner_logos" %> - <% if @actionPage.enable_petition? && @petition && @petition.description.present? && !@actionPage.description.include?('``` letter') -%> <%= render(partial: "action_page/letter", layout: false) %> <% end -%> diff --git a/app/views/admin/action_pages/_email_fields.html.erb b/app/views/admin/action_pages/_email_fields.html.erb index e8ee74353..0a0c98ab7 100644 --- a/app/views/admin/action_pages/_email_fields.html.erb +++ b/app/views/admin/action_pages/_email_fields.html.erb @@ -1,8 +1,4 @@ <%= f.fields_for(:email_campaign) do |sf| %> -
- <%= sf.label :email_addresses, "To" %> - <%= sf.text_field :email_addresses %> -
<%= sf.label :subject %> @@ -13,4 +9,36 @@ <%= sf.label :message %> <%= sf.text_area :message %>
+ +
+ Select State-Level Legislators + + <%= sf.label :state, class: "fancy" do %> + <%= sf.select :state, options_for_select(EmailCampaign::STATES, @actionPage.email_campaign.state), include_blank: "- none -" %> + <% end %> + +
+

For now, please choose only one.

+ <%= sf.label :target_state_lower_chamber do %> + <%= sf.check_box :target_state_lower_chamber, class: "fancy" %> + Lower Chamber + <% end %> + + <%= sf.label :target_state_upper_chamber do %> + <%= sf.check_box :target_state_upper_chamber, class: "fancy" %> + Upper Chamber + <% end %> + + <%= sf.label :target_governor do %> + <%= sf.check_box :target_governor, class: "fancy" %> + Governor + <% end %> +
+ +

+ + <%= sf.label :email_addresses, "Or enter custom email addresses below:" %> + <%= sf.text_field :email_addresses %> +
+ <% end %> diff --git a/app/views/admin/action_pages/_gallery.html.erb b/app/views/admin/action_pages/_gallery.html.erb index 1f315d914..5022f32d7 100644 --- a/app/views/admin/action_pages/_gallery.html.erb +++ b/app/views/admin/action_pages/_gallery.html.erb @@ -1,10 +1,12 @@