FEATURE: Secondary emails support.

This commit is contained in:
Leo McArdle 2018-07-03 12:51:22 +01:00 committed by Guo Xiang Tan
parent 50e59fb9bd
commit 21ebb1cd54
16 changed files with 307 additions and 14 deletions

View File

@ -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>

View File

@ -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
});
}

View File

@ -29,6 +29,10 @@
&:after {
clear: both;
}
&.secondary-emails ul {
margin: 0;
list-style: none;
}
.field {
font-weight: bold;
width: 17.65765%;

View File

@ -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

View File

@ -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) :

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
});