0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-14 13:58:53 +08:00
discourse/app/models/group_user.rb
Bannon Tanner 595c5b3be3
PERF: Add Users to Group Pt. 1 (#38737)
#38125 attempted to do the entire performance update to group add users
in one go, this is Pt 1 for splitting it into multiple, easier to digest
PRs. The main goal for this PR is to change any additions to group
members through one place.

GroupUser callbacks intentionally left in place for this PR to not break
the paths that create GroupUser directly.

### Details
- Add new `GroupManager` class that will be the single source of truth
for adding members to a group.
- Modify the `bulk_add` and `bulk_remove` methods to handle all
side-effects from the original callbacks.
- Add other bulk methods in needed places (group_user, category_user,
tag_user, group_action_logger)
- Add tests for all the new paths
- Fix tests that relied on group being saved before a member could be
added (were using `Fabricate.build` and manually saving later)

---------

Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
2026-03-26 09:11:22 -05:00

235 lines
7.3 KiB
Ruby
Vendored

# frozen_string_literal: true
class GroupUser < ActiveRecord::Base
belongs_to :group
belongs_to :user
before_create :set_notification_level
after_destroy :grant_other_available_title
after_destroy :remove_primary_and_flair_group, :recalculate_trust_level
after_save :update_title
after_save :set_primary_group
after_save :grant_trust_level
after_save :set_category_notifications
after_save :set_tag_notifications
after_commit :increase_group_user_count, on: [:create]
after_commit :decrease_group_user_count, on: [:destroy]
def self.notification_levels
NotificationLevels.all
end
def self.ensure_consistency!(last_seen = 1.hour.ago)
update_first_unread_pm(last_seen)
end
def self.update_first_unread_pm(last_seen, limit: 10_000)
DB.exec(
<<~SQL,
UPDATE group_users gu
SET first_unread_pm_at = Y.min_date
FROM (
SELECT
X.group_id,
X.user_id,
X.min_date
FROM (
SELECT
gu.group_id,
gu.user_id,
COALESCE(Z.min_date, :now) min_date
FROM group_users gu
LEFT JOIN (
SELECT
gu2.group_id,
gu2.user_id,
MIN(t.updated_at) min_date
FROM group_users gu2
INNER JOIN topic_allowed_groups tag ON tag.group_id = gu2.group_id
INNER JOIN topics t ON t.id = tag.topic_id
INNER JOIN users u ON u.id = gu2.user_id
LEFT JOIN topic_users tu ON t.id = tu.topic_id AND tu.user_id = gu2.user_id
WHERE t.deleted_at IS NULL
AND t.archetype = :archetype
AND tu.last_read_post_number < CASE
WHEN u.admin OR u.moderator #{SiteSetting.whispers_allowed_groups_map.any? ? "OR gu2.group_id IN (:whisperers_group_ids)" : ""}
THEN t.highest_staff_post_number
ELSE t.highest_post_number
END
AND (COALESCE(tu.notification_level, 1) >= 2)
GROUP BY gu2.user_id, gu2.group_id
) AS Z ON Z.user_id = gu.user_id AND Z.group_id = gu.group_id
) AS X
WHERE X.user_id IN (
SELECT id
FROM users
WHERE last_seen_at IS NOT NULL
AND last_seen_at > :last_seen
ORDER BY last_seen_at DESC
LIMIT :limit
)
) Y
WHERE gu.user_id = Y.user_id AND gu.group_id = Y.group_id
SQL
archetype: Archetype.private_message,
last_seen: last_seen,
limit: limit,
now: 10.minutes.ago,
whisperers_group_ids: SiteSetting.whispers_allowed_groups_map,
)
end
protected
def set_notification_level
self.notification_level = group&.default_notification_level || 3
end
def set_primary_group
user.update!(primary_group: group) if group.primary_group
end
def remove_primary_and_flair_group
return if self.destroyed_by_association&.active_record == User # User is being destroyed, so don't try to update
updates = {}
updates[:primary_group_id] = nil if user.primary_group_id == group_id
updates[:flair_group_id] = nil if user.flair_group_id == group_id
user.update(updates) if updates.present?
end
def grant_other_available_title
if group.title.present? && group.title == user.title
user.update_attribute(:title, user.next_best_title)
end
end
def update_title
if group.title.present?
DB.exec(
"
UPDATE users SET title = :title
WHERE (title IS NULL OR title = '') AND id = :id",
id: user_id,
title: group.title,
)
end
end
def grant_trust_level
return if group.grant_trust_level.nil? || group.grant_trust_level.zero?
TrustLevelGranter.grant(group.grant_trust_level, user)
end
def recalculate_trust_level
return if group.grant_trust_level.nil? || group.grant_trust_level.zero?
return if self.destroyed_by_association&.active_record == User # User is being destroyed, so don't try to recalculate
Promotion.recalculate(user, use_previous_trust_level: true)
end
def set_category_notifications
self.class.set_category_notifications(group, user)
end
def self.set_category_notifications(group, user)
bulk_set_category_notifications(group, [user.id])
end
def set_tag_notifications
self.class.set_tag_notifications(group, user)
end
def self.set_tag_notifications(group, user)
bulk_set_tag_notifications(group, [user.id])
end
def self.bulk_set_category_notifications(group, user_ids)
defaults = group.group_category_notification_defaults.to_a
return if defaults.empty?
defaults.each do |default|
DB.exec(
<<~SQL,
INSERT INTO category_users (user_id, category_id, notification_level)
SELECT unnest(ARRAY[:user_ids]::int[]), :category_id, :notification_level
ON CONFLICT (user_id, category_id) DO UPDATE
SET notification_level = #{semantically_higher_notification_level_sql("EXCLUDED.notification_level", "category_users.notification_level")}
SQL
user_ids: user_ids,
category_id: default.category_id,
notification_level: default.notification_level,
)
end
CategoryUser.auto_watch(user_ids: user_ids)
CategoryUser.auto_track(user_ids: user_ids)
end
def self.bulk_set_tag_notifications(group, user_ids)
defaults = group.group_tag_notification_defaults.to_a
return if defaults.empty?
defaults.each do |default|
DB.exec(
<<~SQL,
INSERT INTO tag_users (user_id, tag_id, notification_level, created_at, updated_at)
SELECT unnest(ARRAY[:user_ids]::int[]), :tag_id, :notification_level, NOW(), NOW()
ON CONFLICT (user_id, tag_id) DO UPDATE
SET notification_level = #{semantically_higher_notification_level_sql("EXCLUDED.notification_level", "tag_users.notification_level")},
updated_at = NOW()
SQL
user_ids: user_ids,
tag_id: default.tag_id,
notification_level: default.notification_level,
)
end
TagUser.auto_watch(user_ids: user_ids)
TagUser.auto_track(user_ids: user_ids)
end
def increase_group_user_count
Group.increment_counter(:user_count, self.group_id)
end
def decrease_group_user_count
Group.decrement_counter(:user_count, self.group_id)
end
def self.semantically_higher_notification_level_sql(new_col, existing_col)
<<~SQL.squish
CASE
WHEN (CASE #{new_col} WHEN 3 THEN 5 ELSE #{new_col} END) >=
(CASE #{existing_col} WHEN 3 THEN 5 ELSE #{existing_col} END)
THEN #{new_col}
ELSE #{existing_col}
END
SQL
end
private_class_method :semantically_higher_notification_level_sql
end
# == Schema Information
#
# Table name: group_users
#
# id :integer not null, primary key
# group_id :integer not null
# user_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
# owner :boolean default(FALSE), not null
# notification_level :integer default(2), not null
# first_unread_pm_at :datetime not null
#
# Indexes
#
# index_group_users_on_group_id_and_user_id (group_id,user_id) UNIQUE
# index_group_users_on_user_id_and_group_id (user_id,group_id) UNIQUE
#