mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 08:54:36 +08:00
Merge pull request #3375 from techAPJ/patch-2
FEATURE: invite existing users to private topic
This commit is contained in:
commit
2459f52c71
|
@ -7,7 +7,8 @@ export default TextField.extend({
|
||||||
var self = this,
|
var self = this,
|
||||||
selected = [],
|
selected = [],
|
||||||
currentUser = this.currentUser,
|
currentUser = this.currentUser,
|
||||||
includeGroups = this.get('includeGroups') === 'true';
|
includeGroups = this.get('includeGroups') === 'true',
|
||||||
|
allowedUsers = this.get('allowedUsers') === 'true';
|
||||||
|
|
||||||
function excludedUsernames() {
|
function excludedUsernames() {
|
||||||
if (currentUser && self.get('excludeCurrentUser')) {
|
if (currentUser && self.get('excludeCurrentUser')) {
|
||||||
|
@ -27,7 +28,8 @@ export default TextField.extend({
|
||||||
term: term.replace(/[^a-zA-Z0-9_]/, ''),
|
term: term.replace(/[^a-zA-Z0-9_]/, ''),
|
||||||
topicId: self.get('topicId'),
|
topicId: self.get('topicId'),
|
||||||
exclude: excludedUsernames(),
|
exclude: excludedUsernames(),
|
||||||
includeGroups: includeGroups
|
includeGroups,
|
||||||
|
allowedUsers
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,15 @@ export default ObjectController.extend(ModalFunctionality, {
|
||||||
disabled: function() {
|
disabled: function() {
|
||||||
if (this.get('saving')) return true;
|
if (this.get('saving')) return true;
|
||||||
if (this.blank('emailOrUsername')) return true;
|
if (this.blank('emailOrUsername')) return true;
|
||||||
|
// when inviting to forum, email must be valid
|
||||||
if (!this.get('invitingToTopic') && !Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
|
if (!this.get('invitingToTopic') && !Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
|
||||||
|
// normal users (not admin) can't invite users to private topic via email
|
||||||
|
if (!this.get('isAdmin') && this.get('isPrivateTopic') && Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
|
||||||
|
// when invting to private topic via email, group name must be specified
|
||||||
|
if (this.get('isPrivateTopic') && this.blank('groupNames') && Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
|
||||||
if (this.get('model.details.can_invite_to')) return false;
|
if (this.get('model.details.can_invite_to')) return false;
|
||||||
if (this.get('isPrivateTopic') && this.blank('groupNames')) return true;
|
|
||||||
return false;
|
return false;
|
||||||
}.property('emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'groupNames', 'saving'),
|
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'groupNames', 'saving'),
|
||||||
|
|
||||||
buttonTitle: function() {
|
buttonTitle: function() {
|
||||||
return this.get('saving') ? I18n.t('topic.inviting') : I18n.t('topic.invite_reply.action');
|
return this.get('saving') ? I18n.t('topic.inviting') : I18n.t('topic.invite_reply.action');
|
||||||
|
@ -31,20 +35,23 @@ export default ObjectController.extend(ModalFunctionality, {
|
||||||
return this.get('model') !== Discourse.User.current();
|
return this.get('model') !== Discourse.User.current();
|
||||||
}.property('model'),
|
}.property('model'),
|
||||||
|
|
||||||
|
topicId: Ember.computed.alias('model.id'),
|
||||||
|
|
||||||
// Is Private Topic? (i.e. visible only to specific group members)
|
// Is Private Topic? (i.e. visible only to specific group members)
|
||||||
isPrivateTopic: Em.computed.and('invitingToTopic', 'model.category.read_restricted'),
|
isPrivateTopic: Em.computed.and('invitingToTopic', 'model.category.read_restricted'),
|
||||||
|
|
||||||
|
// Is Private Message?
|
||||||
isMessage: Em.computed.equal('model.archetype', 'private_message'),
|
isMessage: Em.computed.equal('model.archetype', 'private_message'),
|
||||||
|
|
||||||
// Allow Existing Members? (username autocomplete)
|
// Allow Existing Members? (username autocomplete)
|
||||||
allowExistingMembers: function() {
|
allowExistingMembers: function() {
|
||||||
return this.get('invitingToTopic') && !this.get('isPrivateTopic');
|
return this.get('invitingToTopic');
|
||||||
}.property('invitingToTopic', 'isPrivateTopic'),
|
}.property('invitingToTopic'),
|
||||||
|
|
||||||
// Show Groups? (add invited user to private group)
|
// Show Groups? (add invited user to private group)
|
||||||
showGroups: function() {
|
showGroups: function() {
|
||||||
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso;
|
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && !this.get('isMessage');
|
||||||
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'invitingToTopic'),
|
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic'),
|
||||||
|
|
||||||
// Instructional text for the modal.
|
// Instructional text for the modal.
|
||||||
inviteInstructions: function() {
|
inviteInstructions: function() {
|
||||||
|
@ -55,7 +62,12 @@ export default ObjectController.extend(ModalFunctionality, {
|
||||||
// inviting to a message
|
// inviting to a message
|
||||||
return I18n.t('topic.invite_private.email_or_username');
|
return I18n.t('topic.invite_private.email_or_username');
|
||||||
} else if (this.get('invitingToTopic')) {
|
} else if (this.get('invitingToTopic')) {
|
||||||
// when inviting to topic, display instructions based on provided entity
|
// inviting to a private/public topic
|
||||||
|
if (this.get('isPrivateTopic') && !this.get('isAdmin')) {
|
||||||
|
// inviting to a private topic and is not admin
|
||||||
|
return I18n.t('topic.invite_reply.to_username');
|
||||||
|
} else {
|
||||||
|
// when inviting to a topic, display instructions based on provided entity
|
||||||
if (this.blank('emailOrUsername')) {
|
if (this.blank('emailOrUsername')) {
|
||||||
return I18n.t('topic.invite_reply.to_topic_blank');
|
return I18n.t('topic.invite_reply.to_topic_blank');
|
||||||
} else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) {
|
} else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) {
|
||||||
|
@ -63,6 +75,7 @@ export default ObjectController.extend(ModalFunctionality, {
|
||||||
} else {
|
} else {
|
||||||
return I18n.t('topic.invite_reply.to_topic_username');
|
return I18n.t('topic.invite_reply.to_topic_username');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// inviting to forum
|
// inviting to forum
|
||||||
return I18n.t('topic.invite_reply.to_forum');
|
return I18n.t('topic.invite_reply.to_forum');
|
||||||
|
|
|
@ -6,7 +6,7 @@ var cache = {},
|
||||||
currentTerm,
|
currentTerm,
|
||||||
oldSearch;
|
oldSearch;
|
||||||
|
|
||||||
function performSearch(term, topicId, includeGroups, resultsFn) {
|
function performSearch(term, topicId, includeGroups, allowedUsers, resultsFn) {
|
||||||
var cached = cache[term];
|
var cached = cache[term];
|
||||||
if (cached) {
|
if (cached) {
|
||||||
resultsFn(cached);
|
resultsFn(cached);
|
||||||
|
@ -17,7 +17,8 @@ function performSearch(term, topicId, includeGroups, resultsFn) {
|
||||||
oldSearch = $.ajax(Discourse.getURL('/users/search/users'), {
|
oldSearch = $.ajax(Discourse.getURL('/users/search/users'), {
|
||||||
data: { term: term,
|
data: { term: term,
|
||||||
topic_id: topicId,
|
topic_id: topicId,
|
||||||
include_groups: includeGroups }
|
include_groups: includeGroups,
|
||||||
|
topic_allowed_users: allowedUsers }
|
||||||
});
|
});
|
||||||
|
|
||||||
var returnVal = CANCELLED_STATUS;
|
var returnVal = CANCELLED_STATUS;
|
||||||
|
@ -75,6 +76,7 @@ function organizeResults(r, options) {
|
||||||
export default function userSearch(options) {
|
export default function userSearch(options) {
|
||||||
var term = options.term || "",
|
var term = options.term || "",
|
||||||
includeGroups = options.includeGroups,
|
includeGroups = options.includeGroups,
|
||||||
|
allowedUsers = options.allowedUsers,
|
||||||
topicId = options.topicId;
|
topicId = options.topicId;
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,7 +103,7 @@ export default function userSearch(options) {
|
||||||
resolve(CANCELLED_STATUS);
|
resolve(CANCELLED_STATUS);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
debouncedSearch(term, topicId, includeGroups, function(r) {
|
debouncedSearch(term, topicId, includeGroups, allowedUsers, function(r) {
|
||||||
clearTimeout(clearPromise);
|
clearTimeout(clearPromise);
|
||||||
resolve(organizeResults(r, options));
|
resolve(organizeResults(r, options));
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,11 @@
|
||||||
{{else}}
|
{{else}}
|
||||||
<label>{{inviteInstructions}}</label>
|
<label>{{inviteInstructions}}</label>
|
||||||
{{#if allowExistingMembers}}
|
{{#if allowExistingMembers}}
|
||||||
{{user-selector single="true" allowAny=true excludeCurrentUser="true" usernames=emailOrUsername includeGroups="true" placeholderKey=placeholderKey}}
|
{{#if isPrivateTopic}}
|
||||||
|
{{user-selector single="true" allowAny=true excludeCurrentUser="true" usernames=emailOrUsername allowedUsers="true" topicId=topicId placeholderKey=placeholderKey}}
|
||||||
|
{{else}}
|
||||||
|
{{user-selector single="true" allowAny=true excludeCurrentUser="true" usernames=emailOrUsername placeholderKey=placeholderKey}}
|
||||||
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}}
|
{{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -446,8 +446,9 @@ class UsersController < ApplicationController
|
||||||
term = params[:term].to_s.strip
|
term = params[:term].to_s.strip
|
||||||
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
|
||||||
|
topic_allowed_users = params[:topic_allowed_users] || false
|
||||||
|
|
||||||
results = UserSearch.new(term, topic_id: topic_id, searching_user: current_user).search
|
results = UserSearch.new(term, topic_id: topic_id, topic_allowed_users: topic_allowed_users, searching_user: current_user).search
|
||||||
|
|
||||||
user_fields = [:username, :upload_avatar_template, :uploaded_avatar_id]
|
user_fields = [:username, :upload_avatar_template, :uploaded_avatar_id]
|
||||||
user_fields << :name if SiteSetting.enable_names?
|
user_fields << :name if SiteSetting.enable_names?
|
||||||
|
|
|
@ -5,6 +5,7 @@ class UserSearch
|
||||||
@term = term
|
@term = term
|
||||||
@term_like = "#{term.downcase}%"
|
@term_like = "#{term.downcase}%"
|
||||||
@topic_id = opts[:topic_id]
|
@topic_id = opts[:topic_id]
|
||||||
|
@topic_allowed_users = opts[:topic_allowed_users]
|
||||||
@searching_user = opts[:searching_user]
|
@searching_user = opts[:searching_user]
|
||||||
@limit = opts[:limit] || 20
|
@limit = opts[:limit] || 20
|
||||||
end
|
end
|
||||||
|
@ -36,6 +37,18 @@ class UserSearch
|
||||||
users = users.not_suspended
|
users = users.not_suspended
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Only show users who have access to private topic
|
||||||
|
if @topic_id && @topic_allowed_users == "true"
|
||||||
|
allowed_user_ids = []
|
||||||
|
topic = Topic.find_by(id: @topic_id)
|
||||||
|
|
||||||
|
if topic.category && topic.category.read_restricted
|
||||||
|
users = users.includes(:secure_categories)
|
||||||
|
.where("users.admin = TRUE OR categories.id = ?", topic.category.id)
|
||||||
|
.references(:categories)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
users.order("CASE WHEN last_seen_at IS NULL THEN 0 ELSE 1 END DESC, last_seen_at DESC, username ASC")
|
users.order("CASE WHEN last_seen_at IS NULL THEN 0 ELSE 1 END DESC, last_seen_at DESC, username ASC")
|
||||||
.limit(@limit)
|
.limit(@limit)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1109,6 +1109,7 @@ en:
|
||||||
to_topic_blank: "Enter the username or email address of the person you'd like to invite to this topic."
|
to_topic_blank: "Enter the username or email address of the person you'd like to invite to this topic."
|
||||||
to_topic_email: "You've entered an email address. We'll email an invitation that allows your friend to immediately reply to this topic."
|
to_topic_email: "You've entered an email address. We'll email an invitation that allows your friend to immediately reply to this topic."
|
||||||
to_topic_username: "You've entered a username. We'll send a notification to that user with a link inviting them to this topic."
|
to_topic_username: "You've entered a username. We'll send a notification to that user with a link inviting them to this topic."
|
||||||
|
to_username: "Enter the username of the person you'd like to invite. We'll send a notification to that user with a link inviting them to this topic."
|
||||||
|
|
||||||
email_placeholder: 'name@example.com'
|
email_placeholder: 'name@example.com'
|
||||||
success_email: "We mailed out an invitation to <b>{{emailOrUsername}}</b>. We'll notify you when the invitation is redeemed. Check the invitations tab on your user page to keep track of your invites."
|
success_email: "We mailed out an invitation to <b>{{emailOrUsername}}</b>. We'll notify you when the invitation is redeemed. Check the invitations tab on your user page to keep track of your invites."
|
||||||
|
|
|
@ -214,14 +214,12 @@ class Guardian
|
||||||
return false unless ( SiteSetting.enable_local_logins && (!SiteSetting.must_approve_users? || is_staff?) )
|
return false unless ( SiteSetting.enable_local_logins && (!SiteSetting.must_approve_users? || is_staff?) )
|
||||||
return true if is_admin?
|
return true if is_admin?
|
||||||
return false if ! can_see?(object)
|
return false if ! can_see?(object)
|
||||||
|
|
||||||
return false if group_ids.present?
|
return false if group_ids.present?
|
||||||
|
|
||||||
if object.is_a?(Topic) && object.category
|
if object.is_a?(Topic) && object.category
|
||||||
if object.category.groups.any?
|
if object.category.groups.any?
|
||||||
return true if object.category.groups.all? { |g| can_edit_group?(g) }
|
return true if object.category.groups.all? { |g| can_edit_group?(g) }
|
||||||
end
|
end
|
||||||
return false if object.category.read_restricted
|
|
||||||
end
|
end
|
||||||
|
|
||||||
user.has_trust_level?(TrustLevel[2])
|
user.has_trust_level?(TrustLevel[2])
|
||||||
|
|
|
@ -1170,6 +1170,25 @@ describe UsersController do
|
||||||
expect(json["users"].map { |u| u["username"] }).to include(user.username)
|
expect(json["users"].map { |u| u["username"] }).to include(user.username)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "searches only for users who have access to private topic" do
|
||||||
|
privileged_user = Fabricate(:user, trust_level: 4, username: "joecabit", name: "Lawrence Tierney")
|
||||||
|
privileged_group = Fabricate(:group)
|
||||||
|
privileged_group.add(privileged_user)
|
||||||
|
privileged_group.save
|
||||||
|
|
||||||
|
category = Fabricate(:category)
|
||||||
|
category.set_permissions(privileged_group => :readonly)
|
||||||
|
category.save
|
||||||
|
|
||||||
|
private_topic = Fabricate(:topic, category: category)
|
||||||
|
|
||||||
|
xhr :post, :search_users, term: user.name.split(" ").last, topic_id: private_topic.id, topic_allowed_users: "true"
|
||||||
|
expect(response).to be_success
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
expect(json["users"].map { |u| u["username"] }).to_not include(user.username)
|
||||||
|
expect(json["users"].map { |u| u["username"] }).to include(privileged_user.username)
|
||||||
|
end
|
||||||
|
|
||||||
context "when `enable_names` is true" do
|
context "when `enable_names` is true" do
|
||||||
before do
|
before do
|
||||||
SiteSetting.enable_names = true
|
SiteSetting.enable_names = true
|
||||||
|
|
Loading…
Reference in New Issue
Block a user