FIX: Base topic details message on current category and tag tracking state (#12937)

The user may have changed their category or tag tracking settings since a topic was tracked/watched based on those settings in the past. In that case we need to alter the reason message we show them otherwise it is very confusing for the end user to be told they are tracking a topic because of a category, when they are no longer tracking that category.

For example: "You will see a count of new replies because you are tracking this category." becomes: "You will see a count of new replies because you were tracking this category in the past."

To do this, it was necessary to add tag and category tracking info to current user serializer. I improved the serializer code so it only does 3 SQL queries instead of 9 to get the tracking information for tags and categories for the current user.
This commit is contained in:
Martin Brennan 2021-05-06 09:14:07 +10:00 committed by GitHub
parent c792c2b5fe
commit 72648dd576
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 530 additions and 64 deletions

View File

@ -1,6 +1,4 @@
import EmberObject from "@ember/object";
import I18n from "I18n";
import { NotificationLevels } from "discourse/lib/notification-levels";
import RestModel from "discourse/models/rest";
import User from "discourse/models/user";
import { ajax } from "discourse/lib/ajax";
@ -9,8 +7,6 @@ import { ajax } from "discourse/lib/ajax";
A model representing a Topic's details that aren't always present, such as a list of participants.
When showing topics in lists and such this information should not be required.
**/
import discourseComputed from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url";
const TopicDetails = RestModel.extend({
loaded: false,
@ -35,34 +31,6 @@ const TopicDetails = RestModel.extend({
this.set("loaded", true);
},
@discourseComputed("notification_level", "notifications_reason_id")
notificationReasonText(level, reason) {
if (typeof level !== "number") {
level = 1;
}
let localeString = `topic.notifications.reasons.${level}`;
if (typeof reason === "number") {
const tmp = localeString + "_" + reason;
// some sane protection for missing translations of edge cases
if (I18n.lookup(tmp, { locale: "en" })) {
localeString = tmp;
}
}
if (
User.currentProp("mailing_list_mode") &&
level > NotificationLevels.MUTED
) {
return I18n.t("topic.notifications.reasons.mailing_list_mode");
} else {
return I18n.t(localeString, {
username: User.currentProp("username_lower"),
basePath: getURL(""),
});
}
},
updateNotifications(level) {
return ajax(`/t/${this.get("topic.id")}/notifications`, {
type: "POST",

View File

@ -3,19 +3,25 @@ import componentTest, {
} from "discourse/tests/helpers/component-test";
import I18n from "I18n";
import Topic from "discourse/models/topic";
import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
import {
discourseModule,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import selectKit from "discourse/tests/helpers/select-kit-helper";
const buildTopic = function (level, archetype = "regular") {
const buildTopic = function (opts) {
return Topic.create({
id: 4563,
}).updateFromJson({
title: "Qunit Test Topic",
details: {
notification_level: level,
notification_level: opts.level,
notifications_reason_id: opts.reason || null,
},
archetype,
archetype: opts.archetype || "regular",
category_id: opts.category_id || null,
tags: opts.tags || [],
});
};
@ -40,7 +46,7 @@ discourseModule(
`,
beforeEach() {
this.set("topic", buildTopic(1));
this.set("topic", buildTopic({ level: 1 }));
},
async test(assert) {
@ -50,7 +56,7 @@ discourseModule(
"it has the correct label"
);
await this.set("topic", buildTopic(2));
await this.set("topic", buildTopic({ level: 2 }));
assert.equal(
selectKit().header().label(),
@ -70,7 +76,10 @@ discourseModule(
beforeEach() {
I18n.translations.en.js.topic.notifications.tracking_pm.title = `${originalTranslation} PM`;
this.set("topic", buildTopic(2, "private_message"));
this.set(
"topic",
buildTopic({ level: 2, archetype: "private_message" })
);
},
test(assert) {
@ -81,5 +90,194 @@ discourseModule(
);
},
});
componentTest("notification reason text - user mailing list mode", {
template: hbs`
{{topic-notifications-button
notificationLevel=topic.details.notification_level
topic=topic
}}
`,
beforeEach() {
this.currentUser.set("mailing_list_mode", true);
this.set("topic", buildTopic({ level: 2 }));
},
test(assert) {
assert.equal(
queryAll(".topic-notifications-button .text").text(),
I18n.t("topic.notifications.reasons.mailing_list_mode"),
"mailing_list_mode enabled for the user shows unique text"
);
},
});
componentTest("notification reason text - bad notification reason", {
template: hbs`
{{topic-notifications-button
notificationLevel=topic.details.notification_level
topic=topic
}}
`,
beforeEach() {
this.set("topic", buildTopic({ level: 2 }));
},
test(assert) {
this.set("topic", buildTopic({ level: 3, reason: 999 }));
assert.equal(
queryAll(".topic-notifications-button .text").text(),
I18n.t("topic.notifications.reasons.3"),
"fallback to regular level translation if reason does not exist"
);
},
});
componentTest("notification reason text - user tracking category", {
template: hbs`
{{topic-notifications-button
notificationLevel=topic.details.notification_level
topic=topic
}}
`,
beforeEach() {
this.currentUser.set("tracked_category_ids", [88]);
this.set("topic", buildTopic({ level: 2, reason: 8, category_id: 88 }));
},
test(assert) {
assert.equal(
queryAll(".topic-notifications-button .text").text(),
I18n.t("topic.notifications.reasons.2_8"),
"use 2_8 notification if user is still tracking category"
);
},
});
componentTest(
"notification reason text - user no longer tracking category",
{
template: hbs`
{{topic-notifications-button
notificationLevel=topic.details.notification_level
topic=topic
}}
`,
beforeEach() {
this.currentUser.set("tracked_category_ids", []);
this.set(
"topic",
buildTopic({ level: 2, reason: 8, category_id: 88 })
);
},
test(assert) {
assert.equal(
queryAll(".topic-notifications-button .text").text(),
I18n.t("topic.notifications.reasons.2_8_stale"),
"use _stale notification if user is no longer tracking category"
);
},
}
);
componentTest("notification reason text - user watching category", {
template: hbs`
{{topic-notifications-button
notificationLevel=topic.details.notification_level
topic=topic
}}
`,
beforeEach() {
this.currentUser.set("watched_category_ids", [88]);
this.set("topic", buildTopic({ level: 3, reason: 6, category_id: 88 }));
},
test(assert) {
assert.equal(
queryAll(".topic-notifications-button .text").text(),
I18n.t("topic.notifications.reasons.3_6"),
"use 3_6 notification if user is still watching category"
);
},
});
componentTest(
"notification reason text - user no longer watching category",
{
template: hbs`
{{topic-notifications-button
notificationLevel=topic.details.notification_level
topic=topic
}}
`,
beforeEach() {
this.currentUser.set("watched_category_ids", []);
this.set(
"topic",
buildTopic({ level: 3, reason: 6, category_id: 88 })
);
},
test(assert) {
assert.equal(
queryAll(".topic-notifications-button .text").text(),
I18n.t("topic.notifications.reasons.3_6_stale"),
"use _stale notification if user is no longer watching category"
);
},
}
);
componentTest("notification reason text - user watching tag", {
template: hbs`
{{topic-notifications-button
notificationLevel=topic.details.notification_level
topic=topic
}}
`,
beforeEach() {
this.currentUser.set("watched_tags", ["test"]);
this.set("topic", buildTopic({ level: 3, reason: 10, tags: ["test"] }));
},
test(assert) {
assert.equal(
queryAll(".topic-notifications-button .text").text(),
I18n.t("topic.notifications.reasons.3_10"),
"use 3_10 notification if user is still watching tag"
);
},
});
componentTest("notification reason text - user no longer watching tag", {
template: hbs`
{{topic-notifications-button
notificationLevel=topic.details.notification_level
topic=topic
}}
`,
beforeEach() {
this.currentUser.set("watched_tags", []);
this.set("topic", buildTopic({ level: 3, reason: 10, tags: ["test"] }));
},
test(assert) {
assert.equal(
queryAll(".topic-notifications-button .text").text(),
I18n.t("topic.notifications.reasons.3_10_stale"),
"use _stale notification if user is no longer watching tag"
);
},
});
}
);

View File

@ -2,8 +2,8 @@ import { module, test } from "qunit";
import Topic from "discourse/models/topic";
import User from "discourse/models/user";
function buildDetails(id) {
const topic = Topic.create({ id: id });
function buildDetails(id, topicParams = {}) {
const topic = Topic.create(Object.assign({ id }, topicParams));
return topic.get("details");
}

View File

@ -1,4 +1,9 @@
import { action, computed } from "@ember/object";
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import { NotificationLevels } from "discourse/lib/notification-levels";
import discourseComputed from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url";
import Component from "@ember/component";
import layout from "select-kit/templates/components/topic-notifications-button";
@ -25,4 +30,85 @@ export default Component.extend({
.finally(() => this.set("isLoading", false));
}
},
@discourseComputed("topic", "topic.details")
notificationReasonText(topic, topicDetails) {
let level = topicDetails.notification_level;
let reason = topicDetails.notifications_reason_id;
if (typeof level !== "number") {
level = 1;
}
let localeString = `topic.notifications.reasons.${level}`;
if (typeof reason === "number") {
let localeStringWithReason = localeString + "_" + reason;
if (
this._notificationReasonStale(level, reason, topic, this.currentUser)
) {
localeStringWithReason += "_stale";
}
// some sane protection for missing translations of edge cases
if (I18n.lookup(localeStringWithReason, { locale: "en" })) {
localeString = localeStringWithReason;
}
}
if (
this.currentUser &&
this.currentUser.mailing_list_mode &&
level > NotificationLevels.MUTED
) {
return I18n.t("topic.notifications.reasons.mailing_list_mode");
} else {
return I18n.t(localeString, {
username: this.currentUser && this.currentUser.username_lower,
basePath: getURL(""),
});
}
},
// The user may have changed their category or tag tracking settings
// since this topic was tracked/watched based on those settings in the
// past. In that case we need to alter the reason message we show them
// otherwise it is very confusing for the end user to be told they are
// tracking a topic because of a category, when they are no longer tracking
// that category.
_notificationReasonStale(level, reason, topic, currentUser) {
if (!currentUser) {
return;
}
let categoryId = topic.category_id;
let tags = topic.tags;
let watchedCategoryIds = currentUser.watched_category_ids || [];
let trackedCategoryIds = currentUser.tracked_category_ids || [];
let watchedTags = currentUser.watched_tags || [];
// 2_8 tracking category
if (categoryId) {
if (level === 2 && reason === 8) {
if (!trackedCategoryIds.includes(categoryId)) {
return true;
}
// 3_6 watching category
} else if (level === 3 && reason === 6) {
if (!watchedCategoryIds.includes(categoryId)) {
return true;
}
}
} else if (!isEmpty(tags)) {
// 3_10 watching tag
if (level === 3 && reason === 10) {
if (!tags.some((tag) => watchedTags.includes(tag))) {
return true;
}
}
}
return false;
},
});

View File

@ -11,7 +11,7 @@
showCaret=showCaret
)
}}
<span class="text">{{html-safe topic.details.notificationReasonText}}</span>
<span class="text">{{html-safe notificationReasonText}}</span>
</p>
{{else}}
{{topic-notifications-options

View File

@ -202,15 +202,21 @@ class CategoryUser < ActiveRecord::Base
end
def self.notification_levels_for(guardian)
# Anonymous users have all default categories set to regular tracking,
# except for default muted categories which stay muted.
if guardian.anonymous?
notification_levels = [
SiteSetting.default_categories_watching.split("|"),
SiteSetting.default_categories_tracking.split("|"),
SiteSetting.default_categories_watching_first_post.split("|"),
SiteSetting.default_categories_regular.split("|")
].flatten.map { |id| [id.to_i, self.notification_levels[:regular]] }
].flatten.map do |id|
[id.to_i, self.notification_levels[:regular]]
end
notification_levels += SiteSetting.default_categories_muted.split("|").map { |id| [id.to_i, self.notification_levels[:muted]] }
notification_levels += SiteSetting.default_categories_muted.split("|").map do |id|
[id.to_i, self.notification_levels[:muted]]
end
else
notification_levels = CategoryUser.where(user: guardian.user).pluck(:category_id, :notification_level)
end

View File

@ -192,6 +192,28 @@ class TagUser < ActiveRecord::Base
auto_track_tag: TopicUser.notification_reasons[:auto_track_tag])
end
def self.notification_levels_for(guardian)
# Anonymous users have all default tags set to regular tracking,
# except for default muted tags which stay muted.
if guardian.anonymous?
notification_levels = [
SiteSetting.default_tags_watching_first_post.split("|"),
SiteSetting.default_tags_watching.split("|"),
SiteSetting.default_tags_tracking.split("|")
].flatten.map do |name|
[name, self.notification_levels[:regular]]
end
notification_levels += SiteSetting.default_tags_muted.split("|").map do |name|
[name, self.notification_levels[:muted]]
end
else
notification_levels = TagUser.where(user: guardian.user).joins(:tag).pluck("tags.name", :notification_level)
end
Hash[*notification_levels.flatten]
end
end
# == Schema Information

View File

@ -1546,17 +1546,27 @@ class User < ActiveRecord::Base
values = []
%w{watching watching_first_post tracking regular muted}.each do |s|
category_ids = SiteSetting.get("default_categories_#{s}").split("|").map(&:to_i)
# The following site settings are used to pre-populate default category
# tracking settings for a user:
#
# * default_categories_watching
# * default_categories_tracking
# * default_categories_watching_first_post
# * default_categories_regular
# * default_categories_muted
%w{watching watching_first_post tracking regular muted}.each do |setting|
category_ids = SiteSetting.get("default_categories_#{setting}").split("|").map(&:to_i)
category_ids.each do |category_id|
next if category_id == 0
values << "(#{self.id}, #{category_id}, #{CategoryUser.notification_levels[s.to_sym]})"
values << {
user_id: self.id,
category_id: category_id,
notification_level: CategoryUser.notification_levels[setting.to_sym]
}
end
end
if values.present?
DB.exec("INSERT INTO category_users (user_id, category_id, notification_level) VALUES #{values.join(",")}")
end
CategoryUser.insert_all!(values) if values.present?
end
def set_default_tags_preferences
@ -1564,12 +1574,25 @@ class User < ActiveRecord::Base
values = []
%w{watching watching_first_post tracking muted}.each do |s|
tag_names = SiteSetting.get("default_tags_#{s}").split("|")
# The following site settings are used to pre-populate default tag
# tracking settings for a user:
#
# * default_tags_watching
# * default_tags_tracking
# * default_tags_watching_first_post
# * default_tags_muted
%w{watching watching_first_post tracking muted}.each do |setting|
tag_names = SiteSetting.get("default_tags_#{setting}").split("|")
now = Time.zone.now
Tag.where(name: tag_names).pluck(:id).each do |tag_id|
values << { user_id: self.id, tag_id: tag_id, notification_level: TagUser.notification_levels[s.to_sym], created_at: now, updated_at: now }
values << {
user_id: self.id,
tag_id: tag_id,
notification_level: TagUser.notification_levels[setting.to_sym],
created_at: now,
updated_at: now
}
end
end

View File

@ -22,4 +22,24 @@ class BasicUserSerializer < ApplicationSerializer
def user
object[:user] || object.try(:user) || object
end
def categories_with_notification_level(lookup_level)
category_user_notification_levels.select do |id, level|
level == CategoryUser.notification_levels[lookup_level]
end.keys
end
def category_user_notification_levels
@category_user_notification_levels ||= CategoryUser.notification_levels_for(scope)
end
def tags_with_notification_level(lookup_level)
tag_user_notification_levels.select do |id, level|
level == TagUser.notification_levels[lookup_level]
end.keys
end
def tag_user_notification_levels
@tag_user_notification_levels ||= TagUser.notification_levels_for(scope)
end
end

View File

@ -28,7 +28,16 @@ class CurrentUserSerializer < BasicUserSerializer
:redirected_to_top,
:custom_fields,
:muted_category_ids,
:regular_category_ids,
:tracked_category_ids,
:watched_first_post_category_ids,
:watched_category_ids,
:muted_tag_ids,
:watched_tags,
:watching_first_post_tags,
:tracked_tags,
:muted_tags,
:regular_tags,
:dismissed_banner_key,
:is_anonymous,
:reviewable_count,
@ -53,7 +62,7 @@ class CurrentUserSerializer < BasicUserSerializer
:skip_new_user_tips,
:do_not_disturb_until,
:has_topic_draft,
:can_review
:can_review,
def groups
object.visible_groups.pluck(:id, :name).map { |id, name| { id: id, name: name } }
@ -181,13 +190,51 @@ class CurrentUserSerializer < BasicUserSerializer
end
def muted_category_ids
CategoryUser.lookup(object, :muted).pluck(:category_id)
categories_with_notification_level(:muted)
end
def regular_category_ids
categories_with_notification_level(:regular)
end
def tracked_category_ids
categories_with_notification_level(:tracking)
end
def watched_category_ids
categories_with_notification_level(:watching)
end
def watched_first_post_category_ids
categories_with_notification_level(:watching_first_post)
end
# this is a weird outlier that is used for topic tracking state which
# needs the actual ids, which is why it is duplicated with muted_tags
def muted_tag_ids
TagUser.lookup(object, :muted).pluck(:tag_id)
end
def muted_tags
tags_with_notification_level(:muted)
end
def tracked_tags
tags_with_notification_level(:tracked)
end
def watching_first_post_tags
tags_with_notification_level(:watching_first_post)
end
def watched_tags
tags_with_notification_level(:watching)
end
def regular_tags
tags_with_notification_level(:regular)
end
def ignored_users
IgnoredUser.where(user: object.id).joins(:ignored_user).pluck(:username)
end

View File

@ -207,39 +207,39 @@ class UserSerializer < UserCardSerializer
### PRIVATE ATTRIBUTES
###
def muted_tags
TagUser.lookup(object, :muted).joins(:tag).pluck('tags.name')
tags_with_notification_level(:muted)
end
def tracked_tags
TagUser.lookup(object, :tracking).joins(:tag).pluck('tags.name')
tags_with_notification_level(:tracked)
end
def watching_first_post_tags
TagUser.lookup(object, :watching_first_post).joins(:tag).pluck('tags.name')
tags_with_notification_level(:watching_first_post)
end
def watched_tags
TagUser.lookup(object, :watching).joins(:tag).pluck('tags.name')
tags_with_notification_level(:watching)
end
def muted_category_ids
CategoryUser.lookup(object, :muted).pluck(:category_id)
categories_with_notification_level(:muted)
end
def regular_category_ids
CategoryUser.lookup(object, :regular).pluck(:category_id)
categories_with_notification_level(:regular)
end
def tracked_category_ids
CategoryUser.lookup(object, :tracking).pluck(:category_id)
categories_with_notification_level(:tracking)
end
def watched_category_ids
CategoryUser.lookup(object, :watching).pluck(:category_id)
categories_with_notification_level(:watching)
end
def watched_first_post_category_ids
CategoryUser.lookup(object, :watching_first_post).pluck(:category_id)
categories_with_notification_level(:watching_first_post)
end
def muted_usernames

View File

@ -2595,12 +2595,15 @@ en:
reasons:
mailing_list_mode: "You have mailing list mode enabled, so you will be notified of replies to this topic via email."
"3_10": "You will receive notifications because you are watching a tag on this topic."
"3_10_stale": "You will receive notifications because you were watching a tag on this topic in the past."
"3_6": "You will receive notifications because you are watching this category."
"3_6_stale": "You will receive notifications because you were watching this category in the past."
"3_5": "You will receive notifications because you started watching this topic automatically."
"3_2": "You will receive notifications because you are watching this topic."
"3_1": "You will receive notifications because you created this topic."
"3": "You will receive notifications because you are watching this topic."
"2_8": "You will see a count of new replies because you are tracking this category."
"2_8_stale": "You will see a count of new replies because you were tracking this category in the past."
"2_4": "You will see a count of new replies because you posted a reply to this topic."
"2_2": "You will see a count of new replies because you are tracking this topic."
"2": 'You will see a count of new replies because you <a href="%{basePath}/u/%{username}/preferences/notifications">read this topic</a>.'

View File

@ -219,6 +219,54 @@ describe CategoryUser do
expect(CategoryUser.where(user_id: user.id).count).to eq(0)
end
end
describe "#notification_levels_for" do
let(:guardian) { Guardian.new(user) }
let!(:category1) { Fabricate(:category) }
let!(:category2) { Fabricate(:category) }
let!(:category3) { Fabricate(:category) }
let!(:category4) { Fabricate(:category) }
let!(:category5) { Fabricate(:category) }
context "for anon" do
let(:user) { nil }
before do
SiteSetting.default_categories_watching = category1.id.to_s
SiteSetting.default_categories_tracking = category2.id.to_s
SiteSetting.default_categories_watching_first_post = category3.id.to_s
SiteSetting.default_categories_regular = category4.id.to_s
SiteSetting.default_categories_muted = category5.id.to_s
end
it "every category from the default_categories_* site settings get overridden to regular, except for muted" do
levels = CategoryUser.notification_levels_for(guardian)
expect(levels[category1.id]).to eq(CategoryUser.notification_levels[:regular])
expect(levels[category2.id]).to eq(CategoryUser.notification_levels[:regular])
expect(levels[category3.id]).to eq(CategoryUser.notification_levels[:regular])
expect(levels[category4.id]).to eq(CategoryUser.notification_levels[:regular])
expect(levels[category5.id]).to eq(CategoryUser.notification_levels[:muted])
end
end
context "for a user" do
before do
CategoryUser.create(user: user, category: category1, notification_level: CategoryUser.notification_levels[:watching])
CategoryUser.create(user: user, category: category2, notification_level: CategoryUser.notification_levels[:tracking])
CategoryUser.create(user: user, category: category3, notification_level: CategoryUser.notification_levels[:watching_first_post])
CategoryUser.create(user: user, category: category4, notification_level: CategoryUser.notification_levels[:regular])
CategoryUser.create(user: user, category: category5, notification_level: CategoryUser.notification_levels[:muted])
end
it "gets the category_user notification levels for all categories the user is tracking and does not
include categories the user is not tracking at all" do
category6 = Fabricate(:category)
levels = CategoryUser.notification_levels_for(guardian)
expect(levels[category1.id]).to eq(CategoryUser.notification_levels[:watching])
expect(levels[category2.id]).to eq(CategoryUser.notification_levels[:tracking])
expect(levels[category3.id]).to eq(CategoryUser.notification_levels[:watching_first_post])
expect(levels[category4.id]).to eq(CategoryUser.notification_levels[:regular])
expect(levels[category5.id]).to eq(CategoryUser.notification_levels[:muted])
expect(levels.key?(category6.id)).to eq(false)
end
end
end
end

View File

@ -232,4 +232,49 @@ describe TagUser do
end
end
end
describe "#notification_levels_for" do
let(:guardian) { Guardian.new(user) }
let!(:tag1) { Fabricate(:tag) }
let!(:tag2) { Fabricate(:tag) }
let!(:tag3) { Fabricate(:tag) }
let!(:tag4) { Fabricate(:tag) }
context "for anon" do
let(:user) { nil }
before do
SiteSetting.default_tags_watching = tag1.name
SiteSetting.default_tags_tracking = tag2.name
SiteSetting.default_tags_watching_first_post = tag3.name
SiteSetting.default_tags_muted = tag4.name
end
it "every tag from the default_tags_* site settings get overridden to watching_first_post, except for muted" do
levels = TagUser.notification_levels_for(guardian)
expect(levels[tag1.name]).to eq(TagUser.notification_levels[:regular])
expect(levels[tag2.name]).to eq(TagUser.notification_levels[:regular])
expect(levels[tag3.name]).to eq(TagUser.notification_levels[:regular])
expect(levels[tag4.name]).to eq(TagUser.notification_levels[:muted])
end
end
context "for a user" do
let(:user) { Fabricate(:user) }
before do
TagUser.create(user: user, tag: tag1, notification_level: TagUser.notification_levels[:watching])
TagUser.create(user: user, tag: tag2, notification_level: TagUser.notification_levels[:tracking])
TagUser.create(user: user, tag: tag3, notification_level: TagUser.notification_levels[:watching_first_post])
TagUser.create(user: user, tag: tag4, notification_level: TagUser.notification_levels[:muted])
end
it "gets the tag_user notification levels for all tags the user is tracking and does not
include tags the user is not tracking at all" do
tag5 = Fabricate(:tag)
levels = TagUser.notification_levels_for(guardian)
expect(levels[tag1.name]).to eq(TagUser.notification_levels[:watching])
expect(levels[tag2.name]).to eq(TagUser.notification_levels[:tracking])
expect(levels[tag3.name]).to eq(TagUser.notification_levels[:watching_first_post])
expect(levels[tag4.name]).to eq(TagUser.notification_levels[:muted])
expect(levels.key?(tag5.name)).to eq(false)
end
end
end
end