diff --git a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 index 6e9f48296d8..957fac42ffe 100644 --- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 @@ -12,6 +12,7 @@ export default Ember.Controller.extend({ canLoadMore: true, invitesLoading: false, reinvitedAll: false, + rescindedAll: false, init: function() { this._super(); @@ -32,7 +33,7 @@ export default Ember.Controller.extend({ inviteRedeemed: Em.computed.equal('filter', 'redeemed'), - showReinviteAllButton: function() { + showBulkActionButtons: function() { return (this.get('filter') === "pending" && this.get('model').invites.length > 4 && this.currentUser.get('staff')); }.property('filter'), @@ -86,6 +87,18 @@ export default Ember.Controller.extend({ return false; }, + rescindAll() { + const self = this; + bootbox.confirm(I18n.t("user.invited.rescind_all_confirm"), confirm => { + if (confirm) { + Invite.rescindAll().then(function() { + self.set('rescindedAll', true); + self.get('model.invites').clear(); + }).catch(popupAjaxError); + } + }); + }, + reinvite(invite) { invite.reinvite(); return false; diff --git a/app/assets/javascripts/discourse/models/invite.js.es6 b/app/assets/javascripts/discourse/models/invite.js.es6 index 1425e63b25a..b3373f10808 100644 --- a/app/assets/javascripts/discourse/models/invite.js.es6 +++ b/app/assets/javascripts/discourse/models/invite.js.es6 @@ -58,6 +58,10 @@ Invite.reopenClass({ reinviteAll() { return ajax('/invites/reinvite-all', { type: 'POST' }); + }, + + rescindAll() { + return ajax('/invites/rescind-all', { type: 'POST' }); } }); diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index 0a78d6e64fe..12a26aa3a0c 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -19,7 +19,12 @@ {{csv-uploader uploading=uploading}} {{fa-icon "question-circle"}} {{/if}} - {{#if showReinviteAllButton}} + {{#if showBulkActionButtons}} + {{#if rescindedAll}} + {{i18n 'user.invited.rescinded_all'}} + {{else}} + {{d-button icon="times" action="rescindAll" class="btn" label="user.invited.rescind_all"}} + {{/if}} {{#if reinvitedAll}} {{i18n 'user.invited.reinvited_all'}} {{else}} diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index b32ae4acbf9..5443ebec953 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -6,7 +6,7 @@ class InvitesController < ApplicationController skip_before_filter :preload_json, except: [:show] skip_before_filter :redirect_to_login_if_required - before_filter :ensure_logged_in, only: [:destroy, :create, :create_invite_link, :resend_invite, :resend_all_invites, :upload_csv] + before_filter :ensure_logged_in, only: [:destroy, :create, :create_invite_link, :rescind_all_invites, :resend_invite, :resend_all_invites, :upload_csv] before_filter :ensure_new_registrations_allowed, only: [:show, :perform_accept_invitation, :redeem_disposable_invite] before_filter :ensure_not_logged_in, only: [:show, :perform_accept_invitation, :redeem_disposable_invite] @@ -150,6 +150,13 @@ class InvitesController < ApplicationController render nothing: true end + def rescind_all_invites + guardian.ensure_can_rescind_all_invites!(current_user) + + Invite.rescind_all_invites_from(current_user) + render nothing: true + end + def resend_invite params.require(:email) RateLimiter.new(current_user, "resend-invite-per-hour", 10, 1.hour).performed! diff --git a/app/models/invite.rb b/app/models/invite.rb index b48b85961b9..4749baa87bc 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -256,6 +256,12 @@ class Invite < ActiveRecord::Base end end + def self.rescind_all_invites_from(user) + Invite.where('invites.user_id IS NULL AND invites.email IS NOT NULL AND invited_by_id = ?', user.id).find_each do |invite| + invite.trash!(user) unless invite.blank? + end + end + def limit_invites_per_day RateLimiter.new(invited_by, "invites-per-day", SiteSetting.max_invites_per_day, 1.day.to_i) end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 73e86c46653..b9524aa2ca1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -836,6 +836,9 @@ en: expired: "This invite has expired." rescind: "Remove" rescinded: "Invite removed" + rescind_all: "Remove all Invites" + rescinded_all: "All Invites removed!" + rescind_all_confirm: "Are you sure you want to remove all invites?" reinvite: "Resend Invite" reinvite_all: "Resend all Invites" reinvite_all_confirm: "Are you sure you want to resend all invites?" diff --git a/config/routes.rb b/config/routes.rb index d4d89799b12..0070fa9662a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -646,6 +646,7 @@ Discourse::Application.routes.draw do resources :invites post "invites/upload_csv" => "invites#upload_csv" + post "invites/rescind-all" => "invites#rescind_all_invites" post "invites/reinvite" => "invites#resend_invite" post "invites/reinvite-all" => "invites#resend_all_invites" post "invites/link" => "invites#create_invite_link" diff --git a/lib/guardian.rb b/lib/guardian.rb index 83513c6eb27..98f95311645 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -269,6 +269,10 @@ class Guardian user.staff? end + def can_rescind_all_invites?(user) + user.staff? + end + def can_see_private_messages?(user_id) is_admin? || (authenticated? && @user.id == user_id) end diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index 60c390a681c..43256048a80 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -484,4 +484,16 @@ describe Invite do end + describe '.rescind_all_invites_from' do + it 'removes all invites sent by a user' do + user = Fabricate(:user) + invite_1 = Fabricate(:invite, invited_by: user) + invite_2 = Fabricate(:invite, invited_by: user) + Invite.rescind_all_invites_from(user) + invite_1.reload + invite_2.reload + expect(invite_1.deleted_at).to be_present + expect(invite_2.deleted_at).to be_present + end + end end