Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
2f982f7
主催者追加時に追加されたユーザーへメールを通知する機能とテストを追加
yokomaru Feb 2, 2026
c1c39e1
主催者追加時に追加されたユーザーへサイト内通知をする機能とテストを追加
yokomaru Feb 2, 2026
3ba385f
主催者追加時の通知イベントの作成してテストを追加
yokomaru Feb 2, 2026
6e20183
主催者追加時のイベントをActiveSupport::Notifications.subscribeに追加
yokomaru Feb 2, 2026
b446db4
定期イベントに新しく主催者が追加されたら通知処理を発行するメソッド(notify_new_organizer)を追加した
yokomaru Feb 2, 2026
e8576ed
Organizerモデルに処理があると見通しが悪かったため、RegularEventにdelete_and_assign_admin_or…
yokomaru Feb 2, 2026
339a6e8
定期イベント主催者の引き継ぎ処理を整理した
yokomaru Feb 2, 2026
0c0426a
未終了の定期イベント参加のみをキャンセルする処理を追加した
yokomaru Feb 2, 2026
969e988
定期イベント更新動線で通知処理を飛ばすようにした
yokomaru Feb 2, 2026
cc9f0d2
退会・休会・研修終了導線に新しい参加者・主催者削除処理を追加
yokomaru Feb 2, 2026
f9f3ba5
定期イベントの主催者引き継ぎ処理の構造を整理した
yokomaru Feb 3, 2026
e9945f1
主催者が作成された時より主催者を追加した時に通知がいく方が実態にあっているため通知名をorganizer.addに変更した
yokomaru Feb 4, 2026
119414c
regular_event.user_idsでも現状のリレーション上organizersのuserは取得できるが、organizersのu…
yokomaru Feb 4, 2026
d5d58b8
userモデルのhand_over_not_finished_regular_event_organizersテストを修正した
yokomaru Feb 4, 2026
e6070b8
退会/休会/研修終了時に表示される主催イベント表示ロジックでnot_finishedではなくholdingを使っているため合わせる
yokomaru Feb 4, 2026
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
4 changes: 2 additions & 2 deletions app/controllers/hibernation_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ 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.cancel_participation_from_holding_regular_events
current_user.hand_over_organizers_of_holding_regular_events
logout
redirect_to hibernation_path
else
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/regular_events_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class RegularEventsController < ApplicationController
class RegularEventsController < ApplicationController # rubocop:disable Metrics/ClassLength
before_action :set_regular_event, only: %i[edit update destroy]

def index
Expand Down Expand Up @@ -42,9 +42,12 @@ def edit; end

def update
set_wip
before_organizer_user_ids = @regular_event.organizers.pluck(:user_id)
Copy link
Contributor

Choose a reason for hiding this comment

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

before_organizer_user_idsを直訳すると"主催者ユーザーIDの(時間的もしくは位置的に)前に"という意味になり変です。
他の箇所で、更新後の主催者をnew_organizer_usersとしているのでそれとの対比でold_organizer_user_idsとすると良さそうです。


if @regular_event.update(regular_event_params)
update_published_at
ActiveSupport::Notifications.instrument('regular_event.update', regular_event: @regular_event, sender: current_user)
@regular_event.notify_new_organizer(sender: current_user, before_organizer_user_ids:)
set_all_user_participants_and_watchers
select_redirect_path
else
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/training_completion_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ def create
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.cancel_participation_from_holding_regular_events
current_user.hand_over_organizers_of_holding_regular_events
ActiveSupport::Notifications.instrument('training_completion.create', user:)
user.clear_github_data
notify_to_user(user)
Expand Down
18 changes: 18 additions & 0 deletions app/mailers/activity_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -468,4 +468,22 @@ def added_work(args = {})
message.perform_deliveries = @user.mail_notification? && !@user.retired?
message
end

# required params: regular_event, receiver
def added_organizer(args = {})
@regular_event ||= args[:regular_event]
@receiver ||= args[:receiver]
@user = @receiver
@link_url = notification_redirector_url(
link: "/regular_events/#{@regular_event.id}",
kind: Notification.kinds[:added_organizer]
)

subject = "[FBC] 定期イベント【#{@regular_event.title}】の主催者に追加されました。"

message = mail(to: @user.email, subject:)
message.perform_deliveries = @user.mail_notification? && !@user.retired?

message
end
end
3 changes: 2 additions & 1 deletion app/models/notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class Notification < ApplicationRecord
create_article: 24,
added_work: 25,
came_inquiry: 26,
training_completed: 27
training_completed: 27,
added_organizer: 28
}

scope :unreads, -> { where(read: false) }
Expand Down
8 changes: 3 additions & 5 deletions app/models/organizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ class Organizer < ApplicationRecord

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
scope :holding, lambda {
joins(:regular_event).merge(RegularEvent.holding)
}
end
13 changes: 13 additions & 0 deletions app/models/organizer_notifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class OrganizerNotifier
Copy link
Contributor

Choose a reason for hiding this comment

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

口頭でも伝えましたが、RegularEvent#notify_new_organizerからのみ呼ばれているのでRegularEventの外にクラスとして処理を切り出す意味はないのではと思いました。

def call(_name, _started, _finished, _id, payload)
regular_event = payload[:regular_event]
sender = payload[:sender]
new_organizer_users = payload[:new_organizer_users]

new_organizer_users.each do |user|
ActivityDelivery.with(regular_event:, sender:, receiver: user).notify(:added_organizer)
end
end
end
37 changes: 30 additions & 7 deletions app/models/regular_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,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 @@ -157,6 +150,29 @@ def publish_with_announcement?
wants_announcement? && !wip?
end

def hand_over_organizer(organizer:, sender:)
before_organizer_user_ids = organizers.pluck(:user_id)

organizer.delete
Copy link
Contributor Author

@yokomaru yokomaru Feb 12, 2026

Choose a reason for hiding this comment

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

  • destroyではなくdeleteを使用しているのは既存の処理であるdelete_and_assign_newを参考にしています(本当はdestroyを使う方がいいとは思うのですが、他にいい方法が浮かばず一旦このまま踏襲しています)
    • RegularEventとOrganizersはpresent: true関連があり、organizersが1人しかいない状態でdestroyを行うとバリデーションエラーになるため、deleteを使用し一時的に関連を無視してorganizerを削除後、主催者が0人の場合は管理者(komagata)を追加している
def delete_and_assign_new
    event = regular_event
    delete
    event.assign_admin_as_organizer_if_none
end

Copy link
Contributor

@ryufuta ryufuta Feb 28, 2026

Choose a reason for hiding this comment

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

この機能は削除することになったのでもう関係はないですが、一応コメントします。
懸念している通りdeleteするのは行儀が悪いので避けたいですね。

もっと綺麗に書けるかもしれませんが、以下のような流れでいけると思います。

if organizers.count > 1
  organizer.destory
  return

# 主催者が引数で渡ってきた`organizer`一人だけなので`komagata`を主催者に追加した後`organizer.destory`する処理
# ...

notify_new_organizer(sender:, before_organizer_user_ids:)

assign_admin_as_organizer_if_none

notify_new_organizer(sender:, before_organizer_user_ids:)
end

def notify_new_organizer(sender:, before_organizer_user_ids:)
new_organizer_user_ids = (organizers.pluck(:user_id) - before_organizer_user_ids) - [sender.id]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

organizers.pluck(:user_id)を再度呼び出して、更新処理後の最新のuser_idsを取得しその差分を比較しています

return if new_organizer_user_ids.blank?

new_organizer_users = User.where(id: new_organizer_user_ids)

ActiveSupport::Notifications.instrument(
'organizer.add',
regular_event: self,
sender:,
new_organizer_users:
)
end

private

def end_at_be_greater_than_start_at
Expand Down Expand Up @@ -197,4 +213,11 @@ def parse_event_time(event_date, event_time)
str_time = event_time.strftime('%R')
Time.zone.parse([str_date, str_time].join(' '))
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
end
4 changes: 4 additions & 0 deletions app/models/regular_event_participation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ class RegularEventParticipation < ApplicationRecord
belongs_to :regular_event

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

scope :holding, lambda {
joins(:regular_event).merge(RegularEvent.holding)
}
end
4 changes: 2 additions & 2 deletions app/models/retirement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ def destroy_cards
end

def cancel_event_subscription
@user.cancel_participation_from_regular_events
@user.cancel_participation_from_holding_regular_events
end

def remove_as_event_organizer
@user.delete_and_assign_new_organizer
@user.hand_over_organizers_of_holding_regular_events
end

def publish
Expand Down
14 changes: 8 additions & 6 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -883,12 +883,8 @@ 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)
def cancel_participation_from_holding_regular_events
regular_event_participations.holding.destroy_all
Copy link
Contributor

Choose a reason for hiding this comment

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

ここでの"開催中の"という意味にはactiveが一番しっくり来ると思います。
英語的にも自然で日本語話者にも通じやすいです。
既存のscopeにあるholdingと合わせたのだと思いますが、hold(開催する)をholdingの形で使用することはほとんどないように思います。
holding_regular_eventsだと"定期イベントを開催すること"という意味になりそうです。
他の箇所のholdingactiveに統一すると良いと思います。

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます!
実は現在holdingと全く同じwhere句(finished: true)で定義されているnot_finishedがあり別でIssueを立てております。

すでに複数表現がある状態でさらに新しい概念を混ぜるとさらにややこしくなる懸念があるのと、使用されている箇所を見ると検証箇所がかなり増えてしまう恐れがあるため、今回は退会/休会/研修終了動線のロジックで使われているholdingの方を使用できればと思っております🙇‍♀️

表現としてはactiveがすごくしっくりきたので、Issueにはactiveを使う方が良いという風にコメントをいたしました!

Copy link
Contributor

Choose a reason for hiding this comment

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

@yokomaru
承知しました。確かに別Issueで対応する方が良さそうですね。

end

def scheduled_retire_at
Expand Down Expand Up @@ -958,6 +954,12 @@ def reports_with_learning_times
reports.joins(:learning_times).distinct.order(reported_on: :asc)
end

def hand_over_organizers_of_holding_regular_events
organizers.holding.includes(:regular_event).find_each do |organizer|
organizer.regular_event.hand_over_organizer(organizer:, sender: self)
end
end

private

def password_required?
Expand Down
16 changes: 16 additions & 0 deletions app/notifiers/activity_notifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,20 @@ def came_inquiry(params = {})
read: false
)
end

def added_organizer(params = {})
params.merge!(@params)
regular_event = params[:regular_event]
receiver = params[:receiver]
sender = params[:sender]

notification(
body: "定期イベント【#{regular_event.title}】の主催者に追加されました。",
kind: :added_organizer,
receiver:,
sender:,
link: Rails.application.routes.url_helpers.polymorphic_path(regular_event),
read: false
)
end
end
4 changes: 4 additions & 0 deletions app/views/activity_mailer/added_organizer.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
= render '/notification_mailer/notification_mailer_template',
title: "定期イベント【#{@regular_event.title}】の主催者に追加されました。",
link_url: @link_url,
link_text: '定期イベント詳細へ'
1 change: 1 addition & 0 deletions config/initializers/active_support_notifications.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
ActiveSupport::Notifications.subscribe('came.comment', CommentNotifier.new)
ActiveSupport::Notifications.subscribe('graduation.update', GraduationNotifier.new)
ActiveSupport::Notifications.subscribe('comeback.update', ComebackNotifier.new)
ActiveSupport::Notifications.subscribe('organizer.add', OrganizerNotifier.new)

learning_status_updater = LearningStatusUpdater.new
ActiveSupport::Notifications.subscribe('product.save', learning_status_updater)
Expand Down
24 changes: 24 additions & 0 deletions test/deliveries/activity_delivery_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -499,4 +499,28 @@ class ActivityDeliveryTest < ActiveSupport::TestCase
ActivityDelivery.with(**params).notify(:added_work)
end
end

test '.notify(:added_organizer)' do
params = {
regular_event: regular_events(:regular_event4),
receiver: users(:komagata),
sender: users(:kimura)
}

assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 1 do
ActivityDelivery.notify!(:added_organizer, **params)
end

assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do
ActivityDelivery.notify(:added_organizer, **params)
end

assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 1 do
ActivityDelivery.with(**params).notify!(:added_organizer)
end

assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do
ActivityDelivery.with(**params).notify(:added_organizer)
end
end
end
19 changes: 19 additions & 0 deletions test/mailers/activity_mailer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,25 @@ class ActivityMailerTest < ActionMailer::TestCase
assert_empty ActionMailer::Base.deliveries
end

test 'added_organizer' do
regular_event = RegularEvent.find(ActiveRecord::FixtureSet.identify(:regular_event1))
receiver = User.find(ActiveRecord::FixtureSet.identify(:komagata))
Comment on lines +1289 to +1290
Copy link
Contributor

Choose a reason for hiding this comment

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

test/mailers/previews/activity_mailer_preview.rbと違ってrequire 'test_helper'を実行しているので以下のような書き方でfixtureを使えます。

Suggested change
regular_event = RegularEvent.find(ActiveRecord::FixtureSet.identify(:regular_event1))
receiver = User.find(ActiveRecord::FixtureSet.identify(:komagata))
regular_event = regular_events(:regular_event1)
receiver = users(:komagata)

こちらの方が可読性が高くて良いと思います。

sender = User.find(ActiveRecord::FixtureSet.identify(:kimura))
Copy link
Contributor

Choose a reason for hiding this comment

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

ここもsenderは不要ですね。
実際に削除したところテストはパスしました。


ActivityMailer.added_organizer(
regular_event:,
receiver:,
sender:
).deliver_now
Comment on lines +1293 to +1297
Copy link
Contributor

Choose a reason for hiding this comment

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

プロダクションコードでは他のメール通知も含め引数付きでメール送信処理を呼び出している箇所はなく、今回実装したメール通知もparams経由でのみ呼び出しています。

ActivityDelivery.with(regular_event:, sender:, receiver: user).notify(:added_organizer)

ここはチームの方針もあるので難しいですが、プロダクションコードで使われている方だけをテストするのが良いと思います。


assert_not ActionMailer::Base.deliveries.empty?
email = ActionMailer::Base.deliveries.last
assert_equal ['noreply@bootcamp.fjord.jp'], email.from
assert_equal ['komagata@fjord.jp'], email.to
assert_equal '[FBC] 定期イベント【開発MTG】の主催者に追加されました。', email.subject
assert_match(/定期イベント/, email.body.to_s)
end

private

def mailer_url_options
Expand Down
7 changes: 7 additions & 0 deletions test/mailers/previews/activity_mailer_preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,11 @@ def added_work

ActivityMailer.with(work:, sender: user, receiver:).added_work
end

def added_organizer
regular_event = RegularEvent.find(ActiveRecord::FixtureSet.identify(:regular_event1))
receiver = User.find(ActiveRecord::FixtureSet.identify(:komagata))
sender = User.find(ActiveRecord::FixtureSet.identify(:kimura))
ActivityMailer.with(regular_event:, receiver:, sender:).added_organizer
Comment on lines +193 to +194
Copy link
Contributor

Choose a reason for hiding this comment

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

メール通知では基本的にデフォルトの自動送信用メールアドレスを使用するようになっています。

default from: 'フィヨルドブートキャンプ <noreply@bootcamp.fjord.jp>'

senderは不要では?
ActivityMailer#added_organizer内でもsenderのメールアドレスを使っていないのでデフォルトのものが使われています。
サーバー起動後以下にアクセスすると確認できます。
http://localhost:3000/rails/mailers/activity_mailer/added_organizer

あと大したことではないので修正するほどではないですが、テストデータは実際のシナリオに近いものを選んだ方がわかりやすいです。
regular_event1は開発MTGでkomagataorganizer1として初めから主催者に設定されています。
komagataが新たに主催者に任命されたという通知をテストするのには向かないと思いました。

end
end
36 changes: 36 additions & 0 deletions test/models/organizer_notifier_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require 'test_helper'

class OrganizerNotifierTest < ActiveSupport::TestCase
Copy link
Contributor

Choose a reason for hiding this comment

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

OrganizerNotifierにはテストするほどのロジックがないため仮にOrganizerNotifierを残すとしてもこのテストクラスは不要だと思います。
通知自体のテストは下流で別途実施しているのでここではRubyの#eachを追加でテストしているかのような内容になってしまっています。
将来リグレッションが起きるリスクが低くこのテストのおかげでリグレッションを検出できるという場面が想像できないです。
ならばコードが増える分保守コストが上がるだけということになります。
例えばOrganizerNotifier#callに渡すハッシュのキーを変更することになった際にテストコードも修正しなければならないということを想像すると、削除した方が良いと納得できるかなと思います。

include ActiveJob::TestHelper

setup do
@regular_event = regular_events(:regular_event4)
@sender = users(:kimura)
end

test 'sends a notification when a new organizer is added' do
new_organizer_users = [users(:hatsuno)]

assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do
OrganizerNotifier.new.call(nil, nil, nil, nil, { regular_event: @regular_event, new_organizer_users:, sender: @sender })
end
end

test 'does not send a notification when no new organizers are added' do
new_organizer_users = []

assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 0 do
OrganizerNotifier.new.call(nil, nil, nil, nil, { regular_event: @regular_event, new_organizer_users:, sender: @sender })
end
end

test 'sends notifications when multiple organizers are added' do
new_organizer_users = [users(:hatsuno), users(:hajime)]

assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 2 do
OrganizerNotifier.new.call(nil, nil, nil, nil, { regular_event: @regular_event, new_organizer_users:, sender: @sender })
end
end
end
16 changes: 0 additions & 16 deletions test/models/organizer_test.rb

This file was deleted.

Loading