diff --git a/app/controllers/practices/reports_controller.rb b/app/controllers/practices/reports_controller.rb index 8b116b29c0f..724a37a00d6 100644 --- a/app/controllers/practices/reports_controller.rb +++ b/app/controllers/practices/reports_controller.rb @@ -3,6 +3,22 @@ class Practices::ReportsController < ApplicationController def index @practice = Practice.find(params[:practice_id]) - @reports = @practice.reports.list.page(params[:page]) + @include_source = include_source? + @reports = + if @include_source + Report.for_practice_including_source(@practice) + else + @practice.reports + end + @reports = @reports.list.page(params[:page]) + end + + private + + def include_source? + # 給付金コースの場合、include_sourceはデフォルトでtrue + return false unless @practice.grant_course? + + params.fetch(:include_source, 'true') == 'true' end end diff --git a/app/helpers/page_tabs/practices_helper.rb b/app/helpers/page_tabs/practices_helper.rb index 1733223eb0f..e1917e2f1ac 100644 --- a/app/helpers/page_tabs/practices_helper.rb +++ b/app/helpers/page_tabs/practices_helper.rb @@ -2,10 +2,12 @@ module PageTabs module PracticesHelper - def practice_page_tabs(practice, active_tab:) + def practice_page_tabs(practice, active_tab:, include_source: nil) + # include_source未指定(nil)時は、給付金コースならtrue + include_source = practice.grant_course? if include_source.nil? tabs = [] tabs << { name: 'プラクティス', link: practice_path(practice) } - tabs << { name: '日報', link: practice_reports_path(practice), count: practice.reports.length } + tabs << { name: '日報', link: practice_reports_path(practice), count: practice.reports_count(include_source:) } tabs << { name: '質問', link: practice_questions_path(practice), count: practice.questions.length } tabs << { name: 'Docs', link: practice_pages_path(practice), count: practice.pages.length } tabs << { name: '動画', link: practice_movies_path(practice), count: practice.movies.length } if movie_available? diff --git a/app/models/practice.rb b/app/models/practice.rb index 986ce8ae0eb..6f286342328 100644 --- a/app/models/practice.rb +++ b/app/models/practice.rb @@ -207,6 +207,17 @@ def include_must_read_books? practices_books.any?(&:must_read) end + def grant_course? + source_id.present? + end + + def reports_count(include_source: nil) + return reports.count unless include_source + + ids = [id, source_id].compact + Report.joins(:practices).where(practices: { id: ids }).distinct.count + end + private def total_learning_minute(report) diff --git a/app/models/report.rb b/app/models/report.rb index 4e7c2f0a2a2..87ae59b1f7c 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -61,6 +61,11 @@ class Report < ApplicationRecord # rubocop:todo Metrics/ClassLength scope :user, ->(user) { where(user_id: user.id) } + scope :for_practice_including_source, lambda { |practice| + ids = [practice.id, practice.source_id].compact + joins(:practices).where(practices: { id: ids }).distinct + } + def self.ransackable_attributes(_auth_object = nil) %w[title description reported_on emotion wip created_at updated_at user_id] end diff --git a/app/views/practices/reports/index.html.slim b/app/views/practices/reports/index.html.slim index 358fd60bee3..ca353fb3556 100644 --- a/app/views/practices/reports/index.html.slim +++ b/app/views/practices/reports/index.html.slim @@ -7,7 +7,19 @@ category: category, practice: @practice -= practice_page_tabs(@practice, active_tab: '日報') += practice_page_tabs(@practice, active_tab: '日報', include_source: @include_source) + +- if @practice.grant_course? + nav.pill-nav + ul.pill-nav__items + li.pill-nav__item + = link_to '全て', + practice_reports_path(@practice, include_source: true), + class: ['pill-nav__item-link', ('is-active' if @include_source)] + li.pill-nav__item + = link_to '給付金コース', + practice_reports_path(@practice, include_source: false), + class: ['pill-nav__item-link', ('is-active' if !@include_source)] .page-body .container.is-md diff --git a/db/fixtures/categories.yml b/db/fixtures/categories.yml index efae29aa68d..6deb90bd8f4 100644 --- a/db/fixtures/categories.yml +++ b/db/fixtures/categories.yml @@ -112,3 +112,8 @@ category23: name: "Ruby on Rails(Rails 6.1版)" slug: "ruby-on-rails" description: "まずはここからはじめましょう。ここでの学習の進め方を学びます。" + +category24: + name: "Mac OS X(Reスキル)" + slug: "mac-os-x-reskill" + description: "Railsコースのカテゴリをコピーしたカテゴリです。" diff --git a/db/fixtures/categories_practices.yml b/db/fixtures/categories_practices.yml index a1af1d1bd73..d7143eadaec 100644 --- a/db/fixtures/categories_practices.yml +++ b/db/fixtures/categories_practices.yml @@ -356,3 +356,13 @@ categories_practice66_2: practice: practice66 category: category22 position: 3 + +categories_practice67: + practice: practice113 + category: category24 + position: 1 + +categories_practice68: + practice: practice114 + category: category24 + position: 2 diff --git a/db/fixtures/courses_categories.yml b/db/fixtures/courses_categories.yml index 26bc579255a..e5adfb0bd52 100644 --- a/db/fixtures/courses_categories.yml +++ b/db/fixtures/courses_categories.yml @@ -222,3 +222,8 @@ courses_category45: course: course5 category: category23 position: 18 + +courses_category46: + course: course5 + category: category24 + position: 19 diff --git a/db/fixtures/discord_profiles.yml b/db/fixtures/discord_profiles.yml index e6dc9568f71..b812ac51866 100644 --- a/db/fixtures/discord_profiles.yml +++ b/db/fixtures/discord_profiles.yml @@ -335,3 +335,8 @@ discord_profile_new-mentor: user: new-mentor account_name: new-mentor times_url: + +discord_profile_grant-course: + user: grant-course + account_name: grant-course + times_url: diff --git a/db/fixtures/practices.yml b/db/fixtures/practices.yml index 24e8ebcf6af..bc5388f9def 100644 --- a/db/fixtures/practices.yml +++ b/db/fixtures/practices.yml @@ -764,3 +764,15 @@ practice112: goal: "goal..." include_progress: true memo: "memo for mentors..." + +practice113: + title: "OS X Mountain Lionをクリーンインストールする(Reスキル)" + description: "Railsコースのプラクティスをコピーしたプラクティスです。" + goal: "goal..." + source_id: <%= ActiveRecord::FixtureSet.identify(:practice1) %> + +practice114: + title: "Terminalの基礎を覚える(Reスキル)" + description: "Railsコースのプラクティスをコピーしたプラクティスです。" + goal: "goal..." + source_id: <%= ActiveRecord::FixtureSet.identify(:practice2) %> diff --git a/db/fixtures/reports.yml b/db/fixtures/reports.yml index b841f3a1892..7e24674c3a5 100644 --- a/db/fixtures/reports.yml +++ b/db/fixtures/reports.yml @@ -505,3 +505,38 @@ report90: description: |- たくさんのスタンプがつきました。 reported_on: "2025-10-01 01:00:00" + +report91: + user: grant-course + title: 給付金コースのプラクティスの日報 + emotion: 2 + description: |- + 給付金コースのプラクティスの日報です。 + practices: practice113 + reported_on: "2026-01-01" + +report92: + user: komagata + title: Railsコースのコピー元プラクティスの日報 + emotion: 2 + description: |- + Railsコースのコピー元プラクティスの日報です。 + practices: practice1 + reported_on: "2026-01-02" + +report93: + user: komagata + title: Railsコースのコピー元プラクティス、給付金コースのプラクティスの両方に関連する日報報 + emotion: 2 + description: |- + Railsコースのコピー元プラクティス、給付金コースのプラクティスの両方に関連する日報報 + practices: practice1, practice113 + reported_on: "2026-01-03" + +report94: + user: komagata + title: プラクティスに関連しない日報 + emotion: 2 + description: |- + プラクティスに関連しない日報 + reported_on: "2026-01-04" diff --git a/db/fixtures/talks.yml b/db/fixtures/talks.yml index e50167eb574..50256007cf4 100644 --- a/db/fixtures/talks.yml +++ b/db/fixtures/talks.yml @@ -235,3 +235,7 @@ talk_pjord: talk_new-mentor: user: new-mentor action_completed: true + +talk_grant_course: + user: grant-course + action_completed: true diff --git a/db/fixtures/users.yml b/db/fixtures/users.yml index d7136570944..8d9ca6b7357 100644 --- a/db/fixtures/users.yml +++ b/db/fixtures/users.yml @@ -1555,3 +1555,23 @@ new-mentor: created_at: <%= Time.current %> sent_student_followup_message: true last_activity_at: <%= Time.current %> + +grant-course: + login_name: grant-course + email: grant-course@fjord.jp + crypted_password: $2a$10$n/xv4/1luueN6plzm2OyDezWlZFyGHjQEf4hwAW1r3k.lCm0frPK. # testtest + salt: zW3kQ9ubsxQQtzzzs4ap + name: 給付金コースのユーザー + name_kana: キュウフキンコースノユーザー + github_account: grant-course + twitter_account: grant-course + facebook_url: https://www.facebook.com/fjordllc/grant-course + blog_url: http://grant-course.org + description: "給付金コースを受講中のユーザーです。" + course: course5 + job: office_worker + os: mac + organization: Rails大学 + updated_at: "2025-01-01 00:00:00" + created_at: "2025-01-01 00:00:00" + last_activity_at: "2025-01-01 00:00:00" diff --git a/test/fixtures/learning_times.yml b/test/fixtures/learning_times.yml index d4babf78703..019cefe961c 100644 --- a/test/fixtures/learning_times.yml +++ b/test/fixtures/learning_times.yml @@ -37,3 +37,8 @@ learning_time8: report: report10 started_at: 2020-09-10 12:00:00 finished_at: 2020-09-10 13:15:00 + +learning_time9: + report: report78 + started_at: 2026-01-01 00:00:00 + finished_at: 2026-01-01 01:00:00 diff --git a/test/fixtures/practices.yml b/test/fixtures/practices.yml index 8ff61dbf322..1bbaa085aed 100644 --- a/test/fixtures/practices.yml +++ b/test/fixtures/practices.yml @@ -390,3 +390,25 @@ practice63: goal: "goal..." include_progress: true memo: "memo for mentors..." + +practice64: + title: "Railsコースのコピー元プラクティス" + description: "Railsコースのコピー元プラクティスです。" + goal: "goal..." + +practice65: + title: "給付金コースのプラクティス" + description: "給付金コースのプラクティスです。" + goal: "goal..." + source_id: <%= ActiveRecord::FixtureSet.identify(:practice64) %> + +practice66: + title: "日報が存在しないRailsコースのコピー元プラクティス" + description: "日報が存在しないRailsコースのコピー元プラクティスです。" + goal: "goal..." + +practice67: + title: "日報が存在しない給付金コースのプラクティス" + description: "日報が存在しない給付金コースのプラクティスです。" + goal: "goal..." + source_id: <%= ActiveRecord::FixtureSet.identify(:practice66) %> diff --git a/test/fixtures/reports.yml b/test/fixtures/reports.yml index 96ac2491bd9..77f43251cd5 100644 --- a/test/fixtures/reports.yml +++ b/test/fixtures/reports.yml @@ -358,3 +358,42 @@ report75: description: WIPです wip: true reported_on: "2022-04-02" + +report76: + user: komagata + title: "Railsコースのコピー元プラクティスの日報" + emotion: 1 + description: Railsコースのコピー元プラクティスの日報です。 + practices: practice64 + reported_on: "2026-01-01" + +report77: + user: komagata + title: "給付金コースのプラクティスの日報" + emotion: 1 + description: 給付金コースのプラクティスの日報です。 + practices: practice65 + reported_on: "2026-01-02" + +report78: + user: komagata + title: "Railsコースのコピー元プラクティス、給付金コースのプラクティスの両方に関連する日報" + emotion: 1 + description: Railsコースのコピー元プラクティス、給付金コースのプラクティスの両方に関連する日報です。 + practices: practice64, practice65 + reported_on: "2026-01-03" + +report79: + user: komagata + title: "給付金コースではないプラクティスの日報" + emotion: 1 + description: 給付金コースではないプラクティスの日報です。 + practices: practice1 + reported_on: "2026-01-04" + +report80: + user: komagata + title: "プラクティスに関連しない日報" + emotion: 1 + description: プラクティスに関連しない日報です。 + reported_on: "2026-01-05" diff --git a/test/integration/api/reports/unchecked_test.rb b/test/integration/api/reports/unchecked_test.rb index c2f5ab0ec18..c537e847fe3 100644 --- a/test/integration/api/reports/unchecked_test.rb +++ b/test/integration/api/reports/unchecked_test.rb @@ -13,6 +13,6 @@ class API::Reports::UncheckedTest < ActionDispatch::IntegrationTest get counts_api_reports_unchecked_index_path(format: :text), headers: { 'Authorization' => "Bearer #{token}" } assert_response :ok - assert_match '65件', response.body + assert_match '70件', response.body end end diff --git a/test/models/practice_test.rb b/test/models/practice_test.rb index 8f2514a039b..684a35447b1 100644 --- a/test/models/practice_test.rb +++ b/test/models/practice_test.rb @@ -87,4 +87,62 @@ class PracticeTest < ActiveSupport::TestCase practice.update!(source_id: 99_999) end end + + test '#grant_course? returns true when practice has source' do + assert Practice.new(source_id: 1).grant_course? + assert_not Practice.new(source_id: nil).grant_course? + end + + test '#reports_count sums reports of self and source when include_source is true' do + source = practices(:practice66) + practice = practices(:practice67) + + assert_equal 0, practice.reports_count(include_source: true) + Report.create!( + user: users(:komagata), + title: '日報が存在しないRailsコースのコピー元プラクティスの日報', + description: '日報が存在しないRailsコースのコピー元プラクティスの日報です。', + practices: [source], + reported_on: Time.zone.today + ) + Report.create!( + user: users(:komagata), + title: '日報が存在しない給付金コースのプラクティスの日報', + description: '日報が存在しない給付金コースのプラクティスの日報です。', + practices: [practice], + reported_on: Time.zone.today - 1 + ) + + assert_equal 2, practice.reports_count(include_source: true) + end + + test '#reports_count counts only self reports when include_source is false' do + practice = practices(:practice66) + + assert_equal 0, practice.reports_count(include_source: false) + Report.create!( + user: users(:komagata), + title: '日報が存在しないRailsコースのコピー元プラクティスの日報', + description: '日報が存在しないRailsコースのコピー元プラクティスの日報です。', + practices: [practice], + reported_on: Time.zone.today + ) + + assert_equal 1, practice.reports_count(include_source: false) + end + + test '#reports_count does not double count reports when associated with both source and practice' do + source = practices(:practice66) + practice = practices(:practice67) + Report.create!( + user: users(:komagata), + title: '日報が存在しないRailsコースのコピー元プラクティス、日報が存在しない給付金コースのプラクティスの両方に関連する日報', + description: '日報が存在しないRailsコースのプラクティス、日報が存在しない給付金コースのプラクティスの両方に関連する日報です。', + practices: [source, practice], + reported_on: Time.zone.today + ) + + assert_equal 1, practice.reports_count(include_source: false) + assert_equal 1, practice.reports_count(include_source: true) + end end diff --git a/test/models/report_test.rb b/test/models/report_test.rb index 2c64e8b351e..8e140dd6701 100644 --- a/test/models/report_test.rb +++ b/test/models/report_test.rb @@ -73,4 +73,20 @@ class ReportTest < ActiveSupport::TestCase assert_empty unchecked_report.checks assert_includes Report.unchecked, unchecked_report end + + test '.for_practice_including_source returns practice and source reports' do + practice = practices(:practice65) + practice_report = reports(:report77) + source_report = reports(:report76) + practice_and_source_report = reports(:report78) + other_report = reports(:report79) + unrelated_report = reports(:report80) + result = Report.for_practice_including_source(practice) + + assert_includes result, practice_report + assert_includes result, source_report + assert_includes result, practice_and_source_report + assert_not_includes result, other_report + assert_not_includes result, unrelated_report + end end diff --git a/test/system/practice/reports_test.rb b/test/system/practice/reports_test.rb index d3d0b38317e..2cdd3dc482c 100644 --- a/test/system/practice/reports_test.rb +++ b/test/system/practice/reports_test.rb @@ -9,4 +9,48 @@ class Practice::ReportsTest < ApplicationSystemTestCase assert_selector 'img[alt="positive"]', count: 2 assert_selector '.card-list-item-title__link', text: '1時間だけ学習' end + + test 'grant filter is not present on rails course practice' do + visit_with_auth "/practices/#{practices(:practice64).id}/reports", 'komagata' + + assert_no_selector 'a.pill-nav__item-link', text: '全て' + assert_no_selector 'a.pill-nav__item-link', text: '給付金コース' + end + + test 'grant filter is present on grant course practice' do + visit_with_auth "/practices/#{practices(:practice65).id}/reports", 'komagata' + + assert_selector 'a.pill-nav__item-link', text: '全て' + assert_selector 'a.pill-nav__item-link', text: '給付金コース' + end + + test 'grant filter shows both copied and grant course reports when "全て" is selected' do + visit_with_auth "/practices/#{practices(:practice65).id}/reports", 'komagata' + + assert_selector 'a.pill-nav__item-link.is-active', text: '全て' + assert_selector '.card-list-item-title__link', text: 'Railsコースのコピー元プラクティスの日報' + assert_selector '.card-list-item-title__link', text: '給付金コースのプラクティスの日報' + end + + test 'grant filter shows only grant course reports when "給付金コース" is selected' do + visit_with_auth "/practices/#{practices(:practice65).id}/reports", 'komagata' + within '.pill-nav' do + click_on '給付金コース' + end + + assert_selector 'a.pill-nav__item-link.is-active', text: '給付金コース' + assert_no_selector '.card-list-item-title__link', text: 'Railsコースのコピー元プラクティスの日報' + assert_selector '.card-list-item-title__link', text: '給付金コースのプラクティスの日報' + end + + test 'grant filter shows empty message when no reports exist' do + visit_with_auth "/practices/#{practices(:practice67).id}/reports", 'komagata' + within '.pill-nav' do + click_on '給付金コース' + end + + assert_selector 'a.pill-nav__item-link.is-active', text: '給付金コース' + assert_no_selector '.card-list-item-title__link' + assert_selector '.o-empty-message__text', text: '日報はまだありません。' + end end