Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/controllers/hibernation_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def show; end

def new
@hibernation = Hibernation.new
@holding_regular_events = RegularEvent.organizer_event(current_user).holding
end

def create
Expand All @@ -18,11 +19,11 @@ def create
destroy_subscription!
notify_to_chat
notify_to_mentors_and_admins
current_user.cancel_participation_from_regular_events
current_user.delete_and_assign_new_organizer
current_user.clean_up_regular_events
logout
redirect_to hibernation_path
else
@holding_regular_events = RegularEvent.organizer_event(current_user).holding
render :new
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/regular_events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def create
set_wip
if @regular_event.save
update_published_at
Organizer.create(user_id: current_user.id, regular_event_id: @regular_event.id)
@regular_event.regular_event_organizers.find_or_create_by(user: current_user)
Copy link
Copy Markdown
Contributor Author

@yokomaru yokomaru Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • イベント登録画面にて、主催者を自分1人にしてイベント作成を行うと、毎回バリデーションエラー(主催者重複違反)となり検証や調査の妨げになるため、すでに登録済みの場合は作成処理を行わないようにfind_or_create_byを使って修正しました
  • 本来であればエラーハンドリングなども含めて処理の見直しが必要だと思われるので、別途立てたIssueで検討になると思います
  • 併せてよりRailsっぽい書き方にするために、外部キー指定ではなく、@regular_eventを起点にしてオブジェクト指定で更新するように修正しています

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LGTM]
#find_or_create_by にしたのはよいですね!
オブジェクトを渡すのも Rails っぽくてよいですね〜

Comment thread
coderabbitai[bot] marked this conversation as resolved.
ActiveSupport::Notifications.instrument('regular_event.create', regular_event: @regular_event)
set_all_user_participants_and_watchers
select_redirect_path
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/retirement_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ class RetirementController < ApplicationController

def show; end

def new; end
def new
@holding_regular_events = RegularEvent.organizer_event(current_user).holding
end

def create
retirement = Retirement.by_self(retire_reason_params, user: current_user)
Expand All @@ -15,6 +17,7 @@ def create
redirect_to retirement_url
else
current_user.retired_on = nil
@holding_regular_events = RegularEvent.organizer_event(current_user).holding
render :new
end
end
Expand Down
8 changes: 5 additions & 3 deletions app/controllers/training_completion_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ class TrainingCompletionController < ApplicationController

def show; end

def new; end
def new
@holding_regular_events = RegularEvent.organizer_event(current_user).holding
end

def create
current_user.assign_attributes(training_complete_params)
current_user.training_completed_at = Time.current
if current_user.save(context: :training_completion)
user = current_user
current_user.cancel_participation_from_regular_events
current_user.delete_and_assign_new_organizer
current_user.clean_up_regular_events
ActiveSupport::Notifications.instrument('training_completion.create', user:)
user.clear_github_data
notify_to_user(user)
Expand All @@ -25,6 +26,7 @@ def create
redirect_to training_completion_url
else
current_user.training_completed_at = nil
@holding_regular_events = RegularEvent.organizer_event(current_user).holding
render :new
end
end
Expand Down
6 changes: 0 additions & 6 deletions app/models/organizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,4 @@ class Organizer < ApplicationRecord
belongs_to :regular_event

validates :user_id, uniqueness: { scope: :regular_event_id }

def delete_and_assign_new
event = regular_event
delete
event.assign_admin_as_organizer_if_none
end
end
24 changes: 17 additions & 7 deletions app/models/regular_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ class RegularEvent < ApplicationRecord # rubocop:disable Metrics/ClassLength

belongs_to :user
has_many :organizers, dependent: :destroy
# TODO: テーブル名を変更したら修正する
has_many :regular_event_organizers, class_name: 'Organizer', dependent: :destroy
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#9640 (comment) にあるように、一時的に変更した関連を使用して修正しています。テーブル名変更時に修正いたします。

has_many :users, through: :organizers
has_many :regular_event_repeat_rules, dependent: :destroy
accepts_nested_attributes_for :regular_event_repeat_rules, allow_destroy: true
Expand Down Expand Up @@ -120,13 +122,6 @@ def participated_by?(user)
regular_event_participations.find_by(user_id: user.id).present?
end

def assign_admin_as_organizer_if_none
return if organizers.exists?

admin_user = User.find_by(login_name: User::DEFAULT_REGULAR_EVENT_ORGANIZER)
Organizer.new(user: admin_user, regular_event: self).save if admin_user
end

def all_scheduled_dates(
from: Date.current,
to: Date.current.next_year
Expand Down Expand Up @@ -159,6 +154,21 @@ def publish_with_announcement?
wants_announcement? && !wip?
end

# 定期イベントは主催者が1人以上必要なため
# 主催者が1人しかいない場合はイベントを終了状態にし
# それ以外の場合は主催者のみを削除する
#
# TODO: 本来は「主催者が0人のイベントは無効」という制約をバリデーションで担保する形にしたい
# https://github.com/fjordllc/bootcamp/pull/9732#discussion_r2969273381
def close_or_destroy_organizer(user)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

実装は問題ないと思うのですが、ここの意図としては、

  • 休会や退会にともなって、定期イベントから主催者を削除したい
  • しかし、主催者不在の定期イベントは存在できない
  • 従って、削除される主催者が唯一の主催者である場合、定期イベントを終了状態にする

という感じだと思います。このうち、「主催者不在の定期イベントは存在できない」要件は今回に限った話ではなさそうであり、モデルのバリデーションの一部になるのが本来の姿のように思います。
そうすると、主催者の削除と、イベントの状態確認・終了状態への遷移は異なるレイヤーの出来事として扱いたくなります。主催者を削除した結果、結果的に定期イベントが不正な状態になり、終了状態になる、という流れです。
しかし、不正な状態(バリデーションが通らない状態)なら自動的に終了状態になる、となると影響範囲が大きく、望んでいない挙動になってしまうでしょう。
なので、メソッドとしては、

# これは擬似コードです
event.remove_organizer
if event.invalid? # 主催者がいない!
  event.finished!
end
event.save

のような感じにして、バリデーションとして、

# これは擬似コードです
validate do
  errors.add if organizers.size.zero? && active # activeというのは、終了状態ではない、くらいの意味
end

みたいにしてあげると綺麗かな、という感じです。

ここまでやると少し大掛かりなので、今現状では、メソッドの先頭にコメントを書いておくだけでもよいと思います。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@okuramasafumi
流れ的にはそれが自然ではありますね〜。
今回何でこういう状態になっているかというと、元々の仕様がもっとややこしくて。
その仕様をかなり整理して、今の状態になっています。
なので上記変更は別タスクで対応させていただければと思うのですがいかがでしょうか 🙏

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@okuramasafumi
ご指摘ありがとうございます!

本来はバリデーションで担保すべきという点や主催者の削除とイベントの状態確認・終了状態への遷移を異なるレイヤーの出来事として扱うという考え方につきまして、とても勉強になりました。
この2つが分離できると責務も明確になり設計としてもスッキリするなと感じました!

今回は影響範囲も考慮し、まずはコメントで意図を補足する形にしようと思います。

残すべきコメントとしては、メソッドの意図(主催者が0人にならないようにするための処理)+TODOとして将来的にバリデーションで担保することを記載するイメージで問題ないでしょうか?

# 定期イベントは主催者が1人以上必要なため
# 主催者が1人しかいない場合はイベントを終了状態にし
# それ以外の場合は主催者のみを削除する
#
# TODO: 本来は「主催者が0人のイベントは無効」という制約をバリデーションで担保する形にしたい
# https://github.com/fjordllc/bootcamp/pull/9732#discussion_r2969273381
def close_or_destroy_organizer(user)

@torinoko
たださんもコメントいただきありがとうございます!Issue化もしておきます。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yokomaru コメントそんな感じでよいと思います!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@okuramasafumi
ありがとうございます!
こちらコメント追加いたしました。

5113131

if regular_event_organizers.count == 1
update(finished: true)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateが失敗したときfalseを返しますが、無視しているので失敗に気づけないのは本来であれば良くないかなと思います。
普通ならupdate!として失敗時に例外を投げれば良いのですが、ここの大元の呼び出し元である退会/休会処理が適切に例外処理をしていないように見えるので、今はこのままにしておくのが安全と思います。
organizer.destroyについても同様です。

本PRのスコープ外ですが、退会/休会処理のもとからあるコードが適切でないという点について一応補足。
この処理には

  1. ユーザーのステータス変更
  2. サブスクリプション解約
  3. 定期イベントの後始末を含むその他の些末な処理

があります。
現状では1,2が先に実行されて成功し、3で失敗した場合ここで例外を投げるとユーザーには500エラーページが表示されるだけなので、何が起きたのか、1,2は成功したのかわからない状態になると理解しています。
3はユーザーにとっては不要で管理者が運用でカバーできるため、適切な例外処理がされていない現状では3で例外を発生させないのが良いと思います。
呼び出し元が複数あってそれぞれの書き方もバラバラなので修正するのは難儀しそうです。

else
organizer = regular_event_organizers.find_by(user:)
organizer.destroy
end
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private

def end_at_be_greater_than_start_at
Expand Down
2 changes: 2 additions & 0 deletions app/models/regular_event_participation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ class RegularEventParticipation < ApplicationRecord
belongs_to :regular_event

validates :user_id, uniqueness: { scope: :regular_event_id }

scope :for_holding_events, -> { joins(:regular_event).merge(RegularEvent.holding) }
end
11 changes: 3 additions & 8 deletions app/models/retirement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ def execute
destroy_subscription
end

cancel_event_subscription
remove_as_event_organizer
clean_up_regular_events
clear_github_info
destroy_cards
publish
Expand Down Expand Up @@ -74,12 +73,8 @@ def destroy_cards
Card.destroy_all(@user.customer_id) if @user.customer_id?
end

def cancel_event_subscription
@user.cancel_participation_from_regular_events
end

def remove_as_event_organizer
@user.delete_and_assign_new_organizer
def clean_up_regular_events
@user.clean_up_regular_events
end

def publish
Expand Down
14 changes: 5 additions & 9 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class User < ApplicationRecord # rubocop:todo Metrics/ClassLength
'adviser' => :advisers,
'admin' => :admins
}.freeze
DEFAULT_REGULAR_EVENT_ORGANIZER = 'komagata'
HIBERNATION_LIMIT = 3.months
HIBERNATION_LIMIT_BEFORE_ONE_WEEK = HIBERNATION_LIMIT - 1.week

Expand Down Expand Up @@ -885,14 +884,6 @@ def become_watcher!(watchable)
watches.find_or_create_by!(watchable:)
end

def cancel_participation_from_regular_events
regular_event_participations.destroy_all
end

def delete_and_assign_new_organizer
organizers.each(&:delete_and_assign_new)
end

def scheduled_retire_at
hibernated_at + User::HIBERNATION_LIMIT if hibernated_at?
end
Expand Down Expand Up @@ -960,6 +951,11 @@ def reports_with_learning_times
reports.joins(:learning_times).distinct.order(reported_on: :asc)
end

def clean_up_regular_events
regular_event_participations.for_holding_events.destroy_all
organize_regular_events.holding.each { |event| event.close_or_destroy_organizer(self) }
end
Comment on lines +954 to +957
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

後始末が途中まで反映される可能性があります。

Line 956-957 は参加解除と主催者側の終了/削除を別々の更新で実行しているので、後段で失敗すると片方だけ反映されます。退会/休会/研修終了の整合性に直結する処理なので、この単位は transaction でまとめたいです。User にはオーケストレーションだけが残っているので、Interactor/PORO への切り出しは別PRでもよさそうです。

💡 修正案
 def clean_up_regular_events
-  regular_event_participations.for_holding_events.destroy_all
-  organize_regular_events.holding.each { |event| event.close_or_destroy_organizer(self) }
+  self.class.transaction do
+    regular_event_participations.for_holding_events.destroy_all
+    organize_regular_events.holding.each do |event|
+      event.close_or_destroy_organizer(self)
+    end
+  end
 end

close_or_destroy_organizer が失敗時に false を返す実装なら、ここで明示的に例外化してロールバックさせたいです。 As per coding guidelines 複数のActiveRecordモデルを操作する1つの責務がある時や外部APIとやりとりする処理がある場合にはInteractorオブジェクトパターンを検討する。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/user.rb` around lines 955 - 958, clean_up_regular_events
の参加解除と主催側の終了処理を別々のDB更新で実行しているため途中失敗で不整合が発生する可能性があるので、regular_event_participations.for_holding_events.destroy_all
と organize_regular_events.holding.each { |event|
event.close_or_destroy_organizer(self) } の両方を一つのトランザクションにまとめて実行し、各
event.close_or_destroy_organizer(self) の戻り値が false
や例外を返した場合は明示的に例外を発生させてロールバックするようにしてください(例: ApplicationRecord.transaction /
ActiveRecord::Base.transaction を使い、失敗時に raise を投げて rollback)。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

以下理由から今回のPRでは対応は不要だと考えております

  • 参加者と主催者は別モデルなので厳密に整合性を取る必要性がない
  • clean_up_regular_eventsは退会にまつわる一連の処理の中の1つだが、このメソッドを呼んでいる箇所でのトランザクションも見直す必要性があるため一度全体を見直してから再度検討したい

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yokomaru ご説明ありがとうございます。おっしゃる通り、参加者と主催者が別モデルである点、また呼び出し元全体のトランザクション設計を一緒に見直す方が整合性のある対応になる点、いずれも納得感のある理由だと思います。今後の全体見直し時に改めてご検討いただければと思います。


✏️ Learnings added
Learnt from: yokomaru
Repo: fjordllc/bootcamp PR: 9732
File: app/models/user.rb:955-958
Timestamp: 2026-03-16T03:19:43.420Z
Learning: fjordllc/bootcamp の `User#clean_up_regular_events` において、`regular_event_participations.for_holding_events.destroy_all`(参加解除)と `organize_regular_events.holding.each { |event| event.close_or_destroy_organizer(self) }`(主催者側終了/削除)はトランザクションでまとめていないが、これは意図的な設計判断である。理由: (1) 参加者と主催者は別モデルであり厳密な整合性は不要、(2) 呼び出し元(退会・休会・研修終了処理)全体のトランザクション設計を一緒に見直す予定があるため、このメソッド単体での対応は見送られた。

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ryufuta
Repo: fjordllc/bootcamp PR: 9732
File: app/models/regular_event.rb:142-149
Timestamp: 2026-03-13T07:43:14.983Z
Learning: fjordllc/bootcamp の `RegularEvent#close_or_destroy_organizer(user)` は `User#clean_up_regular_events` からのみ呼び出される設計であり、後者は `organize_regular_events.holding` でループするため、引数 `user` は必ずそのイベントの主催者である。そのため `regular_event_organizers.find_by(user:)` が nil を返すことはなく、nil ガードは不要。

Learnt from: matuaya
Repo: fjordllc/bootcamp PR: 9108
File: app/models/after_user_retirement.rb:0-0
Timestamp: 2025-09-01T22:31:57.345Z
Learning: fjordllc/bootcampプロジェクトにおいて、`Newspaper.publish(:retirement_create, { user: user })`はUnfinishedDataDestroyer(未チェックProduct・WIP Report削除、career_pathリセット)とTimesChannelDestroyer(Discord分報チャンネル削除)による重要なデータクリーンアップ処理を実行するため、退会のトリガーが何であろうと(admin、hibernation、user)必ず実行する必要がある。

Learnt from: yokomaru
Repo: fjordllc/bootcamp PR: 9505
File: test/fixtures/regular_events.yml:253-262
Timestamp: 2026-01-15T06:30:59.321Z
Learning: fjordllc/bootcamp の RegularEvent モデルにおいて、`user` フィールドは「作成者」を意味し、主催者は別途 Organizer モデルで管理されている。通常は作成者 = 主催者のパターンが多いが、システム上は分離されている設計である。

Learnt from: matuaya
Repo: fjordllc/bootcamp PR: 9108
File: app/models/after_user_retirement.rb:0-0
Timestamp: 2025-09-01T22:31:57.345Z
Learning: fjordllc/bootcampプロジェクトにおいて、`Newspaper.publish(:retirement_create, { user: user })`は通知処理ではなく、UnfinishedDataDestroyerやTimesChannelDestroyer等による重要なデータクリーンアップ処理を実行するため、管理者による退会(triggered_by: 'admin')の場合でも必ず実行する必要がある。

Learnt from: mousu-a
Repo: fjordllc/bootcamp PR: 9626
File: db/schema.rb:1153-1156
Timestamp: 2026-02-11T02:59:47.770Z
Learning: fjordllc/bootcamp の PairWork 機能において、buddy(ペアを申し込んだメンター)ユーザーが削除された場合、buddy_id だけでなく reserved_at も NULL にしてマッチングを解除する必要がある。buddy_id のみ NULL にすると reserved_at が残り不整合な状態になるため、将来のマッチング解除機能実装時に User の削除時コールバックまたは明示的なアンマッチ処理で両方のカラムをクリアする設計とする。

Learnt from: komagata
Repo: fjordllc/bootcamp PR: 9101
File: app/models/graduation_notifier.rb:23-28
Timestamp: 2025-09-12T21:16:47.639Z
Learning: Rails upgrade PRにおいて、configuration systemの変更やwebhook設定の改善提案も、アップグレードに直接関連しない場合は別PRで対応すべきである。PRのスコープ維持が重要。

Learnt from: komagata
Repo: fjordllc/bootcamp PR: 9101
File: app/notifiers/discord_notifier.rb:131-135
Timestamp: 2025-09-12T21:18:00.834Z
Learning: Rails アップグレードPRにおいて、product_review_not_completed メソッドの webhook URL 設定の shared フォールバック追加も、設定システム全体の変更として別PRで対応すべきである。

Learnt from: s-tone-gs
Repo: fjordllc/bootcamp PR: 0
File: :0-0
Timestamp: 2026-03-12T04:37:38.329Z
Learning: fjordllc/bootcamp では、一般受講生(admin/mentor 権限のないユーザー)は休会中ユーザーのプロフィールページにアクセスできない(アクセス制御で弾かれる)。そのため、`test/system/users/profile_test.rb` において「一般受講生が休会中ユーザーの休会履歴を閲覧できないこと」を確認するテストは不要であり、意図的に省略されている。

Learnt from: sjabcdefin
Repo: fjordllc/bootcamp PR: 9247
File: app/controllers/checks_controller.rb:0-0
Timestamp: 2025-10-22T06:04:36.036Z
Learning: ChecksController#createおよび#destroyでは、Checkの作成・削除とActiveSupport::Notifications.instrumentによるイベント発行(プラクティスのステータス更新)を同一トランザクション内で実行し、いずれかが失敗した場合は両方をロールバックする。これによりWebUI表示とDB状態の整合性を保証している。

Learnt from: komagata
Repo: fjordllc/bootcamp PR: 9101
File: app/notifiers/discord_notifier.rb:52-56
Timestamp: 2025-09-12T21:17:45.295Z
Learning: Rails upgrade PRにおいて、configuration systemの改善提案(shared configuration fallback等)も、アップグレードに直接関連しない場合は別PRで対応すべきである。PRのスコープ維持が重要。

Learnt from: mousu-a
Repo: fjordllc/bootcamp PR: 8566
File: app/helpers/pair_work_helper.rb:31-33
Timestamp: 2026-01-15T03:20:24.643Z
Learning: In this Ruby project, prefer Numeric#hours (plural) for adding time durations. Use the plural hours method when converting integers to durations (e.g., hour_count.hours). Apply this consistently across the codebase to ensure uniform time calculations and Rails-style duration handling.

Learnt from: kutimiti1234
Repo: fjordllc/bootcamp PR: 9526
File: app/models/regular_event.rb:0-0
Timestamp: 2026-01-19T14:07:11.947Z
Learning: In Rails apps, avoid placing display/filtering logic in models. Keep business/domain logic in models, and handle display-related filtering in controllers. For RegularEvent filtering based on params[:target] (e.g., 'not_finished' or 'all'), implement the logic in the controller (e.g., RegularEventsController) and keep models free of presentation concerns.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai
こちらでコメントいただいたOutside diff range commentsについても、全体を見直してから再度検討したいと思っています。
#9732 (review)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh api repos/fjordllc/bootcamp/pulls/9732/reviews --jq '.[] | select(.id == 3929430899) | {id, body, state}'
gh api repos/fjordllc/bootcamp/pulls/9732/reviews/3929430899/comments --jq '.[] | {path, line, body}'

Length of output: 13674


@yokomaru 承知しました。Outside diff range で指摘していた app/models/retirement.rbexecute メソッドにおける clean_up_regular_events 呼び出しがトランザクション外になっている点についても、呼び出し元全体のトランザクション設計を見直す際に合わせてご検討いただければと思います。引き続きよろしくお願いします!


✏️ Learnings added
Learnt from: yokomaru
Repo: fjordllc/bootcamp PR: 9732
File: app/models/user.rb:955-958
Timestamp: 2026-03-16T03:24:35.054Z
Learning: fjordllc/bootcamp の `Retirement#execute`(`app/models/retirement.rb`)において、`user.clean_up_regular_events` の呼び出しは `ActiveRecord::Base.transaction` ブロックの外(コミット後)に置かれているが、これは意図的な設計判断である。理由: 退会・休会・研修終了処理全体のトランザクション設計を一緒に見直す予定があり、`clean_up_regular_events` 単体をトランザクション内に移動する対応は別途全体見直し時に検討する。

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: yokomaru
Repo: fjordllc/bootcamp PR: 9732
File: app/models/user.rb:955-958
Timestamp: 2026-03-16T03:19:43.420Z
Learning: fjordllc/bootcamp の `User#clean_up_regular_events` において、`regular_event_participations.for_holding_events.destroy_all`(参加解除)と `organize_regular_events.holding.each { |event| event.close_or_destroy_organizer(self) }`(主催者側終了/削除)はトランザクションでまとめていないが、これは意図的な設計判断である。理由: (1) 参加者と主催者は別モデルであり厳密な整合性は不要、(2) 呼び出し元(退会・休会・研修終了処理)全体のトランザクション設計を一緒に見直す予定があるため、このメソッド単体での対応は見送られた。

Learnt from: ryufuta
Repo: fjordllc/bootcamp PR: 9732
File: app/models/regular_event.rb:142-149
Timestamp: 2026-03-13T07:43:14.983Z
Learning: fjordllc/bootcamp の `RegularEvent#close_or_destroy_organizer(user)` は `User#clean_up_regular_events` からのみ呼び出される設計であり、後者は `organize_regular_events.holding` でループするため、引数 `user` は必ずそのイベントの主催者である。そのため `regular_event_organizers.find_by(user:)` が nil を返すことはなく、nil ガードは不要。

Learnt from: komagata
Repo: fjordllc/bootcamp PR: 9101
File: app/models/graduation_notifier.rb:23-28
Timestamp: 2025-09-12T21:16:47.639Z
Learning: Rails upgrade PRにおいて、configuration systemの変更やwebhook設定の改善提案も、アップグレードに直接関連しない場合は別PRで対応すべきである。PRのスコープ維持が重要。

Learnt from: komagata
Repo: fjordllc/bootcamp PR: 9101
File: app/notifiers/discord_notifier.rb:131-135
Timestamp: 2025-09-12T21:18:00.834Z
Learning: Rails アップグレードPRにおいて、product_review_not_completed メソッドの webhook URL 設定の shared フォールバック追加も、設定システム全体の変更として別PRで対応すべきである。

Learnt from: komagata
Repo: fjordllc/bootcamp PR: 9101
File: app/notifiers/discord_notifier.rb:52-56
Timestamp: 2025-09-12T21:17:45.295Z
Learning: Rails upgrade PRにおいて、configuration systemの改善提案(shared configuration fallback等)も、アップグレードに直接関連しない場合は別PRで対応すべきである。PRのスコープ維持が重要。

Learnt from: mousu-a
Repo: fjordllc/bootcamp PR: 9626
File: db/schema.rb:1153-1156
Timestamp: 2026-02-11T02:59:47.770Z
Learning: fjordllc/bootcamp の PairWork 機能において、buddy(ペアを申し込んだメンター)ユーザーが削除された場合、buddy_id だけでなく reserved_at も NULL にしてマッチングを解除する必要がある。buddy_id のみ NULL にすると reserved_at が残り不整合な状態になるため、将来のマッチング解除機能実装時に User の削除時コールバックまたは明示的なアンマッチ処理で両方のカラムをクリアする設計とする。

Learnt from: s-tone-gs
Repo: fjordllc/bootcamp PR: 0
File: :0-0
Timestamp: 2026-03-12T04:37:38.329Z
Learning: fjordllc/bootcamp では、一般受講生(admin/mentor 権限のないユーザー)は休会中ユーザーのプロフィールページにアクセスできない(アクセス制御で弾かれる)。そのため、`test/system/users/profile_test.rb` において「一般受講生が休会中ユーザーの休会履歴を閲覧できないこと」を確認するテストは不要であり、意図的に省略されている。

Learnt from: matuaya
Repo: fjordllc/bootcamp PR: 9108
File: app/models/after_user_retirement.rb:0-0
Timestamp: 2025-09-01T22:31:57.345Z
Learning: fjordllc/bootcampプロジェクトにおいて、`Newspaper.publish(:retirement_create, { user: user })`はUnfinishedDataDestroyer(未チェックProduct・WIP Report削除、career_pathリセット)とTimesChannelDestroyer(Discord分報チャンネル削除)による重要なデータクリーンアップ処理を実行するため、退会のトリガーが何であろうと(admin、hibernation、user)必ず実行する必要がある。

Learnt from: reckyy
Repo: fjordllc/bootcamp PR: 8356
File: db/fixtures/users.yml:823-826
Timestamp: 2025-07-15T12:33:04.885Z
Learning: fjordllc/bootcampプロジェクトでは、`trainee: true`は「研修に所属していたユーザー」全体を示すフラグであり、研修中のユーザーと研修を終了したユーザーの両方を含む。そのため、`trainee: true`と`training_completed_at`の併存は意図された仕様であり、排他的な関係ではない。

Learnt from: s-tone-gs
Repo: fjordllc/bootcamp PR: 9666
File: app/views/users/_hibernation_history.html.slim:15-16
Timestamp: 2026-03-12T02:58:38.011Z
Learning: `app/views/users/_hibernation_history.html.slim` において、現在休会中(`is_hibernating && i.zero?`)のブロックに `is-only-mentor` クラスが固定で付与されているのは意図的な設計である。fjordllc/bootcamp では休会中のユーザーはシステムにログインできないため、`is_self` が `true` になるケースは存在せず、現在の休会情報は常に管理者・メンターにのみ表示される。`is_self` 分岐は復帰済みユーザー(ever_hibernated?)が自分の過去の休会履歴をダッシュボードで確認できるようにするためのものであり、現在休会中ブロックには不要。

Learnt from: hirokiej
Repo: fjordllc/bootcamp PR: 8740
File: app/helpers/reports_helper.rb:56-64
Timestamp: 2025-06-29T03:44:15.179Z
Learning: このプロジェクト(fjordllc/bootcamp)では、ja.ymlファイルで一部の単語や文章のみI18n対応されているが、多くのテキストは日本語でハードコーディングされており、完全な国際化対応は行われていない。新しい機能でもI18n対応は不要と判断される。

Learnt from: yokomaru
Repo: fjordllc/bootcamp PR: 9505
File: test/fixtures/regular_events.yml:253-262
Timestamp: 2026-01-15T06:30:59.321Z
Learning: fjordllc/bootcamp の RegularEvent モデルにおいて、`user` フィールドは「作成者」を意味し、主催者は別途 Organizer モデルで管理されている。通常は作成者 = 主催者のパターンが多いが、システム上は分離されている設計である。

Learnt from: mousu-a
Repo: fjordllc/bootcamp PR: 8566
File: app/helpers/pair_work_helper.rb:31-33
Timestamp: 2026-01-15T03:20:24.643Z
Learning: In this Ruby project, prefer Numeric#hours (plural) for adding time durations. Use the plural hours method when converting integers to durations (e.g., hour_count.hours). Apply this consistently across the codebase to ensure uniform time calculations and Rails-style duration handling.

Learnt from: kutimiti1234
Repo: fjordllc/bootcamp PR: 9526
File: app/models/regular_event.rb:0-0
Timestamp: 2026-01-19T14:07:11.947Z
Learning: In Rails apps, avoid placing display/filtering logic in models. Keep business/domain logic in models, and handle display-related filtering in controllers. For RegularEvent filtering based on params[:target] (e.g., 'not_finished' or 'all'), implement the logic in the controller (e.g., RegularEventsController) and keep models free of presentation concerns.


private

def password_required?
Expand Down
22 changes: 5 additions & 17 deletions app/views/hibernation/new.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,12 @@ hr.a-border
| こちら
| のページの「分報 URL」欄に分報チャンネルの URL を登録してください。

- holding_regular_events = RegularEvent.organizer_event(current_user).holding
- if holding_regular_events.any?
- if @holding_regular_events.any?
.form-item
label.a-form-label
| 定期イベント
.important-message
.important-message__body
.a-short-text.is-sm
p
|ご自身が主催者である定期イベントがあります。
br
| 休会をお考えの場合、イベントの進行に影響がないよう、任意で他の参加者に主催を引き継ぐことを推奨します。
| 休会手続きを完了する前に、以下のリンク先でイベント設定変更を行なってください。
ul
- holding_regular_events.holding.each do |event|
li
= link_to edit_regular_event_path(event), target: '_blank', rel: 'noopener' do
| 定期イベント「#{event.title}」の設定変更
= render 'shared/regular_events_warning',
events: @holding_regular_events,
case_text: '休会をお考えの',
action_text: '休会'

.form-item
label.a-form-label.is-required
Expand Down
21 changes: 5 additions & 16 deletions app/views/retirement/new.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,12 @@ hr.a-border
= render 'errors', object: current_user
= form_with model: current_user, local: true, url: retirement_path, method: :post, class: 'form' do |f|
.form__items
- holding_regular_events = RegularEvent.organizer_event(current_user).holding
- if holding_regular_events.any?
- if @holding_regular_events.any?
.form-item
.important-message
.important-message__body
.a-short-text.is-sm
p
|ご自身が主催者である定期イベントがあります。
br
| 退会をお考えの場合、イベントの進行に影響がないよう、任意で他の参加者に主催を引き継ぐことを推奨します。
| 退会手続きを完了する前に、
| 以下のリンク先でイベント設定変更を行なってください。
ul
- holding_regular_events.each do |event|
li
= link_to edit_regular_event_path(event), target: '_blank', rel: 'noopener' do
| 定期イベント「#{event.title}」の設定変更
= render 'shared/regular_events_warning',
events: @holding_regular_events,
case_text: '退会をお考えの',
action_text: '退会'

.form-item
= f.label :retire_reasons, '退会の理由を教えてください(複数選択可)', class: 'a-form-label'
Expand Down
18 changes: 18 additions & 0 deletions app/views/shared/_regular_events_warning.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
label.a-form-label
| 定期イベント
.important-message
.important-message__body
.a-short-text.is-sm
p
|ご自身が主催者である定期イベントがあります。
br
| #{case_text}場合、イベントの進行に影響がないよう、任意で他の参加者に主催を引き継ぐことを推奨します。
| #{action_text}手続きを完了する前に、
| 以下のリンク先でイベント設定変更を行なってください。
br
| #{action_text}後、ご自身のみが主催者である開催中のイベントは自動的に終了となります。
ul
- events.each do |event|
li
= link_to edit_regular_event_path(event), target: '_blank', rel: 'noopener' do
| 定期イベント「#{event.title}」の設定変更
20 changes: 5 additions & 15 deletions app/views/training_completion/new.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,12 @@ hr.a-border
= render 'errors', object: current_user
= form_with model: current_user, local: true, url: training_completion_path, method: :post, class: 'form' do |f|
.form__items
- if current_user.regular_events.any?
- if @holding_regular_events.any?
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

current_user.regular_eventsだと主催イベントではなく作成したイベントのみの取得となっていたので修正しています。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LGTM]
オォン
こういうところ他にもありそうですね〜

.form-item
.important-message
.important-message__body
.a-short-text.is-sm
p
|ご自身が主催者である定期イベントがあります。
br
| 研修を修了される場合、イベントの進行に影響がないよう、任意で他の参加者に主催を引き継ぐことを推奨します。
| 退会手続きを完了する前に、
| 以下のリンク先でイベント設定変更を行なってください。
ul
- current_user.regular_events.each do |event|
li
= link_to edit_regular_event_path(event), target: '_blank', rel: 'noopener' do
| 定期イベント「#{event.title}」の設定変更
= render 'shared/regular_events_warning',
events: @holding_regular_events,
case_text: '研修を終了される',
action_text: '研修終了'

.form-item
= f.label :satisfaction, '全体の満足度を教えてください', class: 'a-form-label is-required'
Expand Down
1 change: 0 additions & 1 deletion test/fixtures/regular_event_participations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,3 @@ regular_event_participation6:
regular_event_participation7:
user: kimura
regular_event: regular_event34

16 changes: 0 additions & 16 deletions test/models/organizer_test.rb

This file was deleted.

38 changes: 23 additions & 15 deletions test/models/regular_event_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,29 @@ class RegularEventTest < ActiveSupport::TestCase
assert_not regular_event.participated_by?(user)
end

test '#assign_admin_as_organizer_if_none' do
regular_event = RegularEvent.new(
title: '主催者のいないイベント',
description: '主催者のいないイベント',
finished: false,
hold_national_holiday: false,
start_at: Time.zone.local(2020, 1, 1, 21, 0, 0),
end_at: Time.zone.local(2020, 1, 1, 22, 0, 0),
user: users(:kimura),
category: 0,
published_at: '2023-08-01 00:00:00'
)
regular_event.save(validate: false)
regular_event.assign_admin_as_organizer_if_none
assert_equal User.find_by(login_name: User::DEFAULT_REGULAR_EVENT_ORGANIZER), regular_event.organizers.first
test '#close_or_destroy_organizer closes the event when only one organizer exists' do
user = users(:kimura)
regular_event = regular_events(:regular_event5) # kimuraが1人で主催しているイベント
Comment on lines +102 to +103
Copy link
Copy Markdown
Contributor

@torinoko torinoko Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NITS]
ここはコメントを書くでもよいし、以下のように明示しちゃうという手もありますね。
そうすることで fixture に変更が入れられてもこのテストは壊れません。

Suggested change
user = users(:kimura)
regular_event = regular_events(:regular_event5) # kimuraが1人で主催しているイベント
ruser = users(:kimura)
regular_event = regular_events(:regular_event1)
regular_event.regular_event_organizers.destroy_all
regular_event.regular_event_organizers.create(user: user)

でもこれだと可読性が著しく落ちるんですよね。
いや destroy_all するのがお行儀よくないし、organizer がいないイベントの fixture が用意されていれば destroy_all なんてしなくて済むし、そもそもモデルの設計として(以下略)
なので今回は kimura がひとりで主催している regular_event5 を使うでよいと思います!

ちなみにこれも RSpec だともっと簡易に書けるはず……
Minitest でもできるのかもだけどわかりませんでした……

create(:regular_event, regular_event_organizers:  [ create(:kimura) ])


assert_no_difference -> { regular_event.regular_event_organizers.count },
-> { regular_event.regular_event_organizers.where(user: user).count } do
assert_changes -> { regular_event.finished }, from: false, to: true do
regular_event.close_or_destroy_organizer(user)
end
end
end

test '#close_or_destroy_organizer removes the organizer when multiple organizers exist' do
user = users(:kimura)
regular_event = regular_events(:regular_event5) # kimuraが1人で主催しているイベント
regular_event.regular_event_organizers.create!(user: users(:hatsuno))

assert_difference -> { regular_event.regular_event_organizers.count } => -1,
-> { regular_event.regular_event_organizers.where(user: user).count } => -1 do
assert_no_changes -> { regular_event.finished } do
regular_event.close_or_destroy_organizer(user)
end
end
end

test '#all_scheduled_dates' do
Expand Down
3 changes: 1 addition & 2 deletions test/models/retirement_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ class RetirementTest < ActiveSupport::TestCase
clear_hibernation_state
save_user
destroy_subscription
cancel_event_subscription
remove_as_event_organizer
clean_up_regular_events
clear_github_info
destroy_cards
publish
Expand Down
Loading
Loading