Merge pull request #4252 from techAPJ/invite-email-improvements
FEATURE: customize invite email message
This commit is contained in:
commit
5c3e36aec2
|
@ -6,6 +6,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
// If this isn't defined, it will proxy to the user model on the preferences
|
// If this isn't defined, it will proxy to the user model on the preferences
|
||||||
// page which is wrong.
|
// page which is wrong.
|
||||||
emailOrUsername: null,
|
emailOrUsername: null,
|
||||||
|
hasCustomMessage: false,
|
||||||
|
customMessage: null,
|
||||||
inviteIcon: "envelope",
|
inviteIcon: "envelope",
|
||||||
|
|
||||||
isAdmin: function(){
|
isAdmin: function(){
|
||||||
|
@ -27,6 +29,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'),
|
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'),
|
||||||
|
|
||||||
disabledCopyLink: function() {
|
disabledCopyLink: function() {
|
||||||
|
if (this.get('hasCustomMessage')) return true;
|
||||||
if (this.get('model.saving')) return true;
|
if (this.get('model.saving')) return true;
|
||||||
if (Ember.isEmpty(this.get('emailOrUsername'))) return true;
|
if (Ember.isEmpty(this.get('emailOrUsername'))) return true;
|
||||||
const emailOrUsername = this.get('emailOrUsername').trim();
|
const emailOrUsername = this.get('emailOrUsername').trim();
|
||||||
|
@ -37,7 +40,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
// when inviting to private topic via email, group name must be specified
|
// when inviting to private topic via email, group name must be specified
|
||||||
if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && Discourse.Utilities.emailValid(emailOrUsername)) return true;
|
if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && Discourse.Utilities.emailValid(emailOrUsername)) return true;
|
||||||
return false;
|
return false;
|
||||||
}.property('emailOrUsername', 'model.saving', 'isPrivateTopic', 'model.groupNames'),
|
}.property('emailOrUsername', 'model.saving', 'isPrivateTopic', 'model.groupNames', 'hasCustomMessage'),
|
||||||
|
|
||||||
buttonTitle: function() {
|
buttonTitle: function() {
|
||||||
return this.get('model.saving') ? 'topic.inviting' : 'topic.invite_reply.action';
|
return this.get('model.saving') ? 'topic.inviting' : 'topic.invite_reply.action';
|
||||||
|
@ -71,6 +74,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && Discourse.SiteSettings.enable_local_logins && !this.get('isMessage');
|
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && Discourse.SiteSettings.enable_local_logins && !this.get('isMessage');
|
||||||
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic'),
|
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic'),
|
||||||
|
|
||||||
|
// Show Custom Message textarea? (only shown when inviting new user to forum)
|
||||||
|
showCustomMessage: function() {
|
||||||
|
return this.get('model') === this.currentUser;
|
||||||
|
}.property('model'),
|
||||||
|
|
||||||
// Instructional text for the modal.
|
// Instructional text for the modal.
|
||||||
inviteInstructions: function() {
|
inviteInstructions: function() {
|
||||||
if (Discourse.SiteSettings.enable_sso || !Discourse.SiteSettings.enable_local_logins) {
|
if (Discourse.SiteSettings.enable_sso || !Discourse.SiteSettings.enable_local_logins) {
|
||||||
|
@ -136,9 +144,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
'topic.invite_private.email_or_username_placeholder';
|
'topic.invite_private.email_or_username_placeholder';
|
||||||
}.property(),
|
}.property(),
|
||||||
|
|
||||||
|
customMessagePlaceholder: function() {
|
||||||
|
return I18n.t('invite.custom_message_placeholder');
|
||||||
|
}.property(),
|
||||||
|
|
||||||
// Reset the modal to allow a new user to be invited.
|
// Reset the modal to allow a new user to be invited.
|
||||||
reset() {
|
reset() {
|
||||||
this.set('emailOrUsername', null);
|
this.set('emailOrUsername', null);
|
||||||
|
this.set('hasCustomMessage', false);
|
||||||
|
this.set('customMessage', null);
|
||||||
this.get('model').setProperties({
|
this.get('model').setProperties({
|
||||||
groupNames: null,
|
groupNames: null,
|
||||||
error: false,
|
error: false,
|
||||||
|
@ -147,7 +161,6 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
inviteLink: null
|
inviteLink: null
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
createInvite() {
|
createInvite() {
|
||||||
|
@ -162,7 +175,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
|
|
||||||
model.setProperties({ saving: true, error: false });
|
model.setProperties({ saving: true, error: false });
|
||||||
|
|
||||||
return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames).then(result => {
|
return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames, this.get('customMessage')).then(result => {
|
||||||
model.setProperties({ saving: false, finished: true });
|
model.setProperties({ saving: false, finished: true });
|
||||||
if (!this.get('invitingToTopic')) {
|
if (!this.get('invitingToTopic')) {
|
||||||
Invite.findInvitedBy(this.currentUser, userInvitedController.get('filter')).then(invite_model => {
|
Invite.findInvitedBy(this.currentUser, userInvitedController.get('filter')).then(invite_model => {
|
||||||
|
@ -213,6 +226,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
}
|
}
|
||||||
model.setProperties({ saving: false, error: true });
|
model.setProperties({ saving: false, error: true });
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showCustomMessageBox() {
|
||||||
|
this.toggleProperty('hasCustomMessage');
|
||||||
|
if (this.get('hasCustomMessage')) {
|
||||||
|
this.set('customMessage', I18n.t('invite.custom_message_template'));
|
||||||
|
} else {
|
||||||
|
this.set('customMessage', null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -321,10 +321,10 @@ const User = RestModel.extend({
|
||||||
Discourse.SiteSettings['newuser_max_' + type + 's'] > 0;
|
Discourse.SiteSettings['newuser_max_' + type + 's'] > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
createInvite(email, group_names) {
|
createInvite(email, group_names, custom_message) {
|
||||||
return Discourse.ajax('/invites', {
|
return Discourse.ajax('/invites', {
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: { email, group_names }
|
data: { email, group_names, custom_message }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,12 @@
|
||||||
<label>{{{groupInstructions}}}</label>
|
<label>{{{groupInstructions}}}</label>
|
||||||
{{group-selector groupFinder=groupFinder groupNames=model.groupNames placeholderKey="topic.invite_private.group_name"}}
|
{{group-selector groupFinder=groupFinder groupNames=model.groupNames placeholderKey="topic.invite_private.group_name"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if showCustomMessage}}
|
||||||
|
<br><label><a {{action "showCustomMessageBox"}}>{{i18n 'invite.custom_message'}}</a></label>
|
||||||
|
{{#if hasCustomMessage}}{{textarea value=customMessage placeholder=customMessagePlaceholder}}{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|
|
@ -43,7 +43,7 @@ class InvitesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
if Invite.invite_by_email(params[:email], current_user, _topic=nil, group_ids)
|
if Invite.invite_by_email(params[:email], current_user, _topic=nil, group_ids, params[:custom_message])
|
||||||
render json: success_json
|
render json: success_json
|
||||||
else
|
else
|
||||||
render json: failed_json, status: 422
|
render json: failed_json, status: 422
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Jobs
|
||||||
raise Discourse::InvalidParameters.new(:invite_id) unless args[:invite_id].present?
|
raise Discourse::InvalidParameters.new(:invite_id) unless args[:invite_id].present?
|
||||||
|
|
||||||
invite = Invite.find_by(id: args[:invite_id])
|
invite = Invite.find_by(id: args[:invite_id])
|
||||||
message = InviteMailer.send_invite(invite)
|
message = InviteMailer.send_invite(invite, args[:custom_message])
|
||||||
Email::Sender.new(message, :invite).send
|
Email::Sender.new(message, :invite).send
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,11 @@ require_dependency 'email/message_builder'
|
||||||
class InviteMailer < ActionMailer::Base
|
class InviteMailer < ActionMailer::Base
|
||||||
include Email::BuildEmailHelper
|
include Email::BuildEmailHelper
|
||||||
|
|
||||||
def send_invite(invite)
|
class UserNotificationRenderer < ActionView::Base
|
||||||
|
include UserNotificationsHelper
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_invite(invite, custom_message=nil)
|
||||||
# Find the first topic they were invited to
|
# Find the first topic they were invited to
|
||||||
first_topic = invite.topics.order(:created_at).first
|
first_topic = invite.topics.order(:created_at).first
|
||||||
|
|
||||||
|
@ -31,15 +35,29 @@ class InviteMailer < ActionMailer::Base
|
||||||
site_description: SiteSetting.site_description,
|
site_description: SiteSetting.site_description,
|
||||||
site_title: SiteSetting.title)
|
site_title: SiteSetting.title)
|
||||||
else
|
else
|
||||||
|
html = nil
|
||||||
|
if custom_message.present? && custom_message =~ /{invite_link}/
|
||||||
|
custom_message.gsub!("{invite_link}", "#{Discourse.base_url}/invites/#{invite.invite_key}")
|
||||||
|
custom_message.gsub!("{site_title}", SiteSetting.title) if custom_message =~ /{site_title}/
|
||||||
|
custom_message.gsub!("{site_description}", SiteSetting.site_description) if custom_message =~ /{site_description}/
|
||||||
|
|
||||||
|
html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render(
|
||||||
|
template: 'email/invite',
|
||||||
|
format: :html,
|
||||||
|
locals: { message: PrettyText.cook(custom_message).html_safe,
|
||||||
|
classes: 'custom-invite-email' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
build_email(invite.email,
|
build_email(invite.email,
|
||||||
template: 'invite_forum_mailer',
|
template: 'invite_forum_mailer',
|
||||||
|
html_override: html,
|
||||||
invitee_name: invitee_name,
|
invitee_name: invitee_name,
|
||||||
site_domain_name: Discourse.current_hostname,
|
site_domain_name: Discourse.current_hostname,
|
||||||
invite_link: "#{Discourse.base_url}/invites/#{invite.invite_key}",
|
invite_link: "#{Discourse.base_url}/invites/#{invite.invite_key}",
|
||||||
site_description: SiteSetting.site_description,
|
site_description: SiteSetting.site_description,
|
||||||
site_title: SiteSetting.title)
|
site_title: SiteSetting.title)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_password_instructions(user)
|
def send_password_instructions(user)
|
||||||
|
|
|
@ -72,8 +72,8 @@ class Invite < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.invite_by_email(email, invited_by, topic=nil, group_ids=nil)
|
def self.invite_by_email(email, invited_by, topic=nil, group_ids=nil, custom_message=nil)
|
||||||
create_invite_by_email(email, invited_by, topic, group_ids, true)
|
create_invite_by_email(email, invited_by, topic, group_ids, true, custom_message)
|
||||||
end
|
end
|
||||||
|
|
||||||
# generate invite link
|
# generate invite link
|
||||||
|
@ -85,7 +85,7 @@ class Invite < ActiveRecord::Base
|
||||||
# Create an invite for a user, supplying an optional topic
|
# 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.
|
# 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, topic=nil, group_ids=nil, send_email=true)
|
def self.create_invite_by_email(email, invited_by, topic=nil, group_ids=nil, send_email=true, custom_message=nil)
|
||||||
lower_email = Email.downcase(email)
|
lower_email = Email.downcase(email)
|
||||||
user = User.find_by(email: lower_email)
|
user = User.find_by(email: lower_email)
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ class Invite < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Jobs.enqueue(:invite_email, invite_id: invite.id) if send_email
|
Jobs.enqueue(:invite_email, invite_id: invite.id, custom_message: custom_message) if send_email
|
||||||
|
|
||||||
invite.reload
|
invite.reload
|
||||||
invite
|
invite
|
||||||
|
|
|
@ -3039,3 +3039,21 @@ en:
|
||||||
top: "There are no more top topics."
|
top: "There are no more top topics."
|
||||||
bookmarks: "There are no more bookmarked topics."
|
bookmarks: "There are no more bookmarked topics."
|
||||||
search: "There are no more search results."
|
search: "There are no more search results."
|
||||||
|
|
||||||
|
invite:
|
||||||
|
custom_message: "Make your invite a little bit more personal by writing a custom message (optional)."
|
||||||
|
custom_message_placeholder: "Enter your custom message, use {invite_link} for specifying invite link."
|
||||||
|
custom_message_template: |
|
||||||
|
Hello,
|
||||||
|
|
||||||
|
You've been invited you to join
|
||||||
|
|
||||||
|
> **{site_title}**
|
||||||
|
>
|
||||||
|
> {site_description}
|
||||||
|
|
||||||
|
If you're interested, click the link below:
|
||||||
|
|
||||||
|
{invite_link}
|
||||||
|
|
||||||
|
This invitation is from a trusted user, so you won't need to log in.
|
||||||
|
|
|
@ -16,7 +16,7 @@ describe Jobs::InviteEmail do
|
||||||
|
|
||||||
it 'delegates to the test mailer' do
|
it 'delegates to the test mailer' do
|
||||||
Email::Sender.any_instance.expects(:send)
|
Email::Sender.any_instance.expects(:send)
|
||||||
InviteMailer.expects(:send_invite).with(invite).returns(mailer)
|
InviteMailer.expects(:send_invite).with(invite, nil).returns(mailer)
|
||||||
Jobs::InviteEmail.new.execute(invite_id: invite.id)
|
Jobs::InviteEmail.new.execute(invite_id: invite.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -26,4 +26,3 @@ describe Jobs::InviteEmail do
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,30 +6,88 @@ describe InviteMailer do
|
||||||
|
|
||||||
context "invite to site" do
|
context "invite to site" do
|
||||||
let(:invite) { Fabricate(:invite) }
|
let(:invite) { Fabricate(:invite) }
|
||||||
let(:invite_mail) { InviteMailer.send_invite(invite) }
|
|
||||||
|
|
||||||
it 'renders the invitee email' do
|
context "default invite message" do
|
||||||
expect(invite_mail.to).to eql([invite.email])
|
let(:invite_mail) { InviteMailer.send_invite(invite) }
|
||||||
|
|
||||||
|
it 'renders the invitee email' do
|
||||||
|
expect(invite_mail.to).to eql([invite.email])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the subject' do
|
||||||
|
expect(invite_mail.subject).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders site domain name in subject' do
|
||||||
|
expect(invite_mail.subject).to match(Discourse.current_hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the body' do
|
||||||
|
expect(invite_mail.body).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the inviter email' do
|
||||||
|
expect(invite_mail.from).to eql([SiteSetting.notification_email])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders invite link' do
|
||||||
|
expect(invite_mail.body.encoded).to match("#{Discourse.base_url}/invites/#{invite.invite_key}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders the subject' do
|
context "custom invite message" do
|
||||||
expect(invite_mail.subject).to be_present
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders site domain name in subject' do
|
context "custom message includes invite link" do
|
||||||
expect(invite_mail.subject).to match(Discourse.current_hostname)
|
let(:custom_invite_mail) { InviteMailer.send_invite(invite, "Hello,\n\nYou've been invited you to join\n\n<a href=\"javascript:alert('HACK!')\">Click me.</a>\n\n> **{site_title}**\n>\n> {site_description}\n\nIf you're interested, click the link below:\n\n{invite_link}\n\nThis invitation is from a trusted user, so you won't need to log in.") }
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders the body' do
|
it 'renders the invitee email' do
|
||||||
expect(invite_mail.body).to be_present
|
expect(custom_invite_mail.to).to eql([invite.email])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders the inviter email' do
|
it 'renders the subject' do
|
||||||
expect(invite_mail.from).to eql([SiteSetting.notification_email])
|
expect(custom_invite_mail.subject).to be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders invite link' do
|
it 'renders site domain name in subject' do
|
||||||
expect(invite_mail.body.encoded).to match("#{Discourse.base_url}/invites/#{invite.invite_key}")
|
expect(custom_invite_mail.subject).to match(Discourse.current_hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the html' do
|
||||||
|
expect(custom_invite_mail.html_part).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders custom_message' do
|
||||||
|
expect(custom_invite_mail.html_part.to_s).to match("You've been invited you to join")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the inviter email' do
|
||||||
|
expect(custom_invite_mail.from).to eql([SiteSetting.notification_email])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sanitizes HTML' do
|
||||||
|
expect(custom_invite_mail.html_part.to_s).to_not match("HACK!")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders invite link' do
|
||||||
|
expect(custom_invite_mail.html_part.to_s).to match("#{Discourse.base_url}/invites/#{invite.invite_key}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "custom message does not include invite link" do
|
||||||
|
let(:custom_invite_without_link) { InviteMailer.send_invite(invite, "Hello,\n\nYou've been invited you to join\n\n> **{site_title}**\n>\n> {site_description}") }
|
||||||
|
|
||||||
|
it 'renders default body' do
|
||||||
|
expect(custom_invite_without_link.body).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not render html' do
|
||||||
|
expect(custom_invite_without_link.html_part).to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders invite link' do
|
||||||
|
expect(custom_invite_without_link.body.encoded).to match("#{Discourse.base_url}/invites/#{invite.invite_key}")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue