discourse/app/models/category_user.rb
Krzysztof Kotlarek bdecd697b9
FIX: more performance improvement for PostAlert job ()
Simplified query based on SiteSettings to join only relevant user_options rows.
In addition, index was added to 'watched_precedence_over_muted` column in `user_options` table to speed up query
2023-07-13 09:02:23 +10:00

297 lines
9.7 KiB
Ruby

# frozen_string_literal: true
class CategoryUser < ActiveRecord::Base
belongs_to :category
belongs_to :user
def self.lookup(user, level)
self.where(user: user, notification_level: notification_levels[level])
end
def self.notification_levels
NotificationLevels.all
end
def self.watching_levels
[notification_levels[:watching], notification_levels[:watching_first_post]]
end
def self.batch_set(user, level, category_ids)
level_num = notification_levels[level]
category_ids = Category.where(id: category_ids).pluck(:id)
changed = false
# Update pre-existing category users
if category_ids.present? &&
CategoryUser
.where(user_id: user.id, category_id: category_ids)
.where.not(notification_level: level_num)
.update_all(notification_level: level_num) > 0
changed = true
end
# Remove extraneous category users
if CategoryUser
.where(user_id: user.id, notification_level: level_num)
.where.not(category_id: category_ids)
.delete_all > 0
changed = true
end
if category_ids.present?
params = { user_id: user.id, level_num: level_num }
sql = <<~SQL
INSERT INTO category_users (user_id, category_id, notification_level)
SELECT :user_id, :category_id, :level_num
ON CONFLICT DO NOTHING
SQL
# we could use VALUES here but it would introduce a string
# into the query, plus it is a bit of a micro optimisation
category_ids.each do |category_id|
params[:category_id] = category_id
changed = true if DB.exec(sql, params) > 0
end
end
if changed
auto_watch(user_id: user.id)
auto_track(user_id: user.id)
end
changed
end
def self.set_notification_level_for_category(user, level, category_id)
record = CategoryUser.where(user: user, category_id: category_id).first
return if record && record.notification_level == level
if record.present?
record.notification_level = level
record.save!
else
begin
CategoryUser.create!(user: user, category_id: category_id, notification_level: level)
rescue ActiveRecord::RecordNotUnique
# does not matter
end
end
auto_watch(user_id: user.id)
auto_track(user_id: user.id)
end
def self.auto_track(opts = {})
builder = DB.build <<~SQL
UPDATE topic_users tu
SET notification_level = :tracking,
notifications_reason_id = :auto_track_category
FROM topics t, category_users cu
/*where*/
SQL
builder.where(
"tu.topic_id = t.id AND
cu.category_id = t.category_id AND
cu.user_id = tu.user_id AND
cu.notification_level = :tracking AND
tu.notification_level = :regular",
)
if category_id = opts[:category_id]
builder.where("t.category_id = :category_id", category_id: category_id)
end
if topic_id = opts[:topic_id]
builder.where("tu.topic_id = :topic_id", topic_id: topic_id)
end
if user_id = opts[:user_id]
builder.where("tu.user_id = :user_id", user_id: user_id)
end
builder.exec(
tracking: notification_levels[:tracking],
regular: notification_levels[:regular],
auto_track_category: TopicUser.notification_reasons[:auto_track_category],
)
end
def self.auto_watch(opts = {})
builder = DB.build <<~SQL
UPDATE topic_users tu
SET notification_level =
CASE WHEN should_track THEN :tracking
WHEN should_watch THEN :watching
ELSE notification_level
END,
notifications_reason_id =
CASE WHEN should_track THEN null
WHEN should_watch THEN :auto_watch_category
ELSE notifications_reason_id
END
FROM (
SELECT tu1.topic_id,
tu1.user_id,
CASE WHEN
cu.user_id IS NULL AND tu1.notification_level = :watching AND tu1.notifications_reason_id = :auto_watch_category THEN true
ELSE false
END should_track,
CASE WHEN
cu.user_id IS NOT NULL AND tu1.notification_level in (:regular, :tracking) THEN true
ELSE false
END should_watch
FROM topic_users tu1
JOIN topics t ON t.id = tu1.topic_id
LEFT JOIN category_users cu ON cu.category_id = t.category_id AND cu.user_id = tu1.user_id AND cu.notification_level = :watching
/*where2*/
) as X
/*where*/
SQL
builder.where("X.topic_id = tu.topic_id AND X.user_id = tu.user_id")
builder.where("should_watch OR should_track")
if category_id = opts[:category_id]
builder.where2("t.category_id = :category_id", category_id: category_id)
end
if topic_id = opts[:topic_id]
builder.where("tu.topic_id = :topic_id", topic_id: topic_id)
builder.where2("tu1.topic_id = :topic_id", topic_id: topic_id)
end
if user_id = opts[:user_id]
builder.where("tu.user_id = :user_id", user_id: user_id)
builder.where2("tu1.user_id = :user_id", user_id: user_id)
end
builder.exec(
watching: notification_levels[:watching],
tracking: notification_levels[:tracking],
regular: notification_levels[:regular],
auto_watch_category: TopicUser.notification_reasons[:auto_watch_category],
)
end
def self.ensure_consistency!
DB.exec <<~SQL
DELETE FROM category_users
WHERE user_id IN (
SELECT cu.user_id FROM category_users cu
LEFT JOIN users u ON u.id = cu.user_id
WHERE u.id IS NULL
)
SQL
end
def self.default_notification_level
if SiteSetting.mute_all_categories_by_default
notification_levels[:muted]
else
notification_levels[:regular]
end
end
def self.notification_levels_for(user)
# Anonymous users have all default categories set to regular tracking,
# except for default muted categories which stay muted.
if user.blank?
notification_levels =
[
SiteSetting.default_categories_watching.split("|"),
SiteSetting.default_categories_tracking.split("|"),
SiteSetting.default_categories_watching_first_post.split("|"),
SiteSetting.default_categories_normal.split("|"),
].flatten.map { |id| [id.to_i, self.notification_levels[:regular]] }
notification_levels +=
SiteSetting
.default_categories_muted
.split("|")
.map { |id| [id.to_i, self.notification_levels[:muted]] }
else
notification_levels = CategoryUser.where(user: user).pluck(:category_id, :notification_level)
end
Hash[*notification_levels.flatten]
end
def self.lookup_for(user, category_ids)
return {} if user.blank? || category_ids.blank?
create_lookup(CategoryUser.where(category_id: category_ids, user_id: user.id))
end
def self.create_lookup(category_users)
category_users.each_with_object({}) do |category_user, acc|
acc[category_user.category_id] = category_user
end
end
def self.muted_category_ids_query(user, include_direct: false)
query = Category
query = query.where.not(parent_category_id: nil) if !include_direct
query =
query
.joins("LEFT JOIN categories categories2 ON categories2.id = categories.parent_category_id")
.joins(
"LEFT JOIN category_users ON category_users.category_id = categories.id AND category_users.user_id = #{user.id}",
)
.joins(
"LEFT JOIN category_users category_users2 ON category_users2.category_id = categories2.id AND category_users2.user_id = #{user.id}",
)
direct_category_muted_sql =
"COALESCE(category_users.notification_level, #{CategoryUser.default_notification_level}) = #{CategoryUser.notification_levels[:muted]}"
parent_category_muted_sql =
"(category_users.id IS NULL AND COALESCE(category_users2.notification_level, #{CategoryUser.default_notification_level}) = #{notification_levels[:muted]})"
conditions = [parent_category_muted_sql]
conditions.push(direct_category_muted_sql) if include_direct
if SiteSetting.max_category_nesting === 3
query =
query.joins(
"LEFT JOIN categories categories3 ON categories3.id = categories2.parent_category_id",
).joins(
"LEFT JOIN category_users category_users3 ON category_users3.category_id = categories3.id AND category_users3.user_id = #{user.id}",
)
grandparent_category_muted_sql =
"(category_users.id IS NULL AND category_users2.id IS NULL AND COALESCE(category_users3.notification_level, #{CategoryUser.default_notification_level}) = #{notification_levels[:muted]})"
conditions.push(grandparent_category_muted_sql)
end
query.where(conditions.join(" OR "))
end
def self.muted_category_ids(user)
muted_category_ids_query(user, include_direct: true).pluck("categories.id")
end
def self.indirectly_muted_category_ids(user)
muted_category_ids_query(user).pluck("categories.id")
end
end
# == Schema Information
#
# Table name: category_users
#
# id :integer not null, primary key
# category_id :integer not null
# user_id :integer not null
# notification_level :integer not null
# last_seen_at :datetime
#
# Indexes
#
# idx_category_users_category_id_user_id (category_id,user_id) UNIQUE
# idx_category_users_user_id_category_id (user_id,category_id) UNIQUE
# index_category_users_on_category_id_and_notification_level (category_id,notification_level)
# index_category_users_on_user_id_and_last_seen_at (user_id,last_seen_at)
#