From 986cc8a0fb407fb7747d20a2b67937f45dc37a80 Mon Sep 17 00:00:00 2001
From: Tarek Khalil <45508821+khalilovcmded@users.noreply.github.com>
Date: Wed, 27 Feb 2019 13:49:07 +0000
Subject: [PATCH] FEATURE: Introduce Ignore user (#7072)
---
.../components/user-card-contents.js.es6 | 10 +++
.../discourse/controllers/user.js.es6 | 10 +++
.../javascripts/discourse/models/user.js.es6 | 14 ++++
.../components/user-card-contents.hbs | 15 ++++
.../javascripts/discourse/templates/user.hbs | 27 +++++--
app/controllers/users_controller.rb | 18 ++++-
app/models/ignored_user.rb | 20 ++++++
app/serializers/user_serializer.rb | 10 +++
config/locales/client.en.yml | 2 +
config/routes.rb | 2 +
config/site_settings.yml | 3 +
.../20190225133654_add_ignored_users_table.rb | 12 ++++
lib/topic_view.rb | 40 ++++++-----
spec/fabricators/ignored_user.rb | 3 +
spec/requests/users_controller_spec.rb | 70 +++++++++++++++++++
.../web_hook_user_serializer_spec.rb | 2 +-
16 files changed, 233 insertions(+), 25 deletions(-)
create mode 100644 app/models/ignored_user.rb
create mode 100644 db/migrate/20190225133654_add_ignored_users_table.rb
create mode 100644 spec/fabricators/ignored_user.rb
diff --git a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 b/app/assets/javascripts/discourse/components/user-card-contents.js.es6
index d13fcd101c1..4849a917af8 100644
--- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6
+++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6
@@ -195,6 +195,16 @@ export default Ember.Component.extend(
this._close();
},
+ ignoreUser() {
+ this.get("user").ignore();
+ this._close();
+ },
+
+ watchUser() {
+ this.get("user").watch();
+ this._close();
+ },
+
showUser() {
this.showUser(this.get("user"));
this._close();
diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6
index e0b31bd1727..d6f577fe37f 100644
--- a/app/assets/javascripts/discourse/controllers/user.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user.js.es6
@@ -145,6 +145,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
adminDelete() {
this.get("adminTools").deleteUser(this.get("model.id"));
+ },
+
+ ignoreUser() {
+ const user = this.get("model");
+ user.ignore().then(() => user.set("ignored", true));
+ },
+
+ watchUser() {
+ const user = this.get("model");
+ user.watch().then(() => user.set("ignored", false));
}
}
});
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index 428f08ba5bf..ab7f31538d6 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -615,6 +615,20 @@ const User = RestModel.extend({
}
},
+ ignore() {
+ return ajax(`${userPath(this.get("username"))}/ignore.json`, {
+ type: "PUT",
+ data: { ignored_user_id: this.get("id") }
+ });
+ },
+
+ watch() {
+ return ajax(`${userPath(this.get("username"))}/ignore.json`, {
+ type: "DELETE",
+ data: { ignored_user_id: this.get("id") }
+ });
+ },
+
dismissBanner(bannerKey) {
this.set("dismissed_banner_key", bannerKey);
ajax(userPath(this.get("username") + ".json"), {
diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs
index a7fc8cac358..e40a5a2abe6 100644
--- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs
+++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs
@@ -48,6 +48,21 @@
icon="envelope"
label="user.private_message"}}
+ {{#if user.can_ignore_user}}
+
+ {{#if user.ignored}}
+ {{d-button class="btn-default"
+ action=(action "watchUser")
+ icon="eye"
+ label="user.watch"}}
+ {{else}}
+ {{d-button class="btn-danger"
+ action=(action "ignoreUser")
+ icon="eye-slash"
+ label="user.ignore"}}
+ {{/if}}
+
+ {{/if}}
{{/if}}
{{#if showFilter}}
diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs
index 0376714d9ad..755d9090f1f 100644
--- a/app/assets/javascripts/discourse/templates/user.hbs
+++ b/app/assets/javascripts/discourse/templates/user.hbs
@@ -43,12 +43,27 @@
{{#if model.can_send_private_message_to_user}}
- -
- {{d-button class="btn btn-primary compose-pm"
- action=(route-action "composePrivateMessage" model)
- icon="envelope"
- label="user.private_message"}}
-
+ -
+ {{d-button class="btn-primary compose-pm"
+ action=(route-action "composePrivateMessage" model)
+ icon="envelope"
+ label="user.private_message"}}
+
+ {{#if model.can_ignore_user}}
+ -
+ {{#if model.ignored}}
+ {{d-button class="btn-default"
+ action=(action "watchUser")
+ icon="eye"
+ label="user.watch"}}
+ {{else}}
+ {{d-button class="btn-danger"
+ action=(action "ignoreUser")
+ icon="eye-slash"
+ label="user.ignore"}}
+ {{/if}}
+
+ {{/if}}
{{/if}}
{{#if currentUser.staff}}
- {{d-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index be2ba574f5f..81d6ea8963c 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -14,7 +14,7 @@ class UsersController < ApplicationController
:pick_avatar, :destroy_user_image, :destroy, :check_emails,
:topic_tracking_state, :preferences, :create_second_factor,
:update_second_factor, :create_second_factor_backup, :select_avatar,
- :revoke_auth_token
+ :ignore, :watch, :revoke_auth_token
]
skip_before_action :check_xhr, only: [
@@ -995,6 +995,22 @@ class UsersController < ApplicationController
render json: success_json
end
+ def ignore
+ raise Discourse::NotFound unless SiteSetting.ignore_user_enabled
+
+ ::IgnoredUser.find_or_create_by!(
+ user: current_user,
+ ignored_user_id: params[:ignored_user_id])
+ render json: success_json
+ end
+
+ def watch
+ raise Discourse::NotFound unless SiteSetting.ignore_user_enabled
+
+ IgnoredUser.where(user: current_user, ignored_user_id: params[:ignored_user_id]).delete_all
+ render json: success_json
+ end
+
def read_faq
if user = current_user
user.user_stat.read_faq = 1.second.ago
diff --git a/app/models/ignored_user.rb b/app/models/ignored_user.rb
new file mode 100644
index 00000000000..34c6bdc995b
--- /dev/null
+++ b/app/models/ignored_user.rb
@@ -0,0 +1,20 @@
+class IgnoredUser < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :ignored_user, class_name: "User"
+end
+
+# == Schema Information
+#
+# Table name: ignored_users
+#
+# id :integer not null, primary key
+# user_id :integer not null
+# ignored_user_id :integer not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+# Indexes
+#
+# index_ignored_users_on_ignored_user_id_and_user_id (ignored_user_id,user_id) UNIQUE
+# index_ignored_users_on_user_id_and_ignored_user_id (user_id,ignored_user_id) UNIQUE
+#
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index 325f4dd3f8e..7f30eadbb0c 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -49,6 +49,8 @@ class UserSerializer < BasicUserSerializer
:can_edit_email,
:can_edit_name,
:stats,
+ :ignored,
+ :can_ignore_user,
:can_send_private_messages,
:can_send_private_message_to_user,
:bio_excerpt,
@@ -274,6 +276,14 @@ class UserSerializer < BasicUserSerializer
UserAction.stats(object.id, scope)
end
+ def ignored
+ IgnoredUser.where(user_id: scope.user&.id, ignored_user_id: object.id).exists?
+ end
+
+ def can_ignore_user
+ SiteSetting.ignore_user_enabled
+ end
+
# Needed because 'send_private_message_to_user' will always return false
# when the current user is being serialized
def can_send_private_messages
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 65d4c0d891c..ffc3400f485 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -636,6 +636,8 @@ en:
new_private_message: "New Message"
private_message: "Message"
private_messages: "Messages"
+ ignore: "Ignore"
+ watch: "Watch"
activity_stream: "Activity"
preferences: "Preferences"
profile_hidden: "This user's public profile is hidden."
diff --git a/config/routes.rb b/config/routes.rb
index 820b6d74243..d3ec4656fdb 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -421,6 +421,8 @@ Discourse::Application.routes.draw do
post "#{root_path}/:username/preferences/revoke-auth-token" => "users#revoke_auth_token", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username }
+ put "#{root_path}/:username/ignore" => "users#ignore", constraints: { username: RouteFormat.username }
+ delete "#{root_path}/:username/ignore" => "users#watch", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/invited_count" => "users#invited_count", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/invited/:filter" => "users#invited", constraints: { username: RouteFormat.username }
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 7bdcc2ac667..ed2aedae39b 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -529,6 +529,9 @@ users:
default: false
client: true
log_personal_messages_views: false
+ ignore_user_enabled:
+ hidden: true
+ default: false
groups:
enable_group_directory:
diff --git a/db/migrate/20190225133654_add_ignored_users_table.rb b/db/migrate/20190225133654_add_ignored_users_table.rb
new file mode 100644
index 00000000000..9ae56155660
--- /dev/null
+++ b/db/migrate/20190225133654_add_ignored_users_table.rb
@@ -0,0 +1,12 @@
+class AddIgnoredUsersTable < ActiveRecord::Migration[5.2]
+ def change
+ create_table :ignored_users do |t|
+ t.integer :user_id, null: false
+ t.integer :ignored_user_id, null: false
+ t.timestamps null: false
+ end
+
+ add_index :ignored_users, [:user_id, :ignored_user_id], unique: true
+ add_index :ignored_users, [:ignored_user_id, :user_id], unique: true
+ end
+end
diff --git a/lib/topic_view.rb b/lib/topic_view.rb
index f9b29c44440..a45e36f6ab1 100644
--- a/lib/topic_view.rb
+++ b/lib/topic_view.rb
@@ -330,13 +330,13 @@ class TopicView
return {} if post_ids.blank?
sql = <<~SQL
- SELECT user_id, count(*) AS count_all
- FROM posts
- WHERE id in (:post_ids)
- AND user_id IS NOT NULL
- GROUP BY user_id
- ORDER BY count_all DESC
- LIMIT #{MAX_PARTICIPANTS}
+ SELECT user_id, count(*) AS count_all
+ FROM posts
+ WHERE id in (:post_ids)
+ AND user_id IS NOT NULL
+ GROUP BY user_id
+ ORDER BY count_all DESC
+ LIMIT #{MAX_PARTICIPANTS}
SQL
Hash[*DB.query_single(sql, post_ids: post_ids)]
@@ -515,22 +515,22 @@ class TopicView
def get_sort_order(post_number)
sql = <<~SQL
- SELECT posts.sort_order
- FROM posts
- WHERE posts.post_number = #{post_number.to_i}
- AND posts.topic_id = #{@topic.id.to_i}
- LIMIT 1
+ SELECT posts.sort_order
+ FROM posts
+ WHERE posts.post_number = #{post_number.to_i}
+ AND posts.topic_id = #{@topic.id.to_i}
+ LIMIT 1
SQL
sort_order = DB.query_single(sql).first
if !sort_order
sql = <<~SQL
- SELECT posts.sort_order
- FROM posts
- WHERE posts.topic_id = #{@topic.id.to_i}
- ORDER BY @(post_number - #{post_number.to_i})
- LIMIT 1
+ SELECT posts.sort_order
+ FROM posts
+ WHERE posts.topic_id = #{@topic.id.to_i}
+ ORDER BY @(post_number - #{post_number.to_i})
+ LIMIT 1
SQL
sort_order = DB.query_single(sql).first
@@ -602,6 +602,12 @@ class TopicView
@contains_gaps = false
@filtered_posts = unfiltered_posts
+ if SiteSetting.ignore_user_enabled
+ @filtered_posts = @filtered_posts.where.not("user_id IN (?) AND id <> ?",
+ IgnoredUser.where(user_id: @user.id).select(:ignored_user_id),
+ first_post_id)
+ end
+
# Filters
if @filter == 'summary'
@filtered_posts = @filtered_posts.summary(@topic.id)
diff --git a/spec/fabricators/ignored_user.rb b/spec/fabricators/ignored_user.rb
new file mode 100644
index 00000000000..73175c8cd34
--- /dev/null
+++ b/spec/fabricators/ignored_user.rb
@@ -0,0 +1,3 @@
+Fabricator(:ignored_user) do
+ user
+end
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index 6465bc3bb97..81409de3fbe 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -2020,6 +2020,76 @@ describe UsersController do
end
end
+ describe '#ignore' do
+ it 'raises an error when not logged in' do
+ put "/u/#{user.username}/ignore.json", params: { ignored_user_id: "" }
+ expect(response.status).to eq(403)
+ end
+
+ context 'while logged in' do
+ let(:user) { Fabricate(:user) }
+ let(:another_user) { Fabricate(:user) }
+ before do
+ sign_in(user)
+ end
+
+ describe 'when SiteSetting.ignore_user_enabled is false' do
+ it 'raises an error' do
+ SiteSetting.ignore_user_enabled = false
+ put "/u/#{user.username}/ignore.json"
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'when SiteSetting.ignore_user_enabled is true' do
+ it 'creates IgnoredUser record' do
+ SiteSetting.ignore_user_enabled = true
+ put "/u/#{user.username}/ignore.json", params: { ignored_user_id: another_user.id }
+ expect(response.status).to eq(200)
+ expect(IgnoredUser.find_by(user_id: user.id,
+ ignored_user_id: another_user.id)).to be_present
+ end
+ end
+ end
+ end
+
+ describe '#watch' do
+ it 'raises an error when not logged in' do
+ delete "/u/#{user.username}/ignore.json"
+ expect(response.status).to eq(403)
+ end
+
+ context 'while logged in' do
+ let(:user) { Fabricate(:user) }
+ let(:another_user) { Fabricate(:user) }
+ before do
+ sign_in(user)
+ end
+
+ describe 'when SiteSetting.ignore_user_enabled is false' do
+ it 'raises an error' do
+ SiteSetting.ignore_user_enabled = false
+ delete "/u/#{user.username}/ignore.json", params: { ignored_user_id: another_user.id }
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'when SiteSetting.ignore_user_enabled is true' do
+ before do
+ Fabricate(:ignored_user, user_id: user.id, ignored_user_id: another_user.id)
+ end
+
+ it 'destroys IgnoredUser record' do
+ SiteSetting.ignore_user_enabled = true
+ delete "/u/#{user.username}/ignore.json", params: { ignored_user_id: another_user.id }
+ expect(response.status).to eq(200)
+ expect(IgnoredUser.find_by(user_id: user.id,
+ ignored_user_id: another_user.id)).to be_blank
+ end
+ end
+ end
+ end
+
describe "for user with period in username" do
let(:user_with_period) { Fabricate(:user, username: "myname.test") }
diff --git a/spec/serializers/web_hook_user_serializer_spec.rb b/spec/serializers/web_hook_user_serializer_spec.rb
index 6b6d3b1a623..65b5c8d876d 100644
--- a/spec/serializers/web_hook_user_serializer_spec.rb
+++ b/spec/serializers/web_hook_user_serializer_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe WebHookUserSerializer do
it 'should only include the required keys' do
count = serializer.as_json.keys.count
- difference = count - 43
+ difference = count - 45
expect(difference).to eq(0), lambda {
message = ""