diff --git a/.circleci/config.yml b/.circleci/config.yml index 2a506cd2804..5a74c3007cc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,9 +1,9 @@ version: 2.1 orbs: - ruby: circleci/ruby@2.5.0 - node: circleci/node@7.1.0 - browser-tools: circleci/browser-tools@1.5.2 + ruby: circleci/ruby@2.5.4 + node: circleci/node@7.1.1 + browser-tools: circleci/browser-tools@2.3.1 jobs: build: docker: @@ -18,7 +18,7 @@ jobs: - ruby/install-deps - node/install: install-yarn: true - node-version: '20.9.0' + node-version: '22.19.0' - node/install-packages: pkg-manager: yarn check: @@ -49,7 +49,7 @@ jobs: command: FAIL_ON_ERROR=1 bundle exec rake traceroute - node/install: install-yarn: true - node-version: '20.9.0' + node-version: '22.19.0' - node/install-packages: pkg-manager: yarn - run: @@ -65,22 +65,20 @@ jobs: - image: cimg/postgres:14.4 resource_class: large environment: - BUNDLE_JOBS: "3" - BUNDLE_RETRY: "3" + BUNDLE_JOBS: '3' + BUNDLE_RETRY: '3' PGHOST: 127.0.0.1 PGUSER: postgres - PGPASSWORD: "postgres" + PGPASSWORD: 'postgres' RAILS_ENV: test - DATABASE_URL: "postgres://postgres:postgres@localhost/ci_test" + DATABASE_URL: 'postgres://postgres:postgres@localhost/ci_test' TZ: Asia/Tokyo PARALLEL_WORKERS: 2 parallelism: 3 steps: - checkout - - browser-tools/install-chrome: - replace-existing: true - chrome-version: 130.0.6723.116 - - browser-tools/install-chromedriver + - browser-tools/install_browser_tools: + chrome-version: 130.0.6723.116 - run: command: | google-chrome --version @@ -95,7 +93,7 @@ jobs: clean-bundle: true - node/install: install-yarn: true - node-version: '20.9.0' + node-version: '22.19.0' - node/install-packages: pkg-manager: yarn - run: @@ -132,5 +130,3 @@ workflows: - test: requires: - build - -# VS Code Extension Version: 1.5.1 diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index cc7ce7fdb7e..dbcfff20342 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,7 +5,7 @@ ARG VARIANT=3.1-bullseye FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT} # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 -ARG NODE_VERSION="lts/*" +ARG NODE_VERSION="22.19.0" RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi # Install OS packages diff --git a/.gitignore b/.gitignore index d9ceb3871ea..d1aa6aac20c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,10 @@ storage/ .envrc .env.local /test/reports + +/public/packs +/public/packs-test +/node_modules +/yarn-error.log +yarn-debug.log* +.yarn-integrity diff --git a/.mise.toml b/.mise.toml index ed6604e3b35..42a0368517e 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,3 +1,3 @@ [tools] ruby = "3.1.6" -node = "20.9.0" +node = "22.19.0" diff --git a/.node-version b/.node-version index f3f52b42d3d..e2228113dd0 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.9.0 +22.19.0 diff --git a/.nvmrc b/.nvmrc index f3f52b42d3d..e2228113dd0 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.9.0 +22.19.0 diff --git a/.tool-versions b/.tool-versions index 7b9b33cd5b4..1fc8ba353fe 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ ruby 3.1.6 -nodejs 20.9.0 +nodejs 22.19.0 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..b190071fc81 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `app/` Rails app code: `models/`, `controllers/`, `views/`, `jobs/`, `helpers/`, and frontend under `javascript/` (Shakapacker). +- `config/` environment, routes, and lints (see `.rubocop.yml`, `config/slim_lint.yml`). +- `db/` migrations and schema; `lib/` app-specific utilities; `public/` static assets. +- `test/` Minitest suite: `system/`, `models/`, `controllers/`, fixtures in `test/fixtures/`. +- `bin/` helper scripts; `Procfile.dev` runs Rails and asset dev server. + +## Build, Test, and Development Commands +- Setup: `bin/setup` — install gems, prepare DB, yarn, etc. +- Run (dev): `foreman start -f Procfile.dev` — Rails on `:3000` + Shakapacker. +- Tests (headless): `rails test:all`. +- Tests (browser): `HEADFUL=1 rails test:all`. +- Tests (no parallel): `PARALLEL_WORKERS=1 rails test:all`. +- Lint: `./bin/lint` — RuboCop (auto-correct), Slim-Lint, ESLint/Prettier. +- Profiler: `PROFILE=1 rails server` to enable rack-mini-profiler. + +## Coding Style & Naming Conventions +- Ruby: 2-space indent, snake_case methods, CamelCase classes; enforced by RuboCop (`.rubocop.yml`). +- Views: Slim templates, linted by `config/slim_lint.yml`. +- JS/TS: Code in `app/javascript/`; ESLint + Prettier via `yarn lint` scripts; React 17 and Shakapacker/Webpack 5. +- Files follow Rails conventions (e.g., `app/models/user.rb`, test `test/models/user_test.rb`). + +## Testing Guidelines +- Frameworks: Minitest + Capybara for system tests. +- Structure: place unit/integration tests under matching `test/*` directories; name files `*_test.rb`. +- Run a single test or line: `rails test test/models/user_test.rb:42`. +- Keep tests deterministic; use fixtures in `test/fixtures/`. + +## Commit & Pull Request Guidelines +- Commits: imperative mood and focused scope; reference issues (e.g., "Fix profile validation #123"). +- PRs: clear description, linked issues, screenshots for UI changes, migration notes, and rollback plan if relevant. +- Quality gates: all linters pass (`bin/lint`) and CI (CircleCI) green; add/adjust tests when changing behavior. + +## Security & Configuration Tips +- Never commit secrets; use `.env.local`. Respect `.ruby-version`, `.tool-versions`, and Node versions in `.node-version`/`.nvmrc`. +- Use `bin/setup` for local DB and dependencies; avoid manual tweaks in `config/` without discussion. + +## Agent-Specific Instructions +- Follow these guidelines for any edits. Keep changes minimal, scoped to the task, and update docs/tests when adding commands or changing behavior. + diff --git a/Dockerfile b/Dockerfile index 1843de4b872..9c0126260bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM ruby:3.1.6-slim +# Build stage - includes devDependencies for asset compilation +FROM ruby:3.1.6-slim as builder ENV RAILS_ENV production WORKDIR /app @@ -8,32 +9,40 @@ RUN gem update --system RUN printf "install: --no-rdoc --no-ri\nupdate: --no-rdoc --no-ri" > ~/.gemrc RUN gem install --no-document --force bundler -v 2.4.21 -# Install packages -RUN apt-get update -qq && apt-get install -y \ +# Install build dependencies with minimal footprint +RUN apt-get update -qq && apt-get install -y --no-install-recommends \ build-essential \ git \ - nodejs \ postgresql-client \ libpq-dev \ tzdata \ curl \ gnupg2 \ - libyaml-dev + libyaml-dev \ + ca-certificates \ + libvips-dev && \ + rm -rf /var/lib/apt/lists/* + +# Install Node.js 22.19.0 +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get update && \ + apt-get install -y --no-install-recommends nodejs=22.19.0-1nodesource1 && \ + apt-mark hold nodejs && \ + rm -rf /var/lib/apt/lists/* # Install latest yarn 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 -y yarn + apt-get update && \ + apt-get install -y --no-install-recommends yarn && \ + rm -rf /var/lib/apt/lists/* # Set timezone RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime -# libvips -RUN apt-get install -y libvips-dev - -# Install npm packages +# Install ALL npm packages (including devDependencies for asset compilation) COPY package.json yarn.lock ./ -RUN yarn install --prod --ignore-engines +RUN yarn install --ignore-engines # Install gems COPY Gemfile Gemfile.lock ./ @@ -42,11 +51,67 @@ RUN bundle install -j4 # Copy application code COPY . ./ -# Compile assets +# Compile assets (now with devDependencies available) ENV RAILS_LOG_TO_STDOUT true -RUN SECRET_KEY_BASE=dummy NODE_OPTIONS=--openssl-legacy-provider bundle exec rails assets:precompile +RUN SECRET_KEY_BASE=dummy bundle exec rails assets:precompile + +# Production stage - minimal runtime image +FROM ruby:3.1.6-slim as production + +ENV RAILS_ENV production +WORKDIR /app + +# Update rubygems +RUN gem update --system +RUN printf "install: --no-rdoc --no-ri\nupdate: --no-rdoc --no-ri" > ~/.gemrc +RUN gem install --no-document --force bundler -v 2.4.21 + +# Install only runtime dependencies +RUN apt-get update -qq && apt-get install -y --no-install-recommends \ + postgresql-client \ + libpq-dev \ + tzdata \ + ca-certificates \ + libvips && \ + rm -rf /var/lib/apt/lists/* + +# Set timezone +RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime + +# Install Node.js 22.19.0 (runtime only) +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get update && \ + apt-get install -y --no-install-recommends nodejs=22.19.0-1nodesource1 && \ + apt-mark hold nodejs && \ + rm -rf /var/lib/apt/lists/* + +# Install yarn for runtime +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 -y --no-install-recommends yarn && \ + rm -rf /var/lib/apt/lists/* + +# Copy gems configuration +COPY Gemfile Gemfile.lock ./ +RUN bundle install -j4 + +# Install only production npm packages +COPY package.json yarn.lock ./ +RUN yarn install --prod --ignore-engines + +# Copy application code from builder (excluding large directories) +COPY --from=builder /app/app ./app +COPY --from=builder /app/bin ./bin +COPY --from=builder /app/config ./config +COPY --from=builder /app/db ./db +COPY --from=builder /app/lib ./lib +COPY --from=builder /app/public ./public +COPY --from=builder /app/Rakefile ./Rakefile +COPY --from=builder /app/config.ru ./config.ru ENV PORT 3000 +ENV RAILS_LOG_TO_STDOUT true EXPOSE 3000 CMD bin/rails server -p $PORT -e $RAILS_ENV diff --git a/Gemfile b/Gemfile index b6842536d4c..4a4ad238465 100644 --- a/Gemfile +++ b/Gemfile @@ -9,9 +9,10 @@ gem 'bootsnap', '>= 1.4.4', require: false gem 'ffi', '1.17.1' gem 'image_processing', '~> 1.12' gem 'jbuilder', '~> 2.7' -gem 'puma', '~> 5.0' -gem 'rails', '~> 6.1.7.10' -gem 'webpacker', '~> 5.0' +gem 'puma', '~> 6.0' +gem 'rails', '7.2.2.2' +gem 'shakapacker', '~> 7.0' +gem 'sprockets-rails', '>= 2.0.0' # not default gem 'abstract_notifier', '~> 0.3.2' @@ -32,7 +33,7 @@ gem 'diffy' gem 'discord-notifier' gem 'discordrb', '~> 3.5', require: false gem 'doorkeeper' -gem 'good_job', '~> 3.14', github: 'komagata/good_job' +gem 'good_job', '~> 4.5' gem 'google-cloud-storage', '~> 1.25', require: false gem 'holiday_jp' gem 'icalendar', '~> 2.8' @@ -62,9 +63,9 @@ gem 'postmark-rails' gem 'rack-cors', require: 'rack/cors' gem 'rack-user_agent' gem 'rails_autolink' -gem 'rails-i18n', '~> 6.0.0' +gem 'rails-i18n', '~> 7.0.0' gem 'rails-patterns', '~> 0.2' -gem 'ransack', '3.1.0' +gem 'ransack', '~> 4.3' gem 'react-rails' gem 'recaptcha', '~> 5.12' gem 'rollbar' @@ -92,8 +93,6 @@ end group :development do gem 'listen', '~> 3.3' - gem 'spring' - gem 'spring-watcher-listen', '~> 2.0.0' gem 'web-console', '>= 4.1.0' # not default diff --git a/Gemfile.lock b/Gemfile.lock index de18dcdb5aa..39052ccda2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,16 +1,3 @@ -GIT - remote: https://github.com/komagata/good_job.git - revision: 8b8d2702d9ab62588de0e3a3600dfd0e500e3ccd - specs: - good_job (3.14.1) - activejob (>= 6.0.0) - activerecord (>= 6.0.0) - concurrent-ruby (>= 1.0.2) - fugit (>= 1.1) - railties (>= 6.0.0) - thor (>= 0.14.1) - webrick (>= 1.3) - GIT remote: https://github.com/komagata/stripe-i18n revision: 584c711fc66ad71a5293a9dc21d717ec608c4692 @@ -24,44 +11,50 @@ GEM remote: https://rubygems.org/ specs: abstract_notifier (0.3.2) - actioncable (6.1.7.10) - actionpack (= 6.1.7.10) - activesupport (= 6.1.7.10) + actioncable (7.2.2.2) + actionpack (= 7.2.2.2) + activesupport (= 7.2.2.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.10) - actionpack (= 6.1.7.10) - activejob (= 6.1.7.10) - activerecord (= 6.1.7.10) - activestorage (= 6.1.7.10) - activesupport (= 6.1.7.10) - mail (>= 2.7.1) - actionmailer (6.1.7.10) - actionpack (= 6.1.7.10) - actionview (= 6.1.7.10) - activejob (= 6.1.7.10) - activesupport (= 6.1.7.10) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.7.10) - actionview (= 6.1.7.10) - activesupport (= 6.1.7.10) - rack (~> 2.0, >= 2.0.9) + zeitwerk (~> 2.6) + actionmailbox (7.2.2.2) + actionpack (= 7.2.2.2) + activejob (= 7.2.2.2) + activerecord (= 7.2.2.2) + activestorage (= 7.2.2.2) + activesupport (= 7.2.2.2) + mail (>= 2.8.0) + actionmailer (7.2.2.2) + actionpack (= 7.2.2.2) + actionview (= 7.2.2.2) + activejob (= 7.2.2.2) + activesupport (= 7.2.2.2) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (7.2.2.2) + actionview (= 7.2.2.2) + activesupport (= 7.2.2.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4, < 3.2) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.10) - actionpack (= 6.1.7.10) - activerecord (= 6.1.7.10) - activestorage (= 6.1.7.10) - activesupport (= 6.1.7.10) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (7.2.2.2) + actionpack (= 7.2.2.2) + activerecord (= 7.2.2.2) + activestorage (= 7.2.2.2) + activesupport (= 7.2.2.2) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.7.10) - activesupport (= 6.1.7.10) + actionview (7.2.2.2) + activesupport (= 7.2.2.2) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) active_decorator (1.4.1) activesupport active_delivery (0.4.4) @@ -76,29 +69,36 @@ GEM activestorage (>= 6.1.4) activesupport (>= 6.1.4) marcel (>= 1.0.3) - activejob (6.1.7.10) - activesupport (= 6.1.7.10) + activejob (7.2.2.2) + activesupport (= 7.2.2.2) globalid (>= 0.3.6) - activemodel (6.1.7.10) - activesupport (= 6.1.7.10) - activerecord (6.1.7.10) - activemodel (= 6.1.7.10) - activesupport (= 6.1.7.10) - activestorage (6.1.7.10) - actionpack (= 6.1.7.10) - activejob (= 6.1.7.10) - activerecord (= 6.1.7.10) - activesupport (= 6.1.7.10) + activemodel (7.2.2.2) + activesupport (= 7.2.2.2) + activerecord (7.2.2.2) + activemodel (= 7.2.2.2) + activesupport (= 7.2.2.2) + timeout (>= 0.4.0) + activestorage (7.2.2.2) + actionpack (= 7.2.2.2) + activejob (= 7.2.2.2) + activerecord (= 7.2.2.2) + activesupport (= 7.2.2.2) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (6.1.7.10) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (7.2.2.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - acts-as-taggable-on (10.0.0) - activerecord (>= 6.1, < 7.2) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + acts-as-taggable-on (11.0.0) + activerecord (>= 7.0, < 8.0) + zeitwerk (>= 2.4, < 3.0) acts_as_list (1.2.4) activerecord (>= 6.1) activesupport (>= 6.1) @@ -117,8 +117,8 @@ GEM execjs (~> 2.0) base64 (0.3.0) bcrypt (3.1.20) - benchmark (0.4.0) - bigdecimal (3.1.9) + benchmark (0.4.1) + bigdecimal (3.2.3) bindex (0.8.1) bootsnap (1.18.4) msgpack (~> 1.2) @@ -136,6 +136,7 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + cgi (0.5.0) childprocess (5.1.0) logger (~> 1.5) cocooned (2.3.0) @@ -144,7 +145,7 @@ GEM coercible (1.0.0) descendants_tracker (~> 0.0.1) concurrent-ruby (1.3.4) - connection_pool (2.5.0) + connection_pool (2.5.4) countries (7.1.1) unaccent (~> 0.3) country_select (10.0.1) @@ -179,6 +180,9 @@ GEM dotenv-rails (3.1.8) dotenv (= 3.1.8) railties (>= 6.1) + drb (2.2.3) + erb (4.0.4) + cgi (>= 0.3.3) erubi (1.13.1) et-orbi (1.2.11) tzinfo @@ -202,6 +206,13 @@ GEM raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) + good_job (4.11.2) + activejob (>= 6.1.0) + activerecord (>= 6.1.0) + concurrent-ruby (>= 1.3.1) + fugit (>= 1.11.0) + railties (>= 6.1.0) + thor (>= 1.0.0) google-apis-core (0.16.0) addressable (~> 2.5, >= 2.5.1) googleauth (~> 1.9) @@ -258,6 +269,11 @@ GEM mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) interactor (3.1.2) + io-console (0.8.1) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) jbuilder (2.13.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) @@ -300,7 +316,7 @@ GEM listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.6) + logger (1.7.0) loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -394,6 +410,7 @@ GEM ffi os (1.1.4) ostruct (0.6.1) + package_json (0.1.0) parallel (1.26.3) parser (3.2.2.4) ast (~> 2.4.1) @@ -404,49 +421,60 @@ GEM postmark-rails (0.22.1) actionmailer (>= 3.0.0) postmark (>= 1.21.3, < 2.0) + pp (0.6.2) + prettyprint + prettyprint (0.2.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) pry-byebug (3.10.1) byebug (~> 11.0) pry (>= 0.13, < 0.15) + psych (5.2.6) + date + stringio public_suffix (6.0.1) - puma (5.6.9) + puma (6.6.1) nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.18) + rack (3.1.16) rack-cors (2.0.2) rack (>= 2.0.0) rack-dev-mark (0.8.0) rack (>= 1.1, < 4.0) rack-mini-profiler (2.3.4) rack (>= 1.2.0) - rack-protection (3.2.0) + rack-protection (4.1.1) base64 (>= 0.1.0) - rack (~> 2.2, >= 2.2.4) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) rack-proxy (0.7.7) rack + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) rack-user_agent (0.5.3) rack (>= 1.5) woothee (>= 1.0.0) - rails (6.1.7.10) - actioncable (= 6.1.7.10) - actionmailbox (= 6.1.7.10) - actionmailer (= 6.1.7.10) - actionpack (= 6.1.7.10) - actiontext (= 6.1.7.10) - actionview (= 6.1.7.10) - activejob (= 6.1.7.10) - activemodel (= 6.1.7.10) - activerecord (= 6.1.7.10) - activestorage (= 6.1.7.10) - activesupport (= 6.1.7.10) + rackup (2.2.1) + rack (>= 3) + rails (7.2.2.2) + actioncable (= 7.2.2.2) + actionmailbox (= 7.2.2.2) + actionmailer (= 7.2.2.2) + actionpack (= 7.2.2.2) + actiontext (= 7.2.2.2) + actionview (= 7.2.2.2) + activejob (= 7.2.2.2) + activemodel (= 7.2.2.2) + activerecord (= 7.2.2.2) + activestorage (= 7.2.2.2) + activesupport (= 7.2.2.2) bundler (>= 1.15.0) - railties (= 6.1.7.10) - sprockets-rails (>= 2.0.0) + railties (= 7.2.2.2) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -454,9 +482,9 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails-i18n (6.0.0) + rails-i18n (7.0.10) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 7) + railties (>= 6.0.0, < 8) rails-patterns (0.11.0) actionpack (>= 4.2.6) activerecord (>= 4.2.6) @@ -466,21 +494,26 @@ GEM actionview (> 3.1) activesupport (> 3.1) railties (> 3.1) - railties (6.1.7.10) - actionpack (= 6.1.7.10) - activesupport (= 6.1.7.10) - method_source + railties (7.2.2.2) + actionpack (= 7.2.2.2) + activesupport (= 7.2.2.2) + irb (~> 1.13) + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.0) - ransack (3.1.0) - activerecord (>= 6.0.4) - activesupport (>= 6.0.4) + ransack (4.3.0) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) i18n rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) + rdoc (6.14.2) + erb + psych (>= 4.0.0) react-rails (3.2.1) babel-transpiler (>= 0.7.0) connection_pool @@ -489,6 +522,8 @@ GEM tilt recaptcha (5.19.0) regexp_parser (2.10.0) + reline (0.6.2) + io-console (~> 0.5) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -542,12 +577,19 @@ GEM logger ruby2_keywords (0.0.5) rubyzip (2.4.1) + securerandom (0.4.1) selenium-webdriver (4.17.0) base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) semantic_range (3.1.0) + shakapacker (7.2.3) + activesupport (>= 5.2) + package_json + rack-proxy (>= 0.6.1) + railties (>= 5.2) + semantic_range (>= 2.3.0) signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -574,10 +616,6 @@ GEM sorcery-jwt (0.1.13) jwt (>= 1.0, < 3.0) sorcery (>= 0.13, < 0.17) - spring (2.1.1) - spring-watcher-listen (2.0.1) - listen (>= 2.7, < 4.0) - spring (>= 1.2, < 3.0) sprockets (4.2.2) concurrent-ruby (~> 1.0) logger @@ -605,6 +643,7 @@ GEM unicode-display_width (2.6.0) uniform_notifier (1.16.0) uri (1.0.3) + useragent (0.16.11) vcr (6.3.1) base64 version_gem (1.1.6) @@ -627,12 +666,6 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.4.4) - activesupport (>= 5.2) - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) - webrick (1.9.1) websocket (1.2.11) websocket-client-simple (0.9.0) base64 @@ -680,7 +713,7 @@ DEPENDENCIES dotenv-rails ffi (= 1.17.1) foreman - good_job (~> 3.14)! + good_job (~> 4.5) google-cloud-storage (~> 1.25) holiday_jp icalendar (~> 2.8) @@ -715,16 +748,16 @@ DEPENDENCIES pg (~> 1.4.6) postmark-rails pry-byebug - puma (~> 5.0) + puma (~> 6.0) rack-cors rack-dev-mark rack-mini-profiler (~> 2.0) rack-user_agent - rails (~> 6.1.7.10) - rails-i18n (~> 6.0.0) + rails (= 7.2.2.2) + rails-i18n (~> 7.0.0) rails-patterns (~> 0.2) rails_autolink - ransack (= 3.1.0) + ransack (~> 4.3) react-rails recaptcha (~> 5.12) rollbar @@ -738,12 +771,12 @@ DEPENDENCIES ruby-openai rubyzip selenium-webdriver (~> 4.17.0) + shakapacker (~> 7.0) slim-rails slim_lint sorcery (~> 0.16.2) sorcery-jwt - spring - spring-watcher-listen (~> 2.0.0) + sprockets-rails (>= 2.0.0) stringio (= 3.0.1.2) stripe stripe-i18n! @@ -755,7 +788,6 @@ DEPENDENCIES view_source_map web-console (>= 4.1.0) webmock - webpacker (~> 5.0) RUBY VERSION ruby 3.1.6p260 diff --git a/Procfile.dev b/Procfile.dev index 90246696f4a..65d9e59fea3 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,2 +1,2 @@ rails: rails s -b 0.0.0.0 -p 3000 -webpacker: ./bin/webpack-dev-server +webpacker: ./bin/shakapacker-dev-server diff --git a/app/controllers/admin/home_controller.rb b/app/controllers/admin/home_controller.rb index 3579f59937a..a1c1ceafcdf 100644 --- a/app/controllers/admin/home_controller.rb +++ b/app/controllers/admin/home_controller.rb @@ -2,6 +2,4 @@ class Admin::HomeController < AdminController def index; end - - def test; end end diff --git a/app/controllers/api/notifications_controller.rb b/app/controllers/api/notifications_controller.rb index f09a1d21139..65418f437a4 100644 --- a/app/controllers/api/notifications_controller.rb +++ b/app/controllers/api/notifications_controller.rb @@ -4,16 +4,8 @@ class API::NotificationsController < API::BaseController def index target = params[:target].presence&.to_sym status = params[:status] - latest_notifications = current_user.notifications - .by_target(target) - .by_read_status(status) - .latest_of_each_link - # latest_notifications のクエリで指定している ORDER BY の順序を他と混ぜないようにするため、 - # from を使ってサブクエリとした - @notifications = Notification.with_avatar - .from(latest_notifications, :notifications) - .order(created_at: :desc) + @notifications = UserNotificationsQuery.new(user: current_user, target:, status:).call page = params[:page] return @notifications unless page diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8fef365f37e..53fef684a98 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ApplicationController < ActionController::Base + include ActiveStorage::SetCurrent include Authentication include TestAuthentication if Rails.env.test? include PolicyHelper @@ -11,7 +12,6 @@ class ApplicationController < ActionController::Base before_action :test_login, if: :test? before_action :init_user before_action :allow_cross_domain_access - before_action :set_host_for_disk_storage before_action :require_active_user_login before_action :set_current_user_practice @@ -38,12 +38,6 @@ def set_available_emojis @available_emojis = Reaction.emojis.map { |key, value| { kind: key, value: } } end - def set_host_for_disk_storage - return unless %i[local test].include? Rails.application.config.active_storage.service - - ActiveStorage::Current.host = request.base_url - end - def require_card redirect_to root_path, notice: 'カード登録が必要です。' unless current_user&.card? end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 9be9a0d364d..83fa486e700 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -20,10 +20,6 @@ def index def pricing; end - def test - render :test, layout: false - end - private def set_required_fields diff --git a/app/controllers/stripe/webhooks_controller.rb b/app/controllers/stripe/webhooks_controller.rb index 035fb898e57..f0e271dd81f 100644 --- a/app/controllers/stripe/webhooks_controller.rb +++ b/app/controllers/stripe/webhooks_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Stripe::WebhooksController < ApplicationController - SECRET = Rails.application.secrets['stripe'][:endpoint_secret] + SECRET = Rails.application.config_for(:secrets)['stripe'][:endpoint_secret] ERROR_EVENTS = %w[ payment_intent.payment_failed payment_intent.requires_payment_method diff --git a/app/javascript/card.js b/app/javascript/card.js index b46d02a9bb6..093ee25075c 100644 --- a/app/javascript/card.js +++ b/app/javascript/card.js @@ -43,7 +43,7 @@ document.addEventListener('DOMContentLoaded', () => { } // Create an instance of the card Element. - const card = elements.create('card', { style: style, hidePostalCode: true }) + const card = elements.create('card', { style, hidePostalCode: true }) if (!userRole || checkedCreditCardCheckBox) { card.mount('#card-element') diff --git a/app/javascript/initializeAnswer.js b/app/javascript/initializeAnswer.js index eff9d9908a2..b87a1c607cd 100644 --- a/app/javascript/initializeAnswer.js +++ b/app/javascript/initializeAnswer.js @@ -172,7 +172,7 @@ export default function initializeAnswer(answer) { } const params = { id: answerId, - answer: { description: description } + answer: { description } } fetch(`/api/answers/${answerId}`, { method: 'PUT', diff --git a/app/javascript/initializeComment.js b/app/javascript/initializeComment.js index 68a14f5f6dc..8b73c1f859b 100644 --- a/app/javascript/initializeComment.js +++ b/app/javascript/initializeComment.js @@ -135,7 +135,7 @@ function updateComment(commentId, description) { } const params = { id: commentId, - comment: { description: description } + comment: { description } } fetch(`/api/comments/${commentId}`, { method: 'PUT', diff --git a/app/javascript/markdown-it-headings.js b/app/javascript/markdown-it-headings.js index 59a26abf654..f0391207116 100644 --- a/app/javascript/markdown-it-headings.js +++ b/app/javascript/markdown-it-headings.js @@ -9,5 +9,5 @@ const options = { } export default function (md) { - return plugin(md, options) + return md.use(plugin, options) } diff --git a/app/javascript/micro-reports-edit.js b/app/javascript/micro-reports-edit.js index e3f7caa2a98..86fc3d7a5b7 100644 --- a/app/javascript/micro-reports-edit.js +++ b/app/javascript/micro-reports-edit.js @@ -102,7 +102,7 @@ document.addEventListener('DOMContentLoaded', () => { } const params = { id: microReportId, - micro_report: { content: content } + micro_report: { content } } const url = `/api/micro_reports/${microReportId}` return Bootcamp.patch(url, params).catch((error) => { diff --git a/app/javascript/new-answer.js b/app/javascript/new-answer.js index 22b53199945..6476b7fa54e 100644 --- a/app/javascript/new-answer.js +++ b/app/javascript/new-answer.js @@ -81,7 +81,7 @@ async function createAnswer(description, questionId) { const params = { question_id: questionId, answer: { - description: description + description } } try { diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 7dba229e934..42c89b21ff5 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -86,7 +86,7 @@ import '../tag.js' import '../tag_edit.js' import '../bookmark-button.js' -import '../stylesheets/application' +import '../stylesheets/application.sass' // Support component names relative to this directory: const componentRequireContext = require.context('components', true) diff --git a/app/javascript/packs/lp.js b/app/javascript/packs/lp.js index 82a3a8d860d..7bee7199226 100644 --- a/app/javascript/packs/lp.js +++ b/app/javascript/packs/lp.js @@ -7,7 +7,13 @@ // To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate // layout file, like app/views/layouts/application.html.erb -import '../stylesheets/lp' +import '../stylesheets/lp.sass' // Import images to ensure they are copied by webpack -import '../../assets/images/background/people.png' +import peopleImage from '../../assets/images/background/people.png' + +// Set CSS custom property for the background image +document.documentElement.style.setProperty( + '--people-bg-image', + `url(${peopleImage})` +) diff --git a/app/javascript/packs/not-logged-in.js b/app/javascript/packs/not-logged-in.js index af9383650bb..36e035cc37d 100644 --- a/app/javascript/packs/not-logged-in.js +++ b/app/javascript/packs/not-logged-in.js @@ -7,4 +7,4 @@ // To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate // layout file, like app/views/layouts/application.html.erb -import '../stylesheets/not-logged-in' +import '../stylesheets/not-logged-in.sass' diff --git a/app/javascript/packs/paper.js b/app/javascript/packs/paper.js index b5e9ddf5bfa..84bdaed6405 100644 --- a/app/javascript/packs/paper.js +++ b/app/javascript/packs/paper.js @@ -7,4 +7,4 @@ // To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate // layout file, like app/views/layouts/application.html.erb -import '../stylesheets/paper' +import '../stylesheets/paper.sass' diff --git a/app/javascript/practice_memo.js b/app/javascript/practice_memo.js index fc4e3bf5fab..f4597d7cc30 100644 --- a/app/javascript/practice_memo.js +++ b/app/javascript/practice_memo.js @@ -99,7 +99,7 @@ document.addEventListener('DOMContentLoaded', () => { function updateMemo(memo, practiceId) { const params = { practice: { - memo: memo + memo } } fetch(`/api/practices/${practiceId}`, { diff --git a/app/javascript/reaction.js b/app/javascript/reaction.js index c1558c52cde..903192015e8 100644 --- a/app/javascript/reaction.js +++ b/app/javascript/reaction.js @@ -38,7 +38,7 @@ export function initializeReaction(reaction) { function requestReaction(url, method, callback) { fetch(url, { - method: method, + method, credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', diff --git a/app/javascript/stylesheets/lp/blocks/lp/_lp-top-cover.sass b/app/javascript/stylesheets/lp/blocks/lp/_lp-top-cover.sass index e9932d2386c..682df4abade 100644 --- a/app/javascript/stylesheets/lp/blocks/lp/_lp-top-cover.sass +++ b/app/javascript/stylesheets/lp/blocks/lp/_lp-top-cover.sass @@ -1,5 +1,5 @@ .lp-top-cover - background-image: image-url('background/people.png') + background-image: var(--people-bg-image) background-color: var(--lp-bg-2) background-repeat: repeat position: relative diff --git a/app/javascript/survey_result_chart.js b/app/javascript/survey_result_chart.js index 9d04c9af6ff..7b460282078 100644 --- a/app/javascript/survey_result_chart.js +++ b/app/javascript/survey_result_chart.js @@ -320,7 +320,7 @@ function initLinearScaleCharts() { } }, annotation: { - annotations: annotations + annotations } } } diff --git a/app/javascript/textarea-autocomplte-emoji.js b/app/javascript/textarea-autocomplte-emoji.js index 88b502ed9c9..fece13d1d3c 100644 --- a/app/javascript/textarea-autocomplte-emoji.js +++ b/app/javascript/textarea-autocomplte-emoji.js @@ -50,7 +50,7 @@ export default class { _fetchValues() { this.values = Object.keys(emojis) .map((key) => { - return { key: key, value: emojis[key] } + return { key, value: emojis[key] } }) .concat(this.userValues) } diff --git a/app/javascript/textarea-initializer.js b/app/javascript/textarea-initializer.js index d42348cba10..8faa8ca6f71 100644 --- a/app/javascript/textarea-initializer.js +++ b/app/javascript/textarea-initializer.js @@ -47,7 +47,7 @@ export default class { mention.values.unshift({ login_name: 'mentor', name: 'メンター' }) const collection = [emoji.params(), mention.params()] const tribute = new Tribute({ - collection: collection + collection }) textareas.forEach((textarea) => { diff --git a/app/javascript/vanillaToast.js b/app/javascript/vanillaToast.js index 231f4049368..baed290a1df 100644 --- a/app/javascript/vanillaToast.js +++ b/app/javascript/vanillaToast.js @@ -2,7 +2,7 @@ import Swal from 'sweetalert2' export function toast(title, status = 'success') { Swal.fire({ - title: title, + title, toast: true, position: 'top-end', showConfirmButton: false, diff --git a/app/jobs/ai_answer_create_job.rb b/app/jobs/ai_answer_create_job.rb index 5c0bc6c6a66..5a7bc342dcf 100644 --- a/app/jobs/ai_answer_create_job.rb +++ b/app/jobs/ai_answer_create_job.rb @@ -5,7 +5,7 @@ class AIAnswerCreateJob < ApplicationJob def perform(question_id:) question = Question.find(question_id) - token = Rails.application.secrets[:open_ai][:access_token] + token = Rails.application.config_for(:secrets)[:open_ai][:access_token] generator = AI::AnswerGenerator.new(open_ai_access_token: token) ai_answer = generator.call("#{question.body}\n#{question.description}") question.update(ai_answer:) diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 4d09a880843..ba2c827120d 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -19,6 +19,7 @@ class NotificationMailer < ApplicationMailer @event = params[:event] @page = params[:page] @regular_event = params[:regular_event] + @notification = params[:notification] end # required params: mentionable, receiver @@ -49,7 +50,7 @@ def retired # required params: report, receiver def trainee_report @user = @receiver - @notification = @user.notifications.find_by(link: "/reports/#{@report.id}") + @notification ||= @user.notifications.find_by(link: "/reports/#{@report.id}") subject = "[FBC] #{@report.user.login_name}さんが日報【 #{@report.title} 】を書きました!" mail to: @user.email, subject: end diff --git a/app/models/announcement.rb b/app/models/announcement.rb index 7c5cb41b154..65bfe963979 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -9,11 +9,11 @@ class Announcement < ApplicationRecord include Watchable include Bookmarkable - enum target: { + enum :target, { all: 0, students: 1, job_seekers: 2 - }, _prefix: true + }, prefix: true has_many :watches, as: :watchable, dependent: :destroy has_many :footprints, as: :footprintable, dependent: :destroy @@ -28,6 +28,14 @@ class Announcement < ApplicationRecord scope :wip, -> { where(wip: true) } + def self.ransackable_attributes(_auth_object = nil) + %w[title description target wip created_at updated_at user_id] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user comments reactions watches] + end + def self.copy_announcement(announcement_id) original = find(announcement_id) new(title: original.title, description: original.description, target: original.target) diff --git a/app/models/answer.rb b/app/models/answer.rb index d2cf0ed0c79..fede10e1dfe 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -16,6 +16,14 @@ class Answer < ApplicationRecord mentionable_as :description + def self.ransackable_attributes(_auth_object = nil) + %w[description created_at updated_at user_id question_id] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user question reactions] + end + def receiver question.user end diff --git a/app/models/article.rb b/app/models/article.rb index 47c3e2d4dc0..b83960e3941 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Article < ApplicationRecord - enum thumbnail_type: { + enum :thumbnail_type, { prepared_thumbnail: 0, ruby: 1, ruby_on_rails: 2, @@ -17,12 +17,12 @@ class Article < ApplicationRecord blue: 12 } - enum target: { + enum :target, { all: 0, students: 1, job_seekers: 2, none: 3 - }, _prefix: true + }, prefix: true belongs_to :user alias sender user diff --git a/app/models/coding_test.rb b/app/models/coding_test.rb index 3212e0d1b8a..e11e462dd18 100644 --- a/app/models/coding_test.rb +++ b/app/models/coding_test.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class CodingTest < ApplicationRecord - enum language: { + enum :language, { ruby: 1, javascript: 2 - }, _prefix: true + }, prefix: true belongs_to :practice belongs_to :user diff --git a/app/models/comment.rb b/app/models/comment.rb index 24801f53f64..9e492a4afc5 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -20,6 +20,14 @@ class Comment < ApplicationRecord scope :without_private_comment, -> { where.not(commentable_type: %w[Talk Inquiry CorporateTrainingInquiry]) } + def self.ransackable_attributes(_auth_object = nil) + %w[description commentable_type commentable_id created_at updated_at user_id] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user commentable reactions] + end + class << self def commented_users User.with_attached_avatar diff --git a/app/models/company.rb b/app/models/company.rb index 8b87133d09b..8701f59cfe8 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -19,4 +19,12 @@ def logo_url rescue ActiveStorage::FileNotFoundError image_url('/images/companies/logos/default.png') end + + def self.ransackable_attributes(_auth_object = nil) + %w[name description created_at updated_at] + end + + def self.ransackable_associations(_auth_object = nil) + %w[users] + end end diff --git a/app/models/discord_profile.rb b/app/models/discord_profile.rb index e97cce77dc6..19173a2299d 100644 --- a/app/models/discord_profile.rb +++ b/app/models/discord_profile.rb @@ -3,6 +3,10 @@ class DiscordProfile < ApplicationRecord belongs_to :user + def self.ransackable_attributes(_auth_object = nil) + %w[account_name] + end + validates :times_url, format: { allow_blank: true, diff --git a/app/models/event.rb b/app/models/event.rb index 5de529bf4ba..5c85b294782 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -48,6 +48,14 @@ class Event < ApplicationRecord # rubocop:todo Metrics/ClassLength scope :not_ended, -> { where('end_at > ?', Time.current) } scope :scheduled_on_without_ended, ->(date) { scheduled_on(date).not_ended } + def self.ransackable_attributes(_auth_object = nil) + %w[title description location capacity start_at end_at open_start_at open_end_at wip created_at updated_at user_id job_hunting] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user participations users comments reactions watches] + end + def opening? Time.current.between?(open_start_at, open_end_at) end diff --git a/app/models/graduation_notifier.rb b/app/models/graduation_notifier.rb index 91ff553a82e..42ad4617955 100644 --- a/app/models/graduation_notifier.rb +++ b/app/models/graduation_notifier.rb @@ -9,12 +9,12 @@ def call(_name, _started, _finished, _unique_id, payload) DiscordNotifier.graduated( sender: user, - webhook_url: Rails.application.secrets[:webhook][:admin] + webhook_url: Rails.application.config_for(:secrets)[:webhook][:admin] ).notify_now DiscordNotifier.graduated( sender: user, - webhook_url: Rails.application.secrets[:webhook][:mentor] + webhook_url: Rails.application.config_for(:secrets)[:webhook][:mentor] ).notify_now end end diff --git a/app/models/learning.rb b/app/models/learning.rb index 6f75ba3201c..a15f1e323cb 100644 --- a/app/models/learning.rb +++ b/app/models/learning.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Learning < ApplicationRecord - enum status: { unstarted: 0, started: 1, submitted: 2, complete: 3 } + enum :status, { unstarted: 0, started: 1, submitted: 2, complete: 3 } belongs_to :user, touch: true belongs_to :practice diff --git a/app/models/movie.rb b/app/models/movie.rb index e0e21ff8aab..0d2e94808be 100644 --- a/app/models/movie.rb +++ b/app/models/movie.rb @@ -21,4 +21,12 @@ class Movie < ApplicationRecord scope :wip, -> { where(wip: true) } scope :by_tag, ->(tag) { tag.present? ? tagged_with(tag) : all } + + def self.ransackable_attributes(_auth_object = nil) + %w[title description wip created_at updated_at user_id] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user practices comments reactions watches bookmarks] + end end diff --git a/app/models/notification.rb b/app/models/notification.rb index aec03e5694c..d9e933e4a39 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -15,7 +15,7 @@ class Notification < ApplicationRecord paginates_per 20 - enum kind: { + enum :kind, { came_comment: 0, checked: 1, mentioned: 2, diff --git a/app/models/notification_facade.rb b/app/models/notification_facade.rb index 5a39828fad5..a3107d4f261 100644 --- a/app/models/notification_facade.rb +++ b/app/models/notification_facade.rb @@ -2,13 +2,15 @@ class NotificationFacade def self.trainee_report(report, receiver) - ActivityNotifier.with(report:, receiver:).trainee_report.notify_now + notification = ActivityNotifier.with(report:, receiver:).trainee_report.notify_now return unless receiver.mail_notification? && !receiver.retired? - NotificationMailer.with( - report:, - receiver: - ).trainee_report.deliver_later(wait: 5) + mailer = NotificationMailer.with(report:, receiver:, notification:).trainee_report + if Rails.env.test? + mailer.deliver_now + else + mailer.deliver_later(wait: 5) + end end def self.coming_soon_regular_events(today_events, tomorrow_events) diff --git a/app/models/page.rb b/app/models/page.rb index 14fe492e1f9..c1b12bb5ae9 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -24,6 +24,14 @@ class Page < ApplicationRecord before_validation :empty_slug_to_nil + def self.ransackable_attributes(_auth_object = nil) + %w[title body slug wip created_at updated_at user_id last_updated_user_id practice_id] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user practice last_updated_user comments reactions watches bookmarks] + end + def self.search_by_slug_or_id!(params) attr_name = params.start_with?(/[a-z]/) ? :slug : :id Page.find_by!(attr_name => params) diff --git a/app/models/practice.rb b/app/models/practice.rb index 258483d840b..749c27e3304 100644 --- a/app/models/practice.rb +++ b/app/models/practice.rb @@ -76,6 +76,14 @@ class Practice < ApplicationRecord # rubocop:todo Metrics/ClassLength .order(:id) } + def self.ransackable_attributes(_auth_object = nil) + %w[title description goal created_at updated_at last_updated_user_id submission] + end + + def self.ransackable_associations(_auth_object = nil) + %w[learnings categories products questions pages movies books last_updated_user] + end + class << self def save_learning_minute_statistics Practice.all.find_each do |practice| diff --git a/app/models/product.rb b/app/models/product.rb index 8512b24c529..061349967cd 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -59,6 +59,14 @@ class Product < ApplicationRecord # rubocop:todo Metrics/ClassLength scope :order_for_self_assigned_list, -> { order('commented_at asc nulls first, published_at asc') } scope :unhibernated_user_products, -> { joins(:user).where(user: { hibernated_at: nil }) } + def self.ransackable_attributes(_auth_object = nil) + %w[body wip published_at commented_at created_at updated_at user_id practice_id checker_id] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user practice checker comments reactions checks bookmarks] + end + def self.add_latest_commented_at Product.all.includes(:comments).find_each do |product| next if product.comments.blank? diff --git a/app/models/question.rb b/app/models/question.rb index 61765b76d65..fb55bf053a2 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -49,6 +49,14 @@ class Question < ApplicationRecord mentionable_as :description + def self.ransackable_attributes(_auth_object = nil) + %w[title description wip published_at created_at updated_at user_id practice_id] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user practice correct_answer answers reactions watches bookmarks] + end + class << self def notify_certain_period_passed_after_last_answer return if Question.not_solved_and_certain_period_has_passed.blank? diff --git a/app/models/reaction.rb b/app/models/reaction.rb index 636a41733ca..41cf17f2144 100644 --- a/app/models/reaction.rb +++ b/app/models/reaction.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Reaction < ApplicationRecord - enum kind: { + enum :kind, { thumbsup: 0, thumbsdown: 1, smile: 2, diff --git a/app/models/regular_event.rb b/app/models/regular_event.rb index 66a81fcb9c7..1b72ee7841b 100644 --- a/app/models/regular_event.rb +++ b/app/models/regular_event.rb @@ -30,13 +30,13 @@ class RegularEvent < ApplicationRecord # rubocop:disable Metrics/ClassLength include Watchable include Searchable - enum category: { + enum :category, { reading_circle: 0, chat: 1, question: 2, meeting: 3, others: 4 - }, _prefix: true + }, prefix: true validates :title, presence: true, markdown_prohibited: true validates :user_ids, presence: true @@ -56,7 +56,7 @@ class RegularEvent < ApplicationRecord # rubocop:disable Metrics/ClassLength scope :holding, -> { where(finished: false) } scope :participated_by, ->(user) { where(id: all.filter { |e| e.participated_by?(user) }.map(&:id)) } - scope :organizer_event, ->(user) { where(id: user.organizers.map(&:regular_event_id)) } + scope :organizer_event, ->(user) { joins(:organizers).where(organizers: { user_id: user.id }) } scope :scheduled_on, ->(date) { holding.filter { |event| event.scheduled_on?(date) } } scope :scheduled_on_without_ended, ->(date) { holding.filter { |event| event.scheduled_on?(date) && !event.ended?(date) } } @@ -75,6 +75,14 @@ class RegularEvent < ApplicationRecord # rubocop:disable Metrics/ClassLength columns_for_keyword_search :title, :description + def self.ransackable_attributes(_auth_object = nil) + %w[title description category start_at end_at finished hold_national_holiday created_at updated_at user_id] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user organizers users regular_event_repeat_rules participants comments reactions watches] + end + def scheduled_on?(date) all_scheduled_dates.include?(date) end @@ -92,7 +100,7 @@ def next_event_date end def organizers - users.with_attached_avatar.order('organizers.created_at') + users.preload(avatar_attachment: :blob).order('organizers.created_at') end def cancel_participation(user) diff --git a/app/models/report.rb b/app/models/report.rb index 327b2343c84..4e7c2f0a2a2 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -12,7 +12,7 @@ class Report < ApplicationRecord # rubocop:todo Metrics/ClassLength include Bookmarkable include Taskable - enum emotion: { + enum :emotion, { negative: 1, neutral: 0, positive: 2 @@ -61,6 +61,14 @@ class Report < ApplicationRecord # rubocop:todo Metrics/ClassLength scope :user, ->(user) { where(user_id: user.id) } + def self.ransackable_attributes(_auth_object = nil) + %w[title description reported_on emotion wip created_at updated_at user_id] + end + + def self.ransackable_associations(_auth_object = nil) + %w[user practices comments checks reactions bookmarks] + end + class << self def faces @faces ||= emotions.keys diff --git a/app/models/search_user.rb b/app/models/search_user.rb index e3dbd165c07..92e72dca1de 100644 --- a/app/models/search_user.rb +++ b/app/models/search_user.rb @@ -10,8 +10,8 @@ def initialize(word:, users: nil, target: nil, require_retire_user: false) def search validated_search_word = validate_search_word - # 検索ワードが短すぎる場合はユーザー一覧をそのまま返す - return @users || User.all if validated_search_word.nil? + # 検索ワードが無効な場合は空の結果を返す + return User.none if validated_search_word.nil? # Searcherを使ってユーザーを検索 query_builder = Searcher::QueryBuilder.new(validated_search_word) @@ -34,7 +34,7 @@ def search end def validate_search_word - return '' if @word.nil? + return nil if @word.nil? stripped_word = @word.strip return nil if stripped_word.blank? diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 7beb89b9b25..58d8506f6e1 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -17,7 +17,7 @@ def create(customer_id, idempotency_key = SecureRandom.uuid, trial: 3) customer: customer_id, items: [{ plan: Plan.standard_plan.id, - tax_rates: [Rails.application.secrets[:stripe][:tax_rate_id]] + tax_rates: [Rails.application.config_for(:secrets)[:stripe][:tax_rate_id]] }] } options[:trial_end] = trial.days.since.to_i if trial.positive? diff --git a/app/models/survey_question.rb b/app/models/survey_question.rb index cb876c6cf4b..40a300a045d 100644 --- a/app/models/survey_question.rb +++ b/app/models/survey_question.rb @@ -15,13 +15,13 @@ class SurveyQuestion < ApplicationRecord has_many :surveys, through: :survey_question_listings has_many :survey_question_answers, dependent: :destroy - enum format: { + enum :format, { text_area: 0, text_field: 1, radio_button: 2, check_box: 3, linear_scale: 4 - }, _prefix: true + }, prefix: true validates :title, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index b150f0c4051..4f8e9690dec 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,7 +8,7 @@ class User < ApplicationRecord # rubocop:todo Metrics/ClassLength attr_accessor :credit_card_payment, :role, :uploaded_avatar authenticates_with_sorcery! - VALID_SORT_COLUMNS = %w[id login_name company_id last_activity_at created_at report comment asc desc].freeze + VALID_SORT_COLUMNS = %w[login_name company_id last_activity_at created_at report comment asc desc].freeze AVATAR_SIZE = [120, 120].freeze AVATAR_FORMAT = 'webp' DEFAULT_IMAGE_PATH = '/images/users/avatars/default.png' @@ -38,38 +38,38 @@ class User < ApplicationRecord # rubocop:todo Metrics/ClassLength [I18n.t('invitation_role.mentor'), :mentor] ].freeze - enum job: { + enum :job, { student: 0, office_worker: 2, part_time_worker: 3, vacation: 4, unemployed: 5 - }, _prefix: true + }, prefix: true - enum os: { + enum :os, { mac: 0, mac_apple: 2, linux: 1, windows_wsl2: 3 - }, _prefix: true + }, prefix: true - enum editor: { + enum :editor, { vscode: 0, ruby_mine: 1, vim: 2, emacs: 3, other_editor: 99 - }, _prefix: true + }, prefix: true - enum satisfaction: { + enum :satisfaction, { excellent: 0, good: 1, average: 2, poor: 3, very_poor: 4 - }, _prefix: true + }, prefix: true - enum referral_source: { + enum :referral_source, { search_engine: 0, referral: 1, event: 2, @@ -78,9 +78,9 @@ class User < ApplicationRecord # rubocop:todo Metrics/ClassLength blog: 5, web_ad: 6, other: 99 - }, _prefix: true + }, prefix: true - enum career_path: { + enum :career_path, { unset: 0, job_seeking: 1, employed_via_referral: 2, @@ -88,7 +88,7 @@ class User < ApplicationRecord # rubocop:todo Metrics/ClassLength employed_non_it: 4, internal_transfer_to_programmer: 5, not_employed: 6 - }, _prefix: true + }, prefix: true belongs_to :company, optional: true belongs_to :course @@ -709,7 +709,7 @@ def avatar_url else image_url DEFAULT_IMAGE_PATH end - rescue ActiveStorage::FileNotFoundError, ActiveStorage::InvariableError => e + rescue ActiveStorage::FileNotFoundError, ActiveStorage::Error => e log_avatar_error('avatar_url', e) image_url DEFAULT_IMAGE_PATH end @@ -720,7 +720,7 @@ def profile_image_url else image_url DEFAULT_IMAGE_PATH end - rescue ActiveStorage::FileNotFoundError, ActiveStorage::InvariableError => e + rescue ActiveStorage::FileNotFoundError, ActiveStorage::Error => e log_avatar_error('profile_image_url', e) image_url DEFAULT_IMAGE_PATH end @@ -926,6 +926,25 @@ def search_title login_name end + def self.ransackable_attributes(_auth_object = nil) + %w[ + login_name name name_kana email twitter_account facebook_url + blog_url github_account description profile_text + created_at updated_at last_activity_at + company_id course_id graduated_on retired_on + admin mentor adviser trainee job_seeker hibernated_at + experiences career_path job os editor subdivision_code country_code + ] + end + + def self.ransackable_scopes(_auth_object = nil) + %i[job_seeking] + end + + def self.ransackable_associations(_auth_object = nil) + %w[company course discord_profile] + end + private def password_required? @@ -958,18 +977,21 @@ def attach_custom_avatar custom_key = "avatars/#{login_name}.#{AVATAR_FORMAT}" variant_avatar = avatar.variant(resize_to_fill: AVATAR_SIZE, autorot: true, saver: { strip: true, quality: 60 }, format: AVATAR_FORMAT).processed io = StringIO.new(variant_avatar.download) - custom_blob = ActiveStorage::Blob.create_or_find_by!(key: custom_key) do |blob| - blob.filename = "#{login_name}.#{AVATAR_FORMAT}" - blob.content_type = "image/#{AVATAR_FORMAT}" - blob.byte_size = io.size - blob.checksum = Digest::MD5.base64digest(io.read) - io.rewind - end - return if custom_blob.id_previously_was.present? - custom_blob.upload(io, identify: false) - avatar.attach(custom_blob) - rescue ActiveStorage::FileNotFoundError, ActiveStorage::InvariableError, Vips::Error => e + # Use ActiveStorage's create_and_upload! for proper checksum handling + custom_blob = ActiveStorage::Blob.find_by(key: custom_key) + + unless custom_blob + custom_blob = ActiveStorage::Blob.create_and_upload!( + io:, + filename: "#{login_name}.#{AVATAR_FORMAT}", + content_type: "image/#{AVATAR_FORMAT}", + key: custom_key, + identify: false + ) + avatar.attach(custom_blob) + end + rescue ActiveStorage::FileNotFoundError, ActiveStorage::Error => e log_avatar_error('attach_custom_avatar', e) end diff --git a/app/models/webhook.rb b/app/models/webhook.rb index c212c487044..b05f8d9237a 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -2,7 +2,7 @@ class Webhook class << self - SECREDT = Rails.application.secrets['stripe'][:endpoint_secret] + SECREDT = Rails.application.config_for(:secrets)['stripe'][:endpoint_secret] def construct_event( payload:, diff --git a/app/notifiers/discord_notifier.rb b/app/notifiers/discord_notifier.rb index a700de58f07..4c3cd1d9a3a 100644 --- a/app/notifiers/discord_notifier.rb +++ b/app/notifiers/discord_notifier.rb @@ -16,7 +16,7 @@ def graduated(params = {}) def hibernated(params = {}) params.merge!(@params) - webhook_url = params[:webhook_url] || Rails.application.secrets[:webhook][:admin] + webhook_url = params[:webhook_url] || Rails.application.config_for(:secrets)[:webhook][:admin] notification( body: "#{params[:sender].login_name}さんが休会しました。", @@ -27,7 +27,7 @@ def hibernated(params = {}) def announced(params = {}) params.merge!(@params) - webhook_url = params[:webhook_url] || Rails.application.secrets[:webhook][:all] + webhook_url = params[:webhook_url] || Rails.application.config_for(:secrets)[:webhook][:all] path = Rails.application.routes.url_helpers.polymorphic_path(params[:announce]) url = "https://bootcamp.fjord.jp#{path}" @@ -41,7 +41,7 @@ def announced(params = {}) def coming_soon_regular_events(params = {}) params.merge!(@params) - webhook_url = params[:webhook_url] || Rails.application.secrets[:webhook][:all] + webhook_url = params[:webhook_url] || Rails.application.config_for(:secrets)[:webhook][:all] today_events = params[:today_events].sort_by { |event| event.start_at.strftime('%H%M') } tomorrow_events = params[:tomorrow_events].sort_by { |event| event.start_at.strftime('%H%M') } today = Time.current @@ -85,7 +85,7 @@ def add_event_info(events, date_message, date) def invalid_user(params = {}) params.merge!(@params) - webhook_url = params[:webhook_url] || Rails.application.secrets[:webhook][:admin] + webhook_url = params[:webhook_url] || Rails.application.config_for(:secrets)[:webhook][:admin] body = params[:body].slice(0, 2000) # Discord API restriction notification( @@ -97,7 +97,7 @@ def invalid_user(params = {}) def payment_failed(params = {}) params.merge!(@params) - webhook_url = params[:webhook_url] || Rails.application.secrets[:webhook][:admin] + webhook_url = params[:webhook_url] || Rails.application.config_for(:secrets)[:webhook][:admin] notification( body: params[:body], @@ -108,7 +108,7 @@ def payment_failed(params = {}) def product_review_not_completed(params = {}) params.merge!(@params) - webhook_url = params[:webhook_url] || Rails.application.secrets[:webhook][:mentor] + webhook_url = params[:webhook_url] || Rails.application.config_for(:secrets)[:webhook][:mentor] comment = params[:comment] product_checker_name = comment.commentable.checker.discord_profile&.account_name @@ -131,7 +131,7 @@ def product_review_not_completed(params = {}) def first_report(params = {}) params.merge!(@params) - webhook_url = params[:webhook_url] || Rails.application.secrets[:webhook][:introduction] + webhook_url = params[:webhook_url] || Rails.application.config_for(:secrets)[:webhook][:introduction] report = params[:report] body = <<~TEXT.chomp 🎉 #{report.user.login_name}さんがはじめての日報を書きました! diff --git a/app/queries/user_notifications_query.rb b/app/queries/user_notifications_query.rb new file mode 100644 index 00000000000..6405236deb4 --- /dev/null +++ b/app/queries/user_notifications_query.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class UserNotificationsQuery < Patterns::Query + queries Notification + + private + + def initialize(relation = Notification.all, user:, target: nil, status: nil) + super(relation) + @user = user + @target = target + @status = status + end + + def query + latest_notifications = @user.notifications + .by_target(@target) + .by_read_status(@status) + .latest_of_each_link + + Notification.with_avatar + .from(latest_notifications, :notifications) + .order(created_at: :desc) + end +end diff --git a/app/views/admin/home/test.html.erb b/app/views/admin/home/test.html.erb deleted file mode 100644 index ddaf53f19da..00000000000 --- a/app/views/admin/home/test.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -

Test

-
diff --git a/app/views/application/_stripe.html.erb b/app/views/application/_stripe.html.erb index da3c2604ccb..831fc3d9bb9 100644 --- a/app/views/application/_stripe.html.erb +++ b/app/views/application/_stripe.html.erb @@ -1,3 +1,3 @@ diff --git a/app/views/books/_book.html.slim b/app/views/books/_book.html.slim index c30221ef3e4..79134698b8f 100644 --- a/app/views/books/_book.html.slim +++ b/app/views/books/_book.html.slim @@ -26,7 +26,7 @@ | #{book.title} .card-books-item__row p.card-books-item__price - | #{book.price.to_s(:delimited)}円(税込) + | #{number_with_delimiter(book.price)}円(税込) - if book.description .card-books-item__row .card-books-item__description diff --git a/app/views/courses/books/index.html.slim b/app/views/courses/books/index.html.slim index b2409aefa9d..540a0aef27d 100644 --- a/app/views/courses/books/index.html.slim +++ b/app/views/courses/books/index.html.slim @@ -55,7 +55,7 @@ header.page-header | #{book.title} .card-books-item__row p.card-books-item__price - | #{book.price.to_s(:delimited)}円(税込) + | #{number_with_delimiter(book.price)}円(税込) - if book.description.present? .card-books-item__row .card-books-item__description diff --git a/app/views/home/test.html.slim b/app/views/home/test.html.slim deleted file mode 100644 index 22a9bfbd705..00000000000 --- a/app/views/home/test.html.slim +++ /dev/null @@ -1 +0,0 @@ -h1 TEST diff --git a/app/views/users/_form.html.slim b/app/views/users/_form.html.slim index 5139a144582..0d05c870533 100644 --- a/app/views/users/_form.html.slim +++ b/app/views/users/_form.html.slim @@ -112,7 +112,7 @@ = f.hidden_field :remove_diploma, value: '0', id: 'js-remove-pdf-flag' = f.label :diploma_file, class: 'a-form-label' .a-pdf-input - - if @user.diploma_file.attached? + - if @user.diploma_file.attached? && @user.diploma_file.persisted? = link_to url_for(@user.diploma_file), class: 'a-pdf-input__inner', id: 'js-pdf-file-link', target: '_blank', rel: 'noopener' do .a-pdf-input__file span.a-pdf-input__file-name diff --git a/app/views/works/_form.html.slim b/app/views/works/_form.html.slim index 41d476691de..02089ceddd5 100644 --- a/app/views/works/_form.html.slim +++ b/app/views/works/_form.html.slim @@ -24,7 +24,7 @@ = f.label :thumbnail, class: 'a-form-label' .form-item-file-input.js-file-input.a-file-input.is-thumbnail label.js-file-input__preview - - if work.thumbnail.attached? + - if work.persisted? && work.thumbnail.attached? = image_tag work.thumbnail p 画像を変更 - else diff --git a/bin/rails b/bin/rails index 21d3e02d896..efc0377492f 100755 --- a/bin/rails +++ b/bin/rails @@ -1,5 +1,4 @@ #!/usr/bin/env ruby -load File.expand_path("spring", __dir__) -APP_PATH = File.expand_path('../config/application', __dir__) +APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" diff --git a/bin/rake b/bin/rake index 7327f471e4e..4fbf10b960e 100755 --- a/bin/rake +++ b/bin/rake @@ -1,5 +1,4 @@ #!/usr/bin/env ruby -load File.expand_path("spring", __dir__) require_relative "../config/boot" require "rake" Rake.application.run diff --git a/bin/rubocop b/bin/rubocop index 369a05bedb5..40330c0ff1c 100755 --- a/bin/rubocop +++ b/bin/rubocop @@ -1,27 +1,8 @@ #!/usr/bin/env ruby -# frozen_string_literal: true - -# -# This file was generated by Bundler. -# -# The application 'rubocop' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) - -bundle_binstub = File.expand_path("bundle", __dir__) - -if File.file?(bundle_binstub) - if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") - load(bundle_binstub) - else - abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. -Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") - end -end - require "rubygems" require "bundler/setup" +# explicit rubocop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup index 90700ac4f9a..62de954afbe 100755 --- a/bin/setup +++ b/bin/setup @@ -1,11 +1,11 @@ #!/usr/bin/env ruby require "fileutils" -# path to your application root. -APP_ROOT = File.expand_path('..', __dir__) +APP_ROOT = File.expand_path("..", __dir__) +APP_NAME = "bootcamp" def system!(*args) - system(*args) || abort("\n== Command #{args} failed ==") + system(*args, exception: true) end FileUtils.chdir APP_ROOT do @@ -13,24 +13,37 @@ FileUtils.chdir APP_ROOT do # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') || system!('bundle install') + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") # Install JavaScript dependencies - system! 'bin/yarn' + puts "\n== Installing JavaScript dependencies ==" + if File.exist?("bin/yarn") && File.executable?("bin/yarn") + system!("bin/yarn install") + elsif system("yarn --version", out: File::NULL, err: File::NULL) + system!("yarn install") + else + puts "ERROR: yarn is not available. Please install yarn to continue." + puts "Visit https://yarnpkg.com/getting-started/install for installation instructions." + exit 1 + end # puts "\n== Copying sample files ==" - # unless File.exist?('config/database.yml') - # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" - system! 'bin/rails db:prepare' + system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" - system! 'bin/rails log:clear tmp:clear' + system! "bin/rails log:clear tmp:clear" puts "\n== Restarting application server ==" - system! 'bin/rails restart' + system! "bin/rails restart" + + # puts "\n== Configuring puma-dev ==" + # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" + # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" end diff --git a/bin/shakapacker b/bin/shakapacker new file mode 100755 index 00000000000..13a008dcfe1 --- /dev/null +++ b/bin/shakapacker @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= "development" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) + +require "bundler/setup" +require "shakapacker" +require "shakapacker/webpack_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Shakapacker::WebpackRunner.run(ARGV) +end diff --git a/bin/shakapacker-dev-server b/bin/shakapacker-dev-server new file mode 100755 index 00000000000..5ae8897989d --- /dev/null +++ b/bin/shakapacker-dev-server @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= "development" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) + +require "bundler/setup" +require "shakapacker" +require "shakapacker/dev_server_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Shakapacker::DevServerRunner.run(ARGV) +end diff --git a/bin/spring b/bin/spring deleted file mode 100755 index b4147e84378..00000000000 --- a/bin/spring +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env ruby -if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) - gem "bundler" - require "bundler" - - # Load Spring without loading other gems in the Gemfile, for speed. - Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring| - Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path - gem "spring", spring.version - require "spring/binstub" - rescue Gem::LoadError - # Ignore when Spring is not installed. - end -end diff --git a/bin/webpack b/bin/webpack deleted file mode 100755 index 1031168d012..00000000000 --- a/bin/webpack +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env ruby - -ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" -ENV["NODE_ENV"] ||= "development" - -require "pathname" -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -require "bundler/setup" - -require "webpacker" -require "webpacker/webpack_runner" - -APP_ROOT = File.expand_path("..", __dir__) -Dir.chdir(APP_ROOT) do - Webpacker::WebpackRunner.run(ARGV) -end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server deleted file mode 100755 index dd9662737a6..00000000000 --- a/bin/webpack-dev-server +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env ruby - -ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" -ENV["NODE_ENV"] ||= "development" - -require "pathname" -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -require "bundler/setup" - -require "webpacker" -require "webpacker/dev_server_runner" - -APP_ROOT = File.expand_path("..", __dir__) -Dir.chdir(APP_ROOT) do - Webpacker::DevServerRunner.run(ARGV) -end diff --git a/bin/yarn b/bin/yarn index 9fab2c35079..fe7338622b8 100755 --- a/bin/yarn +++ b/bin/yarn @@ -1,9 +1,10 @@ #!/usr/bin/env ruby -APP_ROOT = File.expand_path('..', __dir__) + +APP_ROOT = File.expand_path("..", __dir__) Dir.chdir(APP_ROOT) do yarn = ENV["PATH"].split(File::PATH_SEPARATOR). select { |dir| File.expand_path(dir) != __dir__ }. - product(["yarn", "yarn.cmd", "yarn.ps1"]). + product(["yarn", "yarnpkg", "yarn.cmd", "yarn.ps1"]). map { |dir, file| File.expand_path(file, dir) }. find { |file| File.executable?(file) } diff --git a/config/application.rb b/config/application.rb index c07a4a1c333..e221a2f2523 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,7 +9,12 @@ module Bootcamp class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.1 + config.load_defaults 7.2 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) # Configuration for the application, engines, and railties goes here. # @@ -21,14 +26,12 @@ class Application < Rails::Application config.time_zone = "Tokyo" config.i18n.default_locale = :ja - config.paths.add "lib", eager_load: true config.paths.add "app/presenters", eager_load: true config.action_view.field_error_proc = Proc.new do |html_tag, instance| html_tag.html_safe end - config.active_storage.resolve_model_to_route = :rails_storage_proxy config.active_storage.variant_processor = :vips config.view_component.capture_compatibility_patch_enabled = true diff --git a/config/boot.rb b/config/boot.rb index 3cda23b4db4..988a5ddc460 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,4 @@ -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/environments/development.rb b/config/environments/development.rb index ef73762e3af..facbfecb613 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -6,7 +6,7 @@ # In the development environment your application's code is reloaded any time # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. - config.cache_classes = false + config.enable_reloading = true # Do not eager load code on boot. config.eager_load = false @@ -14,16 +14,17 @@ # Show full error reports. config.consider_all_requests_local = true + # Enable server timing. + config.server_timing = true + # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - if Rails.root.join('tmp', 'caching-dev.txt').exist? + if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store, { size: 128.megabytes } - config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.to_i}" - } + config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false @@ -37,8 +38,15 @@ # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. config.action_mailer.perform_caching = false + config.action_mailer.delivery_method = :letter_opener_web + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + config.action_controller.asset_host = "http://localhost:3035" + config.action_mailer.asset_host = "http://localhost:3035" + # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log @@ -54,6 +62,9 @@ # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. @@ -66,23 +77,20 @@ # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true - - # Use an evented file watcher to asynchronously detect changes in source code, - # routes, locales, etc. This feature depends on the listen gem. - config.file_watcher = ActiveSupport::EventedFileUpdateChecker + config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! + config.active_job.queue_adapter = :good_job config.good_job.execution_mode = :async - config.action_mailer.delivery_method = :letter_opener_web - config.action_mailer.default_url_options = { host: "localhost", port: 3000 } - config.action_controller.asset_host = "http://localhost:3000" - config.action_mailer.asset_host = "http://localhost:3000" - config.rack_dev_mark.enable = true config.rack_dev_mark.theme = [:title, Rack::DevMark::Theme::GithubForkRibbon.new(position: 'right-bottom')] diff --git a/config/environments/production.rb b/config/environments/production.rb index 5a02e1dcbf0..d2f87be8610 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -4,7 +4,7 @@ # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. - config.cache_classes = true + config.enable_reloading = false # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers @@ -13,29 +13,26 @@ 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"] - # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? - # Compress CSS using a preprocessor. - # config.assets.css_compressor = :sass - # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.asset_host = 'http://assets.example.com' + # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :google @@ -43,19 +40,32 @@ # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil - # config.action_cable.url = 'wss://example.com/cable' - # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # config.assume_ssl = true # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true - # Include generic and useful information about system operation, but avoid logging too much - # information to avoid inadvertent exposure of personally identifiable information (PII). - config.log_level = :info + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new(STDOUT) + .tap { |logger| logger.formatter = ::Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + # Use a different cache store in production. # config.cache_store = :mem_cache_store config.cache_store = :memory_store, { size: 128.megabytes } @@ -65,6 +75,8 @@ config.good_job.execution_mode = :async # config.active_job.queue_name_prefix = "bootcamp_production" + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. config.action_mailer.perform_caching = true # Ignore bad email addresses and do not raise email delivery errors. @@ -75,69 +87,43 @@ # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true - # Send deprecation notices to registered listeners. - config.active_support.deprecation = :notify - - # Log disallowed deprecations. - config.active_support.disallowed_deprecation = :log - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - - # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new - - # Use a different logger for distributed setups. - # require "syslog/logger" - # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') - - if ENV["RAILS_LOG_TO_STDOUT"].present? - logger = ActiveSupport::Logger.new(STDOUT) - logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) - end + # Don't log any deprecations. + config.active_support.report_deprecations = false # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false - # Inserts middleware to perform automatic connection switching. - # The `database_selector` hash is used to pass options to the DatabaseSelector - # middleware. The `delay` is used to determine how long to wait after a write - # to send a subsequent read to the primary. - # - # The `database_resolver` class is used by the middleware to determine which - # database is appropriate to use based on the time delay. - # - # The `database_resolver_context` class is used by the middleware to set - # timestamps for the last write to the primary. The resolver uses the context - # class timestamps to determine how long to wait before reading from the - # replica. - # - # By default Rails will store a last write timestamp in the session. The - # DatabaseSelector middleware is designed as such you can define your own - # strategy for connection switching and pass that into the middleware through - # these configuration options. - # config.active_record.database_selector = { delay: 2.seconds } - # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver - # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session - - config.action_mailer.default_url_options = { host: ENV["APP_HOST_NAME"], protocol: "https" } - config.action_mailer.asset_host = "https://#{ENV["APP_HOST_NAME"]}" - config.action_controller.asset_host = "https://#{ENV["APP_HOST_NAME"]}" - - # Set asset host for webpacker - config.webpacker.check_yarn_integrity = false if respond_to?(:webpacker) + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } + + # Validate APP_HOST_NAME at boot time + app_host_name = ENV["APP_HOST_NAME"] + if app_host_name.nil? || app_host_name.strip.empty? + abort "ERROR: APP_HOST_NAME environment variable is required for production but not set or blank" + end + + config.action_mailer.default_url_options = { host: app_host_name, protocol: "https" } + config.action_mailer.asset_host = "https://#{app_host_name}" + config.action_controller.asset_host = "https://#{app_host_name}" config.action_mailer.delivery_method = :postmark config.action_mailer.postmark_settings = { api_token: ENV["POSTMARK_API_TOKEN"] } config.hosts << ENV["CLOUD_RUN_HOST_NAME"] if ENV["CLOUD_RUN_HOST_NAME"] - config.hosts << ENV["APP_HOST_NAME"] if ENV["APP_HOST_NAME"] + config.hosts << app_host_name AnyLogin.setup do |config| config.enabled = false end - Rails.application.routes.default_url_options[:host] = ENV["APP_HOST_NAME"] + Rails.application.routes.default_url_options[:host] = app_host_name Rails.application.routes.default_url_options[:protocol] = 'https' end diff --git a/config/environments/test.rb b/config/environments/test.rb index 1e1fef8bcaa..5c387d38c0a 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -8,7 +8,8 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - config.cache_classes = false + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false config.action_view.cache_template_loading = true # Do not eager load code on boot. This avoids loading your whole application @@ -18,12 +19,10 @@ # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true - config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.to_i}" - } + config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. - config.consider_all_requests_local = true + config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store @@ -35,7 +34,10 @@ # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test + config.active_storage.url_options = { protocol: 'http', host: 'localhost', port: '3000' } + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. @@ -43,6 +45,8 @@ # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + # Unlike controllers, the mailer instance doesn't have any context about the + # incoming request so you'll need to provide the :host parameter yourself. config.action_mailer.default_url_options = { host: "localhost", port: 3000 } # Print deprecation notices to the stderr. @@ -59,5 +63,12 @@ # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Use inline adapter for Active Job in system tests + config.active_job.queue_adapter = :inline + Rails.application.routes.default_url_options[:host] = 'localhost:3000' end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 35d0f26fcdc..b3076b38fe1 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -1,30 +1,25 @@ # Be sure to restart your server when you modify this file. -# Define an application-wide content security policy -# For further information see the following documentation -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header -# Rails.application.config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https -# # If you are using webpack-dev-server then specify webpack-dev-server host -# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? - -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true # end - -# If you are using UJS then enable automatic nonce generation -# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } - -# Set the nonce only to specific directives -# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) - -# Report CSP violations to a specified URI -# For further information see the following documentation: -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only -# Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 4dea6dde6bd..9b30991b622 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,6 +1,8 @@ # Be sure to restart your server when you modify this file. -# Configure sensitive parameters which will be filtered from the log file. +# Configure parameters to be partially matched (e.g. passw matches password) and 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, :'g-recaptcha' + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, 'g-recaptcha' ] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index c741ac2b097..f149fc7593a 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -4,9 +4,9 @@ # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb new file mode 100644 index 00000000000..b13ef5ed163 --- /dev/null +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -0,0 +1,143 @@ +# Be sure to restart your server when you modify this file. +# +# This file eases your Rails 7.0 framework defaults upgrade. +# +# Uncomment each configuration one by one to switch to the new default. +# Once your application is ready to run with all new defaults, you can remove +# this file and set the `config.load_defaults` to `7.0`. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. +# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html + +# `button_to` view helper will render `