diff --git a/.circleci/config.yml b/.circleci/config.yml
index 230702c40e8..fb973053171 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -125,7 +125,7 @@ jobs:
- run:
name: Test
command: |
- circleci tests glob "test/**/*_test.rb" | timeout 20m circleci tests run --command="xargs bundle exec rails test" --verbose --split-by=timings --timings-type=filename
+ circleci tests glob "test/**/*_test.rb" | grep -v '^test/visual/' | timeout 20m circleci tests run --command="xargs bundle exec rails test" --verbose --split-by=timings --timings-type=filename
- store_test_results:
path: test/reports
- store_artifacts:
diff --git a/.cloudbuild/cloudbuild-task.yaml b/.cloudbuild/cloudbuild-task.yaml
index 72c39e4e29a..4b4aa2f3353 100644
--- a/.cloudbuild/cloudbuild-task.yaml
+++ b/.cloudbuild/cloudbuild-task.yaml
@@ -25,6 +25,15 @@ steps:
- id: Task
name: asia.gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA
automapSubstitutions: true
+ env:
+ - RAILS_ENV=production
+ - APP_HOST_NAME=bootcamp-staging.fjord.jp
+ - SECRET_KEY_BASE=dummy
+ - DB_HOST=/cloudsql/$_CLOUD_SQL_HOST
+ - DB_NAME=$_DB_NAME
+ - DB_USER=$_DB_USER
+ - DB_PASS=$_DB_PASS
+ - OPEN_AI_ACCESS_TOKEN=$_OPEN_AI_ACCESS_TOKEN
waitFor:
- Build
volumes:
@@ -34,7 +43,6 @@ steps:
- bash
- "-c"
- |-
- eval "$(.cloudbuild/cloud-build-env exports)"
bin/rails bootcamp:oneshot:cloudbuild
- id: Kill_SqlProxy
name: gcr.io/cloud-builders/docker
@@ -50,7 +58,7 @@ options:
substitutionOption: ALLOW_LOOSE
automapSubstitutions: true
substitutions:
- _APP_HOST_NAME: _
+ _APP_HOST_NAME: bootcamp-staging.fjord.jp
_CLOUD_SQL_HOST: _
_DB_NAME: _
_DB_PASS: _
diff --git a/.cloudbuild/deploy-cloud-run b/.cloudbuild/deploy-cloud-run
index 0661c5be67f..62233cd65e7 100755
--- a/.cloudbuild/deploy-cloud-run
+++ b/.cloudbuild/deploy-cloud-run
@@ -48,12 +48,12 @@ fi
case "$environment" in
production)
export _RAILS_MAX_THREADS=5
- export _PJORD_LLM_MODEL=claude-sonnet-4-6
+ export _PJORD_LLM_MODEL=claude-sonnet-5
cloud_run_args+=(--concurrency 5)
;;
staging)
export _MISSION_CONTROL_USERNAME="${_MISSION_CONTROL_USERNAME:-fjord}"
- export _PJORD_LLM_MODEL=claude-sonnet-4-6
+ export _PJORD_LLM_MODEL=claude-sonnet-5
;;
review)
export _DISCORD_NOTICE_WEBHOOK_URL=
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 829c7d951f8..55883800ba2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -222,10 +222,10 @@ jobs:
uses: actions/checkout@v4
- name: Generate flakewatch HTML
- # komagata/flakewatch v0.6.31
- uses: komagata/flakewatch@e734ba749bb1f8de2b9ae8a35b2cb02d63bbbf76
+ # komagata/flakewatch v0.6.32
+ uses: komagata/flakewatch@v0.6.32
with:
junit-artifact-pattern: junit-xml-*
source-root: ${{ github.workspace }}
history-branch: flakewatch-data
- history-write: auto
+ history-write: false
diff --git a/.gitignore b/.gitignore
index dd79f1bb921..8f3cd3e6fe0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,12 @@
vendor/bundle/
+# Visual regression: commit baseline PNGs, ignore transient diff/latest output.
+test/visual/screenshots/**/*.diff.png
+test/visual/screenshots/**/*.latest.png
+test/visual/screenshots/**/*.committed.png
+test/visual/screenshots/**/.attempts/
+
/node_modules
/yarn-error.log
diff --git a/Dockerfile b/Dockerfile
index 029636e6977..b93b0499a3e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -60,6 +60,7 @@ RUN apt-get update -qq && apt-get install -y --no-install-recommends \
python3 \
git \
ca-certificates \
+ ffmpeg \
libvips && \
rm -rf /var/lib/apt/lists/*
diff --git a/Dockerfile.dev b/Dockerfile.dev
index 267179af413..bc23cc884dc 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -18,6 +18,7 @@ RUN apt-get update -qq && apt-get install -y --no-install-recommends \
libvips-dev \
postgresql-client \
tzdata \
+ ffmpeg \
vim && \
rm -rf /var/lib/apt/lists/*
diff --git a/Gemfile b/Gemfile
index b32f32705ca..d896757f569 100644
--- a/Gemfile
+++ b/Gemfile
@@ -119,6 +119,7 @@ end
group :test do
gem 'capybara'
gem 'capybara-playwright-driver', '~> 0.5.9'
+ gem 'capybara-screenshot-diff', require: false
gem 'minitest', '< 6.0'
gem 'minitest-ci'
gem 'minitest-retry'
diff --git a/Gemfile.lock b/Gemfile.lock
index cce2cfadd3e..4a71ff2dc2e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -136,6 +136,8 @@ GEM
addressable
capybara
playwright-ruby-client (>= 1.16.0)
+ capybara-screenshot-diff (1.12.0)
+ capybara (>= 2, < 4)
childprocess (5.1.0)
logger (~> 1.5)
cocooned (2.5.0)
@@ -814,6 +816,7 @@ DEPENDENCIES
byebug
capybara
capybara-playwright-driver (~> 0.5.9)
+ capybara-screenshot-diff
cocooned
concurrent-ruby (= 1.3.4)
countries (>= 5.5.0)
diff --git a/app/agents/pjord/product_review_agent.rb b/app/agents/pjord/product_review_agent.rb
index 51b6d48aab2..522c4e56a7a 100644
--- a/app/agents/pjord/product_review_agent.rb
+++ b/app/agents/pjord/product_review_agent.rb
@@ -10,11 +10,27 @@ class Pjord::ProductReviewAgent < Pjord::Agent
class << self
def review(product)
- extract_public_response_body(new.ask(message(product)).content).presence
+ review_result(product)[:body]
+ end
+
+ def review_result(product)
+ content = new.ask(message(product)).content
+ {
+ body: extract_public_response_body(content).presence,
+ auto_check: auto_check?(content)
+ }
end
private
+ def auto_check?(content)
+ return false unless content.respond_to?(:to_h)
+
+ values = content.to_h
+ value = values.key?(:auto_check) ? values[:auto_check] : values['auto_check']
+ ActiveModel::Type::Boolean.new.cast(value) || false
+ end
+
def message(product)
user_course_practice = UserCoursePractice.new(product.user)
<<~TEXT
@@ -31,6 +47,7 @@ def message(product)
## プラクティス
- タイトル: #{product.practice.title}
+ - ピヨルドによる提出物OK: #{product.practice.pjord_auto_check? ? '許可されています' : '許可されていません'}
- ゴール:
#{truncate_for_prompt(product.practice.goal)}
- 説明:
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index 7a041441082..667c0c557a1 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -16,7 +16,6 @@
@import "./application/blocks/admin/_embedding-status.css";
@import "./application/blocks/auth-form/_auth-form-nav.css";
@import "./application/blocks/auth-form/_auth-form.css";
-@import "./application/blocks/auth-form/_next-settlement-time.css";
@import "./application/blocks/auth-form/_skip-practices.css";
@import "./application/blocks/cards/_card-counts.css";
@import "./application/blocks/cards/_card-body-main-actions.css";
@@ -39,7 +38,6 @@
@import "./application/blocks/header/_header.css";
@import "./application/blocks/mention/_mention.css";
@import "./application/blocks/modal/_modal-celebrate-report-count-body.css";
-@import "./application/blocks/page/_o-page-info.css";
@import "./application/blocks/page/_page-body-actions.css";
@import "./application/blocks/page/_page-body-header.css";
@import "./application/blocks/page/_page-body.css";
@@ -47,7 +45,6 @@
@import "./application/blocks/page/_page-header-actions.css";
@import "./application/blocks/page/_page-main-header-actions.css";
@import "./application/blocks/page/_page-main-header.css";
-@import "./application/blocks/page/_page-notices.css";
@import "./application/blocks/page/_page-optional-header.css";
@import "./application/blocks/page/_page-tabs.css";
@import "./application/blocks/page/_page.css";
@@ -63,8 +60,6 @@
@import "./application/blocks/pair-work/_pair-work-schedule-dates.css";
@import "./application/blocks/practice/_categories.css";
@import "./application/blocks/practice/_category-practices-item.css";
-@import "./application/blocks/practice/_completion-massage.css";
-@import "./application/blocks/practice/_practice-content-actions.css";
@import "./application/blocks/practice/_practice-content.css";
@import "./application/blocks/practice/_practice-contents.css";
@import "./application/blocks/practice/_practice-first-actions.css";
@@ -79,14 +74,13 @@
@import "./application/blocks/side/_side-tabs.css";
@import "./application/blocks/side/_user-statuses.css";
@import "./application/blocks/static-pages/_thanks-message.css";
-@import "./application/blocks/static-pages/_thanks.css";
@import "./application/blocks/survey/_survey-added-question.css";
@import "./application/blocks/survey/_survey-additional-question.css";
@import "./application/blocks/survey/_survey-questions-item.css";
@import "./application/blocks/survey/_survey-questions.css";
-@import "./application/blocks/survey/survey-answer.css";
-@import "./application/blocks/survey/survey-result.css";
-@import "./application/blocks/survey/survey-answers.css";
+@import "./application/blocks/survey/_survey-answer.css";
+@import "./application/blocks/survey/_survey-result.css";
+@import "./application/blocks/survey/_survey-answers.css";
@import "./application/blocks/tags/_random-tags.css";
@import "./application/blocks/tags/_tag-links.css";
@import "./application/blocks/tags/_tag-input.css";
@@ -94,10 +88,8 @@
@import "./application/blocks/thread/_thread-comment.css";
@import "./application/blocks/thread/_thread-comments-more.css";
@import "./application/blocks/thread/_thread-comments.css";
-@import "./application/blocks/thread/_thread-form.css";
@import "./application/blocks/thread/_thread-header.css";
@import "./application/blocks/thread/_action-completed.css";
-@import "./application/blocks/user/_active-practices-list.css";
@import "./application/blocks/user/_companies-item.css";
@import "./application/blocks/user/_completed-practices-progress.css";
@import "./application/blocks/user/_following.css";
@@ -109,7 +101,6 @@
@import "./application/blocks/user/_user-group.css";
@import "./application/blocks/user/_user-metas.css";
@import "./application/blocks/user/_user-profile.css";
-@import "./application/blocks/user/_user-secret-attributes.css";
@import "./application/blocks/user/_users-item.css";
@import "./application/blocks/user/_user-study-streak-tracker.css";
@import "./application/blocks/coding-test/_code-editor.css";
diff --git a/app/assets/stylesheets/application/blocks/auth-form/_auth-form.css b/app/assets/stylesheets/application/blocks/auth-form/_auth-form.css
index 2aefc4e3c5b..e9d5861d408 100644
--- a/app/assets/stylesheets/application/blocks/auth-form/_auth-form.css
+++ b/app/assets/stylesheets/application/blocks/auth-form/_auth-form.css
@@ -1,4 +1,3 @@
-
.auth-form-logo-image {
display: block;
}
@@ -60,9 +59,6 @@
padding: 1rem 1.75rem;
}
-.auth-form__body {
-}
-
@media (width >= 48em) {
.auth-form__body {
padding: 1.5rem 1.75rem;
@@ -86,10 +82,6 @@
font-weight: 700;
}
-.auth-form-items {
- margin-bottom: 1.25rem;
-}
-
.auth-form__alert {
padding: .5rem 1.5rem;
background-color: var(--danger);
diff --git a/app/assets/stylesheets/application/blocks/auth-form/_next-settlement-time.css b/app/assets/stylesheets/application/blocks/auth-form/_next-settlement-time.css
deleted file mode 100644
index bfaa580b6a2..00000000000
--- a/app/assets/stylesheets/application/blocks/auth-form/_next-settlement-time.css
+++ /dev/null
@@ -1,28 +0,0 @@
-
-.next-settlement-time {
- @media (width >= 48em) {
- display: flex;
- align-items: center;
- justify-content: center;
-
- }
-}
-
-.next-settlement-time__title {
- font-size: .875rem;
- line-height: 1.4;
- font-weight: 400;
-
- @media (width >= 48em) {
- margin-right: .5em;
-
- }
-}
-
-.next-settlement-time__time {
- font-size: 1rem;
- line-height: 1.4;
- font-weight: 700;
- color: var(--danger);
-
-}
diff --git a/app/assets/stylesheets/application/blocks/cards/_card-counts.css b/app/assets/stylesheets/application/blocks/cards/_card-counts.css
index fe3b3210c87..d79c2d2773b 100644
--- a/app/assets/stylesheets/application/blocks/cards/_card-counts.css
+++ b/app/assets/stylesheets/application/blocks/cards/_card-counts.css
@@ -1,7 +1,3 @@
-
-.card-counts {
-}
-
@media (width >= 48em) {
.card-counts.is-user {
margin-bottom: 1.5rem;
diff --git a/app/assets/stylesheets/application/blocks/company/_company-profile.css b/app/assets/stylesheets/application/blocks/company/_company-profile.css
index ea4ae544971..8fb6eb2a3fd 100644
--- a/app/assets/stylesheets/application/blocks/company/_company-profile.css
+++ b/app/assets/stylesheets/application/blocks/company/_company-profile.css
@@ -1,29 +1,3 @@
-.company-profile__header {
- padding: 1rem;
-}
-
-.company-profile__header .company-links__items {
- justify-content: flex-start;
-}
-
-.company-profile__header-inner {
- text-align: center;
-}
-
-.company-profile__header-start {
- margin-right: 1.25rem;
-}
-
-.company-profile__header-end {
- flex: 1;
-}
-
-.company-profile__logo {
- position: static;
- text-align: center;
- margin-bottom: 0.25rem;
-}
-
.company-profile__logo-image {
background-color: var(--base);
object-fit: contain;
@@ -32,13 +6,6 @@
height: 3.5rem;
}
-.company-profile__name {
- font-weight: 800;
- color: var(--main-text);
- font-size: 1.25rem;
- line-height: 1.45;
-}
-
.company-profile__body {
padding: 1rem;
}
@@ -48,25 +15,11 @@
}
@media (width >= 48em) {
- .company-profile__header-inner {
- display: flex;
- text-align: left;
- }
-
- .company-profile__logo {
- margin-bottom: 0;
- text-align: left;
- }
-
.company-profile__logo-image {
width: 4.5rem;
height: 4.5rem;
}
- .company-profile__name {
- font-size: 1.5rem;
- }
-
.company-profile__description {
font-size: 0.9375rem;
}
diff --git a/app/assets/stylesheets/application/blocks/event/_event-meta.css b/app/assets/stylesheets/application/blocks/event/_event-meta.css
index 323cb3029f7..dae2271bfb0 100644
--- a/app/assets/stylesheets/application/blocks/event/_event-meta.css
+++ b/app/assets/stylesheets/application/blocks/event/_event-meta.css
@@ -1,7 +1,3 @@
-
-.event-meta {
-}
-
@media (width >= 48em) {
.event-meta {
font-size: .8125rem;
@@ -14,9 +10,6 @@
}
}
-.event-meta__inner {
-}
-
@media (width >= 48em) {
.event-meta__inner {
padding: .75rem 1.5rem;
diff --git a/app/assets/stylesheets/application/blocks/footer/_footer-nav.css b/app/assets/stylesheets/application/blocks/footer/_footer-nav.css
index db1a28cf421..156424a28d7 100644
--- a/app/assets/stylesheets/application/blocks/footer/_footer-nav.css
+++ b/app/assets/stylesheets/application/blocks/footer/_footer-nav.css
@@ -1,4 +1,3 @@
-
.footer-nav__items {
display: flex;
line-height: 1.4;
@@ -20,9 +19,6 @@
}
}
-.footer-nav__item {
-}
-
@media (width <= 47.9375em) {
.footer-nav__item {
flex: 0 0 50%;
diff --git a/app/assets/stylesheets/application/blocks/header/_header-dropdown.css b/app/assets/stylesheets/application/blocks/header/_header-dropdown.css
index 7dee77063dc..449d7d129b0 100644
--- a/app/assets/stylesheets/application/blocks/header/_header-dropdown.css
+++ b/app/assets/stylesheets/application/blocks/header/_header-dropdown.css
@@ -256,7 +256,6 @@ input:checked + .header-dropdown .header-dropdown__background {
}
}
-
@media (width <= 47.9375em) {
.header-dropdown__inner.is-notification .pill-nav__item-link.is-active {
background-color: var(--base);
diff --git a/app/assets/stylesheets/application/blocks/header/_header.css b/app/assets/stylesheets/application/blocks/header/_header.css
index 3cb47d242a5..af025658141 100644
--- a/app/assets/stylesheets/application/blocks/header/_header.css
+++ b/app/assets/stylesheets/application/blocks/header/_header.css
@@ -51,9 +51,6 @@
opacity: .8;
}
-.header__title-image {
-}
-
@media (width >= 48em) {
.header__title-image {
height: 2.25rem;
@@ -65,25 +62,3 @@
height: 1.75rem;
}
}
-
-.header-show-mobile-nav {
- display: flex;
- cursor: pointer;
- height: 100%;
- justify-content: center;
- align-items: center;
- width: 3rem;
- position: relative;
-}
-
-.header-show-mobile-nav-items {
- display: none;
-}
-
-@media (width <= 47.9375em) {
- .header-show-mobile-nav-items {
- display: flex;
- height: 100%;
- margin-right: -1rem;
- }
-}
diff --git a/app/assets/stylesheets/application/blocks/micro-report/_micro-report-layout.css b/app/assets/stylesheets/application/blocks/micro-report/_micro-report-layout.css
index 09a3d176aec..9de0ac2044d 100644
--- a/app/assets/stylesheets/application/blocks/micro-report/_micro-report-layout.css
+++ b/app/assets/stylesheets/application/blocks/micro-report/_micro-report-layout.css
@@ -155,47 +155,6 @@
border-right: 1px solid var(--border);
}
-/* =============================================
- Bottom nav (mobile only)
-============================================= */
-
-.mr-bottom-nav {
- display: none;
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- background-color: var(--background);
- border-top: 1px solid var(--border);
- z-index: 100;
-}
-
-.mr-bottom-nav__items {
- list-style: none;
- padding: 0;
- margin: 0;
- display: flex;
-}
-
-.mr-bottom-nav__item {
- flex: 1;
-}
-
-.mr-bottom-nav__link {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 3.25rem;
- font-size: 1.25rem;
- color: var(--default-text);
- text-decoration: none;
- transition: color 0.2s;
-}
-
-.mr-bottom-nav__link:hover {
- color: var(--main);
-}
-
/* =============================================
Responsive
============================================= */
@@ -214,8 +173,4 @@
border-right: none;
padding-bottom: 3.25rem;
}
-
- .mr-bottom-nav {
- display: block;
- }
}
diff --git a/app/assets/stylesheets/application/blocks/micro-report/_micro-report.css b/app/assets/stylesheets/application/blocks/micro-report/_micro-report.css
index 1b7475ff872..2b616552430 100644
--- a/app/assets/stylesheets/application/blocks/micro-report/_micro-report.css
+++ b/app/assets/stylesheets/application/blocks/micro-report/_micro-report.css
@@ -1,5 +1,3 @@
-
-
.micro-report {
position: relative;
display: flex;
@@ -63,9 +61,6 @@
}
}
-.micro-report__title-user-icon {
-}
-
@media (width >= 48em) {
.micro-report__title-user-icon {
display: none;
@@ -82,9 +77,6 @@
}
}
-.micro-report__title-link {
-}
-
@media (width >= 48em) {
.micro-report__title-link {
height: 100%;
@@ -127,9 +119,6 @@
margin-right: .1875rem;
}
-.micro-report__body {
-}
-
@media (width <= 47.9375em) {
.micro-report__body {
overflow-wrap: break-word;
diff --git a/app/assets/stylesheets/application/blocks/micro-report/_micro-reports.css b/app/assets/stylesheets/application/blocks/micro-report/_micro-reports.css
index 44445a24b25..16bfb5d05c2 100644
--- a/app/assets/stylesheets/application/blocks/micro-report/_micro-reports.css
+++ b/app/assets/stylesheets/application/blocks/micro-report/_micro-reports.css
@@ -18,7 +18,3 @@
.micro-reports-with-form {
padding-bottom: 8.75rem;
}
-
-.micro-reports-without-form {
- padding-bottom: 0;
-}
diff --git a/app/assets/stylesheets/application/blocks/page-content/_page-content-header-metas.css b/app/assets/stylesheets/application/blocks/page-content/_page-content-header-metas.css
index 2aec0983b0f..84980fac249 100644
--- a/app/assets/stylesheets/application/blocks/page-content/_page-content-header-metas.css
+++ b/app/assets/stylesheets/application/blocks/page-content/_page-content-header-metas.css
@@ -1,7 +1,3 @@
-
-.page-content-header-metas {
-}
-
@media (width >= 48em) {
.page-content-header-metas {
display: flex;
@@ -15,9 +11,6 @@
flex-wrap: wrap;
}
-.page-content-header-metas__meta {
-}
-
.page-content-header-metas__meta:not(:last-child) {
margin-right: .75em;
}
diff --git a/app/assets/stylesheets/application/blocks/page-content/_page-content-header.css b/app/assets/stylesheets/application/blocks/page-content/_page-content-header.css
index fe46637ccc6..7578dafca91 100644
--- a/app/assets/stylesheets/application/blocks/page-content/_page-content-header.css
+++ b/app/assets/stylesheets/application/blocks/page-content/_page-content-header.css
@@ -1,4 +1,3 @@
-
.page-content-header {
position: relative;
}
@@ -53,9 +52,6 @@
}
}
-.page-content-header__user {
-}
-
@media (width <= 47.9375em) {
.page-content-header__user {
display: flex;
@@ -63,9 +59,6 @@
}
}
-.page-content-header__category {
-}
-
@media (width <= 47.9375em) {
.page-content-header__category {
display: flex;
@@ -164,12 +157,6 @@
}
}
-.page-content-header__before-title .a-user-name {
-}
-
-.page-content-header__before-title .a-meta {
-}
-
.page-content-header__before-title .a-category-link {
font-size: .75rem;
}
@@ -245,9 +232,6 @@
margin-top: .75rem;
}
-.page-content-header__row {
-}
-
.page-content-header__row:not(:last-child) {
margin-bottom: .5rem;
}
@@ -256,9 +240,6 @@
margin-bottom: 1rem;
}
-.page-content-header__description {
-}
-
@media (width >= 48em) {
.page-content-header__description {
padding-block: .25rem;
diff --git a/app/assets/stylesheets/application/blocks/page-content/_page-content.css b/app/assets/stylesheets/application/blocks/page-content/_page-content.css
index 81a5cb3e4ef..eadc7046dc8 100644
--- a/app/assets/stylesheets/application/blocks/page-content/_page-content.css
+++ b/app/assets/stylesheets/application/blocks/page-content/_page-content.css
@@ -1,16 +1,3 @@
-
-.page-content {
-}
-
-.page-content.is-products {
-}
-
-.page-content.is-questions {
-}
-
-.page-content.is-books {
-}
-
.page-content.is-users {
width: 100%;
}
diff --git a/app/assets/stylesheets/application/blocks/page/_o-page-info.css b/app/assets/stylesheets/application/blocks/page/_o-page-info.css
deleted file mode 100644
index 09394a7ad0b..00000000000
--- a/app/assets/stylesheets/application/blocks/page/_o-page-info.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.o-page-info {
- background-color: var(--success-tint);
- font-size: 0.875rem;
- padding-block: 0.75em;
- color: var(--success-text);
- border-bottom: solid 1px var(--success);
-}
diff --git a/app/assets/stylesheets/application/blocks/page/_page-body.css b/app/assets/stylesheets/application/blocks/page/_page-body.css
index 89e17be3268..5a1f732a2ce 100644
--- a/app/assets/stylesheets/application/blocks/page/_page-body.css
+++ b/app/assets/stylesheets/application/blocks/page/_page-body.css
@@ -1,4 +1,3 @@
-
.page-body {
padding-block: 1.5rem;
}
@@ -55,9 +54,6 @@
width: 100%;
}
-.page-body__columns {
-}
-
@media (width >= 64em) {
.page-body__columns {
display: flex;
@@ -88,9 +84,6 @@
}
}
-.page-body__column {
-}
-
.page-body__column.is-main {
flex: 1;
margin-inline: auto;
@@ -104,9 +97,6 @@
flex: 0 0 14rem;
}
-.page-body__description {
-}
-
@media (width >= 48em) {
.page-body__description {
font-size: .875rem;
@@ -190,9 +180,6 @@
color: var(--warning);
}
-.page-body__description .message.alert {
-}
-
.page-body__description .message.danger {
background-color: var(--danger-tint);
color: var(--danger-text);
diff --git a/app/assets/stylesheets/application/blocks/page/_page-header-actions.css b/app/assets/stylesheets/application/blocks/page/_page-header-actions.css
index fa2040f8ad9..c3cf93fc664 100644
--- a/app/assets/stylesheets/application/blocks/page/_page-header-actions.css
+++ b/app/assets/stylesheets/application/blocks/page/_page-header-actions.css
@@ -1,6 +1,3 @@
-.page-header-actions {
-}
-
@media (width <= 47.9375em) {
.page-header-actions {
margin-inline: -1rem;
diff --git a/app/assets/stylesheets/application/blocks/page/_page-header.css b/app/assets/stylesheets/application/blocks/page/_page-header.css
index 69b9ac4111d..ca896f94e3e 100644
--- a/app/assets/stylesheets/application/blocks/page/_page-header.css
+++ b/app/assets/stylesheets/application/blocks/page/_page-header.css
@@ -1,4 +1,3 @@
-
.page-header {
background-color: var(--base);
}
@@ -85,9 +84,6 @@
align-self: center;
}
-.page-header__action {
-}
-
@media (width <= 47.9375em) {
.page-header__action {
display: none;
diff --git a/app/assets/stylesheets/application/blocks/page/_page-main-header-actions.css b/app/assets/stylesheets/application/blocks/page/_page-main-header-actions.css
index ce29df4bb98..7ceb638e1a0 100644
--- a/app/assets/stylesheets/application/blocks/page/_page-main-header-actions.css
+++ b/app/assets/stylesheets/application/blocks/page/_page-main-header-actions.css
@@ -1,13 +1,9 @@
-
.page-main-header-actions__items {
display: flex;
gap: .5rem;
justify-content: center;
}
-.page-main-header-actions__item {
-}
-
@media (width >= 48em) {
.page-main-header-actions__item {
min-width: 10rem;
diff --git a/app/assets/stylesheets/application/blocks/page/_page-notices.css b/app/assets/stylesheets/application/blocks/page/_page-notices.css
deleted file mode 100644
index 8f4c5b38280..00000000000
--- a/app/assets/stylesheets/application/blocks/page/_page-notices.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.page-notices {
- margin-block: -0.5rem 1rem;
- background-color: var(--success);
-}
-
-.page-notices__item .a-badge {
- min-width: 5rem;
-}
-
-.page-notices__item:not(:first-child) {
- border-top: dashed 1px rgb(255 255 255 / 20%);
-}
diff --git a/app/assets/stylesheets/application/blocks/page/_page-tabs.css b/app/assets/stylesheets/application/blocks/page/_page-tabs.css
index 8db0eef628f..54b61d9c0bc 100644
--- a/app/assets/stylesheets/application/blocks/page/_page-tabs.css
+++ b/app/assets/stylesheets/application/blocks/page/_page-tabs.css
@@ -1,4 +1,3 @@
-
.page-tabs {
background-color: var(--base);
border-bottom: solid 1px var(--border);
@@ -13,9 +12,6 @@
padding-inline: 1rem;
}
-.page-tabs__item {
-}
-
@media (width >= 48em) {
.page-tabs__item {
min-width: 8.5rem;
diff --git a/app/assets/stylesheets/application/blocks/practice/_category-practices-item.css b/app/assets/stylesheets/application/blocks/practice/_category-practices-item.css
index 6a44e99854c..e297f99b05b 100644
--- a/app/assets/stylesheets/application/blocks/practice/_category-practices-item.css
+++ b/app/assets/stylesheets/application/blocks/practice/_category-practices-item.css
@@ -1,4 +1,3 @@
-
.category-practices-item {
padding: .75rem 1rem;
position: relative;
@@ -36,9 +35,6 @@
flex: 0 0 2.75rem;
}
-.category-practices-item__anchor {
-}
-
@media (width >= 48em) {
.category-practices-item__anchor {
position: relative;
@@ -151,21 +147,3 @@
a.category-practices-item__title-link:hover .category-practices-item__title-link-label {
text-decoration: underline;
}
-
-.category-practices-item__learning-time {
- font-size: .75rem;
- line-height: 1.4;
- color: var(--muted-text);
-}
-
-@media (width >= 48em) {
- .category-practices-item__learning-time {
- margin-top: -.25rem;
- }
-}
-
-@media (width <= 47.9375em) {
- .category-practices-item__learning-time {
- margin-top: .5rem;
- }
-}
diff --git a/app/assets/stylesheets/application/blocks/practice/_completion-massage.css b/app/assets/stylesheets/application/blocks/practice/_completion-massage.css
deleted file mode 100644
index 7f11a9bb6a5..00000000000
--- a/app/assets/stylesheets/application/blocks/practice/_completion-massage.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.completion-massage {
- background-color: var(--success);
-}
diff --git a/app/assets/stylesheets/application/blocks/practice/_practice-content-actions.css b/app/assets/stylesheets/application/blocks/practice/_practice-content-actions.css
deleted file mode 100644
index 2da477613a3..00000000000
--- a/app/assets/stylesheets/application/blocks/practice/_practice-content-actions.css
+++ /dev/null
@@ -1,45 +0,0 @@
-
-.practice-content-actions__description {
- font-size: .75rem;
- line-height: 1.5;
- margin-top: 0;
- margin-bottom: .75rem;
- text-align: center;
-}
-
-.practice-content-actions__items {
- display: flex;
- align-items: flex-end;
- justify-content: center;
-}
-
-@media (width <= 47.9375em) {
- .practice-content-actions__items {
- flex-direction: column;
- }
-}
-
-.practice-content-actions__item {
- padding-inline: .375rem;
-}
-
-.practice-content-actions__item > * {
- width: 10rem;
- max-width: 100%;
-}
-
-@media (width <= 47.9375em) {
- .practice-content-actions__item {
- width: 100%;
- padding-inline: 0;
- }
-
- .practice-content-actions__item:nth-child(2),
- .practice-content-actions__item:nth-child(3) {
- margin-top: .75rem;
- }
-
- .practice-content-actions__item > * {
- width: 100%;
- }
-}
diff --git a/app/assets/stylesheets/application/blocks/practice/_practice-status-buttons.css b/app/assets/stylesheets/application/blocks/practice/_practice-status-buttons.css
index 6f63ed839ab..335cc125eee 100644
--- a/app/assets/stylesheets/application/blocks/practice/_practice-status-buttons.css
+++ b/app/assets/stylesheets/application/blocks/practice/_practice-status-buttons.css
@@ -1,4 +1,3 @@
-
.practice-status-buttons__start {
text-align: center;
width: 100%;
@@ -6,9 +5,6 @@
align-items: center;
}
-.practice-status-buttons__label {
-}
-
@media (width >= 48em) {
.practice-status-buttons__label {
font-size: .75rem;
@@ -100,9 +96,6 @@
color: var(--muted-text);
}
-.practice-status__buttons-item {
-}
-
@media (width <= 47.9375em) {
.practice-status__buttons-item {
width: 33.33%;
@@ -120,3 +113,23 @@
color: var(--muted-text);
margin-top: .5em;
}
+
+.practice-status-buttons__grant-link {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ max-width: 16rem;
+}
+
+.practice-status-buttons__grant-guidance {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: .75rem;
+}
+
+.practice-status-buttons__grant-note {
+ margin-top: 0;
+ text-align: center;
+}
diff --git a/app/assets/stylesheets/application/blocks/static-pages/_thanks.css b/app/assets/stylesheets/application/blocks/static-pages/_thanks.css
deleted file mode 100644
index 974f746272c..00000000000
--- a/app/assets/stylesheets/application/blocks/static-pages/_thanks.css
+++ /dev/null
@@ -1,70 +0,0 @@
-.thanks {
- max-width: 26rem;
- padding-inline: 1rem;
- margin-inline: auto;
- margin-top: 6rem;
-}
-
-.thanks__logo-image {
- max-width: 20rem;
- display: block;
- margin-inline: auto;
- margin-bottom: 3rem;
-}
-
-.thanks__body {
- background-color: var(--base);
- padding: 1.5rem;
- border-radius: 0.25rem;
-}
-
-.thanks__icon {
- font-size: 4rem;
- line-height: 1;
- margin-top: 0;
- margin-bottom: 0.5rem;
- text-align: center;
- color: var(--main);
-}
-
-.thanks__title {
- font-size: 0.875rem;
- line-height: 1.4;
- margin-top: 0;
- margin-bottom: 0.75rem;
- text-align: center;
- font-weight: 600;
-}
-
-.thanks__message p {
- font-size: 0.75rem;
- line-height: 1.6;
- text-align: center;
-}
-
-.thanks__message a {
- transition: color 0.2s ease-in;
- color: var(--link-text);
- text-decoration: none;
- cursor: pointer;
-}
-
-.thanks__message a:link {
- color: var(--link-text);
-}
-
-.thanks__message a:visited {
- color: hsl(233deg 33% 58%);
-}
-
-.thanks__message a:hover {
- color: var(--hover-text);
-}
-
-.thanks__message a:active {
- color: var(--hover-text);
-}
-
-.thanks__message a:hover, .thanks__message a:active {
- text-decoration: underline;
-}
diff --git a/app/assets/stylesheets/application/blocks/survey/survey-answer.css b/app/assets/stylesheets/application/blocks/survey/_survey-answer.css
similarity index 100%
rename from app/assets/stylesheets/application/blocks/survey/survey-answer.css
rename to app/assets/stylesheets/application/blocks/survey/_survey-answer.css
diff --git a/app/assets/stylesheets/application/blocks/survey/survey-answers.css b/app/assets/stylesheets/application/blocks/survey/_survey-answers.css
similarity index 100%
rename from app/assets/stylesheets/application/blocks/survey/survey-answers.css
rename to app/assets/stylesheets/application/blocks/survey/_survey-answers.css
diff --git a/app/assets/stylesheets/application/blocks/survey/survey-result.css b/app/assets/stylesheets/application/blocks/survey/_survey-result.css
similarity index 98%
rename from app/assets/stylesheets/application/blocks/survey/survey-result.css
rename to app/assets/stylesheets/application/blocks/survey/_survey-result.css
index 14709518890..4ddd54373f1 100644
--- a/app/assets/stylesheets/application/blocks/survey/survey-result.css
+++ b/app/assets/stylesheets/application/blocks/survey/_survey-result.css
@@ -186,5 +186,3 @@
margin-bottom: 1.5rem;
}
}
-
-/*# sourceMappingURL=survey-result.css.map */
diff --git a/app/assets/stylesheets/application/blocks/thread/_thread-comment-form.css b/app/assets/stylesheets/application/blocks/thread/_thread-comment-form.css
index ea2db666dc6..5c345d15f15 100644
--- a/app/assets/stylesheets/application/blocks/thread/_thread-comment-form.css
+++ b/app/assets/stylesheets/application/blocks/thread/_thread-comment-form.css
@@ -1,6 +1,3 @@
-.thread-comment-form {
-}
-
@media (width >= 48em) {
.thread-comment-form {
display: flex;
@@ -38,9 +35,6 @@
border-radius: 50%;
}
-.thread-comment-form__form {
-}
-
.thread-action-completed-form__form {
flex: 1;
position: relative;
diff --git a/app/assets/stylesheets/application/blocks/thread/_thread-comment.css b/app/assets/stylesheets/application/blocks/thread/_thread-comment.css
index 313c2f81a26..0e6b1f9ea66 100644
--- a/app/assets/stylesheets/application/blocks/thread/_thread-comment.css
+++ b/app/assets/stylesheets/application/blocks/thread/_thread-comment.css
@@ -1,4 +1,3 @@
-
.thread-comment {
position: relative;
border-radius: .25rem;
@@ -18,9 +17,6 @@
}
}
-.thread-comment__start {
-}
-
@media (width >= 48em) {
.thread-comment__start {
flex: 0 0 var(--thread-header-author);
@@ -35,9 +31,6 @@
}
}
-.thread-comment__end {
-}
-
@media (width >= 48em) {
.thread-comment__end {
max-width: calc(100% - var(--thread-header-author));
@@ -64,9 +57,6 @@
}
}
-.thread-comment__title-user-icon {
-}
-
@media (width >= 48em) {
.thread-comment__title-user-icon {
display: none;
@@ -83,9 +73,6 @@
}
}
-.thread-comment__title-link {
-}
-
@media (width >= 48em) {
.thread-comment__title-link {
height: 100%;
diff --git a/app/assets/stylesheets/application/blocks/thread/_thread-comments-more.css b/app/assets/stylesheets/application/blocks/thread/_thread-comments-more.css
index 3a7bac73525..2c67364430b 100644
--- a/app/assets/stylesheets/application/blocks/thread/_thread-comments-more.css
+++ b/app/assets/stylesheets/application/blocks/thread/_thread-comments-more.css
@@ -1,4 +1,3 @@
-
.thread-comments-more {
position: relative;
}
@@ -29,9 +28,6 @@
padding-block: 1.25rem;
}
-.thread-comments-more__action {
-}
-
@media (width >= 48em) {
.thread-comments-more__action {
max-width: 16rem;
diff --git a/app/assets/stylesheets/application/blocks/thread/_thread-form.css b/app/assets/stylesheets/application/blocks/thread/_thread-form.css
deleted file mode 100644
index 79656d0ab6e..00000000000
--- a/app/assets/stylesheets/application/blocks/thread/_thread-form.css
+++ /dev/null
@@ -1,43 +0,0 @@
-
-.thread-form {
-}
-
-@media (width >= 48em) {
- .thread-form {
- padding: 1rem 2rem;
- }
-}
-
-@media (width <= 47.9375em) {
- .thread-form {
- padding: .75rem 1rem;
- }
-}
-
-.thread-form__actions {
- margin-top: 1rem;
-}
-
-@media (width >= 48em) {
- .thread-form__actions {
- display: flex;
- justify-content: center;
- gap: .75rem;
- }
-}
-
-.thread-form__action {
-}
-
-@media (width >= 48em) {
- .thread-form__action {
- flex: 0 0 14rem;
- max-width: 50%;
- }
-}
-
-@media (width <= 47.9375em) {
- .thread-form__action:not(:first-child) {
- margin-top: .5rem;
- }
-}
diff --git a/app/assets/stylesheets/application/blocks/user/_active-practices-list.css b/app/assets/stylesheets/application/blocks/user/_active-practices-list.css
deleted file mode 100644
index 70666491a5b..00000000000
--- a/app/assets/stylesheets/application/blocks/user/_active-practices-list.css
+++ /dev/null
@@ -1,41 +0,0 @@
-.active-practices-list__items {
- margin-bottom: -0.25rem;
-}
-
-.active-practices-list-item:not(:last-child) {
- border-bottom: dotted 0.0625rem var(--border-tint);
-}
-
-.active-practices-list-item__link {
- font-size: 0.875rem;
- line-height: 1.55;
- display: block;
- color: var(--main);
- padding-block: 0.5rem;
- padding-inline: 1rem;
- text-decoration: none;
- cursor: pointer;
-}
-
-.active-practices-list-item__link:hover, .active-practices-list-item__link:active {
- text-decoration: underline;
-}
-
-.active-practices-list-item__link::before {
- content: "\f060";
- font-family: "Font Awesome 7 Free";
- font-style: normal;
- font-weight: 900;
- font-variant: normal;
- text-rendering: auto;
- line-height: 1;
- -moz-osx-font-smoothing: grayscale;
- -webkit-font-smoothing: antialiased;
- display: inline-block;
- margin-right: 0.5rem;
- color: var(--accent);
-}
-
-.active-practices-list-item__link:hover::before {
- text-decoration: none;
-}
diff --git a/app/assets/stylesheets/application/blocks/user/_companies-item.css b/app/assets/stylesheets/application/blocks/user/_companies-item.css
index e4d4ee64961..f75cc078d40 100644
--- a/app/assets/stylesheets/application/blocks/user/_companies-item.css
+++ b/app/assets/stylesheets/application/blocks/user/_companies-item.css
@@ -1,11 +1,7 @@
-
.companies-item {
height: 100%;
}
-.companies-item__inner {
-}
-
@media (width >= 48em) {
.companies-item__inner {
height: 100%;
@@ -25,9 +21,6 @@
gap: 1rem;
}
-.companies-item__header-start {
-}
-
@media (width >= 48em) {
.companies-item__header-start {
flex: 0 0 3.5rem;
diff --git a/app/assets/stylesheets/application/blocks/user/_group-company-name.css b/app/assets/stylesheets/application/blocks/user/_group-company-name.css
index 2bb5a14e68f..84edf904f53 100644
--- a/app/assets/stylesheets/application/blocks/user/_group-company-name.css
+++ b/app/assets/stylesheets/application/blocks/user/_group-company-name.css
@@ -1,7 +1,3 @@
-
-.group-company-name {
-}
-
@media (width >= 48em) {
.group-company-name {
font-size: 1.125rem;
diff --git a/app/assets/stylesheets/application/blocks/user/_sns-links.css b/app/assets/stylesheets/application/blocks/user/_sns-links.css
index 9bc8ed77e03..ce9ef21edd0 100644
--- a/app/assets/stylesheets/application/blocks/user/_sns-links.css
+++ b/app/assets/stylesheets/application/blocks/user/_sns-links.css
@@ -1,4 +1,3 @@
-
.sns-links {
margin-top: .5rem;
display: flex;
@@ -7,9 +6,6 @@
justify-content: space-between;
}
-.sns-links__items {
-}
-
.sns-links__item-link {
font-size: .875rem;
}
diff --git a/app/assets/stylesheets/application/blocks/user/_user-group.css b/app/assets/stylesheets/application/blocks/user/_user-group.css
index d2040e6f633..da18853fc17 100644
--- a/app/assets/stylesheets/application/blocks/user/_user-group.css
+++ b/app/assets/stylesheets/application/blocks/user/_user-group.css
@@ -1,4 +1,3 @@
-
.user-group {
padding: 1rem;
}
@@ -24,9 +23,6 @@
}
}
-.user-group__title {
-}
-
@media (width >= 48em) {
.user-group__title {
font-size: 1rem;
@@ -96,9 +92,6 @@
color: var(--muted-text);
}
-.user-group__counts {
-}
-
@media (width >= 48em) {
.user-group__counts {
margin-top: -.5rem;
diff --git a/app/assets/stylesheets/application/blocks/user/_user-profile.css b/app/assets/stylesheets/application/blocks/user/_user-profile.css
index 5494ad2ffd3..0830e07e4d1 100644
--- a/app/assets/stylesheets/application/blocks/user/_user-profile.css
+++ b/app/assets/stylesheets/application/blocks/user/_user-profile.css
@@ -1,4 +1,3 @@
-
.user-profile__icon {
@media (width <= 47.9375em) {
position: static;
@@ -20,14 +19,3 @@
}
}
-
-.user-company-profile__description {
- @media (width >= 48em) {
- font-size: .9375rem;
- }
-
- @media (width <= 47.9375em) {
- font-size: .8125rem;
-
- }
-}
diff --git a/app/assets/stylesheets/application/blocks/user/_user-secret-attributes.css b/app/assets/stylesheets/application/blocks/user/_user-secret-attributes.css
deleted file mode 100644
index 18df8d9a1f3..00000000000
--- a/app/assets/stylesheets/application/blocks/user/_user-secret-attributes.css
+++ /dev/null
@@ -1,46 +0,0 @@
-.user-secret-attributes {
- font-size: 0.75rem;
- border: solid 1px var(--border);
- padding: 0.75em 1em;
-}
-
-.user-secret-attributes:not(:first-child) {
- margin-top: 0.25em;
-}
-
-.user-secret-attributes__title {
- font-size: 0.75rem;
- line-height: 1.4;
- font-weight: 600;
- margin-bottom: 0.25em;
-}
-
-.user-secret-attributes__items {
- display: flex;
- flex-wrap: wrap;
- color: var(--muted-text);
-}
-
-.user-secret-attributes__items + .user-secret-attributes__items {
- margin-top: 0.125rem;
-}
-
-.user-secret-attributes__item {
- line-height: 1.4;
- display: flex;
- align-items: center;
-}
-
-.user-secret-attributes__item:not(:first-child)::before {
- content: "、";
-}
-
-.user-secret-attributes__item-label::after {
- content: ":";
- margin-right: 0.25rem;
-}
-
-.user-secret-attributes__item-value.is-important {
- font-weight: 600;
- color: var(--danger);
-}
diff --git a/app/assets/stylesheets/atoms/_a-border.css b/app/assets/stylesheets/atoms/_a-border.css
index 729a8c9fb4a..fbe2de94d9a 100644
--- a/app/assets/stylesheets/atoms/_a-border.css
+++ b/app/assets/stylesheets/atoms/_a-border.css
@@ -18,10 +18,3 @@
display: block;
border-top: solid 1px var(--danger);
}
-
-.a-border-shade {
- height: 1px;
- width: 100%;
- display: block;
- border-top: solid 1px var(--border-shade);
-}
diff --git a/app/assets/stylesheets/atoms/_a-button.css b/app/assets/stylesheets/atoms/_a-button.css
index e550061127b..080fd6410ef 100644
--- a/app/assets/stylesheets/atoms/_a-button.css
+++ b/app/assets/stylesheets/atoms/_a-button.css
@@ -149,9 +149,9 @@ input:checked + .a-button.is-checkbox::after {
}
.a-button.is-primary {
- background-color: #5752e8;
+ background-color: var(--primary);
color: white;
- border-color: #5752e8;
+ border-color: var(--primary);
}
.a-button.is-primary:focus, .a-button.is-primary:active {
@@ -160,7 +160,7 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-primary:hover, .a-button.is-primary:focus, .a-button.is-primary:active {
background-color: rgb(67.494552381 61.829877551 231.770122449);
- border-color: #5752e8;
+ border-color: var(--primary);
}
.a-button.is-primary.is-disabled, .a-button.is-primary:disabled {
@@ -170,9 +170,8 @@ input:checked + .a-button.is-checkbox::after {
}
.a-button.is-secondary {
- background-color: hsl(242deg 1% 100%);
- color: hsl(242deg 10% 30%);
- border-color: hsl(242deg 3% 88%);
+ background-color: var(--secondary);
+ color: var(--default-text);
border-color: var(--input-border);
}
@@ -192,9 +191,9 @@ input:checked + .a-button.is-checkbox::after {
}
.a-button.is-info {
- background-color: hsl(190deg 73% 48%);
+ background-color: var(--info);
color: white;
- border-color: hsl(190deg 73% 48%);
+ border-color: var(--info);
}
.a-button.is-info:focus, .a-button.is-info:active {
@@ -203,7 +202,7 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-info:hover, .a-button.is-info:focus, .a-button.is-info:active {
background-color: hsl(190deg 75% 44%);
- border-color: hsl(190deg 73% 48%);
+ border-color: var(--info);
}
.a-button.is-info.is-disabled, .a-button.is-info:disabled {
@@ -213,9 +212,9 @@ input:checked + .a-button.is-checkbox::after {
}
.a-button.is-success {
- background-color: hsl(150deg 39% 49%);
+ background-color: var(--success);
color: white;
- border-color: hsl(150deg 39% 49%);
+ border-color: var(--success);
}
.a-button.is-success:focus, .a-button.is-success:active {
@@ -224,7 +223,7 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-success:hover, .a-button.is-success:focus, .a-button.is-success:active {
background-color: hsl(150deg 41% 45%);
- border-color: hsl(150deg 39% 49%);
+ border-color: var(--success);
}
.a-button.is-success.is-disabled, .a-button.is-success:disabled {
@@ -234,9 +233,9 @@ input:checked + .a-button.is-checkbox::after {
}
.a-button.is-warning {
- background-color: hsl(44deg 96% 54%);
- color: hsl(242deg 10% 30%);
- border-color: hsl(44deg 96% 54%);
+ background-color: var(--warning);
+ color: var(--default-text);
+ border-color: var(--warning);
}
.a-button.is-warning:focus, .a-button.is-warning:active {
@@ -245,7 +244,7 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-warning:hover, .a-button.is-warning:focus, .a-button.is-warning:active {
background-color: hsl(44deg 98% 50%);
- border-color: hsl(44deg 96% 54%);
+ border-color: var(--warning);
}
.a-button.is-warning.is-disabled, .a-button.is-warning:disabled {
@@ -255,9 +254,9 @@ input:checked + .a-button.is-checkbox::after {
}
.a-button.is-danger {
- background-color: hsl(349deg 90% 62%);
+ background-color: var(--danger);
color: white;
- border-color: hsl(349deg 90% 62%);
+ border-color: var(--danger);
}
.a-button.is-danger:focus, .a-button.is-danger:active {
@@ -266,7 +265,7 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-danger:hover, .a-button.is-danger:focus, .a-button.is-danger:active {
background-color: hsl(349deg 92% 58%);
- border-color: hsl(349deg 90% 62%);
+ border-color: var(--danger);
}
.a-button.is-danger.is-disabled, .a-button.is-danger:disabled {
@@ -276,9 +275,9 @@ input:checked + .a-button.is-checkbox::after {
}
.a-button.is-disabled {
- background-color: hsl(242deg 11% 85%);
- color: hsl(242deg 10% 30%);
- border-color: hsl(242deg 11% 85%);
+ background-color: var(--disabled);
+ color: var(--default-text);
+ border-color: var(--disabled);
}
.a-button.is-disabled:focus, .a-button.is-disabled:active {
@@ -287,7 +286,7 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-disabled:hover, .a-button.is-disabled:focus, .a-button.is-disabled:active {
background-color: hsl(242deg 13% 81%);
- border-color: hsl(242deg 11% 85%);
+ border-color: var(--disabled);
}
.a-button.is-disabled.is-disabled, .a-button.is-disabled:disabled {
@@ -297,9 +296,9 @@ input:checked + .a-button.is-checkbox::after {
}
.a-button.is-main {
- background-color: hsl(242deg 51% 51%);
+ background-color: var(--main);
color: white;
- border-color: hsl(242deg 51% 51%);
+ border-color: var(--main);
}
.a-button.is-main:focus, .a-button.is-main:active {
@@ -308,7 +307,7 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-main:hover, .a-button.is-main:focus, .a-button.is-main:active {
background-color: hsl(242deg 53% 47%);
- border-color: hsl(242deg 51% 51%);
+ border-color: var(--main);
}
.a-button.is-main.is-disabled, .a-button.is-main:disabled {
@@ -318,9 +317,9 @@ input:checked + .a-button.is-checkbox::after {
}
.a-button.is-secondary.is-active {
- background-color: #5752e8;
+ background-color: var(--primary);
color: white;
- border-color: #5752e8;
+ border-color: var(--primary);
}
.a-button.is-secondary.is-active:focus, .a-button.is-secondary.is-active:active {
@@ -329,7 +328,7 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-secondary.is-active:hover, .a-button.is-secondary.is-active:focus, .a-button.is-secondary.is-active:active {
background-color: rgb(67.494552381 61.829877551 231.770122449);
- border-color: #5752e8;
+ border-color: var(--primary);
}
.a-button.is-secondary.is-active.is-disabled, .a-button.is-secondary.is-active:disabled {
@@ -346,8 +345,8 @@ input:checked + .a-button.is-checkbox::after {
}
.a-button.is-muted-bordered {
- border-color: hsl(242deg 7% 89%);
- color: hsl(242deg 5% 64%);
+ border-color: var(--border);
+ color: var(--muted-text);
font-weight: 400 !important;
background-color: white;
}
@@ -359,8 +358,8 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-primary-border {
background-color: rgb(255 255 255 / 10%);
- color: #5752e8;
- border-color: #5752e8;
+ color: var(--primary);
+ border-color: var(--primary);
}
.a-button.is-primary-border:focus, .a-button.is-primary-border:active {
@@ -379,8 +378,8 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-secondary-border {
background-color: rgb(255 255 255 / 10%);
- color: hsl(242deg 1% 100%);
- border-color: hsl(242deg 1% 100%);
+ color: var(--secondary);
+ border-color: var(--secondary);
}
.a-button.is-secondary-border:focus, .a-button.is-secondary-border:active {
@@ -407,7 +406,7 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-text {
text-decoration: underline;
- color: #5752e8;
+ color: var(--primary);
}
.a-button.is-text:hover,
@@ -439,7 +438,7 @@ input:checked + .a-button.is-checkbox::after {
.a-button.is-text-reversal,
.a-button.is-muted-text {
- color: hsl(242deg 5% 64%);
+ color: var(--muted-text);
}
.a-button.is-xxs,
diff --git a/app/assets/stylesheets/atoms/_a-card.css b/app/assets/stylesheets/atoms/_a-card.css
index 91988e7d755..8e03a6c5514 100644
--- a/app/assets/stylesheets/atoms/_a-card.css
+++ b/app/assets/stylesheets/atoms/_a-card.css
@@ -22,11 +22,6 @@
margin-top: 1rem;
}
-.a-card + .a-card-react,
-.a-card-react + .a-card {
- margin-top: 1rem;
-}
-
.a-card.is-borderd {
border: solid 4px var(--default-text);
}
@@ -100,17 +95,13 @@ table.a-card {
margin-bottom: 1rem;
}
- .a-card + .a-card,
- .a-card + .a-card-react,
- .a-card-react + .a-card {
+ .a-card + .a-card {
margin-top: 1rem;
}
}
@media (width >= 80em) {
- .a-card + .a-card,
- .a-card + .a-card-react,
- .a-card-react + .a-card {
+ .a-card + .a-card {
margin-top: 1.25rem;
}
}
diff --git a/app/assets/stylesheets/atoms/_a-copy-button.css b/app/assets/stylesheets/atoms/_a-copy-button.css
index c05c9921160..3039e1afb9f 100644
--- a/app/assets/stylesheets/atoms/_a-copy-button.css
+++ b/app/assets/stylesheets/atoms/_a-copy-button.css
@@ -11,7 +11,7 @@
}
.a-copy-button.is-active {
- background-color: hsl(242deg 51% 51%);
+ background-color: var(--main);
color: var(--reversal-text);
}
diff --git a/app/assets/stylesheets/atoms/_a-hidden-scrollbar.css b/app/assets/stylesheets/atoms/_a-hidden-scrollbar.css
deleted file mode 100644
index af2cae432ab..00000000000
--- a/app/assets/stylesheets/atoms/_a-hidden-scrollbar.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.a-hidden-scrollbar {
- -ms-overflow-style: none;
- scrollbar-width: none;
-}
-
-.a-hidden-scrollbar::-webkit-scrollbar {
- display: none;
-}
diff --git a/app/assets/stylesheets/atoms/_a-horizontal-dashed.css b/app/assets/stylesheets/atoms/_a-horizontal-dashed.css
deleted file mode 100644
index f4735cfd40c..00000000000
--- a/app/assets/stylesheets/atoms/_a-horizontal-dashed.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.a-horizontal-dashed {
- width: 100%;
- height: 1px;
- border-top: dashed 1px var(--border-shade);
-}
diff --git a/app/assets/stylesheets/atoms/_a-text-link.css b/app/assets/stylesheets/atoms/_a-text-link.css
index d6711be9f68..be279191517 100644
--- a/app/assets/stylesheets/atoms/_a-text-link.css
+++ b/app/assets/stylesheets/atoms/_a-text-link.css
@@ -46,21 +46,11 @@
color: var(--hover-text);
}
-.a-hover-link-reversal,
.a-reversal-text-link {
text-decoration: none;
cursor: pointer;
-}
-
-.a-hover-link-reversal:hover,
-.a-hover-link-reversal:active {
- text-decoration: underline;
-}
-
-.a-reversal-text-link {
transition: color 0.2s ease-in;
color: var(--reversal-text);
- text-decoration: none;
}
.a-reversal-text-link:link {
diff --git a/app/assets/stylesheets/atoms/_a-textarea-bottom-note.css b/app/assets/stylesheets/atoms/_a-textarea-bottom-note.css
deleted file mode 100644
index aabf90685a6..00000000000
--- a/app/assets/stylesheets/atoms/_a-textarea-bottom-note.css
+++ /dev/null
@@ -1,30 +0,0 @@
-@media (width <= 63.9375em) {
- .a-textarea-bottom-note__banner {
- display: none;
- }
-}
-
-@media (width >= 64em) {
- .a-textarea-bottom-note {
- position: relative;
- }
-
- .a-textarea-bottom-note .a-text-input {
- padding-bottom: 3rem;
- }
-
- .a-textarea-bottom-note__banner {
- background-color: var(--background-semi-shade);
- position: absolute;
- left: 1px;
- right: 1px;
- bottom: 1px;
- border-bottom-left-radius: 3px;
- border-bottom-right-radius: 3px;
- padding: 0.5rem;
- font-size: 0.75rem;
- line-height: 1.4;
- text-align: center;
- border-top: solid 1px var(--border-shade);
- }
-}
diff --git a/app/assets/stylesheets/common-imports.css b/app/assets/stylesheets/common-imports.css
index 31267510cb0..7e911d9b850 100644
--- a/app/assets/stylesheets/common-imports.css
+++ b/app/assets/stylesheets/common-imports.css
@@ -39,7 +39,6 @@
@import "./atoms/_a-grab.css";
@import "./atoms/_a-grass.css";
@import "./atoms/_a-help.css";
-@import "./atoms/_a-horizontal-dashed.css";
@import "./atoms/_a-list-item-badge.css";
@import "./atoms/_a-long-text.css";
@import "./atoms/_a-markdown-input.css";
@@ -72,7 +71,6 @@
@import "./atoms/_a-tags.css";
@import "./atoms/_a-link-card.css";
@import "./atoms/_o-empty-message.css";
-@import "./atoms/_a-hidden-scrollbar.css";
@import "./shared/layouts/_wrapper.css";
@import "./shared/layouts/_container.css";
@import "./shared/layouts/_columns.css";
@@ -86,7 +84,6 @@
@import "./shared/blocks/_global-nav.css";
@import "./shared/blocks/_modal.css";
@import "./shared/blocks/_page-nav.css";
-@import "./shared/blocks/_page-search.css";
@import "./shared/blocks/_page-tags-nav.css";
@import "./shared/blocks/_pagination.css";
@import "./shared/blocks/_pill-nav.css";
@@ -107,7 +104,6 @@
@import "./shared/blocks/card/_tags-highlight.css";
@import "./shared/blocks/card-list/_card-list-item-actions.css";
@import "./shared/blocks/card-list/_card-list-item-meta.css";
-@import "./shared/blocks/card-list/_card-list-item-tags-edit.css";
@import "./shared/blocks/card-list/_card-list-item-title.css";
@import "./shared/blocks/card-list/_card-list-item.css";
@import "./shared/blocks/card-list/_card-list-tools.css";
@@ -132,7 +128,6 @@
@import "./shared/blocks/form/_hidden-form-item.css";
@import "./shared/blocks/form/_important-message.css";
@import "./shared/blocks/form/_linear-scale.css";
-@import "./shared/blocks/form/_many-check-boxes.css";
@import "./shared/blocks/form/_markdown-form.css";
@import "./shared/blocks/form/_radios.css";
@import "./shared/blocks/form/_vue-tags-input.css";
diff --git a/app/assets/stylesheets/config/variables/_css-variables.css b/app/assets/stylesheets/config/variables/_css-variables.css
index cb7fde9394e..c60fe4a046b 100644
--- a/app/assets/stylesheets/config/variables/_css-variables.css
+++ b/app/assets/stylesheets/config/variables/_css-variables.css
@@ -23,15 +23,10 @@
--hover-text: hsl(215deg 78% 50%);
--transparent-text: rgb(0 0 0 / 0%);
--placeholder: rgb(0 0 0 / 7.5%);
- --placeholder-text: hsl(242deg 0% 73%);
--border: hsl(242deg 7% 89%);
--border-tint: hsl(242deg 14% 95%);
--border-shade: hsl(242deg 7% 84%);
--border-more-shade: hsl(242deg 5% 64%);
- --welcome-blue: hsl(210deg 100% 55%);
- --welcome-light-blue: hsl(199deg 100% 92%);
- --welcome-yellow: hsl(57deg 100% 49%);
- --welcome-orange: hsl(40deg 98% 59%);
--welcome-pink: hsl(344deg 80% 66%);
--primary: #5752e8;
--secondary: hsl(242deg 1% 100%);
@@ -86,5 +81,3 @@
--thread-header-author: 4.75rem;
--side-nav-width: 17rem;
}
-
-/*# sourceMappingURL=_css-variables.css.map */
diff --git a/app/assets/stylesheets/form/_zip-tel-input.css b/app/assets/stylesheets/form/_zip-tel-input.css
index e03cc205c15..f78cbbf2438 100644
--- a/app/assets/stylesheets/form/_zip-tel-input.css
+++ b/app/assets/stylesheets/form/_zip-tel-input.css
@@ -20,5 +20,3 @@
.a-name-input__field .a-form-label {
margin-bottom: 0.25rem;
}
-
-/*# sourceMappingURL=_zip-tel-input.css.map */
diff --git a/app/assets/stylesheets/lp.css b/app/assets/stylesheets/lp.css
index 8ced09ebb34..7980d4b9a73 100644
--- a/app/assets/stylesheets/lp.css
+++ b/app/assets/stylesheets/lp.css
@@ -31,7 +31,6 @@
@import "./lp/blocks/lp/_lp-left-number-section.css";
@import "./lp/blocks/lp/_lp-capture-section.css";
@import "./lp/blocks/lp/_lp-top-image-sections.css";
-@import "./lp/blocks/lp/_lp-supplementary-info.css";
@import "./lp/blocks/lp/_lp-page-header.css";
@import "./lp/blocks/lp/_lp-page-header-nav.css";
@import "./lp/blocks/lp/_lp-price.css";
@@ -44,7 +43,6 @@
@import "./lp/blocks/lp/_lp-faqs.css";
@import "./lp/blocks/lp/_lp-faq.css";
@import "./lp/blocks/lp/_side-filter.css";
-@import "./lp/blocks/lp/_lp-course-selection.css";
@import "./lp/blocks/lp/_lp-course-selection-nav.css";
@import "./lp/blocks/lp/_lp-course-selection-item.css";
@import "./lp/blocks/lp/_lp-category-practices.css";
diff --git a/app/assets/stylesheets/lp/base/_base.css b/app/assets/stylesheets/lp/base/_base.css
index 358493b3a0d..e938cc8c450 100644
--- a/app/assets/stylesheets/lp/base/_base.css
+++ b/app/assets/stylesheets/lp/base/_base.css
@@ -31,60 +31,3 @@ body.is-lp {
color: var(--main);
border-bottom: solid 1px var(--border);
}
-
-.welcome-practices {
- margin-top: 1.25rem;
- display: flex;
- flex-flow: column wrap;
- gap: 0.75rem 0.5rem;
-}
-
-.welcome-practices__item {
- line-height: 1.6;
- font-size: 0.8125rem;
- padding-block: 0.5rem;
-}
-
-.welcome-practices__item + .welcome-practices__item {
- border-top: solid 1px var(--border-tint);
-}
-
-.welcome-practices__item:first-child {
- padding-top: 0;
-}
-
-.welcome-practices__item:last-child {
- padding-bottom: 0;
-}
-
-@media (width >= 48em) {
- .welcome-practices {
- flex-direction: row;
- }
-
- .welcome-practices__item {
- font-size: 0.875rem;
- padding-block: 0;
- border-top: none !important;
- }
-
- .welcome-practices__item:not(:last-child)::after {
- content: "、";
- }
-}
-
-.faqs-item__header {
- padding-bottom: 0.5rem;
- border-bottom: solid 1px var(--border);
-}
-
-.faqs-item__body {
- margin-top: 1.5rem;
-}
-
-.faqs-item__title {
- font-size: 1.25rem;
- line-height: 1.6;
- font-weight: 700;
- color: var(--main);
-}
diff --git a/app/assets/stylesheets/lp/blocks/lp/_lp-content.css b/app/assets/stylesheets/lp/blocks/lp/_lp-content.css
index 88a7c4092e9..8cb44c6c229 100644
--- a/app/assets/stylesheets/lp/blocks/lp/_lp-content.css
+++ b/app/assets/stylesheets/lp/blocks/lp/_lp-content.css
@@ -181,9 +181,3 @@
gap: 3.5rem;
}
}
-
-.lp-content__body {
- display: flex;
- flex-direction: column;
- gap: 2.5rem;
-}
diff --git a/app/assets/stylesheets/lp/blocks/lp/_lp-course-selection-item.css b/app/assets/stylesheets/lp/blocks/lp/_lp-course-selection-item.css
index b1d442169f0..ebac3eac07c 100644
--- a/app/assets/stylesheets/lp/blocks/lp/_lp-course-selection-item.css
+++ b/app/assets/stylesheets/lp/blocks/lp/_lp-course-selection-item.css
@@ -15,18 +15,6 @@
gap: 1.5rem;
}
-.lp-course-selection-item__title {
- font-weight: 700;
- font-size: 1.25rem;
- line-height: 1.4;
-}
-
-@media (width >= 48em) {
- .lp-course-selection-item__title {
- font-size: 1.5rem;
- }
-}
-
.lp-course-selection-item__action {
max-width: 20rem;
width: 100%;
diff --git a/app/assets/stylesheets/lp/blocks/lp/_lp-course-selection.css b/app/assets/stylesheets/lp/blocks/lp/_lp-course-selection.css
deleted file mode 100644
index c7ea80d176a..00000000000
--- a/app/assets/stylesheets/lp/blocks/lp/_lp-course-selection.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.lp-course-selection__descritpion {
- font-size: 0.875rem;
- margin-bottom: 0.5rem;
-}
-
-@media (width >= 48em) {
- .lp-course-selection__descritpion {
- font-size: 1rem;
- }
-}
diff --git a/app/assets/stylesheets/lp/blocks/lp/_lp-header-nav.css b/app/assets/stylesheets/lp/blocks/lp/_lp-header-nav.css
index c314b061864..d3666edae73 100644
--- a/app/assets/stylesheets/lp/blocks/lp/_lp-header-nav.css
+++ b/app/assets/stylesheets/lp/blocks/lp/_lp-header-nav.css
@@ -45,14 +45,12 @@
top: 0;
bottom: 0;
z-index: 2;
- background-color: white;
background-color: var(--base);
width: 20rem;
max-width: calc(100vw - 3rem);
}
.lp-header-nav__item {
- border-bottom: solid 1px hsl(242deg 14% 95%);
border-bottom: solid 1px var(--border-tint);
}
}
@@ -60,7 +58,6 @@
.lp-header-nav__item-link {
text-decoration: none;
font-weight: 700;
- color: hsl(242deg 51% 51%);
color: var(--main);
white-space: nowrap;
display: flex;
@@ -78,13 +75,11 @@
height: 2.25rem;
padding-inline: 0.75rem;
font-size: 0.875rem;
- background-color: white;
background-color: var(--base);
border-radius: 0.5rem;
}
.lp-header-nav__item-link:hover {
- background-color: #DEDEF2;
background-color: var(--lp-bg-3);
}
}
diff --git a/app/assets/stylesheets/lp/blocks/lp/_lp-price.css b/app/assets/stylesheets/lp/blocks/lp/_lp-price.css
index d8be605f3b0..5b4865fd96d 100644
--- a/app/assets/stylesheets/lp/blocks/lp/_lp-price.css
+++ b/app/assets/stylesheets/lp/blocks/lp/_lp-price.css
@@ -98,15 +98,6 @@
gap: 0.125em;
}
-.lp-price__separator {
- font-weight: 100;
- align-self: baseline;
-}
-
-.lp-price__per-person {
- align-self: baseline;
-}
-
.lp-price__tax {
font-size: 1.25em;
align-self: baseline;
diff --git a/app/assets/stylesheets/lp/blocks/lp/_lp-supplementary-info.css b/app/assets/stylesheets/lp/blocks/lp/_lp-supplementary-info.css
deleted file mode 100644
index cb4c1040d42..00000000000
--- a/app/assets/stylesheets/lp/blocks/lp/_lp-supplementary-info.css
+++ /dev/null
@@ -1,18 +0,0 @@
-.lp-supplementary-info {
- background-color: var(--lp-bg-3);
- border-radius: 0.5rem;
-}
-
-.lp-supplementary-info__inner {
- display: flex;
- flex-direction: column;
- padding: 0.75rem 1rem;
- gap: 0.75rem;
-}
-
-@media (width >= 48em) {
- .lp-supplementary-info__inner {
- padding: 1.5rem 2rem;
- gap: 1.25rem;
- }
-}
diff --git a/app/assets/stylesheets/lp/blocks/lp/_lp-top-cover.css b/app/assets/stylesheets/lp/blocks/lp/_lp-top-cover.css
index dfd550fc003..b5e1fbd2c0f 100644
--- a/app/assets/stylesheets/lp/blocks/lp/_lp-top-cover.css
+++ b/app/assets/stylesheets/lp/blocks/lp/_lp-top-cover.css
@@ -44,42 +44,6 @@
margin-inline: auto;
}
-.lp-top-cover__image-container {
- display: flex;
- justify-content: center;
- align-items: center;
-}
-
-.lp-top-cover__logo {
- height: auto;
- width: 38rem;
- display: block;
- margin-top: 1.125rem;
- margin-inline: auto;
-}
-
-@media (width <= 47.9375em) {
- .lp-top-cover__logo {
- max-width: 90%;
- margin-top: 0.75rem;
- }
-}
-
-.lp-top-cover__description {
- font-size: 1.5rem;
- line-height: 1.4;
- text-align: center;
- font-weight: 800;
- color: var(--reversal-text);
- font-feature-settings: "palt";
-}
-
-@media (width <= 47.9375em) {
- .lp-top-cover__description {
- font-size: 0.875rem;
- }
-}
-
.lp-top-cover__title {
font-feature-settings: "palt";
background-color: var(--accent);
diff --git a/app/assets/stylesheets/lp/blocks/lp/_lp-top-image-sections.css b/app/assets/stylesheets/lp/blocks/lp/_lp-top-image-sections.css
index 2bf34cc76db..51ba6715dca 100644
--- a/app/assets/stylesheets/lp/blocks/lp/_lp-top-image-sections.css
+++ b/app/assets/stylesheets/lp/blocks/lp/_lp-top-image-sections.css
@@ -1,9 +1,3 @@
-.lp-top-image-sections__title-icon {
- height: 3rem;
- width: 3rem;
- margin-right: 0.75rem;
-}
-
.lp-top-image-section {
height: 100%;
}
diff --git a/app/assets/stylesheets/lp/blocks/lp/_welcome-top-info.css b/app/assets/stylesheets/lp/blocks/lp/_welcome-top-info.css
index b70af49f1ef..f9c2699af4f 100644
--- a/app/assets/stylesheets/lp/blocks/lp/_welcome-top-info.css
+++ b/app/assets/stylesheets/lp/blocks/lp/_welcome-top-info.css
@@ -14,30 +14,6 @@
}
}
-.welcome-top-info__label {
- border: solid 1px var(--reversal-text);
- background-color: var(--danger);
- font-size: 0.75rem;
- line-height: 1.4;
- display: flex;
- font-weight: 700;
- white-space: nowrap;
- color: var(--reversal-text);
- padding: 0.25rem 0.75rem;
- border-radius: 1rem;
- align-items: center;
- justify-content: center;
- width: 10rem;
- margin: -26px auto 0.5rem;
-}
-
-@media (width >= 48em) {
- .welcome-top-info__label {
- margin: 0 0.5rem 0 0;
- width: auto;
- }
-}
-
.welcome-top-info__link {
color: var(--reversal-text);
text-decoration: underline;
diff --git a/app/assets/stylesheets/modules/_github.css b/app/assets/stylesheets/modules/_github.css
index 3c5363b8efc..b0ad0a051d4 100644
--- a/app/assets/stylesheets/modules/_github.css
+++ b/app/assets/stylesheets/modules/_github.css
@@ -531,5 +531,3 @@
right: 2px;
bottom: 0 !important;
}
-
-/*# sourceMappingURL=_github.css.map */
diff --git a/app/assets/stylesheets/shared/blocks/_code-toolbar.css b/app/assets/stylesheets/shared/blocks/_code-toolbar.css
index a1807ee2ac3..73bdfbe30dc 100644
--- a/app/assets/stylesheets/shared/blocks/_code-toolbar.css
+++ b/app/assets/stylesheets/shared/blocks/_code-toolbar.css
@@ -56,5 +56,3 @@ pre.code-toolbar > .toolbar span {
background: #f8f8f8;
border: 1px solid #ddd;
}
-
-/*# sourceMappingURL=_code-toolbar.css.map */
diff --git a/app/assets/stylesheets/shared/blocks/_modal.css b/app/assets/stylesheets/shared/blocks/_modal.css
index 0df34f99020..b56f7fd68f3 100644
--- a/app/assets/stylesheets/shared/blocks/_modal.css
+++ b/app/assets/stylesheets/shared/blocks/_modal.css
@@ -148,18 +148,3 @@ input:checked + .modal,
font-size: 1rem;
}
}
-
-.modal-footer {
-}
-
-@media (width >= 48em) {
- .modal-footer {
- padding: .25rem 1.5rem;
- }
-}
-
-@media (width <= 47.9375em) {
- .modal-footer {
- padding: .25rem 1rem;
- }
-}
diff --git a/app/assets/stylesheets/shared/blocks/_page-nav.css b/app/assets/stylesheets/shared/blocks/_page-nav.css
index cf9cad91837..9afde46f7cc 100644
--- a/app/assets/stylesheets/shared/blocks/_page-nav.css
+++ b/app/assets/stylesheets/shared/blocks/_page-nav.css
@@ -181,14 +181,6 @@ a.page-nav__title-inner:hover {
font-weight: 600;
}
-.page-nav-metas {
- margin-top: 0.25rem;
- font-size: 0.75rem;
- display: flex;
- flex-wrap: wrap;
- gap: 0.25rem 0.75rem;
-}
-
.page-nav__item-link-inner {
display: flex;
text-decoration: none;
diff --git a/app/assets/stylesheets/shared/blocks/_page-search.css b/app/assets/stylesheets/shared/blocks/_page-search.css
deleted file mode 100644
index b93f32f1c37..00000000000
--- a/app/assets/stylesheets/shared/blocks/_page-search.css
+++ /dev/null
@@ -1,32 +0,0 @@
-.page-search {
- padding-block: 0.75rem;
- border-top: solid 1px var(--border-shade);
- border-bottom: solid 1px var(--border-shade);
-}
-
-.page-tabs + .page-search {
- padding-block: 0;
-}
-
-.page-search__form {
- width: 30rem;
- max-width: 100%;
- margin-inline: auto;
- display: flex;
- height: 100%;
- align-items: center;
- padding-right: 0.75rem;
-}
-
-.page-search__text-input {
- height: 2rem;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- border-right: none;
-}
-
-.is-icon.page-search__submit {
- height: 2rem;
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
-}
diff --git a/app/assets/stylesheets/shared/blocks/_prism-ghcolors.css b/app/assets/stylesheets/shared/blocks/_prism-ghcolors.css
index c2c2ffc3ac6..b3713205e89 100644
--- a/app/assets/stylesheets/shared/blocks/_prism-ghcolors.css
+++ b/app/assets/stylesheets/shared/blocks/_prism-ghcolors.css
@@ -92,5 +92,3 @@ pre > code.highlight {
outline: 0.4em solid red;
outline-offset: 0.4em;
}
-
-/*# sourceMappingURL=_prism-ghcolors.css.map */
diff --git a/app/assets/stylesheets/shared/blocks/card-list/_card-list-item-tags-edit.css b/app/assets/stylesheets/shared/blocks/card-list/_card-list-item-tags-edit.css
deleted file mode 100644
index da5bf91c2ea..00000000000
--- a/app/assets/stylesheets/shared/blocks/card-list/_card-list-item-tags-edit.css
+++ /dev/null
@@ -1,19 +0,0 @@
-.card-list-item-tags-edit {
- width: 100%;
-}
-
-.card-list-item-tags__item-edit {
- text-decoration: none;
- cursor: pointer;
- color: var(--main-text);
-}
-
-.card-list-item-tags__item-edit:hover, .card-list-item-tags__item-edit:active {
- text-decoration: underline;
-}
-
-.card-list-item-tags-edit__actions {
- margin-top: 0.5rem;
- display: flex;
- gap: 0.5rem;
-}
diff --git a/app/assets/stylesheets/shared/blocks/card-list/_card-list-item-title.css b/app/assets/stylesheets/shared/blocks/card-list/_card-list-item-title.css
index a1b5bdcad99..c47d0f6f805 100644
--- a/app/assets/stylesheets/shared/blocks/card-list/_card-list-item-title.css
+++ b/app/assets/stylesheets/shared/blocks/card-list/_card-list-item-title.css
@@ -1,4 +1,3 @@
-
.card-list-item-title {
display: flex;
justify-content: space-between;
@@ -30,9 +29,6 @@
}
}
-.card-list-item-title__start {
-}
-
@media (width >= 48em) {
.card-list-item-title__start {
display: flex;
diff --git a/app/assets/stylesheets/shared/blocks/card-list/_card-list-item.css b/app/assets/stylesheets/shared/blocks/card-list/_card-list-item.css
index 420edb90141..b52b978eb9f 100644
--- a/app/assets/stylesheets/shared/blocks/card-list/_card-list-item.css
+++ b/app/assets/stylesheets/shared/blocks/card-list/_card-list-item.css
@@ -1,4 +1,3 @@
-
.card-list-item {
position: relative;
}
@@ -143,9 +142,6 @@ a.card-list-item__inner:hover .card-list-item-title__title {
margin: -1rem;
}
-.card-list-item__user {
-}
-
@media (width >= 48em) {
.card-list-item__user {
min-width: 2.75rem;
@@ -158,9 +154,6 @@ a.card-list-item__inner:hover .card-list-item-title__title {
}
}
-.card-list-item__user-icon {
-}
-
@media (width >= 48em) {
.card-list-item__user-icon {
height: 2.75rem;
@@ -240,9 +233,6 @@ input:checked + .card-list-item__user-detail {
display: block;
}
-.card-list-item__user-description {
-}
-
@media (width >= 48em) {
.card-list-item__user-description {
font-size: .8125rem;
@@ -320,9 +310,6 @@ input:checked + .card-list-item__user-detail {
border-radius: 4px;
}
-.card-list-item__checker {
-}
-
@media (width >= 48em) {
.card-list-item__checker {
position: absolute;
diff --git a/app/assets/stylesheets/shared/blocks/card/_card-books.css b/app/assets/stylesheets/shared/blocks/card/_card-books.css
index 53db5550985..01bdcf41c3a 100644
--- a/app/assets/stylesheets/shared/blocks/card/_card-books.css
+++ b/app/assets/stylesheets/shared/blocks/card/_card-books.css
@@ -1,16 +1,9 @@
-
-.card-books-item {
-}
-
@media (width >= 48em) {
.card-books-item {
height: 100%;
}
}
-.card-books-item__body {
-}
-
@media (width >= 48em) {
.card-books-item__body {
flex: 1;
@@ -38,9 +31,6 @@
}
}
-.card-books-item__start {
-}
-
@media (width >= 48em) {
.card-books-item__start {
flex: 0 0 4rem;
diff --git a/app/assets/stylesheets/shared/blocks/form/_form-actions.css b/app/assets/stylesheets/shared/blocks/form/_form-actions.css
index 14390c2bb8c..167609679a1 100644
--- a/app/assets/stylesheets/shared/blocks/form/_form-actions.css
+++ b/app/assets/stylesheets/shared/blocks/form/_form-actions.css
@@ -1,7 +1,3 @@
-
-.form-actions {
-}
-
@media (width >= 48em) {
.form-actions {
margin-top: 1.5rem;
diff --git a/app/assets/stylesheets/shared/blocks/form/_form-added-choice.css b/app/assets/stylesheets/shared/blocks/form/_form-added-choice.css
index 6bfadfce442..44f01faca81 100644
--- a/app/assets/stylesheets/shared/blocks/form/_form-added-choice.css
+++ b/app/assets/stylesheets/shared/blocks/form/_form-added-choice.css
@@ -1,14 +1,7 @@
-
-.form-added-choice {
-}
-
.form-added-choice + .form-added-choice {
margin-top: 1rem;
}
-.form-added-choice__inner {
-}
-
@media (width >= 48em) {
.form-added-choice__inner {
display: flex;
@@ -31,9 +24,6 @@
}
}
-.form-added-choice__action {
-}
-
@media (width <= 47.9375em) {
.form-added-choice__action {
margin-top: .75rem;
diff --git a/app/assets/stylesheets/shared/blocks/form/_form-item-actions.css b/app/assets/stylesheets/shared/blocks/form/_form-item-actions.css
index de6f2a61408..ff9fb0cd1df 100644
--- a/app/assets/stylesheets/shared/blocks/form/_form-item-actions.css
+++ b/app/assets/stylesheets/shared/blocks/form/_form-item-actions.css
@@ -1,7 +1,3 @@
-
-.form-item-actions {
-}
-
@media (width >= 48em) {
.form-item-actions {
margin-top: -.75rem;
diff --git a/app/assets/stylesheets/shared/blocks/form/_form-item.css b/app/assets/stylesheets/shared/blocks/form/_form-item.css
index 0412b936413..9123ca209fc 100644
--- a/app/assets/stylesheets/shared/blocks/form/_form-item.css
+++ b/app/assets/stylesheets/shared/blocks/form/_form-item.css
@@ -1,6 +1,3 @@
-.form-item {
-}
-
.form-item:not(:last-child) {
margin-bottom: 1.75rem;
}
@@ -197,9 +194,6 @@
font-weight: 400;
}
-.form-item__add-times {
-}
-
@media (width >= 48em) {
.form-item__add-times {
margin-top: -.125rem;
diff --git a/app/assets/stylesheets/shared/blocks/form/_form-notice.css b/app/assets/stylesheets/shared/blocks/form/_form-notice.css
index 3c5af9caffc..2c0fff92335 100644
--- a/app/assets/stylesheets/shared/blocks/form/_form-notice.css
+++ b/app/assets/stylesheets/shared/blocks/form/_form-notice.css
@@ -1,7 +1,3 @@
-
-.form-notice {
-}
-
.form-notice:not(:first-child) {
margin-top: 1em;
}
diff --git a/app/assets/stylesheets/shared/blocks/form/_form-times.css b/app/assets/stylesheets/shared/blocks/form/_form-times.css
index 182aee1ea88..1869211d84b 100644
--- a/app/assets/stylesheets/shared/blocks/form/_form-times.css
+++ b/app/assets/stylesheets/shared/blocks/form/_form-times.css
@@ -1,7 +1,3 @@
-
-.form-times {
-}
-
@media (width >= 48em) {
.form-times {
display: flex;
@@ -36,9 +32,6 @@
}
}
-.form-times__action {
-}
-
@media (width <= 47.9375em) {
.form-times__action {
margin-top: .75rem;
diff --git a/app/assets/stylesheets/shared/blocks/form/_many-check-boxes.css b/app/assets/stylesheets/shared/blocks/form/_many-check-boxes.css
deleted file mode 100644
index 821c2f8ddde..00000000000
--- a/app/assets/stylesheets/shared/blocks/form/_many-check-boxes.css
+++ /dev/null
@@ -1,29 +0,0 @@
-.many-check-boxes {
- max-height: 16rem;
- overflow-y: auto;
- background-color: #f7f7f7;
- border: solid 1px #c1c5b9;
- border-radius: 0.25rem;
-}
-
-.many-check-boxes__item-label {
- font-size: 0.8125rem;
- line-height: 1.4;
- display: block;
- padding: 0.5rem;
- border-bottom: solid 1px #ccc;
- cursor: pointer;
-}
-
-.many-check-boxes__item-label.is-checked {
- background-color: var(--accent);
- color: var(--main-text);
-}
-
-.many-check-boxes__item:last-child .many-check-boxes__item-label {
- border-bottom: none;
-}
-
-.many-check-boxes__item-label input {
- margin-right: 0.75rem;
-}
diff --git a/app/assets/stylesheets/shared/layouts/_columns.css b/app/assets/stylesheets/shared/layouts/_columns.css
index 56291870891..b166af0b8dd 100644
--- a/app/assets/stylesheets/shared/layouts/_columns.css
+++ b/app/assets/stylesheets/shared/layouts/_columns.css
@@ -25,7 +25,6 @@
margin-top: 1.5rem;
}
-
/* =========================================================
Gutter variations
========================================================= */
diff --git a/app/components/learnings/learning_component.html.slim b/app/components/learnings/learning_component.html.slim
index b85985f2794..ec6baf441b9 100644
--- a/app/components/learnings/learning_component.html.slim
+++ b/app/components/learnings/learning_component.html.slim
@@ -1,17 +1,25 @@
-.card-main-actions
- ul.card-main-actions__items
- - if submission?
- li.card-main-actions__item
- = link_to product_link, class: 'a-button is-sm is-primary is-block test-product' do
- i.fa-solid.fa-file
- | #{product_label}
- - if @practice.completed?(@current_user)
- li.card-main-actions__item
- button.a-button.is-sm.is-secondary.is-block.is-disabled.test-completed
- i.fa-solid.fa-check
- | 修了しています
- - else
- li.card-main-actions__item
- = button_tag type: 'button', class: 'a-button is-sm is-warning is-block', id: 'js-complete', data: { practice_id: @practice.id } do
- i.fa-solid.fa-check
- | 修了
+- if @practice.guide_to_grant_course?(@current_user)
+ .practice-status-buttons
+ .practice-status-buttons__grant-guidance
+ = link_to practice_path(@practice.copied_practices.first || @practice), class: 'practice-status-buttons__grant-link a-button is-sm is-secondary' do
+ | 給付金コースへ移動する
+ .practice-status-buttons__note.practice-status-buttons__grant-note
+ | 提出・修了は、給付金コースのプラクティス側で行ってください。
+- else
+ .card-main-actions
+ ul.card-main-actions__items
+ - if @practice.submission?
+ li.card-main-actions__item
+ = link_to product_link, class: 'a-button is-sm is-primary is-block test-product' do
+ i.fa-solid.fa-file
+ | #{product_label}
+ - if @practice.completed?(@current_user)
+ li.card-main-actions__item
+ button.a-button.is-sm.is-secondary.is-block.is-disabled.test-completed
+ i.fa-solid.fa-check
+ | 修了しています
+ - else
+ li.card-main-actions__item
+ = button_tag type: 'button', class: 'a-button is-sm is-warning is-block', id: 'js-complete', data: { practice_id: @practice.id } do
+ i.fa-solid.fa-check
+ | 修了
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index fd576802856..f1eb2f802f5 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -62,6 +62,10 @@ def require_login_for_api
render json: { error: 'unauthorized' }, status: :unauthorized unless logged_in?
end
+ def require_staff
+ render json: { message: '権限がありません。' }, status: :forbidden unless current_user&.staff?
+ end
+
def doorkeeper_unauthorized_render_options(error:)
{ json: doorkeeper_error_body(error) }
end
diff --git a/app/controllers/api/movies_controller.rb b/app/controllers/api/movies_controller.rb
index 862634e0f5c..235f64eb1bb 100644
--- a/app/controllers/api/movies_controller.rb
+++ b/app/controllers/api/movies_controller.rb
@@ -1,12 +1,36 @@
# frozen_string_literal: true
class API::MoviesController < API::BaseController
+ before_action -> { doorkeeper_authorize! :write }, only: %i[create direct_uploads], if: -> { doorkeeper_token.present? }
before_action :set_movie, only: %i[update]
+ MOVIE_CONTENT_TYPES = %w[video/mp4 video/quicktime].freeze
+
def index; end
+ def create
+ @movie = current_user.movies.new(create_movie_params.except(:wip))
+ apply_wip
+ update_published_at
+
+ if @movie.save
+ render json: movie_json, status: :created
+ else
+ render json: { errors: @movie.errors }, status: :unprocessable_entity
+ end
+ rescue ActiveRecord::RecordNotFound
+ render json: { errors: { practice_ids: ['に存在しないIDが含まれています'] } }, status: :unprocessable_entity
+ end
+
+ def direct_uploads
+ return render_bad_request('動画ファイルは mp4 または mov を指定してください。') unless movie_content_type?
+
+ blob = ActiveStorage::Blob.create_before_direct_upload!(**direct_upload_params.to_h.symbolize_keys)
+ render json: direct_upload_json(blob), status: :created
+ end
+
def update
- if @movie.update(movie_params)
+ if @movie.update(update_movie_params)
head :ok
else
head :bad_request
@@ -19,7 +43,61 @@ def set_movie
@movie = Movie.find(params[:id])
end
- def movie_params
+ def create_movie_params
+ params.require(:movie).permit(
+ :title,
+ :description,
+ :movie_data,
+ :thumbnail,
+ :tag_list,
+ :wip,
+ practice_ids: []
+ )
+ end
+
+ def update_movie_params
params.require(:movie).permit(:tag_list)
end
+
+ def direct_upload_params
+ params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {})
+ end
+
+ def movie_content_type?
+ MOVIE_CONTENT_TYPES.include?(direct_upload_params[:content_type])
+ end
+
+ def direct_upload_json(blob)
+ blob.as_json(root: false, methods: :signed_id).merge(
+ direct_upload: {
+ url: blob.service_url_for_direct_upload,
+ headers: blob.service_headers_for_direct_upload
+ }
+ )
+ end
+
+ def apply_wip
+ return unless create_movie_params.key?(:wip)
+
+ @movie.wip = ActiveModel::Type::Boolean.new.cast(create_movie_params[:wip])
+ end
+
+ def update_published_at
+ return if @movie.wip? || @movie.published_at?
+
+ @movie.published_at = Time.current
+ end
+
+ def movie_json
+ {
+ id: @movie.id,
+ title: @movie.title,
+ description: @movie.description,
+ user_id: @movie.user_id,
+ wip: @movie.wip,
+ published_at: @movie.published_at,
+ practice_ids: @movie.practice_ids,
+ tag_list: @movie.tag_list
+ }
+ end
end
diff --git a/app/controllers/api/practices_controller.rb b/app/controllers/api/practices_controller.rb
index e77820f1b19..63fb664020e 100644
--- a/app/controllers/api/practices_controller.rb
+++ b/app/controllers/api/practices_controller.rb
@@ -34,6 +34,6 @@ def set_practice
end
def practice_params
- params.require(:practice).permit(:memo)
+ params.require(:practice).permit(:memo, :pjord_auto_check)
end
end
diff --git a/app/controllers/api/reports/checks_controller.rb b/app/controllers/api/reports/checks_controller.rb
new file mode 100644
index 00000000000..0c2359df9d7
--- /dev/null
+++ b/app/controllers/api/reports/checks_controller.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class API::Reports::ChecksController < API::BaseController
+ before_action :require_staff
+ before_action -> { doorkeeper_authorize! :write }, if: -> { doorkeeper_token.present? }
+ before_action -> { doorkeeper_authorize! :mentor }, if: -> { doorkeeper_token.present? }
+
+ def create
+ report_ids = params.require(:report_ids).map(&:to_i).uniq
+ reports = Report.where(id: report_ids).select(:id, :user_id).index_by(&:id)
+ missing_ids = report_ids - reports.keys
+ return render json: { message: '日報が見つかりません。', report_ids: missing_ids }, status: :not_found if missing_ids.any?
+
+ checked_report_ids = Check.where(checkable_type: 'Report', checkable_id: report_ids).pluck(:checkable_id)
+ unchecked_reports = reports.values_at(*(report_ids - checked_report_ids))
+ checks = Check.transaction do
+ insert_checks(unchecked_reports).tap { |inserted_checks| delete_report_caches(inserted_checks, reports) }
+ end
+
+ render json: { checks: checks.map { |check| check_json(check) } }, status: :created
+ end
+
+ private
+
+ def insert_checks(reports)
+ now = Time.current
+ rows = reports.map do |report|
+ { user_id: current_user.id, checkable_type: 'Report', checkable_id: report.id, created_at: now, updated_at: now }
+ end
+ result = Check.insert_all(rows, returning: %w[id]) # rubocop:disable Rails/SkipsModelValidations
+ Check.includes(:user).where(id: result.rows.flatten)
+ end
+
+ def delete_report_caches(checks, reports)
+ checked_report_ids = checks.map(&:checkable_id)
+ return if checked_report_ids.empty?
+
+ Cache.delete_unchecked_report_count
+ checked_report_ids.map { |report_id| reports.fetch(report_id).user_id }.uniq.each do |user_id|
+ Cache.delete_user_unchecked_report_count(user_id)
+ end
+ end
+end
diff --git a/app/controllers/concerns/api/checkable_check.rb b/app/controllers/concerns/api/checkable_check.rb
index a23324fbec3..f2c0f3cb0be 100644
--- a/app/controllers/concerns/api/checkable_check.rb
+++ b/app/controllers/concerns/api/checkable_check.rb
@@ -33,10 +33,6 @@ def destroy
private
- def require_staff
- render json: { message: '権限がありません。' }, status: :forbidden unless current_user&.staff?
- end
-
def set_checkable
@checkable = checkable_class.find_by(id: params[:"#{checkable_name}_id"])
render json: { message: "#{checkable_class.model_name.human}が見つかりません。" }, status: :not_found unless @checkable
diff --git a/app/controllers/mentor/practices_controller.rb b/app/controllers/mentor/practices_controller.rb
index effa529da5c..504b4014f94 100644
--- a/app/controllers/mentor/practices_controller.rb
+++ b/app/controllers/mentor/practices_controller.rb
@@ -11,7 +11,7 @@ def index
end
def new
- @practice = Practice.new(pjord_review: true)
+ @practice = Practice.new(pjord_review: true, pjord_auto_check: false)
end
def edit; end
@@ -56,6 +56,7 @@ def practice_params
:submission,
:open_product,
:pjord_review,
+ :pjord_auto_check,
:include_progress,
:completion_image,
:memo,
diff --git a/app/helpers/practices_helper.rb b/app/helpers/practices_helper.rb
index 08219f7695c..b873a3e4b32 100644
--- a/app/helpers/practices_helper.rb
+++ b/app/helpers/practices_helper.rb
@@ -25,4 +25,16 @@ def practice_ogp_meta_tags(image_url)
twitter: { image: image_url, url: request.url }
)
end
+
+ def difficulty_icon(minutes)
+ case minutes
+ when nil then ''
+ when 0 then 'データ収集中'
+ when ..300 then '🔥' # 5時間以下
+ when ..600 then '🔥🔥' # 10時間以下
+ when ..900 then '🔥🔥🔥' # 15時間以下
+ when ..1200 then '🔥🔥🔥🔥' # 20時間以下
+ else '🔥🔥🔥🔥🔥' # 20時間超
+ end
+ end
end
diff --git a/app/interactors/copy_check.rb b/app/interactors/copy_check.rb
index 9120fcc2d63..e6d6cc16e21 100644
--- a/app/interactors/copy_check.rb
+++ b/app/interactors/copy_check.rb
@@ -17,7 +17,7 @@ def call
private
def find_original_checks
- context.original_checks = context.original_product.checks.to_a
+ context.original_checks = context.original_product.checks.order(:created_at, :id).limit(1).to_a
context.message = if context.original_checks.empty?
'No checks found to copy'
@@ -32,28 +32,15 @@ def copy_all_checks
results = process_checks
store_results(results)
update_completion_message(results)
- rescue ActiveRecord::RecordInvalid => e
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
context.fail!(error: "Failed to create check: #{e.message}")
end
def process_checks
- copied_count = 0
- skipped_count = 0
+ return { copied: 0, skipped: 1 } if Check.exists?(checkable: context.copied_product)
- # Fetch all existing check user IDs for the copied product in one query
- existing_user_ids = Check.where(checkable: context.copied_product)
- .pluck(:user_id)
- .to_set
-
- context.original_checks.each do |original_check|
- if copy_check(original_check, existing_user_ids)
- copied_count += 1
- else
- skipped_count += 1
- end
- end
-
- { copied: copied_count, skipped: skipped_count }
+ copy_check(context.original_checks.first)
+ { copied: 1, skipped: 0 }
end
def store_results(results)
@@ -65,25 +52,14 @@ def update_completion_message(results)
context.message = build_summary_message(results)
end
- def products_available?
- context.original_product && context.copied_product
- end
-
def build_summary_message(results)
"Copied #{results[:copied]} check(s), skipped #{results[:skipped]} existing check(s)"
end
- def copy_check(original_check, existing_user_ids)
- # Check if this user already has a check for the copied product
- return false if existing_user_ids.include?(original_check.user_id)
-
+ def copy_check(original_check)
Check.create!(
user: original_check.user,
checkable: context.copied_product
)
-
- # Add the new user ID to the set to avoid duplicates in subsequent iterations
- existing_user_ids.add(original_check.user_id)
- true
end
end
diff --git a/app/jobs/bulk_generate_movie_thumbnail_job.rb b/app/jobs/bulk_generate_movie_thumbnail_job.rb
new file mode 100644
index 00000000000..e50ebfb84d4
--- /dev/null
+++ b/app/jobs/bulk_generate_movie_thumbnail_job.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class BulkGenerateMovieThumbnailJob < ApplicationJob
+ def perform
+ Movie.where.missing(:thumbnail_attachment).find_each do |movie|
+ GenerateMovieThumbnailJob.perform_later(movie)
+ end
+ end
+end
diff --git a/app/jobs/pjord_product_review_job.rb b/app/jobs/pjord_product_review_job.rb
index faefdf2e4f9..cbe46d5c11f 100644
--- a/app/jobs/pjord_product_review_job.rb
+++ b/app/jobs/pjord_product_review_job.rb
@@ -13,9 +13,21 @@ def perform(product_id:)
pjord = Pjord.user
return if pjord.nil?
- response = Pjord::ProductReviewAgent.review(product)
- return if response.blank?
+ response = Pjord::ProductReviewAgent.review_result(product)
+ body = response[:body]
+ return if body.blank?
- Comment.create!(user: pjord, commentable: product, description: response)
+ Comment.create!(user: pjord, commentable: product, description: body)
+ auto_check_product(product, pjord) if product.practice.pjord_auto_check? && response[:auto_check]
+ end
+
+ private
+
+ def auto_check_product(product, pjord)
+ return if product.checked?
+
+ check = Check.create!(user: pjord, checkable: product)
+ product.checks.reload
+ ActiveSupport::Notifications.instrument('check.create', check:)
end
end
diff --git a/app/models/buzz.rb b/app/models/buzz.rb
index f8833b7de16..23568eaebba 100644
--- a/app/models/buzz.rb
+++ b/app/models/buzz.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
class Buzz < ApplicationRecord
+ ROUTABLE_YEAR_RANGE = 1000..9999
+ ROUTABLE_PUBLISHED_AT_RANGE = Date.new(ROUTABLE_YEAR_RANGE.begin, 1, 1)..Date.new(ROUTABLE_YEAR_RANGE.end, 12, 31)
+
validates :title, presence: true
validates :published_at, presence: true
validates :url, presence: true, uniqueness: { message: 'はすでに登録されています' }
@@ -53,11 +56,11 @@ def for_year(year)
end
def latest_year
- maximum('published_at')&.year
+ where(published_at: ROUTABLE_PUBLISHED_AT_RANGE).maximum(:published_at)&.year
end
def years
- pluck('published_at').map(&:year).uniq.sort.reverse
+ where(published_at: ROUTABLE_PUBLISHED_AT_RANGE).pluck('published_at').map(&:year).uniq.sort.reverse
end
def doc_from_url(url)
diff --git a/app/models/check.rb b/app/models/check.rb
index 1c30d6570cf..f45b6561562 100644
--- a/app/models/check.rb
+++ b/app/models/check.rb
@@ -7,7 +7,7 @@ class Check < ApplicationRecord
after_destroy_commit -> { CheckCallbacks.new.after_destroy(self) }
alias sender user
- validates :user_id, uniqueness: { scope: %i[checkable_id checkable_type] }
+ validates :checkable_id, uniqueness: { scope: :checkable_type }
def receiver
checkable.user
diff --git a/app/models/image.rb b/app/models/image.rb
index 9fcf7220427..c2663021a0e 100644
--- a/app/models/image.rb
+++ b/app/models/image.rb
@@ -4,23 +4,10 @@ class Image < ApplicationRecord
belongs_to :user
has_one_attached :image
- after_commit :strip_exif, on: :create
+ validates :image, attached: true
- private
-
- def strip_exif
- original_image = image
- copied_image = MiniMagick::Image.read(original_image.download)
- copied_image.strip
-
- ext = File.extname(original_image.filename.to_s)
- timestamp = Time.current.strftime('%Y%m%d%H%M%S%L')
- File.open(copied_image.path) do |file|
- original_image.attach(io: file, filename: "#{user.id}_#{timestamp}#{ext}")
- end
- rescue StandardError => e
- Rails.logger.error("Failed to strip EXIF: #{e.message}")
- ensure
- copied_image&.destroy!
+ def image=(attachable)
+ MiniMagick::Image.new(attachable.tempfile.path).strip if attachable.respond_to?(:tempfile)
+ super
end
end
diff --git a/app/models/pjord_product_review_response.rb b/app/models/pjord_product_review_response.rb
index a6a9fe186a7..ef4be828df8 100644
--- a/app/models/pjord_product_review_response.rb
+++ b/app/models/pjord_product_review_response.rb
@@ -13,4 +13,7 @@ class PjordProductReviewResponse < RubyLLM::Schema
string :body,
min_length: 1,
description: '提出者にそのまま見せるレビューコメント本文。検索やツール使用などの内部手順説明、管理側へのメッセージ、運用者向けメモは含めない。'
+
+ boolean :auto_check,
+ description: '提出物がプラクティスのゴールを満たしていて、メンターの追加確認なしでOKにしてよい場合はtrue。確認できない点や改善点がある場合はfalse。'
end
diff --git a/app/models/practice.rb b/app/models/practice.rb
index 9963ea55a5a..412d5ed5ac7 100644
--- a/app/models/practice.rb
+++ b/app/models/practice.rb
@@ -211,6 +211,10 @@ def grant_course?
source_id.present?
end
+ def guide_to_grant_course?(user)
+ user.grant_course? && source_id.blank?
+ end
+
def reports_count(include_source: false)
return reports.count unless include_source
diff --git a/app/prompts/pjord/mention_response_agent/instructions.txt.erb b/app/prompts/pjord/mention_response_agent/instructions.txt.erb
index 192e2ef3362..43ead6c7162 100644
--- a/app/prompts/pjord/mention_response_agent/instructions.txt.erb
+++ b/app/prompts/pjord/mention_response_agent/instructions.txt.erb
@@ -32,5 +32,6 @@
GitHubのPR、ファイル、ディレクトリ、rawファイルへのURLが含まれる場合は、コードや差分を確認してから返信してください。
URL先やコード、画像を確認すれば分かる内容を、メンションしてきたユーザーに質問しないでください。
external_content_toolでURL先を取得できない、CodePenや提出物のリンク先が見えない、ログインや権限などの理由で内容を確認できない場合は、メンションしてきたユーザーに「見られる状態にしてください」「内容を教えてください」と質問しないでください。
-その場合は、bodyに `@mentor` を含めて、メンターにリンク先の確認と対応引き継ぎを依頼する短い返信を書いてください。
+メンターへのメンションや対応引き継ぎの依頼はしないでください。
+リンク先の内容が返信に不可欠でない場合は、確認できなかったことには言及せず、本文で確認できる内容にだけ返信してください。リンク先の内容が不可欠な場合に限り、その内容を確認できなかったことを簡潔に伝えてください。
リンク先を確認できていない状態で、内容を推測して返信しないでください。
diff --git a/app/prompts/pjord/product_review_agent/instructions.txt.erb b/app/prompts/pjord/product_review_agent/instructions.txt.erb
index 5419001dae2..6eebc209a98 100644
--- a/app/prompts/pjord/product_review_agent/instructions.txt.erb
+++ b/app/prompts/pjord/product_review_agent/instructions.txt.erb
@@ -7,6 +7,9 @@
reviewed_points には、提出物本文、URL先の内容、模範解答、過去コメントなどを確認して判断した具体的な点を1つ以上入れてください。
body には reviewed_points の内容を反映したレビューコメントを書いてください。
改善点が見つからない場合も、何を確認して問題ないと判断したかを書いてください。
+「ピヨルドによる提出物OK」が許可されていない場合、auto_check は必ず false にしてください。この場合のbodyは参考コメントであり、しっかりしたレビューやOK判定ではありません。「OKです」「合格です」「次に進んでください」「完了です」など、提出物を承認したと受け取れる表現を書かないでください。最終判断はメンターが行います。
+「ピヨルドによる提出物OK」が許可されている場合のみ、提出物がプラクティスのゴールを満たしていて、メンターの追加確認なしでOKにしてよいと判断できる場合だけ auto_check に true を設定してください。
+提出物やURL先を確認できない場合、改善点がある場合、判断に迷う場合、メンターに引き継ぐ場合は auto_check を false にしてください。
提出物コメントはそのまま提出者本人に表示されます。管理側への説明、内部事情、運用者向けメモ、レビュー生成方針への言及は含めず、すべて提出者本人に向けたコメントとして書いてください。
模範解答や他の提出物の内容をそのままコピーせず、答えを丸ごと教えないでください。
body にはレビューコメント本文だけをmarkdownで出力してください。
@@ -17,7 +20,8 @@ GitHubのPR、ファイル、ディレクトリ、rawファイルへのURLが含
Railsアプリなど複数ファイルの実装では、PRの変更ファイル一覧を確認し、必要に応じて関連ファイルのraw URLも取得してください。
URL先やコードを確認すれば分かる実装内容を、提出者に質問しないでください。
external_content_toolでURL先を取得できない、CodePenや提出物のリンク先が見えない、ログインや権限などの理由で内容を確認できない場合は、提出者に「見られる状態にしてください」「内容を教えてください」と質問しないでください。
-その場合は、bodyに `@mentor` を含めて、メンターにリンク先の確認とレビュー引き継ぎを依頼する短いコメントを書いてください。
+メンターへのメンションやレビュー引き継ぎの依頼はしないでください。
+リンク先の内容がレビューに不可欠でない場合は、確認できなかったことには言及せず、提出物本文などで確認できる内容だけをレビューしてください。リンク先の内容が不可欠な場合に限り、その内容を確認できなかったことを簡潔に伝えてください。
リンク先を確認できていない状態で、内容を推測してレビューしないでください。
## GitHub PRへのコメント
diff --git a/app/prompts/pjord/report_comment_agent/instructions.txt.erb b/app/prompts/pjord/report_comment_agent/instructions.txt.erb
index 321c2e12889..cc55ea060b1 100644
--- a/app/prompts/pjord/report_comment_agent/instructions.txt.erb
+++ b/app/prompts/pjord/report_comment_agent/instructions.txt.erb
@@ -32,5 +32,6 @@
GitHubのPR、ファイル、ディレクトリ、rawファイルへのURLが含まれる場合は、コードや差分を確認してからコメントしてください。
URL先やコード、画像を確認すれば分かる内容を、日報を書いたユーザーに質問しないでください。
external_content_toolでURL先を取得できない、CodePenやリンク先が見えない、ログインや権限などの理由で内容を確認できない場合は、日報を書いたユーザーに「見られる状態にしてください」「内容を教えてください」と質問しないでください。
-その場合は、bodyに `@mentor` を含めて、メンターにリンク先の確認と対応引き継ぎを依頼する短いコメントを書いてください。
+メンターへのメンションや対応引き継ぎの依頼はしないでください。
+リンク先の内容がコメントに不可欠でない場合は、確認できなかったことには言及せず、日報本文で確認できる内容にだけコメントしてください。リンク先の内容が不可欠な場合に限り、その内容を確認できなかったことを簡潔に伝えてください。
リンク先を確認できていない状態で、内容を推測してコメントしないでください。
diff --git a/app/tools/external_content.rb b/app/tools/external_content.rb
index 61751b35ef1..2b3d6f2b807 100644
--- a/app/tools/external_content.rb
+++ b/app/tools/external_content.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module ExternalContent
- UNREADABLE_URL_MESSAGE = 'リンク先を確認できませんでした。提出者に聞かず、@mentor にリンク先の確認と対応引き継ぎを依頼してください。'
+ UNREADABLE_URL_MESSAGE = 'リンク先を確認できませんでした。回答にその内容が不可欠でなければ、取得できなかったことには言及しないでください。@mentor にメンションしないでください。'
def self.fetch(url)
uri = URI.parse(url.to_s)
diff --git a/app/tools/external_content/web_page_reader.rb b/app/tools/external_content/web_page_reader.rb
index 781b9a43a89..021706085d9 100644
--- a/app/tools/external_content/web_page_reader.rb
+++ b/app/tools/external_content/web_page_reader.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
+require 'stringio'
require 'uri'
class ExternalContent::WebPageReader
CONTENT_LIMIT = 20_000
+ IMAGE_SIZE_LIMIT = 10.megabytes
+
def self.fetch(url)
new.fetch(url)
end
@@ -15,6 +18,8 @@ def fetch(url)
response = fetch_response(uri)
return ExternalContent::UNREADABLE_URL_MESSAGE unless response.success?
+ return format_image(response) if image?(response)
+
format_page(response.url, response.body)
rescue URI::InvalidURIError
'URLの形式が正しくありません。'
@@ -26,9 +31,13 @@ def fetch(url)
private
def fetch_response(uri)
- Rails.cache.fetch("external_content/web_page/#{Digest::SHA256.hexdigest(uri.to_s)}", expires_in: 10.minutes) do
- ExternalContent::HttpClient.get(uri.to_s, headers: request_headers)
- end
+ cache_key = "external_content/web_page/#{Digest::SHA256.hexdigest(uri.to_s)}"
+ cached_response = Rails.cache.read(cache_key)
+ return cached_response if cached_response
+
+ response = ExternalContent::HttpClient.get(uri.to_s, headers: request_headers)
+ Rails.cache.write(cache_key, response, expires_in: 10.minutes) unless image?(response)
+ response
end
def format_page(url, body)
@@ -40,6 +49,38 @@ def format_page(url, body)
TEXT
end
+ def format_image(response)
+ return ExternalContent::UNREADABLE_URL_MESSAGE if response.body.to_s.bytesize > IMAGE_SIZE_LIMIT
+
+ io = StringIO.new(response.body.to_s.b)
+ io.binmode
+
+ RubyLLM::Content.new(
+ <<~TEXT,
+ # Image
+ - URL: #{response.url}
+ - Content-Type: #{normalized_content_type(response)}
+
+ この画像の内容を確認して、回答やレビューに必要な文脈として使ってください。
+ TEXT
+ []
+ ).tap do |content|
+ content.add_attachment(io, filename: image_filename(response))
+ end
+ end
+
+ def image_filename(response)
+ extension =
+ case normalized_content_type(response)
+ when 'image/jpeg' then 'jpg'
+ when 'image/svg+xml' then 'svg'
+ else
+ normalized_content_type(response).delete_prefix('image/').presence || 'image'
+ end
+
+ "image.#{extension}"
+ end
+
def extract_text(body)
document = Nokogiri::HTML(body.to_s)
document.css('script, style, noscript').remove
@@ -47,6 +88,14 @@ def extract_text(body)
node.xpath('.//text()').map { |text| text.text.squish }.reject(&:blank?).join(' ')
end
+ def image?(response)
+ normalized_content_type(response).start_with?('image/')
+ end
+
+ def normalized_content_type(response)
+ response.content_type.to_s.split(';').first.to_s.downcase
+ end
+
def request_headers
{ 'User-Agent' => 'fjord-bootcamp-pjord' }
end
diff --git a/app/views/api/mentor/practices/_practice.json.jbuilder b/app/views/api/mentor/practices/_practice.json.jbuilder
index 2bf76c9b08e..8776043736d 100644
--- a/app/views/api/mentor/practices/_practice.json.jbuilder
+++ b/app/views/api/mentor/practices/_practice.json.jbuilder
@@ -3,6 +3,7 @@
json.id practice.id
json.title practice.title
json.submission practice.submission
+json.pjord_auto_check practice.pjord_auto_check
json.category_ids_names practice.category_ids do |category_id|
json.category_id category_id
diff --git a/app/views/api/practices/show.json.jbuilder b/app/views/api/practices/show.json.jbuilder
index 4cf21442388..08a07b83230 100644
--- a/app/views/api/practices/show.json.jbuilder
+++ b/app/views/api/practices/show.json.jbuilder
@@ -1,3 +1,3 @@
# frozen_string_literal: true
-json.(@practice, :id, :memo)
+json.(@practice, :id, :memo, :pjord_auto_check)
diff --git a/app/views/courses/practices/_courses_practice.html.slim b/app/views/courses/practices/_courses_practice.html.slim
index 9ce1f519ec5..3d947c0579b 100644
--- a/app/views/courses/practices/_courses_practice.html.slim
+++ b/app/views/courses/practices/_courses_practice.html.slim
@@ -19,11 +19,32 @@
href="#{practice_path(practice.id)}#learning-Status")
= t("activerecord.enums.learning.status.#{learning_status}")
- .category-practices-item__learning-time.is-only-mentor
- - unless practice.learning_minute_statistic.nil?
- - learning_minute_statistic = practice.learning_minute_statistic
- | 所要時間の目安: #{convert_to_hour_minute(learning_minute_statistic.median)}
- | (平均: #{convert_to_hour_minute(learning_minute_statistic.average)})
+ - learning_statistic = practice.learning_minute_statistic
+ - if learning_statistic.present?
+ .card-list-item__rows
+ .card-list-item__row
+ .card-list-item-meta
+ .card-list-item-meta__items
+ .card-list-item-meta__item
+ .a-meta
+ .a-meta__label
+ | 難易度
+ .a-meta__value
+ = difficulty_icon(learning_statistic.median)
+ - if current_user.mentor? || current_user.admin?
+ .card-list-item-meta__item.is-only-mentor
+ .a-meta
+ .a-meta__label
+ | 所要時間の目安
+ .a-meta__value
+ = convert_to_hour_minute(learning_statistic.median)
+ .card-list-item-meta__item.is-only-mentor
+ .a-meta
+ .a-meta__label
+ | (平均
+ .a-meta__value
+ = convert_to_hour_minute(learning_statistic.average)
+ | )
- if practice.started_or_submitted_students.present?
.a-user-icons
.a-user-icons__items
diff --git a/app/views/mentor/practices/_form.html.slim b/app/views/mentor/practices/_form.html.slim
index 5b764f6e404..9e2869249c6 100644
--- a/app/views/mentor/practices/_form.html.slim
+++ b/app/views/mentor/practices/_form.html.slim
@@ -98,6 +98,10 @@
= f.check_box :pjord_review, class: 'a-toggle-checkbox'
= f.label :pjord_review
| ピヨルドの提出物レビューをする場合はチェック
+ li.checkboxes__item
+ = f.check_box :pjord_auto_check, class: 'a-toggle-checkbox'
+ = f.label :pjord_auto_check
+ | ピヨルドがOKを出してもよい場合はチェック
= f.label :progress, class: 'a-form-label'
.checkboxes
ul.checkboxes__items
diff --git a/app/views/practices/_learning-status.html.slim b/app/views/practices/_learning-status.html.slim
index 68ad356638f..b3fe6146a8c 100644
--- a/app/views/practices/_learning-status.html.slim
+++ b/app/views/practices/_learning-status.html.slim
@@ -1,24 +1,32 @@
.js-learning-status(id="practice" data-id="#{@practice.id}")
.practice-status-buttons
- .practice-status-buttons__start
- .practice-status-buttons__label
- | ステータス
- ul.practice-status-buttons__items.is-button-group
- - button_base_class = 'practice-status-buttons__button a-button is-sm is-block'
- li.practice-status-buttons__item
- - button_class = "#{button_base_class} is-unstarted js-unstarted #{@status == 'unstarted' ? 'is-active' : 'is-inactive'}"
- = content_tag :button, '未着手', class: button_class, disabled: @status == 'unstarted', data: { status: 'unstarted' }
- li.practice-status-buttons__item
- - button_class = "#{button_base_class} is-started js-started #{@status == 'started' ? 'is-active' : 'is-inactive'}"
- = content_tag :button, '着手', class: button_class, disabled: @status == 'started', data: { status: 'started' }
- - if @practice.submission
+ - if @practice.guide_to_grant_course?(current_user)
+ .practice-status-buttons
+ .practice-status-buttons__grant-guidance
+ = link_to practice_path(@practice.copied_practices.first || @practice), class: 'practice-status-buttons__grant-link a-button is-sm is-secondary' do
+ | 給付金コースへ移動する
+ .practice-status-buttons__note.practice-status-buttons__grant-note
+ | 進捗のステータス変更は、給付金コースのプラクティス側で行ってください。
+ - else
+ .practice-status-buttons__start
+ .practice-status-buttons__label
+ | ステータス
+ ul.practice-status-buttons__items.is-button-group
+ - button_base_class = 'practice-status-buttons__button a-button is-sm is-block'
li.practice-status-buttons__item
- - button_class = "#{button_base_class} is-submitted js-submitted #{@status == 'submitted' ? 'is-active' : 'is-inactive'}"
- = content_tag :button, '提出', class: button_class, disabled: @status == 'submitted', data: { status: 'submitted' }
- li.practice-status-buttons__item
- - button_class = "#{button_base_class} is-complete js-complete #{@status == 'complete' ? 'is-active' : 'is-inactive'}"
- = content_tag :button, '修了', class: button_class, disabled: @status == 'complete', data: { status: 'complete' }
-- if !@practice.submission
- .practice-status-buttons__end
- .practice-status-buttons__note
- | このプラクティスに提出物はありません。修了条件をクリアしたら修了にしてください。
+ - button_class = "#{button_base_class} is-unstarted js-unstarted #{@status == 'unstarted' ? 'is-active' : 'is-inactive'}"
+ = content_tag :button, '未着手', class: button_class, disabled: @status == 'unstarted', data: { status: 'unstarted' }
+ li.practice-status-buttons__item
+ - button_class = "#{button_base_class} is-started js-started #{@status == 'started' ? 'is-active' : 'is-inactive'}"
+ = content_tag :button, '着手', class: button_class, disabled: @status == 'started', data: { status: 'started' }
+ - if @practice.submission
+ li.practice-status-buttons__item
+ - button_class = "#{button_base_class} is-submitted js-submitted #{@status == 'submitted' ? 'is-active' : 'is-inactive'}"
+ = content_tag :button, '提出', class: button_class, disabled: @status == 'submitted', data: { status: 'submitted' }
+ li.practice-status-buttons__item
+ - button_class = "#{button_base_class} is-complete js-complete #{@status == 'complete' ? 'is-active' : 'is-inactive'}"
+ = content_tag :button, '修了', class: button_class, disabled: @status == 'complete', data: { status: 'complete' }
+ - if !@practice.submission
+ .practice-status-buttons__end
+ .practice-status-buttons__note
+ | このプラクティスに提出物はありません。修了条件をクリアしたら修了にしてください。
diff --git a/app/views/practices/show.html.slim b/app/views/practices/show.html.slim
index ac034000e37..23f0443d77c 100644
--- a/app/views/practices/show.html.slim
+++ b/app/views/practices/show.html.slim
@@ -124,7 +124,7 @@
hr.a-border-tint
footer.card-footer
= render(Learnings::LearningComponent.new(practice: @practice, current_user:))
- - if @practice.submission
+ - if @practice.submission && !@practice.guide_to_grant_course?(current_user)
.card-footer__alert
= link_to '提出の前に、提出時の注意点を確認しよう',
'https://bootcamp.fjord.jp/pages/info-for-product',
@@ -135,10 +135,11 @@
br
| このプラクティスを修了にしてください。
- else
- .card-footer__description
- | このプラクティスに提出物はありません。
- br
- | 修了条件をクリアしたら修了にしてください。
+ - unless @practice.guide_to_grant_course?(@current_user)
+ .card-footer__description
+ | このプラクティスに提出物はありません。
+ br
+ | 修了条件をクリアしたら修了にしてください。
- if @practice.coding_tests.present?
= render partial: 'coding_tests', locals: { coding_tests: @practice.coding_tests }
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 9e3f54b9088..7e9b8bd0dc1 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -162,6 +162,7 @@ ja:
target: ターゲット
submission: 提出物
pjord_review: ピヨルドの提出物レビュー
+ pjord_auto_check: ピヨルドの提出物OK
submission_answer: 模範解答
progress: 進捗の計算
completion_image: 修了おめでとう画像
diff --git a/config/routes/api.rb b/config/routes/api.rb
index c3e5b0f57ec..1b5e8e1d6b2 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -57,6 +57,7 @@
end
resources :recents, only: %i(index)
end
+ post 'reports/checks', to: 'reports/checks#create', as: :reports_checks
resources :reports, only: %i(index show create update destroy) do
resources :comments, only: %i[create], controller: 'reports/comments'
resources :reactions, only: %i(index create destroy), controller: 'reports/reactions'
@@ -97,7 +98,9 @@
resource :position, only: %i(update), controller: "survey_question_listings/position"
end
resources :reading_circles, only: %i(index)
- resources :movies, only: %i(index update)
+ resources :movies, only: %i(index create update) do
+ post :direct_uploads, on: :collection
+ end
resources :metadata, only: %i(index)
resources :micro_reports, only: %i(update)
resources :trainee_progresses, only: %i(index)
diff --git a/db/data/20260707000000_delete_duplicate_checks.rb b/db/data/20260707000000_delete_duplicate_checks.rb
new file mode 100644
index 00000000000..5a4f2cddd5c
--- /dev/null
+++ b/db/data/20260707000000_delete_duplicate_checks.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class DeleteDuplicateChecks < ActiveRecord::Migration[8.1]
+ def up
+ execute <<~SQL.squish
+ DELETE FROM checks
+ WHERE id IN (
+ SELECT id
+ FROM (
+ SELECT id,
+ ROW_NUMBER() OVER (
+ PARTITION BY checkable_id, checkable_type
+ ORDER BY created_at ASC, id ASC
+ ) AS row_number
+ FROM checks
+ ) duplicate_checks
+ WHERE duplicate_checks.row_number > 1
+ )
+ SQL
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/data_schema.rb b/db/data_schema.rb
index eb0c968a79e..0d5559ad2fa 100644
--- a/db/data_schema.rb
+++ b/db/data_schema.rb
@@ -1 +1 @@
-DataMigrate::Data.define(version: 2026_03_25_014711)
+DataMigrate::Data.define(version: 2026_07_07_000000)
diff --git a/db/fixtures/checks.yml b/db/fixtures/checks.yml
index 36301ee3413..bfafc8bef63 100644
--- a/db/fixtures/checks.yml
+++ b/db/fixtures/checks.yml
@@ -14,10 +14,6 @@ report5_check_machida:
user: machida
checkable: report5 (Report)
-report5_check_komagata:
- user: komagata
- checkable: report5 (Report)
-
report6_check_komagata:
user: komagata
checkable: report6 (Report)
@@ -26,22 +22,22 @@ report7_check_machida:
user: machida
checkable: report7 (Report)
-procuct2_check_komagata:
+product2_check_komagata:
user: komagata
checkable: product2 (Product)
-procuct3_check_komagata:
+product3_check_komagata:
user: komagata
checkable: product3 (Product)
-procuct4_check_komagata:
+product4_check_komagata:
user: komagata
checkable: product4 (Product)
-procuct7_check_komagata:
+product7_check_komagata:
user: komagata
checkable: product7 (Product)
-procuct9_check_komagata:
+product9_check_komagata:
user: komagata
checkable: product9 (Product)
diff --git a/db/migrate/20260707000000_change_checks_unique_index_to_checkable.rb b/db/migrate/20260707000000_change_checks_unique_index_to_checkable.rb
new file mode 100644
index 00000000000..4c6cccdb20b
--- /dev/null
+++ b/db/migrate/20260707000000_change_checks_unique_index_to_checkable.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class ChangeChecksUniqueIndexToCheckable < ActiveRecord::Migration[8.1]
+ def up
+ delete_duplicate_checks
+
+ remove_index :checks, name: 'index_checks_on_user_id_and_checkable_id_and_checkable_type'
+ add_index :checks, %i[checkable_id checkable_type], unique: true
+ end
+
+ def down
+ remove_index :checks, %i[checkable_id checkable_type]
+ add_index :checks, %i[user_id checkable_id checkable_type],
+ unique: true,
+ name: 'index_checks_on_user_id_and_checkable_id_and_checkable_type'
+ end
+
+ private
+
+ def delete_duplicate_checks
+ execute <<~SQL.squish
+ DELETE FROM checks
+ WHERE id IN (
+ SELECT id
+ FROM (
+ SELECT id,
+ ROW_NUMBER() OVER (
+ PARTITION BY checkable_id, checkable_type
+ ORDER BY created_at ASC, id ASC
+ ) AS row_number
+ FROM checks
+ ) duplicate_checks
+ WHERE duplicate_checks.row_number > 1
+ )
+ SQL
+ end
+end
diff --git a/db/migrate/20260708000000_add_pjord_auto_check_to_practices.rb b/db/migrate/20260708000000_add_pjord_auto_check_to_practices.rb
new file mode 100644
index 00000000000..4af0e68f67e
--- /dev/null
+++ b/db/migrate/20260708000000_add_pjord_auto_check_to_practices.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddPjordAutoCheckToPractices < ActiveRecord::Migration[8.1]
+ def change
+ add_column :practices, :pjord_auto_check, :boolean, null: false, default: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 17762346eb6..99e52cbd229 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.1].define(version: 2026_06_30_000000) do
+ActiveRecord::Schema[8.1].define(version: 2026_07_08_000000) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
enable_extension "pgcrypto"
@@ -173,8 +173,8 @@
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
t.integer "user_id", null: false
+ t.index ["checkable_id", "checkable_type"], name: "index_checks_on_checkable_id_and_checkable_type", unique: true
t.index ["checkable_id"], name: "index_checks_on_checkable_id"
- t.index ["user_id", "checkable_id", "checkable_type"], name: "index_checks_on_user_id_and_checkable_id_and_checkable_type", unique: true
t.index ["user_id"], name: "index_checks_on_user_id"
end
@@ -595,6 +595,7 @@
t.integer "last_updated_user_id"
t.text "memo"
t.boolean "open_product", default: false, null: false
+ t.boolean "pjord_auto_check", default: false, null: false
t.boolean "pjord_review", default: true, null: false
t.integer "source_id"
t.boolean "submission", default: false, null: false
diff --git a/db/seeds.rb b/db/seeds.rb
index 53efc3725c6..da6e59505fe 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -79,3 +79,4 @@
]
ActiveRecord::FixtureSet.create_fixtures 'db/fixtures', tables
+Bootcamp::Setup.attachment if Rails.env.development?
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index 7eda55440d2..d613f474429 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -1140,6 +1140,88 @@ paths:
responses:
'200':
$ref: '#/components/responses/JsonObject'
+ post:
+ tags: [Content]
+ summary: Create a movie with uploaded movie data.
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ required:
+ - movie[title]
+ - movie[description]
+ - movie[movie_data]
+ properties:
+ movie[title]:
+ type: string
+ movie[description]:
+ type: string
+ movie[movie_data]:
+ oneOf:
+ - type: string
+ format: binary
+ - type: string
+ description: Signed blob ID returned by /api/movies/direct_uploads.
+ movie[thumbnail]:
+ type: string
+ format: binary
+ movie[tag_list]:
+ type: string
+ movie[wip]:
+ type: boolean
+ movie[practice_ids][]:
+ type: array
+ items:
+ type: integer
+ responses:
+ '201':
+ $ref: '#/components/responses/JsonObject'
+ '403':
+ $ref: '#/components/responses/JsonObject'
+ '422':
+ $ref: '#/components/responses/JsonObject'
+ /api/movies/direct_uploads:
+ post:
+ tags: [Content]
+ summary: Create direct upload data for a movie file.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - blob
+ properties:
+ blob:
+ type: object
+ required:
+ - filename
+ - byte_size
+ - checksum
+ - content_type
+ properties:
+ filename:
+ type: string
+ byte_size:
+ type: integer
+ checksum:
+ type: string
+ description: Base64-encoded MD5 checksum.
+ content_type:
+ type: string
+ enum:
+ - video/mp4
+ - video/quicktime
+ responses:
+ '201':
+ $ref: '#/components/responses/JsonObject'
+ '400':
+ $ref: '#/components/responses/JsonObject'
+ '403':
+ $ref: '#/components/responses/JsonObject'
/api/movies/{id}:
patch:
tags: [Content]
diff --git a/lib/bootcamp/setup.rb b/lib/bootcamp/setup.rb
index 92a0b0eb61b..eb4468ddef7 100644
--- a/lib/bootcamp/setup.rb
+++ b/lib/bootcamp/setup.rb
@@ -83,6 +83,8 @@ def attach_movie_data!
movie_path = Rails.root.join("#{fixtures_dir}/fixtures/files/movies/movie.mov")
movie.movie_data.attach(io: File.open(movie_path), filename: 'movie.mov')
end
+
+ GenerateMovieThumbnailJob.perform_now(movie)
end
end
end
diff --git a/lib/tasks/bootcamp.rake b/lib/tasks/bootcamp.rake
index e98bb43d68f..2319c2f016b 100644
--- a/lib/tasks/bootcamp.rake
+++ b/lib/tasks/bootcamp.rake
@@ -57,6 +57,7 @@ namespace :bootcamp do
puts '== START Cloud Build Task =='
Rake::Task['smart_search:generate_all'].invoke
+ BulkGenerateMovieThumbnailJob.perform_now
puts '== END Cloud Build Task =='
end
diff --git a/public/api-docs/openapi.yaml b/public/api-docs/openapi.yaml
index 7eda55440d2..d613f474429 100644
--- a/public/api-docs/openapi.yaml
+++ b/public/api-docs/openapi.yaml
@@ -1140,6 +1140,88 @@ paths:
responses:
'200':
$ref: '#/components/responses/JsonObject'
+ post:
+ tags: [Content]
+ summary: Create a movie with uploaded movie data.
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ required:
+ - movie[title]
+ - movie[description]
+ - movie[movie_data]
+ properties:
+ movie[title]:
+ type: string
+ movie[description]:
+ type: string
+ movie[movie_data]:
+ oneOf:
+ - type: string
+ format: binary
+ - type: string
+ description: Signed blob ID returned by /api/movies/direct_uploads.
+ movie[thumbnail]:
+ type: string
+ format: binary
+ movie[tag_list]:
+ type: string
+ movie[wip]:
+ type: boolean
+ movie[practice_ids][]:
+ type: array
+ items:
+ type: integer
+ responses:
+ '201':
+ $ref: '#/components/responses/JsonObject'
+ '403':
+ $ref: '#/components/responses/JsonObject'
+ '422':
+ $ref: '#/components/responses/JsonObject'
+ /api/movies/direct_uploads:
+ post:
+ tags: [Content]
+ summary: Create direct upload data for a movie file.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - blob
+ properties:
+ blob:
+ type: object
+ required:
+ - filename
+ - byte_size
+ - checksum
+ - content_type
+ properties:
+ filename:
+ type: string
+ byte_size:
+ type: integer
+ checksum:
+ type: string
+ description: Base64-encoded MD5 checksum.
+ content_type:
+ type: string
+ enum:
+ - video/mp4
+ - video/quicktime
+ responses:
+ '201':
+ $ref: '#/components/responses/JsonObject'
+ '400':
+ $ref: '#/components/responses/JsonObject'
+ '403':
+ $ref: '#/components/responses/JsonObject'
/api/movies/{id}:
patch:
tags: [Content]
diff --git a/test/agents/pjord/mention_response_agent_test.rb b/test/agents/pjord/mention_response_agent_test.rb
index 1964cf75cf9..95efe4f0df8 100644
--- a/test/agents/pjord/mention_response_agent_test.rb
+++ b/test/agents/pjord/mention_response_agent_test.rb
@@ -19,7 +19,8 @@ class Pjord::MentionResponseAgentTest < ActiveSupport::TestCase
assert_includes chat.instructions, 'external_content_toolを使って内容を確認してから返信してください。'
assert_includes chat.instructions, 'GitHubのPR、ファイル、ディレクトリ、rawファイルへのURLが含まれる場合'
assert_includes chat.instructions, 'CodePenや提出物のリンク先が見えない'
- assert_includes chat.instructions, 'bodyに `@mentor` を含めて'
+ assert_includes chat.instructions, 'メンターへのメンションや対応引き継ぎの依頼はしないでください。'
+ assert_includes chat.instructions, 'リンク先の内容が返信に不可欠でない場合'
assert_includes chat.instructions, 'メンションしてきたユーザーに「見られる状態にしてください」「内容を教えてください」と質問しないでください。'
assert_includes chat.instructions, 'ピヨルドのレビューコメントに対して'
assert_includes chat.instructions, 'body を空にして返信しないでください。'
diff --git a/test/agents/pjord/product_review_agent_test.rb b/test/agents/pjord/product_review_agent_test.rb
index 8778d13f0e5..e2aeb3bc21a 100644
--- a/test/agents/pjord/product_review_agent_test.rb
+++ b/test/agents/pjord/product_review_agent_test.rb
@@ -16,6 +16,7 @@ class Pjord::ProductReviewAgentTest < ActiveSupport::TestCase
chat
}) do
assert_equal 'レビュー本文', Pjord::ProductReviewAgent.review(product)
+ assert_equal({ body: 'レビュー本文', auto_check: true }, Pjord::ProductReviewAgent.review_result(product))
end
assert_equal [BootcampSearchTool, UserInfoTool, ExternalContentTool, GithubPullRequestReviewCommentTool], chat.tools
@@ -23,6 +24,7 @@ class Pjord::ProductReviewAgentTest < ActiveSupport::TestCase
asked_message = chat.asked_message
assert_includes asked_message, product.user.login_name
assert_includes asked_message, product.practice.title
+ assert_includes asked_message, 'ピヨルドによる提出物OK: 許可されていません'
assert_includes asked_message, product.practice.goal
assert_includes asked_message, product.body
assert_includes asked_message, product.practice.submission_answer.description
@@ -33,10 +35,14 @@ class Pjord::ProductReviewAgentTest < ActiveSupport::TestCase
assert_includes chat.instructions, '人間らしい文章にする'
assert_includes chat.instructions, '提出物にレビューコメントを書いてください。'
assert_includes chat.instructions, 'reviewed_points には、提出物本文、URL先の内容、模範解答、過去コメントなどを確認して判断した具体的な点を1つ以上入れてください。'
+ assert_includes chat.instructions, '「ピヨルドによる提出物OK」が許可されていない場合、auto_check は必ず false'
+ assert_includes chat.instructions, '「OKです」「合格です」「次に進んでください」「完了です」など、提出物を承認したと受け取れる表現を書かないでください。'
+ assert_includes chat.instructions, 'メンターの追加確認なしでOKにしてよいと判断できる場合だけ auto_check に true'
assert_includes chat.instructions, '管理側への説明、内部事情、運用者向けメモ、レビュー生成方針への言及は含めず'
assert_includes chat.instructions, 'external_content_toolを使って内容を確認してからレビューしてください。'
assert_includes chat.instructions, 'CodePenや提出物のリンク先が見えない'
- assert_includes chat.instructions, 'bodyに `@mentor` を含めて'
+ assert_includes chat.instructions, 'メンターへのメンションやレビュー引き継ぎの依頼はしないでください。'
+ assert_includes chat.instructions, 'リンク先の内容がレビューに不可欠でない場合'
assert_includes chat.instructions, '提出者に「見られる状態にしてください」「内容を教えてください」と質問しないでください。'
assert_includes chat.instructions, 'コードの特定行に対する具体的な指摘は、可能な限りgithub_pull_request_review_comment_toolを使ってPRの該当行へ直接コメントしてください。'
end
@@ -53,6 +59,18 @@ class Pjord::ProductReviewAgentTest < ActiveSupport::TestCase
assert_includes chat.asked_message, '進捗率: 不明'
end
+ test '.review tells when Pjord auto check is allowed' do
+ product = products(:product1)
+ product.practice.update!(pjord_auto_check: true)
+ chat = ProductReviewChatFake.new
+
+ RubyLLM.stub(:chat, chat) do
+ Pjord::ProductReviewAgent.review(product)
+ end
+
+ assert_includes chat.asked_message, 'ピヨルドによる提出物OK: 許可されています'
+ end
+
test '.review truncates long text in prompt' do
product = products(:product1)
product.update!(body: 'a' * (Pjord::ProductReviewAgent::PROMPT_TEXT_LIMIT + 1))
@@ -95,10 +113,20 @@ class Pjord::ProductReviewAgentTest < ActiveSupport::TestCase
assert_not_includes chat.asked_message, '## 提出物内のGitHubリンク先コード'
end
+ test '.review_result returns false when auto_check is false' do
+ product = products(:product1)
+ chat = ProductReviewChatFake.new(auto_check: false)
+
+ RubyLLM.stub(:chat, chat) do
+ assert_equal({ body: 'レビュー本文', auto_check: false }, Pjord::ProductReviewAgent.review_result(product))
+ end
+ end
+
class ProductReviewChatFake
attr_reader :asked_message, :instructions, :schema, :tools
- def initialize
+ def initialize(auto_check: true)
+ @auto_check = auto_check
@tools = []
end
@@ -120,7 +148,7 @@ def with_schema(schema)
def ask(message, with: nil)
@asked_message = message
@attachments = with
- Struct.new(:content).new({ body: 'レビュー本文', reviewed_points: ['提出物本文'] })
+ Struct.new(:content).new({ body: 'レビュー本文', reviewed_points: ['提出物本文'], auto_check: @auto_check })
end
end
end
diff --git a/test/agents/pjord/report_comment_agent_test.rb b/test/agents/pjord/report_comment_agent_test.rb
index 902835f1fce..2932e929fce 100644
--- a/test/agents/pjord/report_comment_agent_test.rb
+++ b/test/agents/pjord/report_comment_agent_test.rb
@@ -18,7 +18,8 @@ class Pjord::ReportCommentAgentTest < ActiveSupport::TestCase
assert_includes chat.instructions, 'external_content_toolを使って内容を確認してからコメントしてください。'
assert_includes chat.instructions, 'GitHubのPR、ファイル、ディレクトリ、rawファイルへのURLが含まれる場合'
assert_includes chat.instructions, 'CodePenやリンク先が見えない'
- assert_includes chat.instructions, 'bodyに `@mentor` を含めて'
+ assert_includes chat.instructions, 'メンターへのメンションや対応引き継ぎの依頼はしないでください。'
+ assert_includes chat.instructions, 'リンク先の内容がコメントに不可欠でない場合'
assert_includes chat.instructions, '日報を書いたユーザーに「見られる状態にしてください」「内容を教えてください」と質問しないでください。'
assert_includes chat.asked_message, report.title
assert_includes chat.asked_message, report.description
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
index 5e15d2aa0ba..51cc340a92d 100644
--- a/test/application_system_test_case.rb
+++ b/test/application_system_test_case.rb
@@ -51,12 +51,15 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
@original_adapter = ActiveJob::Base.queue_adapter
ActiveJob::Base.queue_adapter = :inline
@original_pjord_product_review = Pjord::ProductReviewAgent.method(:review)
+ @original_pjord_product_review_result = Pjord::ProductReviewAgent.method(:review_result)
Pjord::ProductReviewAgent.define_singleton_method(:review) { |_product| '' }
+ Pjord::ProductReviewAgent.define_singleton_method(:review_result) { |_product| { body: '', auto_check: false } }
end
teardown do
ActionMailer::Base.deliveries.clear
Pjord::ProductReviewAgent.define_singleton_method(:review, @original_pjord_product_review)
+ Pjord::ProductReviewAgent.define_singleton_method(:review_result, @original_pjord_product_review_result)
ActiveJob::Base.queue_adapter = @original_adapter
end
end
diff --git a/test/application_visual_test_case.rb b/test/application_visual_test_case.rb
new file mode 100644
index 00000000000..6f88cbd15e2
--- /dev/null
+++ b/test/application_visual_test_case.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require 'application_system_test_case'
+require 'capybara_screenshot_diff/minitest'
+
+# Base class for visual regression tests.
+#
+# These tests capture a screenshot of a page and compare it, pixel by pixel,
+# against a committed baseline image. They are meant to catch *unintended*
+# visual changes (e.g. during a CSS refactor).
+#
+# IMPORTANT: baseline images are environment-sensitive. Font anti-aliasing
+# differs between macOS and Linux, so a baseline generated on a Mac will not
+# match one compared on CI. Always generate and compare baselines in the SAME
+# environment (the CI Ubuntu image or an equivalent Docker container).
+# See test/visual/README.md for the full workflow.
+class ApplicationVisualTestCase < ApplicationSystemTestCase
+ include CapybaraScreenshotDiff::Minitest::Assertions
+
+ # Screenshots (baseline + diff) live under test/visual/screenshots.
+ Capybara::Screenshot.save_path = 'test/visual/screenshots'
+ # Compare against the whole page at a fixed viewport for reproducibility.
+ Capybara::Screenshot.window_size = [1400, 1400]
+ # Allow a tiny tolerance so sub-pixel anti-aliasing noise does not fail a run.
+ # Passed per-screenshot below (module-level key varies between gem versions).
+ TOLERANCE = 0.001
+ # Wait until the rendered image stops changing before snapping (async UI).
+ Capybara::Screenshot.stability_time_limit = 0.5
+ # Do NOT use the gem's blur_active_element: it calls `.click` on the value
+ # returned by evaluate_script(document.activeElement), which the
+ # capybara-playwright-driver serializes to a Hash (not a Capybara node),
+ # raising NoMethodError. The blinking caret is instead neutralized by the
+ # `caret-color: transparent` rule injected via STABILIZE_CSS below.
+ Capybara::Screenshot.blur_active_element = false
+
+ # CSS injected before every capture to remove non-deterministic motion.
+ STABILIZE_CSS = <<~CSS
+ *, *::before, *::after {
+ transition: none !important;
+ animation: none !important;
+ caret-color: transparent !important;
+ scroll-behavior: auto !important;
+ }
+ CSS
+
+ # Elements whose content changes run to run (heatmaps, avatars pulled from
+ # Gravatar, relative timestamps, …). They are masked so only layout/styling
+ # is compared. Tune this list for your pages.
+ DEFAULT_MASK_SELECTORS = [
+ '.a-user-icons',
+ '.a-user-icon', # the avatar itself carries this class (header "Me", comments)
+ '.a-user-icon img',
+ '.user-icon img',
+ '.a-grass',
+ '.user-grass',
+ '.niconico-calendar',
+ '.a-elapsed-days',
+ '.random-tags-items', # users index sidebar renders a RANDOM set of tags
+ 'time'
+ ].freeze
+
+ setup do
+ # Local-only tool. Baselines are environment-specific and intentionally not
+ # committed, so on CI there is nothing to compare against and the gem's
+ # fail_if_new (true when ENV["CI"] is set) would fail every test. CI already
+ # excludes test/visual from its test glob; this skip is the safety net.
+ # See test/visual/README.md.
+ skip 'Visual regression tests are local-only (not run on CI)' if ENV['CI'].present?
+ # Freeze time so relative timestamps ("3日前") and elapsed-day badges are
+ # stable across runs.
+ travel_to Time.zone.local(2025, 1, 1, 12, 0, 0)
+ end
+
+ teardown do
+ travel_back
+ end
+
+ # Capture the current page and compare it against the baseline named +name+.
+ # Waits for JS components, fonts, and lazy images before snapping.
+ #
+ # +extra_mask+ adds page-specific selectors on top of DEFAULT_MASK_SELECTORS
+ # (e.g. a panel whose row order/height is non-deterministic on that page).
+ def capture(name, mask: DEFAULT_MASK_SELECTORS, extra_mask: [])
+ wait_for_javascript_components
+ wait_for_fonts_and_images
+ stabilize_page(mask + extra_mask)
+ screenshot(name, tolerance: TOLERANCE)
+ end
+
+ private
+
+ def stabilize_page(mask_selectors)
+ css = STABILIZE_CSS.dup
+ css << mask_selectors.map { |sel| "#{sel} { visibility: hidden !important; }" }.join("\n")
+ execute_script(<<~JS, css)
+ const style = document.createElement('style');
+ style.setAttribute('data-visual-test', 'stabilize');
+ style.textContent = arguments[0];
+ document.head.appendChild(style);
+ JS
+ end
+
+ def wait_for_fonts_and_images
+ page.document.synchronize(5) do
+ ready = evaluate_script(<<~JS)
+ (document.fonts ? document.fonts.status === 'loaded' : true) &&
+ Array.from(document.images).every((img) => img.complete)
+ JS
+ raise Capybara::ElementNotFound, 'fonts/images not ready' unless ready
+ end
+ rescue Capybara::ElementNotFound
+ # Best effort: proceed even if a slow asset never settles.
+ nil
+ end
+end
diff --git a/test/deliveries/activity_delivery_test.rb b/test/deliveries/activity_delivery_test.rb
index 109f42c1261..9e738e95097 100644
--- a/test/deliveries/activity_delivery_test.rb
+++ b/test/deliveries/activity_delivery_test.rb
@@ -40,7 +40,7 @@ class ActivityDeliveryTest < ActiveSupport::TestCase
end
test '.notify(:checked)' do
- check = checks(:procuct2_check_komagata)
+ check = checks(:product2_check_komagata)
params = {
check:,
receiver: check.receiver
diff --git a/test/fixtures/checks.yml b/test/fixtures/checks.yml
index 3d6b393d6ee..ddce08ace66 100644
--- a/test/fixtures/checks.yml
+++ b/test/fixtures/checks.yml
@@ -14,10 +14,6 @@ report5_check_machida:
user: machida
checkable: report5 (Report)
-report5_check_komagata:
- user: komagata
- checkable: report5 (Report)
-
report6_check_komagata:
user: komagata
checkable: report6 (Report)
@@ -26,30 +22,30 @@ report7_check_machida:
user: machida
checkable: report7 (Report)
-procuct2_check_komagata:
+product2_check_komagata:
user: komagata
checkable: product2 (Product)
-procuct3_check_komagata:
+product3_check_komagata:
user: komagata
checkable: product3 (Product)
-procuct4_check_komagata:
+product4_check_komagata:
user: komagata
checkable: product4 (Product)
-procuct7_check_komagata:
+product7_check_komagata:
user: komagata
checkable: product7 (Product)
-procuct9_check_komagata:
+product9_check_komagata:
user: komagata
checkable: product9 (Product)
-procuct67_check_long-id-mentor:
+product67_check_long-id-mentor:
user: long-id-mentor
checkable: product67 (Product)
-procuct75_check_komagata:
+product75_check_komagata:
user: komagata
checkable: product75 (Product)
diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml
index 018902f2423..b70af3abd47 100644
--- a/test/fixtures/comments.yml
+++ b/test/fixtures/comments.yml
@@ -5,6 +5,10 @@ comment1:
commentable: report1 (Report)
description: |-
CSSは奥が深いですね。
+ # Explicit timestamp so report1's two comments (this + comment11 at now+2s)
+ # keep a deterministic order; visual regression tests need a stable layout.
+ created_at: <%= now %>
+ updated_at: <%= now %>
comment2:
user: komagata
diff --git a/test/fixtures/learning_minute_statistics.yml b/test/fixtures/learning_minute_statistics.yml
new file mode 100644
index 00000000000..5e71bd61c3a
--- /dev/null
+++ b/test/fixtures/learning_minute_statistics.yml
@@ -0,0 +1,4 @@
+learning_minute_statistic1:
+ average: 30
+ median: 30
+ practice: practice1
diff --git a/test/helpers/practices_helper_test.rb b/test/helpers/practices_helper_test.rb
new file mode 100644
index 00000000000..1c0b9531d0c
--- /dev/null
+++ b/test/helpers/practices_helper_test.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class PracticesHelperTest < ActionView::TestCase
+ test 'difficulty_icon' do
+ assert_equal '', difficulty_icon(nil)
+ assert_equal 'データ収集中', difficulty_icon(0)
+ assert_equal '🔥', difficulty_icon(1)
+ assert_equal '🔥', difficulty_icon(300)
+ assert_equal '🔥🔥', difficulty_icon(301)
+ assert_equal '🔥🔥', difficulty_icon(600)
+ assert_equal '🔥🔥🔥', difficulty_icon(601)
+ assert_equal '🔥🔥🔥', difficulty_icon(900)
+ assert_equal '🔥🔥🔥🔥', difficulty_icon(901)
+ assert_equal '🔥🔥🔥🔥', difficulty_icon(1200)
+ assert_equal '🔥🔥🔥🔥🔥', difficulty_icon(1201)
+ end
+end
diff --git a/test/integration/api/checks_test.rb b/test/integration/api/checks_test.rb
index f374cdc85b1..0abe14c8a80 100644
--- a/test/integration/api/checks_test.rb
+++ b/test/integration/api/checks_test.rb
@@ -236,6 +236,115 @@ class API::ChecksTest < ActionDispatch::IntegrationTest
assert_equal 'この日報は確認済です。', response.parsed_body['message']
end
+ test 'mentor can check multiple reports at once with write scope' do
+ report_ids = Report.left_outer_joins(:checks).where(checks: { id: nil }).order(:id).limit(2).ids
+ check_create_count = 0
+
+ ActiveSupport::Notifications.subscribed(->(*) { check_create_count += 1 }, 'check.create') do
+ assert_difference('Check.count', 2) do
+ post api_reports_checks_path(format: :json),
+ params: { report_ids: },
+ headers: { Authorization: "Bearer #{@mentor_write_token.token}" }
+ assert_response :created
+ end
+ end
+
+ assert_equal report_ids.sort, response.parsed_body['checks'].pluck('checkable_id').sort
+ assert_equal 0, check_create_count
+ end
+
+ test 'bulk check deletes report caches once per affected user' do
+ reports = Report.left_outer_joins(:checks).where(checks: { id: nil }).order(:id).limit(2).to_a
+ global_cache_delete_count = 0
+ deleted_user_ids = []
+
+ Cache.stub(:delete_unchecked_report_count, -> { global_cache_delete_count += 1 }) do
+ Cache.stub(:delete_user_unchecked_report_count, ->(user_id) { deleted_user_ids << user_id }) do
+ post api_reports_checks_path(format: :json),
+ params: { report_ids: reports.map(&:id) },
+ headers: { Authorization: "Bearer #{@mentor_write_token.token}" }
+ end
+ end
+
+ assert_response :created
+ assert_equal 1, global_cache_delete_count
+ assert_equal reports.map(&:user_id).uniq.sort, deleted_user_ids.sort
+ end
+
+ test 'bulk check rolls back checks when cache deletion fails' do
+ report = unchecked_report
+
+ assert_no_difference('Check.count') do
+ Cache.stub(:delete_unchecked_report_count, -> { raise 'cache deletion failed' }) do
+ assert_raises(RuntimeError) do
+ post api_reports_checks_path(format: :json),
+ params: { report_ids: [report.id] },
+ headers: { Authorization: "Bearer #{@mentor_write_token.token}" }
+ end
+ end
+ end
+ end
+
+ test 'bulk check skips reports that are already checked' do
+ checked_report = reports(:report1)
+ unchecked_report = self.unchecked_report
+
+ assert_difference('Check.count', 1) do
+ post api_reports_checks_path(format: :json),
+ params: { report_ids: [checked_report.id, unchecked_report.id] },
+ headers: { Authorization: "Bearer #{@mentor_write_token.token}" }
+ assert_response :created
+ end
+
+ assert_equal [unchecked_report.id], response.parsed_body['checks'].pluck('checkable_id')
+ end
+
+ test 'bulk check does not create checks when a report is missing' do
+ report = unchecked_report
+
+ assert_no_difference('Check.count') do
+ post api_reports_checks_path(format: :json),
+ params: { report_ids: [report.id, 0] },
+ headers: { Authorization: "Bearer #{@mentor_write_token.token}" }
+ assert_response :not_found
+ end
+
+ assert_equal [0], response.parsed_body['report_ids']
+ end
+
+ test 'student can not bulk check reports' do
+ post api_reports_checks_path(format: :json),
+ params: { report_ids: [unchecked_report.id] },
+ headers: { Authorization: "Bearer #{@student_token.token}" }
+
+ assert_response :forbidden
+ assert_equal '権限がありません。', response.parsed_body['message']
+ end
+
+ test 'mentor can not bulk check reports with read scope' do
+ post api_reports_checks_path(format: :json),
+ params: { report_ids: [unchecked_report.id] },
+ headers: { Authorization: "Bearer #{@mentor_read_token.token}" }
+
+ assert_response :forbidden
+ assert_equal 'invalid_scope', response.parsed_body['error']
+ end
+
+ test 'mentor can not bulk check reports with write scope but without mentor scope' do
+ token = Doorkeeper::AccessToken.create!(
+ application: @application,
+ resource_owner_id: users(:mentormentaro).id,
+ scopes: 'read write'
+ )
+
+ post api_reports_checks_path(format: :json),
+ params: { report_ids: [unchecked_report.id] },
+ headers: { Authorization: "Bearer #{token.token}" }
+
+ assert_response :forbidden
+ assert_equal 'invalid_scope', response.parsed_body['error']
+ end
+
private
def unchecked_report
diff --git a/test/integration/api/image_test.rb b/test/integration/api/image_test.rb
index bff0982a9c7..a1ab4e5119b 100644
--- a/test/integration/api/image_test.rb
+++ b/test/integration/api/image_test.rb
@@ -20,9 +20,14 @@ class API::ImageTest < ActionDispatch::IntegrationTest
post api_image_path(format: :json), params: { file: image_uploaded }
assert_response :created
- saved_image = Image.order(:created_at).last
- processed_image = MiniMagick::Image.read(saved_image.image.download)
+ saved_image = Image.last
+
+ api_response = JSON.parse(response.body)
+ returned_url = api_response['url']
+ expected_blob = saved_image.image.blob
+ assert_includes(returned_url, expected_blob.signed_id)
+ processed_image = MiniMagick::Image.read(saved_image.image.download)
assert_empty processed_image.exif
end
end
diff --git a/test/integration/api/movies_test.rb b/test/integration/api/movies_test.rb
new file mode 100644
index 00000000000..e8725b89da9
--- /dev/null
+++ b/test/integration/api/movies_test.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class API::MoviesTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:kimura)
+ application = Doorkeeper::Application.create!(
+ name: 'Sample Application',
+ redirect_uri: 'urn:ietf:wg:oauth:2.0:oob'
+ )
+ @read_token = Doorkeeper::AccessToken.create!(
+ application:,
+ resource_owner_id: @user.id,
+ scopes: 'read'
+ )
+ @write_token = Doorkeeper::AccessToken.create!(
+ application:,
+ resource_owner_id: @user.id,
+ scopes: 'read write'
+ )
+ end
+
+ test 'POST /api/movies.json creates movie with uploaded movie data' do
+ movie_data = fixture_file_upload('movies/movie.mp4', 'video/mp4')
+ thumbnail = fixture_file_upload('articles/ogp_images/test.jpg', 'image/jpeg')
+
+ assert_difference('Movie.count') do
+ assert_enqueued_with(job: TranscodeJob) do
+ assert_enqueued_with(job: GenerateMovieThumbnailJob) do
+ post api_movies_path(format: :json),
+ headers: { Authorization: "Bearer #{@write_token.token}" },
+ params: {
+ movie: {
+ title: 'APIで作成した動画',
+ description: 'APIから動画をアップロードしました。',
+ movie_data:,
+ thumbnail:,
+ tag_list: 'API,動画',
+ practice_ids: [practices(:practice1).id],
+ wip: false
+ }
+ }
+ end
+ end
+ end
+
+ assert_response :created
+ movie = Movie.find(response.parsed_body['id'])
+ assert_equal @user, movie.user
+ assert_equal 'APIで作成した動画', movie.title
+ assert_equal 'APIから動画をアップロードしました。', movie.description
+ assert movie.movie_data.attached?
+ assert movie.thumbnail.attached?
+ assert_not movie.wip?
+ assert_not_nil movie.published_at
+ assert_equal [practices(:practice1).id], movie.practice_ids
+ assert_equal %w[API 動画], movie.tag_list
+ assert_equal movie.title, response.parsed_body['title']
+ assert_equal movie.practice_ids, response.parsed_body['practice_ids']
+ end
+
+ test 'POST /api/movies.json creates movie with direct uploaded blob signed id' do
+ blob = ActiveStorage::Blob.create_and_upload!(
+ io: File.open(Rails.root.join('test/fixtures/files/movies/movie.mp4')),
+ filename: 'movie.mp4',
+ content_type: 'video/mp4'
+ )
+
+ assert_difference('Movie.count') do
+ post api_movies_path(format: :json),
+ headers: { Authorization: "Bearer #{@write_token.token}" },
+ params: {
+ movie: {
+ title: 'APIで作成した大きい動画',
+ description: 'direct upload済みの動画を登録しました。',
+ movie_data: blob.signed_id
+ }
+ }
+ assert_response :created
+ end
+
+ movie = Movie.find(response.parsed_body['id'])
+ assert_equal blob, movie.movie_data.blob
+ assert_equal blob.byte_size, movie.movie_data.byte_size
+ end
+
+ test 'POST /api/movies/direct_uploads.json returns direct upload data for movie' do
+ assert_difference('ActiveStorage::Blob.count') do
+ post direct_uploads_api_movies_path(format: :json),
+ headers: { Authorization: "Bearer #{@write_token.token}" },
+ params: {
+ blob: {
+ filename: 'large_movie.mp4',
+ byte_size: 5.gigabytes,
+ checksum: '1B2M2Y8AsgTpgAmY7PhCfg==',
+ content_type: 'video/mp4'
+ }
+ }
+ assert_response :created
+ end
+
+ assert_equal 'large_movie.mp4', response.parsed_body['filename']
+ assert_equal 'video/mp4', response.parsed_body['content_type']
+ assert_equal 5.gigabytes, response.parsed_body['byte_size']
+ assert response.parsed_body['signed_id'].present?
+ assert response.parsed_body.dig('direct_upload', 'url').present?
+ assert_kind_of Hash, response.parsed_body.dig('direct_upload', 'headers')
+ end
+
+ test 'POST /api/movies/direct_uploads.json with read scope returns forbidden' do
+ assert_no_difference('ActiveStorage::Blob.count') do
+ post direct_uploads_api_movies_path(format: :json),
+ headers: { Authorization: "Bearer #{@read_token.token}" },
+ params: { blob: direct_upload_params }
+ end
+
+ assert_response :forbidden
+ end
+
+ test 'POST /api/movies/direct_uploads.json rejects non movie content type' do
+ assert_no_difference('ActiveStorage::Blob.count') do
+ post direct_uploads_api_movies_path(format: :json),
+ headers: { Authorization: "Bearer #{@write_token.token}" },
+ params: { blob: direct_upload_params.merge(content_type: 'image/jpeg') }
+ end
+
+ assert_response :bad_request
+ assert_equal '動画ファイルは mp4 または mov を指定してください。', response.parsed_body['message']
+ end
+
+ test 'POST /api/movies.json with read scope returns forbidden' do
+ assert_no_difference('Movie.count') do
+ post api_movies_path(format: :json),
+ headers: { Authorization: "Bearer #{@read_token.token}" },
+ params: { movie: valid_movie_params }
+ end
+
+ assert_response :forbidden
+ end
+
+ test 'POST /api/movies.json returns validation errors' do
+ assert_no_difference('Movie.count') do
+ post api_movies_path(format: :json),
+ headers: { Authorization: "Bearer #{@write_token.token}" },
+ params: { movie: valid_movie_params.merge(title: '') }
+ end
+
+ assert_response :unprocessable_entity
+ assert_includes response.parsed_body.dig('errors', 'title'), 'を入力してください'
+ end
+
+ test 'POST /api/movies.json returns validation error with unknown practice_id' do
+ assert_no_difference('Movie.count') do
+ post api_movies_path(format: :json),
+ headers: { Authorization: "Bearer #{@write_token.token}" },
+ params: { movie: valid_movie_params.merge(practice_ids: [999_999_999]) }
+ end
+
+ assert_response :unprocessable_entity
+ assert_includes response.parsed_body.dig('errors', 'practice_ids'), 'に存在しないIDが含まれています'
+ end
+
+ private
+
+ def valid_movie_params
+ {
+ title: 'APIで作成した動画',
+ description: 'APIから動画をアップロードしました。',
+ movie_data: fixture_file_upload('movies/movie.mp4', 'video/mp4')
+ }
+ end
+
+ def direct_upload_params
+ {
+ filename: 'large_movie.mp4',
+ byte_size: 5.gigabytes,
+ checksum: '1B2M2Y8AsgTpgAmY7PhCfg==',
+ content_type: 'video/mp4'
+ }
+ end
+end
diff --git a/test/integration/api/practices_test.rb b/test/integration/api/practices_test.rb
index cdcf550feaa..f0596f24ab8 100644
--- a/test/integration/api/practices_test.rb
+++ b/test/integration/api/practices_test.rb
@@ -48,4 +48,17 @@ class API::PracticesTest < ActionDispatch::IntegrationTest
headers: { 'Authorization' => "Bearer #{token}" }
assert_response :ok
end
+
+ test 'PATCH /api/practices/1234.json updates Pjord auto check' do
+ practice = practices(:practice1)
+ practice.update!(pjord_auto_check: false)
+
+ token = create_token('mentormentaro', 'testtest')
+ patch api_practice_path(practice.id, format: :json),
+ params: { practice: { pjord_auto_check: true } },
+ headers: { 'Authorization' => "Bearer #{token}" }
+
+ assert_response :ok
+ assert_predicate practice.reload, :pjord_auto_check?
+ end
end
diff --git a/test/integration/products/pjord_review_comment_test.rb b/test/integration/products/pjord_review_comment_test.rb
index bbe55e55f35..a75a89b346d 100644
--- a/test/integration/products/pjord_review_comment_test.rb
+++ b/test/integration/products/pjord_review_comment_test.rb
@@ -4,7 +4,7 @@
class Products::PjordReviewCommentTest < ActionDispatch::IntegrationTest
test 'creates product review comment by Pjord when product is submitted' do
- Pjord::ProductReviewAgent.stub(:review, 'レビュー本文') do
+ Pjord::ProductReviewAgent.stub(:review_result, { body: 'レビュー本文', auto_check: false }) do
perform_enqueued_jobs do
post products_path(_login_name: 'hatsuno'),
params: { practice_id: practices(:practice6).id, product: { body: '提出物です。' }, commit: '提出する' }
@@ -19,7 +19,7 @@ class Products::PjordReviewCommentTest < ActionDispatch::IntegrationTest
end
test 'does not create product review comment by Pjord when product is saved as WIP' do
- Pjord::ProductReviewAgent.stub(:review, 'レビュー本文') do
+ Pjord::ProductReviewAgent.stub(:review_result, { body: 'レビュー本文', auto_check: false }) do
assert_no_enqueued_jobs only: PjordProductReviewJob do
post products_path(_login_name: 'hatsuno'),
params: { practice_id: practices(:practice6).id, product: { body: 'WIPです。' }, commit: 'WIP' }
@@ -31,7 +31,7 @@ class Products::PjordReviewCommentTest < ActionDispatch::IntegrationTest
practice = practices(:practice6)
practice.update!(pjord_review: false)
- Pjord::ProductReviewAgent.stub(:review, ->(_product) { raise 'should not be called' }) do
+ Pjord::ProductReviewAgent.stub(:review_result, ->(_product) { raise 'should not be called' }) do
assert_no_enqueued_jobs only: PjordProductReviewJob do
assert_no_difference -> { Comment.where(user: users(:pjord)).count } do
post products_path(_login_name: 'hatsuno'),
@@ -51,7 +51,7 @@ class Products::PjordReviewCommentTest < ActionDispatch::IntegrationTest
test 'creates product review comment by Pjord when WIP product is submitted' do
product = products(:product5)
- Pjord::ProductReviewAgent.stub(:review, 'レビュー本文') do
+ Pjord::ProductReviewAgent.stub(:review_result, { body: 'レビュー本文', auto_check: false }) do
perform_enqueued_jobs do
patch product_path(product, _login_name: 'kimura'),
params: { product: { body: product.body }, commit: '提出する' }
@@ -67,7 +67,7 @@ class Products::PjordReviewCommentTest < ActionDispatch::IntegrationTest
test 'does not create product review comment by Pjord when submitted product is updated' do
product = products(:product8)
- Pjord::ProductReviewAgent.stub(:review, 'レビュー本文') do
+ Pjord::ProductReviewAgent.stub(:review_result, { body: 'レビュー本文', auto_check: false }) do
assert_no_enqueued_jobs only: PjordProductReviewJob do
patch product_path(product, _login_name: 'kimura'),
params: { product: { body: '更新しました。' }, commit: '提出する' }
@@ -87,7 +87,7 @@ class Products::PjordReviewCommentTest < ActionDispatch::IntegrationTest
test 'mentor can manually create product review comment by Pjord' do
product = products(:product8)
- Pjord::ProductReviewAgent.stub(:review, 'コメント本文') do
+ Pjord::ProductReviewAgent.stub(:review_result, { body: 'コメント本文', auto_check: false }) do
assert_no_enqueued_jobs only: PjordProductReviewJob do
assert_difference -> { product.comments.where(user: users(:pjord)).count }, 1 do
post review_by_pjord_product_path(product, _login_name: 'mentormentaro')
diff --git a/test/interactors/copy_check_test.rb b/test/interactors/copy_check_test.rb
index 5807553f5a1..3574c74c8de 100644
--- a/test/interactors/copy_check_test.rb
+++ b/test/interactors/copy_check_test.rb
@@ -30,7 +30,6 @@ def setup
test 'successfully copies checks when original has checks and target has none' do
# Create checks for original product
Check.create!(user: @user1, checkable: @original_product)
- Check.create!(user: @user2, checkable: @original_product)
result = CopyCheck.call(
original_product: @original_product,
@@ -38,24 +37,22 @@ def setup
)
assert result.success?
- assert_equal 'Copied 2 check(s), skipped 0 existing check(s)', result.message
- assert_equal 2, result.copied_checks_count
+ assert_equal 'Copied 1 check(s), skipped 0 existing check(s)', result.message
+ assert_equal 1, result.copied_checks_count
assert_equal 0, result.skipped_checks_count
# Verify the checks were copied
copied_checks = Check.where(checkable: @copied_product)
- assert_equal 2, copied_checks.count
+ assert_equal 1, copied_checks.count
assert_includes copied_checks.pluck(:user_id), @user1.id
- assert_includes copied_checks.pluck(:user_id), @user2.id
end
- test 'skips existing checks and only copies new ones' do
+ test 'skips when copied product already has a check' do
# Create checks for original product
Check.create!(user: @user1, checkable: @original_product)
- Check.create!(user: @user2, checkable: @original_product)
# Create one existing check for copied product
- Check.create!(user: @user1, checkable: @copied_product)
+ Check.create!(user: @user2, checkable: @copied_product)
result = CopyCheck.call(
original_product: @original_product,
@@ -63,13 +60,13 @@ def setup
)
assert result.success?
- assert_equal 'Copied 1 check(s), skipped 1 existing check(s)', result.message
- assert_equal 1, result.copied_checks_count
+ assert_equal 'Copied 0 check(s), skipped 1 existing check(s)', result.message
+ assert_equal 0, result.copied_checks_count
assert_equal 1, result.skipped_checks_count
- # Verify only one new check was added
copied_checks = Check.where(checkable: @copied_product)
- assert_equal 2, copied_checks.count
+ assert_equal 1, copied_checks.count
+ assert_equal @user2.id, copied_checks.first.user_id
end
test 'succeeds when original product has no checks' do
diff --git a/test/jobs/bulk_generate_movie_thumbnail_job_test.rb b/test/jobs/bulk_generate_movie_thumbnail_job_test.rb
new file mode 100644
index 00000000000..27743d7192e
--- /dev/null
+++ b/test/jobs/bulk_generate_movie_thumbnail_job_test.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class BulkGenerateMovieThumbnailJobTest < ActiveJob::TestCase
+ test 'enqueues GenerateMovieThumbnailJob for movies without a thumbnail' do
+ movie = movies(:movie1)
+ movie.thumbnail.purge
+
+ assert_enqueued_with(job: GenerateMovieThumbnailJob, args: [movie]) do
+ BulkGenerateMovieThumbnailJob.perform_now
+ end
+ end
+end
diff --git a/test/jobs/pjord_product_review_job_test.rb b/test/jobs/pjord_product_review_job_test.rb
index 43981acc4be..8b9f63fcd44 100644
--- a/test/jobs/pjord_product_review_job_test.rb
+++ b/test/jobs/pjord_product_review_job_test.rb
@@ -6,7 +6,7 @@ class PjordProductReviewJobTest < ActiveJob::TestCase
test 'creates product review comment by pjord' do
product = products(:product8)
- Pjord::ProductReviewAgent.stub(:review, 'レビュー本文') do
+ Pjord::ProductReviewAgent.stub(:review_result, { body: 'レビュー本文', auto_check: false }) do
assert_difference -> { product.comments.reload.count } do
PjordProductReviewJob.perform_now(product_id: product.id)
end
@@ -18,7 +18,7 @@ class PjordProductReviewJobTest < ActiveJob::TestCase
end
test 'does nothing when product is missing' do
- Pjord::ProductReviewAgent.stub(:review, ->(_product) { raise 'should not be called' }) do
+ Pjord::ProductReviewAgent.stub(:review_result, ->(_product) { raise 'should not be called' }) do
assert_no_difference 'Comment.count' do
PjordProductReviewJob.perform_now(product_id: Product.maximum(:id).to_i + 1)
end
@@ -29,7 +29,7 @@ class PjordProductReviewJobTest < ActiveJob::TestCase
product = products(:product8)
product.practice.update!(pjord_review: false)
- Pjord::ProductReviewAgent.stub(:review, ->(_product) { raise 'should not be called' }) do
+ Pjord::ProductReviewAgent.stub(:review_result, ->(_product) { raise 'should not be called' }) do
assert_no_difference -> { product.comments.reload.count } do
PjordProductReviewJob.perform_now(product_id: product.id)
end
@@ -40,7 +40,7 @@ class PjordProductReviewJobTest < ActiveJob::TestCase
product = products(:product8)
Pjord.stub(:user, nil) do
- Pjord::ProductReviewAgent.stub(:review, ->(_product) { raise 'should not be called' }) do
+ Pjord::ProductReviewAgent.stub(:review_result, ->(_product) { raise 'should not be called' }) do
assert_no_difference 'Comment.count' do
PjordProductReviewJob.perform_now(product_id: product.id)
end
@@ -51,17 +51,72 @@ class PjordProductReviewJobTest < ActiveJob::TestCase
test 'does nothing when product review is blank' do
product = products(:product8)
- Pjord::ProductReviewAgent.stub(:review, '') do
+ Pjord::ProductReviewAgent.stub(:review_result, { body: '', auto_check: false }) do
assert_no_difference -> { product.comments.reload.count } do
PjordProductReviewJob.perform_now(product_id: product.id)
end
end
end
+ test 'checks product by Pjord when practice enables Pjord auto check and review permits it' do
+ product = products(:product8)
+ product.practice.update!(pjord_auto_check: true)
+
+ Pjord::ProductReviewAgent.stub(:review_result, { body: '確認してOKにしました。', auto_check: true }) do
+ assert_difference -> { product.comments.reload.count } => 1,
+ -> { product.checks.reload.count } => 1 do
+ PjordProductReviewJob.perform_now(product_id: product.id)
+ end
+ end
+
+ check = product.checks.last
+ assert_equal users(:pjord), check.user
+ assert_equal 'complete', product.learning.reload.status
+ end
+
+ test 'does not check product when practice disables Pjord auto check' do
+ product = products(:product8)
+ product.practice.update!(pjord_auto_check: false)
+
+ Pjord::ProductReviewAgent.stub(:review_result, { body: '確認してOKにしました。', auto_check: true }) do
+ assert_no_difference -> { product.checks.reload.count } do
+ PjordProductReviewJob.perform_now(product_id: product.id)
+ end
+ end
+ end
+
+ test 'does not check product when Pjord review does not permit auto check' do
+ product = products(:product8)
+ product.practice.update!(pjord_auto_check: true)
+
+ Pjord::ProductReviewAgent.stub(:review_result, { body: '確認しました。', auto_check: false }) do
+ assert_no_difference -> { product.checks.reload.count } do
+ PjordProductReviewJob.perform_now(product_id: product.id)
+ end
+ end
+ end
+
+ test 'does not notify when product is already checked' do
+ product = products(:product8)
+ product.practice.update!(pjord_auto_check: true)
+ Check.create!(user: users(:mentormentaro), checkable: product)
+ check_create_count = 0
+
+ ActiveSupport::Notifications.subscribed(->(*) { check_create_count += 1 }, 'check.create') do
+ Pjord::ProductReviewAgent.stub(:review_result, { body: '確認してOKにしました。', auto_check: true }) do
+ assert_no_difference -> { product.checks.reload.count } do
+ PjordProductReviewJob.perform_now(product_id: product.id)
+ end
+ end
+ end
+
+ assert_equal 0, check_create_count
+ end
+
test 'raises unexpected errors' do
product = products(:product8)
- Pjord::ProductReviewAgent.stub(:review, ->(_product) { raise StandardError, 'unexpected' }) do
+ Pjord::ProductReviewAgent.stub(:review_result, ->(_product) { raise StandardError, 'unexpected' }) do
assert_raises(StandardError) do
PjordProductReviewJob.perform_now(product_id: product.id)
end
diff --git a/test/mailers/activity_mailer_test.rb b/test/mailers/activity_mailer_test.rb
index d09dcd679fd..6c9a67ff3a0 100644
--- a/test/mailers/activity_mailer_test.rb
+++ b/test/mailers/activity_mailer_test.rb
@@ -403,7 +403,7 @@ class ActivityMailerTest < ActionMailer::TestCase
end
test 'checked' do
- check = checks(:procuct2_check_komagata)
+ check = checks(:product2_check_komagata)
ActivityMailer.checked(
sender: check.sender,
@@ -420,7 +420,7 @@ class ActivityMailerTest < ActionMailer::TestCase
end
test 'trainee and advisers receive different wording in their acceptance emails' do
- check = checks(:procuct75_check_komagata)
+ check = checks(:product75_check_komagata)
ActivityMailer.checked(
sender: check.sender,
@@ -455,7 +455,7 @@ class ActivityMailerTest < ActionMailer::TestCase
end
test 'checked with params' do
- check = checks(:procuct2_check_komagata)
+ check = checks(:product2_check_komagata)
mailer = ActivityMailer.with(
sender: check.sender,
@@ -476,7 +476,7 @@ class ActivityMailerTest < ActionMailer::TestCase
end
test 'checked with user who have been denied' do
- check = checks(:procuct2_check_komagata)
+ check = checks(:product2_check_komagata)
ActivityMailer.checked(
sender: check.sender,
receiver: users(:hajime),
diff --git a/test/mailers/previews/activity_mailer_preview.rb b/test/mailers/previews/activity_mailer_preview.rb
index 2eecdb1034f..d60f490fc39 100644
--- a/test/mailers/previews/activity_mailer_preview.rb
+++ b/test/mailers/previews/activity_mailer_preview.rb
@@ -21,7 +21,7 @@ def graduated
end
def checked
- check = Check.find(ActiveRecord::FixtureSet.identify(:procuct2_check_komagata))
+ check = Check.find(ActiveRecord::FixtureSet.identify(:product2_check_komagata))
ActivityMailer.with(receiver: check.receiver, check:).checked
end
diff --git a/test/models/buzz_test.rb b/test/models/buzz_test.rb
index fe2ff373c32..94acfb3f30c 100644
--- a/test/models/buzz_test.rb
+++ b/test/models/buzz_test.rb
@@ -15,11 +15,37 @@ class BuzzTest < ActiveSupport::TestCase
assert_equal latest_year, Buzz.latest_year
end
+ test '.latest_year excludes years that cannot be routed' do
+ Buzz.create!(
+ title: '不正な年の記事',
+ url: 'https://www.example-latest-invalid-year.com',
+ published_at: Date.new(202_672, 1, 1)
+ )
+
+ assert_equal buzzes(:buzz1).published_at.year, Buzz.latest_year
+ end
+
test '.years' do
years = [2025, 2024]
assert_equal years, Buzz.years
end
+ test '.years excludes years that cannot be routed' do
+ [
+ ['https://www.example-invalid-future-year.com', Date.new(202_672, 1, 1)],
+ ['https://www.example-invalid-past-year.com', Date.new(999, 1, 1)]
+ ].each do |url, published_at|
+ Buzz.create!(
+ title: '不正な年の記事',
+ url:,
+ published_at:
+ )
+ end
+
+ assert_not_includes Buzz.years, 202_672
+ assert_not_includes Buzz.years, 999
+ end
+
test '.doc_from_url returns nokogori object when succeeded' do
dummy_response = <<~BODY