- {{d-button icon="plus" action=(route-action "showInvite") label="user.invited.create"}}
+ {{d-button icon="plus" action=(action "createInvite") label="user.invited.create"}}
{{#if canBulkInvite}}
- {{csv-uploader uploading=uploading}}
+ {{d-button icon="upload" action=(action "createInviteCsv") label="user.invited.bulk_invite.text"}}
{{/if}}
{{#if showBulkActionButtons}}
- {{#if rescindedAll}}
- {{i18n "user.invited.rescinded_all"}}
+ {{#if removedAll}}
+ {{i18n "user.invited.removed_all"}}
{{else}}
- {{d-button icon="times" action=(action "rescindAll") label="user.invited.rescind_all"}}
+ {{d-button icon="times" action=(action "destroyAllExpired") label="user.invited.remove_all"}}
{{/if}}
{{#if reinvitedAll}}
{{i18n "user.invited.reinvited_all"}}
@@ -44,10 +39,10 @@
{{/if}}
{{#if model.invites}}
-
+ {{/if}}
+ {{conditional-loading-spinner condition=invitesLoading}}
{{else}}
{{#if canBulkInvite}}
diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
index 08fbde994ec..04e1178d441 100644
--- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
+++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
@@ -188,12 +188,6 @@ export function applyDefaultHandlers(pretender) {
});
});
- pretender.get("/u/eviltrout/invited_count.json", () => {
- return response({
- counts: { pending: 1, redeemed: 0, total: 0 },
- });
- });
-
pretender.get("/u/eviltrout/invited.json", () => {
return response({ invites: [{ id: 1 }] });
});
diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss
index 1c8fa7e9469..d7717460ff9 100644
--- a/app/assets/stylesheets/common/base/modal.scss
+++ b/app/assets/stylesheets/common/base/modal.scss
@@ -766,3 +766,137 @@
text-align: right;
}
}
+
+.json-schema-editor-modal {
+ h3.card-title {
+ margin-top: 0;
+ label {
+ display: none;
+ }
+ }
+
+ .card .je-object__container {
+ border-bottom: 1px dashed var(--primary-low);
+ padding-bottom: 1em;
+ margin-bottom: 1em;
+ position: relative;
+
+ .card-title label {
+ display: inline-block;
+ font-size: $font-down-1;
+ color: var(--primary-medium);
+ }
+ .form-group {
+ label {
+ display: inline-block;
+ width: 33%;
+ }
+ .form-control {
+ width: 66%;
+ }
+ }
+ .btn-group:last-child {
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ .btn {
+ font-size: $font-down-2;
+ }
+ }
+ }
+
+ .btn-group {
+ margin-top: 0;
+ }
+
+ .json-editor-btn-delete {
+ @extend .btn-danger !optional;
+ @extend .no-text !optional;
+ .d-icon + span {
+ display: none;
+ }
+ }
+
+ .card-body > .btn-group {
+ // !important needed to override inline style :-(
+ display: block !important;
+ text-align: right;
+ }
+}
+
+.create-invite-modal {
+ .input-group {
+ margin-bottom: 1em;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ input[type="text"] {
+ width: 100%;
+
+ &.invite-link {
+ width: 85%;
+ }
+ }
+ }
+
+ .radio-group {
+ input[type="radio"] {
+ display: table-cell;
+ vertical-align: middle;
+ margin-top: -1px;
+ }
+
+ label {
+ display: inline-block;
+ }
+ }
+
+ .group-chooser,
+ .future-date-input-selector {
+ width: 100%;
+ }
+
+ .input-group input[type="text"],
+ .input-group .btn,
+ .future-date-input .select-kit-header,
+ .control-group:nth-child(2) input,
+ .control-group:nth-child(3) input {
+ height: 34px;
+ }
+
+ .input-group .btn {
+ vertical-align: top;
+ }
+
+ .future-date-input {
+ .date-picker-wrapper {
+ input {
+ margin: 0;
+ }
+ }
+
+ .control-group:nth-child(2),
+ .control-group:nth-child(3) {
+ display: inline-block;
+ margin-bottom: 0;
+ width: 49%;
+
+ input {
+ margin-bottom: 0;
+ width: 150px;
+ }
+ }
+ }
+
+ .invite-max-redemptions {
+ label {
+ display: inline;
+ }
+
+ input {
+ width: 80px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/desktop/modal.scss b/app/assets/stylesheets/desktop/modal.scss
index cf13d930ebb..5c22640ffeb 100644
--- a/app/assets/stylesheets/desktop/modal.scss
+++ b/app/assets/stylesheets/desktop/modal.scss
@@ -107,3 +107,10 @@
min-width: 500px;
}
}
+
+.create-invite-modal,
+.create-invite-bulk-modal {
+ .modal-inner-container {
+ width: 700px;
+ }
+}
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index ddb0c6f36bf..988be6e14ba 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -90,6 +90,10 @@
tr {
td {
padding: 0.667em;
+ &.actions {
+ white-space: nowrap;
+ width: 100px;
+ }
}
}
}
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index c9f236bac4a..8d2becaaae2 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -374,7 +374,7 @@ class GroupsController < ApplicationController
end
emails.each do |email|
- Invite.invite_by_email(email, current_user, nil, [group.id])
+ Invite.generate(current_user, email: email, group_ids: [group.id])
end
render json: success_json.merge!(
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 5d083158578..17f39297b70 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -2,10 +2,7 @@
class InvitesController < ApplicationController
- requires_login only: [
- :destroy, :create, :create_invite_link, :rescind_all_invites,
- :resend_invite, :resend_all_invites, :upload_csv
- ]
+ requires_login only: [:create, :destroy, :destroy_all, :resend_invite, :resend_all_invites, :upload_csv]
skip_before_action :check_xhr, except: [:perform_accept_invitation]
skip_before_action :preload_json, except: [:show]
@@ -23,84 +20,55 @@ class InvitesController < ApplicationController
invited_by: UserNameSerializer.new(invite.invited_by, scope: guardian, root: false),
email: invite.email,
username: UserNameSuggester.suggest(invite.email),
- is_invite_link: invite.is_invite_link?)
- )
+ is_invite_link: invite.is_invite_link?
+ ))
render layout: 'application'
else
- flash.now[:error] = if invite.present? && invite.expired?
+ flash.now[:error] = if invite&.expired?
I18n.t('invite.expired', base_url: Discourse.base_url)
- elsif invite.present? && invite.redeemed?
+ elsif invite&.redeemed?
I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url)
else
I18n.t('invite.not_found', base_url: Discourse.base_url)
end
+
render layout: 'no_ember'
end
end
- def perform_accept_invitation
- params.require(:id)
- params.permit(:email, :username, :name, :password, :timezone, user_custom_fields: {})
- invite = Invite.find_by(invite_key: params[:id])
-
- if invite.present?
- begin
- user = if invite.is_invite_link?
- invite.redeem_invite_link(email: params[:email], username: params[:username], name: params[:name], password: params[:password], user_custom_fields: params[:user_custom_fields], ip_address: request.remote_ip)
- else
- invite.redeem(username: params[:username], name: params[:name], password: params[:password], user_custom_fields: params[:user_custom_fields], ip_address: request.remote_ip)
- end
-
- if user.present?
- log_on_user(user) if user.active?
- user.update_timezone_if_missing(params[:timezone])
- post_process_invite(user)
- response = { success: true }
- else
- response = { success: false, message: I18n.t('invite.not_found_json') }
- end
-
- if user.present? && user.active?
- topic = invite.topics.first
- response[:redirect_to] = topic.present? ? path("#{topic.relative_url}") : path("/")
- elsif user.present?
- response[:message] = I18n.t('invite.confirm_email')
- end
-
- render json: response
- rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
- render json: {
- success: false,
- errors: e.record&.errors&.to_hash || {},
- message: I18n.t('invite.error_message')
- }
- rescue Invite::UserExists => e
- render json: { success: false, message: [e.message] }
- end
- else
- render json: { success: false, message: I18n.t('invite.not_found_json') }
- end
- end
-
def create
- params.require(:email)
-
- groups = Group.lookup_groups(
- group_ids: params[:group_ids],
- group_names: params[:group_names]
- )
-
- guardian.ensure_can_invite_to_forum!(groups)
- group_ids = groups.map(&:id)
-
- if Invite.exists?(email: params[:email])
+ if params[:email].present? && Invite.exists?(email: params[:email])
return render json: failed_json, status: 422
end
+ if params[:topic_id].present?
+ topic = Topic.find_by(id: params[:topic_id])
+ raise Discourse::InvalidParameters.new(:topic_id) if topic.blank?
+ guardian.ensure_can_invite_to!(topic)
+ end
+
+ if params[:group_ids].present? || params[:group_names].present?
+ groups = Group.lookup_groups(group_ids: params[:group_ids], group_names: params[:group_names])
+ end
+
+ guardian.ensure_can_invite_to_forum!(groups)
+
begin
- if Invite.invite_by_email(params[:email], current_user, nil, group_ids, params[:custom_message])
- render json: success_json
+ invite = Invite.generate(current_user,
+ invite_key: params[:invite_key],
+ email: params[:email],
+ skip_email: params[:skip_email],
+ invited_by: current_user,
+ custom_message: params[:custom_message],
+ max_redemptions_allowed: params[:max_redemptions_allowed],
+ topic_id: topic&.id,
+ group_ids: groups&.map(&:id),
+ expires_at: params[:expires_at],
+ )
+
+ if invite.present?
+ render_serialized(invite, InviteSerializer, scope: guardian, root: nil, show_emails: params.has_key?(:email))
else
render json: failed_json, status: 422
end
@@ -109,57 +77,53 @@ class InvitesController < ApplicationController
end
end
- def create_invite_link
- params.permit(:email, :max_redemptions_allowed, :expires_at, :group_ids, :group_names, :topic_id)
+ def update
+ invite = Invite.find_by(invited_by: current_user, id: params[:id])
+ raise Discourse::InvalidParameters.new(:id) if invite.blank?
- is_single_invite = params[:email].present?
- unless is_single_invite
- guardian.ensure_can_send_invite_links!(current_user)
+ if params[:topic_id].present?
+ topic = Topic.find_by(id: params[:topic_id])
+ raise Discourse::InvalidParameters.new(:topic_id) if topic.blank?
+ guardian.ensure_can_invite_to!(topic)
end
- groups = Group.lookup_groups(
- group_ids: params[:group_ids],
- group_names: params[:group_names]
- )
- if !guardian.can_invite_to_forum?(groups)
- raise StandardError.new I18n.t("invite.cant_invite_to_group")
+ if params[:group_ids].present? || params[:group_names].present?
+ groups = Group.lookup_groups(group_ids: params[:group_ids], group_names: params[:group_names])
end
- group_ids = groups.map(&:id)
- if is_single_invite
- invite_exists = Invite.exists?(email: params[:email], invited_by_id: current_user.id)
- if invite_exists && !guardian.can_send_multiple_invites?(current_user)
- return render json: failed_json, status: 422
+ guardian.ensure_can_invite_to_forum!(groups)
+
+ Invite.transaction do
+ if params.has_key?(:topic_id)
+ invite.topic_invites.destroy_all
+ invite.topic_invites.create!(topic_id: topic.id) if topic.present?
end
- if params[:topic_id].present?
- topic = Topic.find_by(id: params[:topic_id])
+ if params.has_key?(:group_ids) || params.has_key?(:group_names)
+ invite.invited_groups.destroy_all
+ groups.each { |group| invite.invited_groups.find_or_create_by!(group_id: group.id) } if groups.present?
+ end
- if topic.present?
- guardian.ensure_can_invite_to!(topic)
- else
- raise Discourse::InvalidParameters.new(:topic_id)
+ if params.has_key?(:email)
+ old_email = invite.email.presence
+ new_email = params[:email].presence
+
+ if old_email != new_email
+ invite.emailed_status = Invite.emailed_status_types[new_email ? :pending : :not_required]
end
+
+ invite.email = new_email
end
+
+ invite.update!(params.permit(:custom_message, :max_redemptions_allowed, :expires_at))
end
- invite_link = if is_single_invite
- Invite.generate_single_use_invite_link(params[:email], current_user, topic, group_ids)
- else
- Invite.generate_multiple_use_invite_link(
- invited_by: current_user,
- max_redemptions_allowed: params[:max_redemptions_allowed],
- expires_at: params[:expires_at],
- group_ids: group_ids
- )
+ if invite.emailed_status == Invite.emailed_status_types[:pending]
+ invite.update_column(:emailed_status, Invite.emailed_status_types[:sending])
+ Jobs.enqueue(:invite_email, invite_id: invite.id)
end
- if invite_link.present?
- render_json_dump(invite_link)
- else
- render json: failed_json, status: 422
- end
- rescue => e
- render json: { errors: [e.message] }, status: 422
+
+ render_serialized(invite, InviteSerializer, scope: guardian, root: nil, show_emails: params.has_key?(:email))
end
def destroy
@@ -167,15 +131,66 @@ class InvitesController < ApplicationController
invite = Invite.find_by(invited_by_id: current_user.id, id: params[:id])
raise Discourse::InvalidParameters.new(:id) if invite.blank?
+
invite.trash!(current_user)
render json: success_json
end
- def rescind_all_invites
- guardian.ensure_can_rescind_all_invites!(current_user)
+ def perform_accept_invitation
+ params.require(:id)
+ params.permit(:email, :username, :name, :password, :timezone, user_custom_fields: {})
+
+ invite = Invite.find_by(invite_key: params[:id])
+
+ if invite.present?
+ begin
+ user = invite.redeem(
+ email: invite.is_invite_link? ? params[:email] : invite.email,
+ username: params[:username],
+ name: params[:name],
+ password: params[:password],
+ user_custom_fields: params[:user_custom_fields],
+ ip_address: request.remote_ip
+ )
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
+ return render json: failed_json.merge(errors: e.record&.errors&.to_hash, message: I18n.t('invite.error_message')), status: 412
+ rescue Invite::UserExists => e
+ return render json: failed_json.merge(message: e.message), status: 412
+ end
+
+ if user.blank?
+ return render json: failed_json.merge(message: I18n.t('invite.not_found_json')), status: 404
+ end
+
+ log_on_user(user) if user.active?
+ user.update_timezone_if_missing(params[:timezone])
+ post_process_invite(user)
+
+ topic = invite.topics.first
+ response = {}
+
+ if user.present? && user.active?
+ response[:redirect_to] = topic.present? ? path(topic.relative_url) : path("/")
+ elsif user.present?
+ response[:message] = I18n.t('invite.confirm_email')
+ cookies[:destination_url] = path(topic.relative_url) if topic.present?
+ end
+
+ render json: success_json.merge(response)
+ else
+ render json: failed_json.merge(message: I18n.t('invite.not_found_json')), status: 404
+ end
+ end
+
+ def destroy_all_expired
+ guardian.ensure_can_destroy_all_invites!(current_user)
+
+ Invite
+ .where(invited_by: current_user)
+ .where('expires_at < ?', Time.zone.now)
+ .find_each { |invite| invite.trash!(current_user) }
- Invite.rescind_all_expired_invites_from(current_user)
render json: success_json
end
@@ -195,7 +210,14 @@ class InvitesController < ApplicationController
def resend_all_invites
guardian.ensure_can_resend_all_invites!(current_user)
- Invite.resend_all_invites_from(current_user.id)
+ Invite
+ .left_outer_joins(:invited_users)
+ .where(invited_by: current_user)
+ .where('invites.email IS NOT NULL')
+ .where('invited_users.user_id IS NULL')
+ .group('invites.id')
+ .find_each { |invite| invite.resend_invite }
+
render json: success_json
end
@@ -233,15 +255,7 @@ class InvitesController < ApplicationController
end
end
- def fetch_username
- params.require(:username)
- params[:username]
- end
-
- def fetch_email
- params.require(:email)
- params[:email]
- end
+ private
def ensure_new_registrations_allowed
unless SiteSetting.allow_new_registrations
@@ -259,8 +273,6 @@ class InvitesController < ApplicationController
end
end
- private
-
def post_process_invite(user)
user.enqueue_welcome_message('welcome_invite') if user.send_welcome_message
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 35397b8084b..cf2ddc4bf73 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -11,7 +11,7 @@ class UsersController < ApplicationController
:update_second_factor, :create_second_factor_backup, :select_avatar,
:notification_level, :revoke_auth_token, :register_second_factor_security_key,
:create_second_factor_security_key, :feature_topic, :clear_featured_topic,
- :bookmarks, :invited, :invite_links, :check_sso_email, :check_sso_payload
+ :bookmarks, :invited, :check_sso_email, :check_sso_payload
]
skip_before_action :check_xhr, only: [
@@ -402,18 +402,19 @@ class UsersController < ApplicationController
def invited
if guardian.can_invite_to_forum?
- offset = params[:offset].to_i || 0
- filter_by = params[:filter] || "redeemed"
+ filter = params[:filter] || "redeemed"
inviter = fetch_user_from_params(include_inactive: current_user.staff? || SiteSetting.show_inactive_accounts)
- invites = if guardian.can_see_invite_details?(inviter) && filter_by == "pending"
- Invite.find_pending_invites_from(inviter, offset)
- elsif filter_by == "redeemed"
- Invite.find_redeemed_invites_from(inviter, offset)
+ invites = if filter == "pending" && guardian.can_see_invite_details?(inviter)
+ Invite.includes(:topics, :groups).pending(inviter)
+ elsif filter == "redeemed"
+ Invite.redeemed_users(inviter)
else
- []
+ Invite.none
end
+ invites = invites.offset(params[:offset].to_i || 0).limit(SiteSetting.invites_per_page)
+
show_emails = guardian.can_see_invite_emails?(inviter)
if params[:search].present? && invites.present?
filter_sql = '(LOWER(users.username) LIKE :filter)'
@@ -421,66 +422,34 @@ class UsersController < ApplicationController
invites = invites.where(filter_sql, filter: "%#{params[:search].downcase}%")
end
+ pending_count = Invite.pending(inviter).reorder(nil).count.to_i
+ redeemed_count = Invite.redeemed_users(inviter).reorder(nil).count.to_i
+
render json: MultiJson.dump(InvitedSerializer.new(
- OpenStruct.new(invite_list: invites.to_a, show_emails: show_emails, inviter: inviter, type: filter_by),
+ OpenStruct.new(
+ invite_list: invites.to_a,
+ show_emails: show_emails,
+ inviter: inviter,
+ type: filter,
+ counts: {
+ pending: pending_count,
+ redeemed: redeemed_count,
+ total: pending_count + redeemed_count
+ }
+ ),
scope: guardian,
root: false
))
- else
- if current_user&.staff?
- message = if SiteSetting.enable_discourse_connect
- I18n.t("invite.disabled_errors.discourse_connect_enabled")
- elsif !SiteSetting.enable_local_logins
- I18n.t("invite.disabled_errors.local_logins_disabled")
- end
-
- render_invite_error(message)
- else
- render_json_error(I18n.t("invite.disabled_errors.invalid_access"))
+ elsif current_user&.staff?
+ message = if SiteSetting.enable_discourse_connect
+ I18n.t("invite.disabled_errors.discourse_connect_enabled")
+ elsif !SiteSetting.enable_local_logins
+ I18n.t("invite.disabled_errors.local_logins_disabled")
end
- end
- end
- def invite_links
- if guardian.can_invite_to_forum?
- inviter = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts))
- guardian.ensure_can_see_invite_details!(inviter)
-
- offset = params[:offset].to_i || 0
- invites = Invite.find_links_invites_from(inviter, offset)
-
- render json: MultiJson.dump(invites: serialize_data(invites.to_a, InviteLinkSerializer), can_see_invite_details: guardian.can_see_invite_details?(inviter))
+ render_invite_error(message)
else
- if current_user&.staff?
- message = if SiteSetting.enable_discourse_connect
- I18n.t("invite.disabled_errors.discourse_connect_enabled")
- elsif !SiteSetting.enable_local_logins
- I18n.t("invite.disabled_errors.local_logins_disabled")
- end
-
- render_invite_error(message)
- else
- render_json_error(I18n.t("invite.disabled_errors.invalid_access"))
- end
- end
- end
-
- def invited_count
- if guardian.can_invite_to_forum?
- inviter = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts))
-
- pending_count = Invite.find_pending_invites_count(inviter)
- redeemed_count = Invite.find_redeemed_invites_count(inviter)
- links_count = Invite.find_links_invites_count(inviter)
-
- render json: { counts: { pending: pending_count, redeemed: redeemed_count, links: links_count,
- total: (pending_count.to_i + redeemed_count.to_i) } }
- else
- if current_user&.staff?
- render json: { counts: 0 }
- else
- render_json_error(I18n.t("invite.disabled_errors.invalid_access"))
- end
+ render_json_error(I18n.t("invite.disabled_errors.invalid_access"))
end
end
diff --git a/app/jobs/regular/bulk_invite.rb b/app/jobs/regular/bulk_invite.rb
index a87344f079e..2965abc4067 100644
--- a/app/jobs/regular/bulk_invite.rb
+++ b/app/jobs/regular/bulk_invite.rb
@@ -107,13 +107,14 @@ module Jobs
end
else
if @total_invites > Invite::BULK_INVITE_EMAIL_LIMIT
- invite = Invite.create_invite_by_email(email, @current_user,
+ invite = Invite.generate(@current_user,
+ email: email,
topic: topic,
group_ids: groups.map(&:id),
emailed_status: Invite.emailed_status_types[:bulk_pending]
)
else
- Invite.invite_by_email(email, @current_user, topic, groups.map(&:id))
+ Invite.generate(@current_user, email: email, topic: topic, group_ids: groups.map(&:id))
end
end
rescue => e
diff --git a/app/models/invite.rb b/app/models/invite.rb
index 491189202e5..f33eeb667be 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -2,6 +2,7 @@
class Invite < ActiveRecord::Base
class UserExists < StandardError; end
+
include RateLimiter::OnCreateRecord
include Trashable
@@ -25,8 +26,12 @@ class Invite < ActiveRecord::Base
has_many :groups, through: :invited_groups
has_many :topic_invites
has_many :topics, through: :topic_invites, source: :topic
+
validates_presence_of :invited_by_id
validates :email, email: true, allow_blank: true
+ validate :ensure_max_redemptions_allowed
+ validate :user_doesnt_already_exist
+ validate :ensure_no_invalid_email_invites
before_create do
self.invite_key ||= SecureRandom.hex
@@ -37,14 +42,8 @@ class Invite < ActiveRecord::Base
self.email = Email.downcase(email) unless email.nil?
end
- validate :ensure_max_redemptions_allowed
- validate :user_doesnt_already_exist
- validate :ensure_no_invalid_email_invites
attr_accessor :email_already_exists
- scope :single_use_invites, -> { where('invites.max_redemptions_allowed = 1') }
- scope :multiple_use_invites, -> { where('invites.max_redemptions_allowed > 1') }
-
def self.emailed_status_types
@emailed_status_types ||= Enum.new(not_required: 0, pending: 1, bulk_pending: 2, sending: 3, sent: 4)
end
@@ -76,228 +75,127 @@ class Invite < ActiveRecord::Base
expires_at < Time.zone.now
end
- # link_valid? indicates whether the invite link can be used to log in to the site
+ def link
+ "#{Discourse.base_url}/invites/#{invite_key}"
+ end
+
def link_valid?
invalidated_at.nil?
end
- def redeem(username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil)
- if !expired? && !destroyed? && link_valid?
- InviteRedeemer.new(invite: self, email: self.email, username: username, name: name, password: password, user_custom_fields: user_custom_fields, ip_address: ip_address).redeem
- end
- end
-
- def self.invite_by_email(email, invited_by, topic = nil, group_ids = nil, custom_message = nil)
- create_invite_by_email(email, invited_by,
- topic: topic,
- group_ids: group_ids,
- custom_message: custom_message,
- emailed_status: emailed_status_types[:pending]
- )
- end
-
- def self.generate_single_use_invite_link(email, invited_by, topic = nil, group_ids = nil)
- invite = create_invite_by_email(email, invited_by,
- topic: topic,
- group_ids: group_ids,
- emailed_status: emailed_status_types[:not_required]
- )
-
- "#{Discourse.base_url}/invites/#{invite.invite_key}" if invite
- end
-
- # Create an invite for a user, supplying an optional topic
- #
- # Return the previously existing invite if already exists. Returns nil if the invite can't be created.
- def self.create_invite_by_email(email, invited_by, opts = nil)
+ def self.generate(invited_by, opts = nil)
opts ||= {}
- topic = opts[:topic]
- group_ids = opts[:group_ids]
- custom_message = opts[:custom_message]
- emailed_status = opts[:emailed_status] || emailed_status_types[:pending]
- lower_email = Email.downcase(email)
+ email = Email.downcase(opts[:email]) if opts[:email].present?
- if user = find_user_by_email(lower_email)
- raise UserExists.new(I18n.t("invite.user_exists",
- email: lower_email,
+ if user = find_user_by_email(email)
+ raise UserExists.new(I18n.t(
+ "invite.user_exists",
+ email: email,
username: user.username,
base_path: Discourse.base_path
))
end
- invite = Invite.with_deleted
- .where(email: lower_email, invited_by_id: invited_by.id)
- .order('created_at DESC')
- .first
+ if email.present?
+ invite = Invite
+ .with_deleted
+ .where(email: email, invited_by_id: invited_by.id)
+ .order('created_at DESC')
+ .first
- if invite && (invite.expired? || invite.deleted_at)
- invite.destroy
- invite = nil
+ if invite && (invite.expired? || invite.deleted_at)
+ invite.destroy
+ invite = nil
+ end
+ end
+
+ emailed_status = if opts[:skip_email] || invite&.emailed_status == emailed_status_types[:not_required]
+ emailed_status_types[:not_required]
+ elsif opts[:emailed_status].present?
+ opts[:emailed_status]
+ elsif email.present?
+ emailed_status_types[:pending]
+ else
+ emailed_status_types[:not_required]
end
if invite
- if invite.emailed_status == Invite.emailed_status_types[:not_required]
- emailed_status = invite.emailed_status
- end
-
invite.update_columns(
created_at: Time.zone.now,
updated_at: Time.zone.now,
- expires_at: SiteSetting.invite_expiry_days.days.from_now,
+ expires_at: opts[:expires_at] || SiteSetting.invite_expiry_days.days.from_now,
emailed_status: emailed_status
)
else
- create_args = {
- invited_by: invited_by,
- email: lower_email,
- emailed_status: emailed_status
- }
+ create_args = opts.slice(:invite_key, :email, :moderator, :custom_message, :max_redemptions_allowed)
+ create_args[:invited_by] = invited_by
+ create_args[:email] = email
+ create_args[:emailed_status] = emailed_status
+ create_args[:expires_at] = opts[:expires_at] || SiteSetting.invite_expiry_days.days.from_now
- create_args[:moderator] = true if opts[:moderator]
- create_args[:custom_message] = custom_message if custom_message
invite = Invite.create!(create_args)
end
- if topic && !invite.topic_invites.pluck(:topic_id).include?(topic.id)
- invite.topic_invites.create!(invite_id: invite.id, topic_id: topic.id)
- # to correct association
- topic.reload
+ topic_id = opts[:topic]&.id || opts[:topic_id]
+ if topic_id.present?
+ invite.topic_invites.find_or_create_by!(topic_id: topic_id)
end
+ group_ids = opts[:group_ids]
if group_ids.present?
- group_ids = group_ids - invite.invited_groups.pluck(:group_id)
-
group_ids.each do |group_id|
- invite.invited_groups.create!(group_id: group_id)
+ invite.invited_groups.find_or_create_by!(group_id: group_id)
end
end
if emailed_status == emailed_status_types[:pending]
- invite.update_column(:emailed_status, Invite.emailed_status_types[:sending])
+ invite.update_column(:emailed_status, emailed_status_types[:sending])
Jobs.enqueue(:invite_email, invite_id: invite.id)
end
invite.reload
+ end
+
+ def redeem(email: nil, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil)
+ if !expired? && !destroyed? && link_valid?
+ raise UserExists.new I18n.t("invite_link.email_taken") if is_invite_link? && UserEmail.exists?(email: email)
+ email = self.email if email.blank? && !is_invite_link?
+ InviteRedeemer.new(invite: self, email: email, username: username, name: name, password: password, user_custom_fields: user_custom_fields, ip_address: ip_address).redeem
+ end
+ end
+
+ def self.redeem_from_email(email)
+ invite = Invite.find_by(email: Email.downcase(email))
+ InviteRedeemer.new(invite: invite, email: invite.email).redeem if invite
invite
end
- def self.generate_multiple_use_invite_link(invited_by:, max_redemptions_allowed: 5, expires_at: 1.month.from_now, group_ids: nil)
- Invite.transaction do
- create_args = {
- invited_by: invited_by,
- max_redemptions_allowed: max_redemptions_allowed.to_i,
- expires_at: expires_at,
- emailed_status: emailed_status_types[:not_required]
- }
- invite = Invite.create!(create_args)
-
- if group_ids.present?
- now = Time.zone.now
- invited_groups = group_ids.map { |group_id| { group_id: group_id, invite_id: invite.id, created_at: now, updated_at: now } }
- InvitedGroup.insert_all(invited_groups)
- end
-
- "#{Discourse.base_url}/invites/#{invite.invite_key}"
- end
- end
-
- # redeem multiple use invite link
- def redeem_invite_link(email: nil, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil)
- DistributedMutex.synchronize("redeem_invite_link_#{self.id}") do
- reload
- if is_invite_link? && !expired? && !redeemed? && !destroyed? && link_valid?
- raise UserExists.new I18n.t("invite_link.email_taken") if UserEmail.exists?(email: email)
- InviteRedeemer.new(invite: self, email: email, username: username, name: name, password: password, user_custom_fields: user_custom_fields, ip_address: ip_address).redeem
- end
- end
- end
-
def self.find_user_by_email(email)
User.with_email(Email.downcase(email)).where(staged: false).first
end
- def self.get_group_ids(group_names)
- group_ids = []
- if group_names
- group_names = group_names.split(',')
- group_names.each { |group_name|
- group_detail = Group.find_by_name(group_name)
- group_ids.push(group_detail.id) if group_detail
- }
- end
- group_ids
- end
-
- def self.find_all_pending_invites_from(inviter, offset = 0, limit = SiteSetting.invites_per_page)
- Invite.single_use_invites
+ def self.pending(inviter)
+ Invite.distinct
.joins("LEFT JOIN invited_users ON invites.id = invited_users.invite_id")
.joins("LEFT JOIN users ON invited_users.user_id = users.id")
- .where('invited_users.user_id IS NULL')
.where(invited_by_id: inviter.id)
- .where('invites.email IS NOT NULL')
+ .where('redemption_count < max_redemptions_allowed')
.order('invites.updated_at DESC')
- .limit(limit)
- .offset(offset)
end
- def self.find_pending_invites_from(inviter, offset = 0)
- find_all_pending_invites_from(inviter, offset)
- end
-
- def self.find_pending_invites_count(inviter)
- find_all_pending_invites_from(inviter, 0, nil).reorder(nil).count
- end
-
- def self.find_all_redeemed_invites_from(inviter, offset = 0, limit = SiteSetting.invites_per_page)
- InvitedUser.includes(:invite)
+ def self.redeemed_users(inviter)
+ InvitedUser
+ .includes(:invite)
.includes(user: :user_stat)
.where('invited_users.user_id IS NOT NULL')
.where('invites.invited_by_id = ?', inviter.id)
.order('user_stats.time_read DESC, invited_users.redeemed_at DESC')
- .limit(limit)
- .offset(offset)
.references('invite')
.references('user')
.references('user_stat')
end
- def self.find_redeemed_invites_from(inviter, offset = 0)
- find_all_redeemed_invites_from(inviter, offset)
- end
-
- def self.find_redeemed_invites_count(inviter)
- find_all_redeemed_invites_from(inviter, 0, nil).reorder(nil).count
- end
-
- def self.find_all_links_invites_from(inviter, offset = 0, limit = SiteSetting.invites_per_page)
- Invite.multiple_use_invites
- .includes(invited_groups: :group)
- .where(invited_by_id: inviter.id)
- .order('invites.updated_at DESC')
- .limit(limit)
- .offset(offset)
- end
-
- def self.find_links_invites_from(inviter, offset = 0)
- find_all_links_invites_from(inviter, offset)
- end
-
- def self.find_links_invites_count(inviter)
- find_all_links_invites_from(inviter, 0, nil).reorder(nil).count
- end
-
- def self.filter_by(email_or_username)
- if email_or_username
- where(
- '(LOWER(invites.email) LIKE :filter) or (LOWER(users.username) LIKE :filter)',
- filter: "%#{email_or_username.downcase}%"
- )
- else
- all
- end
- end
-
def self.invalidate_for_email(email)
i = Invite.find_by(email: Email.downcase(email))
if i
@@ -307,38 +205,11 @@ class Invite < ActiveRecord::Base
i
end
- def self.redeem_from_email(email)
- invite = Invite.single_use_invites.find_by(email: Email.downcase(email))
- InviteRedeemer.new(invite: invite, email: invite.email).redeem if invite
- invite
- end
-
def resend_invite
self.update_columns(updated_at: Time.zone.now, invalidated_at: nil, expires_at: SiteSetting.invite_expiry_days.days.from_now)
Jobs.enqueue(:invite_email, invite_id: self.id)
end
- def self.resend_all_invites_from(user_id)
- Invite.single_use_invites
- .left_outer_joins(:invited_users)
- .where('invited_users.user_id IS NULL AND invites.email IS NOT NULL AND invited_by_id = ?', user_id)
- .group('invites.id')
- .find_each do |invite|
- invite.resend_invite
- end
- end
-
- def self.rescind_all_expired_invites_from(user)
- Invite.single_use_invites
- .includes(:invited_users)
- .where('invited_users.user_id IS NULL AND invites.email IS NOT NULL AND invited_by_id = ? AND invites.expires_at < ?',
- user.id, Time.zone.now)
- .references('invited_users')
- .find_each do |invite|
- invite.trash!(user)
- end
- end
-
def limit_invites_per_day
RateLimiter.new(invited_by, "invites-per-day", SiteSetting.max_invites_per_day, 1.day.to_i)
end
@@ -348,12 +219,10 @@ class Invite < ActiveRecord::Base
end
def ensure_max_redemptions_allowed
- if self.max_redemptions_allowed.nil? || self.max_redemptions_allowed == 1
- self.max_redemptions_allowed ||= 1
- else
- if !self.max_redemptions_allowed.between?(2, SiteSetting.invite_link_max_redemptions_limit)
- errors.add(:max_redemptions_allowed, I18n.t("invite_link.max_redemptions_limit", max_limit: SiteSetting.invite_link_max_redemptions_limit))
- end
+ if self.max_redemptions_allowed.nil?
+ self.max_redemptions_allowed = 1
+ elsif !self.max_redemptions_allowed.between?(1, SiteSetting.invite_link_max_redemptions_limit)
+ errors.add(:max_redemptions_allowed, I18n.t("invite_link.max_redemptions_limit", max_limit: SiteSetting.invite_link_max_redemptions_limit))
end
end
diff --git a/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb
index e95e0041395..3b9edabd213 100644
--- a/app/models/invite_redeemer.rb
+++ b/app/models/invite_redeemer.rb
@@ -110,7 +110,7 @@ InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_
def get_invited_user
result = get_existing_user
- result ||= InviteRedeemer.create_user_from_invite(invite: invite, email: email, username: username, name: name, password: password, user_custom_fields: user_custom_fields, ip_address: ip_address)
+ result ||= InviteRedeemer.create_user_from_invite(email: email, invite: invite, username: username, name: name, password: password, user_custom_fields: user_custom_fields, ip_address: ip_address)
result.send_welcome_message = false
result
end
@@ -164,7 +164,8 @@ InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_
end
def delete_duplicate_invites
- Invite.single_use_invites
+ Invite
+ .where('invites.max_redemptions_allowed = 1')
.joins("LEFT JOIN invited_users ON invites.id = invited_users.invite_id")
.where('invited_users.user_id IS NULL')
.where('invites.email = ? AND invites.id != ?', email, invite.id)
diff --git a/app/models/topic.rb b/app/models/topic.rb
index 2ad0fccdc37..dd9d2871d14 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -1055,8 +1055,11 @@ class Topic < ActiveRecord::Base
!!invite_to_topic(invited_by, target_user, group_ids, guardian)
end
elsif is_email && guardian.can_invite_via_email?(self)
- !!Invite.invite_by_email(
- username_or_email, invited_by, self, group_ids, custom_message
+ !!Invite.generate(invited_by,
+ email: username_or_email,
+ topic: self,
+ group_ids: group_ids,
+ custom_message: custom_message
)
end
end
diff --git a/app/serializers/invite_serializer.rb b/app/serializers/invite_serializer.rb
index f66f1c02c7c..460273161ef 100644
--- a/app/serializers/invite_serializer.rb
+++ b/app/serializers/invite_serializer.rb
@@ -1,7 +1,18 @@
# frozen_string_literal: true
class InviteSerializer < ApplicationSerializer
- attributes :id, :email, :updated_at, :expired
+ attributes :id,
+ :link,
+ :email,
+ :redemption_count,
+ :max_redemptions_allowed,
+ :custom_message,
+ :updated_at,
+ :expires_at,
+ :expired
+
+ has_many :topics, embed: :object, serializer: BasicTopicSerializer
+ has_many :groups, embed: :object, serializer: BasicGroupSerializer
def include_email?
options[:show_emails] && !object.redeemed?
diff --git a/app/serializers/invited_serializer.rb b/app/serializers/invited_serializer.rb
index 68ced8ba108..e31fd0f08c9 100644
--- a/app/serializers/invited_serializer.rb
+++ b/app/serializers/invited_serializer.rb
@@ -1,18 +1,12 @@
# frozen_string_literal: true
class InvitedSerializer < ApplicationSerializer
- attributes :invites, :can_see_invite_details
+ attributes :invites, :can_see_invite_details, :counts
def invites
- serializer = if object.type == "pending"
- InviteSerializer
- else
- InvitedUserSerializer
- end
-
ActiveModel::ArraySerializer.new(
object.invite_list,
- each_serializer: serializer,
+ each_serializer: object.type == "pending" ? InviteSerializer : InvitedUserSerializer,
scope: scope,
root: false,
show_emails: object.show_emails
@@ -23,7 +17,7 @@ class InvitedSerializer < ApplicationSerializer
scope.can_see_invite_details?(object.inviter)
end
- def read_attribute_for_serialization(attr)
- object.respond_to?(attr) ? object.public_send(attr) : public_send(attr)
+ def counts
+ object.counts
end
end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 0fe379cd7f0..4d3f9bdd418 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1439,47 +1439,44 @@ en:
notification_level_when_replying: "When I post in a topic, set that topic to"
invited:
- search: "type to search invites..."
title: "Invites"
- user: "Invited User"
+ pending_tab: "Pending"
+ pending_tab_with_count: "Pending (%{count})"
+ redeemed_tab: "Redeemed"
+ redeemed_tab_with_count: "Redeemed (%{count})"
+ invited_via: "Invited Via"
+ invited_via_link: "link (%{count} / %{max} redeemed)"
+ groups: "Groups"
sent: "Last Sent"
+ expires_at: "Expires"
+ edit: "Edit"
+ remove: "Remove"
+ copy_link: "Get Link"
+ reinvite: "Send Email"
+ reinvited: "Invite re-sent"
+ removed: "Removed"
+ search: "type to search invites..."
+ user: "Invited User"
none: "No invites to display."
truncated:
one: "Showing the first invite."
other: "Showing the first %{count} invites."
redeemed: "Redeemed Invites"
- redeemed_tab: "Redeemed"
- redeemed_tab_with_count: "Redeemed (%{count})"
redeemed_at: "Redeemed"
pending: "Pending Invites"
- pending_tab: "Pending"
- pending_tab_with_count: "Pending (%{count})"
topics_entered: "Topics Viewed"
posts_read_count: "Posts Read"
expired: "This invite has expired."
- rescind: "Remove"
- rescinded: "Invite removed"
- rescind_all: "Remove Expired Invites"
- rescinded_all: "All Expired Invites removed!"
- rescind_all_confirm: "Are you sure you want to remove all expired invites?"
- reinvite: "Resend Invite"
- reinvite_all: "Resend all Invites"
+ remove_all: "Remove Expired Invites"
+ removed_all: "All Expired Invites removed!"
+ remove_all_confirm: "Are you sure you want to remove all expired invites?"
+ reinvite_all: "Resend All Invites"
reinvite_all_confirm: "Are you sure you want to resend all invites?"
- reinvited: "Invite re-sent"
- reinvited_all: "All Invites re-sent!"
+ reinvited_all: "All Invites Sent!"
time_read: "Read Time"
days_visited: "Days Visited"
account_age_days: "Account age in days"
- source: "Invited Via"
- links_tab: "Links"
- links_tab_with_count: "Links (%{count})"
- link_url: "Link"
- link_created_at: "Created"
- link_redemption_stats: "Redemptions"
- link_groups: Groups
- link_expires_at: Expires
create: "Invite"
- copy_link: "Show Link"
generate_link: "Create Invite Link"
link_generated: "Here's your invite link!"
valid_for: "Invite link is only valid for this email address: %{email}"
@@ -1491,12 +1488,47 @@ en:
error: "There was an error generating Invite link"
max_redemptions_allowed_label: "How many people are allowed to register using this link?"
expires_at: "When will this invite link expire?"
+
+ invite:
+ new_title: "Create Invite"
+ edit_title: "Edit Invite"
+ show_link: "Invite Link"
+
+ instructions: "Share this link to instantly grant access to this site:"
+ copy_link: "copy link"
+ expires_at_time: "Your invite expires in %{time}."
+
+ show_advanced: "Show Advanced Options"
+ hide_advanced: "Hide Advanced Options"
+
+ type_email: "Automatically send invitation link via email"
+ type_link: "Manually share an invite link to people"
+
+ email: "Email address of invited person:"
+ max_redemptions_allowed: "Number of times the invite can be used before expiring:"
+
+ add_to_groups: "Include invited people to groups:"
+ invite_to_topic: "Invite people to topic:"
+ expires_at: "Set an expiration date for invite:"
+ custom_message: "Personalize your invites by adding a custom message:"
+
+ send_invite_email: "Send Invite Email"
+ save_invite: "Save Invite"
+
+ invite_saved: "Invite was saved."
+
bulk_invite:
none: "No invitations to display on this page."
+
text: "Bulk Invite"
+ instructions: |
+
Invite a list of users to get your community going quickly. Prepare a CSV file containing at least one row per email address of users you want to invite. The following comma separated information can be provided if you want to add people to groups or send them to a specific topic the first time they sign in.
+
john@smith.com,first_group_name;second_group_name,42
+
Every email address in your uploaded CSV file will be sent an invitation, and you will be able to manage it later.
+
+ progress: "Uploaded %{progress}%..."
success: "File uploaded successfully, you will be notified via message when the process is complete."
error: "Sorry, file should be CSV format."
- confirmation_message: "You’re about to email invites to everyone in the uploaded file."
password:
title: "Password"
diff --git a/config/routes.rb b/config/routes.rb
index 3801d5455d1..db8379d1d02 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -478,9 +478,7 @@ Discourse::Application.routes.draw do
get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/notification_level" => "users#notification_level", 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 }
- get "#{root_path}/:username/invite_links" => "users#invite_links", constraints: { username: RouteFormat.username }
post "#{root_path}/action/send_activation_email" => "users#send_activation_email"
get "#{root_path}/:username/summary" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed", format: :rss, constraints: { username: RouteFormat.username }
@@ -822,12 +820,12 @@ Discourse::Application.routes.draw do
resources :invites, except: [:show]
get "/invites/:id" => "invites#show", constraints: { format: :html }
+ put "/invites/:id" => "invites#update"
post "invites/upload_csv" => "invites#upload_csv"
- post "invites/rescind-all" => "invites#rescind_all_invites"
+ post "invites/destroy-all-expired" => "invites#destroy_all_expired"
post "invites/reinvite" => "invites#resend_invite"
post "invites/reinvite-all" => "invites#resend_all_invites"
- post "invites/link" => "invites#create_invite_link"
delete "invites" => "invites#destroy"
put "invites/show/:id" => "invites#perform_accept_invitation", as: 'perform_accept_invite'
diff --git a/lib/guardian.rb b/lib/guardian.rb
index 81b6084a446..779148222a2 100644
--- a/lib/guardian.rb
+++ b/lib/guardian.rb
@@ -400,19 +400,11 @@ class Guardian
SiteSetting.enable_local_logins
end
- def can_send_invite_links?(user)
- user.staff?
- end
-
- def can_send_multiple_invites?(user)
- user.staff?
- end
-
def can_resend_all_invites?(user)
user.staff?
end
- def can_rescind_all_invites?(user)
+ def can_destroy_all_invites?(user)
user.staff?
end
diff --git a/lib/wizard/builder.rb b/lib/wizard/builder.rb
index 014e514a4e0..4dfee9af9c8 100644
--- a/lib/wizard/builder.rb
+++ b/lib/wizard/builder.rb
@@ -276,10 +276,10 @@ class Wizard
users = JSON.parse(updater.fields[:invite_list])
users.each do |u|
- args = {}
+ args = { email: u['email'] }
args[:moderator] = true if u['role'] == 'moderator'
begin
- Invite.create_invite_by_email(u['email'], @wizard.user, args)
+ Invite.generate(@wizard.user, args)
rescue => e
updater.errors.add(:invite_list, e.message.concat("
"))
end
diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb
index 07325d79829..f7168d3e833 100644
--- a/spec/models/invite_spec.rb
+++ b/spec/models/invite_spec.rb
@@ -82,29 +82,28 @@ describe Invite do
context 'email' do
it 'enqueues a job to email the invite' do
expect do
- Invite.invite_by_email(iceking, inviter, topic)
+ Invite.generate(inviter, email: iceking, topic: topic)
end.to change { Jobs::InviteEmail.jobs.size }
end
end
context 'links' do
it 'does not enqueue a job to email the invite' do
- expect do
- Invite.generate_single_use_invite_link(iceking, inviter, topic)
- end.not_to change { Jobs::InviteEmail.jobs.size }
+ expect { Invite.generate(inviter, email: iceking, topic: topic, skip_email: true) }
+ .not_to change { Jobs::InviteEmail.jobs.size }
end
end
context 'destroyed' do
it "can invite the same user after their invite was destroyed" do
- Invite.invite_by_email(iceking, inviter, topic).destroy!
- invite = Invite.invite_by_email(iceking, inviter, topic)
+ Invite.generate(inviter, email: iceking, topic: topic).destroy!
+ invite = Invite.generate(inviter, email: iceking, topic: topic)
expect(invite).to be_present
end
end
context 'after created' do
- let(:invite) { Invite.invite_by_email(iceking, inviter, topic) }
+ let(:invite) { Invite.generate(inviter, email: iceking, topic: topic) }
it 'belongs to the topic' do
expect(topic.invites).to eq([invite])
@@ -115,7 +114,7 @@ describe Invite do
fab!(:coding_horror) { Fabricate(:coding_horror) }
let(:new_invite) do
- Invite.invite_by_email(iceking, coding_horror, topic)
+ Invite.generate(coding_horror, email: iceking, topic: topic)
end
it 'returns a different invite' do
@@ -132,9 +131,7 @@ describe Invite do
iceking@ADVENTURETIME.ooo
ICEKING@adventuretime.ooo
}.each do |email|
- expect(Invite.invite_by_email(
- email, inviter, topic
- )).to eq(invite)
+ expect(Invite.generate(inviter, email: email, topic: topic)).to eq(invite)
end
end
@@ -142,9 +139,7 @@ describe Invite do
freeze_time
invite.update!(created_at: 10.days.ago)
- resend_invite = Invite.invite_by_email(
- 'iceking@adventuretime.ooo', inviter, topic
- )
+ resend_invite = Invite.generate(inviter, email: 'iceking@adventuretime.ooo', topic: topic)
expect(resend_invite.created_at).to eq_time(Time.zone.now)
end
@@ -153,10 +148,7 @@ describe Invite do
SiteSetting.invite_expiry_days = 1
invite.update!(expires_at: 2.days.ago)
- new_invite = Invite.invite_by_email(
- 'iceking@adventuretime.ooo', inviter, topic
- )
-
+ new_invite = Invite.generate(inviter, email: 'iceking@adventuretime.ooo', topic: topic)
expect(new_invite).not_to eq(invite)
expect(new_invite).not_to be_expired
end
@@ -166,7 +158,7 @@ describe Invite do
fab!(:another_topic) { Fabricate(:topic, user: topic.user) }
it 'should be the same invite' do
- new_invite = Invite.invite_by_email(iceking, inviter, another_topic)
+ new_invite = Invite.generate(inviter, email: iceking, topic: another_topic)
expect(new_invite).to eq(invite)
expect(another_topic.invites).to eq([invite])
expect(invite.topics).to match_array([topic, another_topic])
@@ -186,20 +178,20 @@ describe Invite do
it 'correctly marks invite emailed_status for email invites' do
expect(invite.emailed_status).to eq(Invite.emailed_status_types[:sending])
- Invite.invite_by_email(iceking, inviter, topic)
+ Invite.generate(inviter, email: iceking, topic: topic)
expect(invite.reload.emailed_status).to eq(Invite.emailed_status_types[:sending])
end
it 'does not mark emailed_status as sending after generating invite link' do
expect(invite.emailed_status).to eq(Invite.emailed_status_types[:sending])
- Invite.generate_single_use_invite_link(iceking, inviter, topic)
+ Invite.generate(inviter, email: iceking, topic: topic, emailed_status: Invite.emailed_status_types[:not_required])
expect(invite.reload.emailed_status).to eq(Invite.emailed_status_types[:not_required])
- Invite.invite_by_email(iceking, inviter, topic)
+ Invite.generate(inviter, email: iceking, topic: topic)
expect(invite.reload.emailed_status).to eq(Invite.emailed_status_types[:not_required])
- Invite.generate_single_use_invite_link(iceking, inviter, topic)
+ Invite.generate(inviter, email: iceking, topic: topic, emailed_status: Invite.emailed_status_types[:not_required])
expect(invite.reload.emailed_status).to eq(Invite.emailed_status_types[:not_required])
end
end
@@ -208,32 +200,23 @@ describe Invite do
context 'invite links' do
let(:inviter) { Fabricate(:user) }
- it 'with single use can exist' do
- Invite.generate_multiple_use_invite_link(invited_by: inviter, max_redemptions_allowed: 1)
- invite_link = Invite.last
- expect(invite_link.is_invite_link?).to eq(true)
- end
-
- it "has sane defaults" do
- Invite.generate_multiple_use_invite_link(invited_by: inviter)
- invite_link = Invite.last
- expect(invite_link.max_redemptions_allowed).to eq(5)
- expect(invite_link.expires_at.to_date).to eq(1.month.from_now.to_date)
- expect(invite_link.emailed_status).to eq(Invite.emailed_status_types[:not_required])
- expect(invite_link.is_invite_link?).to eq(true)
+ it "can be created" do
+ invite = Invite.generate(inviter, max_redemptions_allowed: 5)
+ expect(invite.max_redemptions_allowed).to eq(5)
+ expect(invite.expires_at.to_date).to eq(SiteSetting.invite_expiry_days.days.from_now.to_date)
+ expect(invite.emailed_status).to eq(Invite.emailed_status_types[:not_required])
+ expect(invite.is_invite_link?).to eq(true)
end
it 'checks for max_redemptions_allowed range' do
SiteSetting.invite_link_max_redemptions_limit = 1000
- expect do
- Invite.generate_multiple_use_invite_link(invited_by: inviter, max_redemptions_allowed: 1001)
- end.to raise_error(ActiveRecord::RecordInvalid)
+ expect { Invite.generate(inviter, max_redemptions_allowed: 1001) }
+ .to raise_error(ActiveRecord::RecordInvalid)
end
it 'does not enqueue a job to email the invite' do
- expect do
- Invite.generate_multiple_use_invite_link(invited_by: inviter)
- end.not_to change { Jobs::InviteEmail.jobs.size }
+ expect { Invite.generate(inviter) }
+ .not_to change { Jobs::InviteEmail.jobs.size }
end
end
end
@@ -242,17 +225,16 @@ describe Invite do
fab!(:topic) { Fabricate(:topic, category_id: nil, archetype: 'private_message') }
fab!(:coding_horror) { Fabricate(:coding_horror) }
- it "works" do
- expect do
- Invite.invite_by_email(coding_horror.email, topic.user, topic)
- end.to raise_error(Invite::UserExists)
+ it "raises the right error" do
+ expect { Invite.generate(topic.user, email: coding_horror.email, topic: topic) }
+ .to raise_error(Invite::UserExists)
end
end
context 'a staged user' do
it 'creates an invite for a staged user' do
Fabricate(:staged, email: 'staged@account.com')
- invite = Invite.invite_by_email('staged@account.com', Fabricate(:coding_horror))
+ invite = Invite.generate(Fabricate(:coding_horror), email: 'staged@account.com')
expect(invite).to be_valid
expect(invite.email).to eq('staged@account.com')
@@ -424,33 +406,28 @@ describe Invite do
fab!(:invite_link) { Fabricate(:invite, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required]) }
it 'works correctly' do
- user = invite_link.redeem_invite_link(email: 'foo@example.com')
+ user = invite_link.redeem(email: 'foo@example.com')
expect(user.is_a?(User)).to eq(true)
expect(user.send_welcome_message).to eq(true)
expect(user.trust_level).to eq(SiteSetting.default_invitee_trust_level)
expect(user.active).to eq(false)
- invite_link.reload
- expect(invite_link.redemption_count).to eq(1)
+ expect(invite_link.reload.redemption_count).to eq(1)
end
it 'returns error if user with that email already exists' do
user = Fabricate(:user)
- expect do
- invite_link.redeem_invite_link(email: user.email)
- end.to raise_error(Invite::UserExists)
+ expect { invite_link.redeem(email: user.email) }.to raise_error(Invite::UserExists)
end
end
end
- describe '.find_all_pending_invites_from' do
+ describe '.pending' do
context 'with user that has invited' do
it 'returns invites' do
inviter = Fabricate(:user)
invite = Fabricate(:invite, invited_by: inviter)
- invites = Invite.find_all_pending_invites_from(inviter)
-
- expect(invites).to include invite
+ expect(Invite.pending(inviter)).to include(invite)
end
end
@@ -459,107 +436,46 @@ describe Invite do
user = Fabricate(:user)
Fabricate(:invite)
- invites = Invite.find_all_pending_invites_from(user)
-
- expect(invites).to be_empty
+ expect(Invite.pending(user)).to be_empty
end
end
- end
- describe '.find_pending_invites_from' do
it 'returns pending invites only' do
inviter = Fabricate(:user)
- redeemed_invite = Fabricate(
- :invite,
- invited_by: inviter,
- email: 'redeemed@example.com'
- )
- Fabricate(:invited_user, invite: redeemed_invite, user: Fabricate(:user))
- pending_invite = Fabricate(
- :invite,
- invited_by: inviter,
- email: 'pending@example.com'
- )
+ redeemed_invite = Fabricate(:invite, invited_by: inviter, email: 'redeemed@example.com')
+ redeemed_invite.redeem
- invites = Invite.find_pending_invites_from(inviter)
+ pending_invite = Fabricate(:invite, invited_by: inviter, email: 'pending@example.com')
+ pending_link_invite = Fabricate(:invite, invited_by: inviter, max_redemptions_allowed: 5)
- expect(invites.length).to eq(1)
- expect(invites.first).to eq pending_invite
-
- expect(Invite.find_pending_invites_count(inviter)).to eq(1)
+ expect(Invite.pending(inviter)).to contain_exactly(pending_invite, pending_link_invite)
end
end
- describe '.find_redeemed_invites_from' do
+ describe '.redeemed_users' do
it 'returns redeemed invites only' do
inviter = Fabricate(:user)
- Fabricate(
- :invite,
- invited_by: inviter,
- email: 'pending@example.com'
- )
- redeemed_invite = Fabricate(
- :invite,
- invited_by: inviter,
- email: 'redeemed@example.com'
- )
+ Fabricate(:invite, invited_by: inviter, email: 'pending@example.com')
+
+ redeemed_invite = Fabricate(:invite, invited_by: inviter, email: 'redeemed@example.com')
Fabricate(:invited_user, invite: redeemed_invite, user: Fabricate(:user))
- invites = Invite.find_redeemed_invites_from(inviter)
-
- expect(invites.length).to eq(1)
- expect(invites.first).to eq redeemed_invite.invited_users.first
-
- expect(Invite.find_redeemed_invites_count(inviter)).to eq(1)
+ expect(Invite.redeemed_users(inviter)).to contain_exactly(redeemed_invite.invited_users.first)
end
it 'returns redeemed invites for invite links' do
inviter = Fabricate(:user)
- invite_link = Fabricate(
- :invite,
- invited_by: inviter,
- max_redemptions_allowed: 50
- )
- Fabricate(:invited_user, invite: invite_link, user: Fabricate(:user))
- Fabricate(:invited_user, invite: invite_link, user: Fabricate(:user))
- Fabricate(:invited_user, invite: invite_link, user: Fabricate(:user))
+ invite_link = Fabricate(:invite, invited_by: inviter, max_redemptions_allowed: 50)
- invites = Invite.find_redeemed_invites_from(inviter)
- expect(invites.length).to eq(3)
- expect(Invite.find_redeemed_invites_count(inviter)).to eq(3)
- end
- end
+ redeemed = [
+ Fabricate(:invited_user, invite: invite_link, user: Fabricate(:user)),
+ Fabricate(:invited_user, invite: invite_link, user: Fabricate(:user)),
+ Fabricate(:invited_user, invite: invite_link, user: Fabricate(:user))
+ ]
- describe '.find_links_invites_from' do
- it 'returns invite links only' do
- inviter = Fabricate(:user)
- Fabricate(
- :invite,
- invited_by: inviter,
- email: 'pending@example.com'
- )
-
- invite_link_1 = Fabricate(
- :invite,
- invited_by: inviter,
- max_redemptions_allowed: 5
- )
-
- invite_link_2 = Fabricate(
- :invite,
- invited_by: inviter,
- max_redemptions_allowed: 50
- )
-
- invites = Invite.find_links_invites_from(inviter)
-
- expect(invites.length).to eq(2)
- expect(invites.first).to eq(invite_link_2)
- expect(invites.first.max_redemptions_allowed).to eq(50)
-
- expect(Invite.find_links_invites_count(inviter)).to eq(2)
+ expect(Invite.redeemed_users(inviter)).to match_array(redeemed)
end
end
@@ -605,46 +521,6 @@ describe Invite do
end
end
- describe '.resend_all_invites_from' do
- it 'resends all non-redeemed invites by a user' do
- SiteSetting.invite_expiry_days = 30
- user = Fabricate(:user)
- new_invite = Fabricate(:invite, invited_by: user)
- expired_invite = Fabricate(:invite, invited_by: user)
- expired_invite.update!(expires_at: 2.days.ago)
- redeemed_invite = Fabricate(:invite, invited_by: user)
- Fabricate(:invited_user, invite: redeemed_invite, user: Fabricate(:user))
- redeemed_invite.update!(expires_at: 5.days.ago)
-
- Invite.resend_all_invites_from(user.id)
- new_invite.reload
- expired_invite.reload
- redeemed_invite.reload
-
- expect(new_invite.expires_at.to_date).to eq(30.days.from_now.to_date)
- expect(expired_invite.expires_at.to_date).to eq(30.days.from_now.to_date)
- expect(redeemed_invite.expires_at.to_date).to eq(5.days.ago.to_date)
- end
- end
-
- describe '.rescind_all_expired_invites_from' do
- it 'removes all expired invites sent by a user' do
- SiteSetting.invite_expiry_days = 1
- user = Fabricate(:user)
- invite_1 = Fabricate(:invite, invited_by: user)
- invite_2 = Fabricate(:invite, invited_by: user)
- expired_invite = Fabricate(:invite, invited_by: user)
- expired_invite.update!(expires_at: 2.days.ago)
- Invite.rescind_all_expired_invites_from(user)
- invite_1.reload
- invite_2.reload
- expired_invite.reload
- expect(invite_1.deleted_at).to eq(nil)
- expect(invite_2.deleted_at).to eq(nil)
- expect(expired_invite.deleted_at).to be_present
- end
- end
-
describe '#emailed_status_types' do
context "verify enum sequence" do
before do
diff --git a/spec/requests/api/invites_spec.rb b/spec/requests/api/invites_spec.rb
index bf2b18f3c90..5084ae7f0c1 100644
--- a/spec/requests/api/invites_spec.rb
+++ b/spec/requests/api/invites_spec.rb
@@ -33,31 +33,4 @@ describe 'invites' do
end
end
end
-
- path '/invites/link.json' do
- post 'Generate an invite link, but do not send an email' do
- tags 'Invites'
- consumes 'application/json'
- parameter name: 'Api-Key', in: :header, type: :string, required: true
- parameter name: 'Api-Username', in: :header, type: :string, required: true
-
- parameter name: :request_body, in: :body, schema: {
- type: :object,
- properties: {
- email: { type: :string },
- group_names: { type: :string },
- custom_message: { type: :string },
- }, required: ['email']
- }
-
- produces 'application/json'
- response '200', 'success response' do
- schema type: :string, example: "http://discourse.example.com/invites/token_value"
-
- let(:request_body) { { email: 'not-a-user-yet@example.com' } }
- run_test!
- end
- end
- end
-
end
diff --git a/spec/requests/invites_controller_spec.rb b/spec/requests/invites_controller_spec.rb
index 01101545501..735f8c94f1d 100644
--- a/spec/requests/invites_controller_spec.rb
+++ b/spec/requests/invites_controller_spec.rb
@@ -118,11 +118,10 @@ describe InvitesController do
it "fails for normal user if invite email already exists" do
user = sign_in(trust_level_4)
- invite = Invite.invite_by_email("invite@example.com", user)
+ invite = Invite.generate(user, email: "invite@example.com")
post "/invites.json", params: { email: invite.email }
expect(response.status).to eq(422)
- json = response.parsed_body
- expect(json["failed"]).to be_present
+ expect(response.parsed_body["failed"]).to be_present
end
it "allows admins to invite to groups" do
@@ -147,7 +146,7 @@ describe InvitesController do
it "does not allow admins to send multiple invites to same email" do
user = sign_in(admin)
- invite = Invite.invite_by_email("invite@example.com", user)
+ invite = Invite.generate(user, email: "invite@example.com")
post "/invites.json", params: { email: invite.email }
expect(response.status).to eq(422)
end
@@ -156,17 +155,14 @@ describe InvitesController do
sign_in(admin)
post "/invites.json", params: { email: "test@mailinator.com" }
expect(response.status).to eq(422)
- json = response.parsed_body
- expect(json["errors"]).to be_present
+ expect(response.parsed_body["errors"]).to be_present
end
end
- end
- describe "#create_invite_link" do
describe 'single use invite link' do
it 'requires you to be logged in' do
- post "/invites/link.json", params: {
- email: 'jake@adventuretime.ooo'
+ post "/invites.json", params: {
+ email: 'jake@adventuretime.ooo', skip_email: true
}
expect(response.status).to eq(403)
end
@@ -176,29 +172,23 @@ describe InvitesController do
it "fails if you can't invite to the forum" do
sign_in(Fabricate(:user))
- post "/invites/link.json", params: { email: email }
- expect(response.status).to eq(422)
+ post "/invites.json", params: { email: email, skip_email: true }
+ expect(response.status).to eq(403)
end
it "fails for normal user if invite email already exists" do
user = sign_in(trust_level_4)
- invite = Invite.invite_by_email("invite@example.com", user)
-
- post "/invites/link.json", params: {
- email: invite.email
- }
+ invite = Invite.generate(user, email: "invite@example.com")
+ post "/invites.json", params: { email: invite.email, skip_email: true }
expect(response.status).to eq(422)
end
- it "returns the right response when topic_id is invalid" do
+ it "fails when topic_id is invalid" do
sign_in(trust_level_4)
- post "/invites/link.json", params: {
- email: email, topic_id: -9999
- }
-
- expect(response.status).to eq(422)
+ post "/invites.json", params: { email: email, skip_email: true, topic_id: -9999 }
+ expect(response.status).to eq(400)
end
it "verifies that inviter is authorized to invite new user to a group-private topic" do
@@ -207,19 +197,19 @@ describe InvitesController do
group_private_topic = Fabricate(:topic, category: private_category)
sign_in(trust_level_4)
- post "/invites/link.json", params: {
- email: email, topic_id: group_private_topic.id
+ post "/invites.json", params: {
+ email: email, skip_email: true, topic_id: group_private_topic.id
}
- expect(response.status).to eq(422)
+ expect(response.status).to eq(403)
end
it "allows admins to invite to groups" do
group = Fabricate(:group)
sign_in(admin)
- post "/invites/link.json", params: {
- email: email, group_ids: [group.id]
+ post "/invites.json", params: {
+ email: email, skip_email: true, group_ids: [group.id]
}
expect(response.status).to eq(200)
@@ -231,8 +221,8 @@ describe InvitesController do
Fabricate(:group, name: "support")
sign_in(admin)
- post "/invites/link.json", params: {
- email: email, group_names: "security,support"
+ post "/invites.json", params: {
+ email: email, skip_email: true, group_names: "security,support"
}
expect(response.status).to eq(200)
@@ -243,34 +233,26 @@ describe InvitesController do
describe 'multiple use invite link' do
it 'requires you to be logged in' do
- post "/invites/link.json", params: {
+ post "/invites.json", params: {
max_redemptions_allowed: 5
}
expect(response).to be_forbidden
end
context 'while logged in' do
- it "fails for non-staff users" do
- sign_in(trust_level_4)
- post "/invites/link.json", params: {
- max_redemptions_allowed: 5
- }
- expect(response.status).to eq(422)
- end
-
it "allows staff to invite to groups" do
moderator = Fabricate(:moderator)
sign_in(moderator)
group = Fabricate(:group)
group.add_owner(moderator)
- post "/invites/link.json", params: {
+ post "/invites.json", params: {
max_redemptions_allowed: 5,
group_ids: [group.id]
}
expect(response.status).to eq(200)
- expect(Invite.multiple_use_invites.last.invited_groups.count).to eq(1)
+ expect(Invite.last.invited_groups.count).to eq(1)
end
it "allows multiple group invite" do
@@ -278,26 +260,47 @@ describe InvitesController do
Fabricate(:group, name: "support")
sign_in(admin)
- post "/invites/link.json", params: {
+ post "/invites.json", params: {
max_redemptions_allowed: 5,
group_names: "security,support"
}
expect(response.status).to eq(200)
- expect(Invite.multiple_use_invites.last.invited_groups.count).to eq(2)
+ expect(Invite.last.invited_groups.count).to eq(2)
end
end
end
end
+ context '#update' do
+ fab!(:invite) { Fabricate(:invite, invited_by: admin, email: 'test@example.com') }
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'updating email address resends invite email' do
+ put "/invites/#{invite.id}", params: { email: 'test2@example.com' }
+
+ expect(response.status).to eq(200)
+ expect(Jobs::InviteEmail.jobs.size).to eq(1)
+ end
+
+ it 'updating does not resend invite email' do
+ put "/invites/#{invite.id}", params: { custom_message: "new message" }
+
+ expect(response.status).to eq(200)
+ expect(invite.reload.custom_message).to eq("new message")
+ expect(Jobs::InviteEmail.jobs.size).to eq(0)
+ end
+ end
+
context '#perform_accept_invitation' do
context 'with an invalid invite id' do
it "redirects to the root and doesn't change the session" do
put "/invites/show/doesntexist.json"
- expect(response.status).to eq(200)
- json = response.parsed_body
- expect(json["success"]).to eq(false)
- expect(json["message"]).to eq(I18n.t('invite.not_found_json'))
+ expect(response.status).to eq(404)
+ expect(response.parsed_body["message"]).to eq(I18n.t('invite.not_found_json'))
expect(session[:current_user_id]).to be_blank
end
end
@@ -307,20 +310,15 @@ describe InvitesController do
it "responds with error message" do
invite.update_attribute(:email, "John Doe
")
put "/invites/show/#{invite.invite_key}.json"
- expect(response.status).to eq(200)
- json = response.parsed_body
- expect(json["success"]).to eq(false)
- expect(json["message"]).to eq(I18n.t('invite.error_message'))
+ expect(response.status).to eq(412)
+ expect(response.parsed_body["message"]).to eq(I18n.t('invite.error_message'))
expect(session[:current_user_id]).to be_blank
end
end
context 'with a deleted invite' do
fab!(:topic) { Fabricate(:topic) }
-
- let(:invite) do
- Invite.invite_by_email("iceking@adventuretime.ooo", topic.user, topic)
- end
+ let(:invite) { Invite.generate(topic.user, email: "iceking@adventuretime.ooo", topic: topic) }
before do
invite.destroy!
@@ -329,10 +327,8 @@ describe InvitesController do
it "redirects to the root" do
put "/invites/show/#{invite.invite_key}.json"
- expect(response.status).to eq(200)
- json = response.parsed_body
- expect(json["success"]).to eq(false)
- expect(json["message"]).to eq(I18n.t('invite.not_found_json'))
+ expect(response.status).to eq(404)
+ expect(response.parsed_body["message"]).to eq(I18n.t('invite.not_found_json'))
expect(session[:current_user_id]).to be_blank
end
end
@@ -343,19 +339,15 @@ describe InvitesController do
it "response is not successful" do
put "/invites/show/#{invite_link.invite_key}.json"
- expect(response.status).to eq(200)
- json = response.parsed_body
- expect(json["success"]).to eq(false)
- expect(json["message"]).to eq(I18n.t('invite.not_found_json'))
+ expect(response.status).to eq(404)
+ expect(response.parsed_body["message"]).to eq(I18n.t('invite.not_found_json'))
expect(session[:current_user_id]).to be_blank
end
end
context 'with a valid invite id' do
fab!(:topic) { Fabricate(:topic) }
- let(:invite) do
- Invite.invite_by_email("iceking@adventuretime.ooo", topic.user, topic)
- end
+ let(:invite) { Invite.generate(topic.user, email: "iceking@adventuretime.ooo", topic: topic) }
it 'redeems the invite' do
put "/invites/show/#{invite.invite_key}.json"
@@ -387,9 +379,7 @@ describe InvitesController do
it 'redirects to the first topic the user was invited to' do
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
- json = response.parsed_body
- expect(json["success"]).to eq(true)
- expect(json["redirect_to"]).to eq(topic.relative_url)
+ expect(response.parsed_body["redirect_to"]).to eq(topic.relative_url)
end
context "if a timezone guess is provided" do
@@ -406,10 +396,8 @@ describe InvitesController do
context 'failure' do
it "doesn't log in the user if there's a validation error" do
put "/invites/show/#{invite.invite_key}.json", params: { password: "password" }
- expect(response.status).to eq(200)
- json = response.parsed_body
- expect(json["success"]).to eq(false)
- expect(json["errors"]["password"]).to be_present
+ expect(response.status).to eq(412)
+ expect(response.parsed_body["errors"]["password"]).to be_present
end
end
@@ -418,7 +406,6 @@ describe InvitesController do
user.send_welcome_message = true
put "/invites/show/#{invite.invite_key}.json"
expect(response.status).to eq(200)
- expect(response.parsed_body["success"]).to eq(true)
expect(Jobs::SendSystemMessage.jobs.size).to eq(1)
end
@@ -474,7 +461,6 @@ describe InvitesController do
end.to change { UserAuthToken.count }.by(1)
expect(response.status).to eq(200)
- expect(response.parsed_body["success"]).to eq(true)
expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
@@ -494,7 +480,6 @@ describe InvitesController do
end.not_to change { UserAuthToken.count }
expect(response.status).to eq(200)
- expect(response.parsed_body["success"]).to eq(true)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.confirm_email"))
invited_user = User.find_by_email(invite.email)
@@ -527,7 +512,6 @@ describe InvitesController do
end.not_to change { UserAuthToken.count }
expect(response.status).to eq(200)
- expect(response.parsed_body["success"]).to eq(true)
expect(response.parsed_body["message"]).to eq(I18n.t("invite.confirm_email"))
invite_link.reload
@@ -553,9 +537,7 @@ describe InvitesController do
context 'new registrations are disabled' do
fab!(:topic) { Fabricate(:topic) }
- let(:invite) do
- Invite.invite_by_email("iceking@adventuretime.ooo", topic.user, topic)
- end
+ let(:invite) { Invite.generate(topic.user, email: "iceking@adventuretime.ooo", topic: topic) }
before { SiteSetting.allow_new_registrations = false }
@@ -572,9 +554,7 @@ describe InvitesController do
context 'user is already logged in' do
fab!(:topic) { Fabricate(:topic) }
- let(:invite) do
- Invite.invite_by_email("iceking@adventuretime.ooo", topic.user, topic)
- end
+ let(:invite) { Invite.generate(topic.user, email: "iceking@adventuretime.ooo", topic: topic) }
let!(:user) { sign_in(Fabricate(:user)) }
@@ -589,6 +569,26 @@ describe InvitesController do
end
end
+ context "#destroy_all" do
+ it 'removes all expired invites sent by a user' do
+ SiteSetting.invite_expiry_days = 1
+
+ user = Fabricate(:admin)
+ invite_1 = Fabricate(:invite, invited_by: user)
+ invite_2 = Fabricate(:invite, invited_by: user)
+ expired_invite = Fabricate(:invite, invited_by: user)
+ expired_invite.update!(expires_at: 2.days.ago)
+
+ sign_in(user)
+ post "/invites/destroy-all-expired"
+
+ expect(response.status).to eq(200)
+ expect(invite_1.reload.deleted_at).to eq(nil)
+ expect(invite_2.reload.deleted_at).to eq(nil)
+ expect(expired_invite.reload.deleted_at).to be_present
+ end
+ end
+
context '#resend_invite' do
it 'requires you to be logged in' do
post "/invites/reinvite.json", params: { email: 'first_name@example.com' }
@@ -623,6 +623,28 @@ describe InvitesController do
end
end
+ context '#resend_all_invites' do
+ it 'resends all non-redeemed invites by a user' do
+ SiteSetting.invite_expiry_days = 30
+
+ user = Fabricate(:admin)
+ new_invite = Fabricate(:invite, invited_by: user)
+ expired_invite = Fabricate(:invite, invited_by: user)
+ expired_invite.update!(expires_at: 2.days.ago)
+ redeemed_invite = Fabricate(:invite, invited_by: user)
+ Fabricate(:invited_user, invite: redeemed_invite, user: Fabricate(:user))
+ redeemed_invite.update!(expires_at: 5.days.ago)
+
+ sign_in(user)
+ post "/invites/reinvite-all"
+
+ expect(response.status).to eq(200)
+ expect(new_invite.reload.expires_at.to_date).to eq(30.days.from_now.to_date)
+ expect(expired_invite.reload.expires_at.to_date).to eq(30.days.from_now.to_date)
+ expect(redeemed_invite.reload.expires_at.to_date).to eq(5.days.ago.to_date)
+ end
+ end
+
context '#upload_csv' do
it 'requires you to be logged in' do
post "/invites/upload_csv.json"
@@ -658,8 +680,7 @@ describe InvitesController do
expect(response.status).to eq(422)
expect(Jobs::BulkInvite.jobs.size).to eq(1)
- json = response.parsed_body
- expect(json["errors"][0]).to eq(I18n.t("bulk_invite.max_rows", max_bulk_invites: SiteSetting.max_bulk_invites))
+ expect(response.parsed_body["errors"][0]).to eq(I18n.t("bulk_invite.max_rows", max_bulk_invites: SiteSetting.max_bulk_invites))
end
end
end
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index 4c02d4c1ac1..dca299f508c 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -1584,28 +1584,6 @@ describe UsersController do
end
end
- describe "#invited_count" do
- it "fails for anonymous users" do
- user = Fabricate(:user)
- get "/u/#{user.username}/invited_count.json"
- expect(response.status).to eq(422)
- end
-
- it "works for users who can see invites" do
- inviter = Fabricate(:user, trust_level: 2)
- sign_in(inviter)
- invitee = Fabricate(:user)
- _invite = Fabricate(:invite, invited_by: inviter)
- Fabricate(:invited_user, invite: _invite, user: invitee)
- get "/u/#{user.username}/invited_count.json"
- expect(response.status).to eq(200)
-
- json = response.parsed_body
- expect(json).to be_present
- expect(json['counts']).to be_present
- end
- end
-
describe '#invited' do
it 'fails for anonymous users' do
user = Fabricate(:user)
@@ -1616,10 +1594,14 @@ describe UsersController do
it 'returns success' do
user = Fabricate(:user, trust_level: 2)
+ Fabricate(:invite, invited_by: user)
+
sign_in(user)
get "/u/#{user.username}/invited.json", params: { username: user.username }
expect(response.status).to eq(200)
+ expect(response.parsed_body["counts"]["pending"]).to eq(1)
+ expect(response.parsed_body["counts"]["total"]).to eq(1)
end
it 'filters by all if viewing self' do
@@ -1748,6 +1730,46 @@ describe UsersController do
expect(response.status).to eq(422)
end
end
+
+ context 'with permission to see invite links' do
+ it 'returns invites' do
+ inviter = sign_in(Fabricate(:admin))
+ invite = Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required])
+
+ get "/u/#{inviter.username}/invited/pending.json"
+ expect(response.status).to eq(200)
+
+ invites = response.parsed_body['invites']
+ expect(invites.size).to eq(1)
+ expect(invites.first).to include("id" => invite.id)
+ end
+ end
+
+ context 'without permission to see invite links' do
+ it 'does not return invites' do
+ user = Fabricate(:user, trust_level: 2)
+ inviter = Fabricate(:admin)
+ Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required])
+
+ get "/u/#{inviter.username}/invited/pending.json"
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when local logins are disabled' do
+ it 'explains why invites are disabled to staff users' do
+ SiteSetting.enable_local_logins = false
+ inviter = sign_in(Fabricate(:admin))
+ Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required])
+
+ get "/u/#{inviter.username}/invited/pending.json"
+ expect(response.status).to eq(200)
+
+ expect(response.parsed_body['error']).to include(I18n.t(
+ 'invite.disabled_errors.local_logins_disabled'
+ ))
+ end
+ end
end
context 'with redeemed invites' do
@@ -1766,48 +1788,6 @@ describe UsersController do
expect(invites[0]).to include('id' => invite.id)
end
end
-
- context 'with invite links' do
- context 'with permission to see invite links' do
- it 'returns invites' do
- inviter = sign_in(Fabricate(:admin))
- invite = Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required])
-
- get "/u/#{inviter.username}/invite_links.json"
- expect(response.status).to eq(200)
-
- invites = response.parsed_body['invites']
- expect(invites.size).to eq(1)
- expect(invites.first).to include("id" => invite.id)
- end
- end
-
- context 'without permission to see invite links' do
- it 'does not return invites' do
- user = Fabricate(:user, trust_level: 2)
- inviter = Fabricate(:admin)
- Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required])
-
- get "/u/#{inviter.username}/invite_links.json"
- expect(response.status).to eq(403)
- end
- end
-
- context 'when local logins are disabled' do
- it 'explains why invites are disabled to staff users' do
- SiteSetting.enable_local_logins = false
- inviter = sign_in(Fabricate(:admin))
- Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required])
-
- get "/u/#{inviter.username}/invite_links.json"
- expect(response.status).to eq(200)
-
- expect(response.parsed_body['error']).to include(I18n.t(
- 'invite.disabled_errors.local_logins_disabled'
- ))
- end
- end
- end
end
end