mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
FEATURE: customization of html emails (#7934)
This feature adds the ability to customize the HTML part of all emails using a custom HTML template and optionally some CSS to style it. The CSS will be parsed and converted into inline styles because CSS is poorly supported by email clients. When writing the custom HTML and CSS, be aware of what email clients support. Keep customizations very simple. Customizations can be added and edited in Admin > Customize > Email Style. Since the summary email is already heavily styled, there is a setting to disable custom styles for summary emails called "apply custom styles to digest" found in Admin > Settings > Email. As part of this work, RTL locales are now rendered correctly for all emails.
This commit is contained in:
parent
340173eb12
commit
9656a21fdb
1
Gemfile
1
Gemfile
|
@ -78,6 +78,7 @@ gem 'discourse_image_optim', require: 'image_optim'
|
|||
gem 'multi_json'
|
||||
gem 'mustache'
|
||||
gem 'nokogiri'
|
||||
gem 'css_parser', require: false
|
||||
|
||||
gem 'omniauth'
|
||||
gem 'omniauth-openid'
|
||||
|
|
|
@ -88,6 +88,8 @@ GEM
|
|||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.4)
|
||||
css_parser (1.7.0)
|
||||
addressable
|
||||
debug_inspector (0.0.3)
|
||||
diff-lcs (1.3)
|
||||
diffy (3.3.0)
|
||||
|
@ -438,6 +440,7 @@ DEPENDENCIES
|
|||
certified
|
||||
colored2
|
||||
cppjieba_rb
|
||||
css_parser
|
||||
diffy
|
||||
discourse-ember-source (~> 3.10.0)
|
||||
discourse_image_optim
|
||||
|
|
7
app/assets/javascripts/admin/adapters/email-style.js.es6
Normal file
7
app/assets/javascripts/admin/adapters/email-style.js.es6
Normal file
|
@ -0,0 +1,7 @@
|
|||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
pathFor() {
|
||||
return "/admin/customize/email_style";
|
||||
}
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
editorId: Ember.computed.reads("fieldName"),
|
||||
|
||||
@computed("fieldName", "styles.html", "styles.css")
|
||||
resetDisabled(fieldName) {
|
||||
return (
|
||||
this.get(`styles.${fieldName}`) ===
|
||||
this.get(`styles.default_${fieldName}`)
|
||||
);
|
||||
},
|
||||
|
||||
@computed("styles", "fieldName")
|
||||
editorContents: {
|
||||
get(styles, fieldName) {
|
||||
return styles[fieldName];
|
||||
},
|
||||
set(value, styles, fieldName) {
|
||||
styles.setField(fieldName, value);
|
||||
return value;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
reset() {
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.customize.email_style.reset_confirm", {
|
||||
fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`)
|
||||
}),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
result => {
|
||||
if (result) {
|
||||
this.styles.setField(
|
||||
this.fieldName,
|
||||
this.styles.get(`default_${this.fieldName}`)
|
||||
);
|
||||
this.notifyPropertyChange("editorContents");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
@computed("model.isSaving")
|
||||
saveButtonText(isSaving) {
|
||||
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
|
||||
},
|
||||
|
||||
@computed("model.changed", "model.isSaving")
|
||||
saveDisabled(changed, isSaving) {
|
||||
return !changed || isSaving;
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
if (!this.model.saving) {
|
||||
this.set("saving", true);
|
||||
this.model
|
||||
.update(this.model.getProperties("html", "css"))
|
||||
.catch(e => {
|
||||
const msg =
|
||||
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
|
||||
? I18n.t("admin.customize.email_style.save_error_with_reason", {
|
||||
error: e.jqXHR.responseJSON.errors.join(". ")
|
||||
})
|
||||
: I18n.t("generic_error");
|
||||
bootbox.alert(msg);
|
||||
})
|
||||
.finally(() => this.set("model.changed", false));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
10
app/assets/javascripts/admin/models/email-style.js.es6
Normal file
10
app/assets/javascripts/admin/models/email-style.js.es6
Normal file
|
@ -0,0 +1,10 @@
|
|||
import RestModel from "discourse/models/rest";
|
||||
|
||||
export default RestModel.extend({
|
||||
changed: false,
|
||||
|
||||
setField(fieldName, value) {
|
||||
this.set(`${fieldName}`, value);
|
||||
this.set("changed", true);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
export default Ember.Route.extend({
|
||||
model(params) {
|
||||
return {
|
||||
model: this.modelFor("adminCustomizeEmailStyle"),
|
||||
fieldName: params.field_name
|
||||
};
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties({
|
||||
fieldName: model.fieldName,
|
||||
model: model.model
|
||||
});
|
||||
this._shouldAlertUnsavedChanges = true;
|
||||
},
|
||||
|
||||
actions: {
|
||||
willTransition(transition) {
|
||||
if (
|
||||
this.get("controller.model.changed") &&
|
||||
this._shouldAlertUnsavedChanges &&
|
||||
transition.intent.name !== this.routeName
|
||||
) {
|
||||
transition.abort();
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.customize.theme.unsaved_changes_alert"),
|
||||
I18n.t("admin.customize.theme.discard"),
|
||||
I18n.t("admin.customize.theme.stay"),
|
||||
result => {
|
||||
if (!result) {
|
||||
this._shouldAlertUnsavedChanges = false;
|
||||
transition.retry();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return this.store.find("email-style");
|
||||
},
|
||||
|
||||
redirect() {
|
||||
this.transitionTo("adminCustomizeEmailStyle.edit", "html");
|
||||
}
|
||||
});
|
|
@ -90,6 +90,13 @@ export default function() {
|
|||
path: "/robots",
|
||||
resetNamespace: true
|
||||
});
|
||||
this.route(
|
||||
"adminCustomizeEmailStyle",
|
||||
{ path: "/email_style", resetNamespace: true },
|
||||
function() {
|
||||
this.route("edit", { path: "/:field_name" });
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<div class='row'>
|
||||
<div class='admin-controls'>
|
||||
<nav>
|
||||
<ul class='nav nav-pills'>
|
||||
<li>{{#link-to 'adminCustomizeEmailStyle.edit' 'html' replace=true}}{{i18n 'admin.customize.email_style.html'}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeEmailStyle.edit' 'css' replace=true}}{{i18n 'admin.customize.email_style.css'}}{{/link-to}}</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ace-editor content=editorContents mode=fieldName editorId=editorId}}
|
||||
|
||||
<div class='admin-footer'>
|
||||
<div class='buttons'>
|
||||
{{#d-button action=(action "reset") disabled=resetDisabled class='btn-default'}}
|
||||
{{i18n 'admin.customize.email_style.reset'}}
|
||||
{{/d-button}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
{{email-styles-editor styles=model fieldName=fieldName}}
|
||||
|
||||
<div class='admin-footer'>
|
||||
<div class='buttons'>
|
||||
{{#d-button action=(action "save") disabled=saveDisabled class='btn-primary'}}
|
||||
{{saveButtonText}}
|
||||
{{/d-button}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,7 @@
|
|||
<div class='row'>
|
||||
<h2>{{i18n 'admin.customize.email_style.heading'}}</h2>
|
||||
|
||||
<p>{{i18n 'admin.customize.email_style.instructions'}}</p>
|
||||
</div>
|
||||
|
||||
{{outlet}}
|
|
@ -3,6 +3,7 @@
|
|||
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
|
||||
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
|
||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
||||
{{nav-item route='adminCustomizeEmailStyle' label='admin.customize.email_style.title'}}
|
||||
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
|
||||
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
|
||||
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
|
||||
|
|
|
@ -790,3 +790,18 @@
|
|||
height: 55vh;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-customize-email-style {
|
||||
.ace-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
.ace_editor {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
16
app/controllers/admin/email_styles_controller.rb
Normal file
16
app/controllers/admin/email_styles_controller.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::EmailStylesController < Admin::AdminController
|
||||
def show
|
||||
render_serialized(EmailStyle.new, EmailStyleSerializer)
|
||||
end
|
||||
|
||||
def update
|
||||
updater = EmailStyleUpdater.new(current_user)
|
||||
if updater.update(params.require(:email_style).permit(:html, :css))
|
||||
render_serialized(EmailStyle.new, EmailStyleSerializer)
|
||||
else
|
||||
render_json_error(updater.errors, status: 422)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'erb'
|
||||
|
||||
module EmailHelper
|
||||
|
||||
def mailing_list_topic(topic, post_count)
|
||||
|
@ -23,6 +25,14 @@ module EmailHelper
|
|||
raw "<a href='#{Discourse.base_url}#{url}' style='color: ##{@anchor_color}'>#{title}</a>"
|
||||
end
|
||||
|
||||
def email_html_template(binding_arg)
|
||||
template = EmailStyle.new.html.sub(
|
||||
'%{email_content}',
|
||||
'<%= yield %><% if defined?(html_body) %><%= html_body %><% end %>'
|
||||
)
|
||||
ERB.new(template).result(binding_arg)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def extract_details(topic)
|
||||
|
|
|
@ -5,10 +5,7 @@ require_dependency 'email/message_builder'
|
|||
class InviteMailer < ActionMailer::Base
|
||||
include Email::BuildEmailHelper
|
||||
|
||||
class UserNotificationRenderer < ActionView::Base
|
||||
include UserNotificationsHelper
|
||||
include EmailHelper
|
||||
end
|
||||
layout 'email_template'
|
||||
|
||||
def send_invite(invite)
|
||||
# Find the first topic they were invited to
|
||||
|
|
|
@ -12,6 +12,7 @@ class UserNotifications < ActionMailer::Base
|
|||
include ApplicationHelper
|
||||
helper :application, :email
|
||||
default charset: 'UTF-8'
|
||||
layout 'email_template'
|
||||
|
||||
include Email::BuildEmailHelper
|
||||
|
||||
|
@ -362,11 +363,6 @@ class UserNotifications < ActionMailer::Base
|
|||
result
|
||||
end
|
||||
|
||||
class UserNotificationRenderer < ActionView::Base
|
||||
include UserNotificationsHelper
|
||||
include EmailHelper
|
||||
end
|
||||
|
||||
def self.get_context_posts(post, topic_user, user)
|
||||
if (user.user_option.email_previous_replies == UserOption.previous_replies_type[:never]) ||
|
||||
SiteSetting.private_email?
|
||||
|
@ -580,15 +576,7 @@ class UserNotifications < ActionMailer::Base
|
|||
site_description: SiteSetting.site_description
|
||||
)
|
||||
|
||||
unless translation_override_exists
|
||||
html = UserNotificationRenderer.with_view_paths(Rails.configuration.paths["app/views"]).render(
|
||||
template: 'email/invite',
|
||||
format: :html,
|
||||
locals: { message: PrettyText.cook(message, sanitize: false).html_safe,
|
||||
classes: Rtl.new(user).css_class
|
||||
}
|
||||
)
|
||||
end
|
||||
html = PrettyText.cook(message, sanitize: false).html_safe
|
||||
else
|
||||
reached_limit = SiteSetting.max_emails_per_day_per_user > 0
|
||||
reached_limit &&= (EmailLog.where(user_id: user.id)
|
||||
|
@ -608,7 +596,6 @@ class UserNotifications < ActionMailer::Base
|
|||
end
|
||||
|
||||
unless translation_override_exists
|
||||
|
||||
html = UserNotificationRenderer.with_view_paths(Rails.configuration.paths["app/views"]).render(
|
||||
template: 'email/notification',
|
||||
format: :html,
|
||||
|
@ -651,7 +638,6 @@ class UserNotifications < ActionMailer::Base
|
|||
site_description: SiteSetting.site_description,
|
||||
site_title: SiteSetting.title,
|
||||
site_title_url_encoded: URI.encode(SiteSetting.title),
|
||||
style: :notification,
|
||||
locale: locale
|
||||
}
|
||||
|
||||
|
@ -689,13 +675,6 @@ class UserNotifications < ActionMailer::Base
|
|||
@anchor_color = ColorScheme.hex_for_name('tertiary')
|
||||
@markdown_linker = MarkdownLinker.new(@base_url)
|
||||
@unsubscribe_key = UnsubscribeKey.create_key_for(@user, "digest")
|
||||
end
|
||||
|
||||
def apply_notification_styles(email)
|
||||
email.html_part.body = Email::Styles.new(email.html_part.body.to_s).tap do |styles|
|
||||
styles.format_basic
|
||||
styles.format_notification
|
||||
end.to_html
|
||||
email
|
||||
@disable_email_custom_styles = !SiteSetting.apply_custom_styles_to_digest
|
||||
end
|
||||
end
|
||||
|
|
37
app/models/email_style.rb
Normal file
37
app/models/email_style.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class EmailStyle
|
||||
include ActiveModel::Serialization
|
||||
|
||||
attr_accessor :html, :css, :default_html, :default_css
|
||||
|
||||
def id
|
||||
'email-style'
|
||||
end
|
||||
|
||||
def html
|
||||
SiteSetting.email_custom_template.presence || default_html
|
||||
end
|
||||
|
||||
def css
|
||||
SiteSetting.email_custom_css || default_css
|
||||
end
|
||||
|
||||
def default_html
|
||||
self.class.default_template
|
||||
end
|
||||
|
||||
def default_css
|
||||
self.class.default_css
|
||||
end
|
||||
|
||||
def self.default_template
|
||||
@_default_template ||= File.read(
|
||||
File.join(Rails.root, 'app', 'views', 'email', 'default_template.html')
|
||||
)
|
||||
end
|
||||
|
||||
def self.default_css
|
||||
''
|
||||
end
|
||||
end
|
5
app/serializers/email_style_serializer.rb
Normal file
5
app/serializers/email_style_serializer.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class EmailStyleSerializer < ApplicationSerializer
|
||||
attributes :id, :html, :css, :default_html, :default_css
|
||||
end
|
38
app/services/email_style_updater.rb
Normal file
38
app/services/email_style_updater.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class EmailStyleUpdater
|
||||
|
||||
attr_reader :errors
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
@errors = []
|
||||
end
|
||||
|
||||
def update(attrs)
|
||||
if attrs.has_key?(:html)
|
||||
if attrs[:html] == EmailStyle.default_template
|
||||
SiteSetting.remove_override!(:email_custom_template)
|
||||
else
|
||||
if !attrs[:html].include?('%{email_content}')
|
||||
@errors << I18n.t(
|
||||
'email_style.html_missing_placeholder',
|
||||
placeholder: '%{email_content}'
|
||||
)
|
||||
else
|
||||
SiteSetting.email_custom_template = attrs[:html]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if attrs.has_key?(:css)
|
||||
if attrs[:css] == EmailStyle.default_css
|
||||
SiteSetting.remove_override!(:email_custom_css)
|
||||
else
|
||||
SiteSetting.email_custom_css = attrs[:css]
|
||||
end
|
||||
end
|
||||
|
||||
@errors.empty?
|
||||
end
|
||||
end
|
7
app/services/user_notification_renderer.rb
Normal file
7
app/services/user_notification_renderer.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserNotificationRenderer < ActionView::Base
|
||||
include ApplicationHelper
|
||||
include UserNotificationsHelper
|
||||
include EmailHelper
|
||||
end
|
24
app/views/email/default_template.html
Normal file
24
app/views/email/default_template.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="%{html_lang}" xml:lang="%{html_lang}">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-type" name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !important;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
%{email_content}
|
||||
|
||||
<!-- prevent Gmail on iOS font size manipulation -->
|
||||
<div style="display:none;white-space:nowrap;font:15px courier;line-height:0">
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,11 +0,0 @@
|
|||
<div id='main' class=<%= classes %>>
|
||||
|
||||
<div class='header-instructions'>%{header_instructions}</div>
|
||||
|
||||
<% if message.present? %>
|
||||
<div><%= message %></div>
|
||||
<% end %>
|
||||
|
||||
<div class='footer'>%{respond_instructions}</div>
|
||||
|
||||
</div>
|
|
@ -1,13 +0,0 @@
|
|||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="padding: 10px;">
|
||||
<a href="<%= Discourse.base_url %>">
|
||||
<img src="<%= logo_url %>" style="max-width:100%"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="background-color: #fff; padding: 10px 10px; font-family: Arial, Helvetica, sans-serif; font-size: 14px;">
|
||||
<%= raw(html_body) %>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
6
app/views/layouts/email_template.html.erb
Normal file
6
app/views/layouts/email_template.html.erb
Normal file
|
@ -0,0 +1,6 @@
|
|||
<% if @disable_email_custom_styles %>
|
||||
<%= yield %>
|
||||
<% if defined?(html_body) %><%= html_body %><% end %>
|
||||
<% else %>
|
||||
<%= email_html_template(binding).html_safe %>
|
||||
<% end %>
|
|
@ -1,19 +1,4 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="<%= html_lang %>" xml:lang="<%= html_lang %>">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-type" name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body dir="<%= rtl? ? 'rtl' : 'ltr' %>" style="-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;text-align:<%= rtl? ? 'right' : 'left' %>;width:100%">
|
||||
|
||||
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !important;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<div class="summary-email">
|
||||
|
||||
<span class="preheader" style="display:none!important;color:#f3f3f3;font-size:1px;line-height:1px;max-height:0;max-width:0;mso-hide:all!important;opacity:0;overflow:hidden;visibility:hidden">
|
||||
<%= @preheader_text %>
|
||||
|
@ -425,10 +410,4 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
|
|||
|
||||
<%= digest_custom_html("below_footer") %>
|
||||
|
||||
<!-- prevent Gmail on iOS font size manipulation -->
|
||||
<div style="display:none;white-space:nowrap;font:15px courier;line-height:0">
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</div>
|
||||
|
|
|
@ -3644,6 +3644,16 @@ en:
|
|||
title: "Override your site's robots.txt file:"
|
||||
warning: "This will permanently override any related site settings."
|
||||
overridden: Your site's default robots.txt file is overridden.
|
||||
email_style:
|
||||
title: "Email Style"
|
||||
heading: "Customize Email Style"
|
||||
html: "HTML Template"
|
||||
css: "CSS"
|
||||
reset: "Reset to default"
|
||||
reset_confirm: "Are you sure you want to reset to the default %{fieldName} and lose all your changes?"
|
||||
save_error_with_reason: "Your changes were not saved. %{error}"
|
||||
instructions: "Customize the template in which all html emails are rendered, and style using CSS."
|
||||
|
||||
email:
|
||||
title: "Emails"
|
||||
settings: "Settings"
|
||||
|
|
|
@ -1854,6 +1854,7 @@ en:
|
|||
suppress_digest_email_after_days: "Suppress summary emails for users not seen on the site for more than (n) days."
|
||||
digest_suppress_categories: "Suppress these categories from summary emails."
|
||||
disable_digest_emails: "Disable summary emails for all users."
|
||||
apply_custom_styles_to_digest: "Custom email template and css are applied to summary emails."
|
||||
email_accent_bg_color: "The accent color to be used as the background of some elements in HTML emails. Enter a color name ('red') or hex value ('#FF0000')."
|
||||
email_accent_fg_color: "The color of text rendered on the email bg color in HTML emails. Enter a color name ('white') or hex value ('#FFFFFF')."
|
||||
email_link_color: "The color of links in HTML emails. Enter a color name ('blue') or hex value ('#0000FF')."
|
||||
|
@ -4554,3 +4555,6 @@ en:
|
|||
title: "Delete User"
|
||||
confirm: "Are you sure you want to delete that user? This will remove all of their posts and block their email and IP address."
|
||||
reason: "Deleted via review queue"
|
||||
|
||||
email_style:
|
||||
html_missing_placeholder: "The html template must include %{placeholder}"
|
||||
|
|
|
@ -244,6 +244,9 @@ Discourse::Application.routes.draw do
|
|||
get 'robots' => 'robots_txt#show'
|
||||
put 'robots.json' => 'robots_txt#update'
|
||||
delete 'robots.json' => 'robots_txt#reset'
|
||||
|
||||
resource :email_style, only: [:show, :update]
|
||||
get 'email_style/:field' => 'email_styles#show', constraints: { field: /html|css/ }
|
||||
end
|
||||
|
||||
resources :embeddable_hosts, constraints: AdminConstraint.new
|
||||
|
|
|
@ -911,6 +911,7 @@ email:
|
|||
disable_digest_emails:
|
||||
default: false
|
||||
client: true
|
||||
apply_custom_styles_to_digest: true
|
||||
email_accent_bg_color: "#2F70AC"
|
||||
email_accent_fg_color: "#FFFFFF"
|
||||
email_link_color: "#006699"
|
||||
|
@ -1024,6 +1025,12 @@ email:
|
|||
enable_forwarded_emails: false
|
||||
always_show_trimmed_content: false
|
||||
private_email: false
|
||||
email_custom_template:
|
||||
default: ""
|
||||
hidden: true
|
||||
email_custom_css:
|
||||
default: ""
|
||||
hidden: true
|
||||
email_total_attachment_size_limit_kb:
|
||||
default: 0
|
||||
max: 51200
|
||||
|
|
|
@ -107,16 +107,17 @@ module Email
|
|||
html_override.gsub!("%{respond_instructions}", "")
|
||||
end
|
||||
|
||||
styled = Email::Styles.new(html_override, @opts)
|
||||
styled.format_basic
|
||||
|
||||
if style = @opts[:style]
|
||||
styled.public_send("format_#{style}")
|
||||
end
|
||||
html = UserNotificationRenderer.with_view_paths(
|
||||
Rails.configuration.paths["app/views"]
|
||||
).render(
|
||||
template: 'layouts/email_template',
|
||||
format: :html,
|
||||
locals: { html_body: html_override.html_safe }
|
||||
)
|
||||
|
||||
Mail::Part.new do
|
||||
content_type 'text/html; charset=UTF-8'
|
||||
body styled.to_html
|
||||
body html
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -17,15 +17,21 @@ module Email
|
|||
end
|
||||
|
||||
def html
|
||||
if @message.html_part
|
||||
style = Email::Styles.new(@message.html_part.body.to_s, @opts)
|
||||
style.format_basic
|
||||
style.format_html
|
||||
style = if @message.html_part
|
||||
Email::Styles.new(@message.html_part.body.to_s, @opts)
|
||||
else
|
||||
style = Email::Styles.new(PrettyText.cook(text), @opts)
|
||||
style.format_basic
|
||||
unstyled = UserNotificationRenderer.with_view_paths(
|
||||
Rails.configuration.paths["app/views"]
|
||||
).render(
|
||||
template: 'layouts/email_template',
|
||||
format: :html,
|
||||
locals: { html_body: PrettyText.cook(text).html_safe }
|
||||
)
|
||||
Email::Styles.new(unstyled, @opts)
|
||||
end
|
||||
|
||||
style.format_basic
|
||||
style.format_html
|
||||
style.to_html
|
||||
end
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ module Email
|
|||
@html = html
|
||||
@opts = opts || {}
|
||||
@fragment = Nokogiri::HTML.fragment(@html)
|
||||
@custom_styles = nil
|
||||
end
|
||||
|
||||
def self.register_plugin_style(&block)
|
||||
|
@ -32,6 +33,26 @@ module Email
|
|||
end
|
||||
end
|
||||
|
||||
def custom_styles
|
||||
return @custom_styles unless @custom_styles.nil?
|
||||
|
||||
css = EmailStyle.new.css
|
||||
@custom_styles = {}
|
||||
|
||||
if !css.blank?
|
||||
require 'css_parser' unless defined?(CssParser)
|
||||
|
||||
parser = CssParser::Parser.new(import: false)
|
||||
parser.load_string!(css)
|
||||
parser.each_selector do |selector, value|
|
||||
@custom_styles[selector] ||= +''
|
||||
@custom_styles[selector] << value
|
||||
end
|
||||
end
|
||||
|
||||
@custom_styles
|
||||
end
|
||||
|
||||
def format_basic
|
||||
uri = URI(Discourse.base_url)
|
||||
|
||||
|
@ -83,29 +104,6 @@ module Email
|
|||
end
|
||||
end
|
||||
|
||||
def format_notification
|
||||
style('.previous-discussion', 'font-size: 17px; color: #444; margin-bottom:10px;')
|
||||
style('.notification-date', "text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px")
|
||||
style('.username', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#{SiteSetting.email_link_color};text-decoration:none;font-weight:bold")
|
||||
style('.user-title', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: #999;")
|
||||
style('.user-name', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: #{SiteSetting.email_link_color};font-weight:normal;")
|
||||
style('.post-wrapper', "margin-bottom:25px;")
|
||||
style('.user-avatar', 'vertical-align:top;width:55px;')
|
||||
style('.user-avatar img', nil, width: '45', height: '45')
|
||||
style('hr', 'background-color: #ddd; height: 1px; border: 1px;')
|
||||
style('.rtl', 'direction: rtl;')
|
||||
style('div.body', 'padding-top:5px;')
|
||||
style('.whisper div.body', 'font-style: italic; color: #9c9c9c;')
|
||||
style('.lightbox-wrapper .meta', 'display: none')
|
||||
correct_first_body_margin
|
||||
correct_footer_style
|
||||
style('div.undecorated-link-footer a', "font-weight: normal;")
|
||||
correct_footer_style_hilight_first
|
||||
reset_tables
|
||||
onebox_styles
|
||||
plugin_styles
|
||||
end
|
||||
|
||||
def onebox_styles
|
||||
# Links to other topics
|
||||
style('aside.quote', 'padding: 12px 25px 2px 12px; margin-bottom: 10px;')
|
||||
|
@ -164,6 +162,16 @@ module Email
|
|||
end
|
||||
|
||||
def format_html
|
||||
html_lang = SiteSetting.default_locale.sub("_", "-")
|
||||
style('html', nil, lang: html_lang, 'xml:lang' => html_lang)
|
||||
style('body', "text-align:#{ Rtl.new(nil).enabled? ? 'right' : 'left' };")
|
||||
style('body', nil, dir: Rtl.new(nil).enabled? ? 'rtl' : 'ltr')
|
||||
|
||||
style('.with-dir',
|
||||
"text-align:#{ Rtl.new(nil).enabled? ? 'right' : 'left' };",
|
||||
dir: Rtl.new(nil).enabled? ? 'rtl' : 'ltr'
|
||||
)
|
||||
|
||||
style('.with-accent-colors', "background-color: #{SiteSetting.email_accent_bg_color}; color: #{SiteSetting.email_accent_fg_color};")
|
||||
style('h4', 'color: #222;')
|
||||
style('h3', 'margin: 15px 0 20px 0;')
|
||||
|
@ -177,11 +185,39 @@ module Email
|
|||
style('code', 'background-color: #f1f1ff; padding: 2px 5px;')
|
||||
style('pre code', 'display: block; background-color: #f1f1ff; padding: 5px;')
|
||||
style('.featured-topic a', "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color}; line-height:1.5em;")
|
||||
style('.summary-email', "-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;width:100%")
|
||||
|
||||
style('.previous-discussion', 'font-size: 17px; color: #444; margin-bottom:10px;')
|
||||
style('.notification-date', "text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px")
|
||||
style('.username', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#{SiteSetting.email_link_color};text-decoration:none;font-weight:bold")
|
||||
style('.user-title', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: #999;")
|
||||
style('.user-name', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: #{SiteSetting.email_link_color};font-weight:normal;")
|
||||
style('.post-wrapper', "margin-bottom:25px;")
|
||||
style('.user-avatar', 'vertical-align:top;width:55px;')
|
||||
style('.user-avatar img', nil, width: '45', height: '45')
|
||||
style('hr', 'background-color: #ddd; height: 1px; border: 1px;')
|
||||
style('.rtl', 'direction: rtl;')
|
||||
style('div.body', 'padding-top:5px;')
|
||||
style('.whisper div.body', 'font-style: italic; color: #9c9c9c;')
|
||||
style('.lightbox-wrapper .meta', 'display: none')
|
||||
correct_first_body_margin
|
||||
correct_footer_style
|
||||
style('div.undecorated-link-footer a', "font-weight: normal;")
|
||||
correct_footer_style_hilight_first
|
||||
reset_tables
|
||||
|
||||
onebox_styles
|
||||
plugin_styles
|
||||
|
||||
style('.post-excerpt img', "max-width: 50%; max-height: 400px;")
|
||||
|
||||
format_custom
|
||||
end
|
||||
|
||||
def format_custom
|
||||
custom_styles.each do |selector, value|
|
||||
style(selector, value)
|
||||
end
|
||||
end
|
||||
|
||||
# this method is reserved for styles specific to plugin
|
||||
|
@ -240,7 +276,7 @@ module Email
|
|||
end
|
||||
|
||||
def correct_first_body_margin
|
||||
@fragment.css('.body p').each do |element|
|
||||
@fragment.css('div.body p').each do |element|
|
||||
element['style'] = "margin-top:0; border: 0;"
|
||||
end
|
||||
end
|
||||
|
|
122
spec/integration/email_style_spec.rb
Normal file
122
spec/integration/email_style_spec.rb
Normal file
|
@ -0,0 +1,122 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
describe EmailStyle do
|
||||
before do
|
||||
SiteSetting.email_custom_template = "<body><h1>FOR YOU</h1><div>%{email_content}</div></body>"
|
||||
SiteSetting.email_custom_css = 'h1 { color: red; } div.body { color: #FAB; }'
|
||||
end
|
||||
|
||||
after do
|
||||
SiteSetting.remove_override!(:email_custom_template)
|
||||
SiteSetting.remove_override!(:email_custom_css)
|
||||
end
|
||||
|
||||
context 'invite' do
|
||||
fab!(:invite) { Fabricate(:invite) }
|
||||
let(:invite_mail) { InviteMailer.send_invite(invite) }
|
||||
|
||||
subject(:mail_html) { Email::Renderer.new(invite_mail).html }
|
||||
|
||||
it 'applies customizations' do
|
||||
expect(mail_html.scan('<h1 style="color: red;">FOR YOU</h1>').count).to eq(1)
|
||||
expect(mail_html).to match("#{Discourse.base_url}/invites/#{invite.invite_key}")
|
||||
end
|
||||
|
||||
it 'can apply RTL attrs' do
|
||||
SiteSetting.default_locale = 'he'
|
||||
body_attrs = mail_html.match(/<body ([^>])+/)
|
||||
expect(body_attrs[0]&.downcase).to match(/text-align:\s*right/)
|
||||
expect(body_attrs[0]&.downcase).to include('dir="rtl"')
|
||||
end
|
||||
end
|
||||
|
||||
context 'user_replied' do
|
||||
let(:response_by_user) { Fabricate(:user, name: "John Doe") }
|
||||
let(:category) { Fabricate(:category, name: 'India') }
|
||||
let(:topic) { Fabricate(:topic, category: category, title: "Super cool topic") }
|
||||
let(:post) { Fabricate(:post, topic: topic, raw: 'This is My super duper cool topic') }
|
||||
let(:response) { Fabricate(:basic_reply, topic: post.topic, user: response_by_user) }
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:notification) { Fabricate(:replied_notification, user: user, post: response) }
|
||||
|
||||
let(:mail) do
|
||||
UserNotifications.user_replied(
|
||||
user,
|
||||
post: response,
|
||||
notification_type: notification.notification_type,
|
||||
notification_data_hash: notification.data_hash
|
||||
)
|
||||
end
|
||||
|
||||
subject(:mail_html) { Email::Renderer.new(mail).html }
|
||||
|
||||
it "customizations are applied to html part of emails" do
|
||||
expect(mail_html.scan('<h1 style="color: red;">FOR YOU</h1>').count).to eq(1)
|
||||
matches = mail_html.match(/<div style="([^"]+)">#{post.raw}/)
|
||||
expect(matches[1]).to include('color: #FAB;') # custom
|
||||
expect(matches[1]).to include('padding-top:5px;') # div.body
|
||||
end
|
||||
|
||||
# TODO: translation override
|
||||
end
|
||||
|
||||
context 'signup' do
|
||||
let(:signup_mail) { UserNotifications.signup(Fabricate(:user)) }
|
||||
subject(:mail_html) { Email::Renderer.new(signup_mail).html }
|
||||
|
||||
it "customizations are applied to html part of emails" do
|
||||
expect(mail_html.scan('<h1 style="color: red;">FOR YOU</h1>').count).to eq(1)
|
||||
expect(mail_html).to include('activate-account')
|
||||
end
|
||||
|
||||
context 'translation override' do
|
||||
before do
|
||||
TranslationOverride.upsert!(
|
||||
'en',
|
||||
'user_notifications.signup.text_body_template',
|
||||
"CLICK THAT LINK: %{base_url}/u/activate-account/%{email_token}"
|
||||
)
|
||||
end
|
||||
|
||||
after do
|
||||
TranslationOverride.revert!('en', ['user_notifications.signup.text_body_template'])
|
||||
end
|
||||
|
||||
it "applies customizations when translation override exists" do
|
||||
expect(mail_html.scan('<h1 style="color: red;">FOR YOU</h1>').count).to eq(1)
|
||||
expect(mail_html.scan('CLICK THAT LINK').count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with some bad css' do
|
||||
before do
|
||||
SiteSetting.email_custom_css = '@import "nope.css"; h1 {{{ size: really big; '
|
||||
end
|
||||
|
||||
it "can render the html" do
|
||||
expect(mail_html.scan(/<h1\s*(?:style=""){0,1}>FOR YOU<\/h1>/).count).to eq(1)
|
||||
expect(mail_html).to include('activate-account')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'digest' do
|
||||
fab!(:popular_topic) { Fabricate(:topic, user: Fabricate(:coding_horror), created_at: 1.hour.ago) }
|
||||
let(:summary_email) { UserNotifications.digest(Fabricate(:user)) }
|
||||
subject(:mail_html) { Email::Renderer.new(summary_email).html }
|
||||
|
||||
it "customizations are applied to html part of emails" do
|
||||
expect(mail_html.scan('<h1 style="color: red;">FOR YOU</h1>').count).to eq(1)
|
||||
expect(mail_html).to include(popular_topic.title)
|
||||
end
|
||||
|
||||
it "doesn't apply customizations if apply_custom_styles_to_digest is disabled" do
|
||||
SiteSetting.apply_custom_styles_to_digest = false
|
||||
expect(mail_html).to_not include('<h1 style="color: red;">FOR YOU</h1>')
|
||||
expect(mail_html).to_not include('FOR YOU')
|
||||
expect(mail_html).to include(popular_topic.title)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -260,7 +260,7 @@ describe UserNotifications do
|
|||
expect(mail.subject).to match(/Taggo/)
|
||||
expect(mail.subject).to match(/Taggie/)
|
||||
|
||||
mail_html = mail.html_part.to_s
|
||||
mail_html = mail.html_part.body.to_s
|
||||
|
||||
expect(mail_html.scan(/My super duper cool topic/).count).to eq(1)
|
||||
expect(mail_html.scan(/In Reply To/).count).to eq(1)
|
||||
|
@ -287,7 +287,7 @@ describe UserNotifications do
|
|||
notification_data_hash: notification.data_hash
|
||||
)
|
||||
|
||||
expect(mail.html_part.to_s.scan(/In Reply To/).count).to eq(0)
|
||||
expect(mail.html_part.body.to_s.scan(/In Reply To/).count).to eq(0)
|
||||
|
||||
SiteSetting.enable_names = true
|
||||
SiteSetting.display_name_on_posts = true
|
||||
|
@ -304,7 +304,7 @@ describe UserNotifications do
|
|||
notification_data_hash: notification.data_hash
|
||||
)
|
||||
|
||||
mail_html = mail.html_part.to_s
|
||||
mail_html = mail.html_part.body.to_s
|
||||
expect(mail_html.scan(/>Bob Marley/).count).to eq(1)
|
||||
expect(mail_html.scan(/>bobmarley/).count).to eq(0)
|
||||
|
||||
|
@ -317,7 +317,7 @@ describe UserNotifications do
|
|||
notification_data_hash: notification.data_hash
|
||||
)
|
||||
|
||||
mail_html = mail.html_part.to_s
|
||||
mail_html = mail.html_part.body.to_s
|
||||
expect(mail_html.scan(/>Bob Marley/).count).to eq(0)
|
||||
expect(mail_html.scan(/>bobmarley/).count).to eq(1)
|
||||
end
|
||||
|
@ -331,8 +331,8 @@ describe UserNotifications do
|
|||
notification_data_hash: notification.data_hash
|
||||
)
|
||||
|
||||
expect(mail.html_part.to_s).to_not include(response.raw)
|
||||
expect(mail.html_part.to_s).to_not include(topic.url)
|
||||
expect(mail.html_part.body.to_s).to_not include(response.raw)
|
||||
expect(mail.html_part.body.to_s).to_not include(topic.url)
|
||||
expect(mail.text_part.to_s).to_not include(response.raw)
|
||||
expect(mail.text_part.to_s).to_not include(topic.url)
|
||||
end
|
||||
|
@ -365,10 +365,10 @@ describe UserNotifications do
|
|||
expect(mail.subject).not_to match(/Uncategorized/)
|
||||
|
||||
# 1 respond to links as no context by default
|
||||
expect(mail.html_part.to_s.scan(/to respond/).count).to eq(1)
|
||||
expect(mail.html_part.body.to_s.scan(/to respond/).count).to eq(1)
|
||||
|
||||
# 1 unsubscribe link
|
||||
expect(mail.html_part.to_s.scan(/To unsubscribe/).count).to eq(1)
|
||||
expect(mail.html_part.body.to_s.scan(/To unsubscribe/).count).to eq(1)
|
||||
|
||||
# side effect, topic user is updated with post number
|
||||
tu = TopicUser.get(post.topic_id, user)
|
||||
|
@ -384,7 +384,7 @@ describe UserNotifications do
|
|||
notification_data_hash: notification.data_hash
|
||||
)
|
||||
|
||||
expect(mail.html_part.to_s).to_not include(response.raw)
|
||||
expect(mail.html_part.body.to_s).to_not include(response.raw)
|
||||
expect(mail.text_part.to_s).to_not include(response.raw)
|
||||
end
|
||||
|
||||
|
@ -451,13 +451,13 @@ describe UserNotifications do
|
|||
expect(mail.subject).to include("[PM] ")
|
||||
|
||||
# 1 "visit message" link
|
||||
expect(mail.html_part.to_s.scan(/Visit Message/).count).to eq(1)
|
||||
expect(mail.html_part.body.to_s.scan(/Visit Message/).count).to eq(1)
|
||||
|
||||
# 1 respond to link
|
||||
expect(mail.html_part.to_s.scan(/to respond/).count).to eq(1)
|
||||
expect(mail.html_part.body.to_s.scan(/to respond/).count).to eq(1)
|
||||
|
||||
# 1 unsubscribe link
|
||||
expect(mail.html_part.to_s.scan(/To unsubscribe/).count).to eq(1)
|
||||
expect(mail.html_part.body.to_s.scan(/To unsubscribe/).count).to eq(1)
|
||||
|
||||
# side effect, topic user is updated with post number
|
||||
tu = TopicUser.get(topic.id, user)
|
||||
|
@ -473,8 +473,8 @@ describe UserNotifications do
|
|||
notification_data_hash: notification.data_hash
|
||||
)
|
||||
|
||||
expect(mail.html_part.to_s).to_not include(response.raw)
|
||||
expect(mail.html_part.to_s).to_not include(topic.url)
|
||||
expect(mail.html_part.body.to_s).to_not include(response.raw)
|
||||
expect(mail.html_part.body.to_s).to_not include(topic.url)
|
||||
expect(mail.text_part.to_s).to_not include(response.raw)
|
||||
expect(mail.text_part.to_s).to_not include(topic.url)
|
||||
end
|
||||
|
@ -635,7 +635,7 @@ describe UserNotifications do
|
|||
|
||||
# WARNING: you reached the limit of 100 email notifications per day. Further emails will be suppressed.
|
||||
# Consider watching less topics or disabling mailing list mode.
|
||||
expect(mail.html_part.to_s).to match(I18n.t("user_notifications.reached_limit", count: 2))
|
||||
expect(mail.html_part.body.to_s).to match(I18n.t("user_notifications.reached_limit", count: 2))
|
||||
expect(mail.body.to_s).to match(I18n.t("user_notifications.reached_limit", count: 2))
|
||||
end
|
||||
|
||||
|
|
71
spec/requests/admin/email_styles_controller_spec.rb
Normal file
71
spec/requests/admin/email_styles_controller_spec.rb
Normal file
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Admin::EmailStylesController do
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
let(:default_html) { File.read("#{Rails.root}/app/views/email/default_template.html") }
|
||||
let(:default_css) { "" }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
after do
|
||||
SiteSetting.remove_override!(:email_custom_template)
|
||||
SiteSetting.remove_override!(:email_custom_css)
|
||||
end
|
||||
|
||||
describe 'show' do
|
||||
it 'returns default values' do
|
||||
get '/admin/customize/email_style.json'
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
json = ::JSON.parse(response.body)['email_style']
|
||||
expect(json['html']).to eq(default_html)
|
||||
expect(json['css']).to eq(default_css)
|
||||
end
|
||||
|
||||
it 'returns customized values' do
|
||||
SiteSetting.email_custom_template = "For you: %{email_content}"
|
||||
SiteSetting.email_custom_css = ".user-name { font-size: 24px; }"
|
||||
get '/admin/customize/email_style.json'
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
json = ::JSON.parse(response.body)['email_style']
|
||||
expect(json['html']).to eq("For you: %{email_content}")
|
||||
expect(json['css']).to eq(".user-name { font-size: 24px; }")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'update' do
|
||||
let(:valid_params) do
|
||||
{
|
||||
html: 'For you: %{email_content}',
|
||||
css: '.user-name { color: purple; }'
|
||||
}
|
||||
end
|
||||
|
||||
it 'changes the settings' do
|
||||
SiteSetting.email_custom_css = ".user-name { font-size: 24px; }"
|
||||
put '/admin/customize/email_style.json', params: { email_style: valid_params }
|
||||
expect(response.status).to eq(200)
|
||||
expect(SiteSetting.email_custom_template).to eq(valid_params[:html])
|
||||
expect(SiteSetting.email_custom_css).to eq(valid_params[:css])
|
||||
end
|
||||
|
||||
it 'reports errors' do
|
||||
put '/admin/customize/email_style.json', params: {
|
||||
email_style: valid_params.merge(html: 'No email content')
|
||||
}
|
||||
expect(response.status).to eq(422)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['errors']).to include(
|
||||
I18n.t(
|
||||
'email_style.html_missing_placeholder',
|
||||
placeholder: '%{email_content}'
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
46
spec/services/email_style_updater_spec.rb
Normal file
46
spec/services/email_style_updater_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe EmailStyleUpdater do
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
let(:default_html) { File.read("#{Rails.root}/app/views/email/default_template.html") }
|
||||
let(:updater) { EmailStyleUpdater.new(admin) }
|
||||
|
||||
describe 'update' do
|
||||
it 'can change the settings' do
|
||||
expect(
|
||||
updater.update(
|
||||
html: 'For you: %{email_content}',
|
||||
css: 'h1 { color: blue; }'
|
||||
)
|
||||
).to eq(true)
|
||||
expect(SiteSetting.email_custom_template).to eq('For you: %{email_content}')
|
||||
expect(SiteSetting.email_custom_css).to eq('h1 { color: blue; }')
|
||||
end
|
||||
|
||||
it 'will not store defaults' do
|
||||
updater.update(html: default_html, css: '')
|
||||
expect(SiteSetting.email_custom_template).to_not be_present
|
||||
expect(SiteSetting.email_custom_css).to_not be_present
|
||||
end
|
||||
|
||||
it 'can clear settings if defaults given' do
|
||||
SiteSetting.email_custom_template = 'For you: %{email_content}'
|
||||
SiteSetting.email_custom_css = 'h1 { color: blue; }'
|
||||
updater.update(html: default_html, css: '')
|
||||
expect(SiteSetting.email_custom_template).to_not be_present
|
||||
expect(SiteSetting.email_custom_css).to_not be_present
|
||||
end
|
||||
|
||||
it 'fails if html is missing email_content' do
|
||||
expect(updater.update(html: 'No email content', css: '')).to eq(false)
|
||||
expect(updater.errors).to include(
|
||||
I18n.t(
|
||||
'email_style.html_missing_placeholder',
|
||||
placeholder: '%{email_content}'
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user