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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 937 additions and 8 deletions

View File

@ -0,0 +1,14 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
export default Controller.extend({
@discourseComputed(
"model.watchingCategories.[]",
"model.watchingFirstPostCategories.[]",
"model.trackingCategories.[]",
"model.mutedCategories.[]"
)
selectedCategories(watching, watchingFirst, tracking, muted) {
return [].concat(watching, watchingFirst, tracking, muted).filter(t => t);
}
});

View File

@ -0,0 +1,14 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
export default Controller.extend({
@discourseComputed(
"model.watching_tags.[]",
"model.watching_first_post_tags.[]",
"model.tracking_tags.[]",
"model.muted_tags.[]"
)
selectedTags(watching, watchingFirst, tracking, muted) {
return [].concat(watching, watchingFirst, tracking, muted).filter(t => t);
}
});

View File

@ -13,6 +13,14 @@ export default Controller.extend({
route: "group.manage.interaction", route: "group.manage.interaction",
title: "groups.manage.interaction.title" title: "groups.manage.interaction.title"
}, },
{
route: "group.manage.categories",
title: "groups.manage.categories.title"
},
{
route: "group.manage.tags",
title: "groups.manage.tags.title"
},
{ route: "group.manage.logs", title: "groups.manage.logs.title" } { route: "group.manage.logs", title: "groups.manage.logs.title" }
]; ];

View File

@ -176,6 +176,35 @@ const Group = RestModel.extend({
} }
}, },
@observes("watching_category_ids")
_updateWatchingCategories() {
this.set(
"watchingCategories",
Category.findByIds(this.watching_category_ids)
);
},
@observes("tracking_category_ids")
_updateTrackingCategories() {
this.set(
"trackingCategories",
Category.findByIds(this.tracking_category_ids)
);
},
@observes("watching_first_post_category_ids")
_updateWatchingFirstPostCategories() {
this.set(
"watchingFirstPostCategories",
Category.findByIds(this.watching_first_post_category_ids)
);
},
@observes("muted_category_ids")
_updateMutedCategories() {
this.set("mutedCategories", Category.findByIds(this.muted_category_ids));
},
asJSON() { asJSON() {
const attrs = { const attrs = {
name: this.name, name: this.name,
@ -211,6 +240,26 @@ const Group = RestModel.extend({
publish_read_state: this.publish_read_state publish_read_state: this.publish_read_state
}; };
["muted", "watching", "tracking", "watching_first_post"].forEach(s => {
let prop =
s === "watching_first_post"
? "watchingFirstPostCategories"
: s + "Categories";
let categories = this.get(prop);
if (categories) {
attrs[s + "_category_ids"] =
categories.length > 0 ? categories.map(c => c.get("id")) : [-1];
}
let tags = this.get(s + "_tags");
if (tags) {
attrs[s + "_tags"] = tags.length > 0 ? tags : [""];
}
});
if (this.flair_type === "icon") { if (this.flair_type === "icon") {
attrs["flair_icon"] = this.flair_icon; attrs["flair_icon"] = this.flair_icon;
} else if (this.flair_type === "image") { } else if (this.flair_type === "image") {

View File

@ -94,6 +94,8 @@ export default function() {
this.route("interaction"); this.route("interaction");
this.route("email"); this.route("email");
this.route("members"); this.route("members");
this.route("categories");
this.route("tags");
this.route("logs"); this.route("logs");
}); });

View File

@ -0,0 +1,10 @@
import I18n from "I18n";
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
showFooter: true,
titleToken() {
return I18n.t("groups.manage.categories.title");
}
});

View File

@ -0,0 +1,10 @@
import I18n from "I18n";
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
showFooter: true,
titleToken() {
return I18n.t("groups.manage.tags.title");
}
});

View File

@ -0,0 +1,64 @@
<form class="groups-form form-vertical groups-notifications-form">
<div class="control-group">
<label class="control-label">{{i18n "groups.manage.categories.long_title"}}</label>
<div>{{i18n "groups.manage.categories.description"}}</div>
</div>
<div class="control-group">
<label>{{d-icon "d-watching"}} {{i18n "user.watched_categories"}}</label>
{{category-selector
categories=model.watchingCategories
blacklist=selectedCategories
onChange=(action (mut model.watchingCategories))
}}
<div class="control-instructions">
{{i18n "groups.manage.categories.watched_categories_instructions"}}
</div>
</div>
<div class="control-group">
<label>{{d-icon "d-tracking"}} {{i18n "user.tracked_categories"}}</label>
{{category-selector
categories=model.trackingCategories
blacklist=selectedCategories
onChange=(action (mut model.trackingCategories))
}}
<div class="control-instructions">
{{i18n "groups.manage.categories.tracked_categories_instructions"}}
</div>
</div>
<div class="control-group">
<label>{{d-icon "d-watching-first"}} {{i18n "user.watched_first_post_categories"}}</label>
{{category-selector
categories=model.watchingFirstPostCategories
blacklist=selectedCategories
onChange=(action (mut model.watchingFirstPostCategories))
}}
<div class="control-instructions">
{{i18n "groups.manage.categories.watching_first_post_categories_instructions"}}
</div>
</div>
<div class="control-group">
<label>{{d-icon "d-muted"}} {{i18n "user.muted_categories"}}</label>
{{category-selector
categories=model.mutedCategories
blacklist=selectedCategories
onChange=(action (mut model.mutedCategories))
}}
<div class="control-instructions">
{{i18n "groups.manage.categories.muted_categories_instructions"}}
</div>
</div>
{{group-manage-save-button model=model}}
</form>

View File

@ -0,0 +1,72 @@
<form class="groups-form form-vertical groups-notifications-form">
<div class="control-group">
<label class="control-label">{{i18n "groups.manage.tags.long_title"}}</label>
<div>{{i18n "groups.manage.tags.description"}}</div>
</div>
<div class="control-group">
<label>{{d-icon "d-watching"}} {{i18n "user.watched_tags"}}</label>
{{tag-chooser
tags=model.watching_tags
blacklist=selectedTags
allowCreate=false
everyTag=true
unlimitedTagCount=true
}}
<div class="control-instructions">
{{i18n "groups.manage.tags.watched_tags_instructions"}}
</div>
</div>
<div class="control-group">
<label>{{d-icon "d-tracking"}} {{i18n "user.tracked_tags"}}</label>
{{tag-chooser
tags=model.tracking_tags
blacklist=selectedTags
allowCreate=false
everyTag=true
unlimitedTagCount=true
}}
<div class="control-instructions">
{{i18n "groups.manage.tags.tracked_tags_instructions"}}
</div>
</div>
<div class="control-group">
<label>{{d-icon "d-watching-first"}} {{i18n "user.watched_first_post_tags"}}</label>
{{tag-chooser
tags=model.watching_first_post_tags
blacklist=selectedTags
allowCreate=false
everyTag=true
unlimitedTagCount=true
}}
<div class="control-instructions">
{{i18n "groups.manage.tags.watching_first_post_tags_instructions"}}
</div>
</div>
<div class="control-group">
<label>{{d-icon "d-muted"}} {{i18n "user.muted_tags"}}</label>
{{tag-chooser
tags=model.muted_tags
blacklist=selectedTags
allowCreate=false
everyTag=true
unlimitedTagCount=true
}}
<div class="control-instructions">
{{i18n "groups.manage.tags.muted_tags_instructions"}}
</div>
</div>
{{group-manage-save-button model=model}}
</form>

View File

@ -146,4 +146,17 @@
.control-group-inline { .control-group-inline {
display: inline; display: inline;
} }
&.groups-notifications-form {
.control-instructions {
color: $primary-medium;
margin-bottom: 10px;
font-size: $font-down-1;
line-height: $line-height-large;
}
.category-selector,
.tag-chooser {
width: 100%;
}
}
} }

View File

@ -50,3 +50,8 @@
top: 20px; top: 20px;
right: 20px; right: 20px;
} }
.groups-form.groups-notifications-form {
width: 500px;
max-width: 100%;
}

View File

@ -604,6 +604,13 @@ class GroupsController < ApplicationController
default_params default_params
end end
if !automatic || current_user.admin
[:muted, :tracking, :watching, :watching_first_post].each do |level|
permitted_params << { "#{level}_category_ids" => [] }
permitted_params << { "#{level}_tags" => [] }
end
end
params.require(:group).permit(*permitted_params) params.require(:group).permit(*permitted_params)
end end

View File

@ -23,6 +23,7 @@ module Roleable
set_permission('moderator', true) set_permission('moderator', true)
auto_approve_user auto_approve_user
enqueue_staff_welcome_message(:moderator) enqueue_staff_welcome_message(:moderator)
set_default_notification_levels(:moderators)
end end
def revoke_moderation! def revoke_moderation!
@ -34,6 +35,7 @@ module Roleable
set_permission('admin', true) set_permission('admin', true)
auto_approve_user auto_approve_user
enqueue_staff_welcome_message(:admin) enqueue_staff_welcome_message(:admin)
set_default_notification_levels(:admins)
end end
def revoke_admin! def revoke_admin!
@ -52,6 +54,13 @@ module Roleable
save_and_refresh_staff_groups! save_and_refresh_staff_groups!
end 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 private
def auto_approve_user def auto_approve_user

View File

@ -29,6 +29,8 @@ class Group < ActiveRecord::Base
has_many :group_histories, dependent: :destroy has_many :group_histories, dependent: :destroy
has_many :category_reviews, class_name: 'Category', foreign_key: :reviewable_by_group_id, dependent: :nullify 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 :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' 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_created_event, on: :create
after_commit :trigger_group_updated_event, on: :update after_commit :trigger_group_updated_event, on: :update
after_commit :trigger_group_destroyed_event, on: :destroy after_commit :trigger_group_destroyed_event, on: :destroy
after_commit :set_default_notifications, on: [:create, :update]
def expire_cache def expire_cache
ApplicationSerializer.expire_cache_fragment!("group_names") ApplicationSerializer.expire_cache_fragment!("group_names")
@ -382,6 +385,13 @@ class Group < ActiveRecord::Base
end end
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) def self.refresh_automatic_group!(name)
return unless id = AUTO_GROUPS[name] return unless id = AUTO_GROUPS[name]
@ -755,6 +765,32 @@ class Group < ActiveRecord::Base
flair_icon.presence || flair_upload&.short_path flair_icon.presence || flair_upload&.short_path
end 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 def imap_mailboxes
return [] if self.imap_server.blank? || return [] if self.imap_server.blank? ||
self.email_username.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 before_create :set_notification_level
after_save :grant_trust_level after_save :grant_trust_level
after_save :set_category_notifications
after_save :set_tag_notifications
def self.notification_levels def self.notification_levels
NotificationLevels.all NotificationLevels.all
@ -64,6 +66,70 @@ class GroupUser < ActiveRecord::Base
Promotion.recalculate(user) Promotion.recalculate(user)
end 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 end
# == Schema Information # == Schema Information

View File

@ -19,23 +19,50 @@ class TagUser < ActiveRecord::Base
records = TagUser.where(user: user, notification_level: notification_levels[level]) records = TagUser.where(user: user, notification_level: notification_levels[level])
old_ids = records.pluck(:tag_id) 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 tag_ids[tag_ids.index(tag.id)] = tag.target_tag_id
end end
tag_ids.uniq! 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) remove = (old_ids - tag_ids)
if remove.present? if remove.present?
records.where('tag_id in (?)', remove).destroy_all records.where('tag_id in (?)', remove).destroy_all
changed = true changed = true
end end
(tag_ids - old_ids).each do |id| now = Time.zone.now
TagUser.create!(user: user, tag_id: id, notification_level: notification_levels[level])
changed = true 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 end
if changed if changed

View File

@ -57,6 +57,24 @@ class BasicGroupSerializer < ApplicationSerializer
:imap_old_emails, :imap_old_emails,
:imap_new_emails :imap_new_emails
def self.admin_or_owner_attributes(*attrs)
attributes(*attrs)
attrs.each do |attr|
define_method "include_#{attr}?" do
scope.is_admin? || (include_is_group_owner? && is_group_owner)
end
end
end
admin_or_owner_attributes :watching_category_ids,
:tracking_category_ids,
:watching_first_post_category_ids,
:muted_category_ids,
:watching_tags,
:watching_first_post_tags,
:tracking_tags,
:muted_tags
def include_display_name? def include_display_name?
object.automatic object.automatic
end end
@ -103,6 +121,16 @@ class BasicGroupSerializer < ApplicationSerializer
scope.can_see_group_members?(object) scope.can_see_group_members?(object)
end end
[:watching, :tracking, :watching_first_post, :muted].each do |level|
define_method("#{level}_category_ids") do
GroupCategoryNotificationDefault.lookup(object, level).pluck(:category_id)
end
define_method("#{level}_tags") do
GroupTagNotificationDefault.lookup(object, level).joins(:tag).pluck('tags.name')
end
end
private private
def staff? def staff?

View File

@ -656,6 +656,22 @@ en:
membership: membership:
title: Membership title: Membership
access: Access access: Access
categories:
title: Categories
long_title: "Category default notifications"
description: "When users are added to this group, their category notification settings will be set to these defaults. Afterwards, they can change them."
watched_categories_instructions: "Automatically watch all topics in these categories. Group members will be notified of all new posts and topics, and a count of new posts will also appear next to the topic."
tracked_categories_instructions: "Automatically track all topics in these categories. A count of new posts will appear next to the topic."
watching_first_post_categories_instructions: "Users will be notified of the first post in each new topic in these categories."
muted_categories_instructions: "Users will not be notified of anything about new topics in these categories, and they will not appear on the categories or latest topics pages."
tags:
title: Tags
long_title: "Tags default notifications"
description: "When users are added to this group, their tag notification settings will be set to these defaults. Afterwards, they can change them."
watched_tags_instructions: "Automatically watch all topics with these tags. Group members will be notified of all new posts and topics, and a count of new posts will also appear next to the topic."
tracked_tags_instructions: "Automatically track all topics with these tags. A count of new posts will appear next to the topic."
watching_first_post_tags_instructions: "Users will be notified of the first post in each new topic with these tags."
muted_tags_instructions: "Users will not be notified of anything about new topics with these tags, and they will not appear in latest."
logs: logs:
title: "Logs" title: "Logs"
when: "When" when: "When"

View File

@ -577,6 +577,8 @@ Discourse::Application.routes.draw do
manage/membership manage/membership
manage/interaction manage/interaction
manage/email manage/email
manage/categories
manage/tags
manage/logs manage/logs
}.each do |path| }.each do |path|
get path => 'groups#show' get path => 'groups#show'

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class CreateGroupDefaultTracking < ActiveRecord::Migration[6.0]
def change
create_table :group_category_notification_defaults do |t|
t.integer :group_id, null: false
t.integer :category_id, null: false
t.integer :notification_level, null: false
end
add_index :group_category_notification_defaults,
[:group_id, :category_id],
unique: true,
name: :idx_group_category_notification_defaults_unique
create_table :group_tag_notification_defaults do |t|
t.integer :group_id, null: false
t.integer :tag_id, null: false
t.integer :notification_level, null: false
end
add_index :group_tag_notification_defaults,
[:group_id, :tag_id],
unique: true,
name: :idx_group_tag_notification_defaults_unique
end
end

View File

@ -1054,4 +1054,121 @@ describe Group do
expect(group.name).to eq("Bücherwurm") # NFC expect(group.name).to eq("Bücherwurm") # NFC
end end
end end
describe "default notifications" do
let(:category1) { Fabricate(:category) }
let(:category2) { Fabricate(:category) }
let(:category3) { Fabricate(:category) }
let(:tag1) { Fabricate(:tag) }
let(:tag2) { Fabricate(:tag) }
let(:tag3) { Fabricate(:tag) }
let(:synonym1) { Fabricate(:tag, target_tag: tag1) }
let(:synonym2) { Fabricate(:tag, target_tag: tag2) }
it "can set category notifications" do
group.watching_category_ids = [category1.id, category2.id]
group.tracking_category_ids = [category3.id]
group.save!
expect(GroupCategoryNotificationDefault.lookup(group, :watching).pluck(:category_id)).to contain_exactly(category1.id, category2.id)
expect(GroupCategoryNotificationDefault.lookup(group, :tracking).pluck(:category_id)).to eq([category3.id])
new_group = Fabricate.build(:group)
new_group.watching_category_ids = [category1.id, category2.id]
new_group.save!
expect(GroupCategoryNotificationDefault.lookup(new_group, :watching).pluck(:category_id)).to contain_exactly(category1.id, category2.id)
end
it "can remove categories" do
[category1, category2].each do |category|
GroupCategoryNotificationDefault.create!(
group: group,
category: category,
notification_level: GroupCategoryNotificationDefault.notification_levels[:watching]
)
end
group.watching_category_ids = [category2.id]
group.save!
expect(GroupCategoryNotificationDefault.lookup(group, :watching).pluck(:category_id)).to eq([category2.id])
group.watching_category_ids = []
group.save!
expect(GroupCategoryNotificationDefault.lookup(group, :watching).pluck(:category_id)).to be_empty
end
it "can set tag notifications" do
group.watching_tags = [tag1.name, tag2.name]
group.tracking_tags = [tag3.name]
group.save!
expect(GroupTagNotificationDefault.lookup(group, :watching).pluck(:tag_id)).to contain_exactly(tag1.id, tag2.id)
expect(GroupTagNotificationDefault.lookup(group, :tracking).pluck(:tag_id)).to eq([tag3.id])
new_group = Fabricate.build(:group)
new_group.watching_first_post_tags = [tag1.name, tag3.name]
new_group.save!
expect(GroupTagNotificationDefault.lookup(new_group, :watching_first_post).pluck(:tag_id)).to contain_exactly(tag1.id, tag3.id)
end
it "can take tag synonyms" do
group.tracking_tags = [synonym1.name, synonym2.name, tag3.name]
group.save!
expect(GroupTagNotificationDefault.lookup(group, :tracking).pluck(:tag_id)).to contain_exactly(tag1.id, tag2.id, tag3.id)
group.tracking_tags = [synonym1.name, synonym2.name, tag1.name, tag2.name, tag3.name]
group.save!
expect(GroupTagNotificationDefault.lookup(group, :tracking).pluck(:tag_id)).to contain_exactly(tag1.id, tag2.id, tag3.id)
end
it "can remove tags" do
[tag1, tag2].each do |tag|
GroupTagNotificationDefault.create!(
group: group,
tag: tag,
notification_level: GroupTagNotificationDefault.notification_levels[:watching]
)
end
group.watching_tags = [tag2.name]
group.save!
expect(GroupTagNotificationDefault.lookup(group, :watching).pluck(:tag_id)).to eq([tag2.id])
group.watching_tags = []
group.save!
expect(GroupTagNotificationDefault.lookup(group, :watching)).to be_empty
end
it "can apply default notifications for admins group" do
group = Group.find(Group::AUTO_GROUPS[:admins])
group.tracking_category_ids = [category1.id]
group.tracking_tags = [tag1.name]
group.save!
user.grant_admin!
expect(CategoryUser.lookup(user, :tracking).pluck(:category_id)).to eq([category1.id])
expect(TagUser.lookup(user, :tracking).pluck(:tag_id)).to eq([tag1.id])
end
it "can apply default notifications for staff group" do
group = Group.find(Group::AUTO_GROUPS[:staff])
group.tracking_category_ids = [category1.id]
group.tracking_tags = [tag1.name]
group.save!
user.grant_admin!
expect(CategoryUser.lookup(user, :tracking).pluck(:category_id)).to eq([category1.id])
expect(TagUser.lookup(user, :tracking).pluck(:tag_id)).to eq([tag1.id])
end
it "can apply default notifications from two automatic groups" do
staff = Group.find(Group::AUTO_GROUPS[:staff])
staff.tracking_category_ids = [category1.id]
staff.tracking_tags = [tag1.name]
staff.save!
admins = Group.find(Group::AUTO_GROUPS[:admins])
admins.tracking_category_ids = [category2.id]
admins.tracking_tags = [tag2.name]
admins.save!
user.grant_admin!
expect(CategoryUser.lookup(user, :tracking).pluck(:category_id)).to contain_exactly(category1.id, category2.id)
expect(TagUser.lookup(user, :tracking).pluck(:tag_id)).to contain_exactly(tag1.id, tag2.id)
end
end
end end

View File

@ -32,4 +32,123 @@ describe GroupUser do
expect(gu.notification_level).to eq(NotificationLevels.all[:regular]) expect(gu.notification_level).to eq(NotificationLevels.all[:regular])
end end
describe "default category notifications" do
let(:group) { Fabricate(:group) }
let(:user) { Fabricate(:user) }
let(:category1) { Fabricate(:category) }
let(:category2) { Fabricate(:category) }
let(:category3) { Fabricate(:category) }
let(:category4) { Fabricate(:category) }
def levels
CategoryUser.notification_levels
end
it "doesn't change anything with no configured defaults" do
expect { group.add(user) }.to_not change { CategoryUser.count }
end
it "adds new category notifications" do
group.muted_category_ids = [category1.id]
group.tracking_category_ids = [category2.id]
group.watching_category_ids = [category3.id]
group.watching_first_post_category_ids = [category4.id]
group.save!
expect { group.add(user) }.to change { CategoryUser.count }.by(4)
h = CategoryUser.notification_levels_for(Guardian.new(user))
expect(h[category1.id]).to eq(levels[:muted])
expect(h[category2.id]).to eq(levels[:tracking])
expect(h[category3.id]).to eq(levels[:watching])
expect(h[category4.id]).to eq(levels[:watching_first_post])
end
it "only upgrades notifications" do
CategoryUser.create!(user: user, category_id: category1.id, notification_level: levels[:muted])
CategoryUser.create!(user: user, category_id: category2.id, notification_level: levels[:tracking])
CategoryUser.create!(user: user, category_id: category3.id, notification_level: levels[:watching_first_post])
CategoryUser.create!(user: user, category_id: category4.id, notification_level: levels[:watching])
group.watching_first_post_category_ids = [category1.id, category2.id, category3.id, category4.id]
group.save!
group.add(user)
h = CategoryUser.notification_levels_for(Guardian.new(user))
expect(h[category1.id]).to eq(levels[:watching_first_post])
expect(h[category2.id]).to eq(levels[:watching_first_post])
expect(h[category3.id]).to eq(levels[:watching_first_post])
expect(h[category4.id]).to eq(levels[:watching])
end
it "merges notifications" do
CategoryUser.create!(user: user, category_id: category1.id, notification_level: CategoryUser.notification_levels[:tracking])
CategoryUser.create!(user: user, category_id: category2.id, notification_level: CategoryUser.notification_levels[:watching])
CategoryUser.create!(user: user, category_id: category4.id, notification_level: CategoryUser.notification_levels[:watching_first_post])
group.muted_category_ids = [category3.id]
group.tracking_category_ids = [category4.id]
group.save!
group.add(user)
h = CategoryUser.notification_levels_for(Guardian.new(user))
expect(h[category1.id]).to eq(levels[:tracking])
expect(h[category2.id]).to eq(levels[:watching])
expect(h[category3.id]).to eq(levels[:muted])
expect(h[category4.id]).to eq(levels[:watching_first_post])
end
end
describe "default tag notifications" do
let(:group) { Fabricate(:group) }
let(:user) { Fabricate(:user) }
let(:tag1) { Fabricate(:tag) }
let(:tag2) { Fabricate(:tag) }
let(:tag3) { Fabricate(:tag) }
let(:tag4) { Fabricate(:tag) }
let(:synonym1) { Fabricate(:tag, target_tag: tag1) }
def levels
TagUser.notification_levels
end
it "doesn't change anything with no configured defaults" do
expect { group.add(user) }.to_not change { TagUser.count }
end
it "adds new tag notifications" do
group.muted_tags = [synonym1.name]
group.tracking_tags = [tag2.name]
group.watching_tags = [tag3.name]
group.watching_first_post_tags = [tag4.name]
group.save!
expect { group.add(user) }.to change { TagUser.count }.by(4)
expect(TagUser.lookup(user, :muted).pluck(:tag_id)).to eq([tag1.id])
expect(TagUser.lookup(user, :tracking).pluck(:tag_id)).to eq([tag2.id])
expect(TagUser.lookup(user, :watching).pluck(:tag_id)).to eq([tag3.id])
expect(TagUser.lookup(user, :watching_first_post).pluck(:tag_id)).to eq([tag4.id])
end
it "only upgrades notifications" do
TagUser.create!(user: user, tag_id: tag1.id, notification_level: levels[:muted])
TagUser.create!(user: user, tag_id: tag2.id, notification_level: levels[:tracking])
TagUser.create!(user: user, tag_id: tag3.id, notification_level: levels[:watching_first_post])
TagUser.create!(user: user, tag_id: tag4.id, notification_level: levels[:watching])
group.watching_first_post_tags = [tag1.name, tag2.name, tag3.name, tag4.name]
group.save!
group.add(user)
expect(TagUser.lookup(user, :muted).pluck(:tag_id)).to be_empty
expect(TagUser.lookup(user, :tracking).pluck(:tag_id)).to be_empty
expect(TagUser.lookup(user, :watching).pluck(:tag_id)).to eq([tag4.id])
expect(TagUser.lookup(user, :watching_first_post).pluck(:tag_id)).to contain_exactly(tag1.id, tag2.id, tag3.id)
end
it "merges notifications" do
TagUser.create!(user: user, tag_id: tag1.id, notification_level: levels[:tracking])
TagUser.create!(user: user, tag_id: tag2.id, notification_level: levels[:watching])
TagUser.create!(user: user, tag_id: tag4.id, notification_level: levels[:watching_first_post])
group.muted_tags = [tag3.name]
group.tracking_tags = [tag2.name]
group.save!
group.add(user)
expect(TagUser.lookup(user, :muted).pluck(:tag_id)).to eq([tag3.id])
expect(TagUser.lookup(user, :tracking).pluck(:tag_id)).to eq([tag1.id])
expect(TagUser.lookup(user, :watching).pluck(:tag_id)).to eq([tag2.id])
expect(TagUser.lookup(user, :watching_first_post).pluck(:tag_id)).to eq([tag4.id])
end
end
end end

View File

@ -615,6 +615,8 @@ describe GroupsController do
public_exit: false public_exit: false
) )
end end
let(:category) { Fabricate(:category) }
let(:tag) { Fabricate(:tag) }
context "custom_fields" do context "custom_fields" do
before do before do
@ -688,7 +690,9 @@ describe GroupsController do
allow_membership_requests: true, allow_membership_requests: true,
membership_request_template: 'testing', membership_request_template: 'testing',
default_notification_level: 1, default_notification_level: 1,
name: 'testing' name: 'testing',
tracking_category_ids: [category.id],
tracking_tags: [tag.name]
} }
} }
end.to change { GroupHistory.count }.by(13) end.to change { GroupHistory.count }.by(13)
@ -716,6 +720,8 @@ describe GroupsController do
expect(group.primary_group).to eq(false) expect(group.primary_group).to eq(false)
expect(group.incoming_email).to eq(nil) expect(group.incoming_email).to eq(nil)
expect(group.grant_trust_level).to eq(0) expect(group.grant_trust_level).to eq(0)
expect(group.group_category_notification_defaults.first&.category).to eq(category)
expect(group.group_tag_notification_defaults.first&.tag).to eq(tag)
end end
it 'should not be allowed to update automatic groups' do it 'should not be allowed to update automatic groups' do
@ -753,7 +759,9 @@ describe GroupsController do
automatic_membership_email_domains: 'test.org', automatic_membership_email_domains: 'test.org',
grant_trust_level: 2, grant_trust_level: 2,
visibility_level: 1, visibility_level: 1,
members_visibility_level: 3 members_visibility_level: 3,
tracking_category_ids: [category.id],
tracking_tags: [tag.name]
} }
} }
@ -768,6 +776,8 @@ describe GroupsController do
expect(group.members_visibility_level).to eq(3) expect(group.members_visibility_level).to eq(3)
expect(group.automatic_membership_email_domains).to eq('test.org') expect(group.automatic_membership_email_domains).to eq('test.org')
expect(group.grant_trust_level).to eq(2) expect(group.grant_trust_level).to eq(2)
expect(group.group_category_notification_defaults.first&.category).to eq(category)
expect(group.group_tag_notification_defaults.first&.tag).to eq(tag)
expect(Jobs::AutomaticGroupMembership.jobs.first["args"].first["group_id"]) expect(Jobs::AutomaticGroupMembership.jobs.first["args"].first["group_id"])
.to eq(group.id) .to eq(group.id)
@ -790,7 +800,9 @@ describe GroupsController do
visibility_level: 1, visibility_level: 1,
mentionable_level: 1, mentionable_level: 1,
messageable_level: 1, messageable_level: 1,
default_notification_level: 1 default_notification_level: 1,
tracking_category_ids: [category.id],
tracking_tags: [tag.name]
} }
} }
@ -803,6 +815,8 @@ describe GroupsController do
expect(group.mentionable_level).to eq(1) expect(group.mentionable_level).to eq(1)
expect(group.messageable_level).to eq(1) expect(group.messageable_level).to eq(1)
expect(group.default_notification_level).to eq(1) expect(group.default_notification_level).to eq(1)
expect(group.group_category_notification_defaults.first&.category).to eq(category)
expect(group.group_tag_notification_defaults.first&.tag).to eq(tag)
end end
it 'triggers a extensibility event' do it 'triggers a extensibility event' do

View File

@ -0,0 +1,33 @@
import { acceptance, updateCurrentUser } from "helpers/qunit-helpers";
acceptance("Managing Group Category Notification Defaults");
QUnit.test("As an anonymous user", async assert => {
await visit("/g/discourse/manage/categories");
assert.ok(
count(".group-members tr") > 0,
"it should redirect to members page for an anonymous user"
);
});
acceptance("Managing Group Category Notification Defaults", { loggedIn: true });
QUnit.test("As an admin", async assert => {
await visit("/g/discourse/manage/categories");
assert.ok(
find(".groups-notifications-form .category-selector").length === 4,
"it should display category inputs"
);
});
QUnit.test("As a group owner", async assert => {
updateCurrentUser({ moderator: false, admin: false });
await visit("/g/discourse/manage/categories");
assert.ok(
find(".groups-notifications-form .category-selector").length === 4,
"it should display category inputs"
);
});

View File

@ -0,0 +1,33 @@
import { acceptance, updateCurrentUser } from "helpers/qunit-helpers";
acceptance("Managing Group Tag Notification Defaults");
QUnit.test("As an anonymous user", async assert => {
await visit("/g/discourse/manage/tags");
assert.ok(
count(".group-members tr") > 0,
"it should redirect to members page for an anonymous user"
);
});
acceptance("Managing Group Tag Notification Defaults", { loggedIn: true });
QUnit.test("As an admin", async assert => {
await visit("/g/discourse/manage/tags");
assert.ok(
find(".groups-notifications-form .tag-chooser").length === 4,
"it should display tag inputs"
);
});
QUnit.test("As a group owner", async assert => {
updateCurrentUser({ moderator: false, admin: false });
await visit("/g/discourse/manage/tags");
assert.ok(
find(".groups-notifications-form .tag-chooser").length === 4,
"it should display tag inputs"
);
});