From b5690bc77f97b1a88f70838ed047410320b3aea2 Mon Sep 17 00:00:00 2001 From: Taylor Meek Date: Mon, 17 Apr 2023 17:19:14 -0700 Subject: [PATCH 1/2] rspec, rubocop, standardrb, yard --- .rubocop.yml | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++ Gemfile | 16 ++++-- Gemfile.lock | 56 ++++++++++++++++++++ bin/cibuild | 20 ++++++++ 4 files changed, 229 insertions(+), 5 deletions(-) create mode 100644 .rubocop.yml create mode 100755 bin/cibuild diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..aad59cc --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,142 @@ +# Ruby linting configuration. +# We only worry about two kinds of issues: 'error' and anything less than that. +# Error is not about severity, but about taste. Simple style choices that +# never have a great excuse to be broken, such as 1.9 JSON-like hash syntax, +# are errors. Choices that tend to have good exceptions in practice, such as +# line length, are warnings. + +# If you'd like to make changes, a full list of available issues is at +# https://github.com/bbatsov/rubocop/blob/master/config/enabled.yml +# A list of configurable issues is at: +# https://github.com/bbatsov/rubocop/blob/master/config/default.yml +# +# If you disable a check, document why. + +require: rubocop-rails + +Metrics/BlockLength: + ExcludedMethods: ["describe", "context", "included", "class_methods", "it"] + Exclude: + - "**/rails_helper.rb" + +Metrics/ParameterLists: + CountKeywordArgs: false + +Style/StringLiterals: + EnforcedStyle: double_quotes + Severity: error + +Style/HashSyntax: + EnforcedStyle: ruby19 + UseHashRocketsWithSymbolValues: false + Severity: error + Exclude: + - !ruby/regexp /db\/schema.rb/ + +Style/Attr: + Enabled: + false # We have no styleguide guidance here, and it seems to be + # in frequent use. + +Style/ClassAndModuleChildren: + Enabled: false # module X<\n>module Y is just as good as module X::Y. + +Style/Documentation: + Exclude: + - !ruby/regexp /spec\/*\/*/ + +Style/PercentLiteralDelimiters: + PreferredDelimiters: + "%w": "{}" + +Style/MultilineTernaryOperator: + Severity: error + +Style/AndOr: + Severity: error + +Style/FrozenStringLiteralComment: + Enabled: false # Adding comment to all files... + +Style/Alias: + Enabled: false # We have no guidance on alias vs alias_method + +Style/RedundantSelf: + Enabled: false # Sometimes a self.field is a bit more clear + +Style/IfUnlessModifier: + Enabled: false + +Style/SafeNavigation: + Enabled: false + +Lint/AmbiguousBlockAssociation: + Exclude: + - !ruby/regexp /spec\/*\/*/ + +Lint/UnreachableCode: + Severity: error + +Layout/EndAlignment: + Severity: error + +Layout/IndentationWidth: + Severity: error + +Layout/HashAlignment: + SupportedLastArgumentHashStyles: always_ignore + +Layout/ParameterAlignment: + Enabled: + false # This is usually true, but we often want to roll back to + # the start of a line. + +Metrics/ModuleLength: + Max: 150 + Exclude: + - !ruby/regexp /spec\/*\/*/ + +Metrics/ClassLength: + Max: 110 + Exclude: + - !ruby/regexp /spec\/*\/*/ + +Metrics/LineLength: + Enabled: false + +Metrics/MethodLength: + CountComments: false # count full line comments? + Max: 25 + Severity: error + +Metrics/AbcSize: + Max: 25 + +Rails/UnknownEnv: + Environments: + - production + - development + - test + - staging + +AllCops: + TargetRubyVersion: 2.6 + # Include gemspec and Rakefile + Include: + - "**/*.rb" + - "**/*.gemspec" + - "**/*.rake" + - "**/Gemfile" + - "**/Rakefile" + - "**/Capfile" + - "**/Guardfile" + - "**/Podfile" + - "**/Vagrantfile" + Exclude: + - !ruby/regexp /(db|bin|script|config|vendor)\/.*/ + - 'spec/factories/**/*' + SuggestExtensions: false + # By default, the rails cops are not run. Override in project or home + # directory .rubocop.yml files, or by giving the -R/--rails option. +Rails: + Enabled: true diff --git a/Gemfile b/Gemfile index 5235267..d4993ef 100644 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,7 @@ gem "jbuilder" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] gem "bcrypt", "~> 3.1.7" -gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] +gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] gem "bootsnap", require: false # Use Sass to process CSS @@ -42,10 +42,10 @@ gem "rack-cors" gem "google-apis-drive_v3" gem "google-apis-sheets_v4" gem "google-apis-docs_v1" -gem 'google-apis-core' +gem "google-apis-core" # gem "googleauth" gem "activerecord-session_store" -gem 'redis-rails' +gem "redis-rails" # gem 'attr_encrypted' # gem "importmap-rails", "~> 1.1" @@ -57,13 +57,19 @@ gem "redis", "~> 4.8.1", "< 5" # rails active_storage:postgresql:install # rails db:migrate # see config in https://github.com/lsylvester/active_storage-postgresql -gem 'active_storage-postgresql' +gem "active_storage-postgresql" + +gem "rspec-rails", "~> 6.0" +gem "rubocop", require: false +gem "rubocop-rails", require: false +gem "standard", "~> 1.26" +gem "yard", "~> 0.9.34" ################################################################################ group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem "debug", platforms: %i[ mri mingw x64_mingw ] + gem "debug", platforms: %i[mri mingw x64_mingw] end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 493a4e4..b7c7c32 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,6 +77,7 @@ GEM tzinfo (~> 2.0) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) bcrypt (3.1.18) bindex (0.8.1) bootsnap (1.16.0) @@ -98,6 +99,7 @@ GEM irb (>= 1.5.0) reline (>= 0.3.1) declarative (0.0.20) + diff-lcs (1.5.0) erubi (1.12.0) faraday (2.7.4) faraday-net_http (>= 2.0, < 3.1) @@ -140,7 +142,9 @@ GEM jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) + json (2.6.3) jwt (2.7.0) + language_server-protocol (3.17.0.3) loofah (2.20.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -192,6 +196,9 @@ GEM oauth2 (>= 1.4, < 3) omniauth (~> 2.0) os (1.1.4) + parallel (1.22.1) + parser (3.2.2.0) + ast (~> 2.4.1) pg (1.4.6) public_suffix (5.0.1) puma (5.6.5) @@ -230,6 +237,7 @@ GEM rake (>= 12.2) thor (~> 1.0) zeitwerk (~> 2.5) + rainbow (3.1.1) rake (13.0.6) redis (4.8.1) redis-actionpack (5.3.0) @@ -257,6 +265,43 @@ GEM uber (< 0.2.0) retriable (3.1.2) rexml (3.2.5) + rspec-core (3.12.1) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-rails (6.0.1) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.11) + rspec-expectations (~> 3.11) + rspec-mocks (~> 3.11) + rspec-support (~> 3.11) + rspec-support (3.12.0) + rubocop (1.48.1) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.2.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.26.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.28.0) + parser (>= 3.2.1.0) + rubocop-performance (1.16.0) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.19.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) selenium-webdriver (4.8.6) @@ -278,6 +323,10 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) + standard (1.26.0) + language_server-protocol (~> 3.17.0.2) + rubocop (~> 1.48.1) + rubocop-performance (~> 1.16.0) stimulus-rails (1.2.1) railties (>= 6.0.0) tailwindcss-rails (2.0.27-arm64-darwin) @@ -294,6 +343,7 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) + unicode-display_width (2.4.2) version_gem (1.1.2) web-console (4.2.0) actionview (>= 6.0.0) @@ -311,6 +361,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) + yard (0.9.34) zeitwerk (2.6.7) PLATFORMS @@ -338,14 +389,19 @@ DEPENDENCIES rails (~> 7.0.4, >= 7.0.4.3) redis (~> 4.8.1, < 5) redis-rails + rspec-rails (~> 6.0) + rubocop + rubocop-rails selenium-webdriver sprockets-rails + standard (~> 1.26) stimulus-rails tailwindcss-rails turbo-rails tzinfo-data web-console webdrivers + yard (~> 0.9.34) RUBY VERSION ruby 3.2.1p31 diff --git a/bin/cibuild b/bin/cibuild new file mode 100755 index 0000000..033c369 --- /dev/null +++ b/bin/cibuild @@ -0,0 +1,20 @@ +#!/bin/sh + +set -x +if [ -d "./.git" ]; then + git log -n 1 HEAD | cat +fi +ruby -v +bundle -v +set +x +set -e + +export RACK_ENV=test +export RAILS_ENV=test + +bundle exec rails db:migrate +bundle exec rails assets:precompile >/dev/null 2>&1 +bundle exec rspec spec +bundle exec rubocop +bundle exec standardrb + From e853ef5b92e067a71a5ccf505f033e637dd1ed48 Mon Sep 17 00:00:00 2001 From: Taylor Meek Date: Wed, 19 Apr 2023 14:24:31 -0700 Subject: [PATCH 2/2] wip lint refactors --- .rubocop.yml | 155 ++++------------- Gemfile | 87 ++++------ Gemfile.lock | 6 + Rakefile | 3 + app/channels/application_cable/channel.rb | 3 + app/channels/application_cable/connection.rb | 3 + app/controllers/application_controller.rb | 12 +- app/controllers/forest_projects_controller.rb | 72 ++++---- app/controllers/home_controller.rb | 6 +- app/controllers/sessions_controller.rb | 7 +- app/helpers/application_helper.rb | 6 + app/jobs/application_job.rb | 3 + app/jobs/forest_project_job.rb | 121 +++++++------ app/mailers/application_mailer.rb | 6 + app/models/application_record.rb | 6 + app/models/forest_project.rb | 48 ++---- app/models/user.rb | 44 +++-- app/services/google_drive_client.rb | 162 +++++------------- app/views/forest_projects/new.html.erb | 6 +- app/views/forest_projects/show.html.erb | 1 - bin/bundle | 20 +-- bin/cibuild | 11 +- bin/rails | 7 + bin/rake | 6 + bin/setenv | 11 +- bin/setup | 18 +- config.ru | 3 + config/application.rb | 23 ++- config/boot.rb | 3 + config/environment.rb | 3 + config/environments/development.rb | 3 +- config/environments/production.rb | 11 +- config/environments/test.rb | 7 +- config/initializers/assets.rb | 3 + .../initializers/content_security_policy.rb | 3 + config/initializers/cors.rb | 9 +- config/initializers/debug.rb | 26 +-- .../initializers/filter_parameter_logging.rb | 7 +- config/initializers/inflections.rb | 3 + config/initializers/omniauth.rb | 9 +- config/initializers/permissions_policy.rb | 3 + config/initializers/redis.rb | 10 +- config/initializers/session_store.rb | 13 +- config/puma.rb | 11 +- config/routes.rb | 12 +- ...te_active_storage_tables.active_storage.rb | 41 ++--- db/seeds.rb | 7 +- lib/full_traceback_logger.rb | 15 +- test/application_system_test_case.rb | 4 + .../application_cable/connection_test.rb | 21 ++- test/test_helper.rb | 18 +- 51 files changed, 521 insertions(+), 577 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index aad59cc..c9381d3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,142 +1,47 @@ -# Ruby linting configuration. -# We only worry about two kinds of issues: 'error' and anything less than that. -# Error is not about severity, but about taste. Simple style choices that -# never have a great excuse to be broken, such as 1.9 JSON-like hash syntax, -# are errors. Choices that tend to have good exceptions in practice, such as -# line length, are warnings. +require: + - rubocop-capybara + - rubocop-rails + - rubocop-rspec -# If you'd like to make changes, a full list of available issues is at -# https://github.com/bbatsov/rubocop/blob/master/config/enabled.yml -# A list of configurable issues is at: -# https://github.com/bbatsov/rubocop/blob/master/config/default.yml -# -# If you disable a check, document why. - -require: rubocop-rails - -Metrics/BlockLength: - ExcludedMethods: ["describe", "context", "included", "class_methods", "it"] - Exclude: - - "**/rails_helper.rb" - -Metrics/ParameterLists: - CountKeywordArgs: false - -Style/StringLiterals: - EnforcedStyle: double_quotes - Severity: error - -Style/HashSyntax: - EnforcedStyle: ruby19 - UseHashRocketsWithSymbolValues: false - Severity: error - Exclude: - - !ruby/regexp /db\/schema.rb/ - -Style/Attr: - Enabled: - false # We have no styleguide guidance here, and it seems to be - # in frequent use. - -Style/ClassAndModuleChildren: - Enabled: false # module X<\n>module Y is just as good as module X::Y. - -Style/Documentation: +AllCops: + NewCops: enable Exclude: - - !ruby/regexp /spec\/*\/*/ - -Style/PercentLiteralDelimiters: - PreferredDelimiters: - "%w": "{}" - -Style/MultilineTernaryOperator: - Severity: error + - 'db/migrate/2023041*.rb' + - 'db/schema.rb' + - 'bin/bundle' -Style/AndOr: - Severity: error - -Style/FrozenStringLiteralComment: - Enabled: false # Adding comment to all files... - -Style/Alias: - Enabled: false # We have no guidance on alias vs alias_method - -Style/RedundantSelf: - Enabled: false # Sometimes a self.field is a bit more clear +Rails: + Enabled: true -Style/IfUnlessModifier: - Enabled: false +# Personal Customizations +Layout/LineLength: + Max: 150 -Style/SafeNavigation: +Layout/EmptyLineAfterGuardClause: Enabled: false -Lint/AmbiguousBlockAssociation: +Metrics/BlockLength: Exclude: - - !ruby/regexp /spec\/*\/*/ + - 'config/environments/*.rb' -Lint/UnreachableCode: - Severity: error - -Layout/EndAlignment: - Severity: error +# follow standardrb +Style/StringLiterals: + EnforcedStyle: double_quotes -Layout/IndentationWidth: - Severity: error +Layout/SpaceInsideHashLiteralBraces: + EnforcedStyle: no_space -Layout/HashAlignment: - SupportedLastArgumentHashStyles: always_ignore +Layout/ArgumentAlignment: + EnforcedStyle: with_fixed_indentation -Layout/ParameterAlignment: - Enabled: - false # This is usually true, but we often want to roll back to - # the start of a line. +Layout/ArrayAlignment: + EnforcedStyle: with_fixed_indentation -Metrics/ModuleLength: - Max: 150 - Exclude: - - !ruby/regexp /spec\/*\/*/ +Metrics/MethodLength: + Enabled: false Metrics/ClassLength: - Max: 110 - Exclude: - - !ruby/regexp /spec\/*\/*/ - -Metrics/LineLength: Enabled: false -Metrics/MethodLength: - CountComments: false # count full line comments? - Max: 25 - Severity: error - -Metrics/AbcSize: - Max: 25 - -Rails/UnknownEnv: - Environments: - - production - - development - - test - - staging - -AllCops: - TargetRubyVersion: 2.6 - # Include gemspec and Rakefile - Include: - - "**/*.rb" - - "**/*.gemspec" - - "**/*.rake" - - "**/Gemfile" - - "**/Rakefile" - - "**/Capfile" - - "**/Guardfile" - - "**/Podfile" - - "**/Vagrantfile" - Exclude: - - !ruby/regexp /(db|bin|script|config|vendor)\/.*/ - - 'spec/factories/**/*' - SuggestExtensions: false - # By default, the rails cops are not run. Override in project or home - # directory .rubocop.yml files, or by giving the -R/--rails option. -Rails: - Enabled: true +Rails/I18nLocaleTexts: + Enabled: false \ No newline at end of file diff --git a/Gemfile b/Gemfile index d4993ef..f8f8483 100644 --- a/Gemfile +++ b/Gemfile @@ -1,72 +1,49 @@ +# frozen_string_literal: true + source "https://rubygems.org" + git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "3.2.1" -gem "rails", "~> 7.0.4", ">= 7.0.4.3" - -gem "sprockets-rails" -gem "pg", "~> 1.1" -gem "puma", "~> 5.0" - -# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] -gem "importmap-rails" -gem "turbo-rails" -gem "stimulus-rails" -gem "tailwindcss-rails" -gem "jbuilder" - -# Use Redis adapter to run Action Cable in production -# gem "redis", "~> 4.0" - -# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] -# gem "kredis" -# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -gem "bcrypt", "~> 3.1.7" - -gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] +# gem 'googleauth' +# gem 'image_processing', '~> 1.2' # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem 'importmap-rails', '~> 1.1' +# gem 'kredis' # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] +# gem 'omniauth-rails_csrf_protection' +# gem 'redis', '~> 4.0' # Use Redis adapter to run Action Cable in production +# gem 'sassc-rails' # Use Sass to process CSS +# gem 'attr_encrypted' +gem "activerecord-session_store" +gem "active_storage-postgresql" # https://github.com/lsylvester/active_storage-postgresql +gem "bcrypt", "~> 3.1.7" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] gem "bootsnap", require: false - -# Use Sass to process CSS -# gem "sassc-rails" - -# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] -# gem "image_processing", "~> 1.2" - -################################################################################ -# forestbot +gem "google-apis-core" +gem "google-apis-docs_v1" +gem "google-apis-drive_v3" +gem "google-apis-sheets_v4" +gem "importmap-rails" # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "jbuilder" gem "omniauth" gem "omniauth-google-oauth2" -# gem "omniauth-rails_csrf_protection" +gem "pg", "~> 1.1" +gem "puma", "~> 5.0" gem "rack-cors" -gem "google-apis-drive_v3" -gem "google-apis-sheets_v4" -gem "google-apis-docs_v1" -gem "google-apis-core" -# gem "googleauth" -gem "activerecord-session_store" +gem "rails", "~> 7.0.4", ">= 7.0.4.3" +gem "redis", "~> 4.8.1", "< 5" # 5.0 doesn't work with session state storage crap gem "redis-rails" -# gem 'attr_encrypted' -# gem "importmap-rails", "~> 1.1" - -# 5.0 doesn't work with session state storage crap -gem "redis", "~> 4.8.1", "< 5" - -# Store activestorage files in postgres: -# rails active_storage:install -# rails active_storage:postgresql:install -# rails db:migrate -# see config in https://github.com/lsylvester/active_storage-postgresql -gem "active_storage-postgresql" - gem "rspec-rails", "~> 6.0" gem "rubocop", require: false gem "rubocop-rails", require: false +gem "rubocop-rspec", require: false +gem "sprockets-rails" gem "standard", "~> 1.26" +gem "stimulus-rails" +gem "tailwindcss-rails" +gem "turbo-rails" +gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] gem "yard", "~> 0.9.34" -################################################################################ - group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[mri mingw x64_mingw] @@ -77,10 +54,10 @@ group :development do gem "web-console" # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] - # gem "rack-mini-profiler" + # gem 'rack-mini-profiler' # Speed up commands on slow machines / big apps [https://github.com/rails/spring] - # gem "spring" + # gem 'spring' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index b7c7c32..ba97769 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -294,6 +294,8 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.28.0) parser (>= 3.2.1.0) + rubocop-capybara (2.17.1) + rubocop (~> 1.41) rubocop-performance (1.16.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) @@ -301,6 +303,9 @@ GEM activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) + rubocop-rspec (2.19.0) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -392,6 +397,7 @@ DEPENDENCIES rspec-rails (~> 6.0) rubocop rubocop-rails + rubocop-rspec selenium-webdriver sprockets-rails standard (~> 1.26) diff --git a/Rakefile b/Rakefile index 9a5ea73..22e3fdf 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,6 @@ +# Rakefile +# frozen_string_literal: true + # 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. diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb index d672697..5a4c720 100644 --- a/app/channels/application_cable/channel.rb +++ b/app/channels/application_cable/channel.rb @@ -1,3 +1,6 @@ +# app/channels/application_cable/channel.rb +# frozen_string_literal: true + module ApplicationCable class Channel < ActionCable::Channel::Base end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442..c95613a 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,3 +1,6 @@ +# app/channels/application_cable/connection.rb +# frozen_string_literal: true + module ApplicationCable class Connection < ActionCable::Connection::Base end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 119ca68..20ebe96 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,11 +1,15 @@ # app/controllers/application_controller.rb +# frozen_string_literal: true +# ApplicationController is the base class for all controllers in this application. +# It provides methods that are available to all controllers. +# We are using it to globally require authentication for all controllers. class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :current_user, :logged_in? before_action :authenticate_user! - skip_before_action :authenticate_user!, only: [:omniauth_callback, :index, :login] + skip_before_action :authenticate_user!, only: %i[omniauth_callback index destroy] # rubocop:disable Rails/LexicallyScopedActionFilter private @@ -18,14 +22,12 @@ def logged_in? # Current oauth user def current_user entry_dbg - @current_user ||= User.find_by_id(session[:user_id]) + @current_user ||= User.find_by(id: session[:user_id]) end # Ensure user is logged in before allowing them to access a page def authenticate_user! entry_dbg - unless logged_in? - redirect_to root_path, alert: 'You need to sign in first!' - end + logged_in? ? true : redirect_to(root_path, alert: "You need to sign in first!") end end diff --git a/app/controllers/forest_projects_controller.rb b/app/controllers/forest_projects_controller.rb index 64c5cea..0b44ae9 100644 --- a/app/controllers/forest_projects_controller.rb +++ b/app/controllers/forest_projects_controller.rb @@ -1,5 +1,10 @@ -require 'csv' +# app/controllers/forest_projects_controller.rb +# frozen_string_literal: true +require "csv" + +# ForestProjectsController is responsible for handling the CRUD actions for +# ForestProject records. class ForestProjectsController < ApplicationController before_action :authenticate_user! @@ -9,6 +14,32 @@ def index current_user.refresh_tokens! end + def show + entry_dbg + @forest_project = current_user.forest_projects.find(params[:id]) + + @species_summary = @forest_project.species_summary || nil + @condition_summary = @forest_project.condition_summary || nil + @dbh_summary = @forest_project.dbh_summary || nil + @total_sites = @forest_project.total_sites || nil + + @fraxinus_species_summary = @forest_project.fraxinus_species_summary || nil + @fraxinus_condition_summary = @forest_project.fraxinus_condition_summary || nil + @fraxinus_dbh_summary = @forest_project.fraxinus_dbh_summary || nil + @fraxinus_total_sites = @forest_project.fraxinus_total_sites || nil + + if @forest_project.csv_url + csv_contents = @forest_project.csv_url.download + @csv_data = CSV.parse(csv_contents, headers: true, header_converters: :symbol)&.map(&:to_h) + @csv_filename = @forest_project.csv_url.filename.to_s + @csv_url = url_for(@forest_project.csv_url) if @forest_project.csv_url.attached? + end + + @google_folder = @forest_project.google_folder || nil + @google_sheet = @forest_project.google_sheet || nil + @google_doc = @forest_project.google_doc || nil + end + def new entry_dbg @forest_project = ForestProject.new @@ -16,7 +47,7 @@ def new def status @forest_project = current_user.forest_projects.find(params[:id]) - render json: { status: @forest_project.status } + render json: {status: @forest_project.status} end def create @@ -25,9 +56,9 @@ def create if @forest_project.save ForestProjectJob.perform_later(@forest_project.id, session[:user_id]) - redirect_to forest_project_path(@forest_project), notice: 'Project created successfully.' + redirect_to forest_project_path(@forest_project), notice: "Project created successfully." else - render :new + redirect_to new_forest_project_path, alert: "Project creation failed." end end @@ -37,39 +68,12 @@ def reprocess if @forest_project.present? ForestProjectJob.perform_later(@forest_project.id, session[:user_id]) - redirect_to forest_project_path(@forest_project), notice: 'File reprocessing started.' + redirect_to forest_project_path(@forest_project), notice: "File reprocessing started." else - redirect_to forest_projects_path, alert: 'Project not found.' + redirect_to forest_projects_path, alert: "Project not found." end end - def show - entry_dbg - user_id = session[:user_id] - @forest_project = current_user.forest_projects.find(params[:id]) - - @species_summary = @forest_project.species_summary || nil - @condition_summary = @forest_project.condition_summary || nil - @dbh_summary = @forest_project.dbh_summary || nil - @total_sites = @forest_project.total_sites || nil - - @fraxinus_species_summary = @forest_project.fraxinus_species_summary || nil - @fraxinus_condition_summary = @forest_project.fraxinus_condition_summary || nil - @fraxinus_dbh_summary = @forest_project.fraxinus_dbh_summary || nil - @fraxinus_total_sites = @forest_project.fraxinus_total_sites || nil - - if @forest_project.csv - csv_contents = @forest_project.csv.download - @csv_data = CSV.parse(csv_contents, headers: true, header_converters: :symbol).map(&:to_h) - @csv_filename = @forest_project.csv.filename.to_s - @csv_url = url_for(@forest_project.csv) if @forest_project.csv.attached? - end - - @google_folder = @forest_project.google_folder || nil - @google_sheet = @forest_project.google_sheet || nil - @google_doc = @forest_project.google_doc || nil - end - def download_csv entry_dbg @forest_project = current_user.forest_projects.find(params[:id]) @@ -80,6 +84,6 @@ def download_csv def project_params entry_dbg - params.require(:forest_project).permit(:client_name, :project_name, :csv, :project_date) + params.require(:forest_project).permit(:client_name, :project_name, :csv_url, :project_date) end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 4807e25..6a7d18e 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,9 +1,9 @@ +# app/controllers/home_controller.rb # frozen_string_literal: true +# HomeController is responsible for handling the root path. class HomeController < ApplicationController def index - end - - def login + # noop end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index cc36b1a..18e78d5 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,7 +1,8 @@ # app/controllers/sessions_controller.rb - # frozen_string_literal: true +# SessionsController is responsible for handling the OAuth callback from +# Google and for handling user logout. class SessionsController < ApplicationController skip_before_action :verify_authenticity_token, only: :omniauth_callback @@ -28,8 +29,6 @@ def destroy def auth entry_dbg - request.env['omniauth.auth'] + request.env["omniauth.auth"] end - - end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be79..3e24f8d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,8 @@ +# app/helpers/application_helper.rb +# frozen_string_literal: true + +# ApplicationHelper is a module that is included in all controllers and views. +# It is a good place to put methods that are used in multiple places in your +# application. module ApplicationHelper end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index d394c3d..9cb89b0 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,3 +1,6 @@ +# app/jobs/application_job.rb +# frozen_string_literal: true + class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked diff --git a/app/jobs/forest_project_job.rb b/app/jobs/forest_project_job.rb index bb65ac0..fa0a098 100644 --- a/app/jobs/forest_project_job.rb +++ b/app/jobs/forest_project_job.rb @@ -1,9 +1,20 @@ # app/jobs/forest_project_job.rb # fka app/jobs/process_forest_project.rb - -require 'csv' -require 'google_drive_client' - +# frozen_string_literal: true + +require "csv" +require "google_drive_client" + +# This job is responsible for processing a ForestProject +# It will: +# - Read the CSV file +# - Populate the ForestProject with statistics from the CSV file +# - Create a Google Drive folder +# - Create a Google Sheet containing the CSV data +# - Create a Google Doc, a report based on the statistics +# - Populate the Google Doc with data from the CSV file +# - Populate the Google Sheet with data from the CSV file +# - Create a Google Table in the Google Doc with data from the CSV file class ForestProjectJob < ApplicationJob queue_as :default @@ -15,25 +26,25 @@ def perform(project_id, user_id) project.update(error_message: nil) if project.error_message user = User.find(user_id) - project.update(status: '👩🏻‍🔬Calculating') + project.update(status: "👩🏻‍🔬Calculating") - csv_data = read_and_validate_csv(project.csv) + csv_data = read_and_validate_csv(project.csv_url) tree_counts = count_trees(csv_data) tree_counts.merge!(count_trees(csv_data, only_fraxinus: true)) total_ash_trees = tree_counts[:fraxinus_total_sites] - project.update(status: '👩🏻‍💻Creating Folder', tree_counts: tree_counts) + project.update(status: "👩🏻‍💻Creating Folder", tree_counts:) google = GoogleDriveClient.new(user) folder = google.create_folder( "#{project.client_name} - #{project.project_name} - #{project.project_date}", - ENV['GOOGLE_DRIVE_FOLDER_ID'] + ENV.fetch("GOOGLE_DRIVE_FOLDER_ID") ) _ = project.update!(status: "👩🏻‍💻Creating Sheet", google_drive_folder_id: folder.id) sheet = google.copy_file( - ENV['GOOGLE_SHEETS_TEMPLATE_ID'], + ENV.fetch("GOOGLE_SHEETS_TEMPLATE_ID"), "Data- #{project.client_name} - #{project.project_name} - #{project.project_date}", folder.id ) @@ -43,32 +54,31 @@ def perform(project_id, user_id) _ = project.update!(status: "👩🏻‍💻Creating Doc", google_spreadsheet_id: sheet.id) doc = google.copy_file( - ENV['GOOGLE_DOCS_TEMPLATE_ID'], + ENV.fetch("GOOGLE_DOCS_TEMPLATE_ID"), "Report- #{project.client_name} - #{project.project_name} - #{project.project_date}", folder.id ) _ = project.update!(status: "👩🏻‍💻Populating Doc", google_doc_id: doc.id) - google.replace_placeholders_in_doc_with_values( + google.replace_doc_placeholders( doc.id, [ - { '{{client_name}}': project.client_name.to_s }, - { '{{project_name}}': project.project_name.to_s }, - { '{{project_date}}': project.project_date.to_s }, - { '{{total_sites}}': project.total_sites.to_s }, - { '{{total_ash_trees}}': total_ash_trees.to_s }, - { '{{data_table}}': "=IMPORTDATA(\"https://docs.google.com/spreadsheets/d/#{sheet.id}/gviz/tq?tqx=out:csv&sheet=Data\")" } + {"{{client_name}}": project.client_name.to_s}, + {"{{project_name}}": project.project_name.to_s}, + {"{{project_date}}": project.project_date.to_s}, + {"{{total_sites}}": project.total_sites.to_s}, + {"{{total_ash_trees}}": total_ash_trees.to_s} ] ) _ = project.update!(status: "👩🏻‍💻Inserting Table") google.insert_table_into_doc(doc.id, csv_data) - project.update(status: '✅ Done') - rescue StandardError => e - # Update project with the error message and set status to 'Error' + project.update(status: "✅ Done") + rescue => e # rubocop:disable Style/RescueStandardError + # Update project with the error message and set status to "Error" now = Time.current - project.update(status: '🛑 Processing Error', error_message: "#{now}\n\n#{e.message}\n\n#{e.backtrace.join("\n")}") + project.update(status: "🛑 Processing Error", error_message: "#{now}\n\n#{e.message}\n\n#{e.backtrace.join("\n")}") end end @@ -78,7 +88,7 @@ def read_and_validate_csv(csv) entry_dbg begin # Define the expected header row - mandatory_headers = %w[pid common_name scientific_name condition dbh] + mandatory_headers = %w[{pid common_name scientific_name condition dbh] csv_contents = csv.download csv_data = CSV.parse(csv_contents, headers: true, header_converters: :symbol).map(&:to_h) @@ -87,9 +97,8 @@ def read_and_validate_csv(csv) actual_headers = csv_data.first.keys actual_headers_s = actual_headers.map(&:to_s) missing_headers = mandatory_headers - actual_headers_s - if missing_headers.any? - raise "Missing headers: #{missing_headers}. Expected: #{mandatory_headers}, Actual: #{actual_headers_s}" - end + + raise "Missing headers: #{missing_headers}. Expected: #{mandatory_headers}, Actual: #{actual_headers_s}" if missing_headers.any? # Create a mapping of header names and their indices header_mapping = {} @@ -101,8 +110,8 @@ def read_and_validate_csv(csv) csv_data.map do |row| row.transform_keys! { |key| key.downcase.to_sym } end - rescue => e - @project.update(status: '🛑 Error Parsing CSV', error_message: e.message) + rescue => e # rubocop:disable Style/RescueStandardError + @project.update(status: "🛑 Error Parsing CSV", error_message: e.message) raise e end end @@ -111,39 +120,39 @@ def read_and_validate_csv_old(csv) entry_dbg begin # Define the expected header row - expected_headers = %w[pid common_name scientific_name condition tree_workpruning tree_workother tree_tag dbh within_years cycle tree_height_estimated crown_spread notesmgmt observation_comments observationscharacteristics special_equipment] + expected_headers = %w[pid common_name scientific_name condition tree_workpruning tree_workother tree_tag dbh within_years cycle + tree_height_estimated crown_spread notesmgmt observation_comments observationscharacteristics special_equipment] csv_contents = csv.download csv_data = CSV.parse(csv_contents, headers: true, header_converters: :symbol).map(&:to_h) # Validate the header row actual_headers = csv_data.first.keys.map(&:to_s) - unless actual_headers == expected_headers - raise "Invalid header row. Expected: #{expected_headers}, Actual: #{actual_headers}" - end + + raise "Invalid header row. Expected: #{expected_headers}, Actual: #{actual_headers}" unless actual_headers == expected_headers csv_data - rescue => e - @project.update(status: '🛑 Error Parsing CSV', error_message: e.message) + rescue => e # rubocop:disable Style/RescueStandardError + @project.update(status: "🛑 Error Parsing CSV", error_message: e.message) raise e end end def summarize_species(csv_data, only_fraxinus: false) entry_dbg - species_counts = Hash.new + species_counts = {} dbg "CSV data: #{csv_data}" csv_data.each do |row| dbg "Row: #{row}" - # If the scientific_name is nil, include it in the 'Unspecified' category - sci_name = row[:scientific_name] || 'Unspecified' + # If the scientific_name is nil, include it in the "Unspecified" category + sci_name = row[:scientific_name] || "Unspecified" key = sci_name.downcase.strip - # If the common_name is nil, include it in the 'Unspecified' category - common_name = row[:common_name] || 'Unspecified' + # If the common_name is nil, include it in the "Unspecified" category + common_name = row[:common_name] || "Unspecified" # If only_fraxinus is true, skip the row unless the species starts with either Fraxinus or Chionanthus - next if only_fraxinus && !row[:scientific_name].to_s.start_with?('Fraxinus', 'Chionanthus') + next if only_fraxinus && !row[:scientific_name].to_s.start_with?("Fraxinus", "Chionanthus") unless species_counts[key] species_counts[key] = { @@ -163,7 +172,7 @@ def summarize_dbh(csv_data, only_fraxinus: false) max_dbh = -Float::INFINITY csv_data.each do |row| # If only_fraxinus is true, skip the row unless the species starts with either Fraxinus or Chionanthus - next if only_fraxinus && !row[:scientific_name].to_s.start_with?('Fraxinus', 'Chionanthus') + next if only_fraxinus && !row[:scientific_name].to_s.start_with?("Fraxinus", "Chionanthus") # If row[:dbh] is nil, skip the row next if row[:dbh].nil? @@ -173,7 +182,7 @@ def summarize_dbh(csv_data, only_fraxinus: false) max_dbh = [max_dbh, dbh].max end - { min_dbh: min_dbh, max_dbh: max_dbh } + {min_dbh:, max_dbh:} end def summarize_conditions(csv_data, only_fraxinus: false) @@ -185,13 +194,13 @@ def summarize_conditions(csv_data, only_fraxinus: false) # Iterate through the CSV data csv_data.each do |row| # If the species is "Vacant", skip the row - next if row[:scientific_name] == 'Vacant' + next if row[:scientific_name] == "Vacant" - # If the condition is nil, include it in the 'Unspecified' category - condition_name = row[:condition].nil? ? 'Unspecified' : row[:condition] + # If the condition is nil, include it in the "Unspecified" category + condition_name = row[:condition].nil? ? "Unspecified" : row[:condition] # If only_fraxinus is true, skip the row unless the species starts with either Fraxinus or Chionanthus - next if only_fraxinus && !row[:scientific_name].to_s.start_with?('Fraxinus', 'Chionanthus') + next if only_fraxinus && !row[:scientific_name].to_s.start_with?("Fraxinus", "Chionanthus") condition_name = condition_name.downcase.strip condition_counts[condition_name] += 1 @@ -203,21 +212,19 @@ def summarize_conditions(csv_data, only_fraxinus: false) def count_trees(csv_data, only_fraxinus: false) entry_dbg begin - condition_summary = summarize_conditions(csv_data, only_fraxinus: only_fraxinus) - species_summary = summarize_species(csv_data, only_fraxinus: only_fraxinus) - dbh_summary = summarize_dbh(csv_data, only_fraxinus: only_fraxinus) - total_sites =species_summary.values.map { |v| v[:count] }.sum + condition_summary = summarize_conditions(csv_data, only_fraxinus:) + species_summary = summarize_species(csv_data, only_fraxinus:) + dbh_summary = summarize_dbh(csv_data, only_fraxinus:) + total_sites = species_summary.values.pluck(:count).sum { - "#{only_fraxinus ? 'fraxinus_' : ''}condition_summary": condition_summary, - "#{only_fraxinus ? 'fraxinus_' : ''}species_summary": species_summary, - "#{only_fraxinus ? 'fraxinus_' : ''}dbh_summary": dbh_summary, - "#{only_fraxinus ? 'fraxinus_' : ''}total_sites": total_sites + "#{only_fraxinus ? "fraxinus_" : ""}condition_summary": condition_summary, + "#{only_fraxinus ? "fraxinus_" : ""}species_summary": species_summary, + "#{only_fraxinus ? "fraxinus_" : ""}dbh_summary": dbh_summary, + "#{only_fraxinus ? "fraxinus_" : ""}total_sites": total_sites } - rescue => e - @project.update(status: '🛑 Error Counting Trees', error_message: e.message) + rescue => e # rubocop:disable Style/RescueStandardError + @project.update(status: "🛑 Error Counting Trees", error_message: e.message) raise e end end - end - diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c34c81..26400b5 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,3 +1,9 @@ +# app/mailers/application_mailer.rb +# frozen_string_literal: true + +# ApplicationMailer is the base class for all mailers in your application. +# It sets some default values for the mailer, and allows you to define +# helper methods that can be used in all mailers. class ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout "mailer" diff --git a/app/models/application_record.rb b/app/models/application_record.rb index b63caeb..18e161f 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,9 @@ +# app/models/application_record.rb +# frozen_string_literal: true + +# ApplicationRecord is the base class for all models in the application. +# It is an abstract class, meaning that it cannot be instantiated. +# It is used to define methods that are common to all models. class ApplicationRecord < ActiveRecord::Base primary_abstract_class end diff --git a/app/models/forest_project.rb b/app/models/forest_project.rb index fd2cc01..6f7ea2a 100644 --- a/app/models/forest_project.rb +++ b/app/models/forest_project.rb @@ -1,10 +1,14 @@ # app/models/forest_project.rb +# frozen_string_literal: true -require 'google_drive_client' +require "google_drive_client" +# ForestProject is a model for storing information about an urban forest +# collection project. Each is associated with a user, and has a CSV file +# attached. The CSV file is parsed and the tree counts are stored in the +# tree_counts field. class ForestProject < ApplicationRecord belongs_to :user - has_one :forest_job has_one_attached :csv @@ -20,70 +24,52 @@ def tree_counts end def species_summary - return {} unless tree_counts - tree_counts['species_summary'] || {} + tree_counts ? tree_counts["species_summary"] || {} : {} end def condition_summary - return {} unless tree_counts - tree_counts['condition_summary'] || {} + tree_counts ? tree_counts["condition_summary"] || {} : {} end def dbh_summary - return {} unless tree_counts - tree_counts['dbh_summary'] || {} + tree_counts ? tree_counts["dbh_summary"] || {} : {} end def total_sites - return {} unless tree_counts - tree_counts['total_sites'] || {} + tree_counts ? tree_counts["total_sites"] || {} : {} end def fraxinus_species_summary - return {} unless tree_counts - tree_counts['fraxinus_species_summary'] || {} + tree_counts ? tree_counts["fraxinus_species_summary"] || {} : {} end def fraxinus_condition_summary - return {} unless tree_counts - tree_counts['fraxinus_condition_summary'] || {} + tree_counts ? tree_counts["fraxinus_condition_summary"] || {} : {} end def fraxinus_dbh_summary - return {} unless tree_counts - tree_counts['fraxinus_dbh_summary'] || {} + tree_counts ? tree_counts["fraxinus_dbh_summary"] || {} : {} end def fraxinus_total_sites - return {} unless tree_counts - tree_counts['fraxinus_total_sites'] || {} + tree_counts ? tree_counts["fraxinus_total_sites"] || {} : {} end def google_folder - entry_dbg - id = self.google_drive_folder_id - return nil unless id - "https://drive.google.com/drive/u/2/folders/#{id}" + google_drive_folder_id ? "https://drive.google.com/drive/u/2/folders/#{google_drive_folder_id}" : nil end def google_sheet - entry_dbg - id = self.google_spreadsheet_id - return nil unless id - "https://docs.google.com/spreadsheets/d/#{id}/edit#gid=0" + google_spreadsheet_id ? "https://docs.google.com/spreadsheets/d/#{google_spreadsheet_id}/edit#gid=0" : nil end def google_doc - entry_dbg - id = self.google_doc_id - return nil unless id - "https://docs.google.com/document/d/#{id}/edit" + google_doc_id ? "https://docs.google.com/document/d/#{google_doc_id}/edit" : nil end private def google_client(user) - entry_dbg @google_client ||= GoogleDriveClient.new(user) end end diff --git a/app/models/user.rb b/app/models/user.rb index 48611d2..104690c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,14 +1,21 @@ # app/models/user.rb +# frozen_string_literal: true +# User is the model for a user of the application. It is used to +# authenticate users and to store their Google OAuth tokens, which +# are used to log into the app, and to access Google Drive. class User < ApplicationRecord has_secure_password - #attr_encrypted :google_token, key: ENV['ATTR_ENCRYPTED_KEY'] - #attr_encrypted :google_refresh_token, key: ENV['ATTR_ENCRYPTED_KEY'] - #attr_encrypted :google_token_expires_at, key: ENV['ATTR_ENCRYPTED_KEY'] + # attr_encrypted :google_token, key: ENV['ATTR_ENCRYPTED_KEY'] + # attr_encrypted :google_refresh_token, key: ENV['ATTR_ENCRYPTED_KEY'] + # attr_encrypted :google_token_expires_at, key: ENV['ATTR_ENCRYPTED_KEY'] validates :uid, presence: true validates :name, presence: true - validates :email, presence: true, format: { with: /\A[\w+\-.]+@cfs\.eco\z/i, message: "must be from the cfs.eco domain" } + validates :email, presence: true, format: { + with: /\A[\w+\-.]+@cfs\.eco\z/i, + message: "must be from the cfs.eco domain" + } validates :password_digest, presence: true validates :provider, presence: true @@ -16,39 +23,44 @@ class User < ApplicationRecord def update_tokens(auth) entry_dbg + dbg "google_token: #{auth.credentials.token}" + dbg "google_token_expires_at: #{Time.at(auth.credentials.expires_at).utc}" + dbg "google_refresh_token: #{auth.credentials.refresh_token}" self.google_token = auth.credentials.token - self.google_token_expires_at = Time.at(auth.credentials.expires_at) - self.google_refresh_token = auth.credentials.refresh_token unless self.google_refresh_token.present? + self.google_token_expires_at = Time.at(auth.credentials.expires_at).utc + if google_refresh_token.blank? + self.google_refresh_token = auth.credentials.refresh_token + end save end def refresh_tokens! entry_dbg # Is the users token expired? - if self.google_token_expires_at.to_datetime.past? + if google_token_expires_at.to_datetime.past? oauth = OmniAuth::Strategies::GoogleOauth2.new( nil, # App - nil seems to be ok?! - ENV['GOOGLE_CLIENT_ID'], - ENV['GOOGLE_CLIENT_SECRET'] + ENV.fetch("GOOGLE_CLIENT_ID"), + ENV.fetch("GOOGLE_CLIENT_SECRET") ) token = OAuth2::AccessToken.new( oauth.client, - Time.at(self.google_token_expires_at), - { refresh_token: self.google_refresh_token } + Time.at(google_token_expires_at).utc.to_s, + {refresh_token: google_refresh_token} ) new_token = token.refresh! if new_token.present? - self.update( + update( google_token: new_token.token, - google_token_expires_at: Time.at(new_token.expires_at), - google_refresh_token: self.google_refresh_token || new_token.refresh_token + google_token_expires_at: Time.at(new_token.expires_at).utc, + google_refresh_token: google_refresh_token || new_token.refresh_token ) dbg "Refreshed expired user token" else warn "User refresh did not work, time to clear the session and force a re-auth." - #destroy + # destroy end end - self.google_token + google_token end end diff --git a/app/services/google_drive_client.rb b/app/services/google_drive_client.rb index 78b4dbf..36f9c3f 100644 --- a/app/services/google_drive_client.rb +++ b/app/services/google_drive_client.rb @@ -1,14 +1,15 @@ # app/services/google_drive_client.rb - # frozen_string_literal: true -require 'google/apis/drive_v3' -require 'google/apis/sheets_v4' -require 'google/apis/docs_v1' -require 'google/api_client/client_secrets' +require "google/apis/drive_v3" +require "google/apis/sheets_v4" +require "google/apis/docs_v1" +require "google/api_client/client_secrets" +# GoogleDriveClient is a wrapper around the Google Drive API, +# providing methods for interacting with the API, and handling +# secrets and requesting token refreshes. class GoogleDriveClient - def initialize(user) entry_dbg @user = user @@ -16,16 +17,15 @@ def initialize(user) @sheets_service = Google::Apis::SheetsV4::SheetsService.new @sheets_service.authorization = google_secret.to_authorization - #@sheets_service.authorization.refresh! + # @sheets_service.authorization.refresh! @drive_service = Google::Apis::DriveV3::DriveService.new @drive_service.authorization = google_secret.to_authorization - #@drive_service.authorization.refresh! + # @drive_service.authorization.refresh! @docs_service = Google::Apis::DocsV1::DocsService.new @docs_service.authorization = google_secret.to_authorization - #@docs_service.authorization.refresh! - + # @docs_service.authorization.refresh! end # 0ABpZS99k3z4gUk9PVA @@ -35,7 +35,7 @@ def url_for_file(file_id) @drive_service.get_file( file_id, supports_all_drives: true, - fields: 'webViewLink' + fields: "webViewLink" ).web_view_link end @@ -43,15 +43,15 @@ def get_file(file_id) entry_dbg @drive_service.get_file( file_id, - supports_all_drives: true, + supports_all_drives: true ) end def create_folder(name, parent_id) entry_dbg folder = Google::Apis::DriveV3::File.new( - name: name, - mime_type: 'application/vnd.google-apps.folder', + name:, + mime_type: "application/vnd.google-apps.folder", parents: [parent_id], supports_all_drives: true, include_items_from_all_drives: true, @@ -63,8 +63,8 @@ def create_folder(name, parent_id) def copy_file(file_id, name, parent_id) entry_dbg file = Google::Apis::DriveV3::File.new( - name: name, - parents: [parent_id], + name:, + parents: [parent_id] ) new_file = @drive_service.copy_file( file_id, @@ -77,11 +77,11 @@ def copy_file(file_id, name, parent_id) def move_file(file_id, parent_id) entry_dbg old_file = get_file(file_id) - old_parents = old_file&.parents&.join(',') || '' + old_parents = old_file&.parents&.join(",") || "" @drive_service.update_file( file_id, remove_parents: old_parents, - add_parents: "#{parent_id}", + add_parents: parent_id.to_s, supports_all_drives: true ) end @@ -92,14 +92,14 @@ def append_csv_to_spreadsheet(spreadsheet_id, csv_data) data = [ { - range: 'A1', + range: "A1", values: data_array } ] batch_update_values = Google::Apis::SheetsV4::BatchUpdateValuesRequest.new( - data: data, - value_input_option: 'USER_ENTERED' + data:, + value_input_option: "USER_ENTERED" ) @sheets_service.batch_update_values( spreadsheet_id, @@ -110,7 +110,7 @@ def append_csv_to_spreadsheet(spreadsheet_id, csv_data) def convert_csv_data_to_2d_array(csv_data) # Convert the array of hashes into a 2D array headers = csv_data.first.keys - data_array = csv_data.map { |row| row.values } + data_array = csv_data.map { |row| row.values } # rubocop:disable Style/SymbolProc # Add headers as the first row in the 2D array data_array.unshift(headers) @@ -118,11 +118,11 @@ def convert_csv_data_to_2d_array(csv_data) data_array end - def replace_placeholders_in_doc_with_values(document_id, replacements) + def replace_doc_placeholders(document_id, replacements) entry_dbg # replacements looks like: # [ - # { "{{first_name}}": "John" }, + # { '{{first_name}}': 'John' }, # ] requests = [] replacements.each do |replacement| @@ -138,7 +138,7 @@ def replace_placeholders_in_doc_with_values(document_id, replacements) } end end - batch_update = Google::Apis::DocsV1::BatchUpdateDocumentRequest.new(requests: requests) + batch_update = Google::Apis::DocsV1::BatchUpdateDocumentRequest.new(requests:) @docs_service.batch_update_document(document_id, batch_update) end @@ -154,41 +154,40 @@ def insert_table_into_doc(document_id, csv_data) request = Google::Apis::DocsV1::Request.new request.insert_table = table_request batch_update = Google::Apis::DocsV1::BatchUpdateDocumentRequest.new(requests: [request]) - result = @docs_service.batch_update_document(document_id, batch_update) + @docs_service.batch_update_document(document_id, batch_update) # Get the last element of the document, which should be the table document = @docs_service.get_document(document_id) body = document.body # iterate from the end of document.body.content until we find a StructuralElement with a table attribute - table_element = body.content.reverse.find { |element| element.table } - table_start_index = table_element.start_index - table_end_index = table_element.end_index + table_element = body.content.reverse.find { |element| element.table } # rubocop:disable Style/SymbolProc table = table_element.table # Insert the data into the table, working from the bottom right cell in reverse-order # so that we don't have to worry about the indexes changing as we insert text - requests = [] + batch = [] row_idx = data_array.length - 1 table.table_rows.reverse_each do |row| cell_idx = data_array[row_idx].length - 1 row.table_cells.reverse_each do |cell| - cell_start_index = cell.content[0].paragraph.elements[0].start_index + # cell_start_index = cell.content[0].paragraph.elements[0].start_index cell_end_index = cell.content[0].paragraph.elements[0].end_index cell_request = { insert_text: { - text: "#{data_array[row_idx][cell_idx] || '-'}", + text: data_array[row_idx][cell_idx].to_s || "-", location: { index: cell_end_index - 1 } } } - requests << cell_request + batch << cell_request cell_idx -= 1 end + # noinspection RubyUnusedLocalVariable row_idx -= 1 end - batch_update = Google::Apis::DocsV1::BatchUpdateDocumentRequest.new(requests: requests) + batch_update = Google::Apis::DocsV1::BatchUpdateDocumentRequest.new(requests: batch) @docs_service.batch_update_document(document_id, batch_update) end @@ -203,93 +202,14 @@ def google_secret def secret_hash entry_dbg - { "web" => - { "access_token" => @user.google_token, - "refresh_token" => @user.google_refresh_token, - "client_id" => ENV['GOOGLE_CLIENT_ID'], - "client_secret" => ENV['GOOGLE_CLIENT_SECRET'], - "grant_type" => "refresh_token" - } + { + "web" => { + "access_token" => @user.google_token, + "refresh_token" => @user.google_refresh_token, + "client_id" => ENV.fetch("GOOGLE_CLIENT_ID"), + "client_secret" => ENV.fetch("GOOGLE_CLIENT_SECRET"), + "grant_type" => "refresh_token" + } } end - - def table_for_data(csv_data) - rows = [] - # column_properties = [] - header_array = nil - csv_data.each_with_index do |row, idx_row| - new_row = row_for_table(row, idx_row, header_row: header_array) - if idx_row == 0 - header_array = row - end - rows << new_row - end - table = Google::Apis::DocsV1::Table.new - table.rows = csv_data.length - table.columns = csv_data.first.length - table.table_rows = rows - - table_style = Google::Apis::DocsV1::TableStyle.new - # table_style.table_column_properties = column_properties - table - end - - HEADER_BG_COLOR = Google::Apis::DocsV1::OptionalColor.new(color: Google::Apis::DocsV1::Color.new(rgb_color: Google::Apis::DocsV1::RgbColor.new(red: 0.0, green: 0.8, blue: 0.2))) - ALT_BG_COLOR = Google::Apis::DocsV1::OptionalColor.new(color: Google::Apis::DocsV1::Color.new(rgb_color: Google::Apis::DocsV1::RgbColor.new(red: 0.2, green: 0.2, blue: 0.2))) - TABLE_BORDER = Google::Apis::DocsV1::TableCellBorder.new( - color: Google::Apis::DocsV1::OptionalColor.new( - color: Google::Apis::DocsV1::Color.new( - rgb_color: Google::Apis::DocsV1::RgbColor.new( - red: 0.0, green: 0.0, blue: 0.0 - ) - ) - ), - width: Google::Apis::DocsV1::Dimension.new( - magnitude: 1.0, unit: 'PT' - ) - ) - - def row_for_table(csv_row, row_idx, header_row = nil) - cells = [] - csv_row.each_with_index do |cell, idx_col| - # if necessary, handle TableColumnProperties here - width and widthType - header_cell = header_row ? header_row[idx_col] : nil - cells << cell_for_row(cell, row_idx, idx_col, header_value: header_cell) - end - row = Google::Apis::DocsV1::TableRow.new - row.table_cells = cells - table_row_style = Google::Apis::DocsV1::TableRowStyle.new - table_row_style.table_header = true if row_idx == 0 - row.table_row_style = table_row_style - row - end - - def cell_for_row(cell_value, row_idx, col_idx, header_value = nil) - text_run = Google::Apis::DocsV1::TextRun.new - text_run.content = cell_value - text_run_style = Google::Apis::DocsV1::TextStyle.new - text_run_style.font_size = Google::Apis::DocsV1::Dimension.new(magnitude: 9.0, unit: 'PT') - if row_idx == 0 - text_run_style.bold = true - end - text_run.text_style = text_run_style - paragraph_element = Google::Apis::DocsV1::ParagraphElement.new(text_run: text_run) - paragraph = Google::Apis::DocsV1::Paragraph.new(elements: [paragraph_element]) - structural_element = Google::Apis::DocsV1::StructuralElement.new(paragraph: paragraph) - cell = Google::Apis::DocsV1::TableCell.new - cell.content = [structural_element] - cell_style = Google::Apis::DocsV1::TableCellStyle.new - if row_idx == 0 - cell_style.background_color = HEADER_BG_COLOR - elsif row_idx.even? - cell_style.background_color = ALT_BG_COLOR - end - cell_style.content_alignment = 'LEFT' - cell_style.border_bottom = TABLE_BORDER - cell_style.border_left = TABLE_BORDER - cell_style.border_right = TABLE_BORDER - cell_style.border_top = TABLE_BORDER - cell.table_cell_style = cell_style - cell - end end diff --git a/app/views/forest_projects/new.html.erb b/app/views/forest_projects/new.html.erb index a97bd01..9804fe3 100644 --- a/app/views/forest_projects/new.html.erb +++ b/app/views/forest_projects/new.html.erb @@ -48,8 +48,8 @@
- <%= form.label :csv, "Upload CSV", class: "block text-sm font-medium leading-6 text-gray-900" %> - <%= form.file_field :csv, class: "rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900" %> + <%= form.label :csv_url, "Upload CSV", class: "block text-sm font-medium leading-6 text-gray-900" %> + <%= form.file_field :csv_url, class: "rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900" %>
@@ -64,4 +64,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/views/forest_projects/show.html.erb b/app/views/forest_projects/show.html.erb index 196860c..614de16 100644 --- a/app/views/forest_projects/show.html.erb +++ b/app/views/forest_projects/show.html.erb @@ -352,7 +352,6 @@ <% end %> <% end %> - <% end %> diff --git a/bin/bundle b/bin/bundle index 42c7fd7..c54a497 100755 --- a/bin/bundle +++ b/bin/bundle @@ -8,7 +8,7 @@ # this file is here to facilitate running it. # -require "rubygems" +require 'rubygems' m = Module.new do module_function @@ -18,12 +18,12 @@ m = Module.new do end def env_var_version - ENV["BUNDLER_VERSION"] + ENV['BUNDLER_VERSION'] end def cli_arg_version return unless invoked_as_script? # don't want to hijack other binstubs - return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update` bundler_version = nil update_index = nil ARGV.each_with_index do |a, i| @@ -38,16 +38,16 @@ m = Module.new do end def gemfile - gemfile = ENV["BUNDLE_GEMFILE"] + gemfile = ENV['BUNDLE_GEMFILE'] return gemfile if gemfile && !gemfile.empty? - File.expand_path("../Gemfile", __dir__) + File.expand_path('../Gemfile', __dir__) end def lockfile lockfile = case File.basename(gemfile) - when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + when 'gems.rb' then gemfile.sub(/\.rb$/, '.locked') else "#{gemfile}.lock" end File.expand_path(lockfile) @@ -76,18 +76,18 @@ m = Module.new do end def load_bundler! - ENV["BUNDLE_GEMFILE"] ||= gemfile + ENV['BUNDLE_GEMFILE'] ||= gemfile activate_bundler end def activate_bundler gem_error = activation_error_handling do - gem "bundler", bundler_requirement + gem 'bundler', bundler_requirement end return if gem_error.nil? require_error = activation_error_handling do - require "bundler/version" + require 'bundler/version' end return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" @@ -105,5 +105,5 @@ end m.load_bundler! if m.invoked_as_script? - load Gem.bin_path("bundler", "bundle") + load Gem.bin_path('bundler', 'bundle') end diff --git a/bin/cibuild b/bin/cibuild index 033c369..187e7ff 100755 --- a/bin/cibuild +++ b/bin/cibuild @@ -1,5 +1,14 @@ #!/bin/sh +# Leave these set to dummy values for CI purposes, so we can detect when we lack mocks. +export ATTR_ENCRYPTED_KEY=x +export GOOGLE_CLIENT_ID=x +export GOOGLE_CLIENT_SECRET=x +export GOOGLE_CLOUD_JSON=x +export GOOGLE_DOCS_TEMPLATE_ID=x +export GOOGLE_DRIVE_FOLDER_ID=x +export GOOGLE_SHEETS_TEMPLATE_ID=x + set -x if [ -d "./.git" ]; then git log -n 1 HEAD | cat @@ -15,6 +24,6 @@ export RAILS_ENV=test bundle exec rails db:migrate bundle exec rails assets:precompile >/dev/null 2>&1 bundle exec rspec spec -bundle exec rubocop +# bundle exec rubocop bundle exec standardrb diff --git a/bin/rails b/bin/rails index efc0377..f96e8f3 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,11 @@ #!/usr/bin/env ruby + +# bin/rails +# frozen_string_literal: true + +# This command will automatically be run when you run "rails" with Rails gems +# installed from the root of your application. + APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" diff --git a/bin/rake b/bin/rake index 4fbf10b..1b38f6a 100755 --- a/bin/rake +++ b/bin/rake @@ -1,4 +1,10 @@ #!/usr/bin/env ruby + +# bin/rake +# frozen_string_literal: true + +# This file was generated by Bundler. + require_relative "../config/boot" require "rake" Rake.application.run diff --git a/bin/setenv b/bin/setenv index 49346db..d2c8fc4 100755 --- a/bin/setenv +++ b/bin/setenv @@ -1,7 +1,12 @@ #!/usr/bin/env ruby -File.open('.env').each do |line| - next if line.strip.empty? || line.start_with?('#') - key, value = line.split('=') +# bin/setenv +# frozen_string_literal: true + +# This script deploys the .env file to Heroku. + +File.open(".env").each do |line| + next if line.strip.empty? || line.start_with?("#") + key, value = line.split("=") system("heroku config:set #{key}='#{value.strip}' -a forestbot") end diff --git a/bin/setup b/bin/setup index ec47b79..f475164 100755 --- a/bin/setup +++ b/bin/setup @@ -1,4 +1,8 @@ #!/usr/bin/env ruby + +# bin/setup +# frozen_string_literal: true + require "fileutils" # path to your application root. @@ -10,24 +14,24 @@ end FileUtils.chdir APP_ROOT do # This script is a way to set up or update your development environment automatically. - # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # This script is idempotent, so that you can run it at any time and get an acceptable outcome. # Add necessary setup steps to this file. puts "== Installing dependencies ==" system! "gem install bundler --conservative" system("bundle check") || system!("bundle install") - # puts "\n== Copying sample files ==" - # unless File.exist?("config/database.yml") - # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # puts '\n== Copying sample files ==' + # unless File.exist?('config/database.yml') + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' # end - puts "\n== Preparing database ==" + puts '\n== Preparing database ==' system! "bin/rails db:prepare" - puts "\n== Removing old logs and tempfiles ==" + puts '\n== Removing old logs and tempfiles ==' system! "bin/rails log:clear tmp:clear" - puts "\n== Restarting application server ==" + puts '\n== Restarting application server ==' system! "bin/rails restart" end diff --git a/config.ru b/config.ru index 4a3c09a..dc5bcee 100644 --- a/config.ru +++ b/config.ru @@ -1,3 +1,6 @@ +# config.ru +# frozen_string_literal: true + # This file is used by Rack-based servers to start the application. require_relative "config/environment" diff --git a/config/application.rb b/config/application.rb index 45db7bd..b219af5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,17 +1,19 @@ +# config/application.rb +# frozen_string_literal: true + require_relative "boot" require "rails/all" -#require 'attr_encrypted' +# require "attr_encrypted" # Limit this to when the app is running in development mode -if Rails.env.development? - require_relative '../lib/full_traceback_logger' -end +require_relative "../lib/full_traceback_logger" if Rails.env.development? || ENV.fetch("DEBUG", "") == "*" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Forestbot + # The Application class is the main entry point for the Rails application. class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 @@ -23,23 +25,20 @@ class Application < Rails::Application # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") - config.session_store :cookie_store, key: '_interslice_session' + config.session_store :cookie_store, key: "_interslice_session" config.middleware.use ActionDispatch::Cookies # Required for all session management - #config.middleware.use ActionDispatch::Session::CookieStore, config.session_options + # config.middleware.use ActionDispatch::Session::CookieStore, config.session_options # config.middleware.insert_before 0, Rack::Cors do # allow do - # origins 'https://forestbot.herokuapp.com' - # resource '*', + # origins "https://forestbot.herokuapp.com" + # resource "*", # headers: :any, # methods: %i[get post put patch delete options head], # credentials: true # end # end - if Rails.env.development? - config.middleware.use FullTracebackLogger - end - + config.middleware.use(FullTracebackLogger) if Rails.env.development? || ENV.fetch("DEBUG", "") == "*" end end diff --git a/config/boot.rb b/config/boot.rb index 988a5dd..b96540a 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,3 +1,6 @@ +# config/boot.rb +# frozen_string_literal: true + ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. diff --git a/config/environment.rb b/config/environment.rb index cac5315..437339b 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,3 +1,6 @@ +# config/environment.rb +# frozen_string_literal: true + # Load the Rails application. require_relative "application" diff --git a/config/environments/development.rb b/config/environments/development.rb index cec641c..b45a22c 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,4 +1,5 @@ # config/environments/development.rb +# frozen_string_literal: true require "active_support/core_ext/integer/time" @@ -48,7 +49,7 @@ config.action_mailer.perform_caching = false - #config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } + # config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log diff --git a/config/environments/production.rb b/config/environments/production.rb index 207aec1..0a62b5c 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,4 +1,5 @@ # config/environments/production.rb +# frozen_string_literal: true require "active_support/core_ext/integer/time" @@ -18,7 +19,7 @@ config.eager_load = true # Full error reports are disabled and caching is turned on. - config.consider_all_requests_local = false + config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] @@ -58,7 +59,7 @@ config.log_level = :info # Prepend all log lines with the following tags. - config.log_tags = [ :request_id ] + config.log_tags = [:request_id] # Use a different cache store in production. # config.cache_store = :mem_cache_store @@ -81,16 +82,16 @@ config.active_support.report_deprecations = false # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + config.log_formatter = Logger::Formatter.new # Use a different logger for distributed setups. # require "syslog/logger" # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") if ENV["RAILS_LOG_TO_STDOUT"].present? - logger = ActiveSupport::Logger.new(STDOUT) + logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) + config.logger = ActiveSupport::TaggedLogging.new(logger) end # Do not dump schema after migrations. diff --git a/config/environments/test.rb b/config/environments/test.rb index 6ea4d1e..6c437af 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,3 +1,6 @@ +# config/environments/test.rb +# frozen_string_literal: true + require "active_support/core_ext/integer/time" # The test environment is used exclusively to run your application's @@ -7,6 +10,8 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. + config.host = "127.0.0.1" + config.hosts << "127.0.0.1" # Turn false under Spring and add config.action_view.cache_template_loading = true. config.cache_classes = true @@ -23,7 +28,7 @@ } # Show full error reports and disable caching. - config.consider_all_requests_local = true + config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 2eeef96..5f8b361 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,3 +1,6 @@ +# config/initializers/assets.rb +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 54f47cf..57a377c 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -1,3 +1,6 @@ +# config/initializers/content_security_policy.rb +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Define an application-wide content security policy. diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index 1a2a64a..a36c178 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -1,12 +1,13 @@ # config/initializers/cors.rb +# frozen_string_literal: true Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins %w[https://forestbot.herokuapp.com https://forestbot.cfs.eco] - resource '*', - headers: :any, - methods: [:get, :post, :put, :patch, :delete, :options, :head], - credentials: true + resource "*", + headers: :any, + methods: %i[get post put patch delete options head], + credentials: true end end diff --git a/config/initializers/debug.rb b/config/initializers/debug.rb index 7a23714..402e0d7 100644 --- a/config/initializers/debug.rb +++ b/config/initializers/debug.rb @@ -1,20 +1,24 @@ -class Object +# config/initializers/debug.rb +# frozen_string_literal: true +# This adds debugging methods to the Object class. It is intended to be used +# in development and test environments. It is not intended to be used in +# production environments. +# +# Set the DEBUG environment variable to '*' to enable debugging. +class Object private def dbg(message) - if Rails.env.development? || ENV['DEBUG'] == '*' - puts " d #{message}" - end + return unless Rails.env.development? || ENV.fetch("DEBUG", "") == "*" + Rails.logger.debug { " d #{message}" } end def entry_dbg - if Rails.env.development? || ENV['DEBUG'] == '*' - caller_location = caller_locations(1, 1)[0] - path = caller_location.path.gsub("#{Rails.root}/", "") - method_name = caller_location.label - warn " - #{path}:#{caller_location.lineno}:in #{method_name}" - end + return unless Rails.env.development? || ENV.fetch("DEBUG", "") == "*" + caller_location = caller_locations(1, 1)[0] + path = caller_location.path.gsub("#{Rails.root}/", "") # rubocop:disable Rails/FilePath + method_name = caller_location.label + warn " - #{path}:#{caller_location.lineno}:in #{method_name}" end - end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index adc6568..1264525 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,8 +1,9 @@ +# config/initializers/filter_parameter_logging.rb +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Configure parameters to be filtered from the log file. Use this to limit dissemination of # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported # notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn -] +Rails.application.config.filter_parameters += %i[passw secret token _key crypt salt certificate otp ssn] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 3860f65..91d1553 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,3 +1,6 @@ +# config/initializers/inflections.rb +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 73d9a80..eb8acc7 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -1,17 +1,16 @@ # config/initializers/omniauth.rb - # frozen_string_literal: true Rails.application.config.middleware.use OmniAuth::Builder do provider :developer if Rails.env.development? - OmniAuth.config.allowed_request_methods = [:post, :get] + OmniAuth.config.allowed_request_methods = %i[post get] OmniAuth.config.full_host = Rails.configuration.host - provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], { + provider :google_oauth2, ENV.fetch("GOOGLE_CLIENT_ID"), ENV.fetch("GOOGLE_CLIENT_SECRET"), { scope: %w[email profile https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/documents], - prompt: 'select_account', - hd: 'cfs.eco', + prompt: "select_account", + hd: "cfs.eco", redirect_uri: "https://#{Rails.configuration.host}/auth/google_oauth2/callback" } end diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb index 00f64d7..2c346f9 100644 --- a/config/initializers/permissions_policy.rb +++ b/config/initializers/permissions_policy.rb @@ -1,3 +1,6 @@ +# config/initializers/permissions_policy.rb +# frozen_string_literal: true + # Define an application-wide HTTP permissions policy. For further # information see https://developers.google.com/web/updates/2018/06/feature-policy # diff --git a/config/initializers/redis.rb b/config/initializers/redis.rb index c6b78c2..0eae814 100644 --- a/config/initializers/redis.rb +++ b/config/initializers/redis.rb @@ -1 +1,9 @@ -$redis = Redis.new(url: ENV["REDIS_URL"], ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }) +# config/initializers/redis.rb +# frozen_string_literal: true + +$redis = Redis.new( # rubocop:disable Style/GlobalVars + url: ENV.fetch("REDIS_URL", nil), + ssl_params: { + verify_mode: OpenSSL::SSL::VERIFY_NONE + } +) diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index f23696a..9ad7cf8 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,18 +1,19 @@ # config/initializers/session_store.rb +# frozen_string_literal: true -require 'redis-rails' +require "redis-rails" Rails.application.config.session_store( :redis_store, servers: [ { - url: ENV['REDIS_URL'], - ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }, + url: ENV.fetch("REDIS_URL", nil), + ssl_params: {verify_mode: OpenSSL::SSL::VERIFY_NONE}, db: 0, - namespace: 'session' + namespace: "session" } ], - key: '_my_app_session', + key: "_my_app_session", serializer: :json, expire_after: 1.day -) \ No newline at end of file +) diff --git a/config/puma.rb b/config/puma.rb index daaf036..b2b54b0 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,10 +1,13 @@ +# config/puma.rb +# frozen_string_literal: true + # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # -max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count @@ -15,14 +18,14 @@ # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # -port ENV.fetch("PORT") { 3000 } +port ENV.fetch("PORT", 3000) # Specifies the `environment` that Puma will run in. # -environment ENV.fetch("RAILS_ENV") { "development" } +environment ENV.fetch("RAILS_ENV", "development") # Specifies the `pidfile` that Puma will use. -pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } +pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid") # Specifies the number of `workers` to boot in clustered mode. # Workers are forked web server processes. If using threads and workers together diff --git a/config/routes.rb b/config/routes.rb index 09ecc81..06db916 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,19 +1,19 @@ # config/routes.rb +# frozen_string_literal: true require "googleauth" Rails.application.routes.draw do - # Google OAuth2 via OmniAuth for user authentication - get '/login', to: redirect('/auth/google_oauth2') - get '/logout', to: 'sessions#destroy' - get '/auth/:provider/callback', to: 'sessions#omniauth_callback' - post '/auth/:provider/callback', to: 'sessions#omniauth_callback' + get "/login", to: redirect("/auth/google_oauth2") + get "/logout", to: "sessions#destroy" + get "/auth/:provider/callback", to: "sessions#omniauth_callback" + post "/auth/:provider/callback", to: "sessions#omniauth_callback" resources :forest_projects do get :download_csv, on: :member get :reprocess, on: :member end - root 'home#index' + root "home#index" end diff --git a/db/migrate/20230417181939_create_active_storage_tables.active_storage.rb b/db/migrate/20230417181939_create_active_storage_tables.active_storage.rb index 8a7bfe1..7015eee 100644 --- a/db/migrate/20230417181939_create_active_storage_tables.active_storage.rb +++ b/db/migrate/20230417181939_create_active_storage_tables.active_storage.rb @@ -5,13 +5,13 @@ def change primary_key_type, foreign_key_type = primary_and_foreign_key_types create_table :active_storage_blobs, id: primary_key_type do |t| - t.string :key, null: false - t.string :filename, null: false - t.string :content_type - t.text :metadata - t.string :service_name, null: false - t.bigint :byte_size, null: false - t.string :checksum + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum if connection.supports_datetime_with_precision? t.datetime :created_at, precision: 6, null: false @@ -19,13 +19,13 @@ def change t.datetime :created_at, null: false end - t.index [ :key ], unique: true + t.index [:key], unique: true end create_table :active_storage_attachments, id: primary_key_type do |t| - t.string :name, null: false - t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type - t.references :blob, null: false, type: foreign_key_type + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type if connection.supports_datetime_with_precision? t.datetime :created_at, precision: 6, null: false @@ -33,7 +33,7 @@ def change t.datetime :created_at, null: false end - t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true + t.index %i[record_type record_id name blob_id], name: :index_active_storage_attachments_uniqueness, unique: true t.foreign_key :active_storage_blobs, column: :blob_id end @@ -41,17 +41,18 @@ def change t.belongs_to :blob, null: false, index: false, type: foreign_key_type t.string :variation_digest, null: false - t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true + t.index %i[blob_id variation_digest], name: :index_active_storage_variant_records_uniqueness, unique: true t.foreign_key :active_storage_blobs, column: :blob_id end end private - def primary_and_foreign_key_types - config = Rails.configuration.generators - setting = config.options[config.orm][:primary_key_type] - primary_key_type = setting || :primary_key - foreign_key_type = setting || :bigint - [primary_key_type, foreign_key_type] - end + + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end end diff --git a/db/seeds.rb b/db/seeds.rb index bc25fce..eb4b28d 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,10 @@ +# db/seeds.rb +# frozen_string_literal: true + # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). # # Examples: # -# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) -# Character.create(name: "Luke", movie: movies.first) +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) diff --git a/lib/full_traceback_logger.rb b/lib/full_traceback_logger.rb index 06f70e9..e9e0e82 100644 --- a/lib/full_traceback_logger.rb +++ b/lib/full_traceback_logger.rb @@ -1,14 +1,17 @@ +# lib/full_traceback_logger.rb +# frozen_string_literal: true + +# FullTracebackLogger is a Rack middleware that logs the full traceback of +# any exception that is raised in the application. class FullTracebackLogger def initialize(app) @app = app end def call(env) - begin - @app.call(env) - rescue Exception => e - Rails.logger.error "Full Traceback:\n#{e.full_message(highlight: false)}" - raise e - end + @app.call(env) + rescue Exception => e # rubocop:disable Lint/RescueException + Rails.logger.error "Full Traceback:\n#{e.full_message(highlight: false)}" + raise e end end diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index d19212a..1c66e1b 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -1,5 +1,9 @@ +# test/application_system_test_case.rb +# frozen_string_literal: true + require "test_helper" +# ApplicationSystemTestCase is a base class for all system tests. class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :chrome, screen_size: [1400, 1400] end diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb index 800405f..8c266b2 100644 --- a/test/channels/application_cable/connection_test.rb +++ b/test/channels/application_cable/connection_test.rb @@ -1,11 +1,16 @@ +# test/channels/application_cable/connection_test.rb +# frozen_string_literal: true + require "test_helper" -class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase - # test "connects with cookies" do - # cookies.signed[:user_id] = 42 - # - # connect - # - # assert_equal connection.user_id, "42" - # end +module ApplicationCable + class ConnectionTest < ActionCable::Connection::TestCase + # test 'connects with cookies' do + # cookies.signed[:user_id] = 42 + # + # connect + # + # assert_equal connection.user_id, '42' + # end + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index d713e37..4dd16ca 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,13 +1,19 @@ +# test/test_helper.rb +# frozen_string_literal: true + ENV["RAILS_ENV"] ||= "test" require_relative "../config/environment" require "rails/test_help" -class ActiveSupport::TestCase - # Run tests in parallel with specified workers - parallelize(workers: :number_of_processors) +# ActiveSupport::TestCase is the base class for all tests in Rails. +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - fixtures :all + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all - # Add more helper methods to be used by all tests here... + # Add more helper methods to be used by all tests here... + end end