FEATURE: Secondary emails support.
This commit is contained in:
parent
50e59fb9bd
commit
21ebb1cd54
|
@ -60,7 +60,7 @@
|
|||
|
||||
{{#if canCheckEmails}}
|
||||
<div class='display-row email'>
|
||||
<div class='field'>{{i18n 'user.email.title'}}</div>
|
||||
<div class='field'>{{i18n 'user.email.primary'}}</div>
|
||||
<div class='value'>
|
||||
{{#unless model.active}}
|
||||
<div class='controls'>{{i18n 'admin.users.not_verified'}}</div>
|
||||
|
@ -73,6 +73,30 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class='display-row secondary-emails'>
|
||||
<div class='field'>{{i18n 'user.email.secondary'}}</div>
|
||||
|
||||
<div class='value'>
|
||||
{{#if model.email}}
|
||||
{{#if model.secondary_emails}}
|
||||
<ul>
|
||||
{{#each model.secondary_emails as |email| }}
|
||||
<li><a href="mailto:{{unbound email}}">{{email}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
{{i18n 'user.email.no_secondary'}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{d-button action="checkEmail"
|
||||
actionParam=model
|
||||
icon="envelope-o"
|
||||
label="admin.users.check_email.text"
|
||||
title="admin.users.check_email.title"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="display-row bounce-score">
|
||||
<div class='field'><a href="{{model.bounceLink}}">{{i18n 'admin.user.bounce_score'}}</a></div>
|
||||
<div class='value'>{{model.bounceScore}}</div>
|
||||
|
|
|
@ -604,6 +604,7 @@ const User = RestModel.extend({
|
|||
if (result) {
|
||||
this.setProperties({
|
||||
email: result.email,
|
||||
secondary_emails: result.secondary_emails,
|
||||
associated_accounts: result.associated_accounts
|
||||
});
|
||||
}
|
||||
|
|
|
@ -29,6 +29,10 @@
|
|||
&:after {
|
||||
clear: both;
|
||||
}
|
||||
&.secondary-emails ul {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.field {
|
||||
font-weight: bold;
|
||||
width: 17.65765%;
|
||||
|
|
|
@ -146,8 +146,11 @@ class UsersController < ApplicationController
|
|||
|
||||
StaffActionLogger.new(current_user).log_check_email(user, context: params[:context])
|
||||
|
||||
email, *secondary_emails = user.emails
|
||||
|
||||
render json: {
|
||||
email: user.email,
|
||||
email: email,
|
||||
secondary_emails: secondary_emails,
|
||||
associated_accounts: user.associated_accounts
|
||||
}
|
||||
rescue Discourse::InvalidAccess
|
||||
|
|
|
@ -1069,6 +1069,14 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def emails
|
||||
self.user_emails.order("user_emails.primary DESC NULLS LAST").pluck(:email)
|
||||
end
|
||||
|
||||
def secondary_emails
|
||||
self.user_emails.secondary.pluck(:email)
|
||||
end
|
||||
|
||||
def recent_time_read
|
||||
self.created_at && self.created_at < 60.days.ago ?
|
||||
self.user_visits.where('visited_at >= ?', 60.days.ago).sum(:time_read) :
|
||||
|
|
|
@ -15,6 +15,8 @@ class UserEmail < ActiveRecord::Base
|
|||
validate :user_id_not_changed, if: :primary
|
||||
validate :unique_email
|
||||
|
||||
scope :secondary, -> { where(primary: false) }
|
||||
|
||||
private
|
||||
|
||||
def strip_downcase_email
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class AdminUserListSerializer < BasicUserSerializer
|
||||
|
||||
attributes :email,
|
||||
:secondary_emails,
|
||||
:active,
|
||||
:admin,
|
||||
:moderator,
|
||||
|
@ -41,6 +42,7 @@ class AdminUserListSerializer < BasicUserSerializer
|
|||
(scope.is_staff? && object.staged?)
|
||||
end
|
||||
|
||||
alias_method :include_secondary_emails?, :include_email?
|
||||
alias_method :include_associated_accounts?, :include_email?
|
||||
|
||||
def silenced
|
||||
|
|
|
@ -808,6 +808,9 @@ en:
|
|||
|
||||
email:
|
||||
title: "Email"
|
||||
primary: "Primary Email"
|
||||
secondary: "Secondary Emails"
|
||||
no_secondary: "No secondary emails"
|
||||
instructions: "never shown to the public"
|
||||
ok: "We will email you to confirm"
|
||||
invalid: "Please enter a valid email address"
|
||||
|
|
|
@ -169,6 +169,21 @@ describe Email::Receiver do
|
|||
expect { process(:from_reply_by_email_address) }.to raise_error(Email::Receiver::FromReplyByAddressError)
|
||||
end
|
||||
|
||||
it "accepts reply from secondary email address" do
|
||||
Fabricate(:secondary_email, email: "someone_else@bar.com", user: user)
|
||||
|
||||
expect { process(:reply_user_not_matching) }
|
||||
.to change { topic.posts.count }
|
||||
|
||||
post = Post.last
|
||||
|
||||
expect(post.raw).to eq(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
)
|
||||
|
||||
expect(post.user).to eq(user)
|
||||
end
|
||||
|
||||
it "raises a TopicNotFoundError when the topic was deleted" do
|
||||
topic.update_columns(deleted_at: 1.day.ago)
|
||||
expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicNotFoundError)
|
||||
|
@ -490,6 +505,27 @@ describe Email::Receiver do
|
|||
expect(topic.topic_users.count).to eq(3)
|
||||
end
|
||||
|
||||
it "invites users with a secondary email in the chain" do
|
||||
user1 = Fabricate(:user,
|
||||
trust_level: SiteSetting.email_in_min_trust,
|
||||
user_emails: [
|
||||
Fabricate.build(:secondary_email, email: "discourse@bar.com"),
|
||||
Fabricate.build(:secondary_email, email: "someone@else.com"),
|
||||
]
|
||||
)
|
||||
|
||||
user2 = Fabricate(:user,
|
||||
trust_level: SiteSetting.email_in_min_trust,
|
||||
user_emails: [
|
||||
Fabricate.build(:secondary_email, email: "team@bar.com"),
|
||||
Fabricate.build(:secondary_email, email: "wat@bar.com"),
|
||||
]
|
||||
)
|
||||
|
||||
expect { process(:cc) }.to change(Topic, :count)
|
||||
expect(Topic.last.allowed_users).to contain_exactly(user1, user2)
|
||||
end
|
||||
|
||||
it "cap the number of staged users created per email" do
|
||||
SiteSetting.maximum_staged_users_per_email = 1
|
||||
expect { process(:cc) }.to change(Topic, :count)
|
||||
|
@ -696,6 +732,24 @@ describe Email::Receiver do
|
|||
SiteSetting.ignore_by_title = "foo"
|
||||
expect { process(:ignored) }.to_not change(Topic, :count)
|
||||
end
|
||||
|
||||
it "associates email from a secondary address with user" do
|
||||
user = Fabricate(:user,
|
||||
trust_level: SiteSetting.email_in_min_trust,
|
||||
user_emails: [
|
||||
Fabricate.build(:secondary_email, email: "existing@bar.com")
|
||||
]
|
||||
)
|
||||
|
||||
expect { process(:existing_user) }.to change(Topic, :count).by(1)
|
||||
|
||||
topic = Topic.last
|
||||
|
||||
expect(topic.posts.last.raw)
|
||||
.to eq("Hey, this is a topic from an existing user ;)")
|
||||
|
||||
expect(topic.user).to eq(user)
|
||||
end
|
||||
end
|
||||
|
||||
context "new topic in a category that allows strangers" do
|
||||
|
|
|
@ -3,7 +3,7 @@ Fabricator(:user_email) do
|
|||
primary true
|
||||
end
|
||||
|
||||
Fabricator(:alternate_email, from: :user_email) do
|
||||
Fabricator(:secondary_email, from: :user_email) do
|
||||
email { sequence(:email) { |i| "bwayne#{i}@wayne.com" } }
|
||||
primary false
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ Fabricator(:user_single_email, class_name: :user) do
|
|||
end
|
||||
|
||||
Fabricator(:user, from: :user_single_email) do
|
||||
after_create { |user| Fabricate(:alternate_email, user: user) }
|
||||
after_create { |user| Fabricate(:secondary_email, user: user) }
|
||||
end
|
||||
|
||||
Fabricator(:coding_horror, from: :user) do
|
||||
|
|
|
@ -6,14 +6,14 @@ describe UserEmail do
|
|||
it "allows only one primary email" do
|
||||
user = Fabricate(:user_single_email)
|
||||
expect {
|
||||
Fabricate(:alternate_email, user: user, primary: true)
|
||||
Fabricate(:secondary_email, user: user, primary: true)
|
||||
}.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
|
||||
it "allows multiple secondary emails" do
|
||||
user = Fabricate(:user_single_email)
|
||||
Fabricate(:alternate_email, user: user, primary: false)
|
||||
Fabricate(:alternate_email, user: user, primary: false)
|
||||
Fabricate(:secondary_email, user: user, primary: false)
|
||||
Fabricate(:secondary_email, user: user, primary: false)
|
||||
expect(user.user_emails.count).to eq 3
|
||||
end
|
||||
end
|
||||
|
@ -22,14 +22,14 @@ describe UserEmail do
|
|||
it "allows only one primary email" do
|
||||
user = Fabricate(:user_single_email)
|
||||
expect {
|
||||
Fabricate.build(:alternate_email, user: user, primary: true).save(validate: false)
|
||||
Fabricate.build(:secondary_email, user: user, primary: true).save(validate: false)
|
||||
}.to raise_error(ActiveRecord::RecordNotUnique)
|
||||
end
|
||||
|
||||
it "allows multiple secondary emails" do
|
||||
user = Fabricate(:user_single_email)
|
||||
Fabricate.build(:alternate_email, user: user, primary: false).save(validate: false)
|
||||
Fabricate.build(:alternate_email, user: user, primary: false).save(validate: false)
|
||||
Fabricate.build(:secondary_email, user: user, primary: false).save(validate: false)
|
||||
Fabricate.build(:secondary_email, user: user, primary: false).save(validate: false)
|
||||
expect(user.user_emails.count).to eq 3
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1758,4 +1758,17 @@ describe User do
|
|||
filter_by(:filter_by_username_or_email)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#secondary_emails" do
|
||||
let(:user) { Fabricate(:user_single_email) }
|
||||
|
||||
it "only contains secondary emails" do
|
||||
expect(user.user_emails.secondary).to eq([])
|
||||
|
||||
secondary_email = Fabricate(:secondary_email, user: user)
|
||||
|
||||
expect(user.user_emails.secondary).to contain_exactly(secondary_email)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -1900,14 +1900,16 @@ describe UsersController do
|
|||
expect(response).to be_forbidden
|
||||
end
|
||||
|
||||
it "returns both email and associated_accounts when you're allowed to see them" do
|
||||
it "returns emails and associated_accounts when you're allowed to see them" do
|
||||
user = Fabricate(:user)
|
||||
sign_in_admin
|
||||
|
||||
get "/u/#{Fabricate(:user).username}/emails.json"
|
||||
get "/u/#{user.username}/emails.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json["email"]).to be_present
|
||||
expect(json["email"]).to eq(user.email)
|
||||
expect(json["secondary_emails"]).to eq(user.secondary_emails)
|
||||
expect(json["associated_accounts"]).to be_present
|
||||
end
|
||||
|
||||
|
@ -1919,7 +1921,8 @@ describe UsersController do
|
|||
|
||||
expect(response.status).to eq(200)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json["email"]).to be_present
|
||||
expect(json["email"]).to eq(inactive_user.email)
|
||||
expect(json["secondary_emails"]).to eq(inactive_user.secondary_emails)
|
||||
expect(json["associated_accounts"]).to be_present
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
require 'rails_helper'
|
||||
require_dependency 'user'
|
||||
|
||||
describe AdminUserListSerializer do
|
||||
|
||||
context "emails" do
|
||||
let(:admin) { Fabricate(:user_single_email, admin: true, email: "admin@email.com") }
|
||||
let(:user) { Fabricate(:user_single_email, email: "user@email.com") }
|
||||
let(:guardian) { Guardian.new(admin) }
|
||||
|
||||
let(:json) do
|
||||
AdminUserListSerializer.new(user,
|
||||
scope: guardian,
|
||||
root: false
|
||||
).as_json
|
||||
end
|
||||
|
||||
def fabricate_secondary_emails_for(u)
|
||||
["first", "second"].each do |name|
|
||||
Fabricate(:secondary_email, user: u, email: "#{name}@email.com")
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "shown" do |email|
|
||||
it "contains emails" do
|
||||
expect(json[:email]).to eq(email)
|
||||
|
||||
expect(json[:secondary_emails]).to contain_exactly(
|
||||
"first@email.com",
|
||||
"second@email.com"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "not shown" do
|
||||
it "doesn't contain emails" do
|
||||
expect(json[:email]).to eq(nil)
|
||||
expect(json[:secondary_emails]).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "with myself" do
|
||||
let(:user) { admin }
|
||||
|
||||
before do
|
||||
fabricate_secondary_emails_for(admin)
|
||||
end
|
||||
|
||||
include_examples "shown", "admin@email.com"
|
||||
end
|
||||
|
||||
context "with a normal user" do
|
||||
before do
|
||||
fabricate_secondary_emails_for(user)
|
||||
end
|
||||
|
||||
include_examples "not shown"
|
||||
end
|
||||
|
||||
context "with a normal user after clicking 'show emails'" do
|
||||
before do
|
||||
guardian.can_see_emails = true
|
||||
fabricate_secondary_emails_for(user)
|
||||
end
|
||||
|
||||
include_examples "shown", "user@email.com"
|
||||
end
|
||||
|
||||
context "with a staged user" do
|
||||
before do
|
||||
user.staged = true
|
||||
fabricate_secondary_emails_for(user)
|
||||
end
|
||||
|
||||
include_examples "shown", "user@email.com"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,98 @@
|
|||
import { acceptance } from "helpers/qunit-helpers";
|
||||
|
||||
acceptance("Admin - User Emails", { loggedIn: true });
|
||||
|
||||
const responseWithSecondary = secondaryEmails => {
|
||||
return [
|
||||
200,
|
||||
{ "Content-Type": "application/json" },
|
||||
{
|
||||
id: 1,
|
||||
username: "eviltrout",
|
||||
email: "eviltrout@example.com",
|
||||
secondary_emails: secondaryEmails
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const assertNoSecondary = assert => {
|
||||
assert.equal(
|
||||
find(".display-row.email .value a").text(),
|
||||
"eviltrout@example.com",
|
||||
"it should display the primary email"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".display-row.secondary-emails .value").text().trim(),
|
||||
I18n.t("user.email.no_secondary"),
|
||||
"it should not display secondary emails"
|
||||
);
|
||||
};
|
||||
|
||||
const assertMultipleSecondary = assert => {
|
||||
assert.equal(
|
||||
find(".display-row.secondary-emails .value li:first-of-type a").text(),
|
||||
"eviltrout1@example.com",
|
||||
"it should display the first secondary email"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".display-row.secondary-emails .value li:last-of-type a").text(),
|
||||
"eviltrout2@example.com",
|
||||
"it should display the second secondary email"
|
||||
);
|
||||
};
|
||||
|
||||
QUnit.test("viewing self without secondary emails", async assert => {
|
||||
server.get("/admin/users/1.json", () => { // eslint-disable-line no-undef
|
||||
return responseWithSecondary([]);
|
||||
});
|
||||
|
||||
await visit("/admin/users/1/eviltrout");
|
||||
|
||||
assertNoSecondary(assert);
|
||||
});
|
||||
|
||||
QUnit.test("viewing self with multiple secondary emails", async assert => {
|
||||
server.get("/admin/users/1.json", () => { // eslint-disable-line no-undef
|
||||
return responseWithSecondary([
|
||||
"eviltrout1@example.com",
|
||||
"eviltrout2@example.com",
|
||||
]);
|
||||
});
|
||||
|
||||
await visit("/admin/users/1/eviltrout");
|
||||
|
||||
assert.equal(
|
||||
find(".display-row.email .value a").text(),
|
||||
"eviltrout@example.com",
|
||||
"it should display the user's primary email"
|
||||
);
|
||||
|
||||
assertMultipleSecondary(assert);
|
||||
});
|
||||
|
||||
QUnit.test("viewing another user with no secondary email", async assert => {
|
||||
await visit("/admin/users/1234/regular");
|
||||
await click(`.display-row.secondary-emails button`);
|
||||
|
||||
assertNoSecondary(assert);
|
||||
});
|
||||
|
||||
QUnit.test("viewing another account with secondary emails", async assert => {
|
||||
server.get("/u/regular/emails.json", () => { // eslint-disable-line no-undef
|
||||
return [
|
||||
200,
|
||||
{ "Content-Type": "application/json" },
|
||||
{
|
||||
email: "eviltrout@example.com",
|
||||
secondary_emails: ["eviltrout1@example.com", "eviltrout2@example.com"]
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
await visit("/admin/users/1234/regular");
|
||||
await click(`.display-row.secondary-emails button`);
|
||||
|
||||
assertMultipleSecondary(assert);
|
||||
});
|
Loading…
Reference in New Issue