FEATURE: set notification levels when added to a group (#10378)

* FEATURE: set notification levels when added to a group

This feature allows admins and group owners to define default
category and tag tracking levels that will be applied to user
preferences automatically at the time when users are added to the
group. Users are free to change those preferences afterwards.
When removed from a group, the user's notification preferences aren't
changed.
This commit is contained in:
Neil Lalonde
2020-08-06 12:27:27 -04:00
committed by GitHub
parent cd4f251891
commit 1ca81fbb95
27 changed files with 937 additions and 8 deletions

View File

@ -23,6 +23,7 @@ module Roleable
set_permission('moderator', true)
auto_approve_user
enqueue_staff_welcome_message(:moderator)
set_default_notification_levels(:moderators)
end
def revoke_moderation!
@ -34,6 +35,7 @@ module Roleable
set_permission('admin', true)
auto_approve_user
enqueue_staff_welcome_message(:admin)
set_default_notification_levels(:admins)
end
def revoke_admin!
@ -52,6 +54,13 @@ module Roleable
save_and_refresh_staff_groups!
end
def set_default_notification_levels(group_name)
Group.set_category_and_tag_default_notification_levels!(self, group_name)
if group_name == :admins || group_name == :moderators
Group.set_category_and_tag_default_notification_levels!(self, :staff)
end
end
private
def auto_approve_user

View File

@ -29,6 +29,8 @@ class Group < ActiveRecord::Base
has_many :group_histories, dependent: :destroy
has_many :category_reviews, class_name: 'Category', foreign_key: :reviewable_by_group_id, dependent: :nullify
has_many :reviewables, foreign_key: :reviewable_by_group_id, dependent: :nullify
has_many :group_category_notification_defaults, dependent: :destroy
has_many :group_tag_notification_defaults, dependent: :destroy
belongs_to :flair_upload, class_name: 'Upload'
@ -51,6 +53,7 @@ class Group < ActiveRecord::Base
after_commit :trigger_group_created_event, on: :create
after_commit :trigger_group_updated_event, on: :update
after_commit :trigger_group_destroyed_event, on: :destroy
after_commit :set_default_notifications, on: [:create, :update]
def expire_cache
ApplicationSerializer.expire_cache_fragment!("group_names")
@ -382,6 +385,13 @@ class Group < ActiveRecord::Base
end
end
def self.set_category_and_tag_default_notification_levels!(user, group_name)
if group = lookup_group(group_name)
GroupUser.set_category_notifications(group, user)
GroupUser.set_tag_notifications(group, user)
end
end
def self.refresh_automatic_group!(name)
return unless id = AUTO_GROUPS[name]
@ -755,6 +765,32 @@ class Group < ActiveRecord::Base
flair_icon.presence || flair_upload&.short_path
end
[:muted, :tracking, :watching, :watching_first_post].each do |level|
define_method("#{level}_category_ids=") do |category_ids|
@category_notifications ||= {}
@category_notifications[level] = category_ids
end
define_method("#{level}_tags=") do |tag_names|
@tag_notifications ||= {}
@tag_notifications[level] = tag_names
end
end
def set_default_notifications
if @category_notifications
@category_notifications.each do |level, category_ids|
GroupCategoryNotificationDefault.batch_set(self, level, category_ids)
end
end
if @tag_notifications
@tag_notifications.each do |level, tag_names|
GroupTagNotificationDefault.batch_set(self, level, tag_names)
end
end
end
def imap_mailboxes
return [] if self.imap_server.blank? ||
self.email_username.blank? ||

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
class GroupCategoryNotificationDefault < ActiveRecord::Base
belongs_to :group
belongs_to :category
def self.notification_levels
NotificationLevels.all
end
def self.lookup(group, level)
self.where(group: group, notification_level: notification_levels[level])
end
def self.batch_set(group, level, category_ids)
level_num = notification_levels[level]
category_ids = Category.where(id: category_ids).pluck(:id)
changed = false
# Update pre-existing
if category_ids.present? && GroupCategoryNotificationDefault
.where(group_id: group.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 GroupCategoryNotificationDefault
.where(group_id: group.id, notification_level: level_num)
.where.not(category_id: category_ids)
.delete_all > 0
changed = true
end
if category_ids.present?
params = {
group_id: group.id,
level_num: level_num,
}
sql = <<~SQL
INSERT INTO group_category_notification_defaults (group_id, category_id, notification_level)
SELECT :group_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
if DB.exec(sql, params) > 0
changed = true
end
end
end
changed
end
end
# == Schema Information
#
# Table name: group_category_notification_defaults
#
# id :bigint not null, primary key
# group_id :integer not null
# category_id :integer not null
# notification_level :integer not null
#
# Indexes
#
# idx_group_category_notification_defaults_unique (group_id,category_id) UNIQUE
#

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
class GroupTagNotificationDefault < ActiveRecord::Base
belongs_to :group
belongs_to :tag
def self.notification_levels
NotificationLevels.all
end
def self.lookup(group, level)
self.where(group: group, notification_level: notification_levels[level])
end
def self.batch_set(group, level, tag_names)
tag_names ||= []
changed = false
records = self.where(group: group, notification_level: notification_levels[level])
old_ids = records.pluck(:tag_id)
tag_ids = tag_names.empty? ? [] : Tag.where_name(tag_names).pluck(:id)
Tag.where_name(tag_names).joins(:target_tag).each do |tag|
tag_ids[tag_ids.index(tag.id)] = tag.target_tag_id
end
tag_ids.uniq!
remove = (old_ids - tag_ids)
if remove.present?
records.where('tag_id in (?)', remove).destroy_all
changed = true
end
(tag_ids - old_ids).each do |id|
self.create!(group: group, tag_id: id, notification_level: notification_levels[level])
changed = true
end
changed
end
end
# == Schema Information
#
# Table name: group_tag_notification_defaults
#
# id :bigint not null, primary key
# group_id :integer not null
# tag_id :integer not null
# notification_level :integer not null
#
# Indexes
#
# idx_group_tag_notification_defaults_unique (group_id,tag_id) UNIQUE
#

View File

@ -12,6 +12,8 @@ class GroupUser < ActiveRecord::Base
before_create :set_notification_level
after_save :grant_trust_level
after_save :set_category_notifications
after_save :set_tag_notifications
def self.notification_levels
NotificationLevels.all
@ -64,6 +66,70 @@ class GroupUser < ActiveRecord::Base
Promotion.recalculate(user)
end
def set_category_notifications
self.class.set_category_notifications(group, user)
end
def self.set_category_notifications(group, user)
group_levels = group.group_category_notification_defaults.each_with_object({}) do |r, h|
h[r.notification_level] ||= []
h[r.notification_level] << r.category_id
end
return if group_levels.empty?
user_levels = CategoryUser.where(user_id: user.id).each_with_object({}) do |r, h|
h[r.notification_level] ||= []
h[r.notification_level] << r.category_id
end
higher_level_category_ids = user_levels.values.flatten
[:muted, :tracking, :watching_first_post, :watching].each do |level|
level_num = NotificationLevels.all[level]
higher_level_category_ids -= (user_levels[level_num] || [])
if group_category_ids = group_levels[level_num]
CategoryUser.batch_set(
user,
level,
group_category_ids + (user_levels[level_num] || []) - higher_level_category_ids
)
end
end
end
def set_tag_notifications
self.class.set_tag_notifications(group, user)
end
def self.set_tag_notifications(group, user)
group_levels = group.group_tag_notification_defaults.each_with_object({}) do |r, h|
h[r.notification_level] ||= []
h[r.notification_level] << r.tag_id
end
return if group_levels.empty?
user_levels = TagUser.where(user_id: user.id).each_with_object({}) do |r, h|
h[r.notification_level] ||= []
h[r.notification_level] << r.tag_id
end
higher_level_tag_ids = user_levels.values.flatten
[:muted, :tracking, :watching_first_post, :watching].each do |level|
level_num = NotificationLevels.all[level]
higher_level_tag_ids -= (user_levels[level_num] || [])
if group_tag_ids = group_levels[level_num]
TagUser.batch_set(
user,
level,
group_tag_ids + (user_levels[level_num] || []) - higher_level_tag_ids
)
end
end
end
end
# == Schema Information

View File

@ -19,23 +19,50 @@ class TagUser < ActiveRecord::Base
records = TagUser.where(user: user, notification_level: notification_levels[level])
old_ids = records.pluck(:tag_id)
tag_ids = tags.empty? ? [] : Tag.where_name(tags).pluck(:id)
tag_ids = if tags.empty?
[]
elsif tags.first&.is_a?(String)
Tag.where_name(tags).pluck(:id)
else
tags
end
Tag.where_name(tags).joins(:target_tag).each do |tag|
Tag.where(id: tag_ids).joins(:target_tag).each do |tag|
tag_ids[tag_ids.index(tag.id)] = tag.target_tag_id
end
tag_ids.uniq!
if tag_ids.present? &&
TagUser.where(user_id: user.id, tag_id: tag_ids)
.where
.not(notification_level: notification_levels[level])
.update_all(notification_level: notification_levels[level]) > 0
changed = true
end
remove = (old_ids - tag_ids)
if remove.present?
records.where('tag_id in (?)', remove).destroy_all
changed = true
end
(tag_ids - old_ids).each do |id|
TagUser.create!(user: user, tag_id: id, notification_level: notification_levels[level])
changed = true
now = Time.zone.now
new_records_attrs = (tag_ids - old_ids).map do |tag_id|
{
user_id: user.id,
tag_id: tag_id,
notification_level: notification_levels[level],
created_at: now,
updated_at: now
}
end
unless new_records_attrs.empty?
result = TagUser.insert_all(new_records_attrs)
changed = true if result.rows.length > 0
end
if changed