mirror of
https://github.com/discourse/discourse.git
synced 2025-02-16 18:24:52 -06:00
FEATURE: change /invites.json api endpoint to optionally accept array of emails (#24853)
https://meta.discourse.org/t/feature-request-sending-bulk-invitations-via-api/272423/18
This commit is contained in:
parent
14269232ba
commit
ddd750cda7
@ -37,7 +37,10 @@ class InvitesController < ApplicationController
|
||||
render layout: "no_ember"
|
||||
end
|
||||
|
||||
def create
|
||||
def create_multiple
|
||||
guardian.ensure_can_bulk_invite_to_forum!(current_user)
|
||||
emails = params[:email]
|
||||
# validate that topics and groups can accept invites.
|
||||
if params[:topic_id].present?
|
||||
topic = Topic.find_by(id: params[:topic_id])
|
||||
raise Discourse::InvalidParameters.new(:topic_id) if topic.blank?
|
||||
@ -59,37 +62,107 @@ class InvitesController < ApplicationController
|
||||
)
|
||||
end
|
||||
|
||||
invite =
|
||||
Invite.generate(
|
||||
current_user,
|
||||
email: params[:email],
|
||||
domain: params[:domain],
|
||||
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],
|
||||
invite_to_topic: params[:invite_to_topic],
|
||||
if emails.size > SiteSetting.max_api_invites
|
||||
return(
|
||||
render_json_error(
|
||||
I18n.t("invite.max_invite_emails_limit_exceeded", max: SiteSetting.max_api_invites),
|
||||
422,
|
||||
)
|
||||
)
|
||||
|
||||
if invite.present?
|
||||
render_serialized(
|
||||
invite,
|
||||
InviteSerializer,
|
||||
scope: guardian,
|
||||
root: nil,
|
||||
show_emails: params.has_key?(:email),
|
||||
show_warnings: true,
|
||||
)
|
||||
else
|
||||
render json: failed_json, status: 422
|
||||
end
|
||||
rescue Invite::UserExists => e
|
||||
render_json_error(e.message)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render_json_error(e.record.errors.full_messages.first)
|
||||
|
||||
success = []
|
||||
fail = []
|
||||
|
||||
emails.map do |email|
|
||||
begin
|
||||
invite =
|
||||
Invite.generate(
|
||||
current_user,
|
||||
email: email,
|
||||
domain: params[:domain],
|
||||
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],
|
||||
invite_to_topic: params[:invite_to_topic],
|
||||
)
|
||||
success.push({ email: email, invite: invite }) if invite
|
||||
rescue Invite::UserExists => e
|
||||
fail.push({ email: email, error: e.message })
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
fail.push({ email: email, error: e.record.errors.full_messages.first })
|
||||
end
|
||||
end
|
||||
|
||||
render json: {
|
||||
num_successfully_created_invitations: success.length,
|
||||
num_failed_invitations: fail.length,
|
||||
failed_invitations: fail,
|
||||
successful_invitations:
|
||||
success.map do |s| InviteSerializer.new(s[:invite], scope: guardian) end,
|
||||
}
|
||||
end
|
||||
|
||||
def create
|
||||
begin
|
||||
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)
|
||||
|
||||
if !groups_can_see_topic?(groups, topic)
|
||||
editable_topic_groups = topic.category.groups.filter { |g| guardian.can_edit_group?(g) }
|
||||
return(
|
||||
render_json_error(
|
||||
I18n.t("invite.requires_groups", groups: editable_topic_groups.pluck(:name).join(", ")),
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
invite =
|
||||
Invite.generate(
|
||||
current_user,
|
||||
email: params[:email],
|
||||
domain: params[:domain],
|
||||
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],
|
||||
invite_to_topic: params[:invite_to_topic],
|
||||
)
|
||||
|
||||
if invite.present?
|
||||
render_serialized(
|
||||
invite,
|
||||
InviteSerializer,
|
||||
scope: guardian,
|
||||
root: nil,
|
||||
show_emails: params.has_key?(:email),
|
||||
show_warnings: true,
|
||||
)
|
||||
else
|
||||
render json: failed_json, status: 422
|
||||
end
|
||||
rescue Invite::UserExists => e
|
||||
render_json_error(e.message)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render_json_error(e.record.errors.full_messages.first)
|
||||
end
|
||||
end
|
||||
|
||||
def retrieve
|
||||
|
@ -297,6 +297,7 @@ en:
|
||||
discourse_connect_enabled: "Invites are disabled because DiscourseConnect is enabled."
|
||||
invalid_access: "You are not permitted to view the requested resource."
|
||||
requires_groups: "Invite was not saved because the specified topic is inaccessible. Add one of the following groups: %{groups}."
|
||||
max_invite_emails_limit_exceeded: "Request failed because number of emails exceeded the maximum (%{max})."
|
||||
domain_not_allowed: "Your email cannot be used to redeem this invite."
|
||||
max_redemptions_allowed_one: "for email invites should be 1."
|
||||
redemption_count_less_than_max: "should be less than %{max_redemptions_allowed}."
|
||||
|
@ -1432,6 +1432,7 @@ Discourse::Application.routes.draw do
|
||||
|
||||
resources :invites, only: %i[create update destroy]
|
||||
get "/invites/:id" => "invites#show", :constraints => { format: :html }
|
||||
post "invites/create-multiple" => "invites#create_multiple", :constraints => { format: :json }
|
||||
|
||||
post "invites/upload_csv" => "invites#upload_csv"
|
||||
post "invites/destroy-all-expired" => "invites#destroy_all_expired"
|
||||
|
@ -2787,6 +2787,10 @@ uncategorized:
|
||||
default: 50000
|
||||
hidden: true
|
||||
|
||||
max_api_invites:
|
||||
default: 200
|
||||
hidden: true
|
||||
|
||||
overridden_robots_txt:
|
||||
default: ""
|
||||
hidden: true
|
||||
|
122
spec/requests/api/multiple_invites_spec.rb
Normal file
122
spec/requests/api/multiple_invites_spec.rb
Normal file
@ -0,0 +1,122 @@
|
||||
# frozen_string_literal: true
|
||||
require "swagger_helper"
|
||||
|
||||
RSpec.describe "multiple invites" do
|
||||
let(:"Api-Key") { Fabricate(:api_key).key }
|
||||
let(:"Api-Username") { "system" }
|
||||
|
||||
path "/invites/create-multiple.json" do
|
||||
post "Create multiple invites" do
|
||||
tags "Invites"
|
||||
operationId "createMultipleInvites"
|
||||
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,
|
||||
example: %w[not-a-user-yet-1@example.com not-a-user-yet-2@example.com],
|
||||
description:
|
||||
"pass 1 email per invite to be generated. other properties will be shared by each invite.",
|
||||
},
|
||||
skip_email: {
|
||||
type: :boolean,
|
||||
default: false,
|
||||
},
|
||||
custom_message: {
|
||||
type: :string,
|
||||
description: "optional, for email invites",
|
||||
},
|
||||
max_redemptions_allowed: {
|
||||
type: :integer,
|
||||
example: 5,
|
||||
default: 1,
|
||||
description: "optional, for link invites",
|
||||
},
|
||||
topic_id: {
|
||||
type: :integer,
|
||||
},
|
||||
group_ids: {
|
||||
type: :string,
|
||||
description:
|
||||
"Optional, either this or `group_names`. Comma separated list for multiple ids.",
|
||||
example: "42,43",
|
||||
},
|
||||
group_names: {
|
||||
type: :string,
|
||||
description:
|
||||
"Optional, either this or `group_ids`. Comma separated list for multiple names.",
|
||||
example: "foo,bar",
|
||||
},
|
||||
expires_at: {
|
||||
type: :string,
|
||||
description:
|
||||
"optional, if not supplied, the invite_expiry_days site setting is used",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
produces "application/json"
|
||||
response "200", "success response" do
|
||||
schema type: :object,
|
||||
properties: {
|
||||
num_successfully_created_invitations: {
|
||||
type: :integer,
|
||||
example: 42,
|
||||
},
|
||||
num_failed_invitations: {
|
||||
type: :integer,
|
||||
example: 42,
|
||||
},
|
||||
failed_invitations: {
|
||||
type: :array,
|
||||
items: {
|
||||
},
|
||||
example: [],
|
||||
},
|
||||
successful_invitations: {
|
||||
type: :array,
|
||||
example: [
|
||||
{
|
||||
id: 42,
|
||||
link: "http://example.com/invites/9045fd767efe201ca60c6658bcf14158",
|
||||
email: "not-a-user-yet-1@example.com",
|
||||
emailed: true,
|
||||
custom_message: "Hello world!",
|
||||
topics: [],
|
||||
groups: [],
|
||||
created_at: "2021-01-01T12:00:00.000Z",
|
||||
updated_at: "2021-01-01T12:00:00.000Z",
|
||||
expires_at: "2021-02-01T12:00:00.000Z",
|
||||
expired: false,
|
||||
},
|
||||
{
|
||||
id: 42,
|
||||
link: "http://example.com/invites/c6658bcf141589045fd767efe201ca60",
|
||||
email: "not-a-user-yet-2@example.com",
|
||||
emailed: true,
|
||||
custom_message: "Hello world!",
|
||||
topics: [],
|
||||
groups: [],
|
||||
created_at: "2021-01-01T12:00:00.000Z",
|
||||
updated_at: "2021-01-01T12:00:00.000Z",
|
||||
expires_at: "2021-02-01T12:00:00.000Z",
|
||||
expired: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
let(:request_body) do
|
||||
{ email: %w[not-a-user-yet-1@example.com not-a-user-yet-2@example.com] }
|
||||
end
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -505,6 +505,152 @@ RSpec.describe InvitesController do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create-multiple" do
|
||||
it "fails if you are not admin" do
|
||||
sign_in(Fabricate(:user))
|
||||
post "/invites/create-multiple.json",
|
||||
params: {
|
||||
email: %w[test@example.com test1@example.com bademail],
|
||||
}
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it "creates multiple invites for multiple emails" do
|
||||
sign_in(admin)
|
||||
post "/invites/create-multiple.json",
|
||||
params: {
|
||||
email: %w[test@example.com test1@example.com bademail],
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
json = JSON(response.body)
|
||||
expect(json["failed_invitations"].length).to eq(1)
|
||||
expect(json["successful_invitations"].length).to eq(2)
|
||||
end
|
||||
|
||||
it "creates many invite codes with one request" do #change to
|
||||
sign_in(admin)
|
||||
num_emails = 5 # increase manually for load testing
|
||||
post "/invites/create-multiple.json",
|
||||
params: {
|
||||
email: 1.upto(num_emails).map { |i| "test#{i}@example.com" },
|
||||
#email: %w[test+1@example.com test1@example.com]
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
json = JSON(response.body)
|
||||
expect(json["failed_invitations"].length).to eq(0)
|
||||
expect(json["successful_invitations"].length).to eq(num_emails)
|
||||
end
|
||||
|
||||
context "with invite to topic" do
|
||||
fab!(:topic)
|
||||
|
||||
it "works" do
|
||||
sign_in(admin)
|
||||
|
||||
post "/invites/create-multiple.json",
|
||||
params: {
|
||||
email: ["test@example.com"],
|
||||
topic_id: topic.id,
|
||||
invite_to_topic: true,
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
expect(Jobs::InviteEmail.jobs.first["args"].first["invite_to_topic"]).to be_truthy
|
||||
end
|
||||
|
||||
it "fails when topic_id is invalid" do
|
||||
sign_in(admin)
|
||||
|
||||
post "/invites/create-multiple.json",
|
||||
params: {
|
||||
email: ["test@example.com"],
|
||||
topic_id: -9999,
|
||||
}
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
end
|
||||
|
||||
context "with invite to group" do
|
||||
fab!(:group)
|
||||
|
||||
it "works for admins" do
|
||||
sign_in(admin)
|
||||
|
||||
post "/invites/create-multiple.json",
|
||||
params: {
|
||||
email: ["test@example.com"],
|
||||
group_ids: [group.id],
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
expect(Invite.find_by(email: "test@example.com").invited_groups.count).to eq(1)
|
||||
end
|
||||
|
||||
it "works with multiple groups" do
|
||||
sign_in(admin)
|
||||
group2 = Fabricate(:group)
|
||||
|
||||
post "/invites/create-multiple.json",
|
||||
params: {
|
||||
email: ["test@example.com"],
|
||||
group_names: "#{group.name},#{group2.name}",
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
expect(Invite.find_by(email: "test@example.com").invited_groups.count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context "with email invite" do
|
||||
subject(:create_multiple_invites) { post "/invites/create-multiple.json", params: params }
|
||||
|
||||
let(:params) { { email: [email] } }
|
||||
let(:email) { "test@example.com" }
|
||||
|
||||
before { sign_in(admin) }
|
||||
|
||||
context "when doing successive calls" do
|
||||
let(:invite) { Invite.last }
|
||||
|
||||
it "creates invite once and updates it after" do
|
||||
create_multiple_invites
|
||||
expect(response).to have_http_status :ok
|
||||
expect(Jobs::InviteEmail.jobs.size).to eq(1)
|
||||
|
||||
create_multiple_invites
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response.parsed_body["successful_invitations"][0]["invite"]["id"]).to eq(invite.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when "skip_email" parameter is provided' do
|
||||
before { params[:skip_email] = true }
|
||||
|
||||
it "accepts the parameter" do
|
||||
create_multiple_invites
|
||||
expect(response).to have_http_status :ok
|
||||
expect(Jobs::InviteEmail.jobs.size).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "fails if asked to generate too many invites at once" do
|
||||
SiteSetting.max_api_invites = 3
|
||||
sign_in(admin)
|
||||
post "/invites/create-multiple.json",
|
||||
params: {
|
||||
email: %w[
|
||||
mail1@mailinator.com
|
||||
mail2@mailinator.com
|
||||
mail3@mailinator.com
|
||||
mail4@mailinator.com
|
||||
],
|
||||
}
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(response.parsed_body["errors"][0]).to eq(
|
||||
I18n.t("invite.max_invite_emails_limit_exceeded", max: SiteSetting.max_api_invites),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#retrieve" do
|
||||
it "requires to be logged in" do
|
||||
get "/invites/retrieve.json", params: { email: "test@example.com" }
|
||||
|
Loading…
Reference in New Issue
Block a user