From 2aeed956944119cdd8ee253c75ab3628b4569509 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Mon, 13 Apr 2026 10:48:26 +0900 Subject: [PATCH 01/80] =?UTF-8?q?Exif=E5=89=8A=E9=99=A4=E3=81=AE=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=82=92=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9=E3=82=AA?= =?UTF-8?q?=E3=83=96=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E3=81=AB=E7=A7=BB?= =?UTF-8?q?=E5=8B=95=E3=81=97=E3=80=81=E6=B0=B8=E7=B6=9A=E5=8C=96=E5=89=8D?= =?UTF-8?q?=E3=81=AB=E5=AE=9F=E8=A1=8C=E3=81=95=E3=82=8C=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/image_controller.rb | 4 +++- app/models/exif_stripper.rb | 14 ++++++++++++++ app/models/image.rb | 20 -------------------- 3 files changed, 17 insertions(+), 21 deletions(-) create mode 100644 app/models/exif_stripper.rb diff --git a/app/controllers/api/image_controller.rb b/app/controllers/api/image_controller.rb index cea930a86fe..d33a04f4808 100644 --- a/app/controllers/api/image_controller.rb +++ b/app/controllers/api/image_controller.rb @@ -2,7 +2,9 @@ class API::ImageController < API::BaseController def create - @image = Image.new(user: current_user, image: params[:file]) + @image = Image.new(user: current_user) + processed_image = ExifStripper.call(params[:file]) + @image.image.attach(processed_image) if @image.save render :create, status: :created diff --git a/app/models/exif_stripper.rb b/app/models/exif_stripper.rb new file mode 100644 index 00000000000..2e95ea54d53 --- /dev/null +++ b/app/models/exif_stripper.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ExifStripper + def self.call(uploaded_file) + image = MiniMagick::Image.read(uploaded_file.tempfile) + image.strip + + { + io: StringIO.new(image.to_blob), + filename: uploaded_file.original_filename.to_s, + content_type: uploaded_file.content_type + } + end +end diff --git a/app/models/image.rb b/app/models/image.rb index 9fcf7220427..d7a73613377 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -3,24 +3,4 @@ class Image < ApplicationRecord belongs_to :user has_one_attached :image - - after_commit :strip_exif, on: :create - - 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! - end end From cc80049139b187ed2602696b1537f7694a6c5520 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Mon, 13 Apr 2026 10:53:49 +0900 Subject: [PATCH 02/80] =?UTF-8?q?API=E3=81=8C=E8=BF=94=E3=81=97=E3=81=9FUR?= =?UTF-8?q?L=E3=81=8C=E6=AD=A3=E3=81=97=E3=81=84blob=E3=82=92=E6=8C=87?= =?UTF-8?q?=E3=81=97=E3=81=A6=E3=81=84=E3=82=8B=E3=81=93=E3=81=A8=E3=82=92?= =?UTF-8?q?=E7=A2=BA=E8=AA=8D=E3=81=99=E3=82=8B=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/api/image_test.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 From a6cd548ac3ae622a1d2ed211f927efcc7a5c0246 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Tue, 14 Apr 2026 14:17:32 +0900 Subject: [PATCH 03/80] =?UTF-8?q?ExifStripper=E3=81=AEcall=E3=83=A1?= =?UTF-8?q?=E3=82=BD=E3=83=83=E3=83=89=E3=81=8CExif=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4=E3=81=99=E3=82=8B=E3=81=93=E3=81=A8?= =?UTF-8?q?=E3=82=92=E6=A4=9C=E8=A8=BC=E3=81=99=E3=82=8B=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/models/exif_stripper_test.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 test/models/exif_stripper_test.rb diff --git a/test/models/exif_stripper_test.rb b/test/models/exif_stripper_test.rb new file mode 100644 index 00000000000..9e241fc3ab9 --- /dev/null +++ b/test/models/exif_stripper_test.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ExifStripperTest < ActiveSupport::TestCase + include ActionDispatch::TestProcess::FixtureFile + test '#call strips exif data from an uploaded image' do + file_path = Rails.root.join('test/fixtures/files/articles/ogp_images/test.jpg') + original_image = MiniMagick::Image.open(file_path) + + assert_not_empty original_image.exif + + uploaded_file = fixture_file_upload(file_path, 'image/jpeg') + processed_file = ExifStripper.call(uploaded_file) + processed_image = MiniMagick::Image.read(processed_file[:io].read) + + assert_empty processed_image.exif + end +end From 1c44e110786936658980141ea17e6d109b50d048 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Mon, 20 Apr 2026 18:33:57 +0900 Subject: [PATCH 04/80] =?UTF-8?q?EXIF=E5=89=8A=E9=99=A4=E3=83=AD=E3=82=B8?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=82=92=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9?= =?UTF-8?q?=E3=82=AF=E3=83=A9=E3=82=B9=E3=81=8B=E3=82=89Image=E3=83=A2?= =?UTF-8?q?=E3=83=87=E3=83=AB=E3=81=AB=E7=A7=BB=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/image_controller.rb | 3 +-- app/models/exif_stripper.rb | 14 -------------- app/models/image.rb | 13 +++++++++++++ test/models/exif_stripper_test.rb | 19 ------------------- 4 files changed, 14 insertions(+), 35 deletions(-) delete mode 100644 app/models/exif_stripper.rb delete mode 100644 test/models/exif_stripper_test.rb diff --git a/app/controllers/api/image_controller.rb b/app/controllers/api/image_controller.rb index d33a04f4808..f3fc6425398 100644 --- a/app/controllers/api/image_controller.rb +++ b/app/controllers/api/image_controller.rb @@ -3,8 +3,7 @@ class API::ImageController < API::BaseController def create @image = Image.new(user: current_user) - processed_image = ExifStripper.call(params[:file]) - @image.image.attach(processed_image) + @image.strip_exif(params[:file]) if @image.save render :create, status: :created diff --git a/app/models/exif_stripper.rb b/app/models/exif_stripper.rb deleted file mode 100644 index 2e95ea54d53..00000000000 --- a/app/models/exif_stripper.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -class ExifStripper - def self.call(uploaded_file) - image = MiniMagick::Image.read(uploaded_file.tempfile) - image.strip - - { - io: StringIO.new(image.to_blob), - filename: uploaded_file.original_filename.to_s, - content_type: uploaded_file.content_type - } - end -end diff --git a/app/models/image.rb b/app/models/image.rb index d7a73613377..a75995394c4 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -3,4 +3,17 @@ class Image < ApplicationRecord belongs_to :user has_one_attached :image + + def strip_exif(uploaded_file) + original_image = MiniMagick::Image.read(uploaded_file.tempfile) + original_image.strip + + blob = { + io: StringIO.new(original_image.to_blob), + filename: uploaded_file.original_filename.to_s, + content_type: uploaded_file.content_type + } + + image.attach(blob) + end end diff --git a/test/models/exif_stripper_test.rb b/test/models/exif_stripper_test.rb deleted file mode 100644 index 9e241fc3ab9..00000000000 --- a/test/models/exif_stripper_test.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -class ExifStripperTest < ActiveSupport::TestCase - include ActionDispatch::TestProcess::FixtureFile - test '#call strips exif data from an uploaded image' do - file_path = Rails.root.join('test/fixtures/files/articles/ogp_images/test.jpg') - original_image = MiniMagick::Image.open(file_path) - - assert_not_empty original_image.exif - - uploaded_file = fixture_file_upload(file_path, 'image/jpeg') - processed_file = ExifStripper.call(uploaded_file) - processed_image = MiniMagick::Image.read(processed_file[:io].read) - - assert_empty processed_image.exif - end -end From c8c142db1ede811d499e0da1f3f08a9b90015c11 Mon Sep 17 00:00:00 2001 From: matsuyama Date: Mon, 18 May 2026 18:06:17 +0900 Subject: [PATCH 05/80] =?UTF-8?q?EXIF=E6=83=85=E5=A0=B1=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=82=92=E5=BC=95=E6=95=B0=E3=81=AA=E3=81=97=E3=81=A7=E8=A1=8C?= =?UTF-8?q?=E3=81=84=E3=83=A2=E3=83=87=E3=83=AB=E5=86=85=E3=81=A7=E5=AE=8C?= =?UTF-8?q?=E7=B5=90=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/image_controller.rb | 4 ++-- app/models/image.rb | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/image_controller.rb b/app/controllers/api/image_controller.rb index f3fc6425398..745c4aa6803 100644 --- a/app/controllers/api/image_controller.rb +++ b/app/controllers/api/image_controller.rb @@ -2,8 +2,8 @@ class API::ImageController < API::BaseController def create - @image = Image.new(user: current_user) - @image.strip_exif(params[:file]) + @image = Image.new(user: current_user, file: params[:file]) + @image.strip_exif! if @image.save render :create, status: :created diff --git a/app/models/image.rb b/app/models/image.rb index a75995394c4..fa0a85e6adb 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -4,7 +4,10 @@ class Image < ApplicationRecord belongs_to :user has_one_attached :image - def strip_exif(uploaded_file) + attr_accessor :file + + def strip_exif! + uploaded_file = file original_image = MiniMagick::Image.read(uploaded_file.tempfile) original_image.strip From 263dd6d7af33671bae37cab6e74aa283b0fd4fdc Mon Sep 17 00:00:00 2001 From: matsuyama Date: Mon, 18 May 2026 18:32:28 +0900 Subject: [PATCH 06/80] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E6=9C=AA=E6=8C=87=E5=AE=9A=E6=99=82=E3=81=AB=E6=97=A9=E6=9C=9F?= =?UTF-8?q?=E3=83=AA=E3=82=BF=E3=83=BC=E3=83=B3=E3=82=92=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/image.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/image.rb b/app/models/image.rb index fa0a85e6adb..26ba52d65bb 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -7,6 +7,8 @@ class Image < ApplicationRecord attr_accessor :file def strip_exif! + return if file.blank? + uploaded_file = file original_image = MiniMagick::Image.read(uploaded_file.tempfile) original_image.strip From 574f89da9e4fcb130e941964421165c9ddab626a Mon Sep 17 00:00:00 2001 From: matsuyama Date: Mon, 18 May 2026 18:54:08 +0900 Subject: [PATCH 07/80] =?UTF-8?q?=E6=B7=BB=E4=BB=98=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=81=AE=E5=AD=98=E5=9C=A8=E3=83=90=E3=83=AA?= =?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/image.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/image.rb b/app/models/image.rb index 26ba52d65bb..08d8a7bad02 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -4,6 +4,8 @@ class Image < ApplicationRecord belongs_to :user has_one_attached :image + validates :image, attached: true + attr_accessor :file def strip_exif! From 21fd7816200b8635a7a76bb0a4e3c64a95d03a0f Mon Sep 17 00:00:00 2001 From: matsuyama Date: Tue, 19 May 2026 06:18:19 +0900 Subject: [PATCH 08/80] =?UTF-8?q?=E3=82=A2=E3=83=83=E3=83=97=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=83=89=E7=94=BB=E5=83=8F=E3=81=AE=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E3=82=92attr=5Faccessor=E3=81=8B=E3=82=89att?= =?UTF-8?q?achment=5Fchanges=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/image_controller.rb | 2 +- app/models/image.rb | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/image_controller.rb b/app/controllers/api/image_controller.rb index 745c4aa6803..9b9df6cc20e 100644 --- a/app/controllers/api/image_controller.rb +++ b/app/controllers/api/image_controller.rb @@ -2,7 +2,7 @@ class API::ImageController < API::BaseController def create - @image = Image.new(user: current_user, file: params[:file]) + @image = Image.new(user: current_user, image: params[:file]) @image.strip_exif! if @image.save diff --git a/app/models/image.rb b/app/models/image.rb index 08d8a7bad02..2f9d75e71b4 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -6,12 +6,11 @@ class Image < ApplicationRecord validates :image, attached: true - attr_accessor :file - def strip_exif! - return if file.blank? + attachment = attachment_changes['image'] + return unless attachment - uploaded_file = file + uploaded_file = attachment.attachable original_image = MiniMagick::Image.read(uploaded_file.tempfile) original_image.strip From ae3013b629e6bf3a34ce3329c3c9b00ec2ba8cfd Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Sat, 13 Jun 2026 21:00:03 +0900 Subject: [PATCH 09/80] =?UTF-8?q?Re=E3=82=B9=E3=82=AD=E3=83=AB=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AF?= =?UTF-8?q?=E9=80=9A=E5=B8=B8=E3=82=B3=E3=83=BC=E3=82=B9=E3=81=AE=E7=9D=80?= =?UTF-8?q?=E6=89=8B=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92=E3=82=AF=E3=83=AA?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=81=95=E3=81=9B=E3=81=AA=E3=81=84=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/practices/_learning-status.html.slim | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/practices/_learning-status.html.slim b/app/views/practices/_learning-status.html.slim index 68ad356638f..766425a1811 100644 --- a/app/views/practices/_learning-status.html.slim +++ b/app/views/practices/_learning-status.html.slim @@ -8,9 +8,10 @@ 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.source_id.present? || !current_user.grant_course? + 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'}" From 08451b68432429ed28ef370bb4c253d2cc364e00 Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Sat, 13 Jun 2026 21:13:33 +0900 Subject: [PATCH 10/80] =?UTF-8?q?Re=E3=82=B9=E3=82=AD=E3=83=AB=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AF?= =?UTF-8?q?=E9=80=9A=E5=B8=B8=E3=82=B3=E3=83=BC=E3=82=B9=E3=81=AE=E6=8F=90?= =?UTF-8?q?=E5=87=BA=E7=89=A9=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92=E3=82=AF?= =?UTF-8?q?=E3=83=AA=E3=83=83=E3=82=AF=E3=81=95=E3=81=9B=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/learnings/learning_component.html.slim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/learnings/learning_component.html.slim b/app/components/learnings/learning_component.html.slim index b85985f2794..021008a5e53 100644 --- a/app/components/learnings/learning_component.html.slim +++ b/app/components/learnings/learning_component.html.slim @@ -1,6 +1,6 @@ .card-main-actions ul.card-main-actions__items - - if submission? + - if submission? && @practice.source_id.present? || !@current_user.grant_course? 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 From 7363cd120ec00bb31ac81c52063ea8e17095ba34 Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Mon, 15 Jun 2026 09:21:07 +0900 Subject: [PATCH 11/80] =?UTF-8?q?Re=E3=82=B9=E3=82=AD=E3=83=AB=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=8C?= =?UTF-8?q?=E9=80=9A=E5=B8=B8=E3=83=97=E3=83=A9=E3=82=AF=E3=83=86=E3=82=A3?= =?UTF-8?q?=E3=82=B9=E3=81=AB=E9=81=B7=E7=A7=BB=E3=81=97=E3=81=9F=E3=81=A8?= =?UTF-8?q?=E3=81=8D=E3=81=AB=E6=8F=90=E5=87=BA=E7=89=A9=E3=83=9C=E3=82=BF?= =?UTF-8?q?=E3=83=B3=E3=82=92=E6=8A=BC=E3=81=9B=E3=81=AA=E3=81=84=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=97=E3=80=81=E4=BB=A3=E3=82=8F=E3=82=8A?= =?UTF-8?q?=E3=81=ABRe=E3=82=B9=E3=82=AD=E3=83=AB=E3=83=97=E3=83=A9?= =?UTF-8?q?=E3=82=AF=E3=83=86=E3=82=A3=E3=82=B9=E3=81=AB=E9=81=B7=E7=A7=BB?= =?UTF-8?q?=E3=81=95=E3=81=9B=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97?= =?UTF-8?q?=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/learnings/learning_component.html.slim | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/components/learnings/learning_component.html.slim b/app/components/learnings/learning_component.html.slim index 021008a5e53..02b014f9149 100644 --- a/app/components/learnings/learning_component.html.slim +++ b/app/components/learnings/learning_component.html.slim @@ -5,6 +5,10 @@ = link_to product_link, class: 'a-button is-sm is-primary is-block test-product' do i.fa-solid.fa-file | #{product_label} + - else + li.card-main-actions__item + = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm is-block' do + | Reスキルコースへ遷移する - if @practice.completed?(@current_user) li.card-main-actions__item button.a-button.is-sm.is-secondary.is-block.is-disabled.test-completed From 8848b483f08098b14773d23a48d3f62bbfe27026 Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Thu, 18 Jun 2026 20:13:21 +0900 Subject: [PATCH 12/80] =?UTF-8?q?Re=E3=82=B9=E3=82=AD=E3=83=AB=E3=81=8B?= =?UTF-8?q?=E3=82=89=E7=B5=A6=E4=BB=98=E9=87=91=E3=81=AB=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=A8=E3=80=81=E3=82=B9=E3=83=86=E3=83=BC=E3=82=BF=E3=82=B9?= =?UTF-8?q?=E3=82=92=E7=B5=A6=E4=BB=98=E9=87=91=E3=82=B3=E3=83=BC=E3=82=B9?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AE=E5=A0=B4=E5=90=88?= =?UTF-8?q?=E3=81=AF=E3=82=B9=E3=83=86=E3=83=BC=E3=82=BF=E3=82=B9=E3=82=92?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=81=97=E3=80=81=E5=85=83=E3=83=97=E3=83=A9?= =?UTF-8?q?=E3=82=AF=E3=83=86=E3=82=A3=E3=82=B9=E3=81=AE=E6=99=82=E3=81=AF?= =?UTF-8?q?=E7=B5=A6=E4=BB=98=E9=87=91=E3=83=97=E3=83=A9=E3=82=AF=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=82=B9=E3=81=B8=E9=81=B7=E7=A7=BB=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92?= =?UTF-8?q?=E8=A8=AD=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../learnings/learning_component.html.slim | 2 +- .../practices/_learning-status.html.slim | 37 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/app/components/learnings/learning_component.html.slim b/app/components/learnings/learning_component.html.slim index 02b014f9149..69c7387408c 100644 --- a/app/components/learnings/learning_component.html.slim +++ b/app/components/learnings/learning_component.html.slim @@ -8,7 +8,7 @@ - else li.card-main-actions__item = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm is-block' do - | Reスキルコースへ遷移する + | 給付金コースへ遷移する - if @practice.completed?(@current_user) li.card-main-actions__item button.a-button.is-sm.is-secondary.is-block.is-disabled.test-completed diff --git a/app/views/practices/_learning-status.html.slim b/app/views/practices/_learning-status.html.slim index 766425a1811..2c1476932b0 100644 --- a/app/views/practices/_learning-status.html.slim +++ b/app/views/practices/_learning-status.html.slim @@ -1,24 +1,27 @@ .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' } - - if @practice.source_id.present? || !current_user.grant_course? + - if @practice.source_id.present? || !current_user.grant_course? + .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-started js-started #{@status == 'started' ? 'is-active' : 'is-inactive'}" - = content_tag :button, '着手', class: button_class, disabled: @status == 'started', data: { status: 'started' } - - if @practice.submission + - 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-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' } + - 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' } + - else + = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm' do + | 給付金コースへ遷移する - if !@practice.submission .practice-status-buttons__end .practice-status-buttons__note From 0215b961b877776636cb51502cfe48bbc398153e Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Thu, 18 Jun 2026 21:40:10 +0900 Subject: [PATCH 13/80] =?UTF-8?q?=E7=B5=A6=E4=BB=98=E9=87=91=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=8C?= =?UTF-8?q?=E5=85=83=E3=83=97=E3=83=A9=E3=82=AF=E3=83=86=E3=82=A3=E3=82=B9?= =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AB=E6=9D=A5=E3=81=9F=E6=99=82?= =?UTF-8?q?=E3=80=81=E4=BF=AE=E4=BA=86=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92?= =?UTF-8?q?=E9=9D=9E=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../learnings/learning_component.html.slim | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/components/learnings/learning_component.html.slim b/app/components/learnings/learning_component.html.slim index 69c7387408c..4479bb9c02f 100644 --- a/app/components/learnings/learning_component.html.slim +++ b/app/components/learnings/learning_component.html.slim @@ -5,17 +5,17 @@ = 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 + | 修了 - else li.card-main-actions__item = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm is-block' do | 給付金コースへ遷移する - - 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 - | 修了 From dcb8be0fd2e5f1f41d736a528a903eb9ec1f524c Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Fri, 19 Jun 2026 21:16:16 +0900 Subject: [PATCH 14/80] =?UTF-8?q?=E7=B5=82=E4=BA=86=E3=81=AE=E3=81=BF?= =?UTF-8?q?=E3=81=AE=E5=A0=B4=E5=90=88=E3=81=AE=E3=82=B9=E3=83=86=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E3=82=B9=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/practices/_learning-status.html.slim | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/practices/_learning-status.html.slim b/app/views/practices/_learning-status.html.slim index 2c1476932b0..d836f4ba2be 100644 --- a/app/views/practices/_learning-status.html.slim +++ b/app/views/practices/_learning-status.html.slim @@ -19,10 +19,11 @@ 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 + | このプラクティスに提出物はありません。修了条件をクリアしたら修了にしてください。 - else = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm' do | 給付金コースへ遷移する -- if !@practice.submission - .practice-status-buttons__end - .practice-status-buttons__note - | このプラクティスに提出物はありません。修了条件をクリアしたら修了にしてください。 + From 6e68c554a113525c55aaf8f41d0f1dc88366df76 Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Sun, 21 Jun 2026 16:39:43 +0900 Subject: [PATCH 15/80] =?UTF-8?q?=E6=8F=90=E5=87=BA=E7=89=A9=E3=83=9C?= =?UTF-8?q?=E3=82=BF=E3=83=B3=E3=81=AE=E7=AE=87=E6=89=80=E3=82=92=E7=B5=A6?= =?UTF-8?q?=E4=BB=98=E9=87=91=E3=82=B3=E3=83=BC=E3=82=B9=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=81=AF=E5=85=83=E3=83=97=E3=83=A9=E3=82=AF?= =?UTF-8?q?=E3=83=86=E3=82=A3=E3=82=B9=E3=81=A7=E3=81=AF=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=81=95=E3=81=9B=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/learnings/learning_component.html.slim | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/components/learnings/learning_component.html.slim b/app/components/learnings/learning_component.html.slim index 4479bb9c02f..426f0b2efa6 100644 --- a/app/components/learnings/learning_component.html.slim +++ b/app/components/learnings/learning_component.html.slim @@ -1,6 +1,10 @@ .card-main-actions ul.card-main-actions__items - - if submission? && @practice.source_id.present? || !@current_user.grant_course? + - if @current_user.grant_course? && @practice.source_id.blank? + li.card-main-actions__item + = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm is-block' do + | 給付金コースへ遷移する + - else 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 @@ -15,7 +19,3 @@ = 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 | 修了 - - else - li.card-main-actions__item - = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm is-block' do - | 給付金コースへ遷移する From 5d495804c06a0a163fc1ec1ee6000ed8976320cd Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Sun, 21 Jun 2026 21:59:57 +0900 Subject: [PATCH 16/80] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E3=82=B9=E3=81=AEli=E3=82=BF=E3=82=B0=E3=81=AE=E5=85=A5?= =?UTF-8?q?=E3=82=8C=E5=AD=90=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/practices/_learning-status.html.slim | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/views/practices/_learning-status.html.slim b/app/views/practices/_learning-status.html.slim index d836f4ba2be..2c0bbc17683 100644 --- a/app/views/practices/_learning-status.html.slim +++ b/app/views/practices/_learning-status.html.slim @@ -9,9 +9,9 @@ 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' } + 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'}" @@ -26,4 +26,3 @@ - else = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm' do | 給付金コースへ遷移する - From 6bad9e1c2dd9224bc4016c275c7fe49b2834a724 Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Sun, 21 Jun 2026 23:17:56 +0900 Subject: [PATCH 17/80] =?UTF-8?q?=E6=8F=90=E5=87=BA=E3=81=8C=E5=BF=85?= =?UTF-8?q?=E8=A6=81=E3=81=AA=E3=81=84=E6=99=82=E3=81=AF=E7=B5=82=E4=BA=86?= =?UTF-8?q?=E3=81=AE=E3=81=BF=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/learnings/learning_component.html.slim | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/components/learnings/learning_component.html.slim b/app/components/learnings/learning_component.html.slim index 426f0b2efa6..da05a595e6c 100644 --- a/app/components/learnings/learning_component.html.slim +++ b/app/components/learnings/learning_component.html.slim @@ -5,10 +5,11 @@ = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm is-block' do | 給付金コースへ遷移する - else - 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.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 From dff06975fc4b80ad232e4bfff81293695f22d1f0 Mon Sep 17 00:00:00 2001 From: karlley Date: Thu, 16 Apr 2026 06:03:30 +0900 Subject: [PATCH 18/80] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=AF=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=82=B9=E3=81=AE=E9=9B=A3=E6=98=93=E5=BA=A6=E3=82=92?= =?UTF-8?q?=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=81=A7=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practice/_category-practices-item.css | 18 ++++++++++++++++++ app/helpers/practices_helper.rb | 12 ++++++++++++ .../practices/_courses_practice.html.slim | 12 +++++++----- 3 files changed, 37 insertions(+), 5 deletions(-) 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..c6a9b9fbf1a 100644 --- a/app/assets/stylesheets/application/blocks/practice/_category-practices-item.css +++ b/app/assets/stylesheets/application/blocks/practice/_category-practices-item.css @@ -169,3 +169,21 @@ a.category-practices-item__title-link:hover .category-practices-item__title-link margin-top: .5rem; } } + +.category-practices-item__practice-difficulty { + font-size: .75rem; + line-height: 1.4; + color: var(--muted-text); +} + +@media (min-width: 48em) { + .category-practices-item__practice-difficulty { + margin-top: -.25rem; + } +} + +@media (max-width: 47.9375em) { + .category-practices-item__practice-difficulty { + margin-top: .5rem; + } +} diff --git a/app/helpers/practices_helper.rb b/app/helpers/practices_helper.rb index 08219f7695c..ee236bc8660 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) + return '' if minutes.nil? + + case minutes + 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/views/courses/practices/_courses_practice.html.slim b/app/views/courses/practices/_courses_practice.html.slim index 9ce1f519ec5..4892266e541 100644 --- a/app/views/courses/practices/_courses_practice.html.slim +++ b/app/views/courses/practices/_courses_practice.html.slim @@ -19,11 +19,13 @@ 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? + .category-practices-item__practice-difficulty + | 難易度: #{difficulty_icon(learning_statistic.median)} + .category-practices-item__learning-time.is-only-mentor + | 所要時間の目安: #{convert_to_hour_minute(learning_statistic.median)} + | (平均: #{convert_to_hour_minute(learning_statistic.average)}) - if practice.started_or_submitted_students.present? .a-user-icons .a-user-icons__items From 44eb371e1393be46aba268ba87b76e7cda03acb0 Mon Sep 17 00:00:00 2001 From: karlley Date: Thu, 7 May 2026 05:25:21 +0900 Subject: [PATCH 19/80] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=AF=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=82=B9=E3=81=AE=E9=9B=A3=E6=98=93=E5=BA=A6=E3=82=A2?= =?UTF-8?q?=E3=82=A4=E3=82=B3=E3=83=B3=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/fixtures/learning_minute_statistics.yml | 4 ++++ test/helpers/practices_helper_test.rb | 18 ++++++++++++++++ test/system/course/practices_test.rb | 22 ++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 test/fixtures/learning_minute_statistics.yml create mode 100644 test/helpers/practices_helper_test.rb 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..405a1bca93c --- /dev/null +++ b/test/helpers/practices_helper_test.rb @@ -0,0 +1,18 @@ +# 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(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/system/course/practices_test.rb b/test/system/course/practices_test.rb index 063a72a3704..9899adec541 100644 --- a/test/system/course/practices_test.rb +++ b/test/system/course/practices_test.rb @@ -42,4 +42,26 @@ class Course::PracticesTest < ApplicationSystemTestCase assert_text 'OS X Mountain Lionをクリーンインストールする' end end + + test 'difficulty icon is displayed for all users' do + visit_with_auth course_practices_path(courses(:course1).id), 'kimura' + assert_selector '.category-practices-item__practice-difficulty', text: '難易度:' + logout + visit_with_auth course_practices_path(courses(:course1).id), 'komagata' + assert_selector '.category-practices-item__practice-difficulty', text: '難易度:' + logout + visit_with_auth course_practices_path(courses(:course1).id), 'mentormentaro' + assert_selector '.category-practices-item__practice-difficulty', text: '難易度:' + end + + test 'learning time is displayed only for admin and mentor' do + visit_with_auth course_practices_path(courses(:course1).id), 'kimura' + assert_no_selector '.category-practices-item__learning-time.is-only-mentor', text: '所要時間の目安:' + logout + visit_with_auth course_practices_path(courses(:course1).id), 'komagata' + assert_selector '.category-practices-item__learning-time.is-only-mentor', text: '所要時間の目安:' + logout + visit_with_auth course_practices_path(courses(:course1).id), 'mentormentaro' + assert_selector '.category-practices-item__learning-time.is-only-mentor', text: '所要時間の目安:' + end end From 256c04354bf8a28f5a926d929d37f743253de4ff Mon Sep 17 00:00:00 2001 From: machida Date: Thu, 21 May 2026 17:02:56 +0900 Subject: [PATCH 20/80] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=AF=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=82=B9=E3=81=AE=E9=9B=A3=E6=98=93=E5=BA=A6=E3=83=BB?= =?UTF-8?q?=E6=89=80=E8=A6=81=E6=99=82=E9=96=93=E3=81=AE=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=82=92=E5=85=B1=E9=80=9A=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=8D=E3=83=B3=E3=83=88=E3=81=AB=E7=BD=AE=E3=81=8D=E6=8F=9B?= =?UTF-8?q?=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practice/_category-practices-item.css | 36 ------------------- .../practices/_courses_practice.html.slim | 29 ++++++++++++--- test/system/course/practices_test.rb | 12 +++---- 3 files changed, 30 insertions(+), 47 deletions(-) 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 c6a9b9fbf1a..fe8a6d024fd 100644 --- a/app/assets/stylesheets/application/blocks/practice/_category-practices-item.css +++ b/app/assets/stylesheets/application/blocks/practice/_category-practices-item.css @@ -151,39 +151,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; - } -} - -.category-practices-item__practice-difficulty { - font-size: .75rem; - line-height: 1.4; - color: var(--muted-text); -} - -@media (min-width: 48em) { - .category-practices-item__practice-difficulty { - margin-top: -.25rem; - } -} - -@media (max-width: 47.9375em) { - .category-practices-item__practice-difficulty { - margin-top: .5rem; - } -} diff --git a/app/views/courses/practices/_courses_practice.html.slim b/app/views/courses/practices/_courses_practice.html.slim index 4892266e541..3d947c0579b 100644 --- a/app/views/courses/practices/_courses_practice.html.slim +++ b/app/views/courses/practices/_courses_practice.html.slim @@ -21,11 +21,30 @@ - learning_statistic = practice.learning_minute_statistic - if learning_statistic.present? - .category-practices-item__practice-difficulty - | 難易度: #{difficulty_icon(learning_statistic.median)} - .category-practices-item__learning-time.is-only-mentor - | 所要時間の目安: #{convert_to_hour_minute(learning_statistic.median)} - | (平均: #{convert_to_hour_minute(learning_statistic.average)}) + .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/test/system/course/practices_test.rb b/test/system/course/practices_test.rb index 9899adec541..7d34a67ec84 100644 --- a/test/system/course/practices_test.rb +++ b/test/system/course/practices_test.rb @@ -45,23 +45,23 @@ class Course::PracticesTest < ApplicationSystemTestCase test 'difficulty icon is displayed for all users' do visit_with_auth course_practices_path(courses(:course1).id), 'kimura' - assert_selector '.category-practices-item__practice-difficulty', text: '難易度:' + assert_selector '.card-list-item-meta__item .a-meta__label', text: '難易度' logout visit_with_auth course_practices_path(courses(:course1).id), 'komagata' - assert_selector '.category-practices-item__practice-difficulty', text: '難易度:' + assert_selector '.card-list-item-meta__item .a-meta__label', text: '難易度' logout visit_with_auth course_practices_path(courses(:course1).id), 'mentormentaro' - assert_selector '.category-practices-item__practice-difficulty', text: '難易度:' + assert_selector '.card-list-item-meta__item .a-meta__label', text: '難易度' end test 'learning time is displayed only for admin and mentor' do visit_with_auth course_practices_path(courses(:course1).id), 'kimura' - assert_no_selector '.category-practices-item__learning-time.is-only-mentor', text: '所要時間の目安:' + assert_no_selector '.card-list-item-meta__item.is-only-mentor .a-meta__label', text: '所要時間の目安' logout visit_with_auth course_practices_path(courses(:course1).id), 'komagata' - assert_selector '.category-practices-item__learning-time.is-only-mentor', text: '所要時間の目安:' + assert_selector '.card-list-item-meta__item.is-only-mentor .a-meta__label', text: '所要時間の目安' logout visit_with_auth course_practices_path(courses(:course1).id), 'mentormentaro' - assert_selector '.category-practices-item__learning-time.is-only-mentor', text: '所要時間の目安:' + assert_selector '.card-list-item-meta__item.is-only-mentor .a-meta__label', text: '所要時間の目安' end end From f72059f5aea220bc48578c459582bb29de5ffc67 Mon Sep 17 00:00:00 2001 From: karlley Date: Thu, 25 Jun 2026 18:03:31 +0900 Subject: [PATCH 21/80] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AAEarly=20Return?= =?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/practices_helper.rb | 4 +--- test/helpers/practices_helper_test.rb | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/helpers/practices_helper.rb b/app/helpers/practices_helper.rb index ee236bc8660..82231c695f4 100644 --- a/app/helpers/practices_helper.rb +++ b/app/helpers/practices_helper.rb @@ -27,9 +27,7 @@ def practice_ogp_meta_tags(image_url) end def difficulty_icon(minutes) - return '' if minutes.nil? - - case minutes + case minutes.to_i # nilの場合は0として扱う(nil.to_i == 0) when ..300 then '🔥' # 5時間以下 when ..600 then '🔥🔥' # 10時間以下 when ..900 then '🔥🔥🔥' # 15時間以下 diff --git a/test/helpers/practices_helper_test.rb b/test/helpers/practices_helper_test.rb index 405a1bca93c..d8dee19af14 100644 --- a/test/helpers/practices_helper_test.rb +++ b/test/helpers/practices_helper_test.rb @@ -4,7 +4,6 @@ class PracticesHelperTest < ActionView::TestCase test 'difficulty_icon' do - assert_equal '', difficulty_icon(nil) assert_equal '🔥', difficulty_icon(0) assert_equal '🔥', difficulty_icon(300) assert_equal '🔥🔥', difficulty_icon(301) From daf3b07b1fc4cd32d03ae52017d07bbc49d95f18 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 1 Jul 2026 17:26:12 +0900 Subject: [PATCH 22/80] Remove unused CSS files (dead BEM blocks) Delete 12 stylesheets whose classes are not referenced anywhere in views, components, or JavaScript, and drop their @import lines. No visual change. Co-Authored-By: Claude Opus 4.8 --- app/assets/stylesheets/application.css | 6 --- .../auth-form/_next-settlement-time.css | 28 ------------ .../application/blocks/page/_o-page-info.css | 7 --- .../blocks/practice/_completion-massage.css | 3 -- .../practice/_practice-content-actions.css | 45 ------------------- .../blocks/thread/_thread-form.css | 43 ------------------ .../blocks/user/_active-practices-list.css | 41 ----------------- .../stylesheets/atoms/_a-hidden-scrollbar.css | 8 ---- .../atoms/_a-horizontal-dashed.css | 5 --- .../atoms/_a-textarea-bottom-note.css | 30 ------------- app/assets/stylesheets/common-imports.css | 4 -- app/assets/stylesheets/lp.css | 1 - .../lp/blocks/lp/_lp-supplementary-info.css | 18 -------- .../card-list/_card-list-item-tags-edit.css | 19 -------- .../shared/blocks/form/_many-check-boxes.css | 29 ------------ 15 files changed, 287 deletions(-) delete mode 100644 app/assets/stylesheets/application/blocks/auth-form/_next-settlement-time.css delete mode 100644 app/assets/stylesheets/application/blocks/page/_o-page-info.css delete mode 100644 app/assets/stylesheets/application/blocks/practice/_completion-massage.css delete mode 100644 app/assets/stylesheets/application/blocks/practice/_practice-content-actions.css delete mode 100644 app/assets/stylesheets/application/blocks/thread/_thread-form.css delete mode 100644 app/assets/stylesheets/application/blocks/user/_active-practices-list.css delete mode 100644 app/assets/stylesheets/atoms/_a-hidden-scrollbar.css delete mode 100644 app/assets/stylesheets/atoms/_a-horizontal-dashed.css delete mode 100644 app/assets/stylesheets/atoms/_a-textarea-bottom-note.css delete mode 100644 app/assets/stylesheets/lp/blocks/lp/_lp-supplementary-info.css delete mode 100644 app/assets/stylesheets/shared/blocks/card-list/_card-list-item-tags-edit.css delete mode 100644 app/assets/stylesheets/shared/blocks/form/_many-check-boxes.css diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 7a041441082..6db5e6a3e37 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"; @@ -63,8 +61,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"; @@ -94,10 +90,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"; 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/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/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/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/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-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..1a84402ea3f 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"; @@ -107,7 +105,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 +129,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/lp.css b/app/assets/stylesheets/lp.css index 8ced09ebb34..fd6b234e5e8 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"; 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/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/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; -} From 523b101bf26f08c3ef9746bc5afa2a6ab71c3524 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 1 Jul 2026 17:28:35 +0900 Subject: [PATCH 23/80] Remove leftover sourceMappingURL comments Drop dead /*# sourceMappingURL=*.map */ build artifacts pointing at map files that do not exist in the repository. No visual change. Co-Authored-By: Claude Opus 4.8 --- .../stylesheets/application/blocks/survey/survey-result.css | 2 -- app/assets/stylesheets/config/variables/_css-variables.css | 2 -- app/assets/stylesheets/form/_zip-tel-input.css | 2 -- app/assets/stylesheets/modules/_github.css | 2 -- app/assets/stylesheets/shared/blocks/_code-toolbar.css | 2 -- app/assets/stylesheets/shared/blocks/_prism-ghcolors.css | 2 -- 6 files changed, 12 deletions(-) diff --git a/app/assets/stylesheets/application/blocks/survey/survey-result.css b/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/config/variables/_css-variables.css b/app/assets/stylesheets/config/variables/_css-variables.css index cb7fde9394e..ccc41d69e4c 100644 --- a/app/assets/stylesheets/config/variables/_css-variables.css +++ b/app/assets/stylesheets/config/variables/_css-variables.css @@ -86,5 +86,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/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/_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 */ From 2c98ec7ff2faeb904c90bd1ac33f118e99a85056 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 1 Jul 2026 21:26:45 +0900 Subject: [PATCH 24/80] Replace hardcoded colors with matching CSS variables Swap literal color values for the existing :root variables that hold the identical value (e.g. #5752e8 -> var(--primary), semantic button variant colors -> var(--info/--success/--warning/--danger/--main), LP nav -> var(--main)/var(--border-tint)). Values are unchanged, so rendering is identical; also drops a now-redundant duplicate declaration in the LP header nav. Co-Authored-By: Claude Opus 4.8 --- app/assets/stylesheets/atoms/_a-button.css | 68 +++++++++---------- .../stylesheets/atoms/_a-copy-button.css | 2 +- .../lp/blocks/lp/_lp-header-nav.css | 2 - 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/app/assets/stylesheets/atoms/_a-button.css b/app/assets/stylesheets/atoms/_a-button.css index e550061127b..1210773cca3 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,8 +170,8 @@ input:checked + .a-button.is-checkbox::after { } .a-button.is-secondary { - background-color: hsl(242deg 1% 100%); - color: hsl(242deg 10% 30%); + background-color: var(--secondary); + color: var(--default-text); border-color: hsl(242deg 3% 88%); border-color: var(--input-border); } @@ -192,9 +192,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 +203,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 +213,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 +224,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 +234,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 +245,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 +255,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 +266,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 +276,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 +287,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 +297,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 +308,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 +318,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 +329,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,7 +346,7 @@ input:checked + .a-button.is-checkbox::after { } .a-button.is-muted-bordered { - border-color: hsl(242deg 7% 89%); + border-color: var(--border); color: hsl(242deg 5% 64%); font-weight: 400 !important; background-color: white; @@ -359,8 +359,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 +379,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 +407,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, 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/lp/blocks/lp/_lp-header-nav.css b/app/assets/stylesheets/lp/blocks/lp/_lp-header-nav.css index c314b061864..a90567079fb 100644 --- a/app/assets/stylesheets/lp/blocks/lp/_lp-header-nav.css +++ b/app/assets/stylesheets/lp/blocks/lp/_lp-header-nav.css @@ -52,7 +52,6 @@ } .lp-header-nav__item { - border-bottom: solid 1px hsl(242deg 14% 95%); border-bottom: solid 1px var(--border-tint); } } @@ -60,7 +59,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; From 970bb0cb090971023cb6d3f480ee64dd057e3c63 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 1 Jul 2026 21:34:00 +0900 Subject: [PATCH 25/80] Remove dead literal fallbacks overridden by var() Delete legacy `prop: ` declarations that are immediately overridden by an identical `prop: var(--x)` on the next line. The var() declaration always wins in browsers that support custom properties (which this modern-CSS codebase already requires), so rendering is unchanged. Co-Authored-By: Claude Opus 4.8 --- app/assets/stylesheets/atoms/_a-button.css | 1 - app/assets/stylesheets/lp/blocks/lp/_lp-header-nav.css | 3 --- 2 files changed, 4 deletions(-) diff --git a/app/assets/stylesheets/atoms/_a-button.css b/app/assets/stylesheets/atoms/_a-button.css index 1210773cca3..199b4d7a384 100644 --- a/app/assets/stylesheets/atoms/_a-button.css +++ b/app/assets/stylesheets/atoms/_a-button.css @@ -172,7 +172,6 @@ input:checked + .a-button.is-checkbox::after { .a-button.is-secondary { background-color: var(--secondary); color: var(--default-text); - border-color: hsl(242deg 3% 88%); border-color: var(--input-border); } 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 a90567079fb..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,7 +45,6 @@ top: 0; bottom: 0; z-index: 2; - background-color: white; background-color: var(--base); width: 20rem; max-width: calc(100vw - 3rem); @@ -76,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); } } From 4e513628262cc1d15d79c557210d3639a7837397 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 1 Jul 2026 21:49:03 +0900 Subject: [PATCH 26/80] Normalize survey partial filenames with leading underscore Rename survey-answer/survey-answers/survey-result.css to the _-prefixed partial convention used by every other imported stylesheet, and update their @import paths. No visual change. Co-Authored-By: Claude Opus 4.8 --- app/assets/stylesheets/application.css | 6 +++--- .../blocks/survey/{survey-answer.css => _survey-answer.css} | 0 .../survey/{survey-answers.css => _survey-answers.css} | 0 .../blocks/survey/{survey-result.css => _survey-result.css} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename app/assets/stylesheets/application/blocks/survey/{survey-answer.css => _survey-answer.css} (100%) rename app/assets/stylesheets/application/blocks/survey/{survey-answers.css => _survey-answers.css} (100%) rename app/assets/stylesheets/application/blocks/survey/{survey-result.css => _survey-result.css} (100%) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6db5e6a3e37..f3bf36a58c3 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -80,9 +80,9 @@ @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"; 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 100% rename from app/assets/stylesheets/application/blocks/survey/survey-result.css rename to app/assets/stylesheets/application/blocks/survey/_survey-result.css From 81f54a66f7742c0e582d5e18414d2e6ea86b459c Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 1 Jul 2026 22:07:16 +0900 Subject: [PATCH 27/80] Remove unused CSS custom properties Drop --placeholder-text and the --welcome-blue/-light-blue/-yellow/ -orange palette variables, none of which are referenced anywhere. Unused declarations render nothing, so there is no visual change. Co-Authored-By: Claude Opus 4.8 --- app/assets/stylesheets/config/variables/_css-variables.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/assets/stylesheets/config/variables/_css-variables.css b/app/assets/stylesheets/config/variables/_css-variables.css index ccc41d69e4c..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%); From a8f288a32c2d10d496e49c4a1873a8a863864992 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 1 Jul 2026 22:57:38 +0900 Subject: [PATCH 28/80] =?UTF-8?q?=E7=B5=A6=E4=BB=98=E9=87=91=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=81=B8=E3=81=AE=E7=A7=BB=E5=8B=95=E5=B0=8E?= =?UTF-8?q?=E7=B7=9A=E3=82=92=E8=AA=BF=E6=95=B4=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practice/_practice-status-buttons.css | 20 +++++++++++++++++++ .../learnings/learning_component.html.slim | 17 +++++++++------- .../practices/_learning-status.html.slim | 8 ++++++-- app/views/practices/show.html.slim | 4 ++-- test/system/practices_test.rb | 15 ++++++++++++++ 5 files changed, 53 insertions(+), 11 deletions(-) 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..91bc51aea23 100644 --- a/app/assets/stylesheets/application/blocks/practice/_practice-status-buttons.css +++ b/app/assets/stylesheets/application/blocks/practice/_practice-status-buttons.css @@ -120,3 +120,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/components/learnings/learning_component.html.slim b/app/components/learnings/learning_component.html.slim index da05a595e6c..f872bd7ee68 100644 --- a/app/components/learnings/learning_component.html.slim +++ b/app/components/learnings/learning_component.html.slim @@ -1,10 +1,13 @@ -.card-main-actions - ul.card-main-actions__items - - if @current_user.grant_course? && @practice.source_id.blank? - li.card-main-actions__item - = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm is-block' do - | 給付金コースへ遷移する - - else +- if @current_user.grant_course? && @practice.source_id.blank? + .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 diff --git a/app/views/practices/_learning-status.html.slim b/app/views/practices/_learning-status.html.slim index 2c0bbc17683..824e1f05ded 100644 --- a/app/views/practices/_learning-status.html.slim +++ b/app/views/practices/_learning-status.html.slim @@ -24,5 +24,9 @@ .practice-status-buttons__note | このプラクティスに提出物はありません。修了条件をクリアしたら修了にしてください。 - else - = link_to practice_path(@practice.copied_practices.first || @practice), class: 'a-button is-sm' do - | 給付金コースへ遷移する + .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 + | 進捗のステータス変更は、給付金コースのプラクティス側で行ってください。 diff --git a/app/views/practices/show.html.slim b/app/views/practices/show.html.slim index ac034000e37..4d0dc666e39 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.source_id.blank? && current_user.grant_course?) .card-footer__alert = link_to '提出の前に、提出時の注意点を確認しよう', 'https://bootcamp.fjord.jp/pages/info-for-product', @@ -134,7 +134,7 @@ | 提出物を作成し提出し、メンターから確認をもらったら br | このプラクティスを修了にしてください。 - - else + - elsif !(@practice.source_id.blank? && current_user.grant_course?) .card-footer__description | このプラクティスに提出物はありません。 br diff --git a/test/system/practices_test.rb b/test/system/practices_test.rb index de6021650ec..23f1b2159f8 100644 --- a/test/system/practices_test.rb +++ b/test/system/practices_test.rb @@ -114,4 +114,19 @@ def wait_for_status_change assert_text 'rubyをインストールする' assert_no_link '給付金コース' end + + test 'show status guidance link on source practice for grant course user' do + source_practice = practices(:practice23) + + visit_with_auth practice_path(source_practice), 'grant-course' + + assert_link '給付金コースへ移動する', href: practice_path(practices(:practice64)) + assert_text '進捗のステータス変更は、給付金コースのプラクティス側で行ってください。' + assert_text '提出・修了は、給付金コースのプラクティス側で行ってください。' + assert_no_text '提出の前に、提出時の注意点を確認しよう' + assert_no_text '提出物を作成し提出し、メンターから確認をもらったら' + assert_no_text 'このプラクティスを修了にしてください。' + assert_no_text 'このプラクティスに提出物はありません。' + assert_no_text '修了条件をクリアしたら修了にしてください。' + end end From 986243a36bd20e61788f95af69ade2e50b48deff Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Wed, 1 Jul 2026 22:40:33 +0900 Subject: [PATCH 29/80] =?UTF-8?q?=E5=8B=95=E7=94=BB=E3=82=A2=E3=83=83?= =?UTF-8?q?=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89API=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/movies_controller.rb | 80 ++++++++++- config/routes/api.rb | 4 +- docs/openapi.yaml | 83 +++++++++++ public/api-docs/openapi.yaml | 83 +++++++++++ test/integration/api/movies_test.rb | 171 +++++++++++++++++++++++ 5 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 test/integration/api/movies_test.rb diff --git a/app/controllers/api/movies_controller.rb b/app/controllers/api/movies_controller.rb index 862634e0f5c..b58f531dbc8 100644 --- a/app/controllers/api/movies_controller.rb +++ b/app/controllers/api/movies_controller.rb @@ -1,12 +1,34 @@ # 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 + 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 +41,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/config/routes/api.rb b/config/routes/api.rb index c3e5b0f57ec..e2ac4975e5a 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -97,7 +97,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/docs/openapi.yaml b/docs/openapi.yaml index 7eda55440d2..2c4919da57c 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1140,6 +1140,89 @@ 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 + properties: + movie: + type: object + required: + - title + - description + - movie_data + properties: + title: + type: string + description: + type: string + movie_data: + oneOf: + - type: string + format: binary + - type: string + description: Signed blob ID returned by /api/movies/direct_uploads. + thumbnail: + type: string + format: binary + tag_list: + type: string + wip: + type: boolean + practice_ids: + type: array + items: + type: integer + responses: + '201': + $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' /api/movies/{id}: patch: tags: [Content] diff --git a/public/api-docs/openapi.yaml b/public/api-docs/openapi.yaml index 7eda55440d2..2c4919da57c 100644 --- a/public/api-docs/openapi.yaml +++ b/public/api-docs/openapi.yaml @@ -1140,6 +1140,89 @@ 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 + properties: + movie: + type: object + required: + - title + - description + - movie_data + properties: + title: + type: string + description: + type: string + movie_data: + oneOf: + - type: string + format: binary + - type: string + description: Signed blob ID returned by /api/movies/direct_uploads. + thumbnail: + type: string + format: binary + tag_list: + type: string + wip: + type: boolean + practice_ids: + type: array + items: + type: integer + responses: + '201': + $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' /api/movies/{id}: patch: tags: [Content] diff --git a/test/integration/api/movies_test.rb b/test/integration/api/movies_test.rb new file mode 100644 index 00000000000..695184bd3c8 --- /dev/null +++ b/test/integration/api/movies_test.rb @@ -0,0 +1,171 @@ +# 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 + + 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 From 736a839a48691b12be41aba1d3a0d0a3aa4f833d Mon Sep 17 00:00:00 2001 From: machida Date: Thu, 2 Jul 2026 11:34:46 +0900 Subject: [PATCH 30/80] Remove empty CSS rulesets Delete rulesets with no declarations across 35 stylesheets. Empty rules produce no output (the compiled CSS is byte-identical before and after), so there is no visual change. Co-Authored-By: Claude Opus 4.8 --- .../blocks/auth-form/_auth-form.css | 4 ---- .../application/blocks/cards/_card-counts.css | 4 ---- .../application/blocks/event/_event-meta.css | 7 ------- .../application/blocks/footer/_footer-nav.css | 4 ---- .../blocks/header/_header-dropdown.css | 1 - .../application/blocks/header/_header.css | 3 --- .../blocks/micro-report/_micro-report.css | 11 ----------- .../_page-content-header-metas.css | 7 ------- .../page-content/_page-content-header.css | 19 ------------------- .../blocks/page-content/_page-content.css | 13 ------------- .../application/blocks/page/_page-body.css | 13 ------------- .../blocks/page/_page-header-actions.css | 3 --- .../application/blocks/page/_page-header.css | 4 ---- .../blocks/page/_page-main-header-actions.css | 4 ---- .../application/blocks/page/_page-tabs.css | 4 ---- .../practice/_category-practices-item.css | 4 ---- .../practice/_practice-status-buttons.css | 7 ------- .../blocks/thread/_thread-comment-form.css | 6 ------ .../blocks/thread/_thread-comment.css | 13 ------------- .../blocks/thread/_thread-comments-more.css | 4 ---- .../blocks/user/_companies-item.css | 7 ------- .../blocks/user/_group-company-name.css | 4 ---- .../application/blocks/user/_sns-links.css | 4 ---- .../application/blocks/user/_user-group.css | 7 ------- .../stylesheets/shared/blocks/_modal.css | 3 --- .../card-list/_card-list-item-title.css | 4 ---- .../blocks/card-list/_card-list-item.css | 13 ------------- .../shared/blocks/card/_card-books.css | 10 ---------- .../shared/blocks/form/_form-actions.css | 4 ---- .../shared/blocks/form/_form-added-choice.css | 10 ---------- .../shared/blocks/form/_form-item-actions.css | 4 ---- .../shared/blocks/form/_form-item.css | 6 ------ .../shared/blocks/form/_form-notice.css | 4 ---- .../shared/blocks/form/_form-times.css | 7 ------- .../stylesheets/shared/layouts/_columns.css | 1 - 35 files changed, 223 deletions(-) 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..9638707e55d 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; 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/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..5459ba4ed42 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; 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/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/_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-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..5ac98512c02 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; 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..13741223a54 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%; 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/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/shared/blocks/_modal.css b/app/assets/stylesheets/shared/blocks/_modal.css index 0df34f99020..42ee87c7e98 100644 --- a/app/assets/stylesheets/shared/blocks/_modal.css +++ b/app/assets/stylesheets/shared/blocks/_modal.css @@ -149,9 +149,6 @@ input:checked + .modal, } } -.modal-footer { -} - @media (width >= 48em) { .modal-footer { padding: .25rem 1.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/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 ========================================================= */ From c36347deee01afd44cfc4cc099642941e432ea74 Mon Sep 17 00:00:00 2001 From: machida Date: Thu, 2 Jul 2026 11:36:00 +0900 Subject: [PATCH 31/80] Variablize muted text color in button atoms Replace the literal hsl(242deg 5% 64%) on .is-muted-bordered and .is-muted-text with the identical var(--muted-text). Same value, no visual change. Co-Authored-By: Claude Opus 4.8 --- app/assets/stylesheets/atoms/_a-button.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/atoms/_a-button.css b/app/assets/stylesheets/atoms/_a-button.css index 199b4d7a384..080fd6410ef 100644 --- a/app/assets/stylesheets/atoms/_a-button.css +++ b/app/assets/stylesheets/atoms/_a-button.css @@ -346,7 +346,7 @@ input:checked + .a-button.is-checkbox::after { .a-button.is-muted-bordered { border-color: var(--border); - color: hsl(242deg 5% 64%); + color: var(--muted-text); font-weight: 400 !important; background-color: white; } @@ -438,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, From ae3c99b654d93db56c9b8516fe4f3a4e7fbf0d55 Mon Sep 17 00:00:00 2001 From: machida Date: Thu, 2 Jul 2026 14:59:02 +0900 Subject: [PATCH 32/80] Remove unused CSS blocks left over from refactors Delete _page-notices, _page-search and _user-secret-attributes stylesheets. Their root classes are not referenced in any template, component, or JS (e.g. the user secret-attributes partial now renders .user-metas), so these blocks are dead. Also drop their @import lines. No visual change. Co-Authored-By: Claude Opus 4.8 --- app/assets/stylesheets/application.css | 2 - .../application/blocks/page/_page-notices.css | 12 ----- .../blocks/user/_user-secret-attributes.css | 46 ------------------- app/assets/stylesheets/common-imports.css | 1 - .../shared/blocks/_page-search.css | 32 ------------- 5 files changed, 93 deletions(-) delete mode 100644 app/assets/stylesheets/application/blocks/page/_page-notices.css delete mode 100644 app/assets/stylesheets/application/blocks/user/_user-secret-attributes.css delete mode 100644 app/assets/stylesheets/shared/blocks/_page-search.css diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index f3bf36a58c3..41809b4470c 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -45,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"; @@ -103,7 +102,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/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/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/common-imports.css b/app/assets/stylesheets/common-imports.css index 1a84402ea3f..7e911d9b850 100644 --- a/app/assets/stylesheets/common-imports.css +++ b/app/assets/stylesheets/common-imports.css @@ -84,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"; 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; -} From 0adbea4a06e943f3dfc40baa9cb24be9d04721a7 Mon Sep 17 00:00:00 2001 From: machida Date: Thu, 2 Jul 2026 16:07:14 +0900 Subject: [PATCH 33/80] Remove unused CSS selectors from live stylesheets Delete dead rules whose classes are not referenced in any template, component, or JS: .auth-form-items, .header-show-mobile-nav(-items), .micro-reports-without-form, .a-border-shade, .a-card-react (from the grouped adjacency selectors), .a-hover-link-reversal, .welcome-practices, .page-nav-metas and the .modal-footer media rules. Also merge the two duplicate .a-reversal-text-link blocks left behind. The live selectors grouped alongside them (e.g. .a-card + .a-card) are preserved, so there is no visual change. Co-Authored-By: Claude Opus 4.8 --- .../blocks/auth-form/_auth-form.css | 4 -- .../application/blocks/header/_header.css | 22 ---------- .../blocks/micro-report/_micro-reports.css | 4 -- app/assets/stylesheets/atoms/_a-border.css | 7 ---- app/assets/stylesheets/atoms/_a-card.css | 13 +----- app/assets/stylesheets/atoms/_a-text-link.css | 10 ----- app/assets/stylesheets/lp/base/_base.css | 41 ------------------- .../stylesheets/shared/blocks/_modal.css | 12 ------ .../stylesheets/shared/blocks/_page-nav.css | 8 ---- 9 files changed, 2 insertions(+), 119 deletions(-) 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 9638707e55d..e9d5861d408 100644 --- a/app/assets/stylesheets/application/blocks/auth-form/_auth-form.css +++ b/app/assets/stylesheets/application/blocks/auth-form/_auth-form.css @@ -82,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/header/_header.css b/app/assets/stylesheets/application/blocks/header/_header.css index 5459ba4ed42..af025658141 100644 --- a/app/assets/stylesheets/application/blocks/header/_header.css +++ b/app/assets/stylesheets/application/blocks/header/_header.css @@ -62,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-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/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-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-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/lp/base/_base.css b/app/assets/stylesheets/lp/base/_base.css index 358493b3a0d..5465d6488c7 100644 --- a/app/assets/stylesheets/lp/base/_base.css +++ b/app/assets/stylesheets/lp/base/_base.css @@ -32,47 +32,6 @@ body.is-lp { 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); diff --git a/app/assets/stylesheets/shared/blocks/_modal.css b/app/assets/stylesheets/shared/blocks/_modal.css index 42ee87c7e98..b56f7fd68f3 100644 --- a/app/assets/stylesheets/shared/blocks/_modal.css +++ b/app/assets/stylesheets/shared/blocks/_modal.css @@ -148,15 +148,3 @@ input:checked + .modal, font-size: 1rem; } } - -@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; From 84c1cce116b306dbdd28b9869cc5d985605310df Mon Sep 17 00:00:00 2001 From: machida Date: Thu, 2 Jul 2026 17:20:44 +0900 Subject: [PATCH 34/80] Remove unused mr-bottom-nav and stale user-company-profile rules Delete the .mr-bottom-nav component (micro report bottom nav) and the orphaned .user-company-profile__description rule; neither class is referenced in any template, component, or JS. The .mr-main mobile padding is left untouched to keep the layout identical. No visual change. Co-Authored-By: Claude Opus 4.8 --- .../micro-report/_micro-report-layout.css | 45 ------------------- .../application/blocks/user/_user-profile.css | 12 ----- 2 files changed, 57 deletions(-) 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/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; - - } -} From 3da208273ab84c711ec1c66f2396421b9db7673d Mon Sep 17 00:00:00 2001 From: machida Date: Thu, 2 Jul 2026 18:47:05 +0900 Subject: [PATCH 35/80] Remove unused .thanks block and stale faqs-item rules Delete _thanks.css (the .thanks/.thanks__* block is unused; the thanks page markup uses the separate .thanks-message block) and the leftover .faqs-item__* rules in the LP base (the FAQ partial renders .lp-faq*). Neither class is referenced in any template. No visual change. Co-Authored-By: Claude Opus 4.8 --- app/assets/stylesheets/application.css | 1 - .../blocks/static-pages/_thanks.css | 70 ------------------- app/assets/stylesheets/lp/base/_base.css | 16 ----- 3 files changed, 87 deletions(-) delete mode 100644 app/assets/stylesheets/application/blocks/static-pages/_thanks.css diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 41809b4470c..667c0c557a1 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -74,7 +74,6 @@ @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"; 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/lp/base/_base.css b/app/assets/stylesheets/lp/base/_base.css index 5465d6488c7..e938cc8c450 100644 --- a/app/assets/stylesheets/lp/base/_base.css +++ b/app/assets/stylesheets/lp/base/_base.css @@ -31,19 +31,3 @@ body.is-lp { color: var(--main); border-bottom: solid 1px var(--border); } - -.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); -} From 202dcc0cc54278639911f60d3d66b4e1da7b03a7 Mon Sep 17 00:00:00 2001 From: zamami Date: Thu, 2 Jul 2026 12:05:16 +0900 Subject: [PATCH 36/80] =?UTF-8?q?seed=E3=81=AE=E5=8B=95=E7=94=BB=E3=81=AB?= =?UTF-8?q?=E3=82=82=E3=82=B5=E3=83=A0=E3=83=8D=E3=82=A4=E3=83=AB=E3=82=92?= =?UTF-8?q?=E7=94=9F=E6=88=90=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bootcamp/setup.rb | 2 ++ 1 file changed, 2 insertions(+) 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 From 79419565a5e10542e5f5d1eae9049cfb4b10a9a2 Mon Sep 17 00:00:00 2001 From: zamami Date: Thu, 2 Jul 2026 12:08:12 +0900 Subject: [PATCH 37/80] =?UTF-8?q?=E9=96=8B=E7=99=BA=E7=92=B0=E5=A2=83?= =?UTF-8?q?=E3=81=AEseed=E3=81=A7Bootcamp::Setup.attachment=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E8=A1=8C=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E5=BE=A9?= =?UTF-8?q?=E6=B4=BB=E3=81=95=E3=81=9B=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/seeds.rb | 1 + 1 file changed, 1 insertion(+) 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? From 0af5bd0b28109eeb9487e74588b90e2f74a1fc23 Mon Sep 17 00:00:00 2001 From: zamami Date: Fri, 3 Jul 2026 15:35:00 +0900 Subject: [PATCH 38/80] =?UTF-8?q?=E6=9C=AC=E7=95=AA=E7=92=B0=E5=A2=83?= =?UTF-8?q?=E3=80=81=E9=96=8B=E7=99=BA=E7=92=B0=E5=A2=83=E3=81=AEdockerfil?= =?UTF-8?q?e=E3=81=ABffmpeg=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 1 + Dockerfile.dev | 1 + 2 files changed, 2 insertions(+) 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/* From e9f70f5a29dfc449024b6a7419966c2d06073269 Mon Sep 17 00:00:00 2001 From: zamami Date: Fri, 3 Jul 2026 16:14:39 +0900 Subject: [PATCH 39/80] =?UTF-8?q?=E6=97=A2=E5=AD=98=E3=81=AE=E5=8B=95?= =?UTF-8?q?=E7=94=BB=E3=81=A7=E3=82=B5=E3=83=A0=E3=83=8D=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=8C=E3=81=AA=E3=81=84=E5=8B=95=E7=94=BB=E3=81=AE=E3=82=B5?= =?UTF-8?q?=E3=83=A0=E3=83=8D=E3=82=A4=E3=83=AB=E3=82=92=E8=87=AA=E5=8B=95?= =?UTF-8?q?=E7=94=9F=E6=88=90=E3=81=99=E3=82=8B=E5=87=A6=E7=90=86=E3=82=92?= =?UTF-8?q?=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/jobs/bulk_movie_thumbnail_job.rb | 9 +++++++++ lib/tasks/bootcamp.rake | 5 +++++ 2 files changed, 14 insertions(+) create mode 100644 app/jobs/bulk_movie_thumbnail_job.rb diff --git a/app/jobs/bulk_movie_thumbnail_job.rb b/app/jobs/bulk_movie_thumbnail_job.rb new file mode 100644 index 00000000000..7faac0d9fba --- /dev/null +++ b/app/jobs/bulk_movie_thumbnail_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class BulkMovieThumbnailJob < ApplicationJob + def perform + Movie.where.missing(:thumbnail_attachment).find_each do |movie| + GenerateMovieThumbnailJob.perform_later(movie) + end + end +end diff --git a/lib/tasks/bootcamp.rake b/lib/tasks/bootcamp.rake index e98bb43d68f..e9762356a43 100644 --- a/lib/tasks/bootcamp.rake +++ b/lib/tasks/bootcamp.rake @@ -60,6 +60,11 @@ namespace :bootcamp do puts '== END Cloud Build Task ==' end + + desc 'Backfill thumbnails for existing movies' + task backfill_movie_thumbnails: :environment do + BulkMovieThumbnailJob.perform_now + end end namespace :statistics do From 45ee5a27360df83b74b9a2852471da0f1baaaf59 Mon Sep 17 00:00:00 2001 From: zamami Date: Fri, 3 Jul 2026 16:47:31 +0900 Subject: [PATCH 40/80] =?UTF-8?q?=E3=82=B5=E3=83=A0=E3=83=8D=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=81=8C=E3=81=AA=E3=81=84=E5=8B=95=E7=94=BB=E3=81=AB?= =?UTF-8?q?=E5=AF=BE=E3=81=97=E3=81=A6=E3=82=B5=E3=83=A0=E3=83=8D=E4=BD=9C?= =?UTF-8?q?=E6=88=90=E3=82=B8=E3=83=A7=E3=83=96=E3=81=8C=E5=AE=9F=E8=A1=8C?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=82=8B=E3=81=8B=E7=A2=BA=E8=AA=8D=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/jobs/bulk_movie_thumbnail_job_test.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 test/jobs/bulk_movie_thumbnail_job_test.rb diff --git a/test/jobs/bulk_movie_thumbnail_job_test.rb b/test/jobs/bulk_movie_thumbnail_job_test.rb new file mode 100644 index 00000000000..ae4eb7169c3 --- /dev/null +++ b/test/jobs/bulk_movie_thumbnail_job_test.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'test_helper' + +class BulkMovieThumbnailJobTest < 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 + BulkMovieThumbnailJob.perform_now + end + end +end From a01bf840aa6e246746f53ae99d96fdc594b41f72 Mon Sep 17 00:00:00 2001 From: machida Date: Sat, 4 Jul 2026 16:45:16 +0900 Subject: [PATCH 41/80] Remove unused elements from company-profile and lp-top-cover Delete BEM element rules with no template references: the company-profile __header/__header-*/__name/__logo rules (the profile partial only renders __logo-image/__body/__description) and the lp-top-cover __image-container/__logo/__description rules (the main visual partial only renders __container/__inner/__contents/__title). Verified against their sole consuming partials. No visual change. Co-Authored-By: Claude Opus 4.8 --- .../blocks/company/_company-profile.css | 47 ------------------- .../lp/blocks/lp/_lp-top-cover.css | 36 -------------- 2 files changed, 83 deletions(-) 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/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); From 552e0530f667b2c6efd34b5323b5e583fc26774a Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Thu, 2 Jul 2026 11:59:55 +0900 Subject: [PATCH 42/80] =?UTF-8?q?AI=E3=83=A2=E3=83=87=E3=83=AB=E3=82=92Cla?= =?UTF-8?q?ude=20Sonnet=205=E3=81=AB=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cloudbuild/deploy-cloud-run | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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= From 46a765568aacf679d69726b42818a9e374a30a67 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Sun, 5 Jul 2026 16:25:37 +0900 Subject: [PATCH 43/80] =?UTF-8?q?CodeRabbit=E3=81=AE=E5=8B=95=E7=94=BBAPI?= =?UTF-8?q?=E6=8C=87=E6=91=98=E3=82=92=E5=8F=8D=E6=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/movies_controller.rb | 2 + docs/openapi.yaml | 55 ++++++++++++------------ public/api-docs/openapi.yaml | 55 ++++++++++++------------ test/integration/api/movies_test.rb | 11 +++++ 4 files changed, 67 insertions(+), 56 deletions(-) diff --git a/app/controllers/api/movies_controller.rb b/app/controllers/api/movies_controller.rb index b58f531dbc8..235f64eb1bb 100644 --- a/app/controllers/api/movies_controller.rb +++ b/app/controllers/api/movies_controller.rb @@ -18,6 +18,8 @@ def create 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 diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 2c4919da57c..d613f474429 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1150,39 +1150,36 @@ paths: schema: type: object required: - - movie + - movie[title] + - movie[description] + - movie[movie_data] properties: - movie: - type: object - required: - - title - - description - - movie_data - properties: - title: - type: string - description: - type: string - movie_data: - oneOf: - - type: string - format: binary - - type: string - description: Signed blob ID returned by /api/movies/direct_uploads. - thumbnail: - type: string + movie[title]: + type: string + movie[description]: + type: string + movie[movie_data]: + oneOf: + - type: string format: binary - tag_list: - type: string - wip: - type: boolean - practice_ids: - type: array - items: - type: integer + - 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: @@ -1223,6 +1220,8 @@ paths: $ref: '#/components/responses/JsonObject' '400': $ref: '#/components/responses/JsonObject' + '403': + $ref: '#/components/responses/JsonObject' /api/movies/{id}: patch: tags: [Content] diff --git a/public/api-docs/openapi.yaml b/public/api-docs/openapi.yaml index 2c4919da57c..d613f474429 100644 --- a/public/api-docs/openapi.yaml +++ b/public/api-docs/openapi.yaml @@ -1150,39 +1150,36 @@ paths: schema: type: object required: - - movie + - movie[title] + - movie[description] + - movie[movie_data] properties: - movie: - type: object - required: - - title - - description - - movie_data - properties: - title: - type: string - description: - type: string - movie_data: - oneOf: - - type: string - format: binary - - type: string - description: Signed blob ID returned by /api/movies/direct_uploads. - thumbnail: - type: string + movie[title]: + type: string + movie[description]: + type: string + movie[movie_data]: + oneOf: + - type: string format: binary - tag_list: - type: string - wip: - type: boolean - practice_ids: - type: array - items: - type: integer + - 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: @@ -1223,6 +1220,8 @@ paths: $ref: '#/components/responses/JsonObject' '400': $ref: '#/components/responses/JsonObject' + '403': + $ref: '#/components/responses/JsonObject' /api/movies/{id}: patch: tags: [Content] diff --git a/test/integration/api/movies_test.rb b/test/integration/api/movies_test.rb index 695184bd3c8..e8725b89da9 100644 --- a/test/integration/api/movies_test.rb +++ b/test/integration/api/movies_test.rb @@ -150,6 +150,17 @@ def setup 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 From 9424711a242cbb845912341b8dba43de995f18ce Mon Sep 17 00:00:00 2001 From: machida Date: Mon, 6 Jul 2026 01:07:25 +0900 Subject: [PATCH 44/80] Remove unused LP marketing-page CSS elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete dead rules from the static (JS-free, server-rendered) LP blocks, none referenced in any template: _lp-course-selection.css (whole file — only held the misspelled .lp-course-selection__descritpion), plus .lp-price__separator/__per-person, .lp-content__body, .lp-course-selection-item__title, .lp-top-image-sections__title-icon and .welcome-top-info__label. Also drop the removed file's @import. No visual change. Co-Authored-By: Claude Opus 4.8 --- app/assets/stylesheets/lp.css | 1 - .../stylesheets/lp/blocks/lp/_lp-content.css | 6 ----- .../blocks/lp/_lp-course-selection-item.css | 12 ---------- .../lp/blocks/lp/_lp-course-selection.css | 10 -------- .../stylesheets/lp/blocks/lp/_lp-price.css | 9 ------- .../lp/blocks/lp/_lp-top-image-sections.css | 6 ----- .../lp/blocks/lp/_welcome-top-info.css | 24 ------------------- 7 files changed, 68 deletions(-) delete mode 100644 app/assets/stylesheets/lp/blocks/lp/_lp-course-selection.css diff --git a/app/assets/stylesheets/lp.css b/app/assets/stylesheets/lp.css index fd6b234e5e8..7980d4b9a73 100644 --- a/app/assets/stylesheets/lp.css +++ b/app/assets/stylesheets/lp.css @@ -43,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/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-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-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; From f81fffa4bddeaeca8bb0ae1729dce077547efe08 Mon Sep 17 00:00:00 2001 From: zamami Date: Mon, 6 Jul 2026 11:19:13 +0900 Subject: [PATCH 45/80] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E5=90=8D=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._thumbnail_job.rb => bulk_generate_movie_thumbnail_job.rb} | 2 +- lib/tasks/bootcamp.rake | 2 +- ..._job_test.rb => bulk_generate_movie_thumbnail_job_test.rb} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename app/jobs/{bulk_movie_thumbnail_job.rb => bulk_generate_movie_thumbnail_job.rb} (77%) rename test/jobs/{bulk_movie_thumbnail_job_test.rb => bulk_generate_movie_thumbnail_job_test.rb} (71%) diff --git a/app/jobs/bulk_movie_thumbnail_job.rb b/app/jobs/bulk_generate_movie_thumbnail_job.rb similarity index 77% rename from app/jobs/bulk_movie_thumbnail_job.rb rename to app/jobs/bulk_generate_movie_thumbnail_job.rb index 7faac0d9fba..e50ebfb84d4 100644 --- a/app/jobs/bulk_movie_thumbnail_job.rb +++ b/app/jobs/bulk_generate_movie_thumbnail_job.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class BulkMovieThumbnailJob < ApplicationJob +class BulkGenerateMovieThumbnailJob < ApplicationJob def perform Movie.where.missing(:thumbnail_attachment).find_each do |movie| GenerateMovieThumbnailJob.perform_later(movie) diff --git a/lib/tasks/bootcamp.rake b/lib/tasks/bootcamp.rake index e9762356a43..593875d2b41 100644 --- a/lib/tasks/bootcamp.rake +++ b/lib/tasks/bootcamp.rake @@ -63,7 +63,7 @@ namespace :bootcamp do desc 'Backfill thumbnails for existing movies' task backfill_movie_thumbnails: :environment do - BulkMovieThumbnailJob.perform_now + BulkGenerateMovieThumbnailJob.perform_now end end diff --git a/test/jobs/bulk_movie_thumbnail_job_test.rb b/test/jobs/bulk_generate_movie_thumbnail_job_test.rb similarity index 71% rename from test/jobs/bulk_movie_thumbnail_job_test.rb rename to test/jobs/bulk_generate_movie_thumbnail_job_test.rb index ae4eb7169c3..27743d7192e 100644 --- a/test/jobs/bulk_movie_thumbnail_job_test.rb +++ b/test/jobs/bulk_generate_movie_thumbnail_job_test.rb @@ -2,13 +2,13 @@ require 'test_helper' -class BulkMovieThumbnailJobTest < ActiveJob::TestCase +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 - BulkMovieThumbnailJob.perform_now + BulkGenerateMovieThumbnailJob.perform_now end end end From c9586add740d94b52d32263ed7c405127744e711 Mon Sep 17 00:00:00 2001 From: machida Date: Mon, 6 Jul 2026 19:40:46 +0900 Subject: [PATCH 46/80] Add Playwright-based visual regression test scaffold Introduce visual regression testing on top of the existing Minitest + capybara-playwright-driver system tests, using capybara_screenshot_diff to compare pages against committed baseline PNGs. - test/application_visual_test_case.rb: base class with deterministic rendering (frozen time, animation/caret disabling, dynamic-content masking, font/image wait) and a capture(name) helper - test/visual/pages_visual_test.rb: thin starter coverage of six representative screens (dashboard, report show/form, users index, user profile, LP practices) - test/visual/README.md: workflow, incl. the critical same-environment baseline rule and a before/after check for this refactor - Gemfile: add capybara_screenshot_diff (run `bundle install` to update Gemfile.lock) - .gitignore: keep baselines, ignore transient diff output Co-Authored-By: Claude Opus 4.8 --- .gitignore | 6 ++ Gemfile | 1 + test/application_visual_test_case.rb | 100 +++++++++++++++++++++++++++ test/visual/README.md | 92 ++++++++++++++++++++++++ test/visual/pages_visual_test.rb | 50 ++++++++++++++ 5 files changed, 249 insertions(+) create mode 100644 test/application_visual_test_case.rb create mode 100644 test/visual/README.md create mode 100644 test/visual/pages_visual_test.rb 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/Gemfile b/Gemfile index b32f32705ca..df160afe8ad 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' gem 'minitest', '< 6.0' gem 'minitest-ci' gem 'minitest-retry' diff --git a/test/application_visual_test_case.rb b/test/application_visual_test_case.rb new file mode 100644 index 00000000000..107d9b0fd20 --- /dev/null +++ b/test/application_visual_test_case.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'application_system_test_case' +require 'capybara/screenshot/diff' + +# 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 Capybara::Screenshot::Diff + + # 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 + # Hide the text cursor so a blinking caret never causes a diff. + Capybara::Screenshot.blur_active_element = true + + # 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 img', + '.user-icon img', + '.a-grass', + '.user-grass', + '.niconico-calendar', + '.a-elapsed-days', + 'time' + ].freeze + + setup do + # 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. + def capture(name, mask: DEFAULT_MASK_SELECTORS) + wait_for_javascript_components + wait_for_fonts_and_images + stabilize_page(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/visual/README.md b/test/visual/README.md new file mode 100644 index 00000000000..ec82f5d442c --- /dev/null +++ b/test/visual/README.md @@ -0,0 +1,92 @@ +# ビジュアルリグレッションテスト + +ページのスクリーンショットを撮り、コミット済みのベースライン画像とピクセル単位で +比較して **意図しない見た目の変化** を検出します。CSS リファクタなど「見た目を +変えないはずの変更」の安全網です。 + +- 基盤: 既存の Minitest system test(Capybara + `capybara-playwright-driver`) +- 比較: [`capybara_screenshot_diff`](https://github.com/donv/capybara_screenshot_diff) +- ベースクラス: `test/application_visual_test_case.rb` +- テスト: `test/visual/*_test.rb` +- 画像: `test/visual/screenshots/`(ベースラインは git 管理、差分/一時画像は `.gitignore`) + +## ⚠️ 最重要: 描画環境を固定する + +スクリーンショットはフォントのアンチエイリアスなどが **OS によって異なる** ため、 +macOS で生成したベースラインを Linux(CI)で比較すると全ページ差分になります。 + +**ベースラインの生成と比較は必ず同じ環境で行ってください。** 推奨は CI と同じ +Ubuntu、またはそれに合わせた Docker コンテナです。ローカル Mac で撮った画像は +コミットしないこと。 + +## セットアップ + +```sh +bundle install # capybara_screenshot_diff を導入(Gemfile.lock 更新) +npx playwright install chromium +``` + +## 使い方 + +### 1. ベースラインを生成(初回 / 意図的な変更時) + +ベースライン画像が無い状態でテストを実行すると、その回のスクショが新しい +ベースラインとして保存されます(テストは pending 扱い)。 + +```sh +bin/rails test test/visual/pages_visual_test.rb +git add test/visual/screenshots +git commit -m "Add visual regression baselines" +``` + +### 2. 比較(通常のテスト実行) + +ベースラインが存在する状態で実行すると、現在の描画と比較します。差分が許容値を +超えるとテストが失敗し、`*.diff.png` が出力されます。 + +```sh +bin/rails test test/visual/pages_visual_test.rb +``` + +### 3. 意図した変更でベースラインを更新 + +デザインを意図的に変えたら、該当ベースラインを削除して再生成し、差分画像を +レビューしてからコミットします。 + +```sh +rm test/visual/screenshots/report_show.png +bin/rails test test/visual/pages_visual_test.rb -n /report_show/ +git add test/visual/screenshots/report_show.png +``` + +## このリファクタの検証(before / after ワンショット) + +`css_refactor` ブランチが本当に見た目を変えていないかは、`main` で撮った +ベースラインをこのブランチで比較すれば確認できます(同一環境で実行すること)。 + +```sh +git switch main +bin/rails test test/visual/pages_visual_test.rb # main のベースライン生成 +git switch css_refactor +git checkout main -- test/visual/screenshots # main の画像を持ち込む +bin/rails test test/visual/pages_visual_test.rb # 差分ゼロなら見た目不変 +``` + +## 動的コンテンツの扱い + +`application_visual_test_case.rb` で以下を自動化しています。 + +- `travel_to` による時刻固定(相対時間・経過日数バッジの安定化) +- transition / animation / キャレットの無効化 CSS 注入 +- アバター・草(グラフ)・時刻など非決定要素のマスク(`DEFAULT_MASK_SELECTORS`) +- フォント・画像のロード待ち、描画の安定待ち(`stability_time_limit`) + +ページ固有の動的要素は、各テストで `capture('name', mask: [...])` に +セレクタを渡してマスクを追加してください。 + +## CI への組み込み(任意) + +既存の system test ジョブ(`.github/workflows/ci.yml`、ubuntu-latest + chromium) +と同じ環境なので、そこにビジュアルテストのステップと、失敗時の +`test/visual/screenshots/**/*.diff.png` のアーティファクト保存を追加すれば +そのまま回せます。ベースラインは必ずこの CI 環境で生成・更新してください。 diff --git a/test/visual/pages_visual_test.rb b/test/visual/pages_visual_test.rb new file mode 100644 index 00000000000..f9f62fdedb2 --- /dev/null +++ b/test/visual/pages_visual_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'application_visual_test_case' + +# Visual regression coverage for a curated set of representative screens. +# +# Start thin: these pages exercise the shared building blocks that the CSS +# refactor touched (buttons, cards, forms, page/report layout, LP). Add more +# pages once the baseline is stable in CI. +# +# Run just these: bin/rails test test/visual/pages_visual_test.rb +class PagesVisualTest < ApplicationVisualTestCase + test 'dashboard' do + visit_with_auth '/', 'hatsuno' + assert_selector '.page-body' + capture('dashboard') + end + + test 'report show' do + report = reports(:report1) + visit_with_auth "/reports/#{report.id}", 'komagata' + assert_selector '.page-content' + capture('report_show') + end + + test 'report form' do + visit_with_auth '/reports/new', 'komagata' + assert_selector 'form' + capture('report_form') + end + + test 'users index' do + visit_with_auth '/users', 'komagata' + assert_selector '.users-item', minimum: 1 + capture('users_index') + end + + test 'user profile' do + user = users(:hatsuno) + visit_with_auth "/users/#{user.id}", 'komagata' + assert_selector '.user-profile, .page-content' + capture('user_profile') + end + + test 'lp practices' do + visit '/practices' + assert_selector '.lp-header' + capture('lp_practices') + end +end From b59d26b5ba40c75a4a6a78e5696e9e6c18fbf115 Mon Sep 17 00:00:00 2001 From: machida Date: Tue, 7 Jul 2026 13:31:25 +0900 Subject: [PATCH 47/80] docs(visual): add resume checklist for visual regression setup Capture the current state (scaffold committed, runtime not yet exercised) and the ordered next steps so the work can be picked up after a restart. Co-Authored-By: Claude Opus 4.8 --- test/visual/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/visual/README.md b/test/visual/README.md index ec82f5d442c..75ad7128bfe 100644 --- a/test/visual/README.md +++ b/test/visual/README.md @@ -84,6 +84,28 @@ bin/rails test test/visual/pages_visual_test.rb # 差分ゼロなら見 ページ固有の動的要素は、各テストで `capture('name', mask: [...])` に セレクタを渡してマスクを追加してください。 +## 再開時の TODO(現状メモ) + +足場(ベースクラス・サンプルテスト・gem 追加・.gitignore・本ドキュメント)は +コミット済み。**実行系はまだ回していない**ので、再開時は以下を上から順に。 + +1. [ ] `bundle install` を実行して `Gemfile.lock` に `capybara_screenshot_diff` + を反映(この作業はまだ未実施)。 +2. [ ] `npx playwright install chromium` でブラウザを用意。 +3. [ ] `bin/rails test test/visual/pages_visual_test.rb` を **CI と同じ + Linux 環境**で実行し、ベースラインを生成 → 内容を確認して + `test/visual/screenshots/` をコミット。 +4. [ ] サンプルテストの assert セレクタ・マスク対象(`DEFAULT_MASK_SELECTORS`) + を実ページに合わせて微調整。差分画像 `*.diff.png` を見ながら安定化。 +5. [ ] 安定したら「このリファクタの検証」節の before/after 手順で + `main` ↔ `css_refactor` を比較し、見た目不変を実ブラウザで裏取り。 +6. [ ] CI(`.github/workflows/ci.yml`)へステップ追加(下記)。 +7. [ ] 対象ページを代表画面から徐々に拡張。 + +補足: CSS リファクタ本体(デッドコード削除・色の変数化など)は `css_refactor` +ブランチに別途コミット済みで、コンパイル後 CSS の値不変は検証済み。本ビジュアル +テストはその実描画での裏取りが目的。 + ## CI への組み込み(任意) 既存の system test ジョブ(`.github/workflows/ci.yml`、ubuntu-latest + chromium) From 3b7cab70fdd42fba97fea7648d114f0bb6256451 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Tue, 7 Jul 2026 14:56:03 +0900 Subject: [PATCH 48/80] Allow Pjord to read linked images --- app/tools/external_content/web_page_reader.rb | 31 +++++++++++++++++++ test/tools/external_content_tool_test.rb | 19 ++++++++++++ 2 files changed, 50 insertions(+) diff --git a/app/tools/external_content/web_page_reader.rb b/app/tools/external_content/web_page_reader.rb index 781b9a43a89..f78aebef74b 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の形式が正しくありません。' @@ -40,6 +45,24 @@ 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 + [io] + ) + end + def extract_text(body) document = Nokogiri::HTML(body.to_s) document.css('script, style, noscript').remove @@ -47,6 +70,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/test/tools/external_content_tool_test.rb b/test/tools/external_content_tool_test.rb index 886e29236a8..0281ba8cbe8 100644 --- a/test/tools/external_content_tool_test.rb +++ b/test/tools/external_content_tool_test.rb @@ -44,6 +44,25 @@ class ExternalContentToolTest < ActiveSupport::TestCase assert_includes result, 'Moved content' end + test 'returns image content for Active Storage blob redirect urls' do + image_body = Rails.root.join('test/fixtures/files/companies-logos-1.jpg').binread + blob_url = 'https://bootcamp.fjord.jp/rails/active_storage/blobs/redirect/signed-id/image.jpg' + redirected_url = 'https://bootcamp.fjord.jp/rails/active_storage/disk/key/image.jpg' + stub_request(:get, blob_url) + .to_return(status: 302, headers: { 'Location' => redirected_url }) + stub_request(:get, redirected_url) + .to_return(status: 200, body: image_body, headers: { 'Content-Type' => 'image/jpeg' }) + + result = @tool.execute(url: blob_url) + + assert_instance_of RubyLLM::Content, result + assert_includes result.text, '# Image' + assert_includes result.text, "URL: #{redirected_url}" + assert_equal 1, result.attachments.size + assert_predicate result.attachments.first, :image? + assert_equal image_body, result.attachments.first.content + end + test 'rejects non http urls' do assert_equal 'httpまたはhttpsのURLだけ取得できます。', @tool.execute(url: 'file:///etc/passwd') end From 17e287c82aa782095512f1e9c0ba166df7b07026 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Tue, 7 Jul 2026 15:08:43 +0900 Subject: [PATCH 49/80] Address Pjord image review feedback --- app/tools/external_content/web_page_reader.rb | 28 +++++++++++++++---- test/tools/external_content_tool_test.rb | 20 +++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/app/tools/external_content/web_page_reader.rb b/app/tools/external_content/web_page_reader.rb index f78aebef74b..021706085d9 100644 --- a/app/tools/external_content/web_page_reader.rb +++ b/app/tools/external_content/web_page_reader.rb @@ -31,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) @@ -59,8 +63,22 @@ def format_image(response) この画像の内容を確認して、回答やレビューに必要な文脈として使ってください。 TEXT - [io] - ) + [] + ).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) diff --git a/test/tools/external_content_tool_test.rb b/test/tools/external_content_tool_test.rb index 0281ba8cbe8..f544fb440e5 100644 --- a/test/tools/external_content_tool_test.rb +++ b/test/tools/external_content_tool_test.rb @@ -60,7 +60,27 @@ class ExternalContentToolTest < ActiveSupport::TestCase assert_includes result.text, "URL: #{redirected_url}" assert_equal 1, result.attachments.size assert_predicate result.attachments.first, :image? + assert_equal 'image.jpg', result.attachments.first.filename assert_equal image_body, result.attachments.first.content + + @tool.execute(url: blob_url) + + assert_requested :get, blob_url, times: 2 + assert_requested :get, redirected_url, times: 2 + end + + test 'returns svg image content with explicit filename' do + svg_body = 'SVG' + stub_request(:get, 'https://example.com/image.svg') + .to_return(status: 200, body: svg_body, headers: { 'Content-Type' => 'image/svg+xml' }) + + result = @tool.execute(url: 'https://example.com/image.svg') + + assert_instance_of RubyLLM::Content, result + assert_equal 1, result.attachments.size + assert_predicate result.attachments.first, :image? + assert_equal 'image.svg', result.attachments.first.filename + assert_equal 'image/svg+xml', result.attachments.first.mime_type end test 'rejects non http urls' do From 6c5ad71b5711e1c88be2bd2e8fc57a7864a25de6 Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Tue, 7 Jul 2026 20:05:20 +0900 Subject: [PATCH 50/80] =?UTF-8?q?practice=E3=81=AEmodel=E3=81=AB=E7=B5=A6?= =?UTF-8?q?=E4=BB=98=E9=87=91=E3=82=B3=E3=83=BC=E3=82=B9=E3=81=B8=E7=A7=BB?= =?UTF-8?q?=E5=8B=95=E3=81=A7=E3=81=8D=E3=82=8B=E5=85=B1=E9=80=9A=E5=8C=96?= =?UTF-8?q?=E3=83=A1=E3=82=BD=E3=83=83=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/learnings/learning_component.html.slim | 2 +- app/models/practice.rb | 4 ++++ app/views/practices/_learning-status.html.slim | 2 +- app/views/practices/show.html.slim | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/components/learnings/learning_component.html.slim b/app/components/learnings/learning_component.html.slim index f872bd7ee68..ec6baf441b9 100644 --- a/app/components/learnings/learning_component.html.slim +++ b/app/components/learnings/learning_component.html.slim @@ -1,4 +1,4 @@ -- if @current_user.grant_course? && @practice.source_id.blank? +- 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 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/views/practices/_learning-status.html.slim b/app/views/practices/_learning-status.html.slim index 824e1f05ded..0b6d3fc8ec9 100644 --- a/app/views/practices/_learning-status.html.slim +++ b/app/views/practices/_learning-status.html.slim @@ -1,6 +1,6 @@ .js-learning-status(id="practice" data-id="#{@practice.id}") .practice-status-buttons - - if @practice.source_id.present? || !current_user.grant_course? + - unless @practice.guide_to_grant_course?(current_user) .practice-status-buttons__start .practice-status-buttons__label | ステータス diff --git a/app/views/practices/show.html.slim b/app/views/practices/show.html.slim index 4d0dc666e39..e6ee1ed25dd 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 && !(@practice.source_id.blank? && current_user.grant_course?) + - if @practice.submission && !@practice.guide_to_grant_course?(current_user) .card-footer__alert = link_to '提出の前に、提出時の注意点を確認しよう', 'https://bootcamp.fjord.jp/pages/info-for-product', @@ -134,7 +134,7 @@ | 提出物を作成し提出し、メンターから確認をもらったら br | このプラクティスを修了にしてください。 - - elsif !(@practice.source_id.blank? && current_user.grant_course?) + - elsif !@practice.guide_to_grant_course?(current_user) .card-footer__description | このプラクティスに提出物はありません。 br From f84ac5c061ce3babd1c2b9bc0abb148388c670ed Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Tue, 7 Jul 2026 21:15:35 +0900 Subject: [PATCH 51/80] =?UTF-8?q?test=E6=96=87=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=E3=81=A6unless=E3=81=8B=E3=82=89if=E6=96=87=E3=81=AB?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practices/_learning-status.html.slim | 16 +++++++------- test/system/practices_test.rb | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/app/views/practices/_learning-status.html.slim b/app/views/practices/_learning-status.html.slim index 0b6d3fc8ec9..b3fe6146a8c 100644 --- a/app/views/practices/_learning-status.html.slim +++ b/app/views/practices/_learning-status.html.slim @@ -1,6 +1,13 @@ .js-learning-status(id="practice" data-id="#{@practice.id}") .practice-status-buttons - - unless @practice.guide_to_grant_course?(current_user) + - 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 | ステータス @@ -23,10 +30,3 @@ .practice-status-buttons__end .practice-status-buttons__note | このプラクティスに提出物はありません。修了条件をクリアしたら修了にしてください。 - - else - .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 - | 進捗のステータス変更は、給付金コースのプラクティス側で行ってください。 diff --git a/test/system/practices_test.rb b/test/system/practices_test.rb index 23f1b2159f8..73fe2836a75 100644 --- a/test/system/practices_test.rb +++ b/test/system/practices_test.rb @@ -129,4 +129,25 @@ def wait_for_status_change assert_no_text 'このプラクティスに提出物はありません。' assert_no_text '修了条件をクリアしたら修了にしてください。' end + + test 'show normal status buttons on source practice for non grant course user' do + source_practice = practices(:practice23) + + visit_with_auth practice_path(source_practice), 'kimura' + + assert_selector '.practice-status-buttons__button' + assert_no_link '給付金コースへ移動する' + assert_no_text '進捗のステータス変更は、給付金コースのプラクティス側で行ってください。' + assert_no_text '提出・修了は、給付金コースのプラクティス側で行ってください。' + end + + test 'show normal status buttons on grant course practice for grant course user' do + grant_course_practice = practices(:practice64) + + visit_with_auth practice_path(grant_course_practice), 'grant-course' + + assert_selector '.practice-status-buttons__button' + assert_no_link '給付金コースへ移動する' + assert_no_text '進捗のステータス変更は、給付金コースのプラクティス側で行ってください。' + end end From ccd7d45f1e01a53474273cc503c5af7f2ced7aa5 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Tue, 7 Jul 2026 22:16:51 +0900 Subject: [PATCH 52/80] =?UTF-8?q?Check=E3=82=92=E6=8F=90=E5=87=BA=E7=89=A9?= =?UTF-8?q?=E3=81=94=E3=81=A8=E3=81=AB=E4=B8=80=E6=84=8F=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/interactors/copy_check.rb | 32 +++------------- app/models/check.rb | 2 +- .../20260707000000_delete_duplicate_checks.rb | 25 +++++++++++++ db/data_schema.rb | 2 +- db/fixtures/checks.yml | 4 -- ...change_checks_unique_index_to_checkable.rb | 37 +++++++++++++++++++ db/schema.rb | 4 +- test/fixtures/checks.yml | 4 -- test/interactors/copy_check_test.rb | 21 +++++------ test/models/check_test.rb | 15 ++++++++ 10 files changed, 96 insertions(+), 50 deletions(-) create mode 100644 db/data/20260707000000_delete_duplicate_checks.rb create mode 100644 db/migrate/20260707000000_change_checks_unique_index_to_checkable.rb create mode 100644 test/models/check_test.rb diff --git a/app/interactors/copy_check.rb b/app/interactors/copy_check.rb index 9120fcc2d63..4babbc97c78 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) @@ -73,17 +60,10 @@ 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/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/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..954740b01ef 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) 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/schema.rb b/db/schema.rb index 17762346eb6..bd8a980ecf8 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_07_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 diff --git a/test/fixtures/checks.yml b/test/fixtures/checks.yml index 3d6b393d6ee..3097651c5d1 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) 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/models/check_test.rb b/test/models/check_test.rb new file mode 100644 index 00000000000..5acf3cae4d8 --- /dev/null +++ b/test/models/check_test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'test_helper' + +class CheckTest < ActiveSupport::TestCase + test 'cannot create multiple checks for the same checkable even by different users' do + report = Report.left_outer_joins(:checks).where(checks: { id: nil }).first + + Check.create!(user: users(:komagata), checkable: report) + check = Check.new(user: users(:mentormentaro), checkable: report) + + assert_not check.valid? + assert_includes check.errors[:checkable_id], 'はすでに存在します' + end +end From 565924223ce9c95f6a0395ab478efd9dd48693f2 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Tue, 7 Jul 2026 23:40:59 +0900 Subject: [PATCH 53/80] =?UTF-8?q?buzzes=E3=81=AE=E5=B9=B4=E5=88=A5?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E3=81=A7=E4=B8=8D=E6=AD=A3=E3=81=AA?= =?UTF-8?q?=E5=B9=B4=E3=82=92=E9=99=A4=E5=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/buzz.rb | 7 +++++-- test/models/buzz_test.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) 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/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 buzz1 From 8789ce5489b0a4ad6aa4d23cadd640004ee3d5ad Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Tue, 7 Jul 2026 23:45:22 +0900 Subject: [PATCH 54/80] =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E3=81=AECheck?= =?UTF-8?q?=E3=82=B3=E3=83=94=E3=83=BC=E5=88=A4=E5=AE=9A=E3=83=A1=E3=82=BD?= =?UTF-8?q?=E3=83=83=E3=83=89=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/interactors/copy_check.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/interactors/copy_check.rb b/app/interactors/copy_check.rb index 4babbc97c78..e6d6cc16e21 100644 --- a/app/interactors/copy_check.rb +++ b/app/interactors/copy_check.rb @@ -52,10 +52,6 @@ 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 From e405b80e7f6cd5c3415896116f0961aaf7018211 Mon Sep 17 00:00:00 2001 From: karlley Date: Sat, 11 Jul 2026 06:05:54 +0900 Subject: [PATCH 55/80] =?UTF-8?q?=E5=AD=A6=E7=BF=92=E6=99=82=E9=96=930?= =?UTF-8?q?=E6=99=82=E9=96=93=E3=81=AE=E8=A1=A8=E7=A4=BA=E3=82=92=E3=80=8C?= =?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=BF=E5=8F=8E=E9=9B=86=E4=B8=AD=E3=80=8D?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/practices_helper.rb | 4 +++- test/helpers/practices_helper_test.rb | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/helpers/practices_helper.rb b/app/helpers/practices_helper.rb index 82231c695f4..b873a3e4b32 100644 --- a/app/helpers/practices_helper.rb +++ b/app/helpers/practices_helper.rb @@ -27,7 +27,9 @@ def practice_ogp_meta_tags(image_url) end def difficulty_icon(minutes) - case minutes.to_i # nilの場合は0として扱う(nil.to_i == 0) + case minutes + when nil then '' + when 0 then 'データ収集中' when ..300 then '🔥' # 5時間以下 when ..600 then '🔥🔥' # 10時間以下 when ..900 then '🔥🔥🔥' # 15時間以下 diff --git a/test/helpers/practices_helper_test.rb b/test/helpers/practices_helper_test.rb index d8dee19af14..1c0b9531d0c 100644 --- a/test/helpers/practices_helper_test.rb +++ b/test/helpers/practices_helper_test.rb @@ -4,7 +4,9 @@ class PracticesHelperTest < ActionView::TestCase test 'difficulty_icon' do - assert_equal '🔥', difficulty_icon(0) + 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) From 26f7356455b750b45ef65fed72b442fc185ec9b5 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Tue, 7 Jul 2026 17:04:13 +0900 Subject: [PATCH 56/80] =?UTF-8?q?=E3=83=94=E3=83=A8=E3=83=AB=E3=83=89?= =?UTF-8?q?=E3=81=8C=E6=8F=90=E5=87=BA=E7=89=A9OK=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=82=8B=E3=83=95=E3=83=A9=E3=82=B0=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/agents/pjord/product_review_agent.rb | 18 ++++++- app/controllers/api/practices_controller.rb | 2 +- .../mentor/practices_controller.rb | 3 +- app/jobs/pjord_product_review_job.rb | 20 ++++++-- app/models/pjord_product_review_response.rb | 3 ++ .../product_review_agent/instructions.txt.erb | 2 + .../mentor/practices/_practice.json.jbuilder | 1 + app/views/api/practices/show.json.jbuilder | 2 +- app/views/mentor/practices/_form.html.slim | 4 ++ config/locales/ja.yml | 1 + ...00000_add_pjord_auto_check_to_practices.rb | 7 +++ db/schema.rb | 1 + .../agents/pjord/product_review_agent_test.rb | 16 +++++- test/application_system_test_case.rb | 3 ++ test/integration/api/practices_test.rb | 13 +++++ .../products/pjord_review_comment_test.rb | 12 ++--- test/jobs/pjord_product_review_job_test.rb | 50 ++++++++++++++++--- 17 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 db/migrate/20260707000000_add_pjord_auto_check_to_practices.rb diff --git a/app/agents/pjord/product_review_agent.rb b/app/agents/pjord/product_review_agent.rb index 51b6d48aab2..732f3eb3929 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 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/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/jobs/pjord_product_review_job.rb b/app/jobs/pjord_product_review_job.rb index faefdf2e4f9..1c8150bb3a6 100644 --- a/app/jobs/pjord_product_review_job.rb +++ b/app/jobs/pjord_product_review_job.rb @@ -13,9 +13,23 @@ 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.transaction do + check = Check.create!(user: pjord, checkable: product) + product.checks.reload + ActiveSupport::Notifications.instrument('check.create', check:) + end 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/prompts/pjord/product_review_agent/instructions.txt.erb b/app/prompts/pjord/product_review_agent/instructions.txt.erb index 5419001dae2..067da441db1 100644 --- a/app/prompts/pjord/product_review_agent/instructions.txt.erb +++ b/app/prompts/pjord/product_review_agent/instructions.txt.erb @@ -7,6 +7,8 @@ reviewed_points には、提出物本文、URL先の内容、模範解答、過去コメントなどを確認して判断した具体的な点を1つ以上入れてください。 body には reviewed_points の内容を反映したレビューコメントを書いてください。 改善点が見つからない場合も、何を確認して問題ないと判断したかを書いてください。 +auto_check には、提出物がプラクティスのゴールを満たしていて、メンターの追加確認なしでOKにしてよいと判断できる場合だけ true を設定してください。 +提出物やURL先を確認できない場合、改善点がある場合、判断に迷う場合、メンターに引き継ぐ場合は auto_check を false にしてください。 提出物コメントはそのまま提出者本人に表示されます。管理側への説明、内部事情、運用者向けメモ、レビュー生成方針への言及は含めず、すべて提出者本人に向けたコメントとして書いてください。 模範解答や他の提出物の内容をそのままコピーせず、答えを丸ごと教えないでください。 body にはレビューコメント本文だけをmarkdownで出力してください。 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/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/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/db/migrate/20260707000000_add_pjord_auto_check_to_practices.rb b/db/migrate/20260707000000_add_pjord_auto_check_to_practices.rb new file mode 100644 index 00000000000..d554545a84a --- /dev/null +++ b/db/migrate/20260707000000_add_pjord_auto_check_to_practices.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddPjordAutoCheckToPractices < ActiveRecord::Migration[8.0] + 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 bd8a980ecf8..fb80fdc8020 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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/test/agents/pjord/product_review_agent_test.rb b/test/agents/pjord/product_review_agent_test.rb index 8778d13f0e5..37172e0455d 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 @@ -33,6 +34,7 @@ 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にしてよいと判断できる場合だけ true' assert_includes chat.instructions, '管理側への説明、内部事情、運用者向けメモ、レビュー生成方針への言及は含めず' assert_includes chat.instructions, 'external_content_toolを使って内容を確認してからレビューしてください。' assert_includes chat.instructions, 'CodePenや提出物のリンク先が見えない' @@ -95,10 +97,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 +132,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/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/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/jobs/pjord_product_review_job_test.rb b/test/jobs/pjord_product_review_job_test.rb index 43981acc4be..02996cc02a9 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,55 @@ 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 '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 From 2da163be00b8825747e5bb21c0a7c07c5e001a66 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Tue, 7 Jul 2026 21:39:22 +0900 Subject: [PATCH 57/80] =?UTF-8?q?CodeRabbit=E3=81=AE=E6=8C=87=E6=91=98?= =?UTF-8?q?=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/jobs/pjord_product_review_job.rb | 18 ++++++++++++------ ...000000_add_pjord_auto_check_to_practices.rb | 2 +- test/jobs/pjord_product_review_job_test.rb | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/jobs/pjord_product_review_job.rb b/app/jobs/pjord_product_review_job.rb index 1c8150bb3a6..d043585484b 100644 --- a/app/jobs/pjord_product_review_job.rb +++ b/app/jobs/pjord_product_review_job.rb @@ -24,12 +24,18 @@ def perform(product_id:) private def auto_check_product(product, pjord) - return if product.checked? - - Check.transaction do - check = Check.create!(user: pjord, checkable: product) - product.checks.reload - ActiveSupport::Notifications.instrument('check.create', check:) + created_check = nil + + product.with_lock do + unless product.checks.exists? + check = Check.create_or_find_by!(user: pjord, checkable: product) + if check.previously_new_record? + product.checks.reload + created_check = check + end + end end + + ActiveSupport::Notifications.instrument('check.create', check: created_check) if created_check end end diff --git a/db/migrate/20260707000000_add_pjord_auto_check_to_practices.rb b/db/migrate/20260707000000_add_pjord_auto_check_to_practices.rb index d554545a84a..4af0e68f67e 100644 --- a/db/migrate/20260707000000_add_pjord_auto_check_to_practices.rb +++ b/db/migrate/20260707000000_add_pjord_auto_check_to_practices.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class AddPjordAutoCheckToPractices < ActiveRecord::Migration[8.0] +class AddPjordAutoCheckToPractices < ActiveRecord::Migration[8.1] def change add_column :practices, :pjord_auto_check, :boolean, null: false, default: false end diff --git a/test/jobs/pjord_product_review_job_test.rb b/test/jobs/pjord_product_review_job_test.rb index 02996cc02a9..46a491e451c 100644 --- a/test/jobs/pjord_product_review_job_test.rb +++ b/test/jobs/pjord_product_review_job_test.rb @@ -96,6 +96,23 @@ class PjordProductReviewJobTest < ActiveJob::TestCase end end + test 'does not notify when Pjord check already exists' do + product = products(:product8) + product.practice.update!(pjord_auto_check: true) + Check.create!(user: users(:pjord), 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) From cccc1937a3745f2f8f899d1948fc4afc6f9c27f3 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Wed, 8 Jul 2026 15:08:42 +0900 Subject: [PATCH 58/80] =?UTF-8?q?=E6=8F=90=E5=87=BA=E7=89=A9=E3=81=AECheck?= =?UTF-8?q?=E4=B8=80=E6=84=8F=E5=88=B6=E7=B4=84=E3=82=92=E5=89=8D=E6=8F=90?= =?UTF-8?q?=E3=81=AB=E7=B0=A1=E6=BD=94=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/jobs/pjord_product_review_job.rb | 18 +++++------------- test/jobs/pjord_product_review_job_test.rb | 4 ++-- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/app/jobs/pjord_product_review_job.rb b/app/jobs/pjord_product_review_job.rb index d043585484b..cbe46d5c11f 100644 --- a/app/jobs/pjord_product_review_job.rb +++ b/app/jobs/pjord_product_review_job.rb @@ -24,18 +24,10 @@ def perform(product_id:) private def auto_check_product(product, pjord) - created_check = nil - - product.with_lock do - unless product.checks.exists? - check = Check.create_or_find_by!(user: pjord, checkable: product) - if check.previously_new_record? - product.checks.reload - created_check = check - end - end - end - - ActiveSupport::Notifications.instrument('check.create', check: created_check) if created_check + 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/test/jobs/pjord_product_review_job_test.rb b/test/jobs/pjord_product_review_job_test.rb index 46a491e451c..8b9f63fcd44 100644 --- a/test/jobs/pjord_product_review_job_test.rb +++ b/test/jobs/pjord_product_review_job_test.rb @@ -96,10 +96,10 @@ class PjordProductReviewJobTest < ActiveJob::TestCase end end - test 'does not notify when Pjord check already exists' do + test 'does not notify when product is already checked' do product = products(:product8) product.practice.update!(pjord_auto_check: true) - Check.create!(user: users(:pjord), checkable: product) + Check.create!(user: users(:mentormentaro), checkable: product) check_create_count = 0 ActiveSupport::Notifications.subscribed(->(*) { check_create_count += 1 }, 'check.create') do From 19b683e7df7ddb5ba86d25552321bcd607af9279 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Wed, 8 Jul 2026 15:16:38 +0900 Subject: [PATCH 59/80] =?UTF-8?q?Pjord=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=81=AEOK=E8=A1=A8=E7=8F=BE=E3=82=92=E3=83=95=E3=83=A9?= =?UTF-8?q?=E3=82=B0=E3=81=A7=E5=88=B6=E5=BE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/agents/pjord/product_review_agent.rb | 1 + .../product_review_agent/instructions.txt.erb | 3 ++- ...000000_add_pjord_auto_check_to_practices.rb} | 0 db/schema.rb | 2 +- test/agents/pjord/product_review_agent_test.rb | 17 ++++++++++++++++- 5 files changed, 20 insertions(+), 3 deletions(-) rename db/migrate/{20260707000000_add_pjord_auto_check_to_practices.rb => 20260708000000_add_pjord_auto_check_to_practices.rb} (100%) diff --git a/app/agents/pjord/product_review_agent.rb b/app/agents/pjord/product_review_agent.rb index 732f3eb3929..522c4e56a7a 100644 --- a/app/agents/pjord/product_review_agent.rb +++ b/app/agents/pjord/product_review_agent.rb @@ -47,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/prompts/pjord/product_review_agent/instructions.txt.erb b/app/prompts/pjord/product_review_agent/instructions.txt.erb index 067da441db1..08b1cac200e 100644 --- a/app/prompts/pjord/product_review_agent/instructions.txt.erb +++ b/app/prompts/pjord/product_review_agent/instructions.txt.erb @@ -7,7 +7,8 @@ reviewed_points には、提出物本文、URL先の内容、模範解答、過去コメントなどを確認して判断した具体的な点を1つ以上入れてください。 body には reviewed_points の内容を反映したレビューコメントを書いてください。 改善点が見つからない場合も、何を確認して問題ないと判断したかを書いてください。 -auto_check には、提出物がプラクティスのゴールを満たしていて、メンターの追加確認なしでOKにしてよいと判断できる場合だけ true を設定してください。 +「ピヨルドによる提出物OK」が許可されていない場合、auto_check は必ず false にしてください。この場合のbodyは参考コメントであり、しっかりしたレビューやOK判定ではありません。「OKです」「合格です」「次に進んでください」「完了です」など、提出物を承認したと受け取れる表現を書かないでください。最終判断はメンターが行います。 +「ピヨルドによる提出物OK」が許可されている場合のみ、提出物がプラクティスのゴールを満たしていて、メンターの追加確認なしでOKにしてよいと判断できる場合だけ auto_check に true を設定してください。 提出物やURL先を確認できない場合、改善点がある場合、判断に迷う場合、メンターに引き継ぐ場合は auto_check を false にしてください。 提出物コメントはそのまま提出者本人に表示されます。管理側への説明、内部事情、運用者向けメモ、レビュー生成方針への言及は含めず、すべて提出者本人に向けたコメントとして書いてください。 模範解答や他の提出物の内容をそのままコピーせず、答えを丸ごと教えないでください。 diff --git a/db/migrate/20260707000000_add_pjord_auto_check_to_practices.rb b/db/migrate/20260708000000_add_pjord_auto_check_to_practices.rb similarity index 100% rename from db/migrate/20260707000000_add_pjord_auto_check_to_practices.rb rename to db/migrate/20260708000000_add_pjord_auto_check_to_practices.rb diff --git a/db/schema.rb b/db/schema.rb index fb80fdc8020..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_07_07_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" diff --git a/test/agents/pjord/product_review_agent_test.rb b/test/agents/pjord/product_review_agent_test.rb index 37172e0455d..b08d1691a16 100644 --- a/test/agents/pjord/product_review_agent_test.rb +++ b/test/agents/pjord/product_review_agent_test.rb @@ -24,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 @@ -34,7 +35,9 @@ 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にしてよいと判断できる場合だけ true' + 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や提出物のリンク先が見えない' @@ -55,6 +58,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)) From cea6a4073add680a9e1bd15540b88f242649978f Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Thu, 9 Jul 2026 01:32:56 +0900 Subject: [PATCH 60/80] =?UTF-8?q?flakewatch=E3=82=A2=E3=82=AF=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 829c7d951f8..aa32830cdb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -222,8 +222,8 @@ 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 }} From bac2a2af01aa75f3438ca6952397571300ee7fdf Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Thu, 9 Jul 2026 01:52:30 +0900 Subject: [PATCH 61/80] =?UTF-8?q?flakewatch=E3=81=AECI=E3=82=92=E5=86=8D?= =?UTF-8?q?=E5=AE=9F=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From bd4db5bb68a22d32e5c30868a574cdcf7df7c8c8 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Thu, 9 Jul 2026 01:53:06 +0900 Subject: [PATCH 62/80] =?UTF-8?q?flakewatch=E5=B1=A5=E6=AD=B4=E3=81=AE?= =?UTF-8?q?=E6=9B=B8=E3=81=8D=E8=BE=BC=E3=81=BF=E3=82=92=E7=84=A1=E5=8A=B9?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa32830cdb4..55883800ba2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -228,4 +228,4 @@ jobs: junit-artifact-pattern: junit-xml-* source-root: ${{ github.workspace }} history-branch: flakewatch-data - history-write: auto + history-write: false From f93065c76ac5385a78017af94aa437a2d5bbde17 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Mon, 13 Jul 2026 12:30:36 +0900 Subject: [PATCH 63/80] =?UTF-8?q?=E8=A4=87=E6=95=B0=E3=81=AE=E6=97=A5?= =?UTF-8?q?=E5=A0=B1=E3=82=92=E4=B8=80=E6=8B=AC=E3=81=A7=E7=A2=BA=E8=AA=8D?= =?UTF-8?q?=E6=B8=88=E3=81=BF=E3=81=AB=E3=81=99=E3=82=8BAPI=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/reports/bulk_check_controller.rb | 32 ++++++++++++ config/routes/api.rb | 3 ++ test/integration/api/checks_test.rb | 49 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 app/controllers/api/reports/bulk_check_controller.rb diff --git a/app/controllers/api/reports/bulk_check_controller.rb b/app/controllers/api/reports/bulk_check_controller.rb new file mode 100644 index 00000000000..069c05334ff --- /dev/null +++ b/app/controllers/api/reports/bulk_check_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class API::Reports::BulkCheckController < 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.includes(:checks).where(id: report_ids).index_by(&:id) + missing_ids = report_ids - reports.keys + return render json: { message: '日報が見つかりません。', report_ids: missing_ids }, status: :not_found if missing_ids.any? + + checks = Check.transaction do + reports.values.filter_map do |report| + next if report.checked? + + Check.create!(user: current_user, checkable: report).tap do |check| + ActiveSupport::Notifications.instrument('check.create', check:) + end + end + end + + render json: { checks: checks.map { |check| check_json(check) } }, status: :created + end + + private + + def require_staff + render json: { message: '権限がありません。' }, status: :forbidden unless current_user&.staff? + end +end diff --git a/config/routes/api.rb b/config/routes/api.rb index e2ac4975e5a..8d88f3b0499 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -58,6 +58,9 @@ resources :recents, only: %i(index) end resources :reports, only: %i(index show create update destroy) do + collection do + resource :bulk_check, only: %i(create), controller: 'reports/bulk_check', as: :reports_bulk_check + end resources :comments, only: %i[create], controller: 'reports/comments' resources :reactions, only: %i(index create destroy), controller: 'reports/reactions' resource :check, only: %i(create destroy), controller: 'reports/check' diff --git a/test/integration/api/checks_test.rb b/test/integration/api/checks_test.rb index f374cdc85b1..a3987142dec 100644 --- a/test/integration/api/checks_test.rb +++ b/test/integration/api/checks_test.rb @@ -236,6 +236,55 @@ 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 + + assert_difference('Check.count', 2) do + post api_reports_bulk_check_path(format: :json), + params: { report_ids: }, + headers: { Authorization: "Bearer #{@mentor_write_token.token}" } + assert_response :created + end + + assert_equal report_ids.sort, response.parsed_body['checks'].pluck('checkable_id').sort + 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_bulk_check_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_bulk_check_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_bulk_check_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 + private def unchecked_report From 3a19290767bbcfb454edd382cac885a3cf6b19a5 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Mon, 13 Jul 2026 13:39:52 +0900 Subject: [PATCH 64/80] =?UTF-8?q?=E6=97=A5=E5=A0=B1=E3=81=AE=E4=B8=80?= =?UTF-8?q?=E6=8B=AC=E7=A2=BA=E8=AA=8DAPI=E3=82=92=E9=AB=98=E9=80=9F?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/reports/bulk_check_controller.rb | 32 ------------- .../api/reports/checks_controller.rb | 46 +++++++++++++++++++ config/routes/api.rb | 4 +- test/integration/api/checks_test.rb | 38 +++++++++++---- 4 files changed, 77 insertions(+), 43 deletions(-) delete mode 100644 app/controllers/api/reports/bulk_check_controller.rb create mode 100644 app/controllers/api/reports/checks_controller.rb diff --git a/app/controllers/api/reports/bulk_check_controller.rb b/app/controllers/api/reports/bulk_check_controller.rb deleted file mode 100644 index 069c05334ff..00000000000 --- a/app/controllers/api/reports/bulk_check_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -class API::Reports::BulkCheckController < 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.includes(:checks).where(id: report_ids).index_by(&:id) - missing_ids = report_ids - reports.keys - return render json: { message: '日報が見つかりません。', report_ids: missing_ids }, status: :not_found if missing_ids.any? - - checks = Check.transaction do - reports.values.filter_map do |report| - next if report.checked? - - Check.create!(user: current_user, checkable: report).tap do |check| - ActiveSupport::Notifications.instrument('check.create', check:) - end - end - end - - render json: { checks: checks.map { |check| check_json(check) } }, status: :created - end - - private - - def require_staff - render json: { message: '権限がありません。' }, status: :forbidden unless current_user&.staff? - 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..0f280de7ccf --- /dev/null +++ b/app/controllers/api/reports/checks_controller.rb @@ -0,0 +1,46 @@ +# 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 = insert_checks(unchecked_reports) + delete_report_caches(checks, reports) + + 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 + + def require_staff + render json: { message: '権限がありません。' }, status: :forbidden unless current_user&.staff? + end +end diff --git a/config/routes/api.rb b/config/routes/api.rb index 8d88f3b0499..1b5e8e1d6b2 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -57,10 +57,8 @@ 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 - collection do - resource :bulk_check, only: %i(create), controller: 'reports/bulk_check', as: :reports_bulk_check - end resources :comments, only: %i[create], controller: 'reports/comments' resources :reactions, only: %i(index create destroy), controller: 'reports/reactions' resource :check, only: %i(create destroy), controller: 'reports/check' diff --git a/test/integration/api/checks_test.rb b/test/integration/api/checks_test.rb index a3987142dec..65eccd9cb11 100644 --- a/test/integration/api/checks_test.rb +++ b/test/integration/api/checks_test.rb @@ -238,15 +238,37 @@ class API::ChecksTest < ActionDispatch::IntegrationTest 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 - assert_difference('Check.count', 2) do - post api_reports_bulk_check_path(format: :json), - params: { report_ids: }, - headers: { Authorization: "Bearer #{@mentor_write_token.token}" } - assert_response :created + 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 skips reports that are already checked' do @@ -254,7 +276,7 @@ class API::ChecksTest < ActionDispatch::IntegrationTest unchecked_report = self.unchecked_report assert_difference('Check.count', 1) do - post api_reports_bulk_check_path(format: :json), + 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 @@ -267,7 +289,7 @@ class API::ChecksTest < ActionDispatch::IntegrationTest report = unchecked_report assert_no_difference('Check.count') do - post api_reports_bulk_check_path(format: :json), + post api_reports_checks_path(format: :json), params: { report_ids: [report.id, 0] }, headers: { Authorization: "Bearer #{@mentor_write_token.token}" } assert_response :not_found @@ -277,7 +299,7 @@ class API::ChecksTest < ActionDispatch::IntegrationTest end test 'student can not bulk check reports' do - post api_reports_bulk_check_path(format: :json), + post api_reports_checks_path(format: :json), params: { report_ids: [unchecked_report.id] }, headers: { Authorization: "Bearer #{@student_token.token}" } From d2b74f8c221d47d89898039813faf43589767fdf Mon Sep 17 00:00:00 2001 From: zecky1120 Date: Mon, 13 Jul 2026 14:52:50 +0900 Subject: [PATCH 65/80] =?UTF-8?q?else=E6=96=87=E3=81=AB=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=97=E3=81=A6=E3=81=9D=E3=81=AE=E4=B8=AD=E3=81=ABunless?= =?UTF-8?q?=E6=96=87=E3=81=A7=E6=9D=A1=E4=BB=B6=E5=88=86=E5=B2=90=E3=81=97?= =?UTF-8?q?=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/practices/show.html.slim | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/views/practices/show.html.slim b/app/views/practices/show.html.slim index e6ee1ed25dd..23f0443d77c 100644 --- a/app/views/practices/show.html.slim +++ b/app/views/practices/show.html.slim @@ -134,11 +134,12 @@ | 提出物を作成し提出し、メンターから確認をもらったら br | このプラクティスを修了にしてください。 - - elsif !@practice.guide_to_grant_course?(current_user) - .card-footer__description - | このプラクティスに提出物はありません。 - br - | 修了条件をクリアしたら修了にしてください。 + - else + - 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 } From cfb295a5d77e0b6a07369f2e9a286283e5a38634 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Mon, 13 Jul 2026 15:59:25 +0900 Subject: [PATCH 66/80] =?UTF-8?q?CodeRabbit=E3=81=AE=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E6=8C=87=E6=91=98=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/base_controller.rb | 4 ++ .../api/reports/checks_controller.rb | 9 ++--- .../concerns/api/checkable_check.rb | 4 -- test/integration/api/checks_test.rb | 38 +++++++++++++++++++ 4 files changed, 45 insertions(+), 10 deletions(-) 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/reports/checks_controller.rb b/app/controllers/api/reports/checks_controller.rb index 0f280de7ccf..0c2359df9d7 100644 --- a/app/controllers/api/reports/checks_controller.rb +++ b/app/controllers/api/reports/checks_controller.rb @@ -13,8 +13,9 @@ def create 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 = insert_checks(unchecked_reports) - delete_report_caches(checks, reports) + 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 @@ -39,8 +40,4 @@ def delete_report_caches(checks, reports) Cache.delete_user_unchecked_report_count(user_id) end end - - def require_staff - render json: { message: '権限がありません。' }, status: :forbidden unless current_user&.staff? - 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/test/integration/api/checks_test.rb b/test/integration/api/checks_test.rb index 65eccd9cb11..0abe14c8a80 100644 --- a/test/integration/api/checks_test.rb +++ b/test/integration/api/checks_test.rb @@ -271,6 +271,20 @@ class API::ChecksTest < ActionDispatch::IntegrationTest 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 @@ -307,6 +321,30 @@ class API::ChecksTest < ActionDispatch::IntegrationTest 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 From d3fd157322f24507602f3091488eb58703e7580e Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Mon, 13 Jul 2026 17:07:17 +0900 Subject: [PATCH 67/80] =?UTF-8?q?EXIF=E5=89=8A=E9=99=A4=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=82=92=E7=B0=A1=E6=BD=94=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/image_controller.rb | 5 +++-- app/models/image.rb | 17 ----------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/app/controllers/api/image_controller.rb b/app/controllers/api/image_controller.rb index 9b9df6cc20e..c8f40441a10 100644 --- a/app/controllers/api/image_controller.rb +++ b/app/controllers/api/image_controller.rb @@ -2,8 +2,9 @@ class API::ImageController < API::BaseController def create - @image = Image.new(user: current_user, image: params[:file]) - @image.strip_exif! + uploaded_image = params[:file] + MiniMagick::Image.new(uploaded_image.tempfile.path).strip if uploaded_image + @image = Image.new(user: current_user, image: uploaded_image) if @image.save render :create, status: :created diff --git a/app/models/image.rb b/app/models/image.rb index 2f9d75e71b4..eeac5722b92 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -5,21 +5,4 @@ class Image < ApplicationRecord has_one_attached :image validates :image, attached: true - - def strip_exif! - attachment = attachment_changes['image'] - return unless attachment - - uploaded_file = attachment.attachable - original_image = MiniMagick::Image.read(uploaded_file.tempfile) - original_image.strip - - blob = { - io: StringIO.new(original_image.to_blob), - filename: uploaded_file.original_filename.to_s, - content_type: uploaded_file.content_type - } - - image.attach(blob) - end end From f2749e30b49d08654726e218c33b1c0d69d725ce Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Mon, 13 Jul 2026 17:16:56 +0900 Subject: [PATCH 68/80] =?UTF-8?q?=E7=94=BB=E5=83=8F=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=99=82=E3=81=AB=E3=82=82EXIF=E6=83=85=E5=A0=B1=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/image_controller.rb | 4 +--- app/models/image.rb | 5 +++++ test/models/image_test.rb | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 test/models/image_test.rb diff --git a/app/controllers/api/image_controller.rb b/app/controllers/api/image_controller.rb index c8f40441a10..cea930a86fe 100644 --- a/app/controllers/api/image_controller.rb +++ b/app/controllers/api/image_controller.rb @@ -2,9 +2,7 @@ class API::ImageController < API::BaseController def create - uploaded_image = params[:file] - MiniMagick::Image.new(uploaded_image.tempfile.path).strip if uploaded_image - @image = Image.new(user: current_user, image: uploaded_image) + @image = Image.new(user: current_user, image: params[:file]) if @image.save render :create, status: :created diff --git a/app/models/image.rb b/app/models/image.rb index eeac5722b92..c2663021a0e 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -5,4 +5,9 @@ class Image < ApplicationRecord has_one_attached :image validates :image, attached: true + + def image=(attachable) + MiniMagick::Image.new(attachable.tempfile.path).strip if attachable.respond_to?(:tempfile) + super + end end diff --git a/test/models/image_test.rb b/test/models/image_test.rb new file mode 100644 index 00000000000..c39f92b0dd6 --- /dev/null +++ b/test/models/image_test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ImageTest < ActiveSupport::TestCase + test 'remove exif data when image is updated' do + image_path = Rails.root.join('test/fixtures/files/articles/ogp_images/test.jpg') + image = Image.create!(user: users(:hajime), image: Rack::Test::UploadedFile.new(image_path, 'image/jpeg')) + + image.update!(image: Rack::Test::UploadedFile.new(image_path, 'image/jpeg')) + + updated_image = MiniMagick::Image.read(image.image.download) + assert_empty updated_image.exif + end +end From 4689d4b405c62e712b5794126e70b31fbf57802a Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Wed, 15 Jul 2026 02:43:55 +0900 Subject: [PATCH 69/80] =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E5=A4=B1=E6=95=97=E6=99=82=E3=81=AE=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=82=BF=E3=83=BC=E9=80=9A=E7=9F=A5=E3=82=92=E3=82=84=E3=82=81?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/prompts/pjord/mention_response_agent/instructions.txt.erb | 3 ++- app/prompts/pjord/product_review_agent/instructions.txt.erb | 3 ++- app/prompts/pjord/report_comment_agent/instructions.txt.erb | 3 ++- app/tools/external_content.rb | 2 +- test/agents/pjord/mention_response_agent_test.rb | 3 ++- test/agents/pjord/product_review_agent_test.rb | 3 ++- test/agents/pjord/report_comment_agent_test.rb | 3 ++- test/tools/external_content/github_reader_test.rb | 2 +- test/tools/external_content_tool_test.rb | 4 +++- 9 files changed, 17 insertions(+), 9 deletions(-) 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 08b1cac200e..6eebc209a98 100644 --- a/app/prompts/pjord/product_review_agent/instructions.txt.erb +++ b/app/prompts/pjord/product_review_agent/instructions.txt.erb @@ -20,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/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 b08d1691a16..e2aeb3bc21a 100644 --- a/test/agents/pjord/product_review_agent_test.rb +++ b/test/agents/pjord/product_review_agent_test.rb @@ -41,7 +41,8 @@ class Pjord::ProductReviewAgentTest < ActiveSupport::TestCase 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 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/tools/external_content/github_reader_test.rb b/test/tools/external_content/github_reader_test.rb index a206b70bb09..cf453d507ef 100644 --- a/test/tools/external_content/github_reader_test.rb +++ b/test/tools/external_content/github_reader_test.rb @@ -111,7 +111,7 @@ class ExternalContent::GithubReaderTest < ActiveSupport::TestCase end end - test 'asks Pjord to mention mentors when GitHub content cannot be fetched' do + test 'tells Pjord not to mention mentors when GitHub content cannot be fetched' do stub_request(:get, 'https://raw.githubusercontent.com/fjordllc/bootcamp/main/app/models/product.rb') .to_return(status: 404, body: 'Not Found') diff --git a/test/tools/external_content_tool_test.rb b/test/tools/external_content_tool_test.rb index f544fb440e5..16f44f44286 100644 --- a/test/tools/external_content_tool_test.rb +++ b/test/tools/external_content_tool_test.rb @@ -23,13 +23,15 @@ class ExternalContentToolTest < ActiveSupport::TestCase assert_not_includes result, 'ignore' end - test 'asks Pjord to mention mentors when external links cannot be fetched' do + test 'tells Pjord not to mention mentors when external links cannot be fetched' do stub_request(:get, 'https://example.com/unreadable') .to_return(status: 404, body: 'Not Found') result = @tool.execute(url: 'https://example.com/unreadable') assert_equal ExternalContent::UNREADABLE_URL_MESSAGE, result + assert_includes result, '@mentor にメンションしない' + assert_includes result, '取得できなかったことには言及しない' end test 'follows redirects' do From 9d0b59ebd81d17be2e5e78da25c078ed81466329 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 15 Jul 2026 21:03:09 +0900 Subject: [PATCH 70/80] Make visual regression scaffold runnable and verify CSS refactor parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Get the Playwright-based visual regression scaffold actually executing and use it to confirm the css_refactor branch does not change the rendered UI. Scaffold fixes: - Gemfile: capybara_screenshot_diff -> capybara-screenshot-diff (1.12, require: false so Bundler doesn't auto-require the deprecated entrypoint) - application_visual_test_case: require capybara_screenshot_diff/minitest and include the 1.12 Minitest assertions; blur_active_element=false (the playwright driver returns a Hash for document.activeElement); add an extra_mask param to capture for per-page masking Determinism (elements that varied run-to-run independent of CSS): - mask .a-user-icon (the avatar carries the class directly) and .random-tags-items (users index sidebar is a random tag sample) - mask the report_show "直近の日報" side panel (#side-tabs-content-1); its recent-reports order/height is non-deterministic under frozen time - comments.yml: give comment1 an explicit created_at so report1's two comments keep a stable order README: mark TODO 1-5 done, document the committed-baseline false-green gotcha (the gem compares against git show HEAD:) and the determinism findings. Verified locally (macOS, same browser): main CSS vs css_refactor CSS render identically across 6 pages (0 px on 4, <0.01% AA noise on 2), and two same-CSS runs are reproducible. No visual change from the refactor. Co-Authored-By: Claude Opus 4.8 --- Gemfile | 2 +- Gemfile.lock | 3 + test/application_visual_test_case.rb | 21 ++++-- test/fixtures/comments.yml | 4 ++ test/visual/README.md | 103 ++++++++++++++++++--------- test/visual/pages_visual_test.rb | 5 +- 6 files changed, 98 insertions(+), 40 deletions(-) diff --git a/Gemfile b/Gemfile index df160afe8ad..d896757f569 100644 --- a/Gemfile +++ b/Gemfile @@ -119,7 +119,7 @@ end group :test do gem 'capybara' gem 'capybara-playwright-driver', '~> 0.5.9' - gem 'capybara_screenshot_diff' + 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/test/application_visual_test_case.rb b/test/application_visual_test_case.rb index 107d9b0fd20..b5eb80f96c6 100644 --- a/test/application_visual_test_case.rb +++ b/test/application_visual_test_case.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'application_system_test_case' -require 'capybara/screenshot/diff' +require 'capybara_screenshot_diff/minitest' # Base class for visual regression tests. # @@ -15,7 +15,7 @@ # environment (the CI Ubuntu image or an equivalent Docker container). # See test/visual/README.md for the full workflow. class ApplicationVisualTestCase < ApplicationSystemTestCase - include Capybara::Screenshot::Diff + include CapybaraScreenshotDiff::Minitest::Assertions # Screenshots (baseline + diff) live under test/visual/screenshots. Capybara::Screenshot.save_path = 'test/visual/screenshots' @@ -26,8 +26,12 @@ class ApplicationVisualTestCase < ApplicationSystemTestCase TOLERANCE = 0.001 # Wait until the rendered image stops changing before snapping (async UI). Capybara::Screenshot.stability_time_limit = 0.5 - # Hide the text cursor so a blinking caret never causes a diff. - Capybara::Screenshot.blur_active_element = true + # 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 @@ -44,12 +48,14 @@ class ApplicationVisualTestCase < ApplicationSystemTestCase # 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 @@ -65,10 +71,13 @@ class ApplicationVisualTestCase < ApplicationSystemTestCase # Capture the current page and compare it against the baseline named +name+. # Waits for JS components, fonts, and lazy images before snapping. - def capture(name, mask: DEFAULT_MASK_SELECTORS) + # + # +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) + stabilize_page(mask + extra_mask) screenshot(name, tolerance: TOLERANCE) end 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/visual/README.md b/test/visual/README.md index 75ad7128bfe..6c6ccdf68cd 100644 --- a/test/visual/README.md +++ b/test/visual/README.md @@ -5,7 +5,7 @@ 変えないはずの変更」の安全網です。 - 基盤: 既存の Minitest system test(Capybara + `capybara-playwright-driver`) -- 比較: [`capybara_screenshot_diff`](https://github.com/donv/capybara_screenshot_diff) +- 比較: [`capybara-screenshot-diff`](https://github.com/donv/capybara-screenshot-diff)(1.12 系。`require: false` で導入し、ベースクラスで `capybara_screenshot_diff/minitest` を require) - ベースクラス: `test/application_visual_test_case.rb` - テスト: `test/visual/*_test.rb` - 画像: `test/visual/screenshots/`(ベースラインは git 管理、差分/一時画像は `.gitignore`) @@ -22,7 +22,7 @@ Ubuntu、またはそれに合わせた Docker コンテナです。ローカル ## セットアップ ```sh -bundle install # capybara_screenshot_diff を導入(Gemfile.lock 更新) +bundle install # capybara-screenshot-diff を導入(Gemfile.lock 更新済み) npx playwright install chromium ``` @@ -48,6 +48,12 @@ git commit -m "Add visual regression baselines" bin/rails test test/visual/pages_visual_test.rb ``` +> ⚠️ **ベースラインは commit していないと比較されません(false green の罠)。** +> このgemは working tree のスクショを **git 管理下のベースライン**(`git show +> HEAD:` で取り出す)と比較します。ベースラインが未追跡・未コミットだと +> 「新規ベースライン=pending 扱いで pass」になり、**差分があっても緑になります**。 +> 実際に比較させたいベースラインは必ず commit 済みにしておくこと。 + ### 3. 意図した変更でベースラインを更新 デザインを意図的に変えたら、該当ベースラインを削除して再生成し、差分画像を @@ -59,19 +65,38 @@ bin/rails test test/visual/pages_visual_test.rb -n /report_show/ git add test/visual/screenshots/report_show.png ``` -## このリファクタの検証(before / after ワンショット) +## このリファクタの検証(before / after)— ✅ 実施済み + +`css_refactor` が本当に見た目を変えていないかを、`main` の CSS と +`css_refactor` の CSS を **同一環境(同じ Mac・同じブラウザ・時刻固定)** で +描画して 6 ページをピクセル比較して裏取りしました。 -`css_refactor` ブランチが本当に見た目を変えていないかは、`main` で撮った -ベースラインをこのブランチで比較すれば確認できます(同一環境で実行すること)。 +やり方(README の commit 前提を避けるため、git を介さず直接比較する方式): ```sh -git switch main -bin/rails test test/visual/pages_visual_test.rb # main のベースライン生成 -git switch css_refactor -git checkout main -- test/visual/screenshots # main の画像を持ち込む -bin/rails test test/visual/pages_visual_test.rb # 差分ゼロなら見た目不変 +# 1. main のコンパイル済み CSS を app/assets/builds/ に入れて sprockets を warm、 +# 6ページ生成 → before セットとして退避 +# 2. css_refactor の CSS に差し替えて warm、6ページ生成 → after セット +# 3. before/after を ImageMagick で AE 比較(compare -metric AE a.png b.png) ``` +結果(before=main CSS / after=css_refactor CSS、決定化の対応後): + +| ページ | 差分px | 判定 | +|---|---|---| +| dashboard / report_form / users_index / lp_practices | 0 | 完全一致 | +| report_show | 129 (0.007%) | サブピクセルの AA ノイズ | +| user_profile | 46 (0.002%) | サブピクセルの AA ノイズ | + +→ **リファクタによる見た目の変化なし**を実描画で確認済み。残差はすべて許容値 +(`TOLERANCE = 0.001` = 0.1%)を大きく下回るアンチエイリアスのゆらぎ。 + +同一 CSS で 2 回撮って比較する「再現性テスト」でも全ページ差分 0〜100px 未満で、 +決定的に描画されることを確認しています。 + +> 補足: gem 標準の「main でベースライン生成 → ブランチで比較」フローでもよいが、 +> その場合はベースラインを **commit してから** 比較すること(上の false-green の罠)。 + ## 動的コンテンツの扱い `application_visual_test_case.rb` で以下を自動化しています。 @@ -81,30 +106,44 @@ bin/rails test test/visual/pages_visual_test.rb # 差分ゼロなら見 - アバター・草(グラフ)・時刻など非決定要素のマスク(`DEFAULT_MASK_SELECTORS`) - フォント・画像のロード待ち、描画の安定待ち(`stability_time_limit`) -ページ固有の動的要素は、各テストで `capture('name', mask: [...])` に -セレクタを渡してマスクを追加してください。 - -## 再開時の TODO(現状メモ) - -足場(ベースクラス・サンプルテスト・gem 追加・.gitignore・本ドキュメント)は -コミット済み。**実行系はまだ回していない**ので、再開時は以下を上から順に。 - -1. [ ] `bundle install` を実行して `Gemfile.lock` に `capybara_screenshot_diff` - を反映(この作業はまだ未実施)。 -2. [ ] `npx playwright install chromium` でブラウザを用意。 -3. [ ] `bin/rails test test/visual/pages_visual_test.rb` を **CI と同じ - Linux 環境**で実行し、ベースラインを生成 → 内容を確認して - `test/visual/screenshots/` をコミット。 -4. [ ] サンプルテストの assert セレクタ・マスク対象(`DEFAULT_MASK_SELECTORS`) - を実ページに合わせて微調整。差分画像 `*.diff.png` を見ながら安定化。 -5. [ ] 安定したら「このリファクタの検証」節の before/after 手順で - `main` ↔ `css_refactor` を比較し、見た目不変を実ブラウザで裏取り。 -6. [ ] CI(`.github/workflows/ci.yml`)へステップ追加(下記)。 +ページ固有の動的要素は、各テストで `capture('name', extra_mask: [...])` に +セレクタを渡すとデフォルトマスクに追加できます。 + +### 実際に踏んだ非決定要素(対応済み) + +検証中に、CSS とは無関係に「実行ごとに描画が変わる」箇所がいくつか見つかったので +対応済み。同種の揺れに当たったら参考に。 + +- **コメントの表示順**(report show): `report1` の 2 件のコメントが `created_at` + 同値でタイになり順序が非決定 → `test/fixtures/comments.yml` の `comment1` に + 明示 `created_at` を付与して決定化。 +- **アバター画像**: 実体は `img.a-user-icon`(img 自身がクラス保持)。旧セレクタ + `.a-user-icon img` では拾えないため `.a-user-icon` を DEFAULT_MASK に追加。 +- **ユーザータグのサイドバー**(users index): `_random_tags` が文字どおり + ランダム抽出 → `.random-tags-items` を DEFAULT_MASK に追加。 +- **「直近の日報」サイドパネル**(report show): `@recent_reports` の並び/末尾 + カードの高さが非決定 → report show テストで `#side-tabs-content-1` を extra_mask。 + +## 進捗と残 TODO + +ローカル(macOS)で実行系まで通し、リファクタの見た目不変を裏取り済み。 + +1. [x] `bundle install`(`Gemfile.lock` に `capybara-screenshot-diff` 反映済み)。 +2. [x] `npx playwright install chromium`。 +3. [x] `bin/rails test test/visual/pages_visual_test.rb` が 6 ページ green で実行可能。 +4. [x] マスク・並び順の微調整(上「実際に踏んだ非決定要素」参照)。同一 CSS 2 回撮りで + 全ページ再現性を確認。 +5. [x] before/after で `main` ↔ `css_refactor` を実描画比較し、**見た目不変を裏取り** + (上「このリファクタの検証」参照)。 +6. [ ] CI(`.github/workflows/ci.yml`)へステップ追加(任意・下記)。**未対応**。 7. [ ] 対象ページを代表画面から徐々に拡張。 -補足: CSS リファクタ本体(デッドコード削除・色の変数化など)は `css_refactor` -ブランチに別途コミット済みで、コンパイル後 CSS の値不変は検証済み。本ビジュアル -テストはその実描画での裏取りが目的。 +補足: +- CSS リファクタ本体(デッドコード削除・色の変数化など)は `css_refactor` に別途 + コミット済みで、コンパイル後 CSS の値不変も検証済み。本テストは実描画での裏取り。 +- ローカル検証は同一 Mac 環境での **CSS 差し替え比較**で行った(ベースラインを + リポジトリに commit していない = macOS 画像を履歴に残していない)。CI に載せる + 場合は §「CI への組み込み」の通り Linux 環境でベースラインを生成・コミットする。 ## CI への組み込み(任意) diff --git a/test/visual/pages_visual_test.rb b/test/visual/pages_visual_test.rb index f9f62fdedb2..0d9044b19d9 100644 --- a/test/visual/pages_visual_test.rb +++ b/test/visual/pages_visual_test.rb @@ -20,7 +20,10 @@ class PagesVisualTest < ApplicationVisualTestCase report = reports(:report1) visit_with_auth "/reports/#{report.id}", 'komagata' assert_selector '.page-content' - capture('report_show') + # The mentor "直近の日報" side panel lists the author's recent reports; + # under frozen time their order/last-card height is non-deterministic, so + # mask that panel (its card CSS is covered by the main report body). + capture('report_show', extra_mask: ['#side-tabs-content-1']) end test 'report form' do From 24ef8b56f7e2617485e9cc9f868bf51740c1dd06 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 15 Jul 2026 21:59:16 +0900 Subject: [PATCH 71/80] docs(visual): document local-only workflow, drop CI integration This is a local-use tool for checking visual shifts while editing CSS, not a CI gate. Rewrite the README around a commit-free before/after recipe: snapshot, edit CSS (+ tailwindcss:build), snapshot again, and diff the two PNG sets with ImageMagick (compare -metric AE), reading *.diff.png for any unintended shift. Reframe the "same environment" warning as "same machine for before/after", keep the gem's committed-baseline pass/fail flow as an opt-in note (with the false-green caveat), and record that CI integration is intentionally not done. Co-Authored-By: Claude Opus 4.8 --- test/visual/README.md | 117 +++++++++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 46 deletions(-) diff --git a/test/visual/README.md b/test/visual/README.md index 6c6ccdf68cd..b4280f818af 100644 --- a/test/visual/README.md +++ b/test/visual/README.md @@ -1,23 +1,25 @@ # ビジュアルリグレッションテスト -ページのスクリーンショットを撮り、コミット済みのベースライン画像とピクセル単位で -比較して **意図しない見た目の変化** を検出します。CSS リファクタなど「見た目を -変えないはずの変更」の安全網です。 +ページのスクリーンショットを撮り、ピクセル単位で比較して **意図しない見た目の +変化** を検出します。CSS を触るときに「レイアウトがズレていないか」を手元で +確かめるための、**ローカル専用**のツールです(CI には組み込みません)。 - 基盤: 既存の Minitest system test(Capybara + `capybara-playwright-driver`) - 比較: [`capybara-screenshot-diff`](https://github.com/donv/capybara-screenshot-diff)(1.12 系。`require: false` で導入し、ベースクラスで `capybara_screenshot_diff/minitest` を require) - ベースクラス: `test/application_visual_test_case.rb` - テスト: `test/visual/*_test.rb` -- 画像: `test/visual/screenshots/`(ベースラインは git 管理、差分/一時画像は `.gitignore`) +- 画像: `test/visual/screenshots/`(`.gitignore` 済み。**画像はコミットしない**) +- 画像比較には ImageMagick を使う: `brew install imagemagick` -## ⚠️ 最重要: 描画環境を固定する +## ⚠️ 最重要: before / after は同じマシンで撮る -スクリーンショットはフォントのアンチエイリアスなどが **OS によって異なる** ため、 -macOS で生成したベースラインを Linux(CI)で比較すると全ページ差分になります。 +スクリーンショットはフォントのアンチエイリアスなどが **OS・マシンによって微妙に +異なります**。別環境で撮った画像同士を比べると、CSS を一切変えていなくても全面 +差分になります。 -**ベースラインの生成と比較は必ず同じ環境で行ってください。** 推奨は CI と同じ -Ubuntu、またはそれに合わせた Docker コンテナです。ローカル Mac で撮った画像は -コミットしないこと。 +**「変更前」と「変更後」は必ず同じマシン・同じブラウザで撮って比較してください。** +撮った画像は環境依存なので **リポジトリにコミットしないこと**(`test/visual/ +screenshots/` は `.gitignore` 済み)。 ## セットアップ @@ -26,44 +28,68 @@ bundle install # capybara-screenshot-diff を導入(Gemfile.lo npx playwright install chromium ``` -## 使い方 +## ローカルで画面のズレをチェックする(普段使い) -### 1. ベースラインを生成(初回 / 意図的な変更時) - -ベースライン画像が無い状態でテストを実行すると、その回のスクショが新しい -ベースラインとして保存されます(テストは pending 扱い)。 +CSS を編集する前後でスクショを撮り、ピクセル比較して差分を見ます。**画像を +コミットする必要はありません**。テストは撮った画像を `test/visual/screenshots/` +に書き出すので、それを「変更前」「変更後」で退避して比べるだけです。 ```sh -bin/rails test test/visual/pages_visual_test.rb -git add test/visual/screenshots -git commit -m "Add visual regression baselines" -``` +# 0) (初回だけ)ブラウザと ImageMagick +npx playwright install chromium +brew install imagemagick -### 2. 比較(通常のテスト実行) +# 1) 変更前の状態でスナップショットを撮って退避 +bin/rails test test/visual/pages_visual_test.rb +mkdir -p tmp/visual/before && cp test/visual/screenshots/*.png tmp/visual/before/ -ベースラインが存在する状態で実行すると、現在の描画と比較します。差分が許容値を -超えるとテストが失敗し、`*.diff.png` が出力されます。 +# 2) ここで CSS を編集する +# app/assets/stylesheets/*.css を編集したら Tailwind を再ビルド: +bin/rails tailwindcss:build -```sh +# 3) 変更後をもう一度撮る +rm -f test/visual/screenshots/*.png bin/rails test test/visual/pages_visual_test.rb +mkdir -p tmp/visual/after && cp test/visual/screenshots/*.png tmp/visual/after/ + +# 4) 比較。差分ピクセル数を表示し、ズレた箇所は *.diff.png で可視化 +for f in tmp/visual/before/*.png; do + n=$(basename "$f") + d=$(compare -metric AE "$f" "tmp/visual/after/$n" "tmp/visual/${n%.png}.diff.png" 2>&1) + printf '%-16s %s px\n' "${n%.png}" "$d" +done ``` -> ⚠️ **ベースラインは commit していないと比較されません(false green の罠)。** -> このgemは working tree のスクショを **git 管理下のベースライン**(`git show -> HEAD:` で取り出す)と比較します。ベースラインが未追跡・未コミットだと -> 「新規ベースライン=pending 扱いで pass」になり、**差分があっても緑になります**。 -> 実際に比較させたいベースラインは必ず commit 済みにしておくこと。 +読み方: -### 3. 意図した変更でベースラインを更新 +- **0 または数十 px** … 実質一致(サブピクセルのアンチエイリアス揺れ)。ズレなし。 +- **数千 px 以上** … どこかがズレている。`tmp/visual/.diff.png` を開くと + 変わったピクセルが赤くハイライトされるので、そこを確認する。 -デザインを意図的に変えたら、該当ベースラインを削除して再生成し、差分画像を -レビューしてからコミットします。 +差分が「自分が意図した変更」なら OK。意図していない箇所が赤くなっていたら +レイアウト崩れなので直す。`tmp/` は `.gitignore` 済みなので後片付けも不要。 -```sh -rm test/visual/screenshots/report_show.png -bin/rails test test/visual/pages_visual_test.rb -n /report_show/ -git add test/visual/screenshots/report_show.png -``` +> 補足(最初の1ページが遅い / タイムアウトするとき): sprockets が CSS を初回 +> コンパイルするため、キャッシュが冷えていると最初のページ遷移が数十秒かかり +> ます。落ちたらもう一度実行すれば、2 回目以降は warm で速くなります。 + +### 対象ページを増やす + +`test/visual/pages_visual_test.rb` にテストを足すだけです。動的要素があれば +`capture('name', extra_mask: ['.selector'])` でマスクを追加します(次節参照)。 + +### (参考)gem 標準の pass/fail フロー + +`bin/rails test` の合否だけで判定したい場合、gem は working tree のスクショを +**git の HEAD にコミット済みのベースライン**(`git show HEAD:` で取得)と +比較します。つまりベースラインを一度コミットしておけば、以降は `bin/rails test` +の赤/緑でズレを検出できます。 + +> ⚠️ **未コミットのベースラインは比較されません(false green の罠)。** ローカルの +> `fail_if_new` は false なので、ベースラインが HEAD に無いと「新規=pass」になり、 +> 差分があっても緑になります。この方式を使うなら画像を commit すること(ただし +> 環境依存なので push はしない・ローカル限定運用)。普段使いは上の before/after +> 方式のほうが手軽で安全です。 ## このリファクタの検証(before / after)— ✅ 実施済み @@ -71,7 +97,7 @@ git add test/visual/screenshots/report_show.png `css_refactor` の CSS を **同一環境(同じ Mac・同じブラウザ・時刻固定)** で 描画して 6 ページをピクセル比較して裏取りしました。 -やり方(README の commit 前提を避けるため、git を介さず直接比較する方式): +やり方(上の「普段使い」と同じく、画像をコミットせず直接 before/after 比較): ```sh # 1. main のコンパイル済み CSS を app/assets/builds/ に入れて sprockets を warm、 @@ -135,19 +161,18 @@ git add test/visual/screenshots/report_show.png 全ページ再現性を確認。 5. [x] before/after で `main` ↔ `css_refactor` を実描画比較し、**見た目不変を裏取り** (上「このリファクタの検証」参照)。 -6. [ ] CI(`.github/workflows/ci.yml`)へステップ追加(任意・下記)。**未対応**。 +6. [—] CI 組み込みは **見送り**。スクショが環境依存で不安定になりやすいため、 + ローカルで手元確認する用途に割り切る。 7. [ ] 対象ページを代表画面から徐々に拡張。 補足: - CSS リファクタ本体(デッドコード削除・色の変数化など)は `css_refactor` に別途 コミット済みで、コンパイル後 CSS の値不変も検証済み。本テストは実描画での裏取り。 -- ローカル検証は同一 Mac 環境での **CSS 差し替え比較**で行った(ベースラインを - リポジトリに commit していない = macOS 画像を履歴に残していない)。CI に載せる - 場合は §「CI への組み込み」の通り Linux 環境でベースラインを生成・コミットする。 +- ローカル検証は同一 Mac 環境での **CSS 差し替え比較**で行った(画像はリポジトリに + コミットしていない = 環境依存の画像を履歴に残していない)。 -## CI への組み込み(任意) +## CI には組み込まない -既存の system test ジョブ(`.github/workflows/ci.yml`、ubuntu-latest + chromium) -と同じ環境なので、そこにビジュアルテストのステップと、失敗時の -`test/visual/screenshots/**/*.diff.png` のアーティファクト保存を追加すれば -そのまま回せます。ベースラインは必ずこの CI 環境で生成・更新してください。 +ビジュアル差分は OS・マシンでスクショが微妙に変わり CI では不安定になりやすいので、 +**あえて CI には載せず、ローカルで手元確認するツール**として運用します。上の +「ローカルで画面のズレをチェックする」手順を、CSS を触ったときに回してください。 From 1cf68389bdf65a17c180e36d477c0444338f2ec0 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 15 Jul 2026 22:16:37 +0900 Subject: [PATCH 72/80] Fix Layout/ExtraSpacing in visual test base class Co-Authored-By: Claude Opus 4.8 --- test/application_visual_test_case.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/application_visual_test_case.rb b/test/application_visual_test_case.rb index b5eb80f96c6..6c8a1e1254e 100644 --- a/test/application_visual_test_case.rb +++ b/test/application_visual_test_case.rb @@ -48,7 +48,7 @@ class ApplicationVisualTestCase < ApplicationSystemTestCase # 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', # the avatar itself carries this class (header "Me", comments) '.a-user-icon img', '.user-icon img', '.a-grass', From 21c2bb62b59282a4992f85d2a83a1253c3ccbd25 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 15 Jul 2026 22:42:01 +0900 Subject: [PATCH 73/80] Keep visual regression tests out of CI They are a local-only tool: baselines are environment-specific and intentionally uncommitted, so on CI the gem's fail_if_new (true when ENV["CI"] is set) fails every test with "No existing screenshot found". - .circleci/config.yml: exclude test/visual/ from the test glob - ApplicationVisualTestCase: skip when ENV["CI"] is set, as a safety net - README: document both guards Co-Authored-By: Claude Opus 4.8 --- .circleci/config.yml | 2 +- test/application_visual_test_case.rb | 6 ++++++ test/visual/README.md | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) 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/test/application_visual_test_case.rb b/test/application_visual_test_case.rb index 6c8a1e1254e..6f88cbd15e2 100644 --- a/test/application_visual_test_case.rb +++ b/test/application_visual_test_case.rb @@ -60,6 +60,12 @@ class ApplicationVisualTestCase < ApplicationSystemTestCase ].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) diff --git a/test/visual/README.md b/test/visual/README.md index b4280f818af..70b9d9d59f6 100644 --- a/test/visual/README.md +++ b/test/visual/README.md @@ -176,3 +176,11 @@ done ビジュアル差分は OS・マシンでスクショが微妙に変わり CI では不安定になりやすいので、 **あえて CI には載せず、ローカルで手元確認するツール**として運用します。上の 「ローカルで画面のズレをチェックする」手順を、CSS を触ったときに回してください。 + +CI から外すために 2 段で守っています: + +- `.circleci/config.yml` のテスト glob で `test/visual/` を除外 + (`... | grep -v '^test/visual/' | ...`)。CI はこれらを実行しない。 +- 保険として、`ApplicationVisualTestCase` の setup で `ENV['CI']` がある場合は + `skip` する。CI 環境では gem の `fail_if_new` が true になり、コミット済み + ベースラインが無いと全テストが失敗するため(この失敗を避ける)。 From 339b5e3705bfa7f1028093dc949347273d6b5cde Mon Sep 17 00:00:00 2001 From: zamami Date: Thu, 16 Jul 2026 11:36:22 +0900 Subject: [PATCH 74/80] =?UTF-8?q?=E3=82=B5=E3=83=A0=E3=83=8D=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E3=83=90=E3=83=83=E3=82=AF=E3=83=95=E3=82=A3?= =?UTF-8?q?=E3=83=AB=E5=87=A6=E7=90=86=E3=82=92cloudbuild=E3=81=AEoneshot?= =?UTF-8?q?=E3=82=BF=E3=82=B9=E3=82=AF=E5=86=85=E3=81=A7=E5=AE=9F=E8=A1=8C?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tasks/bootcamp.rake | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/tasks/bootcamp.rake b/lib/tasks/bootcamp.rake index 593875d2b41..2319c2f016b 100644 --- a/lib/tasks/bootcamp.rake +++ b/lib/tasks/bootcamp.rake @@ -57,14 +57,10 @@ namespace :bootcamp do puts '== START Cloud Build Task ==' Rake::Task['smart_search:generate_all'].invoke + BulkGenerateMovieThumbnailJob.perform_now puts '== END Cloud Build Task ==' end - - desc 'Backfill thumbnails for existing movies' - task backfill_movie_thumbnails: :environment do - BulkGenerateMovieThumbnailJob.perform_now - end end namespace :statistics do From 82909dc5dc0a7589498ed6fe841f3e020d1804ef Mon Sep 17 00:00:00 2001 From: mikan-v7y Date: Thu, 16 Jul 2026 12:15:31 +0900 Subject: [PATCH 75/80] =?UTF-8?q?checks.yml=E3=81=AEproduct=E3=81=AE?= =?UTF-8?q?=E3=82=B9=E3=83=9A=E3=83=AB=E3=83=9F=E3=82=B9=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/fixtures/checks.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/fixtures/checks.yml b/test/fixtures/checks.yml index 3097651c5d1..ddce08ace66 100644 --- a/test/fixtures/checks.yml +++ b/test/fixtures/checks.yml @@ -22,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) From 3925d4aaa993e1b88294381ea8f9425588e1faaf Mon Sep 17 00:00:00 2001 From: mikan-v7y Date: Thu, 16 Jul 2026 13:02:58 +0900 Subject: [PATCH 76/80] =?UTF-8?q?fixture=E5=90=8D=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=AB=E4=BC=B4=E3=81=84=E5=8F=82=E7=85=A7=E7=AE=87=E6=89=80?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/deliveries/activity_delivery_test.rb | 2 +- test/mailers/activity_mailer_test.rb | 8 ++++---- test/mailers/previews/activity_mailer_preview.rb | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) 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/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 From aef4282e4b1526d147ca400db3d4a367725a120a Mon Sep 17 00:00:00 2001 From: mikan-v7y Date: Thu, 16 Jul 2026 13:12:19 +0900 Subject: [PATCH 77/80] =?UTF-8?q?db/fixtures/checks.yml=E3=81=AE=E3=82=AD?= =?UTF-8?q?=E3=83=BC=E5=90=8D=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/fixtures/checks.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/db/fixtures/checks.yml b/db/fixtures/checks.yml index 954740b01ef..bfafc8bef63 100644 --- a/db/fixtures/checks.yml +++ b/db/fixtures/checks.yml @@ -22,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) From 0f7055305c9b9668c309a07db0b82c06353f6fd7 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Fri, 17 Jul 2026 16:38:23 +0900 Subject: [PATCH 78/80] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=83=B3=E3=82=B0task=E3=81=ABAPP=5FHOST=5FNAME=E3=81=AE?= =?UTF-8?q?=E6=97=A2=E5=AE=9A=E5=80=A4=E3=82=92=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cloudbuild/cloudbuild-task.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.cloudbuild/cloudbuild-task.yaml b/.cloudbuild/cloudbuild-task.yaml index 72c39e4e29a..be6292ba38c 100644 --- a/.cloudbuild/cloudbuild-task.yaml +++ b/.cloudbuild/cloudbuild-task.yaml @@ -35,6 +35,7 @@ steps: - "-c" - |- eval "$(.cloudbuild/cloud-build-env exports)" + export APP_HOST_NAME="${APP_HOST_NAME:-bootcamp-staging.fjord.jp}" bin/rails bootcamp:oneshot:cloudbuild - id: Kill_SqlProxy name: gcr.io/cloud-builders/docker @@ -50,7 +51,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: _ From a0c36cc882e89b00ec597ced13205e84267ee5e7 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Fri, 17 Jul 2026 19:40:59 +0900 Subject: [PATCH 79/80] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=83=B3=E3=82=B0task=E3=81=ABSECRET=5FKEY=5FBASE=E3=81=AE?= =?UTF-8?q?=E6=97=A2=E5=AE=9A=E5=80=A4=E3=82=92=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cloudbuild/cloudbuild-task.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.cloudbuild/cloudbuild-task.yaml b/.cloudbuild/cloudbuild-task.yaml index be6292ba38c..2ab67bf30d7 100644 --- a/.cloudbuild/cloudbuild-task.yaml +++ b/.cloudbuild/cloudbuild-task.yaml @@ -36,6 +36,7 @@ steps: - |- eval "$(.cloudbuild/cloud-build-env exports)" export APP_HOST_NAME="${APP_HOST_NAME:-bootcamp-staging.fjord.jp}" + export SECRET_KEY_BASE="${SECRET_KEY_BASE:-dummy}" bin/rails bootcamp:oneshot:cloudbuild - id: Kill_SqlProxy name: gcr.io/cloud-builders/docker From aae2b2354621b63000556df5eb293c3c915c9261 Mon Sep 17 00:00:00 2001 From: Masaki Komagata Date: Sat, 18 Jul 2026 00:15:49 +0900 Subject: [PATCH 80/80] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=83=B3=E3=82=B0task=E3=81=ABDB=E6=8E=A5=E7=B6=9A=E7=92=B0?= =?UTF-8?q?=E5=A2=83=E5=A4=89=E6=95=B0=E3=82=92=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cloudbuild/cloudbuild-task.yaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.cloudbuild/cloudbuild-task.yaml b/.cloudbuild/cloudbuild-task.yaml index 2ab67bf30d7..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,9 +43,6 @@ steps: - bash - "-c" - |- - eval "$(.cloudbuild/cloud-build-env exports)" - export APP_HOST_NAME="${APP_HOST_NAME:-bootcamp-staging.fjord.jp}" - export SECRET_KEY_BASE="${SECRET_KEY_BASE:-dummy}" bin/rails bootcamp:oneshot:cloudbuild - id: Kill_SqlProxy name: gcr.io/cloud-builders/docker