SiteSetting to hide regular names from users

This commit is contained in:
Robin Ward 2013-10-30 15:45:13 -04:00
parent c8d5db38d6
commit 3d6d7c8abe
20 changed files with 236 additions and 83 deletions

View File

@ -12,9 +12,7 @@ Discourse.UserController = Discourse.ObjectController.extend({
return this.get('content.username') === Discourse.User.currentProp('username'); return this.get('content.username') === Discourse.User.currentProp('username');
}.property('content.username'), }.property('content.username'),
collapsedInfo: function() { collapsedInfo: Em.computed.not('indexStream'),
return !this.get('indexStream');
}.property('indexStream'),
canSeePrivateMessages: function() { canSeePrivateMessages: function() {
return this.get('viewingSelf') || Discourse.User.currentProp('staff'); return this.get('viewingSelf') || Discourse.User.currentProp('staff');

View File

@ -49,9 +49,9 @@ Discourse.Route.buildRoutes(function() {
this.route('index', { path: '/'} ); this.route('index', { path: '/'} );
this.resource('userActivity', { path: '/activity' }, function() { this.resource('userActivity', { path: '/activity' }, function() {
var resource = this; var self = this;
Object.keys(Discourse.UserAction.TYPES).forEach(function (userAction) { Object.keys(Discourse.UserAction.TYPES).forEach(function (userAction) {
resource.route(userAction, { path: userAction.replace("_", "-") }); self.route(userAction, { path: userAction.replace("_", "-") });
}); });
}); });

View File

@ -15,6 +15,11 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' }); this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
}, },
setupController: function(controller, model) {
controller.set('model', model);
this.controllerFor('user').set('indexStream', false);
},
actions: { actions: {
showAvatarSelector: function() { showAvatarSelector: function() {
Discourse.Route.showModal(this, 'avatarSelector'); Discourse.Route.showModal(this, 'avatarSelector');

View File

@ -13,5 +13,11 @@ Discourse.UserInvitedRoute = Discourse.Route.extend({
model: function() { model: function() {
return Discourse.InviteList.findInvitedBy(this.modelFor('user')); return Discourse.InviteList.findInvitedBy(this.modelFor('user'));
},
setupController: function(controller, model) {
controller.set('model', model);
this.controllerFor('user').set('indexStream', false);
} }
}); });

View File

@ -4,9 +4,9 @@
<section class='user-navigation'> <section class='user-navigation'>
<ul class='action-list nav-stacked'> <ul class='action-list nav-stacked'>
{{activityFilter count=statsCountNonPM user=model}} {{discourse-activity-filter count=statsCountNonPM user=model userActionType=userActionType indexStream=indexStream}}
{{#each stat in statsExcludingPms}} {{#each stat in statsExcludingPms}}
{{activityFilter content=stat user=model}} {{discourse-activity-filter content=stat user=model userActionType=userActionType indexStream=indexStream}}
{{/each}} {{/each}}
</ul> </ul>

View File

@ -135,6 +135,7 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, {
} }
}, },
actions: {
/** /**
Toggle the replies this post is a reply to Toggle the replies this post is a reply to
@ -166,6 +167,7 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, {
}); });
}); });
} }
}
}, },
// Add the quote controls to a post // Add the quote controls to a post

View File

@ -2,15 +2,14 @@
This view handles rendering of an activity in a user's profile This view handles rendering of an activity in a user's profile
@class ActivityFilterView @class ActivityFilterView
@extends Discourse.View @extends Ember.Component
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.ActivityFilterView = Discourse.View.extend({ Discourse.ActivityFilterView = Ember.Component.extend({
tagName: 'li', tagName: 'li',
classNameBindings: ['active', 'noGlyph'], classNameBindings: ['active', 'noGlyph'],
userActionType: Em.computed.alias('controller.userActionType'),
shouldRerender: Discourse.View.renderIfChanged('count'), shouldRerender: Discourse.View.renderIfChanged('count'),
noGlyph: Em.computed.empty('icon'), noGlyph: Em.computed.empty('icon'),
@ -19,9 +18,9 @@ Discourse.ActivityFilterView = Discourse.View.extend({
if (content) { if (content) {
return parseInt(this.get('userActionType'), 10) === parseInt(Em.get(content, 'action_type'), 10); return parseInt(this.get('userActionType'), 10) === parseInt(Em.get(content, 'action_type'), 10);
} else { } else {
return this.blank('userActionType'); return this.get('indexStream');
} }
}.property('userActionType', 'content.action_type'), }.property('userActionType', 'indexStream'),
activityCount: function() { activityCount: function() {
return this.get('content.count') || this.get('count'); return this.get('content.count') || this.get('count');
@ -75,4 +74,4 @@ Discourse.ActivityFilterView = Discourse.View.extend({
}); });
Discourse.View.registerHelper('activityFilter', Discourse.ActivityFilterView); Discourse.View.registerHelper('discourse-activity-filter', Discourse.ActivityFilterView);

View File

@ -240,10 +240,12 @@ class UsersController < ApplicationController
topic_id = params[:topic_id] topic_id = params[:topic_id]
topic_id = topic_id.to_i if topic_id topic_id = topic_id.to_i if topic_id
results = UserSearch.search term, topic_id results = UserSearch.new(term, topic_id).search
render json: { users: results.as_json(only: [ :username, :name, :use_uploaded_avatar, :upload_avatar_template, :uploaded_avatar_id], user_fields = [:username, :use_uploaded_avatar, :upload_avatar_template, :uploaded_avatar_id]
methods: :avatar_template) } user_fields << :name if SiteSetting.enable_names?
render json: { users: results.as_json(only: user_fields, methods: :avatar_template) }
end end
# [LEGACY] avatars in quotes/oneboxes might still be pointing to this route # [LEGACY] avatars in quotes/oneboxes might still be pointing to this route

View File

@ -270,6 +270,8 @@ class SiteSetting < ActiveRecord::Base
# hidden setting only used by system # hidden setting only used by system
setting(:uncategorized_category_id, -1, hidden: true) setting(:uncategorized_category_id, -1, hidden: true)
client_setting(:enable_names, true)
def self.call_discourse_hub? def self.call_discourse_hub?
self.enforce_global_nicknames? && self.discourse_org_access_key.present? self.enforce_global_nicknames? && self.discourse_org_access_key.present?
end end

View File

@ -1,36 +1,38 @@
# Searches for a user by username or full text or name (if enabled in SiteSettings)
class UserSearch class UserSearch
def self.search term, topic_id = nil
sql = User.sql_builder(
"select id, username, name, email, use_uploaded_avatar, uploaded_avatar_template, uploaded_avatar_id from users u
/*left_join*/
/*where*/
/*order_by*/")
def initialize(term, topic_id=nil)
if topic_id @term = term
sql.left_join "(select distinct p.user_id from posts p where topic_id = :topic_id) s on s.user_id = u.id", topic_id: topic_id @term_like = "#{term.downcase}%"
@topic_id = topic_id
end end
if term.present? def search
sql.where("username_lower like :term_like or users = User.order(User.sql_fragment("CASE WHEN username_lower = ? THEN 0 ELSE 1 END ASC", :term))
to_tsvector('simple', name) @@
to_tsquery('simple', if @term.present?
regexp_replace( if SiteSetting.enable_names?
regexp_replace( users = users.where("username_lower LIKE :term_like OR
cast(plainto_tsquery(:term) as text) TO_TSVECTOR('simple', name) @@
TO_TSQUERY('simple',
REGEXP_REPLACE(
REGEXP_REPLACE(
CAST(PLAINTO_TSQUERY(:term) AS TEXT)
,'\''(?: |$)', ':*''', 'g'), ,'\''(?: |$)', ':*''', 'g'),
'''', '', 'g') '''', '', 'g')
)", term: term, term_like: "#{term.downcase}%") )", term: @term, term_like: @term_like)
else
sql.order_by "case when username_lower = :term then 0 else 1 end asc" users = users.where("username_lower LIKE :term_like", term_like: @term_like)
end
end end
if topic_id if @topic_id
sql.order_by "case when s.user_id is null then 0 else 1 end desc" users = users.joins(User.sql_fragment("LEFT JOIN (SELECT DISTINCT p.user_id FROM POSTS p WHERE topic_id = ?) s ON s.user_id = users.id", @topic_id))
.order("CASE WHEN s.user_id IS NULL THEN 0 ELSE 1 END DESC")
end end
sql.order_by "case when last_seen_at is null then 0 else 1 end desc, last_seen_at desc, username asc limit(20)" users.order("CASE WHEN last_seen_at IS NULL THEN 0 ELSE 1 END DESC, last_seen_at DESC, username ASC")
.limit(20)
sql.exec
end end
end end

View File

@ -31,4 +31,8 @@ class BasicPostSerializer < ApplicationSerializer
end end
end end
def include_name?
SiteSetting.enable_names?
end
end end

View File

@ -112,7 +112,6 @@ class PostSerializer < BasicPostSerializer
def reply_to_user def reply_to_user
{ {
username: object.reply_to_user.username, username: object.reply_to_user.username,
name: object.reply_to_user.name,
avatar_template: object.reply_to_user.avatar_template avatar_template: object.reply_to_user.avatar_template
} }
end end
@ -194,6 +193,10 @@ class PostSerializer < BasicPostSerializer
post_actions.present? && post_actions.keys.include?(PostActionType.types[:bookmark]) post_actions.present? && post_actions.keys.include?(PostActionType.types[:bookmark])
end end
def include_display_username?
SiteSetting.enable_names?
end
private private
def post_actions def post_actions

View File

@ -92,4 +92,8 @@ class UserSerializer < BasicUserSerializer
User.gravatar_template(object.email) User.gravatar_template(object.email)
end end
def include_name?
SiteSetting.enable_names?
end
end end

View File

@ -721,6 +721,8 @@ en:
dominating_topic_minimum_percent: "What percentage of posts a user has to make in a topic before we consider it dominating." dominating_topic_minimum_percent: "What percentage of posts a user has to make in a topic before we consider it dominating."
enable_names: "Allow users to show their full names"
notification_types: notification_types:
mentioned: "%{display_username} mentioned you in %{link}" mentioned: "%{display_username} mentioned you in %{link}"
liked: "%{display_username} liked your post in %{link}" liked: "%{display_username} liked your post in %{link}"

View File

@ -11,6 +11,10 @@ class ActiveRecord::Base
exec_sql(*args).cmd_tuples exec_sql(*args).cmd_tuples
end end
def self.sql_fragment(*sql_array)
ActiveRecord::Base.send(:sanitize_sql_array, sql_array)
end
# exists fine in rails4 # exists fine in rails4
unless rails4? unless rails4?
# note: update_attributes still spins up a transaction this can cause contention # note: update_attributes still spins up a transaction this can cause contention

View File

@ -893,6 +893,30 @@ describe UsersController do
json["users"].map { |u| u["username"] }.should include(user.username) json["users"].map { |u| u["username"] }.should include(user.username)
end end
context "when `enable_names` is true" do
before do
SiteSetting.stubs(:enable_names?).returns(true)
end
it "returns names" do
xhr :post, :search_users, term: user.name
json = JSON.parse(response.body)
json["users"].map { |u| u["name"] }.should include(user.name)
end
end
context "when `enable_names` is false" do
before do
SiteSetting.stubs(:enable_names?).returns(false)
end
it "returns names" do
xhr :post, :search_users, term: user.name
json = JSON.parse(response.body)
json["users"].map { |u| u["name"] }.should_not include(user.name)
end
end
end end
describe 'send_activation_email' do describe 'send_activation_email' do

View File

@ -21,53 +21,68 @@ describe UserSearch do
Fabricate :post, user: user6, topic: topic Fabricate :post, user: user6, topic: topic
end end
def search_for(*args)
UserSearch.new(*args).search
end
# this is a seriously expensive integration test, re-creating this entire test db is too expensive # this is a seriously expensive integration test, re-creating this entire test db is too expensive
# reuse # reuse
it "operates correctly" do it "operates correctly" do
# normal search # normal search
results = UserSearch.search user1.name.split(" ").first results = search_for(user1.name.split(" ").first)
results.size.should == 1 results.size.should == 1
results.first.should == user1 results.first.should == user1
# lower case # lower case
results = UserSearch.search user1.name.split(" ").first.downcase results = search_for(user1.name.split(" ").first.downcase)
results.size.should == 1 results.size.should == 1
results.first.should == user1 results.first.should == user1
# username # username
results = UserSearch.search user4.username results = search_for(user4.username)
results.size.should == 1 results.size.should == 1
results.first.should == user4 results.first.should == user4
# case insensitive # case insensitive
results = UserSearch.search user4.username.upcase results = search_for(user4.username.upcase)
results.size.should == 1 results.size.should == 1
results.first.should == user4 results.first.should == user4
# substrings # substrings
results = UserSearch.search "mr" results = search_for("mr")
results.size.should == 6 results.size.should == 6
results = UserSearch.search "mrb" results = search_for("mrb")
results.size.should == 3 results.size.should == 3
results = UserSearch.search "MR" results = search_for("MR")
results.size.should == 6 results.size.should == 6
results = UserSearch.search "MRB" results = search_for("MRB")
results.size.should == 3 results.size.should == 3
# topic priority # topic priority
results = UserSearch.search "mrb", topic.id results = search_for("mrb", topic.id)
results.first.should == user1 results.first.should == user1
results = UserSearch.search "mrb", topic2.id results = search_for("mrb", topic2.id)
results.first.should == user2 results.first.should == user2
results = UserSearch.search "mrb", topic3.id results = search_for("mrb", topic3.id)
results.first.should == user5 results.first.should == user5
# When searching by name is enabled, it returns the record
SiteSetting.stubs(:enable_names).returns(true)
results = search_for("Tarantino")
results.size.should == 1
# When searching by name is disabled, it will not return the record
SiteSetting.stubs(:enable_names).returns(false)
results = search_for("Tarantino")
results.size.should == 0
end end
end end

View File

@ -0,0 +1,25 @@
require 'spec_helper'
require_dependency 'post'
require_dependency 'user'
describe BasicPostSerializer do
context "name" do
let(:user) { Fabricate.build(:user) }
let(:post) { Fabricate.build(:post, user: user) }
let(:serializer) { BasicPostSerializer.new(post, scope: Guardian.new, root: false) }
let(:json) { serializer.as_json }
it "returns the name it when `enable_names` is true" do
SiteSetting.stubs(:enable_names?).returns(true)
json[:name].should be_present
end
it "doesn't return the name it when `enable_names` is false" do
SiteSetting.stubs(:enable_names?).returns(false)
json[:name].should be_blank
end
end
end

View File

@ -58,4 +58,21 @@ describe PostSerializer do
end end
end end
context "display_username" do
let(:user) { Fabricate.build(:user) }
let(:post) { Fabricate.build(:post, user: user) }
let(:serializer) { PostSerializer.new(post, scope: Guardian.new, root: false) }
let(:json) { serializer.as_json }
it "returns the display_username it when `enable_names` is on" do
SiteSetting.stubs(:enable_names).returns(true)
json[:display_username].should be_present
end
it "doesn't return the display_username it when `enable_names` is off" do
SiteSetting.stubs(:enable_names).returns(false)
json[:display_username].should be_blank
end
end
end end

View File

@ -0,0 +1,39 @@
require 'spec_helper'
require_dependency 'user'
describe UserSerializer do
context "with a user" do
let(:user) { Fabricate.build(:user) }
let(:serializer) { UserSerializer.new(user, scope: Guardian.new, root: false) }
let(:json) { serializer.as_json }
it "produces json" do
json.should be_present
end
context "with `enable_names` true" do
before do
SiteSetting.stubs(:enable_names?).returns(true)
end
it "has a name" do
json[:name].should == "Bruce Wayne"
end
end
context "with `enable_names` false" do
before do
SiteSetting.stubs(:enable_names?).returns(false)
end
it "has a name" do
puts json[:name]
json[:name].should be_blank
end
end
end
end