mirror of
https://github.com/discourse/discourse.git
synced 2026-08-14 13:58:53 +08:00
#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>
235 lines
7.3 KiB
Ruby
Vendored
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
|
|
#
|