FEATURE: improved tag and category watching and tracking

- present tags watched on the user prefs page
- automatically watch or unwatch old topics based on watch status

New watching and tracking logic takes care of handling old topics
(either with or without read state)

When you watch a topic you now watch historically

Also removes confusing warnings from user.
This commit is contained in:
Sam 2016-07-08 12:58:18 +10:00
parent 58c2389a7b
commit 4161ee210a
19 changed files with 583 additions and 224 deletions

View file

@ -36,8 +36,13 @@ export default Ember.TextField.extend({
const site = this.site,
self = this,
filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
var limit = this.siteSettings.max_tags_per_topic;
if (this.get('allowCreate') !== false) {
this.set('allowCreate', site.get('can_create_tag'));
}
if (this.get('unlimitedTagCount')) {
limit = null;
} else if (this.get('limit')) {
@ -46,7 +51,7 @@ export default Ember.TextField.extend({
this.$().select2({
tags: true,
placeholder: I18n.t(this.get('placeholderKey') || 'tagging.choose_for_topic'),
placeholder: this.get('placeholder') === "" ? "" : I18n.t(this.get('placeholderKey') || 'tagging.choose_for_topic'),
maximumInputLength: this.siteSettings.max_tag_length,
maximumSelectionSize: limit,
initSelection(element, callback) {
@ -73,7 +78,7 @@ export default Ember.TextField.extend({
term = term.replace(filterRegexp, '').trim();
// No empty terms, make sure the user has permission to create the tag
if (!term.length || !site.get('can_create_tag')) { return; }
if (!term.lenght || !this.get('allowCreate')) return;
if ($(data).filter(function() {
return this.text.localeCompare(term) === 0;

View file

@ -2,7 +2,6 @@ import { setting } from 'discourse/lib/computed';
import CanCheckEmails from 'discourse/mixins/can-check-emails';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from "ember-addons/ember-computed-decorators";
import { categoryBadgeHTML } from "discourse/helpers/category-link";
export default Ember.Controller.extend(CanCheckEmails, {
@ -136,24 +135,6 @@ export default Ember.Controller.extend(CanCheckEmails, {
const model = this.get('model');
// watched status changes warn user
const changedWatch = model.changedCategoryNotifications("watched");
if (changedWatch.remove.length > 0 && !this.get("warnedRemoveWatch")) {
var categories = Discourse.Category.findByIds(changedWatch.remove).map((cat) => {
return categoryBadgeHTML(cat);
}).join(" ");
bootbox.confirm(I18n.t('user.warn_unwatch.message', {categories: categories}),
I18n.t('user.warn_unwatch.no_value', {count: changedWatch.remove.length}), I18n.t('user.warn_unwatch.yes_value'),
(yes)=>{
this.set('unwatchCategoryTopics', yes ? changedWatch.remove : false);
this.send('save');
});
this.set("warnedRemoveWatch", true);
return;
}
const userFields = this.get('userFields');
// Update the user fields
@ -169,9 +150,6 @@ export default Ember.Controller.extend(CanCheckEmails, {
// Cook the bio for preview
model.set('name', this.get('newNameInput'));
var options = {};
if (this.get('warnedRemoveWatch') && this.get('unwatchCategoryTopics')) {
options["unwatchCategoryTopics"] = this.get("unwatchCategoryTopics");
}
return model.save(options).then(() => {
if (Discourse.User.currentProp('id') === model.get('id')) {
@ -179,8 +157,6 @@ export default Ember.Controller.extend(CanCheckEmails, {
}
model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw'))));
this.set('saved', true);
this.set("unwatchTopics", false);
this.set('warnedRemoveWatch', false);
}).catch(popupAjaxError);
},

View file

@ -141,7 +141,7 @@ const User = RestModel.extend({
return Discourse.User.create(this.getProperties(Object.keys(this)));
},
save(options) {
save() {
const data = this.getProperties(
'bio_raw',
'website',
@ -152,7 +152,10 @@ const User = RestModel.extend({
'user_fields',
'muted_usernames',
'profile_background',
'card_background'
'card_background',
'muted_tags',
'tracked_tags',
'watched_tags'
);
[ 'email_always',
@ -192,10 +195,6 @@ const User = RestModel.extend({
data['edit_history_public'] = this.get('user_option.edit_history_public');
}
if (options && options.unwatchCategoryTopics) {
data.unwatch_category_topics = options.unwatchCategoryTopics;
}
// TODO: We can remove this when migrated fully to rest model.
this.set('isSaving', true);
return Discourse.ajax(`/users/${this.get('username_lower')}`, {

View file

@ -274,6 +274,27 @@
</div>
</div>
{{#if siteSettings.tagging_enabled}}
<div class="control-group tags">
<label class="control-label">{{i18n 'user.tag_settings'}}</label>
<div class="controls tag-controls">
<label><span class="icon fa fa-exclamation-circle watching"></span> {{i18n 'user.watched_tags'}}</label>
{{tag-chooser tags=model.watched_tags blacklist=selectedTags allowCreate=false placeholder=""}}
</div>
<div class="instructions">{{i18n 'user.watched_tags_instructions'}}</div>
<div class="controls tag-controls">
<label><span class="icon fa fa-circle tracking"></span> {{i18n 'user.tracked_tags'}}</label>
{{tag-chooser tags=model.tracked_tags blacklist=selectedTags allowCreate=false placeholder=""}}
</div>
<div class="instructions">{{i18n 'user.tracked_tags_instructions'}}</div>
<div class="controls tag-controls">
<label><span class="icon fa fa-times-circle muted"></span> {{i18n 'user.muted_tags'}}</label>
{{tag-chooser tags=model.muted_tags blacklist=selectedTags allowCreate=false placeholder=""}}
</div>
<div class="instructions">{{i18n 'user.muted_tags_instructions'}}</div>
</div>
{{/if}}
<div class="control-group muting">
<label class="control-label">{{i18n 'user.users'}}</label>
<div class="controls category-controls">

View file

@ -279,3 +279,12 @@ and (max-width : 600px) {
width: 100%;
}
}
.user-preferences .tags .select2-container-multi {
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
width: 540px;
border-radius: 0;
.select2-choices {
border: none;
}
}

View file

@ -89,10 +89,6 @@ class UsersController < ApplicationController
user = fetch_user_from_params
guardian.ensure_can_edit!(user)
if params[:unwatch_category_topics]
TopicUser.unwatch_categories!(user, params[:unwatch_category_topics])
end
if params[:user_fields].present?
params[:custom_fields] = {} unless params[:custom_fields].present?

View file

@ -20,85 +20,141 @@ class CategoryUser < ActiveRecord::Base
[notification_levels[:watching], notification_levels[:watching_first_post]]
end
%w{watch track}.each do |s|
define_singleton_method("auto_#{s}_new_topic") do |topic, new_category=nil|
category_id = topic.category_id
if new_category && topic.created_at > 5.days.ago
# we want to apply default of the new category
category_id = new_category.id
# remove defaults from previous category
remove_default_from_topic(topic.id, category_id, TopicUser.notification_levels[:"#{s}ing"], TopicUser.notification_reasons[:"auto_#{s}_category"])
end
apply_default_to_topic(topic.id, category_id, TopicUser.notification_levels[:"#{s}ing"], TopicUser.notification_reasons[:"auto_#{s}_category"])
end
end
def self.batch_set(user, level, category_ids)
records = CategoryUser.where(user: user, notification_level: notification_levels[level])
old_ids = records.pluck(:category_id)
changed = false
category_ids = Category.where('id in (?)', category_ids).pluck(:id)
remove = (old_ids - category_ids)
if remove.present?
records.where('category_id in (?)', remove).destroy_all
changed = true
end
(category_ids - old_ids).each do |id|
CategoryUser.create!(user: user, category_id: id, notification_level: notification_levels[level])
changed = true
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
CategoryUser.create!(user: user, category_id: category_id, notification_level: level)
end
auto_watch(user_id: user.id)
auto_track(user_id: user.id)
end
def self.apply_default_to_topic(topic_id, category_id, level, reason)
# Can not afford to slow down creation of topics when a pile of users are watching new topics, reverting to SQL for max perf here
sql = <<-SQL
INSERT INTO topic_users(user_id, topic_id, notification_level, notifications_reason_id)
SELECT user_id, :topic_id, :level, :reason
FROM category_users
WHERE notification_level = :level
AND category_id = :category_id
AND NOT EXISTS(SELECT 1 FROM topic_users WHERE topic_id = :topic_id AND user_id = category_users.user_id)
SQL
def self.auto_track(opts={})
exec_sql(sql,
topic_id: topic_id,
category_id: category_id,
level: level,
reason: reason
)
builder = SqlBuilder.new <<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.remove_default_from_topic(topic_id, category_id, level, reason)
sql = <<-SQL
DELETE FROM topic_users
WHERE topic_id = :topic_id
AND notifications_changed_at IS NULL
AND notification_level = :level
AND notifications_reason_id = :reason
AND NOT EXISTS(SELECT 1 FROM category_users WHERE category_users.category_id = :category_id AND category_users.notification_level = :level AND category_users.user_id = topic_users.user_id)
SQL
def self.auto_watch(opts={})
builder = SqlBuilder.new <<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])
exec_sql(sql,
topic_id: topic_id,
category_id: category_id,
level: level,
reason: reason
)
end
def self.ensure_consistency!
exec_sql <<SQL
DELETE FROM category_users
@ -110,7 +166,6 @@ class CategoryUser < ActiveRecord::Base
SQL
end
private_class_method :apply_default_to_topic, :remove_default_from_topic
end
# == Schema Information

View file

@ -8,6 +8,38 @@ class TagUser < ActiveRecord::Base
NotificationLevels.all
end
def self.lookup(user, level)
where(user: user, notification_level: notification_levels[level])
end
def self.batch_set(user, level, tags)
tags ||= []
changed = false
records = TagUser.where(user: user, notification_level: notification_levels[level])
old_ids = records.pluck(:tag_id)
tag_ids = tags.empty? ? [] : Tag.where('name in (?)', tags).pluck(:id)
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
end
if changed
auto_watch(user_id: user.id)
auto_track(user_id: user.id)
end
changed
end
def self.change(user_id, tag_id, level)
tag_id = tag_id.id if tag_id.is_a?(::Tag)
user_id = user_id.id if user_id.is_a?(::User)
@ -25,75 +57,100 @@ class TagUser < ActiveRecord::Base
tag_user = TagUser.create(user_id: user_id, tag_id: tag_id, notification_level: level)
end
auto_watch(user_id: user_id)
auto_track(user_id: user_id)
tag_user
rescue ActiveRecord::RecordNotUnique
# In case of a race condition to insert, do nothing
end
%w{watch track}.each do |s|
define_singleton_method("auto_#{s}_new_topic") do |topic, new_tags=nil|
tag_ids = topic.tags.pluck(:id)
if !new_tags.nil? && topic.created_at && topic.created_at > 5.days.ago
tag_ids = new_tags.map(&:id)
remove_default_from_topic( topic.id, tag_ids,
TopicUser.notification_levels[:"#{s}ing"],
TopicUser.notification_reasons[:"auto_#{s}_tag"] )
end
def self.auto_watch(opts)
builder = SqlBuilder.new <<SQL
apply_default_to_topic( topic.id, tag_ids,
TopicUser.notification_levels[:"#{s}ing"],
TopicUser.notification_reasons[:"auto_#{s}_tag"])
end
end
UPDATE topic_users
SET notification_level = CASE WHEN should_watch THEN :watching ELSE :tracking END,
notifications_reason_id = CASE WHEN should_watch THEN :auto_watch_tag ELSE NULL END
FROM
(
SELECT tu.topic_id, tu.user_id, CASE
WHEN MAX(tag_users.notification_level) = :watching THEN true
ELSE false
END
should_watch,
def self.apply_default_to_topic(topic_id, tag_ids, level, reason)
sql = <<-SQL
INSERT INTO topic_users(user_id, topic_id, notification_level, notifications_reason_id)
SELECT user_id, :topic_id, :level, :reason
FROM tag_users
WHERE notification_level = :level
AND tag_id in (:tag_ids)
AND NOT EXISTS(SELECT 1 FROM topic_users WHERE topic_id = :topic_id AND user_id = tag_users.user_id)
LIMIT 1
SQL
CASE WHEN MAX(tag_users.notification_level) IS NULL AND
tu.notification_level = :watching AND
tu.notifications_reason_id = :auto_watch_tag
THEN true
ELSE false
END
should_track
exec_sql(sql,
topic_id: topic_id,
tag_ids: tag_ids,
level: level,
reason: reason
)
end
FROM topic_users tu
LEFT JOIN topic_tags ON tu.topic_id = topic_tags.topic_id
LEFT JOIN tag_users ON tag_users.user_id = tu.user_id
AND topic_tags.tag_id = tag_users.tag_id
AND tag_users.notification_level = :watching
/*where*/
GROUP BY tu.topic_id, tu.user_id, tu.notification_level, tu.notifications_reason_id
) AS X
WHERE X.topic_id = topic_users.topic_id AND
X.user_id = topic_users.user_id AND
(should_track OR should_watch)
def self.remove_default_from_topic(topic_id, tag_ids, level, reason)
sql = <<-SQL
DELETE FROM topic_users
WHERE topic_id = :topic_id
AND notifications_changed_at IS NULL
AND notification_level = :level
AND notifications_reason_id = :reason
SQL
SQL
if !tag_ids.empty?
sql << <<-SQL
AND NOT EXISTS(
SELECT 1
FROM tag_users
WHERE tag_users.tag_id in (:tag_ids)
AND tag_users.notification_level = :level
AND tag_users.user_id = topic_users.user_id)
SQL
builder.where("tu.notification_level in (:tracking, :regular, :watching)")
if topic_id = opts[:topic_id]
builder.where("tu.topic_id = :topic_id", topic_id: topic_id)
end
exec_sql(sql,
topic_id: topic_id,
level: level,
reason: reason,
tag_ids: tag_ids
)
if user_id = opts[:user_id]
builder.where("tu.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_tag: TopicUser.notification_reasons[:auto_watch_tag])
end
def self.auto_track(opts)
builder = SqlBuilder.new <<SQL
UPDATE topic_users
SET notification_level = :tracking, notifications_reason_id = :auto_track_tag
FROM (
SELECT DISTINCT tu.topic_id, tu.user_id
FROM topic_users tu
JOIN topic_tags ON tu.topic_id = topic_tags.topic_id
JOIN tag_users ON tag_users.user_id = tu.user_id
AND topic_tags.tag_id = tag_users.tag_id
AND tag_users.notification_level = :tracking
/*where*/
) as X
WHERE
topic_users.notification_level = :regular AND
topic_users.topic_id = X.topic_id AND
topic_users.user_id = X.user_id
SQL
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_tag: TopicUser.notification_reasons[:auto_track_tag])
end
private_class_method :apply_default_to_topic, :remove_default_from_topic
end
# == Schema Information

View file

@ -25,7 +25,7 @@ class Topic < ActiveRecord::Base
def_delegator :notifier, :mute!, :notify_muted!
def_delegator :notifier, :toggle_mute, :toggle_mute
attr_accessor :allowed_user_ids
attr_accessor :allowed_user_ids, :tags_changed
def self.max_sort_order
@max_sort_order ||= (2 ** 31) - 1
@ -187,6 +187,12 @@ class Topic < ActiveRecord::Base
if archetype_was == banner || archetype == banner
ApplicationController.banner_json_cache.clear
end
if tags_changed
TagUser.auto_watch(topic_id: id)
TagUser.auto_track(topic_id: id)
self.tags_changed = false
end
end
def initialize_default_values
@ -518,13 +524,16 @@ class Topic < ActiveRecord::Base
self.category_id = new_category.id
self.update_column(:category_id, new_category.id)
Category.where(id: old_category.id).update_all("topic_count = topic_count - 1") if old_category
# when a topic changes category we may have to start watching it
# if we happen to have read state for it
CategoryUser.auto_watch(category_id: new_category.id, topic_id: self.id)
CategoryUser.auto_track(category_id: new_category.id, topic_id: self.id)
end
Category.where(id: new_category.id).update_all("topic_count = topic_count + 1")
CategoryFeaturedTopic.feature_topics_for(old_category) unless @import_mode
CategoryFeaturedTopic.feature_topics_for(new_category) unless @import_mode || old_category.id == new_category.id
CategoryUser.auto_watch_new_topic(self, new_category)
CategoryUser.auto_track_new_topic(self, new_category)
end
true

View file

@ -131,15 +131,7 @@ SQL
rows = TopicUser.where(topic_id: topic_id, user_id: user_id).update_all([attrs_sql, *vals])
if rows == 0
now = DateTime.now
auto_track_after = UserOption.where(user_id: user_id).pluck(:auto_track_topics_after_msecs).first
auto_track_after ||= SiteSetting.default_other_auto_track_topics_after_msecs
if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed].to_i || 0)
attrs[:notification_level] ||= notification_levels[:tracking]
end
TopicUser.create(attrs.merge!(user_id: user_id, topic_id: topic_id, first_visited_at: now ,last_visited_at: now))
create_missing_record(user_id, topic_id, attrs)
else
observe_after_save_callbacks_for topic_id, user_id
end
@ -153,12 +145,64 @@ SQL
# In case of a race condition to insert, do nothing
end
def create_missing_record(user_id, topic_id, attrs)
now = DateTime.now
unless attrs[:notification_level]
category_notification_level = CategoryUser.where(user_id: user_id)
.where("category_id IN (SELECT category_id FROM topics WHERE id = :id)", id: topic_id)
.where("notification_level IN (:levels)", levels: [CategoryUser.notification_levels[:watching],
CategoryUser.notification_levels[:tracking]])
.order("notification_level DESC")
.limit(1)
.pluck(:notification_level)
.first
tag_notification_level = TagUser.where(user_id: user_id)
.where("tag_id IN (SELECT tag_id FROM topic_tags WHERE topic_id = :id)", id: topic_id)
.where("notification_level IN (:levels)", levels: [CategoryUser.notification_levels[:watching],
CategoryUser.notification_levels[:tracking]])
.order("notification_level DESC")
.limit(1)
.pluck(:notification_level)
.first
if category_notification_level && !(tag_notification_level && (tag_notification_level > category_notification_level))
attrs[:notification_level] = category_notification_level
attrs[:notifications_changed_at] = DateTime.now
attrs[:notifications_reason_id] = category_notification_level == CategoryUser.notification_levels[:watching] ?
TopicUser.notification_reasons[:auto_watch_category] :
TopicUser.notification_reasons[:auto_track_category]
elsif tag_notification_level
attrs[:notification_level] = tag_notification_level
attrs[:notifications_changed_at] = DateTime.now
attrs[:notifications_reason_id] = tag_notification_level == TagUser.notification_levels[:watching] ?
TopicUser.notification_reasons[:auto_watch_tag] :
TopicUser.notification_reasons[:auto_track_tag]
end
end
unless attrs[:notification_level]
auto_track_after = UserOption.where(user_id: user_id).pluck(:auto_track_topics_after_msecs).first
auto_track_after ||= SiteSetting.default_other_auto_track_topics_after_msecs
if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed].to_i || 0)
attrs[:notification_level] ||= notification_levels[:tracking]
end
end
TopicUser.create(attrs.merge!(user_id: user_id, topic_id: topic_id, first_visited_at: now ,last_visited_at: now))
end
def track_visit!(topic_id, user_id)
now = DateTime.now
rows = TopicUser.where(topic_id: topic_id, user_id: user_id).update_all(last_visited_at: now)
if rows == 0
TopicUser.create(topic_id: topic_id, user_id: user_id, last_visited_at: now, first_visited_at: now)
change(user_id, topic_id, last_visited_at: now, first_visited_at: now)
else
observe_after_save_callbacks_for(topic_id, user_id)
end

View file

@ -83,6 +83,9 @@ class UserSerializer < BasicUserSerializer
private_attributes :locale,
:muted_category_ids,
:watched_tags,
:tracked_tags,
:muted_tags,
:tracked_category_ids,
:watched_category_ids,
:private_messages_stats,
@ -246,6 +249,17 @@ class UserSerializer < BasicUserSerializer
###
### PRIVATE ATTRIBUTES
###
def muted_tags
TagUser.lookup(object, :muted).joins(:tag).pluck('tags.name')
end
def tracked_tags
TagUser.lookup(object, :tracking).joins(:tag).pluck('tags.name')
end
def watched_tags
TagUser.lookup(object, :watching).joins(:tag).pluck('tags.name')
end
def muted_category_ids
CategoryUser.lookup(object, :muted).pluck(:category_id)

View file

@ -452,14 +452,47 @@ class PostAlerter
end
def notify_post_users(post, notified)
notify = TopicUser.where(topic_id: post.topic_id)
.where(notification_level: TopicUser.notification_levels[:watching])
return unless post.topic
condition = <<SQL
id IN (
SELECT user_id FROM topic_users
WHERE notification_level = :watching AND topic_id = :topic_id
UNION ALL
SELECT cu.user_id FROM category_users cu
LEFT JOIN topic_users tu ON tu.user_id = cu.user_id AND tu.topic_id = :topic_id
WHERE cu.notification_level = :watching AND cu.category_id = :category_id AND tu.user_id IS NULL
/*tags*/
)
SQL
tag_ids = post.topic.topic_tags.pluck('topic_tags.tag_id')
if tag_ids.present?
condition.sub! "/*tags*/", <<SQL
UNION ALL
SELECT tag_users.user_id FROM tag_users
LEFT JOIN topic_users tu ON tu.user_id = tag_users.user_id AND tu.topic_id = :topic_id
WHERE tag_users.notification_level = :watching AND tag_users.tag_id IN (:tag_ids) AND tu.user_id IS NULL
SQL
end
notify = User.where(condition,
watching: TopicUser.notification_levels[:watching],
topic_id: post.topic_id,
category_id: post.topic.category_id,
tag_ids: tag_ids
)
exclude_user_ids = notified.map(&:id)
notify = notify.where("user_id NOT IN (?)", exclude_user_ids) if exclude_user_ids.present?
notify = notify.where("id NOT IN (?)", exclude_user_ids) if exclude_user_ids.present?
notify.includes(:user).each do |tu|
create_notification(tu.user, Notification.types[:posted], post)
notify.each do |user|
create_notification(user, Notification.types[:posted], post)
end
end

View file

@ -6,6 +6,12 @@ class UserUpdater
muted_category_ids: :muted
}
TAG_NAMES = {
watched_tags: :watching,
tracked_tags: :tracking,
muted_tags: :muted
}
OPTION_ATTR = [
:email_always,
:mailing_list_mode,
@ -55,6 +61,10 @@ class UserUpdater
end
end
TAG_NAMES.each do |attribute, level|
TagUser.batch_set(user, level, attributes[attribute])
end
save_options = false

View file

@ -539,8 +539,15 @@ en:
individual: "Send an email for every new post"
many_per_day: "Send me an email for every new post (about {{dailyEmailEstimate}} per day)"
few_per_day: "Send me an email for every new post (about 2 per day)"
tag_settings: "Tags"
watched_tags: "Watched"
watched_tags_instructions: "You will automatically watch all topics with these tags. You will be notified of all new posts and topics, and a count of new posts will also appear next to the topic."
tracked_tags: "Tracked"
tracked_tags_instructions: "You will automatically track all new topics with these tags. A count of new posts will appear next to the topic."
muted_tags: "Muted"
muted_tags_instructions: "You will not be notified of anything about new topics with these tags, and they will not appear in latest."
watched_categories: "Watched"
watched_categories_instructions: "You will automatically watch all new topics in these categories. You will be notified of all new posts and topics, and a count of new posts will also appear next to the topic."
watched_categories_instructions: "You will automatically watch all topics in these categories. You will be notified of all new posts and topics, and a count of new posts will also appear next to the topic."
tracked_categories: "Tracked"
tracked_categories_instructions: "You will automatically track all new topics in these categories. A count of new posts will appear next to the topic."
muted_categories: "Muted"
@ -577,13 +584,6 @@ en:
failed_to_move: "Failed to move selected messages (perhaps your network is down)"
select_all: "Select All"
warn_unwatch:
message: "Also stop watching previously watched topics in {{categories}}?"
yes_value: "Yes, stop watching topics"
no_value:
one: "No, only stop watching category"
other: "No, only stop watching categories"
change_password:
success: "(email sent)"
in_progress: "(sending email)"
@ -1349,6 +1349,7 @@ en:
title: change how often you get notified about this topic
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 this a tag on this topic.'
"3_6": 'You will receive notifications because you are watching this category.'
"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.'

View file

@ -3,10 +3,6 @@ module DiscourseTagging
TAGS_FIELD_NAME = "tags"
TAGS_FILTER_REGEXP = /[<\\\/\>\#\?\&\s]/
# class Engine < ::Rails::Engine
# engine_name "discourse_tagging"
# isolate_namespace DiscourseTagging
# end
def self.tag_topic_by_names(topic, guardian, tag_names_arg)
if SiteSetting.tagging_enabled
@ -43,13 +39,11 @@ module DiscourseTagging
end
end
auto_notify_for(tags, topic)
topic.tags = tags
else
auto_notify_for([], topic)
topic.tags = []
end
topic.tags_changed=true
end
true
end
@ -146,11 +140,6 @@ module DiscourseTagging
query
end
def self.auto_notify_for(tags, topic)
TagUser.auto_watch_new_topic(topic, tags)
TagUser.auto_track_new_topic(topic, tags)
end
def self.clean_tag(tag)
tag.downcase.strip[0...SiteSetting.max_tag_length].gsub(TAGS_FILTER_REGEXP, '')
end

View file

@ -90,14 +90,6 @@ class TopicCreator
topic.notifier.send(action, gu.user_id)
end
end
unless topic.private_message?
# In order of importance:
CategoryUser.auto_watch_new_topic(topic)
CategoryUser.auto_track_new_topic(topic)
TagUser.auto_watch_new_topic(topic)
TagUser.auto_track_new_topic(topic)
end
end
def setup_topic_params

View file

@ -5,6 +5,14 @@ require_dependency 'post_creator'
describe CategoryUser do
def tracking
CategoryUser.notification_levels[:tracking]
end
def regular
CategoryUser.notification_levels[:regular]
end
it 'allows batch set' do
user = Fabricate(:user)
category1 = Fabricate(:category)
@ -22,6 +30,21 @@ describe CategoryUser do
expect(watching.count).to eq 1
end
it 'should correctly auto_track' do
tracking_user = Fabricate(:user)
user = Fabricate(:user)
topic = Fabricate(:post).topic
TopicUser.change(user.id, topic.id, total_msecs_viewed: 10)
TopicUser.change(tracking_user.id, topic.id, total_msecs_viewed: 10)
CategoryUser.create!(user: tracking_user, category: topic.category, notification_level: tracking)
CategoryUser.auto_track(user_id: tracking_user.id)
expect(TopicUser.get(topic, tracking_user).notification_level).to eq(tracking)
expect(TopicUser.get(topic, user).notification_level).to eq(regular)
end
context 'integration' do
before do
@ -35,6 +58,8 @@ describe CategoryUser do
user = Fabricate(:user)
early_watched_post = create_post(category: watched_category)
CategoryUser.create!(user: user, category: watched_category, notification_level: CategoryUser.notification_levels[:watching])
CategoryUser.create!(user: user, category: muted_category, notification_level: CategoryUser.notification_levels[:muted])
CategoryUser.create!(user: user, category: tracked_category, notification_level: CategoryUser.notification_levels[:tracking])
@ -43,28 +68,36 @@ describe CategoryUser do
_muted_post = create_post(category: muted_category)
tracked_post = create_post(category: tracked_category)
create_post(topic_id: early_watched_post.topic_id)
expect(Notification.where(user_id: user.id, topic_id: watched_post.topic_id).count).to eq 1
expect(Notification.where(user_id: user.id, topic_id: early_watched_post.topic_id).count).to eq 1
expect(Notification.where(user_id: user.id, topic_id: tracked_post.topic_id).count).to eq 0
# we must create a record so tracked flicks over
TopicUser.change(user.id, tracked_post.topic_id, total_msecs_viewed: 10)
tu = TopicUser.get(tracked_post.topic, user)
expect(tu.notification_level).to eq TopicUser.notification_levels[:tracking]
expect(tu.notifications_reason_id).to eq TopicUser.notification_reasons[:auto_track_category]
end
it "watches categories that have been changed" do
it "topics that move to a tracked category should auto track" do
user = Fabricate(:user)
watched_category = Fabricate(:category)
CategoryUser.create!(user: user, category: watched_category, notification_level: CategoryUser.notification_levels[:watching])
post = create_post
expect(TopicUser.get(post.topic, user)).to be_blank
first_post = create_post
tracked_category = first_post.topic.category
# Now, change the topic's category
post.topic.change_category_to_id(watched_category.id)
tu = TopicUser.get(post.topic, user)
expect(tu.notification_level).to eq TopicUser.notification_levels[:watching]
TopicUser.change(user.id, first_post.topic_id, total_msecs_viewed: 10)
tu = TopicUser.get(first_post.topic, user)
expect(tu.notification_level).to eq TopicUser.notification_levels[:regular]
CategoryUser.set_notification_level_for_category(user, CategoryUser.notification_levels[:tracking], tracked_category.id)
tu = TopicUser.get(first_post.topic, user)
expect(tu.notification_level).to eq TopicUser.notification_levels[:tracking]
end
it "unwatches categories that have been changed" do
user = Fabricate(:user)
watched_category = Fabricate(:category)
@ -72,12 +105,15 @@ describe CategoryUser do
post = create_post(category: watched_category)
tu = TopicUser.get(post.topic, user)
# we start watching cause a notification is sent to the watching user
# this position sent is tracking in topic users
expect(tu.notification_level).to eq TopicUser.notification_levels[:watching]
# Now, change the topic's category
unwatched_category = Fabricate(:category)
post.topic.change_category_to_id(unwatched_category.id)
expect(TopicUser.get(post.topic, user)).to be_blank
expect(TopicUser.get(post.topic, user).notification_level).to eq TopicUser.notification_levels[:tracking]
end
it "does not delete TopicUser record when topic category is changed, and new category has same notification level" do
@ -87,16 +123,26 @@ describe CategoryUser do
user = Fabricate(:user)
watched_category_1 = Fabricate(:category)
watched_category_2 = Fabricate(:category)
category_3 = Fabricate(:category)
post = create_post(category: watched_category_1)
CategoryUser.create!(user: user, category: watched_category_1, notification_level: CategoryUser.notification_levels[:watching])
CategoryUser.create!(user: user, category: watched_category_2, notification_level: CategoryUser.notification_levels[:watching])
post = create_post(category: watched_category_1)
tu = TopicUser.get(post.topic, user)
expect(tu.notification_level).to eq TopicUser.notification_levels[:watching]
# we must have a topic user record otherwise it will be watched implicitly
TopicUser.change(user.id, post.topic_id, total_msecs_viewed: 10)
expect(TopicUser.get(post.topic, user).notification_level).to eq TopicUser.notification_levels[:watching]
post.topic.change_category_to_id(category_3.id)
expect(TopicUser.get(post.topic, user).notification_level).to eq TopicUser.notification_levels[:tracking]
# Now, change the topic's category
post.topic.change_category_to_id(watched_category_2.id)
expect(TopicUser.get(post.topic, user)).to eq tu
expect(TopicUser.get(post.topic, user).notification_level).to eq TopicUser.notification_levels[:watching]
post.topic.change_category_to_id(watched_category_1.id)
expect(TopicUser.get(post.topic, user).notification_level).to eq TopicUser.notification_levels[:watching]
end
it "deletes TopicUser record when topic category is changed, and new category has different notification level" do

View file

@ -4,13 +4,72 @@ require 'rails_helper'
require_dependency 'post_creator'
describe TagUser do
before do
SiteSetting.tagging_enabled = true
SiteSetting.min_trust_to_create_tag = 0
SiteSetting.min_trust_level_to_tag_topics = 0
end
def regular
TagUser.notification_levels[:regular]
end
def tracking
TagUser.notification_levels[:tracking]
end
def watching
TagUser.notification_levels[:watching]
end
context "change" do
it "watches or tracks on change" do
user = Fabricate(:user)
tag = Fabricate(:tag)
post = create_post(tags: [tag.name])
topic = post.topic
TopicUser.change(user.id, topic.id, total_msecs_viewed: 1)
TagUser.change(user.id, tag.id, tracking)
expect(TopicUser.get(topic, user).notification_level).to eq tracking
TagUser.change(user.id, tag.id, watching)
expect(TopicUser.get(topic, user).notification_level).to eq watching
TagUser.change(user.id, tag.id, regular)
expect(TopicUser.get(topic, user).notification_level).to eq tracking
end
end
context "batch_set" do
it "watches and unwatches tags correctly" do
user = Fabricate(:user)
tag = Fabricate(:tag)
post = create_post(tags: [tag.name])
topic = post.topic
# we need topic user record to ensure watch picks up other wise it is implicit
TopicUser.change(user.id, topic.id, total_msecs_viewed: 1)
TagUser.batch_set(user, :tracking, [tag.name])
expect(TopicUser.get(topic, user).notification_level).to eq tracking
TagUser.batch_set(user, :watching, [tag.name])
expect(TopicUser.get(topic, user).notification_level).to eq watching
TagUser.batch_set(user, :watching, [])
expect(TopicUser.get(topic, user).notification_level).to eq tracking
end
end
context "integration" do
before do
ActiveRecord::Base.observers.enable :all
SiteSetting.tagging_enabled = true
SiteSetting.min_trust_to_create_tag = 0
SiteSetting.min_trust_level_to_tag_topics = 0
end
let(:user) { Fabricate(:user) }
@ -20,28 +79,43 @@ describe TagUser do
let(:tracked_tag) { Fabricate(:tag) }
context "with some tag notification settings" do
before do
let :watched_post do
TagUser.create!(user: user, tag: watched_tag, notification_level: TagUser.notification_levels[:watching])
create_post(tags: [watched_tag.name])
end
let :muted_post do
TagUser.create!(user: user, tag: muted_tag, notification_level: TagUser.notification_levels[:muted])
create_post(tags: [muted_tag.name])
end
let :tracked_post do
TagUser.create!(user: user, tag: tracked_tag, notification_level: TagUser.notification_levels[:tracking])
create_post(tags: [tracked_tag.name])
end
it "sets notification levels correctly" do
watched_post = create_post(tags: [watched_tag.name])
muted_post = create_post(tags: [muted_tag.name])
tracked_post = create_post(tags: [tracked_tag.name])
expect(Notification.where(user_id: user.id, topic_id: watched_post.topic_id).count).to eq 1
expect(Notification.where(user_id: user.id, topic_id: tracked_post.topic_id).count).to eq 0
TopicUser.change(user.id, tracked_post.topic.id, total_msecs_viewed: 1)
tu = TopicUser.get(tracked_post.topic, user)
expect(tu.notification_level).to eq TopicUser.notification_levels[:tracking]
expect(tu.notifications_reason_id).to eq TopicUser.notification_reasons[:auto_track_tag]
end
it "sets notification level to the highest one if there are multiple tags" do
TagUser.create!(user: user, tag: tracked_tag, notification_level: TagUser.notification_levels[:tracking])
TagUser.create!(user: user, tag: muted_tag, notification_level: TagUser.notification_levels[:muted])
TagUser.create!(user: user, tag: watched_tag, notification_level: TagUser.notification_levels[:watching])
post = create_post(tags: [muted_tag.name, tracked_tag.name, watched_tag.name])
expect(Notification.where(user_id: user.id, topic_id: post.topic_id).count).to eq 1
TopicUser.change(user.id, post.topic.id, total_msecs_viewed: 1)
tu = TopicUser.get(post.topic, user)
expect(tu.notification_level).to eq TopicUser.notification_levels[:watching]
expect(tu.notifications_reason_id).to eq TopicUser.notification_reasons[:auto_watch_tag]
@ -49,36 +123,43 @@ describe TagUser do
it "can start watching after tag has been added" do
post = create_post
expect(TopicUser.get(post.topic, user)).to be_blank
DiscourseTagging.tag_topic_by_names(post.topic, Guardian.new(user), [watched_tag.name])
tu = TopicUser.get(post.topic, user)
expect(tu.notification_level).to eq(TopicUser.notification_levels[:watching])
end
it "can start watching after tag has changed" do
post = create_post(tags: [Fabricate(:tag).name])
expect(TopicUser.get(post.topic, user)).to be_blank
# this is assuming post was already visited in the past, but now cause tag
# was added we should start watching it
TopicUser.change(user.id, post.topic.id, total_msecs_viewed: 1)
TagUser.create!(user: user, tag: watched_tag, notification_level: TagUser.notification_levels[:watching])
DiscourseTagging.tag_topic_by_names(post.topic, Guardian.new(user), [watched_tag.name])
post.topic.save!
tu = TopicUser.get(post.topic, user)
expect(tu.notification_level).to eq(TopicUser.notification_levels[:watching])
end
it "can stop watching after tag has changed" do
post = create_post(tags: [watched_tag.name])
expect(TopicUser.get(post.topic, user)).to be_present
DiscourseTagging.tag_topic_by_names(post.topic, Guardian.new(user), [Fabricate(:tag).name])
expect(TopicUser.get(post.topic, user)).to be_blank
end
watched_tag2 = Fabricate(:tag)
TagUser.create!(user: user, tag: watched_tag, notification_level: TagUser.notification_levels[:watching])
TagUser.create!(user: user, tag: watched_tag2, notification_level: TagUser.notification_levels[:watching])
post = create_post(tags: [watched_tag.name, watched_tag2.name])
TopicUser.change(user.id, post.topic_id, total_msecs_viewed: 1)
expect(TopicUser.get(post.topic, user).notification_level).to eq TopicUser.notification_levels[:watching]
DiscourseTagging.tag_topic_by_names(post.topic, Guardian.new(user), [watched_tag.name])
post.topic.save!
expect(TopicUser.get(post.topic, user).notification_level).to eq TopicUser.notification_levels[:watching]
it "can stop watching after tags have been removed" do
post = create_post(tags: [muted_tag.name, tracked_tag.name, watched_tag.name])
expect(TopicUser.get(post.topic, user)).to be_present
DiscourseTagging.tag_topic_by_names(post.topic, Guardian.new(user), [])
expect(TopicUser.get(post.topic, user)).to be_blank
post.topic.save!
expect(TopicUser.get(post.topic, user).notification_level).to eq TopicUser.notification_levels[:tracking]
end
it "is destroyed when a user is deleted" do
expect(TagUser.where(user_id: user.id).count).to eq(3)
TagUser.create!(user: user, tag: tracked_tag, notification_level: TagUser.notification_levels[:tracking])
user.destroy!
expect(TagUser.where(user_id: user.id).count).to eq(0)
end

View file

@ -39,6 +39,28 @@ describe UserUpdater do
expect(user.reload.name).to eq 'Jim Tom'
end
it 'can update categories and tags' do
category = Fabricate(:category)
tag = Fabricate(:tag)
user = Fabricate(:user)
updater = UserUpdater.new(acting_user, user)
updater.update(watched_tags: [tag.name], muted_category_ids: [category.id])
expect(TagUser.where(
user_id: user.id,
tag_id: tag.id,
notification_level: TagUser.notification_levels[:watching]
).count).to eq(1)
expect(CategoryUser.where(
user_id: user.id,
category_id: category.id,
notification_level: CategoryUser.notification_levels[:muted]
).count).to eq(1)
end
it 'updates various fields' do
user = Fabricate(:user)
updater = UserUpdater.new(acting_user, user)